From 746758fe15ccafb08eca954d910635cc004163a7 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 1 Dec 2024 02:58:56 -0500 Subject: [PATCH] Add support for running the TwoQubitControlledUDecomposer Since #13139 merged we have another two qubit decomposer available to run in rust, the TwoQubitControlledUDecomposer. This commit updates the new TwoQubitPeepholeOptimization to call this decomposer if the target supports appropriate 2q gates. --- crates/accelerate/src/two_qubit_decompose.rs | 44 +++++++++---------- crates/accelerate/src/two_qubit_peephole.rs | 45 +++++++++++++++++--- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index d0217dd5ff87..3eba734a9171 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2436,23 +2436,23 @@ pub enum RXXEquivalent { } impl RXXEquivalent { - fn matrix(&self, py: Python, param: f64) -> PyResult> { + fn matrix(&self, param: f64) -> PyResult> { match self { Self::Standard(gate) => Ok(gate.matrix(&[Param::Float(param)]).unwrap()), - Self::CustomPython(gate_cls) => { + Self::CustomPython(gate_cls) => Python::with_gil(|py: Python| { let gate_obj = gate_cls.bind(py).call1((param,))?; let raw_matrix = gate_obj .call_method0(intern!(py, "to_matrix"))? .extract::>()?; Ok(raw_matrix.as_array().to_owned()) - } + }), } } } #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitControlledUDecomposer { - rxx_equivalent_gate: RXXEquivalent, + pub rxx_equivalent_gate: RXXEquivalent, #[pyo3(get)] scale: f64, } @@ -2467,7 +2467,6 @@ impl TwoQubitControlledUDecomposer { /// invert 2q gate sequence fn invert_2q_gate( &self, - py: Python, gate: (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>), ) -> PyResult { let (gate, params, qubits) = gate; @@ -2504,7 +2503,7 @@ impl TwoQubitControlledUDecomposer { .collect::>(); Ok((Some(inv_gate.0), inv_gate_params, qubits)) } - RXXEquivalent::CustomPython(gate_cls) => { + RXXEquivalent::CustomPython(gate_cls) => Python::with_gil(|py: Python| { let gate_obj = gate_cls.bind(py).call1(PyTuple::new_bound(py, params))?; let raw_inverse = gate_obj.call_method0(intern!(py, "inverse"))?; let inverse: OperationFromPython = raw_inverse.extract()?; @@ -2525,7 +2524,7 @@ impl TwoQubitControlledUDecomposer { "rxx gate inverse is not valid for this decomposer", )) } - } + }), } } } @@ -2538,14 +2537,14 @@ impl TwoQubitControlledUDecomposer { /// Circuit: Circuit equivalent to an RXXGate. /// Raises: /// QiskitError: If the circuit is not equivalent to an RXXGate. - fn to_rxx_gate(&self, py: Python, angle: f64) -> PyResult { + fn to_rxx_gate(&self, angle: f64) -> PyResult { // The user-provided RXXGate equivalent gate may be locally equivalent to the RXXGate // but with some scaling in the rotation angle. For example, RXXGate(angle) has Weyl // parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e. // :code:`self.rxx_equivalent_gate(angle)` might produce the Weyl parameters // (scale * angle, 0, 0) where scale != 1. This is the case for the CPhaseGate. - let mat = self.rxx_equivalent_gate.matrix(py, self.scale * angle)?; + let mat = self.rxx_equivalent_gate.matrix(self.scale * angle)?; let decomposer_inv = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; @@ -2611,18 +2610,17 @@ impl TwoQubitControlledUDecomposer { /// Appends U_d(a, b, c) to the circuit. fn weyl_gate( &self, - py: Python, circ: &mut TwoQubitGateSequence, target_decomposed: TwoQubitWeylDecomposition, atol: f64, ) -> PyResult<()> { - let circ_a = self.to_rxx_gate(py, -2.0 * target_decomposed.a)?; + let circ_a = self.to_rxx_gate(-2.0 * target_decomposed.a)?; circ.gates.extend(circ_a.gates); let mut global_phase = circ_a.global_phase; // translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate. if (target_decomposed.b).abs() > atol { - let circ_b = self.to_rxx_gate(py, -2.0 * target_decomposed.b)?; + let circ_b = self.to_rxx_gate(-2.0 * target_decomposed.b)?; global_phase += circ_b.global_phase; circ.gates .push((Some(StandardGate::SdgGate), smallvec![], smallvec![0])); @@ -2644,7 +2642,7 @@ impl TwoQubitControlledUDecomposer { // circuit if c < 0. let mut gamma = -2.0 * target_decomposed.c; if gamma <= 0.0 { - let circ_c = self.to_rxx_gate(py, gamma)?; + let circ_c = self.to_rxx_gate(gamma)?; global_phase += circ_c.global_phase; circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); @@ -2658,7 +2656,7 @@ impl TwoQubitControlledUDecomposer { } else { // invert the circuit above gamma *= -1.0; - let circ_c = self.to_rxx_gate(py, gamma)?; + let circ_c = self.to_rxx_gate(gamma)?; global_phase -= circ_c.global_phase; circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); @@ -2666,7 +2664,7 @@ impl TwoQubitControlledUDecomposer { .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); for gate in circ_c.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params, inv_gate_qubits) = - self.invert_2q_gate(py, gate)?; + self.invert_2q_gate(gate)?; circ.gates .push((inv_gate_name, inv_gate_params, inv_gate_qubits)); } @@ -2683,9 +2681,8 @@ impl TwoQubitControlledUDecomposer { /// Returns the Weyl decomposition in circuit form. /// Note: atol is passed to OneQubitEulerDecomposer. - fn call_inner( + pub fn call_inner( &self, - py: Python, unitary: ArrayView2, atol: f64, ) -> PyResult { @@ -2729,7 +2726,7 @@ impl TwoQubitControlledUDecomposer { gates, global_phase, }; - self.weyl_gate(py, &mut gates1, target_decomposed, atol)?; + self.weyl_gate(&mut gates1, target_decomposed, atol)?; global_phase += gates1.global_phase; if let Some(unitary_c1r) = unitary_c1r { @@ -2760,7 +2757,7 @@ impl TwoQubitControlledUDecomposer { /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. #[new] #[pyo3(signature=(rxx_equivalent_gate))] - pub fn new(py: Python, rxx_equivalent_gate: RXXEquivalent) -> PyResult { + pub fn new(rxx_equivalent_gate: RXXEquivalent) -> PyResult { let atol = DEFAULT_ATOL; let test_angles = [0.2, 0.3, PI2]; @@ -2776,14 +2773,17 @@ impl TwoQubitControlledUDecomposer { } } RXXEquivalent::CustomPython(gate_cls) => { - if gate_cls.bind(py).call1((test_angle,)).ok().is_none() { + let takes_param = Python::with_gil(|py: Python| { + gate_cls.bind(py).call1((test_angle,)).ok().is_none() + }); + if takes_param { return Err(QiskitError::new_err( "Equivalent gate needs to take exactly 1 angle parameter.", )); } } }; - let mat = rxx_equivalent_gate.matrix(py, test_angle)?; + let mat = rxx_equivalent_gate.matrix(test_angle)?; let decomp = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; let mat_rxx = StandardGate::RXXGate @@ -2834,7 +2834,7 @@ impl TwoQubitControlledUDecomposer { unitary: PyReadonlyArray2, atol: f64, ) -> PyResult { - let sequence = self.call_inner(py, unitary.as_array(), atol)?; + let sequence = self.call_inner(unitary.as_array(), atol)?; match &self.rxx_equivalent_gate { RXXEquivalent::Standard(rxx_gate) => CircuitData::from_standard_gates( py, diff --git a/crates/accelerate/src/two_qubit_peephole.rs b/crates/accelerate/src/two_qubit_peephole.rs index cf852fbbf997..d6a59b93b4cf 100644 --- a/crates/accelerate/src/two_qubit_peephole.rs +++ b/crates/accelerate/src/two_qubit_peephole.rs @@ -31,13 +31,20 @@ use crate::euler_one_qubit_decomposer::{ }; use crate::nlayout::PhysicalQubit; use crate::target_transpiler::Target; -use crate::two_qubit_decompose::{TwoQubitBasisDecomposer, TwoQubitGateSequence}; +use crate::two_qubit_decompose::{ + RXXEquivalent, TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer, TwoQubitGateSequence, +}; + +enum TwoQubitDecomposer { + Basis(TwoQubitBasisDecomposer), + ControlledU(TwoQubitControlledUDecomposer), +} fn get_decomposers_from_target( target: &Target, qubits: &[Qubit], fidelity: f64, -) -> PyResult> { +) -> PyResult> { let physical_qubits = smallvec![PhysicalQubit(qubits[0].0), PhysicalQubit(qubits[1].0)]; let gate_names = match target.operation_names_for_qargs(Some(&physical_qubits)) { Ok(names) => names, @@ -94,7 +101,7 @@ fn get_decomposers_from_target( let euler_bases: Vec = target_basis_set.get_bases().collect(); - available_kak_gate + let decomposers: PyResult> = available_kak_gate .iter() .filter_map(|(two_qubit_name, two_qubit_gate, params)| { let matrix = two_qubit_gate.matrix(params); @@ -107,11 +114,26 @@ fn get_decomposers_from_target( *euler_basis, None, ) + .map(TwoQubitDecomposer::Basis) }) }) }) .flatten() - .collect() + .collect(); + let mut decomposers = decomposers?; + for gate in [ + StandardGate::RXXGate, + StandardGate::RZZGate, + StandardGate::RYYGate, + StandardGate::RZXGate, + ] { + if gate_names.contains(gate.name()) { + decomposers.push(TwoQubitDecomposer::ControlledU( + TwoQubitControlledUDecomposer::new(RXXEquivalent::Standard(gate))?, + )); + } + } + Ok(decomposers) } #[inline] @@ -229,13 +251,22 @@ pub(crate) fn two_qubit_unitary_peephole_optimize( let sequence = decomposers .iter() - .map(|decomposer| { - ( + .map(|decomposer| match decomposer { + TwoQubitDecomposer::Basis(decomposer) => ( decomposer .call_inner(matrix.view(), None, true, None) .unwrap(), decomposer.gate_name().to_string(), - ) + ), + TwoQubitDecomposer::ControlledU(decomposer) => ( + decomposer.call_inner(matrix.view(), 1e-12).unwrap(), + match decomposer.rxx_equivalent_gate { + RXXEquivalent::Standard(gate) => gate.name().to_string(), + RXXEquivalent::CustomPython(_) => { + unreachable!("Decomposer only uses standard gates") + } + }, + ), }) .enumerate() .min_by(order_sequence)