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

binary matrices utils in rust #12456

Merged
merged 49 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5b7c6b6
gaussian elimination in rust
ShellyGarion May 23, 2024
10633a2
handle lint errors
ShellyGarion May 26, 2024
0f34296
Merge branch 'main' into linear_rust
ShellyGarion May 30, 2024
352ec0b
replace python function by rust function for gauss elimination
ShellyGarion May 30, 2024
5b17b58
change matrix elements type from bool to i8
ShellyGarion Jun 2, 2024
678ec0a
add parallelization in row operations
ShellyGarion Jun 4, 2024
361b562
update matrices in place
ShellyGarion Jun 5, 2024
19326bd
change matrix type in rust code to bool
ShellyGarion Jun 5, 2024
0cf7502
handle type in python code
ShellyGarion Jun 5, 2024
c8aee3a
update filter following review
ShellyGarion Jun 5, 2024
2f45606
remove parallelization using rayon
ShellyGarion Jun 6, 2024
3bcda6d
move _gauss_elimination_with_perm to rust
ShellyGarion Jun 13, 2024
77ff20b
fix conflicts with main
ShellyGarion Jun 13, 2024
b4f05bf
fix fmt error
ShellyGarion Jun 13, 2024
b79ea04
simplify _gauss_elimination function
ShellyGarion Jun 13, 2024
21f060e
update _compute_rank_after_gauss_elim to rust
ShellyGarion Jun 13, 2024
598f913
update _row_op and _col_op
ShellyGarion Jun 17, 2024
7510b2c
transfer _row_op and _col_op from python to rust
ShellyGarion Jun 18, 2024
44296ae
fix code due to failing tests
ShellyGarion Jun 18, 2024
45fef0f
minor update of types
ShellyGarion Jun 18, 2024
89479a5
move calc_inverse_matrix to rust, add _binary_matmul in rust
ShellyGarion Jun 19, 2024
a333f36
fix failing tests, by changing mat type from int to bool
ShellyGarion Jun 20, 2024
14d09ab
update rust docstrings
ShellyGarion Jun 20, 2024
8c27cae
add function _add_row_or_col to rust code
ShellyGarion Jun 20, 2024
d9b2b4b
improve binary_matmul
ShellyGarion Jun 24, 2024
783eed5
code refactor. fix conflicts with main branch
ShellyGarion Jun 24, 2024
e0d0e08
proper error handling
alexanderivrii Jun 24, 2024
5b1b3ed
unified format of function names
ShellyGarion Jun 24, 2024
a13a717
fix merge conflics
ShellyGarion Jun 24, 2024
a88ddba
move compute_rank from python to rust, update errors
ShellyGarion Jun 24, 2024
2c0601f
update type of mat in compute_rank
ShellyGarion Jun 24, 2024
26a95d2
move random_invertible_binary_matrix and check_invertible_binary_matr…
ShellyGarion Jun 25, 2024
ec2ce02
Updating HighLevelSynthesis tests that depend on the specific random …
alexanderivrii Jun 25, 2024
88e46de
Updating LinearSynthesis tests to pass seeds
alexanderivrii Jun 25, 2024
634b92f
Updating tests in test_linear_function
alexanderivrii Jun 25, 2024
7fdf87b
changing the matrix type in random dyhedral to be a matrix of ints ra…
alexanderivrii Jun 25, 2024
11ac885
updating cx_cz synthesis tests
alexanderivrii Jun 25, 2024
c2d6330
updating clifford tests
alexanderivrii Jun 25, 2024
f67ad33
Merge branch 'main' into linear_rust
alexanderivrii Jun 25, 2024
4b83395
remove unused imports
ShellyGarion Jun 25, 2024
713d593
add option seed=None
ShellyGarion Jun 25, 2024
4406441
enhance rust docs
ShellyGarion Jun 25, 2024
af7ac2a
add release notes
ShellyGarion Jun 25, 2024
cd3fa5d
Merge branch 'main' into linear_rust
ShellyGarion Jun 25, 2024
3424807
remove unnecessary copy in python
ShellyGarion Jun 27, 2024
3327ddc
another copy fix
ShellyGarion Jun 27, 2024
63a114e
another copy fix
ShellyGarion Jun 27, 2024
102cc90
update rust docstrings
ShellyGarion Jun 27, 2024
c1fc559
update release notes
ShellyGarion Jun 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
190 changes: 190 additions & 0 deletions crates/accelerate/src/synthesis/linear/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// 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::QiskitError;
use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2, PyReadwriteArray2};
use pyo3::prelude::*;

mod utils;

