diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index fb8c58baab9d..731cb82b6a54 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::{ @@ -54,7 +55,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}; @@ -2394,6 +2395,484 @@ 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 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, + _ => unreachable!("Parameterized inverse generated from non-parameterized gate."), + }) + .collect::>(); + (inv_gate.0, inv_gate_params) +} + +#[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 { + rxx_equivalent_gate: RXXEquivalent, + #[pyo3(get)] + scale: f64, +} + +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}` +/// 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 { + 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, + _ => { + unreachable!("Parameterized inverse generated from non-parameterized gate.") + } + }) + .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, + _ => unreachable!( + "Parameterized inverse generated from non-parameterized gate." + ), + }) + .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() { + Ok((Some(gate), params, qubits)) + } else if raw_inverse.is_instance(gate_cls.bind(py))? { + Ok((None, params, qubits)) + } else { + 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: + /// 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, 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(py, self.scale * angle)?; + 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); + + // 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; + + 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 { + 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])); + } + } + gates.push((None, smallvec![self.scale * angle], smallvec![0, 1])); + + 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])); + } + } + + Ok(TwoQubitGateSequence { + gates, + global_phase, + }) + } + + /// 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)?; + 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)?; + global_phase += circ_b.global_phase; + 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])); + } + + // # 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(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])); + 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 + gamma *= -1.0; + 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) = + self.invert_2q_gate(py, gate)?; + circ.gates + .push((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])); + } + } + + circ.global_phase = global_phase; + Ok(()) + } + + /// Returns the Weyl decomposition in circuit form. + /// Note: atol is passed to OneQubitEulerDecomposer. + fn call_inner( + &self, + py: Python, + 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::with_capacity(59); + 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])); + } + } + let mut gates1 = TwoQubitGateSequence { + gates, + global_phase, + }; + self.weyl_gate(py, &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) + } +} + +#[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`. + #[new] + #[pyo3(signature=(rxx_equivalent_gate))] + 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| { + 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) => { + 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)?; + 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(), + Some(DEFAULT_FIDELITY), + 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( + "The provided gate is not equivalent to an RXXGate.", + )); + } + 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 { + 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, + }) + } + + #[pyo3(signature=(unitary, atol))] + fn __call__( + &self, + py: Python, + unitary: PyReadonlyArray2, + atol: f64, + ) -> PyResult { + 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), + ), + } + } +} + 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))?; @@ -2407,5 +2886,6 @@ pub fn two_qubit_decompose(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index d4c7702da35a..79a444e6220c 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 @@ -280,159 +279,27 @@ 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 _ - decomp = TwoQubitWeylDecomposition(rxx_equivalent_gate(test_angle)) - - circ = QuantumCircuit(2) - circ.rxx(test_angle, 0, 1) - decomposer_rxx = TwoQubitWeylDecomposition( - Operator(circ).data, - fidelity=None, - _specialization=two_qubit_decompose.Specialization.ControlledEquiv, + if rxx_equivalent_gate._standard_gate is not None: + self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( + rxx_equivalent_gate._standard_gate ) - - circ = QuantumCircuit(2) - circ.append(rxx_equivalent_gate(test_angle), qargs=[0, 1]) - decomposer_equiv = TwoQubitWeylDecomposition( - Operator(circ).data, - 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." + else: + self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( + rxx_equivalent_gate ) - - self.scale = scales[0] - self.rxx_equivalent_gate = 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. - - Note: atol ist passed to OneQubitEulerDecomposer. - """ - - # pylint: disable=attribute-defined-outside-init - self.decomposer = 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, - ) - ) - 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) - + unitary (Operator or ndarray): :math:`4 \times 4` unitary to synthesize. Returns: - Circuit: Circuit equivalent to an RXXGate. - - Raises: - QiskitError: If the circuit is not equivalent to an RXXGate. + QuantumCircuit: Synthesized quantum circuit. + Note: atol is passed to OneQubitEulerDecomposer. """ - - # 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]) - decomposer_inv = TwoQubitWeylDecomposition(Operator(circ).data) - - 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) - - 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: diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index b08240197211..82557d9dd391 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, @@ -46,6 +48,8 @@ RZXGate, CPhaseGate, CRZGate, + CRXGate, + CRYGate, RXGate, RYGate, RZGate, @@ -1426,7 +1430,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)) @@ -1440,6 +1444,52 @@ 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)) + + 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.assertRaisesRegex(QiskitError, "ControlledEquiv calculated fidelity"): + TwoQubitControlledUDecomposer(CustomXYGate) + class TestDecomposeProductRaises(QiskitTestCase): """Check that exceptions are raised when 2q matrix is not a product of 1q unitaries"""