Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Oxidize random clifford #12695

Merged
merged 57 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
51a7e3b
starting to experiment
alexanderivrii Jun 12, 2024
62f4f70
porting code
alexanderivrii Jun 13, 2024
8b2d5d4
messy code porting
alexanderivrii Jun 17, 2024
6456f1a
printing statements to enable debugging
alexanderivrii Jun 17, 2024
ed97fb7
merge conflicts
alexanderivrii Jun 17, 2024
e7d86f9
fixes
alexanderivrii Jun 17, 2024
3a26b75
fixing phase
alexanderivrii Jun 18, 2024
07a9382
removing some of the printing statements
alexanderivrii Jun 18, 2024
52c26c2
fixing inaccuracy for cost computation
alexanderivrii Jun 18, 2024
7122fed
Moving some of the functionality to SymplecticMatrix class
alexanderivrii Jun 19, 2024
f21bf7e
reducing the number of warnings
alexanderivrii Jun 19, 2024
2715497
formatting
alexanderivrii Jun 19, 2024
419e9bc
replacing expensive adjoint and compose operations for symplectic mat…
alexanderivrii Jun 19, 2024
3879cad
Merge branch 'main' into oxidize-cliffords
alexanderivrii Jun 19, 2024
bd0d1f1
resolving merge conflicts
alexanderivrii Jun 19, 2024
fdd3e9d
cleanup
alexanderivrii Jun 19, 2024
57c6a4f
code cleanup
alexanderivrii Jun 19, 2024
c0f5d5d
cleanup
alexanderivrii Jun 19, 2024
e3072a0
cleanup
alexanderivrii Jun 19, 2024
6d3f497
cleanup
alexanderivrii Jun 19, 2024
8394de5
cleanup
alexanderivrii Jun 19, 2024
1901df1
cleanup
alexanderivrii Jun 19, 2024
073a68c
cleanup
alexanderivrii Jun 19, 2024
ed9c743
cleanup
alexanderivrii Jun 19, 2024
e42f44b
using fast lookup
alexanderivrii Jun 19, 2024
0aaba7f
cleanup
alexanderivrii Jun 19, 2024
3880a39
clippy
alexanderivrii Jun 19, 2024
3a6ef0b
including params in gate_seq to avoid mapping
alexanderivrii Jun 19, 2024
b324f22
removing unnecessary inner function
alexanderivrii Jun 19, 2024
1cccf27
cleanup
alexanderivrii Jun 20, 2024
922ecbf
renaming
alexanderivrii Jun 20, 2024
82af9be
changes on the python side
alexanderivrii Jun 20, 2024
ff2ba6c
reno
alexanderivrii Jun 20, 2024
bd3a27d
adding error handling
alexanderivrii Jun 20, 2024
4dd6c85
improved error handling
alexanderivrii Jun 20, 2024
646216d
removing redundant Ok(Some(...))
alexanderivrii Jun 24, 2024
1d91369
using random_clifford in tests
alexanderivrii Jun 24, 2024
7101a24
Merge branch 'main' into oxidize-cliffords
alexanderivrii Jun 29, 2024
8f52b1c
reorganizing clifford code
alexanderivrii Jun 29, 2024
8029a06
fixes
alexanderivrii Jun 29, 2024
67db9d3
formatting
alexanderivrii Jun 29, 2024
eb454ad
improved error handling
alexanderivrii Jun 29, 2024
0d89a35
do not panic
alexanderivrii Jun 29, 2024
bbc6ceb
formatting
alexanderivrii Jun 29, 2024
03390f8
initial implementation
alexanderivrii Jun 30, 2024
136108a
cleanup
alexanderivrii Jun 30, 2024
0a1eb8f
changing return to be the full tableau
alexanderivrii Jun 30, 2024
eaa659e
fixing merge conflicts
alexanderivrii Jul 3, 2024
86f96b9
Merge branch 'main' into oxidize-random-clifford
alexanderivrii Aug 22, 2024
38bed6b
parallelized computation for binary matrix multiplication for matrice…
alexanderivrii Aug 22, 2024
21f4229
updating python code and adding release notes
alexanderivrii Aug 22, 2024
148f9ba
lint, test, reno
alexanderivrii Aug 22, 2024
a020dc1
removing obsolete python code
alexanderivrii Aug 26, 2024
0a310d7
removing todo (after experimenting with alternative implementation)
alexanderivrii Aug 26, 2024
29acd8e
removing another obsolete todo
alexanderivrii Aug 26, 2024
c67ecaa
unused import
alexanderivrii Aug 26, 2024
7f443e0
adding comment
alexanderivrii Aug 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion crates/accelerate/src/synthesis/clifford/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@