#[pyfunction]
#[pyo3(signature = (mat, ncols=None, full_elim=false))]
/// Gauss elimination of a matrix mat with m rows and n columns.
/// If full_elim = True, it allows full elimination of mat[:, 0 : ncols]
/// Modifies the matrix mat in-place, and returns the permutation perm that was done
/// on the rows during the process. perm[0 : rank] represents the indices of linearly
/// independent rows in the original matrix.
/// Args:
/// mat: a boolean matrix with n rows and m columns
/// ncols: the number of columns for the gaussian elimination,
/// if ncols=None, then the elimination is done over all the columns
/// full_elim: whether to do a full elimination, or partial (upper triangular form)
/// Returns:
/// perm: the permutation perm that was done on the rows during the process
fn gauss_elimination_with_perm(
py: Python,
mut mat: PyReadwriteArray2<bool>,
ncols: Option<usize>,
full_elim: Option<bool>,
) -> PyResult<PyObject> {
let matmut = mat.as_array_mut();
let perm = utils::gauss_elimination_with_perm_inner(matmut, ncols, full_elim);
Ok(perm.to_object(py))
}

#[pyfunction]
#[pyo3(signature = (mat, ncols=None, full_elim=false))]
/// Gauss elimination of a matrix mat with m rows and n columns.
/// If full_elim = True, it allows full elimination of mat[:, 0 : ncols]
/// This function modifies the input matrix in-place.
/// Args:
/// mat: a boolean matrix with n rows and m columns
/// ncols: the number of columns for the gaussian elimination,
/// if ncols=None, then the elimination is done over all the columns
/// full_elim: whether to do a full elimination, or partial (upper triangular form)
fn gauss_elimination(
mut mat: PyReadwriteArray2<bool>,
ncols: Option<usize>,
full_elim: Option<bool>,
) {
let matmut = mat.as_array_mut();
let _perm = utils::gauss_elimination_with_perm_inner(matmut, ncols, full_elim);
}

#[pyfunction]
#[pyo3(signature = (mat))]
/// Given a boolean matrix mat after Gaussian elimination, computes its rank
/// (i.e. simply the number of nonzero rows)
/// Args:
/// mat: a boolean matrix after gaussian elimination
/// Returns:
/// rank: the rank of the matrix
fn compute_rank_after_gauss_elim(py: Python, mat: PyReadonlyArray2<bool>) -> PyResult<PyObject> {
let view = mat.as_array();
let rank = utils::compute_rank_after_gauss_elim_inner(view);
Ok(rank.to_object(py))
}

#[pyfunction]
#[pyo3(signature = (mat))]
/// Given a boolean matrix mat computes its rank
/// Args:
/// mat: a boolean matrix
/// Returns:
/// rank: the rank of the matrix
fn compute_rank(py: Python, mat: PyReadonlyArray2<bool>) -> PyResult<PyObject> {
let rank = utils::compute_rank_inner(mat.as_array());
Ok(rank.to_object(py))
}

#[pyfunction]
#[pyo3(signature = (mat, verify=false))]
/// Given a boolean matrix mat, tries to calculate its inverse matrix
/// Args:
/// mat: a boolean square matrix.
/// verify: if True asserts that the multiplication of mat and its inverse is the identity matrix.
/// Returns:
/// the inverse matrix.
/// Raises:
/// QiskitError: if the matrix is not square or not invertible.
pub fn calc_inverse_matrix(
py: Python,
mat: PyReadonlyArray2<bool>,
verify: Option<bool>,
) -> PyResult<Py<PyArray2<bool>>> {
let view = mat.as_array();
let invmat =
utils::calc_inverse_matrix_inner(view, verify.is_some()).map_err(QiskitError::new_err)?;
Ok(invmat.into_pyarray_bound(py).unbind())
}

#[pyfunction]
#[pyo3(signature = (mat1, mat2))]
/// Binary matrix multiplication
/// Args:
/// mat1: a boolean matrix
/// mat2: a boolean matrix
/// Returns:
/// a boolean matrix which is the multiplication of mat1 and mat2
/// Raises:
/// QiskitError: if the dimensions of mat1 and mat2 do not match
pub fn binary_matmul(
py: Python,
mat1: PyReadonlyArray2<bool>,
mat2: PyReadonlyArray2<bool>,
) -> PyResult<Py<PyArray2<bool>>> {
let view1 = mat1.as_array();
let view2 = mat2.as_array();
let result = utils::binary_matmul_inner(view1, view2).map_err(QiskitError::new_err)?;
Ok(result.into_pyarray_bound(py).unbind())
}

#[pyfunction]
#[pyo3(signature = (mat, ctrl, trgt))]
/// Perform ROW operation on a matrix mat
fn row_op(mut mat: PyReadwriteArray2<bool>, ctrl: usize, trgt: usize) {
let matmut = mat.as_array_mut();
utils::_add_row_or_col(matmut, &false, ctrl, trgt)
}

