-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Use average gate fidelity in the commutation checker #13874
Changes from all commits
125c9e8
ba5d37d
92ebe54
501c808
7d9fdb3
2edef3d
2697f9f
cb03668
0401aea
38e04b3
c1efb19
3116483
c2d77a2
005ec93
c6180da
afc4ed4
adbd9be
cda06f8
2c48ac4
8f49c1c
6bc5656
34e9fcc
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 |
---|---|---|
|
@@ -14,6 +14,7 @@ use hashbrown::{HashMap, HashSet}; | |
use ndarray::linalg::kron; | ||
use ndarray::Array2; | ||
use num_complex::Complex64; | ||
use num_complex::ComplexFloat; | ||
use once_cell::sync::Lazy; | ||
use smallvec::SmallVec; | ||
|
||
|
@@ -34,6 +35,7 @@ use qiskit_circuit::operations::{ | |
}; | ||
use qiskit_circuit::{BitType, Clbit, Qubit}; | ||
|
||
use crate::gate_metrics; | ||
use crate::unitary_compose; | ||
use crate::QiskitError; | ||
|
||
|
@@ -54,48 +56,30 @@ static SUPPORTED_OP: Lazy<HashSet<&str>> = Lazy::new(|| { | |
// and their pi-periodicity. Here we mean a gate is n-pi periodic, if for angles that are | ||
// multiples of n*pi, the gate is equal to the identity up to a global phase. | ||
// E.g. RX is generated by X and 2-pi periodic, while CRX is generated by CX and 4-pi periodic. | ||
static SUPPORTED_ROTATIONS: Lazy<HashMap<&str, (u8, Option<OperationRef>)>> = Lazy::new(|| { | ||
static SUPPORTED_ROTATIONS: Lazy<HashMap<&str, Option<OperationRef>>> = Lazy::new(|| { | ||
HashMap::from([ | ||
( | ||
"rx", | ||
(2, Some(OperationRef::StandardGate(StandardGate::XGate))), | ||
), | ||
( | ||
"ry", | ||
(2, Some(OperationRef::StandardGate(StandardGate::YGate))), | ||
), | ||
( | ||
"rz", | ||
(2, Some(OperationRef::StandardGate(StandardGate::ZGate))), | ||
), | ||
( | ||
"p", | ||
(2, Some(OperationRef::StandardGate(StandardGate::ZGate))), | ||
), | ||
( | ||
"u1", | ||
(2, Some(OperationRef::StandardGate(StandardGate::ZGate))), | ||
), | ||
("rxx", (2, None)), // None means the gate is in the commutation dictionary | ||
("ryy", (2, None)), | ||
("rzx", (2, None)), | ||
("rzz", (2, None)), | ||
("rx", Some(OperationRef::StandardGate(StandardGate::XGate))), | ||
("ry", Some(OperationRef::StandardGate(StandardGate::YGate))), | ||
("rz", Some(OperationRef::StandardGate(StandardGate::ZGate))), | ||
("p", Some(OperationRef::StandardGate(StandardGate::ZGate))), | ||
("u1", Some(OperationRef::StandardGate(StandardGate::ZGate))), | ||
("rxx", None), // None means the gate is in the commutation dictionary | ||
("ryy", None), | ||
("rzx", None), | ||
("rzz", None), | ||
( | ||
"crx", | ||
(4, Some(OperationRef::StandardGate(StandardGate::CXGate))), | ||
Some(OperationRef::StandardGate(StandardGate::CXGate)), | ||
), | ||
( | ||
"cry", | ||
(4, Some(OperationRef::StandardGate(StandardGate::CYGate))), | ||
Some(OperationRef::StandardGate(StandardGate::CYGate)), | ||
), | ||
( | ||
"crz", | ||
(4, Some(OperationRef::StandardGate(StandardGate::CZGate))), | ||
), | ||
( | ||
"cp", | ||
(2, Some(OperationRef::StandardGate(StandardGate::CZGate))), | ||
Some(OperationRef::StandardGate(StandardGate::CZGate)), | ||
), | ||
("cp", Some(OperationRef::StandardGate(StandardGate::CZGate))), | ||
]) | ||
}); | ||
|
||
|
@@ -155,13 +139,14 @@ impl CommutationChecker { | |
} | ||
} | ||
|
||
#[pyo3(signature=(op1, op2, max_num_qubits=3))] | ||
#[pyo3(signature=(op1, op2, max_num_qubits=3, approximation_degree=1.))] | ||
fn commute_nodes( | ||
&mut self, | ||
py: Python, | ||
op1: &DAGOpNode, | ||
op2: &DAGOpNode, | ||
max_num_qubits: u32, | ||
approximation_degree: f64, | ||
) -> PyResult<bool> { | ||
let (qargs1, qargs2) = get_bits::<Qubit>( | ||
py, | ||
|
@@ -185,10 +170,11 @@ impl CommutationChecker { | |
&qargs2, | ||
&cargs2, | ||
max_num_qubits, | ||
approximation_degree, | ||
) | ||
} | ||
|
||
#[pyo3(signature=(op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits=3))] | ||
#[pyo3(signature=(op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits=3, approximation_degree=1.))] | ||
#[allow(clippy::too_many_arguments)] | ||
fn commute( | ||
&mut self, | ||
|
@@ -200,6 +186,7 @@ impl CommutationChecker { | |
qargs2: Option<&Bound<PySequence>>, | ||
cargs2: Option<&Bound<PySequence>>, | ||
max_num_qubits: u32, | ||
approximation_degree: f64, | ||
) -> PyResult<bool> { | ||
let qargs1 = qargs1.map_or_else(|| Ok(PyTuple::empty(py)), PySequenceMethods::to_tuple)?; | ||
let cargs1 = cargs1.map_or_else(|| Ok(PyTuple::empty(py)), PySequenceMethods::to_tuple)?; | ||
|
@@ -220,6 +207,7 @@ impl CommutationChecker { | |
&qargs2, | ||
&cargs2, | ||
max_num_qubits, | ||
approximation_degree, | ||
) | ||
} | ||
|
||
|
@@ -288,20 +276,20 @@ impl CommutationChecker { | |
qargs2: &[Qubit], | ||
cargs2: &[Clbit], | ||
max_num_qubits: u32, | ||
approximation_degree: f64, | ||
) -> PyResult<bool> { | ||
// relative and absolute tolerance used to (1) check whether rotation gates commute | ||
// trivially (i.e. the rotation angle is so small we assume it commutes) and (2) define | ||
// comparison for the matrix-based commutation checks | ||
let rtol = 1e-5; | ||
let atol = 1e-8; | ||
// If the average gate infidelity is below this tolerance, they commute. The tolerance | ||
// is set to max(1e-12, 1 - approximation_degree), to account for roundoffs and for | ||
// consistency with other places in Qiskit. | ||
let tol = 1e-12_f64.max(1. - approximation_degree); | ||
|
||
// if we have rotation gates, we attempt to map them to their generators, for example | ||
// RX -> X or CPhase -> CZ | ||
let (op1, params1, trivial1) = map_rotation(op1, params1, rtol); | ||
let (op1, params1, trivial1) = map_rotation(op1, params1, tol); | ||
if trivial1 { | ||
return Ok(true); | ||
} | ||
let (op2, params2, trivial2) = map_rotation(op2, params2, rtol); | ||
let (op2, params2, trivial2) = map_rotation(op2, params2, tol); | ||
if trivial2 { | ||
return Ok(true); | ||
} | ||
|
@@ -367,8 +355,7 @@ impl CommutationChecker { | |
second_op, | ||
second_params, | ||
second_qargs, | ||
rtol, | ||
atol, | ||
tol, | ||
); | ||
} | ||
|
||
|
@@ -403,8 +390,7 @@ impl CommutationChecker { | |
second_op, | ||
second_params, | ||
second_qargs, | ||
rtol, | ||
atol, | ||
tol, | ||
)?; | ||
|
||
// TODO: implement a LRU cache for this | ||
|
@@ -439,8 +425,7 @@ impl CommutationChecker { | |
second_op: &OperationRef, | ||
second_params: &[Param], | ||
second_qargs: &[Qubit], | ||
rtol: f64, | ||
atol: f64, | ||
tol: f64, | ||
) -> PyResult<bool> { | ||
// Compute relative positioning of qargs of the second gate to the first gate. | ||
// Since the qargs come out the same BitData, we already know there are no accidential | ||
|
@@ -481,81 +466,49 @@ impl CommutationChecker { | |
None => return Ok(false), | ||
}; | ||
|
||
if first_qarg == second_qarg { | ||
match first_qarg.len() { | ||
1 => Ok(unitary_compose::commute_1q( | ||
&first_mat.view(), | ||
&second_mat.view(), | ||
rtol, | ||
atol, | ||
)), | ||
2 => Ok(unitary_compose::commute_2q( | ||
&first_mat.view(), | ||
&second_mat.view(), | ||
&[Qubit(0), Qubit(1)], | ||
rtol, | ||
atol, | ||
)), | ||
_ => Ok(unitary_compose::allclose( | ||
&second_mat.dot(&first_mat).view(), | ||
&first_mat.dot(&second_mat).view(), | ||
rtol, | ||
atol, | ||
)), | ||
} | ||
// TODO Optimize this bit to avoid unnecessary Kronecker products: | ||
// 1. We currently sort the operations for the cache by operation size, putting the | ||
// *smaller* operation first: (smaller op, larger op) | ||
// 2. This code here expands the first op to match the second -- hence we always | ||
// match the operator sizes. | ||
// This whole extension logic could be avoided since we know the second one is larger. | ||
let extra_qarg2 = num_qubits - first_qarg.len() as u32; | ||
let first_mat = if extra_qarg2 > 0 { | ||
let id_op = Array2::<Complex64>::eye(usize::pow(2, extra_qarg2)); | ||
kron(&id_op, &first_mat) | ||
} else { | ||
// TODO Optimize this bit to avoid unnecessary Kronecker products: | ||
// 1. We currently sort the operations for the cache by operation size, putting the | ||
// *smaller* operation first: (smaller op, larger op) | ||
// 2. This code here expands the first op to match the second -- hence we always | ||
// match the operator sizes. | ||
// This whole extension logic could be avoided since we know the second one is larger. | ||
let extra_qarg2 = num_qubits - first_qarg.len() as u32; | ||
let first_mat = if extra_qarg2 > 0 { | ||
let id_op = Array2::<Complex64>::eye(usize::pow(2, extra_qarg2)); | ||
kron(&id_op, &first_mat) | ||
} else { | ||
first_mat | ||
}; | ||
|
||
// the 1 qubit case cannot happen, since that would already have been captured | ||
// by the previous if clause; first_qarg == second_qarg (if they overlap they must | ||
// be the same) | ||
if num_qubits == 2 { | ||
return Ok(unitary_compose::commute_2q( | ||
&first_mat.view(), | ||
&second_mat.view(), | ||
&second_qarg, | ||
rtol, | ||
atol, | ||
)); | ||
}; | ||
first_mat | ||
}; | ||
|
||
let op12 = match unitary_compose::compose( | ||
&first_mat.view(), | ||
&second_mat.view(), | ||
&second_qarg, | ||
false, | ||
) { | ||
Ok(matrix) => matrix, | ||
Err(e) => return Err(PyRuntimeError::new_err(e)), | ||
}; | ||
let op21 = match unitary_compose::compose( | ||
&first_mat.view(), | ||
&second_mat.view(), | ||
&second_qarg, | ||
true, | ||
) { | ||
Ok(matrix) => matrix, | ||
Err(e) => return Err(PyRuntimeError::new_err(e)), | ||
}; | ||
Ok(unitary_compose::allclose( | ||
&op12.view(), | ||
&op21.view(), | ||
rtol, | ||
atol, | ||
)) | ||
} | ||
// the 1 qubit case cannot happen, since that would already have been captured | ||
// by the previous if clause; first_qarg == second_qarg (if they overlap they must | ||
// be the same) | ||
let op12 = match unitary_compose::compose( | ||
&first_mat.view(), | ||
&second_mat.view(), | ||
&second_qarg, | ||
false, | ||
) { | ||
Ok(matrix) => matrix, | ||
Err(e) => return Err(PyRuntimeError::new_err(e)), | ||
}; | ||
let op21 = match unitary_compose::compose( | ||
&first_mat.view(), | ||
&second_mat.view(), | ||
&second_qarg, | ||
true, | ||
) { | ||
Ok(matrix) => matrix, | ||
Err(e) => return Err(PyRuntimeError::new_err(e)), | ||
}; | ||
let (fid, phase) = gate_metrics::gate_fidelity(&op12.view(), &op21.view(), None); | ||
|
||
// we consider the gates as commuting if the process fidelity of | ||
// AB (BA)^\dagger is approximately the identity and there is no global phase difference | ||
// let dim = op12.ncols() as f64; | ||
// let matrix_tol = tol * dim.powi(2); | ||
let matrix_tol = tol; | ||
Ok(phase.abs() <= tol && (1.0 - fid).abs() <= matrix_tol) | ||
} | ||
|
||
fn clear_cache(&mut self) { | ||
|
@@ -652,13 +605,19 @@ fn map_rotation<'a>( | |
) -> (&'a OperationRef<'a>, &'a [Param], bool) { | ||
let name = op.name(); | ||
|
||
if let Some((pi_multiple, generator)) = SUPPORTED_ROTATIONS.get(name) { | ||
if let Some(generator) = SUPPORTED_ROTATIONS.get(name) { | ||
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 not for this PR but we can drop the hashset here all together and have a static lookup table based on the standard gates. Something like: const fn builld_lut() -> [Option<StandardGate>; STANDARD_GATE_SIZE] {
...
}
let static supported_rotations: [Option<StandardGate>; STANDARD_GATE_SIZE] = build_lut() Alternatively you could just do a couple of if When in rust space and working solely with standard gates we really never have a need for strings, so this just sticks out to me every time I see it. But it was pre-existing so we can do this in a 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. Yeah that sounds like a good idea, I'll put it on the follow-up list if that's good for you 🙂 |
||
// If the rotation angle is below the tolerance, the gate is assumed to | ||
// commute with everything, and we simply return the operation with the flag that | ||
// it commutes trivially. | ||
if let Param::Float(angle) = params[0] { | ||
let periodicity = (*pi_multiple as f64) * ::std::f64::consts::PI; | ||
if (angle % periodicity).abs() < tol { | ||
let gate = op | ||
.standard_gate() | ||
.expect("Supported gates are standard gates"); | ||
let (tr_over_dim, dim) = gate_metrics::rotation_trace_and_dim(gate, angle) | ||
.expect("All rotation should be covered at this point"); | ||
let gate_fidelity = tr_over_dim.abs().powi(2); | ||
let process_fidelity = (dim * gate_fidelity + 1.) / (dim + 1.); | ||
if (1. - process_fidelity).abs() <= tol { | ||
return (op, params, true); | ||
}; | ||
}; | ||
|
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.
This does mean we can't approximate more than what a 1e-12 tolerance provides. But it's the same logic we use in other places so I think that's fine.
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.
Yeah it is unfortunately. I don't like this very much either, but currently
approximation_degree
is1.
everywhere per default which doesn't really allow us to set another default like1-1e-12
. The other option would've been to useNone
as indicator, but that already means that thetarget
error rates should be used.