mod bm_synthesis;
mod greedy_synthesis;
mod random_clifford;
mod utils;

use crate::synthesis::clifford::bm_synthesis::synth_clifford_bm_inner;
use crate::synthesis::clifford::greedy_synthesis::GreedyCliffordSynthesis;
use crate::QiskitError;
use numpy::PyReadonlyArray2;
use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2};
use pyo3::prelude::*;
use qiskit_circuit::circuit_data::CircuitData;
use qiskit_circuit::operations::Param;
Expand All @@ -43,6 +44,28 @@ fn synth_clifford_greedy(py: Python, clifford: PyReadonlyArray2<bool>) -> PyResu
CircuitData::from_standard_gates(py, num_qubits as u32, clifford_gates, Param::Float(0.0))
}

/// Generate a random Clifford tableau.
///
/// The Clifford is sampled using the method of the paper "Hadamard-free circuits
/// expose the structure of the Clifford group" by S. Bravyi and D. Maslov (2020),
/// `https://arxiv.org/abs/2003.09412`__.
///
/// Args:
/// num_qubits: the number of qubits.
/// seed: an optional random seed.
/// Returns:
/// result: a random clifford tableau.
#[pyfunction]
#[pyo3(signature = (num_qubits, seed=None))]
fn random_clifford_tableau(
py: Python,
num_qubits: usize,
seed: Option<u64>,
) -> PyResult<Py<PyArray2<bool>>> {
let tableau = random_clifford::random_clifford_tableau_inner(num_qubits, seed);
Ok(tableau.into_pyarray_bound(py).unbind())
}

/// Create a circuit that optimally synthesizes a given Clifford operator represented as
/// a tableau for Cliffords up to 3 qubits.
///
Expand All @@ -60,5 +83,6 @@ fn synth_clifford_bm(py: Python, clifford: PyReadonlyArray2<bool>) -> PyResult<C
pub fn clifford(m: &Bound<PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(synth_clifford_greedy, m)?)?;
m.add_function(wrap_pyfunction!(synth_clifford_bm, m)?)?;
m.add_function(wrap_pyfunction!(random_clifford_tableau, m)?)?;
Ok(())
}
162 changes: 162 additions & 0 deletions crates/accelerate/src/synthesis/clifford/random_clifford.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2024
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use crate::synthesis::linear::utils::{
binary_matmul_inner, calc_inverse_matrix_inner, replace_row_inner, swap_rows_inner,
};
use ndarray::{concatenate, s, Array1, Array2, ArrayView2, ArrayViewMut2, Axis};
use rand::{Rng, SeedableRng};
use rand_pcg::Pcg64Mcg;

/// Sample from the quantum Mallows distribution.
fn sample_qmallows(n: usize, rng: &mut Pcg64Mcg) -> (Array1<bool>, Array1<usize>) {
// Hadamard layer
let mut had = Array1::from_elem(n, false);

// Permutation layer
let mut perm = Array1::from_elem(n, 0);
let mut inds: Vec<usize> = (0..n).collect();

for i in 0..n {
let m = n - i;
let eps: f64 = 4f64.powi(-(m as i32));
let r: f64 = rng.gen();
let index: usize = -((r + (1f64 - r) * eps).log2().ceil() as isize) as usize;
had[i] = index < m;
let k = if index < m { index } else { 2 * m - index - 1 };
perm[i] = inds[k];
inds.remove(k);
Comment on lines +36 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this ever run into the problem that it's trying to access an entry that has been removed? Maybe it's obvious that it cannot but I don't quite see it directly 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this was not clear to me either, but this is both a direct port of the now removed python function, and the pseudocode in the referenced paper.

}
(had, perm)
}

/// Add symmetric random boolean value to off diagonal entries.
fn fill_tril(mut mat: ArrayViewMut2<bool>, rng: &mut Pcg64Mcg, symmetric: bool) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps this function should be in linear utils? it's useful to generate a random symmetric boolean matrix, which can represent a CZ circuit. In this case, it should be called something like "random_boolean matrix".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should expose the two very special "random linear" functions required for this PR: one that generates random symmetric binary matrices with 0s on the diagonal, and another that generates upper-triangular random symmetric binary matrices with 1s on the diagonal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, but I still think that the name "fill_tril" could be changed to something more meaningful (it also doesn't appear in the paper)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But... this is the name from our Python code :). Ok, will try to think of better names for this and other functions.