#[pyfunction]
#[pyo3(signature = (mat, ctrl, trgt))]
/// Perform COL operation on a matrix mat (in the inverse direction)
fn col_op(mut mat: PyReadwriteArray2<bool>, ctrl: usize, trgt: usize) {
let matmut = mat.as_array_mut();
utils::_add_row_or_col(matmut, &true, trgt, ctrl)
}

#[pyfunction]
#[pyo3(signature = (num_qubits, seed=None))]
/// Generate a random invertible n x n binary matrix.
/// Args:
/// num_qubits: the matrix size.
/// seed: a random seed.
/// Returns:
/// np.ndarray: A random invertible binary matrix of size num_qubits.
fn random_invertible_binary_matrix(
py: Python,
num_qubits: usize,
seed: Option<u64>,
) -> PyResult<Py<PyArray2<bool>>> {
let matrix = utils::random_invertible_binary_matrix_inner(num_qubits, seed);
Ok(matrix.into_pyarray_bound(py).unbind())
}

#[pyfunction]
#[pyo3(signature = (mat))]
/// Check that a binary matrix is invertible.
/// Args:
/// mat: a binary matrix.
/// Returns:
/// bool: True if mat in invertible and False otherwise.
fn check_invertible_binary_matrix(py: Python, mat: PyReadonlyArray2<bool>) -> PyResult<PyObject> {
let view = mat.as_array();
let out = utils::check_invertible_binary_matrix_inner(view);
Ok(out.to_object(py))
}

#[pymodule]
pub fn linear(m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(gauss_elimination_with_perm))?;
m.add_wrapped(wrap_pyfunction!(gauss_elimination))?;
m.add_wrapped(wrap_pyfunction!(compute_rank_after_gauss_elim))?;
m.add_wrapped(wrap_pyfunction!(compute_rank))?;
m.add_wrapped(wrap_pyfunction!(calc_inverse_matrix))?;
m.add_wrapped(wrap_pyfunction!(row_op))?;
m.add_wrapped(wrap_pyfunction!(col_op))?;
m.add_wrapped(wrap_pyfunction!(binary_matmul))?;
m.add_wrapped(wrap_pyfunction!(random_invertible_binary_matrix))?;
m.add_wrapped(wrap_pyfunction!(check_invertible_binary_matrix))?;
Ok(())
}
200 changes: 200 additions & 0 deletions crates/accelerate/src/synthesis/linear/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// 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 ndarray::{concatenate, s, Array2, ArrayView2, ArrayViewMut2, Axis};
use rand::{Rng, SeedableRng};
use rand_pcg::Pcg64Mcg;

/// Binary matrix multiplication
pub fn binary_matmul_inner(
mat1: ArrayView2<bool>,
mat2: ArrayView2<bool>,
) -> Result<Array2<bool>, String> {
let n1_rows = mat1.nrows();
let n1_cols = mat1.ncols();
let n2_rows = mat2.nrows();
let n2_cols = mat2.ncols();
if n1_cols != n2_rows {
return Err(format!(
"Cannot multiply matrices with inappropriate dimensions {}, {}",
n1_cols, n2_rows
));
}

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)
}))
}

/// Gauss elimination of a matrix mat with m rows and n columns.
/// If full_elim = True, it allows full elimination of mat[:, 0 : ncols]
/// Returns the matrix mat, and the permutation perm that was done on the rows during the process.
/// perm[0 : rank] represents the indices of linearly independent rows in the original matrix.
pub fn gauss_elimination_with_perm_inner(
mut mat: ArrayViewMut2<bool>,
ncols: Option<usize>,
full_elim: Option<bool>,
) -> Vec<usize> {
let (m, mut n) = (mat.nrows(), mat.ncols()); // no. of rows and columns
if let Some(ncols_val) = ncols {
n = usize::min(n, ncols_val); // no. of active columns
}
let mut perm: Vec<usize> = Vec::from_iter(0..m);

let mut r = 0; // current rank
let k = 0; // current pivot column
let mut new_k = 0;
while (r < m) && (k < n) {
let mut is_non_zero = false;
let mut new_r = r;
for j in k..n {
new_k = k;
for i in r..m {
if mat[(i, j)] {
is_non_zero = true;
new_k = j;
new_r = i;
break;
}
}
if is_non_zero {
break;
}
}
if !is_non_zero {
return perm; // A is in the canonical form
}
Cryoris marked this conversation as resolved.
Show resolved Hide resolved

if new_r != r {
let temp_r = mat.slice_mut(s![r, ..]).to_owned();
let temp_new_r = mat.slice_mut(s![new_r, ..]).to_owned();
mat.slice_mut(s![r, ..]).assign(&temp_new_r);
mat.slice_mut(s![new_r, ..]).assign(&temp_r);
perm.swap(r, new_r);
}

// Copy source row to avoid trying multiple borrows at once
let row0 = mat.row(r).to_owned();
mat.axis_iter_mut(Axis(0))
.enumerate()
.filter(|(i, row)| {
(full_elim == Some(true) && (*i < r) && row[new_k])
|| (*i > r && *i < m && row[new_k])
})
.for_each(|(_i, mut row)| {
row.zip_mut_with(&row0, |x, &y| *x ^= y);
});

r += 1;
}
perm
}

