From 2e3a38360baca9dc8e22b10d25a6082b35f17e8b Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 12 Sep 2024 02:17:32 -0500 Subject: [PATCH 01/27] replace calls to TwoQubitWeylDecomposition by calling rust functions --- .../two_qubit/two_qubit_decompose.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 633de3a64f78..b7f46920eeba 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -315,20 +315,21 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): rxx_equivalent_gate(test_angle, label="foo") except TypeError as _: raise QiskitError("Equivalent gate needs to take exactly 1 angle parameter.") from _ - decomp = TwoQubitWeylDecomposition(rxx_equivalent_gate(test_angle)) + mat = rxx_equivalent_gate(test_angle).to_matrix() + decomp = two_qubit_decompose.TwoQubitWeylDecomposition(mat) - circ = QuantumCircuit(2) - circ.rxx(test_angle, 0, 1) - decomposer_rxx = TwoQubitWeylDecomposition( - Operator(circ).data, + from qiskit.circuit.library import RXXGate + + mat = RXXGate(test_angle).to_matrix() + decomposer_rxx = two_qubit_decompose.TwoQubitWeylDecomposition( + mat, fidelity=None, _specialization=two_qubit_decompose.Specialization.ControlledEquiv, ) - circ = QuantumCircuit(2) - circ.append(rxx_equivalent_gate(test_angle), qargs=[0, 1]) - decomposer_equiv = TwoQubitWeylDecomposition( - Operator(circ).data, + mat = rxx_equivalent_gate(test_angle).to_matrix() + decomposer_equiv = two_qubit_decompose.TwoQubitWeylDecomposition( + mat, fidelity=None, _specialization=two_qubit_decompose.Specialization.ControlledEquiv, ) @@ -360,7 +361,7 @@ def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: """ # pylint: disable=attribute-defined-outside-init - self.decomposer = TwoQubitWeylDecomposition(unitary) + self.decomposer = two_qubit_decompose.TwoQubitWeylDecomposition(unitary.data) oneq_decompose = OneQubitEulerDecomposer("ZYZ") c1l, c1r, c2l, c2r = ( @@ -403,7 +404,8 @@ def _to_rxx_gate(self, angle: float) -> QuantumCircuit: circ = QuantumCircuit(2) circ.append(self.rxx_equivalent_gate(self.scale * angle), qargs=[0, 1]) - decomposer_inv = TwoQubitWeylDecomposition(Operator(circ).data) + mat = self.rxx_equivalent_gate(self.scale * angle).to_matrix() + decomposer_inv = two_qubit_decompose.TwoQubitWeylDecomposition(mat) oneq_decompose = OneQubitEulerDecomposer("ZYZ") From 7ab59b46c2c3066267078327c6b4a99e2099afa8 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 12 Sep 2024 02:52:24 -0500 Subject: [PATCH 02/27] fix failing test --- qiskit/synthesis/two_qubit/two_qubit_decompose.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index b7f46920eeba..ad70cee31a93 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -361,7 +361,8 @@ def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: """ # pylint: disable=attribute-defined-outside-init - self.decomposer = two_qubit_decompose.TwoQubitWeylDecomposition(unitary.data) + unitary = np.asarray(unitary, dtype=complex) + self.decomposer = two_qubit_decompose.TwoQubitWeylDecomposition(unitary) oneq_decompose = OneQubitEulerDecomposer("ZYZ") c1l, c1r, c2l, c2r = ( From 285a91ddf14beda20ee99d62aba7af871b8c5864 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 8 Oct 2024 02:35:38 -0500 Subject: [PATCH 03/27] add code for TwoQubitControlledUDecomposer in rust --- crates/accelerate/src/two_qubit_decompose.rs | 310 ++++++++++++++++++- 1 file changed, 309 insertions(+), 1 deletion(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index ff084323b716..8913b142decc 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -54,7 +54,7 @@ use rand_pcg::Pcg64Mcg; use qiskit_circuit::circuit_data::CircuitData; use qiskit_circuit::circuit_instruction::OperationFromPython; use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; -use qiskit_circuit::operations::{Param, StandardGate}; +use qiskit_circuit::operations::{Operation, Param, StandardGate}; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::{c64, GateArray1Q, GateArray2Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM}; @@ -2344,6 +2344,313 @@ pub fn local_equivalence(weyl: PyReadonlyArray1) -> PyResult<[f64; 3]> { Ok([g0_equiv + 0., g1_equiv + 0., g2_equiv + 0.]) } +fn invert_1q_gate(gate: (StandardGate, SmallVec<[f64; 3]>)) -> (StandardGate, SmallVec<[f64; 3]>) { + let gate_params = gate.1.into_iter().map(Param::Float).collect::>(); + let Some(inv_gate) = gate + .0 + .try_inverse(&gate_params) else {panic!()}; + let inv_gate_params = inv_gate + .1 + .into_iter() + .map(|param| match param { + Param::Float(val) => val, + _ => panic!(), + }) + .collect::>(); + (inv_gate.0, inv_gate_params) +} + +fn invert_2q_gate( + gate: (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>), +) -> (StandardGate, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>) { + let Some(inv_gate) = gate + .0 + .unwrap() + .try_inverse(&gate.1.into_iter().map(Param::Float).collect::>()) else {panic!()}; + let inv_gate_params = inv_gate + .1 + .into_iter() + .map(|param| match param { + Param::Float(val) => val, + _ => panic!(), + }) + .collect::>(); + (inv_gate.0, inv_gate_params, gate.2) +} + +#[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] +pub struct TwoQubitControlledUDecomposer { + rxx_equivalent_gate: StandardGate, + scale: f64, +} + +const DEFAULT_ATOL: f64 = 1e-12; + +impl TwoQubitControlledUDecomposer { + fn to_rxx_gate(&self, angle: f64) -> PyResult { + let mat = self + .rxx_equivalent_gate + .matrix(&[Param::Float(self.scale * angle)]) + .unwrap(); + let decomposer_inv = + TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; + + let euler_basis = EulerBasis::ZYZ; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(euler_basis); + + let mut gates = Vec::new(); + let global_phase = -decomposer_inv.global_phase; + + let decomp_k1r = decomposer_inv.K1r.view(); + let decomp_k2r = decomposer_inv.K2r.view(); + let decomp_k1l = decomposer_inv.K1l.view(); + let decomp_k2l = decomposer_inv.K2l.view(); + + let unitary_k1r = + unitary_to_gate_sequence_inner(decomp_k1r, &target_1q_basis_list, 0, None, true, None); + let unitary_k2r = + unitary_to_gate_sequence_inner(decomp_k2r, &target_1q_basis_list, 0, None, true, None); + let unitary_k1l = + unitary_to_gate_sequence_inner(decomp_k1l, &target_1q_basis_list, 0, None, true, None); + let unitary_k2l = + unitary_to_gate_sequence_inner(decomp_k2l, &target_1q_basis_list, 0, None, true, None); + + if let Some(unitary_k2r) = unitary_k2r { + for gate in unitary_k2r.gates.into_iter().rev() { + let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); + gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); + } + } + if let Some(unitary_k2l) = unitary_k2l { + for gate in unitary_k2l.gates.into_iter().rev() { + let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); + gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); + } + } + gates.push(( + Some(self.rxx_equivalent_gate), + smallvec![angle], + smallvec![0, 1], + )); + + if let Some(unitary_k1r) = unitary_k1r { + for gate in unitary_k1r.gates.into_iter().rev() { + let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); + gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); + } + } + if let Some(unitary_k1l) = unitary_k1l { + for gate in unitary_k1l.gates.into_iter().rev() { + let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); + gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); + } + } + + Ok(TwoQubitGateSequence { + gates, + global_phase, + }) + } + + fn weyl_gate( + &self, + circ: &mut TwoQubitGateSequence, + target_decomposed: TwoQubitWeylDecomposition, + atol: f64, + ) -> PyResult<()> { + let circ_a = self.to_rxx_gate(-2.0 * target_decomposed.a)?; + circ.gates.extend(circ_a.gates); + + if (target_decomposed.b).abs() > atol { + let circ_b = self.to_rxx_gate(-2.0 * target_decomposed.b)?; + circ.gates + .push((Some(StandardGate::SdgGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::SdgGate), smallvec![], smallvec![1])); + circ.gates.extend(circ_b.gates); + circ.gates + .push((Some(StandardGate::SGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::SGate), smallvec![], smallvec![1])); + } + + if (target_decomposed.c).abs() > atol { + let mut gamma = -2.0 * target_decomposed.c; + let mut invert = false; + if gamma > 0.0 { + gamma *= -1.0; + invert = true; + } + let circ_c = self.to_rxx_gate(gamma)?; + if !invert { + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + circ.gates.extend(circ_c.gates); + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + } else { + // invert the circuit above + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); + circ.gates + .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) = invert_2q_gate(gate); + circ.gates + .push((Some(inv_gate_name), inv_gate_params, inv_gate_qubits)); + } + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); + circ.gates + .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + } + } + + Ok(()) + } + + fn call_inner( + &self, + unitary: ArrayView2, + atol: f64, + ) -> PyResult { + let target_decomposed = + TwoQubitWeylDecomposition::new_inner(unitary, Some(DEFAULT_FIDELITY), None)?; + + let euler_basis = EulerBasis::ZYZ; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(euler_basis); + + let c1r = target_decomposed.K1r.view(); + let c2r = target_decomposed.K2r.view(); + let c1l = target_decomposed.K1l.view(); + let c2l = target_decomposed.K2l.view(); + + let unitary_c1r = + unitary_to_gate_sequence_inner(c1r, &target_1q_basis_list, 0, None, true, None); + let unitary_c2r = + unitary_to_gate_sequence_inner(c2r, &target_1q_basis_list, 0, None, true, None); + let unitary_c1l = + unitary_to_gate_sequence_inner(c1l, &target_1q_basis_list, 0, None, true, None); + let unitary_c2l = + unitary_to_gate_sequence_inner(c2l, &target_1q_basis_list, 0, None, true, None); + + let mut gates = Vec::new(); + let global_phase = target_decomposed.global_phase; + + if let Some(unitary_c2r) = unitary_c2r { + for gate in unitary_c2r.gates.into_iter() { + gates.push((Some(gate.0), gate.1, smallvec![0])); + } + } + if let Some(unitary_c2l) = unitary_c2l { + for gate in unitary_c2l.gates.into_iter() { + gates.push((Some(gate.0), gate.1, smallvec![1])); + } + } + let mut gates1 = TwoQubitGateSequence { + gates, + global_phase, + }; + let _ = self.weyl_gate(&mut gates1, target_decomposed, atol); + + if let Some(unitary_c1r) = unitary_c1r { + for gate in unitary_c1r.gates.into_iter() { + gates1.gates.push((Some(gate.0), gate.1, smallvec![0])); + } + } + if let Some(unitary_c1l) = unitary_c1l { + for gate in unitary_c1l.gates.into_iter() { + gates1.gates.push((Some(gate.0), gate.1, smallvec![1])); + } + } + + Ok(gates1) + } + + fn new_inner(rxx_equivalent_gate: StandardGate) -> PyResult { + let atol = DEFAULT_ATOL; + let mut scales = Vec::new(); + let test_angles = [0.2, 0.3, PI2]; + + for test_angle in test_angles { + if rxx_equivalent_gate.num_params() != 1 { + return Err(QiskitError::new_err( + "Equivalent gate needs to take exactly 1 angle parameter", + )); + } + let mat = rxx_equivalent_gate + .matrix(&[Param::Float(test_angle)]) + .unwrap(); + let decomp = + TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; + + let mat_rxx = StandardGate::RXXGate + .matrix(&[Param::Float(test_angle)]) + .unwrap(); + let decomposer_rxx = TwoQubitWeylDecomposition::new_inner( + mat_rxx.view(), + None, + Some(Specialization::ControlledEquiv), + )?; + let decomposer_equiv = TwoQubitWeylDecomposition::new_inner( + mat.view(), + None, + Some(Specialization::ControlledEquiv), + )?; + let scale = decomposer_rxx.a / decomposer_equiv.a; + + if (decomp.a * 2.0 - test_angle / scale).abs() > atol { + return Err(QiskitError::new_err(format!( + "The gate {} + is not equivalent to an RXXGate", + rxx_equivalent_gate.name() + ))); + } + + // error handling - TBD + scales.push(scale); + + for scale_val in scales.clone().into_iter() { + if !abs_diff_eq!(scale_val, scale) { + return Err(QiskitError::new_err( + "Inconsistent scaling parameters in check", + )); + } + } + } + + let scale = scales[0]; + + Ok(TwoQubitControlledUDecomposer { + scale, + rxx_equivalent_gate, + }) + } +} + +#[pymethods] +impl TwoQubitControlledUDecomposer { + #[new] + #[pyo3(signature=(rxx_equivalent_gate))] + fn new(rxx_equivalent_gate: StandardGate) -> PyResult { + TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate) + } + #[pyo3(signature=(unitary, atol))] + fn __call__( + &self, + unitary: PyReadonlyArray2, + atol: f64, + ) -> PyResult { + self.call_inner(unitary.as_array(), atol) + } +} + pub fn two_qubit_decompose(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(_num_basis_gates))?; m.add_wrapped(wrap_pyfunction!(py_decompose_two_qubit_product_gate))?; @@ -2356,5 +2663,6 @@ pub fn two_qubit_decompose(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } From 14c4a565a532b9565002b97f747ff2577156d16d Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Wed, 9 Oct 2024 00:32:58 -0500 Subject: [PATCH 04/27] update try_inverse to inverse --- crates/accelerate/src/two_qubit_decompose.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 8913b142decc..6f57e3d6c2f2 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2348,7 +2348,7 @@ fn invert_1q_gate(gate: (StandardGate, SmallVec<[f64; 3]>)) -> (StandardGate, Sm let gate_params = gate.1.into_iter().map(Param::Float).collect::>(); let Some(inv_gate) = gate .0 - .try_inverse(&gate_params) else {panic!()}; + .inverse(&gate_params) else {panic!()}; let inv_gate_params = inv_gate .1 .into_iter() @@ -2366,7 +2366,7 @@ fn invert_2q_gate( let Some(inv_gate) = gate .0 .unwrap() - .try_inverse(&gate.1.into_iter().map(Param::Float).collect::>()) else {panic!()}; + .inverse(&gate.1.into_iter().map(Param::Float).collect::>()) else {panic!()}; let inv_gate_params = inv_gate .1 .into_iter() From eb1b928bfdf6597f80bf812fecd19f9ce4eaf2e6 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Wed, 9 Oct 2024 05:22:44 -0500 Subject: [PATCH 05/27] fix rust code. add rust docstrings --- crates/accelerate/src/two_qubit_decompose.rs | 58 ++++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 6f57e3d6c2f2..563e3a2ee6fc 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2344,6 +2344,7 @@ pub fn local_equivalence(weyl: PyReadonlyArray1) -> PyResult<[f64; 3]> { Ok([g0_equiv + 0., g1_equiv + 0., g2_equiv + 0.]) } +/// invert 1q gate sequence fn invert_1q_gate(gate: (StandardGate, SmallVec<[f64; 3]>)) -> (StandardGate, SmallVec<[f64; 3]>) { let gate_params = gate.1.into_iter().map(Param::Float).collect::>(); let Some(inv_gate) = gate @@ -2360,6 +2361,7 @@ fn invert_1q_gate(gate: (StandardGate, SmallVec<[f64; 3]>)) -> (StandardGate, Sm (inv_gate.0, inv_gate_params) } +/// invert 2q gate sequence fn invert_2q_gate( gate: (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>), ) -> (StandardGate, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>) { @@ -2380,13 +2382,26 @@ fn invert_2q_gate( #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitControlledUDecomposer { + #[pyo3(get)] rxx_equivalent_gate: StandardGate, + #[pyo3(get)] scale: f64, } const DEFAULT_ATOL: f64 = 1e-12; +/// Decompose two-qubit unitary in terms of a desired +/// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` +/// gate that is locally equivalent to an :class:`.RXXGate`. impl TwoQubitControlledUDecomposer { + /// Takes an angle and returns the circuit equivalent to an RXXGate with the + /// RXX equivalent gate as the two-qubit unitary. + /// Args: + /// angle: Rotation angle (in this case one of the Weyl parameters a, b, or c) + /// Returns: + /// Circuit: Circuit equivalent to an RXXGate. + /// Raises: + /// QiskitError: If the circuit is not equivalent to an RXXGate. fn to_rxx_gate(&self, angle: f64) -> PyResult { let mat = self .rxx_equivalent_gate @@ -2425,12 +2440,12 @@ impl TwoQubitControlledUDecomposer { if let Some(unitary_k2l) = unitary_k2l { for gate in unitary_k2l.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); - gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); + gates.push((Some(inv_gate_name), inv_gate_params, smallvec![1])); } } gates.push(( Some(self.rxx_equivalent_gate), - smallvec![angle], + smallvec![self.scale * angle], smallvec![0, 1], )); @@ -2443,7 +2458,7 @@ impl TwoQubitControlledUDecomposer { if let Some(unitary_k1l) = unitary_k1l { for gate in unitary_k1l.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); - gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); + gates.push((Some(inv_gate_name), inv_gate_params, smallvec![1])); } } @@ -2453,6 +2468,7 @@ impl TwoQubitControlledUDecomposer { }) } + /// Appends U_d(a, b, c) to the circuit. fn weyl_gate( &self, circ: &mut TwoQubitGateSequence, @@ -2514,6 +2530,8 @@ impl TwoQubitControlledUDecomposer { Ok(()) } + /// Returns the Weyl decomposition in circuit form. + /// Note: atol is passed to OneQubitEulerDecomposer. fn call_inner( &self, unitary: ArrayView2, @@ -2573,6 +2591,12 @@ impl TwoQubitControlledUDecomposer { Ok(gates1) } + /// Initialize the KAK decomposition. + /// Args: + /// rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`: + /// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. + /// Raises: + /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. fn new_inner(rxx_equivalent_gate: StandardGate) -> PyResult { let atol = DEFAULT_ATOL; let mut scales = Vec::new(); @@ -2581,7 +2605,7 @@ impl TwoQubitControlledUDecomposer { for test_angle in test_angles { if rxx_equivalent_gate.num_params() != 1 { return Err(QiskitError::new_err( - "Equivalent gate needs to take exactly 1 angle parameter", + "Equivalent gate needs to take exactly 1 angle parameter.", )); } let mat = rxx_equivalent_gate @@ -2603,30 +2627,30 @@ impl TwoQubitControlledUDecomposer { None, Some(Specialization::ControlledEquiv), )?; - let scale = decomposer_rxx.a / decomposer_equiv.a; + let scale_a = decomposer_rxx.a / decomposer_equiv.a; - if (decomp.a * 2.0 - test_angle / scale).abs() > atol { + if (decomp.a * 2.0 - test_angle / scale_a).abs() > atol { return Err(QiskitError::new_err(format!( "The gate {} - is not equivalent to an RXXGate", + is not equivalent to an RXXGate.", rxx_equivalent_gate.name() ))); } - // error handling - TBD - scales.push(scale); - - for scale_val in scales.clone().into_iter() { - if !abs_diff_eq!(scale_val, scale) { - return Err(QiskitError::new_err( - "Inconsistent scaling parameters in check", - )); - } - } + scales.push(scale_a); } let scale = scales[0]; + // Check that all three tested angles give the same scale + for scale_val in scales.clone().into_iter() { + if !abs_diff_eq!(scale_val, scale, epsilon = atol) { + return Err(QiskitError::new_err( + "Inconsistent scaling parameters in check.", + )); + } + } + Ok(TwoQubitControlledUDecomposer { scale, rxx_equivalent_gate, From c9b7553aa7cb8e6b93f9343c7eed25d26184f2b0 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Wed, 9 Oct 2024 05:23:04 -0500 Subject: [PATCH 06/27] remove python code --- .../two_qubit/two_qubit_decompose.py | 160 ++---------------- 1 file changed, 13 insertions(+), 147 deletions(-) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 38bb94802419..2eb55708af3c 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -280,160 +280,26 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): Raises: QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. """ - atol = DEFAULT_ATOL - - scales, test_angles, scale = [], [0.2, 0.3, np.pi / 2], None - - for test_angle in test_angles: - # Check that gate takes a single angle parameter - try: - rxx_equivalent_gate(test_angle, label="foo") - except TypeError as _: - raise QiskitError("Equivalent gate needs to take exactly 1 angle parameter.") from _ - mat = rxx_equivalent_gate(test_angle).to_matrix() - decomp = two_qubit_decompose.TwoQubitWeylDecomposition(mat) - - from qiskit.circuit.library import RXXGate - - mat = RXXGate(test_angle).to_matrix() - decomposer_rxx = two_qubit_decompose.TwoQubitWeylDecomposition( - mat, - fidelity=None, - _specialization=two_qubit_decompose.Specialization.ControlledEquiv, - ) - - mat = rxx_equivalent_gate(test_angle).to_matrix() - decomposer_equiv = two_qubit_decompose.TwoQubitWeylDecomposition( - mat, - fidelity=None, - _specialization=two_qubit_decompose.Specialization.ControlledEquiv, - ) - - scale = decomposer_rxx.a / decomposer_equiv.a - - if abs(decomp.a * 2 - test_angle / scale) > atol: - raise QiskitError( - f"{rxx_equivalent_gate.__name__} is not equivalent to an RXXGate." - ) - - scales.append(scale) - - # Check that all three tested angles give the same scale - if not np.allclose(scales, [scale] * len(test_angles)): - raise QiskitError( - f"Cannot initialize {self.__class__.__name__}: with gate {rxx_equivalent_gate}. " - "Inconsistent scaling parameters in checks." - ) - - self.scale = scales[0] - - self.rxx_equivalent_gate = rxx_equivalent_gate + self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( + rxx_equivalent_gate._standard_gate + ) + self.rxx_equivalent_gate = self._inner_decomposition.rxx_equivalent_gate + self.scale = self._inner_decomposition.scale def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: """Returns the Weyl decomposition in circuit form. - Note: atol ist passed to OneQubitEulerDecomposer. + Note: atol is passed to OneQubitEulerDecomposer. """ + sequence = self._inner_decomposition(np.asarray(unitary.to_matrix(), dtype=complex), atol) - # pylint: disable=attribute-defined-outside-init - unitary = np.asarray(unitary, dtype=complex) - self.decomposer = two_qubit_decompose.TwoQubitWeylDecomposition(unitary) - - oneq_decompose = OneQubitEulerDecomposer("ZYZ") - c1l, c1r, c2l, c2r = ( - oneq_decompose(k, atol=atol) - for k in ( - self.decomposer.K1l, - self.decomposer.K1r, - self.decomposer.K2l, - self.decomposer.K2r, + q = QuantumRegister(2) + circ = QuantumCircuit(q, global_phase=sequence.global_phase) + for gate, params, qubits in sequence: + inst = CircuitInstruction.from_standard( + gate, qubits=tuple(q[x] for x in qubits), params=params ) - ) - circ = QuantumCircuit(2, global_phase=self.decomposer.global_phase) - circ.compose(c2r, [0], inplace=True) - circ.compose(c2l, [1], inplace=True) - self._weyl_gate(circ) - circ.compose(c1r, [0], inplace=True) - circ.compose(c1l, [1], inplace=True) - return circ - - def _to_rxx_gate(self, angle: float) -> QuantumCircuit: - """ - Takes an angle and returns the circuit equivalent to an RXXGate with the - RXX equivalent gate as the two-qubit unitary. - - Args: - angle: Rotation angle (in this case one of the Weyl parameters a, b, or c) - - Returns: - Circuit: Circuit equivalent to an RXXGate. - - Raises: - QiskitError: If the circuit is not equivalent to an RXXGate. - """ - - # 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. - - circ = QuantumCircuit(2) - circ.append(self.rxx_equivalent_gate(self.scale * angle), qargs=[0, 1]) - mat = self.rxx_equivalent_gate(self.scale * angle).to_matrix() - decomposer_inv = two_qubit_decompose.TwoQubitWeylDecomposition(mat) - - oneq_decompose = OneQubitEulerDecomposer("ZYZ") - - # Express the RXXGate in terms of the user-provided RXXGate equivalent gate. - rxx_circ = QuantumCircuit(2, global_phase=-decomposer_inv.global_phase) - rxx_circ.compose(oneq_decompose(decomposer_inv.K2r).inverse(), inplace=True, qubits=[0]) - rxx_circ.compose(oneq_decompose(decomposer_inv.K2l).inverse(), inplace=True, qubits=[1]) - rxx_circ.compose(circ, inplace=True) - rxx_circ.compose(oneq_decompose(decomposer_inv.K1r).inverse(), inplace=True, qubits=[0]) - rxx_circ.compose(oneq_decompose(decomposer_inv.K1l).inverse(), inplace=True, qubits=[1]) - - return rxx_circ - - def _weyl_gate(self, circ: QuantumCircuit, atol=1.0e-13): - """Appends U_d(a, b, c) to the circuit.""" - - circ_rxx = self._to_rxx_gate(-2 * self.decomposer.a) - circ.compose(circ_rxx, inplace=True) - - # translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate. - if abs(self.decomposer.b) > atol: - circ_ryy = QuantumCircuit(2) - circ_ryy.sdg(0) - circ_ryy.sdg(1) - circ_ryy.compose(self._to_rxx_gate(-2 * self.decomposer.b), inplace=True) - circ_ryy.s(0) - circ_ryy.s(1) - circ.compose(circ_ryy, inplace=True) - - # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. - if abs(self.decomposer.c) > atol: - # Since the Weyl chamber is here defined as a > b > |c| we may have - # negative c. This will cause issues in _to_rxx_gate - # as TwoQubitWeylControlledEquiv will map (c, 0, 0) to (|c|, 0, 0). - # We therefore produce RZZGate(|c|) and append its inverse to the - # circuit if c < 0. - gamma, invert = -2 * self.decomposer.c, False - if gamma > 0: - gamma *= -1 - invert = True - - circ_rzz = QuantumCircuit(2) - circ_rzz.h(0) - circ_rzz.h(1) - circ_rzz.compose(self._to_rxx_gate(gamma), inplace=True) - circ_rzz.h(0) - circ_rzz.h(1) - - if invert: - circ.compose(circ_rzz.inverse(), inplace=True) - else: - circ.compose(circ_rzz, inplace=True) + circ._append(inst) return circ From 71caa9e565c77682843a3cab296fcfc76e1d49ad Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Wed, 9 Oct 2024 08:16:04 -0500 Subject: [PATCH 07/27] update unitary in call --- qiskit/synthesis/two_qubit/two_qubit_decompose.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 2eb55708af3c..944916ba7c0b 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -286,12 +286,15 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): self.rxx_equivalent_gate = self._inner_decomposition.rxx_equivalent_gate self.scale = self._inner_decomposition.scale - def __call__(self, unitary, *, atol=DEFAULT_ATOL) -> QuantumCircuit: + def __call__(self, unitary: Operator | np.ndarray, *, atol=DEFAULT_ATOL) -> QuantumCircuit: """Returns the Weyl decomposition in circuit form. - + Args: + unitary (Operator or ndarray): :math:`4 \times 4` unitary to synthesize. + Returns: + QuantumCircuit: Synthesized quantum circuit. Note: atol is passed to OneQubitEulerDecomposer. """ - sequence = self._inner_decomposition(np.asarray(unitary.to_matrix(), dtype=complex), atol) + sequence = self._inner_decomposition(np.asarray(unitary, dtype=complex), atol) q = QuantumRegister(2) circ = QuantumCircuit(q, global_phase=sequence.global_phase) From eb4968b6740f6afec974a1f2d832ec90b63d0226 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 10 Oct 2024 02:28:27 -0500 Subject: [PATCH 08/27] handle global_phase --- crates/accelerate/src/two_qubit_decompose.rs | 21 +++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 563e3a2ee6fc..39b3f6b71334 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2415,7 +2415,7 @@ impl TwoQubitControlledUDecomposer { target_1q_basis_list.add_basis(euler_basis); let mut gates = Vec::new(); - let global_phase = -decomposer_inv.global_phase; + let mut global_phase = -decomposer_inv.global_phase; let decomp_k1r = decomposer_inv.K1r.view(); let decomp_k2r = decomposer_inv.K2r.view(); @@ -2432,12 +2432,14 @@ impl TwoQubitControlledUDecomposer { unitary_to_gate_sequence_inner(decomp_k2l, &target_1q_basis_list, 0, None, true, None); if let Some(unitary_k2r) = unitary_k2r { + global_phase -= unitary_k2r.global_phase; for gate in unitary_k2r.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); } } if let Some(unitary_k2l) = unitary_k2l { + global_phase -= unitary_k2l.global_phase; for gate in unitary_k2l.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); gates.push((Some(inv_gate_name), inv_gate_params, smallvec![1])); @@ -2450,12 +2452,14 @@ impl TwoQubitControlledUDecomposer { )); if let Some(unitary_k1r) = unitary_k1r { + global_phase += unitary_k1r.global_phase; for gate in unitary_k1r.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); } } if let Some(unitary_k1l) = unitary_k1l { + global_phase += unitary_k1l.global_phase; for gate in unitary_k1l.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); gates.push((Some(inv_gate_name), inv_gate_params, smallvec![1])); @@ -2477,9 +2481,11 @@ impl TwoQubitControlledUDecomposer { ) -> PyResult<()> { 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; if (target_decomposed.b).abs() > atol { 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])); circ.gates @@ -2500,6 +2506,7 @@ impl TwoQubitControlledUDecomposer { } let circ_c = self.to_rxx_gate(gamma)?; if !invert { + global_phase += circ_c.global_phase; circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); circ.gates @@ -2511,6 +2518,7 @@ impl TwoQubitControlledUDecomposer { .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); } else { // invert the circuit above + global_phase -= circ_c.global_phase; circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); circ.gates @@ -2527,6 +2535,7 @@ impl TwoQubitControlledUDecomposer { } } + circ.global_phase = global_phase; Ok(()) } @@ -2559,14 +2568,16 @@ impl TwoQubitControlledUDecomposer { unitary_to_gate_sequence_inner(c2l, &target_1q_basis_list, 0, None, true, None); let mut gates = Vec::new(); - let global_phase = target_decomposed.global_phase; + let mut global_phase = target_decomposed.global_phase; if let Some(unitary_c2r) = unitary_c2r { + global_phase += unitary_c2r.global_phase; for gate in unitary_c2r.gates.into_iter() { gates.push((Some(gate.0), gate.1, smallvec![0])); } } if let Some(unitary_c2l) = unitary_c2l { + global_phase += unitary_c2l.global_phase; for gate in unitary_c2l.gates.into_iter() { gates.push((Some(gate.0), gate.1, smallvec![1])); } @@ -2575,19 +2586,23 @@ impl TwoQubitControlledUDecomposer { gates, global_phase, }; - let _ = self.weyl_gate(&mut gates1, target_decomposed, atol); + let _ = self.weyl_gate(&mut gates1, target_decomposed, atol)?; + global_phase += gates1.global_phase; if let Some(unitary_c1r) = unitary_c1r { + global_phase -= unitary_c1r.global_phase; for gate in unitary_c1r.gates.into_iter() { gates1.gates.push((Some(gate.0), gate.1, smallvec![0])); } } if let Some(unitary_c1l) = unitary_c1l { + global_phase -= unitary_c1l.global_phase; for gate in unitary_c1l.gates.into_iter() { gates1.gates.push((Some(gate.0), gate.1, smallvec![1])); } } + gates1.global_phase = global_phase; Ok(gates1) } From a2943c97e48252f9d9afb4d99c659451710e7255 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 10 Oct 2024 03:21:49 -0500 Subject: [PATCH 09/27] minor fix --- crates/accelerate/src/two_qubit_decompose.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 39b3f6b71334..097cb013af12 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2586,7 +2586,7 @@ impl TwoQubitControlledUDecomposer { gates, global_phase, }; - let _ = self.weyl_gate(&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 { From 7acb09aa8973cc53c51ea75060dd3c1eb3dbb253 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 10 Oct 2024 03:51:10 -0500 Subject: [PATCH 10/27] fix lint --- qiskit/synthesis/two_qubit/two_qubit_decompose.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 944916ba7c0b..686acdc1747a 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -51,7 +51,6 @@ from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import Operator from qiskit.synthesis.one_qubit.one_qubit_decompose import ( - OneQubitEulerDecomposer, DEFAULT_ATOL, ) from qiskit.utils.deprecation import deprecate_func From e532c8a9e1df7dff37331b6413857c1f53a4ce36 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 15 Oct 2024 00:48:16 -0500 Subject: [PATCH 11/27] set self.rxx_equivalent_gate --- qiskit/synthesis/two_qubit/two_qubit_decompose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 686acdc1747a..ef430d2925c3 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -282,7 +282,7 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( rxx_equivalent_gate._standard_gate ) - self.rxx_equivalent_gate = self._inner_decomposition.rxx_equivalent_gate + self.rxx_equivalent_gate = rxx_equivalent_gate self.scale = self._inner_decomposition.scale def __call__(self, unitary: Operator | np.ndarray, *, atol=DEFAULT_ATOL) -> QuantumCircuit: From 31828388b0808cffd2bb7def11b70382fc50e595 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 15 Oct 2024 00:58:25 -0500 Subject: [PATCH 12/27] check that rxx_equivalent_gate is a standard gate --- qiskit/synthesis/two_qubit/two_qubit_decompose.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index ef430d2925c3..33e3c9b65aa0 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -279,9 +279,12 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): Raises: QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. """ - self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( - rxx_equivalent_gate._standard_gate - ) + if rxx_equivalent_gate._standard_gate is not None: + self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( + rxx_equivalent_gate._standard_gate + ) + else: + raise QiskitError("Must be initialized with a standard gate object") self.rxx_equivalent_gate = rxx_equivalent_gate self.scale = self._inner_decomposition.scale From 59ef018f29c107d5c7cadbfa2442e5c0e94a8742 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 15 Oct 2024 01:14:57 -0500 Subject: [PATCH 13/27] iterating over test_angles and scales --- crates/accelerate/src/two_qubit_decompose.rs | 78 ++++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 097cb013af12..d7f189e0faff 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2614,52 +2614,52 @@ impl TwoQubitControlledUDecomposer { /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. fn new_inner(rxx_equivalent_gate: StandardGate) -> PyResult { let atol = DEFAULT_ATOL; - let mut scales = Vec::new(); let test_angles = [0.2, 0.3, PI2]; - for test_angle in test_angles { - if rxx_equivalent_gate.num_params() != 1 { - return Err(QiskitError::new_err( - "Equivalent gate needs to take exactly 1 angle parameter.", - )); - } - let mat = rxx_equivalent_gate - .matrix(&[Param::Float(test_angle)]) - .unwrap(); - let decomp = - TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; - - let mat_rxx = StandardGate::RXXGate - .matrix(&[Param::Float(test_angle)]) - .unwrap(); - let decomposer_rxx = TwoQubitWeylDecomposition::new_inner( - mat_rxx.view(), - None, - Some(Specialization::ControlledEquiv), - )?; - let decomposer_equiv = TwoQubitWeylDecomposition::new_inner( - mat.view(), - None, - Some(Specialization::ControlledEquiv), - )?; - let scale_a = decomposer_rxx.a / decomposer_equiv.a; - - if (decomp.a * 2.0 - test_angle / scale_a).abs() > atol { - return Err(QiskitError::new_err(format!( - "The gate {} + let scales: PyResult> = test_angles + .into_iter() + .map(|test_angle| { + if rxx_equivalent_gate.num_params() != 1 { + return Err(QiskitError::new_err( + "Equivalent gate needs to take exactly 1 angle parameter.", + )); + } + let mat = rxx_equivalent_gate + .matrix(&[Param::Float(test_angle)]) + .unwrap(); + let decomp = + TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; + let mat_rxx = StandardGate::RXXGate + .matrix(&[Param::Float(test_angle)]) + .unwrap(); + let decomposer_rxx = TwoQubitWeylDecomposition::new_inner( + mat_rxx.view(), + None, + Some(Specialization::ControlledEquiv), + )?; + let decomposer_equiv = TwoQubitWeylDecomposition::new_inner( + mat.view(), + None, + Some(Specialization::ControlledEquiv), + )?; + let scale_a = decomposer_rxx.a / decomposer_equiv.a; + if (decomp.a * 2.0 - test_angle / scale_a).abs() > atol { + return Err(QiskitError::new_err(format!( + "The gate {} is not equivalent to an RXXGate.", - rxx_equivalent_gate.name() - ))); - } - - scales.push(scale_a); - } + rxx_equivalent_gate.name() + ))); + } + Ok(scale_a) + }) + .collect(); + let scales = scales?; let scale = scales[0]; // Check that all three tested angles give the same scale - for scale_val in scales.clone().into_iter() { - if !abs_diff_eq!(scale_val, scale, epsilon = atol) { + for scale_val in &scales { + if !abs_diff_eq!(scale_val, &scale, epsilon = atol) { return Err(QiskitError::new_err( "Inconsistent scaling parameters in check.", )); From 1ceb94e13e9f3abee67736b19017dc3063fdffb2 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 15 Oct 2024 01:20:34 -0500 Subject: [PATCH 14/27] add Vec capacity --- crates/accelerate/src/two_qubit_decompose.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index d7f189e0faff..162d86ff4aea 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2414,7 +2414,7 @@ impl TwoQubitControlledUDecomposer { let mut target_1q_basis_list = EulerBasisSet::new(); target_1q_basis_list.add_basis(euler_basis); - let mut gates = Vec::new(); + let mut gates = Vec::with_capacity(13); let mut global_phase = -decomposer_inv.global_phase; let decomp_k1r = decomposer_inv.K1r.view(); @@ -2567,7 +2567,7 @@ impl TwoQubitControlledUDecomposer { let unitary_c2l = unitary_to_gate_sequence_inner(c2l, &target_1q_basis_list, 0, None, true, None); - let mut gates = Vec::new(); + let mut gates = Vec::with_capacity(59); let mut global_phase = target_decomposed.global_phase; if let Some(unitary_c2r) = unitary_c2r { From 9ac68b1d1ead2d8ae18be96732ecf2b6b2af638d Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 15 Oct 2024 01:38:52 -0500 Subject: [PATCH 15/27] remove invert variable --- crates/accelerate/src/two_qubit_decompose.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 162d86ff4aea..bc34ffd31588 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2499,13 +2499,8 @@ impl TwoQubitControlledUDecomposer { if (target_decomposed.c).abs() > atol { let mut gamma = -2.0 * target_decomposed.c; - let mut invert = false; - if gamma > 0.0 { - gamma *= -1.0; - invert = true; - } - let circ_c = self.to_rxx_gate(gamma)?; - if !invert { + if gamma <= 0.0 { + let circ_c = self.to_rxx_gate(gamma)?; global_phase += circ_c.global_phase; circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); @@ -2518,6 +2513,8 @@ impl TwoQubitControlledUDecomposer { .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); } else { // invert the circuit above + gamma *= -1.0; + let circ_c = self.to_rxx_gate(gamma)?; global_phase -= circ_c.global_phase; circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); From 6ad3869b034ebadb2ccba53dc26332baa6087f89 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 15 Oct 2024 01:53:01 -0500 Subject: [PATCH 16/27] move comments from Python to Rust code --- crates/accelerate/src/two_qubit_decompose.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index bc34ffd31588..3b9f69f21800 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2403,6 +2403,12 @@ impl TwoQubitControlledUDecomposer { /// Raises: /// QiskitError: If the circuit is not equivalent to an RXXGate. 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(&[Param::Float(self.scale * angle)]) @@ -2414,6 +2420,7 @@ impl TwoQubitControlledUDecomposer { let mut target_1q_basis_list = EulerBasisSet::new(); target_1q_basis_list.add_basis(euler_basis); + // Express the RXXGate in terms of the user-provided RXXGate equivalent gate. let mut gates = Vec::with_capacity(13); let mut global_phase = -decomposer_inv.global_phase; @@ -2483,6 +2490,7 @@ impl TwoQubitControlledUDecomposer { 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(-2.0 * target_decomposed.b)?; global_phase += circ_b.global_phase; @@ -2497,7 +2505,13 @@ impl TwoQubitControlledUDecomposer { .push((Some(StandardGate::SGate), smallvec![], smallvec![1])); } + // # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. if (target_decomposed.c).abs() > atol { + // Since the Weyl chamber is here defined as a > b > |c| we may have + // negative c. This will cause issues in _to_rxx_gate + // as TwoQubitWeylControlledEquiv will map (c, 0, 0) to (|c|, 0, 0). + // We therefore produce RZZGate(|c|) and append its inverse to the + // circuit if c < 0. let mut gamma = -2.0 * target_decomposed.c; if gamma <= 0.0 { let circ_c = self.to_rxx_gate(gamma)?; From ebc0e73ebecd19e88dc3dc4969298f09227c9441 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 15 Oct 2024 03:52:29 -0500 Subject: [PATCH 17/27] remove new_inner function, move code into new --- crates/accelerate/src/two_qubit_decompose.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 3b9f69f21800..9daa8c38e935 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2616,14 +2616,19 @@ impl TwoQubitControlledUDecomposer { gates1.global_phase = global_phase; Ok(gates1) } +} +#[pymethods] +impl TwoQubitControlledUDecomposer { /// Initialize the KAK decomposition. /// Args: /// rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`: /// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. /// Raises: /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. - fn new_inner(rxx_equivalent_gate: StandardGate) -> PyResult { + #[new] + #[pyo3(signature=(rxx_equivalent_gate))] + pub fn new(rxx_equivalent_gate: StandardGate) -> PyResult { let atol = DEFAULT_ATOL; let test_angles = [0.2, 0.3, PI2]; @@ -2682,15 +2687,7 @@ impl TwoQubitControlledUDecomposer { rxx_equivalent_gate, }) } -} -#[pymethods] -impl TwoQubitControlledUDecomposer { - #[new] - #[pyo3(signature=(rxx_equivalent_gate))] - fn new(rxx_equivalent_gate: StandardGate) -> PyResult { - TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate) - } #[pyo3(signature=(unitary, atol))] fn __call__( &self, From 02cd73ed85dfa227892ea0c26003f478c8dbf308 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 17 Oct 2024 01:09:10 -0500 Subject: [PATCH 18/27] call returns a CircuitData --- crates/accelerate/src/two_qubit_decompose.rs | 27 +++++++++++++++++-- .../two_qubit/two_qubit_decompose.py | 13 ++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 9daa8c38e935..6b6e5cd76df9 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2691,10 +2691,33 @@ impl TwoQubitControlledUDecomposer { #[pyo3(signature=(unitary, atol))] fn __call__( &self, + py: Python, unitary: PyReadonlyArray2, atol: f64, - ) -> PyResult { - self.call_inner(unitary.as_array(), atol) + ) -> PyResult { + let mut gate_sequence = CircuitData::with_capacity(py, 2, 0, 59, Param::Float(0.))?; + let gates_list = self.call_inner(unitary.as_array(), atol)?; + let global_phase: f64 = gates_list.global_phase; + for gate in gates_list.gates { + if let Some(gate_name) = gate.0 { + let qubits = gate.2; + if qubits.len() == 1 { + gate_sequence.push_standard_gate( + gate_name, + &gate.1.into_iter().map(Param::Float).collect::>(), + &[Qubit(qubits[0] as u32)], + )? + } else { + gate_sequence.push_standard_gate( + gate_name, + &gate.1.into_iter().map(Param::Float).collect::>(), + &[Qubit(qubits[0] as u32), Qubit(qubits[1] as u32)], + )? + } + } + } + gate_sequence.set_global_phase(py, Param::Float(global_phase))?; + Ok(gate_sequence) } } diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 33e3c9b65aa0..4df611167378 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -296,17 +296,8 @@ def __call__(self, unitary: Operator | np.ndarray, *, atol=DEFAULT_ATOL) -> Quan QuantumCircuit: Synthesized quantum circuit. Note: atol is passed to OneQubitEulerDecomposer. """ - sequence = self._inner_decomposition(np.asarray(unitary, dtype=complex), atol) - - q = QuantumRegister(2) - circ = QuantumCircuit(q, global_phase=sequence.global_phase) - for gate, params, qubits in sequence: - inst = CircuitInstruction.from_standard( - gate, qubits=tuple(q[x] for x in qubits), params=params - ) - circ._append(inst) - - return circ + circ_data = self._inner_decomposition(np.asarray(unitary, dtype=complex), atol) + return QuantumCircuit._from_circuit_data(circ_data, add_regs=True) class TwoQubitBasisDecomposer: From f46edbee26053d555d21283b9046b45207f863a5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 29 Oct 2024 17:43:37 -0400 Subject: [PATCH 19/27] Support custom gates for decomposition too This commit adds back the missing support for custom gate classes so that a user defined gate class can be used with the decomposer. --- crates/accelerate/src/two_qubit_decompose.rs | 250 ++++++++++++------ .../two_qubit/two_qubit_decompose.py | 4 +- test/python/synthesis/test_synthesis.py | 18 ++ 3 files changed, 195 insertions(+), 77 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 1594dcd897e2..7a4e7709d71b 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -35,9 +35,10 @@ use numpy::{IntoPyArray, ToPyArray}; use numpy::{PyArray2, PyArrayLike2, PyReadonlyArray1, PyReadonlyArray2}; use pyo3::exceptions::PyValueError; +use pyo3::intern; use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; -use pyo3::types::PyList; +use pyo3::types::{PyList, PyTuple, PyType}; use crate::convert_2q_block_matrix::change_basis; use crate::euler_one_qubit_decomposer::{ @@ -2397,9 +2398,9 @@ pub fn local_equivalence(weyl: PyReadonlyArray1) -> PyResult<[f64; 3]> { /// invert 1q gate sequence fn invert_1q_gate(gate: (StandardGate, SmallVec<[f64; 3]>)) -> (StandardGate, SmallVec<[f64; 3]>) { let gate_params = gate.1.into_iter().map(Param::Float).collect::>(); - let Some(inv_gate) = gate - .0 - .inverse(&gate_params) else {panic!()}; + let Some(inv_gate) = gate.0.inverse(&gate_params) else { + panic!() + }; let inv_gate_params = inv_gate .1 .into_iter() @@ -2411,29 +2412,30 @@ fn invert_1q_gate(gate: (StandardGate, SmallVec<[f64; 3]>)) -> (StandardGate, Sm (inv_gate.0, inv_gate_params) } -/// invert 2q gate sequence -fn invert_2q_gate( - gate: (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>), -) -> (StandardGate, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>) { - let Some(inv_gate) = gate - .0 - .unwrap() - .inverse(&gate.1.into_iter().map(Param::Float).collect::>()) else {panic!()}; - let inv_gate_params = inv_gate - .1 - .into_iter() - .map(|param| match param { - Param::Float(val) => val, - _ => panic!(), - }) - .collect::>(); - (inv_gate.0, inv_gate_params, gate.2) +#[derive(Clone, Debug, FromPyObject)] +pub enum RXXEquivalent { + Standard(StandardGate), + CustomPython(Py), +} + +impl RXXEquivalent { + fn matrix(&self, py: Python, param: f64) -> PyResult> { + match self { + Self::Standard(gate) => Ok(gate.matrix(&[Param::Float(param)]).unwrap()), + Self::CustomPython(gate_cls) => { + 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 { - #[pyo3(get)] - rxx_equivalent_gate: StandardGate, + rxx_equivalent_gate: RXXEquivalent, #[pyo3(get)] scale: f64, } @@ -2444,6 +2446,68 @@ const DEFAULT_ATOL: f64 = 1e-12; /// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` /// gate that is locally equivalent to an :class:`.RXXGate`. impl TwoQubitControlledUDecomposer { + /// invert 2q gate sequence + fn invert_2q_gate( + &self, + py: Python, + gate: (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>), + ) -> PyResult<(Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>)> { + let (gate, params, qubits) = gate; + if let Some(gate) = gate { + let inv_gate = gate + .inverse(¶ms.into_iter().map(Param::Float).collect::>()) + .unwrap(); + let inv_gate_params = inv_gate + .1 + .into_iter() + .map(|param| match param { + Param::Float(val) => val, + _ => panic!(), + }) + .collect::>(); + Ok((Some(inv_gate.0), inv_gate_params, qubits)) + } else { + match &self.rxx_equivalent_gate { + RXXEquivalent::Standard(gate) => { + let inv_gate = gate + .inverse(¶ms.into_iter().map(Param::Float).collect::>()) + .unwrap(); + let inv_gate_params = inv_gate + .1 + .into_iter() + .map(|param| match param { + Param::Float(val) => val, + _ => panic!(), + }) + .collect::>(); + Ok((Some(inv_gate.0), inv_gate_params, qubits)) + } + RXXEquivalent::CustomPython(gate_cls) => { + 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()?; + let params: SmallVec<[f64; 3]> = inverse + .params + .into_iter() + .map(|x| match x { + Param::Float(val) => val, + _ => panic!("Inverse has invalid parameter"), + }) + .collect(); + if let Some(gate) = inverse.operation.try_standard_gate() { + return Ok((Some(gate), params, qubits)); + } else if raw_inverse.is_instance(gate_cls.bind(py))? { + return Ok((None, params, qubits)); + } else { + return Err(QiskitError::new_err( + "rxx gate inverse is not valid for this decomposer", + )); + } + } + } + } + } + /// Takes an angle and returns the circuit equivalent to an RXXGate with the /// RXX equivalent gate as the two-qubit unitary. /// Args: @@ -2452,17 +2516,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, angle: f64) -> PyResult { + fn to_rxx_gate(&self, py: Python, 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(&[Param::Float(self.scale * angle)]) - .unwrap(); + let mat = self.rxx_equivalent_gate.matrix(py, self.scale * angle)?; let decomposer_inv = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; @@ -2502,11 +2563,7 @@ impl TwoQubitControlledUDecomposer { gates.push((Some(inv_gate_name), inv_gate_params, smallvec![1])); } } - gates.push(( - Some(self.rxx_equivalent_gate), - smallvec![self.scale * angle], - smallvec![0, 1], - )); + gates.push((None, smallvec![self.scale * angle], smallvec![0, 1])); if let Some(unitary_k1r) = unitary_k1r { global_phase += unitary_k1r.global_phase; @@ -2532,17 +2589,18 @@ 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(-2.0 * target_decomposed.a)?; + let circ_a = self.to_rxx_gate(py, -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(-2.0 * target_decomposed.b)?; + let circ_b = self.to_rxx_gate(py, -2.0 * target_decomposed.b)?; global_phase += circ_b.global_phase; circ.gates .push((Some(StandardGate::SdgGate), smallvec![], smallvec![0])); @@ -2564,7 +2622,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(gamma)?; + let circ_c = self.to_rxx_gate(py, gamma)?; global_phase += circ_c.global_phase; circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); @@ -2578,16 +2636,17 @@ impl TwoQubitControlledUDecomposer { } else { // invert the circuit above gamma *= -1.0; - let circ_c = self.to_rxx_gate(gamma)?; + let circ_c = self.to_rxx_gate(py, gamma)?; global_phase -= circ_c.global_phase; circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); circ.gates .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) = invert_2q_gate(gate); + let (inv_gate_name, inv_gate_params, inv_gate_qubits) = + self.invert_2q_gate(py, gate)?; circ.gates - .push((Some(inv_gate_name), inv_gate_params, inv_gate_qubits)); + .push((inv_gate_name, inv_gate_params, inv_gate_qubits)); } circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); @@ -2604,6 +2663,7 @@ impl TwoQubitControlledUDecomposer { /// Note: atol is passed to OneQubitEulerDecomposer. fn call_inner( &self, + py: Python, unitary: ArrayView2, atol: f64, ) -> PyResult { @@ -2647,7 +2707,7 @@ impl TwoQubitControlledUDecomposer { gates, global_phase, }; - self.weyl_gate(&mut gates1, target_decomposed, atol)?; + self.weyl_gate(py, &mut gates1, target_decomposed, atol)?; global_phase += gates1.global_phase; if let Some(unitary_c1r) = unitary_c1r { @@ -2678,21 +2738,32 @@ impl TwoQubitControlledUDecomposer { /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. #[new] #[pyo3(signature=(rxx_equivalent_gate))] - pub fn new(rxx_equivalent_gate: StandardGate) -> PyResult { + pub fn new(py: Python, rxx_equivalent_gate: RXXEquivalent) -> PyResult { let atol = DEFAULT_ATOL; let test_angles = [0.2, 0.3, PI2]; let scales: PyResult> = test_angles .into_iter() .map(|test_angle| { - if rxx_equivalent_gate.num_params() != 1 { - return Err(QiskitError::new_err( - "Equivalent gate needs to take exactly 1 angle parameter.", - )); - } - let mat = rxx_equivalent_gate - .matrix(&[Param::Float(test_angle)]) - .unwrap(); + match &rxx_equivalent_gate { + RXXEquivalent::Standard(gate) => { + if gate.num_params() != 1 { + return Err(QiskitError::new_err( + "Equivalent gate needs to take exactly 1 angle parameter.", + )); + } + } + RXXEquivalent::CustomPython(gate_cls) => { + println!("gate obj {:?}", gate_cls.bind(py).str()); + gate_cls.bind(py).call1((test_angle,))?; + // if gate_cls.bind(py).call1((test_angle,)){ + // return Err(QiskitError::new_err( + // "Equivalent gate needs to take exactly 1 angle parameter.", + // )); + // } + } + }; + let mat = rxx_equivalent_gate.matrix(py, test_angle)?; let decomp = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; let mat_rxx = StandardGate::RXXGate @@ -2710,11 +2781,9 @@ impl TwoQubitControlledUDecomposer { )?; let scale_a = decomposer_rxx.a / decomposer_equiv.a; if (decomp.a * 2.0 - test_angle / scale_a).abs() > atol { - return Err(QiskitError::new_err(format!( - "The gate {} - is not equivalent to an RXXGate.", - rxx_equivalent_gate.name() - ))); + return Err(QiskitError::new_err( + "The provided gate is not equivalent to an RXXGate.", + )); } Ok(scale_a) }) @@ -2745,29 +2814,58 @@ impl TwoQubitControlledUDecomposer { unitary: PyReadonlyArray2, atol: f64, ) -> PyResult { - let mut gate_sequence = CircuitData::with_capacity(py, 2, 0, 59, Param::Float(0.))?; - let gates_list = self.call_inner(unitary.as_array(), atol)?; - let global_phase: f64 = gates_list.global_phase; - for gate in gates_list.gates { - if let Some(gate_name) = gate.0 { - let qubits = gate.2; - if qubits.len() == 1 { - gate_sequence.push_standard_gate( - gate_name, - &gate.1.into_iter().map(Param::Float).collect::>(), - &[Qubit(qubits[0] as u32)], - )? - } else { - gate_sequence.push_standard_gate( - gate_name, - &gate.1.into_iter().map(Param::Float).collect::>(), - &[Qubit(qubits[0] as u32), Qubit(qubits[1] as u32)], - )? - } - } + let sequence = self.call_inner(py, unitary.as_array(), atol)?; + match &self.rxx_equivalent_gate { + RXXEquivalent::Standard(rxx_gate) => CircuitData::from_standard_gates( + py, + 2, + sequence + .gates + .into_iter() + .map(|(gate, params, qubits)| match gate { + Some(gate) => ( + gate, + params.into_iter().map(Param::Float).collect(), + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + ), + None => ( + *rxx_gate, + params.into_iter().map(Param::Float).collect(), + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + ), + }), + Param::Float(sequence.global_phase), + ), + RXXEquivalent::CustomPython(gate_cls) => CircuitData::from_packed_operations( + py, + 2, + 0, + sequence + .gates + .into_iter() + .map(|(gate, params, qubits)| match gate { + Some(gate) => Ok(( + PackedOperation::from_standard(gate), + params.into_iter().map(Param::Float).collect(), + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + Vec::new(), + )), + None => { + let raw_gate_obj = + gate_cls.bind(py).call1(PyTuple::new_bound(py, params))?; + let op: OperationFromPython = raw_gate_obj.extract()?; + + Ok(( + op.operation, + op.params, + qubits.into_iter().map(|x| Qubit(x.into())).collect(), + Vec::new(), + )) + } + }), + Param::Float(sequence.global_phase), + ), } - gate_sequence.set_global_phase(py, Param::Float(global_phase))?; - Ok(gate_sequence) } } diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 4df611167378..79a444e6220c 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -284,7 +284,9 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): rxx_equivalent_gate._standard_gate ) else: - raise QiskitError("Must be initialized with a standard gate object") + self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( + rxx_equivalent_gate + ) self.rxx_equivalent_gate = rxx_equivalent_gate self.scale = self._inner_decomposition.scale diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index b08240197211..843273a6b5df 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -1440,6 +1440,24 @@ def test_not_rxx_equivalent(self): "Equivalent gate needs to take exactly 1 angle parameter.", exc.exception.message ) + @combine(seed=range(10), name="seed_{seed}") + def test_correct_unitary_custom_gate(self, seed): + """Test synthesis with a custom controlled u equivalent gate.""" + unitary = random_unitary(4, seed=seed) + + class CustomXXGate(RXXGate): + """Custom RXXGate subclass that's not a standard gate""" + _standard_gate = None + + def __init__(self, theta, label=None): + super().__init__(theta, label) + self.name = "MyCustomXXGate" + + decomposer = TwoQubitControlledUDecomposer(CustomXXGate) + circ = decomposer(unitary) + self.assertEqual(Operator(unitary), Operator(circ)) + + class TestDecomposeProductRaises(QiskitTestCase): """Check that exceptions are raised when 2q matrix is not a product of 1q unitaries""" From 2e57608dad7d044c08f08fca89c981de4c778723 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 29 Oct 2024 17:47:25 -0400 Subject: [PATCH 20/27] Remove bare panics --- crates/accelerate/src/two_qubit_decompose.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 7a4e7709d71b..bad48638a63d 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2398,15 +2398,16 @@ pub fn local_equivalence(weyl: PyReadonlyArray1) -> PyResult<[f64; 3]> { /// invert 1q gate sequence fn invert_1q_gate(gate: (StandardGate, SmallVec<[f64; 3]>)) -> (StandardGate, SmallVec<[f64; 3]>) { let gate_params = gate.1.into_iter().map(Param::Float).collect::>(); - let Some(inv_gate) = gate.0.inverse(&gate_params) else { - panic!() - }; + let inv_gate = gate + .0 + .inverse(&gate_params) + .expect("An unexpected standard gate was inverted"); let inv_gate_params = inv_gate .1 .into_iter() .map(|param| match param { Param::Float(val) => val, - _ => panic!(), + _ => unreachable!("Parameterized inverse generated from non-parameterized gate."), }) .collect::>(); (inv_gate.0, inv_gate_params) @@ -2462,7 +2463,9 @@ impl TwoQubitControlledUDecomposer { .into_iter() .map(|param| match param { Param::Float(val) => val, - _ => panic!(), + _ => { + unreachable!("Parameterized inverse generated from non-parameterized gate.") + } }) .collect::>(); Ok((Some(inv_gate.0), inv_gate_params, qubits)) @@ -2477,7 +2480,9 @@ impl TwoQubitControlledUDecomposer { .into_iter() .map(|param| match param { Param::Float(val) => val, - _ => panic!(), + _ => unreachable!( + "Parameterized inverse generated from non-parameterized gate." + ), }) .collect::>(); Ok((Some(inv_gate.0), inv_gate_params, qubits)) From 7e0c2a248a7d8436b9d865b84f65cc01e17a7305 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 29 Oct 2024 17:51:12 -0400 Subject: [PATCH 21/27] Undo debug testing for invalid custom gate check --- crates/accelerate/src/two_qubit_decompose.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index bad48638a63d..4819ba2e7c00 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2759,13 +2759,11 @@ impl TwoQubitControlledUDecomposer { } } RXXEquivalent::CustomPython(gate_cls) => { - println!("gate obj {:?}", gate_cls.bind(py).str()); - gate_cls.bind(py).call1((test_angle,))?; - // if gate_cls.bind(py).call1((test_angle,)){ - // return Err(QiskitError::new_err( - // "Equivalent gate needs to take exactly 1 angle parameter.", - // )); - // } + if gate_cls.bind(py).call1((test_angle,)).ok().is_none() { + return Err(QiskitError::new_err( + "Equivalent gate needs to take exactly 1 angle parameter.", + )); + } } }; let mat = rxx_equivalent_gate.matrix(py, test_angle)?; From 7769833af90872582fc1fff0ca7cd87c685c5206 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 29 Oct 2024 17:52:56 -0400 Subject: [PATCH 22/27] Fix lint --- crates/accelerate/src/two_qubit_decompose.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 4819ba2e7c00..17437ef841fa 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2442,6 +2442,7 @@ pub struct TwoQubitControlledUDecomposer { } const DEFAULT_ATOL: f64 = 1e-12; +type InverseReturn = (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>); /// Decompose two-qubit unitary in terms of a desired /// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` @@ -2452,7 +2453,7 @@ impl TwoQubitControlledUDecomposer { &self, py: Python, gate: (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>), - ) -> PyResult<(Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>)> { + ) -> PyResult { let (gate, params, qubits) = gate; if let Some(gate) = gate { let inv_gate = gate @@ -2500,13 +2501,13 @@ impl TwoQubitControlledUDecomposer { }) .collect(); if let Some(gate) = inverse.operation.try_standard_gate() { - return Ok((Some(gate), params, qubits)); + Ok((Some(gate), params, qubits)) } else if raw_inverse.is_instance(gate_cls.bind(py))? { - return Ok((None, params, qubits)); + Ok((None, params, qubits)) } else { - return Err(QiskitError::new_err( + Err(QiskitError::new_err( "rxx gate inverse is not valid for this decomposer", - )); + )) } } } From af17ab80409301a5d7d65a64602ff68a9a8fe1ff Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 30 Oct 2024 05:14:55 -0400 Subject: [PATCH 23/27] Fix python lint --- test/python/synthesis/test_synthesis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index 843273a6b5df..bb639cc188a0 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -1447,6 +1447,7 @@ def test_correct_unitary_custom_gate(self, seed): class CustomXXGate(RXXGate): """Custom RXXGate subclass that's not a standard gate""" + _standard_gate = None def __init__(self, theta, label=None): @@ -1458,7 +1459,6 @@ def __init__(self, theta, label=None): self.assertEqual(Operator(unitary), Operator(circ)) - class TestDecomposeProductRaises(QiskitTestCase): """Check that exceptions are raised when 2q matrix is not a product of 1q unitaries""" From 06c3da4c6e44e5c19ed85d78575961b2809c1c5e Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 12 Nov 2024 08:43:51 -0600 Subject: [PATCH 24/27] add default fidelity value --- crates/accelerate/src/two_qubit_decompose.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 17437ef841fa..731cb82b6a54 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2780,7 +2780,7 @@ impl TwoQubitControlledUDecomposer { )?; let decomposer_equiv = TwoQubitWeylDecomposition::new_inner( mat.view(), - None, + Some(DEFAULT_FIDELITY), Some(Specialization::ControlledEquiv), )?; let scale_a = decomposer_rxx.a / decomposer_equiv.a; From ee3e88ec951598d410eaee8de8975aa8cad3d700 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 12 Nov 2024 08:44:09 -0600 Subject: [PATCH 25/27] add more gates for the tests --- test/python/synthesis/test_synthesis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index bb639cc188a0..4ebe5f791e57 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -46,6 +46,8 @@ RZXGate, CPhaseGate, CRZGate, + CRXGate, + CRYGate, RXGate, RYGate, RZGate, @@ -1426,7 +1428,7 @@ class TestTwoQubitControlledUDecompose(CheckDecompositions): def test_correct_unitary(self, seed): """Verify unitary for different gates in the decomposition""" unitary = random_unitary(4, seed=seed) - for gate in [RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate]: + for gate in [RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate]: decomposer = TwoQubitControlledUDecomposer(gate) circ = decomposer(unitary) self.assertEqual(Operator(unitary), Operator(circ)) From 1bb06160ee2fccbafe97a6a9d1ef282445b7a5de Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 12 Nov 2024 10:22:39 -0600 Subject: [PATCH 26/27] assert that CustomXYGate raises an error --- test/python/synthesis/test_synthesis.py | 33 ++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index 4ebe5f791e57..e3fb72f5595e 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -16,6 +16,7 @@ import unittest import contextlib import logging +import math import numpy as np import scipy import scipy.stats @@ -23,7 +24,8 @@ from qiskit import QiskitError, transpile from qiskit.dagcircuit.dagcircuit import DAGCircuit -from qiskit.circuit import QuantumCircuit, QuantumRegister +from qiskit.circuit import QuantumCircuit, QuantumRegister, Gate +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.converters import dag_to_circuit, circuit_to_dag from qiskit.circuit.library import ( HGate, @@ -1460,6 +1462,35 @@ def __init__(self, theta, label=None): circ = decomposer(unitary) self.assertEqual(Operator(unitary), Operator(circ)) + def test_unitary_custom_gate_raises(self): + """Test that a custom gate raises an exception, as it's not equivalent to an RXX gate""" + + class CustomXYGate(Gate): + """Custom Gate subclass that's not a standard gate and not RXX equivalent""" + + _standard_gate = None + + def __init__(self, theta: ParameterValueType, label=None): + """Create new custom rotstion XY gate.""" + super().__init__("MyCustomXYGate", 2, [theta]) + + def __array__(self, dtype=None): + """Return a Numpy.array for the custom gate.""" + theta = self.params[0] + cos = math.cos(theta) + isin = 1j * math.sin(theta) + return np.array( + [[1, 0, 0, 0], [0, cos, -isin, 0], [0, -isin, cos, 0], [0, 0, 0, 1]], + dtype=dtype, + ) + + def inverse(self, annotated: bool = False): + return CustomXYGate(-self.params[0]) + + with self.assertRaises(QiskitError) as exc: + _ = TwoQubitControlledUDecomposer(CustomXYGate) + self.assertIn("Specialization: ControlledEquiv calculated fidelity", exc.exception.message) + class TestDecomposeProductRaises(QiskitTestCase): """Check that exceptions are raised when 2q matrix is not a product of 1q unitaries""" From 8949f7f4c3a0c02318f368f82eea3d901aa5ac26 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Wed, 13 Nov 2024 00:55:00 -0600 Subject: [PATCH 27/27] update assertion in test --- test/python/synthesis/test_synthesis.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index e3fb72f5595e..82557d9dd391 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -1487,9 +1487,8 @@ def __array__(self, dtype=None): def inverse(self, annotated: bool = False): return CustomXYGate(-self.params[0]) - with self.assertRaises(QiskitError) as exc: - _ = TwoQubitControlledUDecomposer(CustomXYGate) - self.assertIn("Specialization: ControlledEquiv calculated fidelity", exc.exception.message) + with self.assertRaisesRegex(QiskitError, "ControlledEquiv calculated fidelity"): + TwoQubitControlledUDecomposer(CustomXYGate) class TestDecomposeProductRaises(QiskitTestCase):