-
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 the numeric code in the Isometry gate class #12197
Changes from all commits
6dca86e
5c21069
cf3c5ff
8107fb2
63e19e3
b0b7a33
eaf43f0
23f5e2e
cb8fc67
7b61dcc
6a6f3bc
6e86efe
3676469
29ffc99
366a934
4843ef5
d26bd4a
091d229
d540511
604e33a
3bd4358
c04aa17
b8b82a6
fcf587f
2c07b79
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,367 @@ | ||
// 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 std::ops::BitAnd; | ||
|
||
use approx::abs_diff_eq; | ||
use num_complex::{Complex64, ComplexFloat}; | ||
use pyo3::prelude::*; | ||
use pyo3::wrap_pyfunction; | ||
use pyo3::Python; | ||
|
||
use hashbrown::HashSet; | ||
use itertools::Itertools; | ||
use ndarray::prelude::*; | ||
use numpy::{IntoPyArray, PyReadonlyArray1, PyReadonlyArray2}; | ||
|
||
use crate::two_qubit_decompose::ONE_QUBIT_IDENTITY; | ||
|
||
/// Find special unitary matrix that maps [c0,c1] to [r,0] or [0,r] if basis_state=0 or | ||
/// basis_state=1 respectively | ||
#[pyfunction] | ||
pub fn reverse_qubit_state( | ||
py: Python, | ||
state: [Complex64; 2], | ||
basis_state: usize, | ||
epsilon: f64, | ||
) -> PyObject { | ||
reverse_qubit_state_inner(&state, basis_state, epsilon) | ||
.into_pyarray_bound(py) | ||
.into() | ||
} | ||
|
||
#[inline(always)] | ||
fn l2_norm(vec: &[Complex64]) -> f64 { | ||
vec.iter() | ||
.fold(0., |acc, elem| acc + elem.norm_sqr()) | ||
.sqrt() | ||
} | ||
|
||
fn reverse_qubit_state_inner( | ||
state: &[Complex64; 2], | ||
basis_state: usize, | ||
epsilon: f64, | ||
) -> Array2<Complex64> { | ||
let r = l2_norm(state); | ||
let r_inv = 1. / r; | ||
if r < epsilon { | ||
Array2::eye(2) | ||
} else if basis_state == 0 { | ||
array![ | ||
[state[0].conj() * r_inv, state[1].conj() * r_inv], | ||
[-state[1] * r_inv, state[0] * r_inv], | ||
] | ||
} else { | ||
array![ | ||
[-state[1] * r_inv, state[0] * r_inv], | ||
[state[0].conj() * r_inv, state[1].conj() * r_inv], | ||
] | ||
} | ||
} | ||
|
||
/// This method finds the single-qubit gates for a UCGate to disentangle a qubit: | ||
/// we consider the n-qubit state v[:,0] starting with k zeros (in the computational basis). | ||
/// The qubit with label n-s-1 is disentangled into the basis state k_s(k,s). | ||
|
||
#[pyfunction] | ||
pub fn find_squs_for_disentangling( | ||
mtreinish marked this conversation as resolved.
Show resolved
Hide resolved
|
||
py: Python, | ||
v: PyReadonlyArray2<Complex64>, | ||
k: usize, | ||
s: usize, | ||
epsilon: f64, | ||
n: usize, | ||
) -> Vec<PyObject> { | ||
let v = v.as_array(); | ||
let k_prime = 0; | ||
let i_start = if b(k, s + 1) == 0 { | ||
a(k, s + 1) | ||
} else { | ||
a(k, s + 1) + 1 | ||
}; | ||
let mut output: Vec<Array2<Complex64>> = (0..i_start).map(|_| Array2::eye(2)).collect(); | ||
let mut squs: Vec<Array2<Complex64>> = (i_start..2_usize.pow((n - s - 1) as u32)) | ||
.map(|i| { | ||
reverse_qubit_state_inner( | ||
&[ | ||
v[[2 * i * 2_usize.pow(s as u32) + b(k, s), k_prime]], | ||
v[[(2 * i + 1) * 2_usize.pow(s as u32) + b(k, s), k_prime]], | ||
], | ||
k_s(k, s), | ||
epsilon, | ||
) | ||
}) | ||
.collect(); | ||
output.append(&mut squs); | ||
output | ||
.into_iter() | ||
.map(|x| x.into_pyarray_bound(py).into()) | ||
.collect() | ||
} | ||
|
||
#[pyfunction] | ||
pub fn apply_ucg( | ||
py: Python, | ||
m: PyReadonlyArray2<Complex64>, | ||
k: usize, | ||
single_qubit_gates: Vec<PyReadonlyArray2<Complex64>>, | ||
) -> PyObject { | ||
let mut m = m.as_array().to_owned(); | ||
let shape = m.shape(); | ||
let num_qubits = shape[0].ilog2(); | ||
let num_col = shape[1]; | ||
let spacing: usize = 2_usize.pow(num_qubits - k as u32 - 1); | ||
for j in 0..2_usize.pow(num_qubits - 1) { | ||
let i = (j / spacing) * spacing + j; | ||
let gate_index = i / (2_usize.pow(num_qubits - k as u32)); | ||
for col in 0..num_col { | ||
let gate = single_qubit_gates[gate_index].as_array(); | ||
let a = m[[i, col]]; | ||
let b = m[[i + spacing, col]]; | ||
m[[i, col]] = gate[[0, 0]] * a + gate[[0, 1]] * b; | ||
m[[i + spacing, col]] = gate[[1, 0]] * a + gate[[1, 1]] * b; | ||
} | ||
} | ||
m.into_pyarray_bound(py).into() | ||
} | ||
|
||
#[inline(always)] | ||
fn bin_to_int(bin: &[u8]) -> usize { | ||
bin.iter() | ||
.fold(0_usize, |acc, digit| (acc << 1) + *digit as usize) | ||
} | ||
|
||
#[pyfunction] | ||
pub fn apply_diagonal_gate( | ||
py: Python, | ||
m: PyReadonlyArray2<Complex64>, | ||
action_qubit_labels: Vec<usize>, | ||
diag: PyReadonlyArray1<Complex64>, | ||
) -> PyResult<PyObject> { | ||
let diag = diag.as_slice()?; | ||
let mut m = m.as_array().to_owned(); | ||
let shape = m.shape(); | ||
let num_qubits = shape[0].ilog2(); | ||
let num_col = shape[1]; | ||
for state in std::iter::repeat([0_u8, 1_u8]) | ||
.take(num_qubits as usize) | ||
.multi_cartesian_product() | ||
{ | ||
let diag_index = action_qubit_labels | ||
.iter() | ||
.fold(0_usize, |acc, i| (acc << 1) + state[*i] as usize); | ||
let i = bin_to_int(&state); | ||
Comment on lines
+155
to
+162
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. Completely pre-existing, but holy moly. This is a wild way of spelling No need to change it here unless you felt like it - the whole implementation needs a bit more TLC than that. |
||
for j in 0..num_col { | ||
m[[i, j]] = diag[diag_index] * m[[i, j]] | ||
} | ||
} | ||
Ok(m.into_pyarray_bound(py).into()) | ||
} | ||
|
||
#[pyfunction] | ||
pub fn apply_diagonal_gate_to_diag( | ||
mut m_diagonal: Vec<Complex64>, | ||
action_qubit_labels: Vec<usize>, | ||
diag: PyReadonlyArray1<Complex64>, | ||
num_qubits: usize, | ||
) -> PyResult<Vec<Complex64>> { | ||
let diag = diag.as_slice()?; | ||
if m_diagonal.is_empty() { | ||
return Ok(m_diagonal); | ||
} | ||
for state in std::iter::repeat([0_u8, 1_u8]) | ||
.take(num_qubits) | ||
.multi_cartesian_product() | ||
.take(m_diagonal.len()) | ||
{ | ||
let diag_index = action_qubit_labels | ||
.iter() | ||
.fold(0_usize, |acc, i| (acc << 1) + state[*i] as usize); | ||
let i = bin_to_int(&state); | ||
m_diagonal[i] *= diag[diag_index] | ||
} | ||
Ok(m_diagonal) | ||
} | ||
|
||
/// Helper method for _apply_multi_controlled_gate. This constructs the basis states the MG gate | ||
/// is acting on for a specific state state_free of the qubits we neither control nor act on | ||
fn construct_basis_states( | ||
state_free: &[u8], | ||
control_set: &HashSet<usize>, | ||
target_label: usize, | ||
) -> [usize; 2] { | ||
let size = state_free.len() + control_set.len() + 1; | ||
let mut e1: usize = 0; | ||
let mut e2: usize = 0; | ||
let mut j = 0; | ||
for i in 0..size { | ||
e1 <<= 1; | ||
e2 <<= 1; | ||
if control_set.contains(&i) { | ||
e1 += 1; | ||
e2 += 1; | ||
} else if i == target_label { | ||
e2 += 1; | ||
} else { | ||
assert!(j <= 1); | ||
e1 += state_free[j] as usize; | ||
e2 += state_free[j] as usize; | ||
j += 1 | ||
} | ||
} | ||
[e1, e2] | ||
} | ||
|
||
#[pyfunction] | ||
pub fn apply_multi_controlled_gate( | ||
py: Python, | ||
m: PyReadonlyArray2<Complex64>, | ||
control_labels: Vec<usize>, | ||
target_label: usize, | ||
gate: PyReadonlyArray2<Complex64>, | ||
) -> PyObject { | ||
let mut m = m.as_array().to_owned(); | ||
let gate = gate.as_array(); | ||
let shape = m.shape(); | ||
let num_qubits = shape[0].ilog2(); | ||
let num_col = shape[1]; | ||
let free_qubits = num_qubits as usize - control_labels.len() - 1; | ||
let control_set: HashSet<usize> = control_labels.into_iter().collect(); | ||
if free_qubits == 0 { | ||
let [e1, e2] = construct_basis_states(&[], &control_set, target_label); | ||
for i in 0..num_col { | ||
let temp: Vec<_> = gate | ||
.dot(&aview2(&[[m[[e1, i]]], [m[[e2, i]]]])) | ||
.into_iter() | ||
.take(2) | ||
.collect(); | ||
m[[e1, i]] = temp[0]; | ||
m[[e2, i]] = temp[1]; | ||
} | ||
return m.into_pyarray_bound(py).into(); | ||
} | ||
for state_free in std::iter::repeat([0_u8, 1_u8]) | ||
.take(free_qubits) | ||
.multi_cartesian_product() | ||
{ | ||
let [e1, e2] = construct_basis_states(&state_free, &control_set, target_label); | ||
for i in 0..num_col { | ||
let temp: Vec<_> = gate | ||
.dot(&aview2(&[[m[[e1, i]]], [m[[e2, i]]]])) | ||
.into_iter() | ||
.take(2) | ||
.collect(); | ||
m[[e1, i]] = temp[0]; | ||
m[[e2, i]] = temp[1]; | ||
} | ||
} | ||
m.into_pyarray_bound(py).into() | ||
} | ||
|
||
#[pyfunction] | ||
pub fn ucg_is_identity_up_to_global_phase( | ||
single_qubit_gates: Vec<PyReadonlyArray2<Complex64>>, | ||
epsilon: f64, | ||
) -> bool { | ||
let global_phase: Complex64 = if single_qubit_gates[0].as_array()[[0, 0]].abs() >= epsilon { | ||
single_qubit_gates[0].as_array()[[0, 0]].finv() | ||
} else { | ||
return false; | ||
}; | ||
for raw_gate in single_qubit_gates { | ||
let gate = raw_gate.as_array(); | ||
if !abs_diff_eq!( | ||
gate.mapv(|x| x * global_phase), | ||
aview2(&ONE_QUBIT_IDENTITY), | ||
epsilon = 1e-8 // Default tolerance from numpy for allclose() | ||
) { | ||
return false; | ||
} | ||
} | ||
true | ||
} | ||
Comment on lines
+270
to
+291
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 is pre-existing, but 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. Maybe |
||
|
||
#[pyfunction] | ||
fn diag_is_identity_up_to_global_phase(diag: Vec<Complex64>, epsilon: f64) -> bool { | ||
let global_phase: Complex64 = if diag[0].abs() >= epsilon { | ||
diag[0].finv() | ||
} else { | ||
return false; | ||
}; | ||
for d in diag { | ||
if (global_phase * d - 1.0).abs() >= epsilon { | ||
return false; | ||
} | ||
} | ||
true | ||
} | ||
|
||
#[pyfunction] | ||
pub fn merge_ucgate_and_diag( | ||
py: Python, | ||
single_qubit_gates: Vec<PyReadonlyArray2<Complex64>>, | ||
diag: Vec<Complex64>, | ||
) -> Vec<PyObject> { | ||
single_qubit_gates | ||
.iter() | ||
.enumerate() | ||
.map(|(i, raw_gate)| { | ||
let gate = raw_gate.as_array(); | ||
let res = aview2(&[ | ||
[diag[2 * i], Complex64::new(0., 0.)], | ||
[Complex64::new(0., 0.), diag[2 * i + 1]], | ||
]) | ||
.dot(&gate); | ||
res.into_pyarray_bound(py).into() | ||
}) | ||
.collect() | ||
} | ||
|
||
#[inline(always)] | ||
#[pyfunction] | ||
fn k_s(k: usize, s: usize) -> usize { | ||
if k == 0 { | ||
0 | ||
} else { | ||
let filter = 1 << s; | ||
k.bitand(filter) >> s | ||
} | ||
} | ||
|
||
#[inline(always)] | ||
#[pyfunction] | ||
fn a(k: usize, s: usize) -> usize { | ||
k / 2_usize.pow(s as u32) | ||
} | ||
|
||
#[inline(always)] | ||
#[pyfunction] | ||
fn b(k: usize, s: usize) -> usize { | ||
k - (a(k, s) * 2_usize.pow(s as u32)) | ||
} | ||
|
||
#[pymodule] | ||
pub fn isometry(m: &Bound<PyModule>) -> PyResult<()> { | ||
m.add_wrapped(wrap_pyfunction!(diag_is_identity_up_to_global_phase))?; | ||
m.add_wrapped(wrap_pyfunction!(find_squs_for_disentangling))?; | ||
m.add_wrapped(wrap_pyfunction!(reverse_qubit_state))?; | ||
m.add_wrapped(wrap_pyfunction!(apply_ucg))?; | ||
m.add_wrapped(wrap_pyfunction!(apply_diagonal_gate))?; | ||
m.add_wrapped(wrap_pyfunction!(apply_diagonal_gate_to_diag))?; | ||
m.add_wrapped(wrap_pyfunction!(apply_multi_controlled_gate))?; | ||
m.add_wrapped(wrap_pyfunction!(ucg_is_identity_up_to_global_phase))?; | ||
m.add_wrapped(wrap_pyfunction!(merge_ucgate_and_diag))?; | ||
m.add_wrapped(wrap_pyfunction!(a))?; | ||
m.add_wrapped(wrap_pyfunction!(b))?; | ||
m.add_wrapped(wrap_pyfunction!(k_s))?; | ||
Ok(()) | ||
} |
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.
I'd prefer that this were somehow called the
2-norm
orp-norm
forp=2
("somehow" with a valid identifier).l2
is a space, usually a sequence space. But upon googling for uses of p norm, l2 norm, etc. it seems that, as far as the internet is concerned, especially machine-learning internet, lp, Lp, can mean whatever you want them to mean. [EDIT. looks like this is translated fromnp.linalg.norm
, which, along with Julia'sLinearAlgebra
, calls this the 2-norm)]We might want a function for p norms with p as a parameter, with 2 as the default, and make sure constants are propagated, or whatever it takes to get the correct norm at compile time. But, unless we have an existing use for
p != 2
that can be postponed indefinitely.In any case, at some point, we will wish that functions like this had been collected in a repo-wide module. (probably not in the ten thousandth "utils.xxx"). And in that case it's worth having somewhat more general trait bounds. Not repeating this function could be important for correctness and performance. For example, the Julia 2-norm function appears to sometimes scale the elements to avoid overflow. However, the function
mynorm(v) = sqrt(sum(x -> x^2, v));
is more than 3 times faster than top-level entry pointnorm
. We'd want an api and implementation that reflects our common uses, which might be almost all 2- and 4-element vectors. Indeed, an optimization, which we don't need to make at the moment, would take advantage the length of the vector, which is known here at compile time to be two.The main point for now is to consider at least collecting these somewhere where they are visible so that the questions of performance and correctness can be more easily addressed at some point
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.
I don't want to get into a bikeshedding conversation about naming, but
l2_norm
orlp_norm
is a pretty standard terminology for this. If you look at the comment in the Julia code below what you linked they use it there too:https://github.com/JuliaLang/julia/blob/68da780b93518204d874410307791702d5200e29/stdlib/LinearAlgebra/src/generic.jl#L496
# Compute L_p norm ‖x‖ₚ = sum(abs(x).^p)^(1/p)
That being said the response to the main comment here is that we can look at creating a dedicated mathematical functions module, or even a separate crate if it's generic enough for these kind of things in a follow up PR. Ideally we'd contribute this to something like ndarray or faer imo though assuming a generic implementation. I only did it like this because it was so small and simple and the usage was very minimal. So far the only function I think that falls into this category is the 2x2 determinant function which is used in this PR, the two qubit decomposer, and the one qubit decomposer (where it was originally added). But again it's just a single line so I didn't feel like it was worth creating a dedicate module for it.