-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Oxidize random clifford #12695
Changes from 56 commits
51a7e3b
62f4f70
8b2d5d4
6456f1a
ed97fb7
e7d86f9
3a26b75
07a9382
52c26c2
7122fed
f21bf7e
2715497
419e9bc
3879cad
bd0d1f1
fdd3e9d
57c6a4f
c0f5d5d
e3072a0
6d3f497
8394de5
1901df1
073a68c
ed9c743
e42f44b
0aaba7f
3880a39
3a6ef0b
b324f22
1cccf27
922ecbf
82af9be
ff2ba6c
bd3a27d
4dd6c85
646216d
1d91369
7101a24
8f52b1c
8029a06
67db9d3
eb454ad
0d89a35
bbc6ceb
03390f8
136108a
0a1eb8f
eaa659e
86f96b9
38bed6b
21f4229
148f9ba
a020dc1
0a310d7
29acd8e
c67ecaa
7f443e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
} | ||
(had, perm) | ||
} | ||
|
||
/// Add symmetric random boolean value to off diagonal entries. | ||
fn fill_tril(mut mat: ArrayViewMut2<bool>, rng: &mut Pcg64Mcg, symmetric: bool) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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". There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another idea for improvement would be to have
where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically speaking only half of the entries (when |
||
|
||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? 🙂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code for parallel execution mimics the code in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
|
@@ -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. | ||
|
@@ -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); | ||
} |
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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.