/// Given a boolean matrix A after Gaussian elimination, computes its rank
/// (i.e. simply the number of nonzero rows)
pub fn compute_rank_after_gauss_elim_inner(mat: ArrayView2<bool>) -> usize {
let rank: usize = mat
.axis_iter(Axis(0))
.map(|row| row.fold(false, |out, val| out | *val) as usize)
.sum();
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
rank
}

/// Given a boolean matrix mat computes its rank
pub fn compute_rank_inner(mat: ArrayView2<bool>) -> usize {
let mut temp_mat = mat.to_owned();
gauss_elimination_with_perm_inner(temp_mat.view_mut(), None, Some(false));
let rank = compute_rank_after_gauss_elim_inner(temp_mat.view());
rank
}

/// Given a square boolean matrix mat, tries to compute its inverse.
pub fn calc_inverse_matrix_inner(
mat: ArrayView2<bool>,
verify: bool,
) -> Result<Array2<bool>, String> {
if mat.shape()[0] != mat.shape()[1] {
return Err("Matrix to invert is a non-square matrix.".to_string());
}
let n = mat.shape()[0];

// concatenate the matrix and identity
let identity_matrix: Array2<bool> = Array2::from_shape_fn((n, n), |(i, j)| i == j);
let mut mat1 = concatenate(Axis(1), &[mat.view(), identity_matrix.view()]).unwrap();

gauss_elimination_with_perm_inner(mat1.view_mut(), None, Some(true));

let r = compute_rank_after_gauss_elim_inner(mat1.slice(s![.., 0..n]));
if r < n {
return Err("The matrix is not invertible.".to_string());
}

let invmat = mat1.slice(s![.., n..2 * n]).to_owned();

if verify {
let mat2 = binary_matmul_inner(mat, (&invmat).into())?;
let identity_matrix: Array2<bool> = Array2::from_shape_fn((n, n), |(i, j)| i == j);
if mat2.ne(&identity_matrix) {
return Err("The inverse matrix is not correct.".to_string());
}
}
Comment on lines +145 to +151
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand that this was here before, but it seems very strange to essentially have a runtime test of our code here... if we provide the function then we should also be sure it is correct and not need to verify the result 😅 I'm fine keeping it because it existed before, but I would prefer removing this.

Copy link
Member Author

Choose a reason for hiding this comment

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

There is a parameter "verify" in several parts of the synthesis code, which is mainly used for tests, since the algorithms are quite complicated (the default value is False)

Copy link
Contributor

Choose a reason for hiding this comment

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

We can leave it for this PR 👍🏻 but a common subroutine like matrix inversion seems like something stable enough that we don't need to bake in a verification and instead just have some tests in the test suite (fwiw other packages like NumPy/SciPy also don't have any arguments to check that 🙂).


Ok(invmat)
}

/// Mutate a matrix inplace by adding the value of the ``ctrl`` row to the
/// ``target`` row. If ``add_cols`` is true, add columns instead of rows.
pub fn _add_row_or_col(mut mat: ArrayViewMut2<bool>, add_cols: &bool, ctrl: usize, trgt: usize) {
// get the two rows (or columns)
let info = if *add_cols {
(s![.., ctrl], s![.., trgt])
} else {
(s![ctrl, ..], s![trgt, ..])
};
let (row0, mut row1) = mat.multi_slice_mut(info);

// add them inplace
row1.zip_mut_with(&row0, |x, &y| *x ^= y);
}

/// Generate a random invertible n x n binary matrix.
pub fn random_invertible_binary_matrix_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 mut matrix = Array2::from_elem((num_qubits, num_qubits), false);

loop {
for value in matrix.iter_mut() {
*value = rng.gen_bool(0.5);
}

let rank = compute_rank_inner(matrix.view());
if rank == num_qubits {
break;
}
}
matrix
}

/// Check that a binary matrix is invertible.
pub fn check_invertible_binary_matrix_inner(mat: ArrayView2<bool>) -> bool {
if mat.nrows() != mat.ncols() {
return false;
}
let rank = compute_rank_inner(mat);
rank == mat.nrows()
}
Loading
Loading