let n = mat.shape()[0];
for i in 0..n {
for j in 0..i {
mat[[i, j]] = rng.gen();
if symmetric {
mat[[j, i]] = mat[[i, j]];
}
}
}
}

/// Invert a lower-triangular matrix with unit diagonal.
fn inverse_tril(mat: ArrayView2<bool>) -> Array2<bool> {
calc_inverse_matrix_inner(mat, false).unwrap()
}

/// Generate a random Clifford tableau.
///
/// The Clifford is sampled using the method of the paper "Hadamard-free circuits
/// expose the structure of the Clifford group" by S. Bravyi and D. Maslov (2020),
/// `https://arxiv.org/abs/2003.09412`__.
///
/// The function returns a random clifford tableau.
pub fn random_clifford_tableau_inner(num_qubits: usize, seed: Option<u64>) -> Array2<bool> {
let mut rng = match seed {
Some(seed) => Pcg64Mcg::seed_from_u64(seed),
None => Pcg64Mcg::from_entropy(),
};

let (had, perm) = sample_qmallows(num_qubits, &mut rng);

let mut gamma1: Array2<bool> = Array2::from_elem((num_qubits, num_qubits), false);
for i in 0..num_qubits {
gamma1[[i, i]] = rng.gen();
}
fill_tril(gamma1.view_mut(), &mut rng, true);

let mut gamma2: Array2<bool> = Array2::from_elem((num_qubits, num_qubits), false);
for i in 0..num_qubits {
gamma2[[i, i]] = rng.gen();
}
fill_tril(gamma2.view_mut(), &mut rng, true);
Comment on lines +81 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another idea for improvement would be to have fill_tril do the random diagonal, and avoid constructing the matrix just to rewrite almost all entries again 🙂 E.g.

fn get_tril(rng: ... , symmetric: bool, random_diag: bool) -> Array2<bool>

where random_diag=false sets the diagonal to true (for the delta matrices) and random_diag=true creates the random diagonal for the gamma matrices

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically speaking only half of the entries (when symmetric is False). :) I will use the same excuse that I tried to keep the code as close as possible to the original. In any case I don't think this makes much runtime difference (compared to the matrix multiplication between table1 and table).


let mut delta1: Array2<bool> = Array2::from_shape_fn((num_qubits, num_qubits), |(i, j)| i == j);
fill_tril(delta1.view_mut(), &mut rng, false);

let mut delta2: Array2<bool> = Array2::from_shape_fn((num_qubits, num_qubits), |(i, j)| i == j);
fill_tril(delta2.view_mut(), &mut rng, false);

// Compute stabilizer table
let zero = Array2::from_elem((num_qubits, num_qubits), false);
let prod1 = binary_matmul_inner(gamma1.view(), delta1.view()).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're looking for more improvements, we could have a custom multiplication that takes into account that the upper triangular part of delta is always false (or by adding a flag to the binary_matmul_inner) 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, indeed we probably could exploit this somehow. If you think it's worth it, we can think about this and treat this in a potential follow-up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was just a suggestion since you mentioned that the matrix multiplications are slow, but I'm fine not adding this at this point, as I'm not sure random_clifford is a bottleneck -- so up to you 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the suggestion, but would rather not go down this rabbit hole for this PR. As they say, "premature optimization is the root of all evil" 🙂.

let prod2 = binary_matmul_inner(gamma2.view(), delta2.view()).unwrap();
let inv1 = inverse_tril(delta1.view()).t().to_owned();
let inv2 = inverse_tril(delta2.view()).t().to_owned();

let table1 = concatenate(
Axis(0),
&[
concatenate(Axis(1), &[delta1.view(), zero.view()])
.unwrap()
.view(),
concatenate(Axis(1), &[prod1.view(), inv1.view()])
.unwrap()
.view(),
],
)
.unwrap();

let table2 = concatenate(
Axis(0),
&[
concatenate(Axis(1), &[delta2.view(), zero.view()])
.unwrap()
.view(),
concatenate(Axis(1), &[prod2.view(), inv2.view()])
.unwrap()
.view(),
],
)
.unwrap();

// Compute the full stabilizer tableau

// The code below is identical to the Python implementation, but is based on the original
// code in the paper.

let mut table = Array2::from_elem((2 * num_qubits, 2 * num_qubits), false);

// Apply qubit permutation
for i in 0..num_qubits {
replace_row_inner(table.view_mut(), i, table2.slice(s![i, ..]));
replace_row_inner(
table.view_mut(),
perm[i] + num_qubits,
table2.slice(s![perm[i] + num_qubits, ..]),
);
}

// Apply layer of Hadamards
for i in 0..num_qubits {
if had[i] {
swap_rows_inner(table.view_mut(), i, i + num_qubits);
}
}

// Apply table
let random_symplectic_mat = binary_matmul_inner(table1.view(), table.view()).unwrap();

// Generate random phases
let random_phases: Array2<bool> = Array2::from_shape_fn((2 * num_qubits, 1), |_| rng.gen());

let random_tableau: Array2<bool> = concatenate(
Axis(1),
&[random_symplectic_mat.view(), random_phases.view()],
)
.unwrap();
random_tableau
}
49 changes: 43 additions & 6 deletions crates/accelerate/src/synthesis/linear/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use ndarray::{concatenate, s, Array2, ArrayView2, ArrayViewMut2, Axis};
use ndarray::{azip, concatenate, s, Array2, ArrayView1, ArrayView2, ArrayViewMut2, Axis, Zip};
use rand::{Rng, SeedableRng};
use rand_pcg::Pcg64Mcg;
use rayon::iter::{IndexedParallelIterator, ParallelIterator};
use rayon::prelude::IntoParallelIterator;

use crate::getenv_use_multiple_threads;

const PARALLEL_THRESHOLD: usize = 10;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be helpful to add a comment on this number? I assume it's a heuristic from when the parallel execution is actually faster? 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code for parallel execution mimics the code in pauli_exp_val.rs, though there the threshold is chosen to be 19. Indeed, I have chosen 10 based on some local experiments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment explaining this, so we know it in the future?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, added an explicit comment in 7f443e0


/// Binary matrix multiplication
pub fn binary_matmul_inner(
Expand All @@ -30,11 +36,29 @@ pub fn binary_matmul_inner(
));
}

Ok(Array2::from_shape_fn((n1_rows, n2_cols), |(i, j)| {
(0..n2_rows)
.map(|k| mat1[[i, k]] & mat2[[k, j]])
.fold(false, |acc, v| acc ^ v)
}))
let run_in_parallel = getenv_use_multiple_threads();
Cryoris marked this conversation as resolved.
Show resolved Hide resolved

if n1_rows < PARALLEL_THRESHOLD || !run_in_parallel {
Ok(Array2::from_shape_fn((n1_rows, n2_cols), |(i, j)| {
(0..n2_rows)
.map(|k| mat1[[i, k]] & mat2[[k, j]])
.fold(false, |acc, v| acc ^ v)
}))
} else {
let mut result = Array2::from_elem((n1_rows, n2_cols), false);
result
.axis_iter_mut(Axis(0))
.into_par_iter()
.enumerate()
.for_each(|(i, mut row)| {
for j in 0..n2_cols {
row[j] = (0..n2_rows)
.map(|k| mat1[[i, k]] & mat2[[k, j]])
.fold(false, |acc, v| acc ^ v)
}
});
Ok(result)
}
}

/// Gauss elimination of a matrix mat with m rows and n columns.
Expand Down Expand Up @@ -198,3 +222,16 @@ pub fn check_invertible_binary_matrix_inner(mat: ArrayView2<bool>) -> bool {
let rank = compute_rank_inner(mat);
rank == mat.nrows()
}

/// Mutate matrix ``mat`` in-place by swapping the contents of rows ``i`` and ``j``.
pub fn swap_rows_inner(mut mat: ArrayViewMut2<bool>, i: usize, j: usize) {
let (mut x, mut y) = mat.multi_slice_mut((s![i, ..], s![j, ..]));
azip!((x in &mut x, y in &mut y) (*x, *y) = (*y, *x));
}

/// Mutate matrix ``mat`` in-place by replacing the contents of row ``i`` by ``row``.
pub fn replace_row_inner(mut mat: ArrayViewMut2<bool>, i: usize, row: ArrayView1<bool>) {
let mut x = mat.slice_mut(s![i, ..]);
let y = row.slice(s![..]);
Zip::from(&mut x).and(&y).for_each(|x, &y| *x = y);
}
Loading
Loading