diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index de20430d25bf..5ce6d248c4eb 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -54,7 +54,9 @@ 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::gate_matrix::{ + CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SDG_GATE, SX_GATE, S_GATE, X_GATE, +}; use qiskit_circuit::operations::{Operation, Param, StandardGate}; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; @@ -2448,23 +2450,25 @@ pub enum RXXEquivalent { } impl RXXEquivalent { - fn matrix(&self, py: Python, param: f64) -> PyResult> { + fn matrix(&self, param: f64) -> PyResult> { match self { Self::Standard(gate) => Ok(gate.matrix(&[Param::Float(param)]).unwrap()), - Self::CustomPython(gate_cls) => { + Self::CustomPython(gate_cls) => Python::with_gil(|py: Python| { let gate_obj = gate_cls.bind(py).call1((param,))?; let raw_matrix = gate_obj .call_method0(intern!(py, "to_matrix"))? .extract::>()?; Ok(raw_matrix.as_array().to_owned()) - } + }), } } } +#[derive(Clone, Debug)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitControlledUDecomposer { rxx_equivalent_gate: RXXEquivalent, + euler_basis: EulerBasis, #[pyo3(get)] scale: f64, } @@ -2479,7 +2483,6 @@ impl TwoQubitControlledUDecomposer { /// invert 2q gate sequence fn invert_2q_gate( &self, - py: Python, gate: (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>), ) -> PyResult { let (gate, params, qubits) = gate; @@ -2516,7 +2519,7 @@ impl TwoQubitControlledUDecomposer { .collect::>(); Ok((Some(inv_gate.0), inv_gate_params, qubits)) } - RXXEquivalent::CustomPython(gate_cls) => { + RXXEquivalent::CustomPython(gate_cls) => Python::with_gil(|py: Python| { let gate_obj = gate_cls.bind(py).call1(PyTuple::new(py, params)?)?; let raw_inverse = gate_obj.call_method0(intern!(py, "inverse"))?; let inverse: OperationFromPython = raw_inverse.extract()?; @@ -2537,7 +2540,7 @@ impl TwoQubitControlledUDecomposer { "rxx gate inverse is not valid for this decomposer", )) } - } + }), } } } @@ -2550,20 +2553,19 @@ impl TwoQubitControlledUDecomposer { /// Circuit: Circuit equivalent to an RXXGate. /// Raises: /// QiskitError: If the circuit is not equivalent to an RXXGate. - fn to_rxx_gate(&self, py: Python, angle: f64) -> PyResult { + fn to_rxx_gate(&self, angle: f64) -> PyResult { // The user-provided RXXGate equivalent gate may be locally equivalent to the RXXGate // but with some scaling in the rotation angle. For example, RXXGate(angle) has Weyl // parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e. // :code:`self.rxx_equivalent_gate(angle)` might produce the Weyl parameters // (scale * angle, 0, 0) where scale != 1. This is the case for the CPhaseGate. - let mat = self.rxx_equivalent_gate.matrix(py, self.scale * angle)?; + let mat = self.rxx_equivalent_gate.matrix(self.scale * angle)?; let decomposer_inv = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; - let euler_basis = EulerBasis::ZYZ; let mut target_1q_basis_list = EulerBasisSet::new(); - target_1q_basis_list.add_basis(euler_basis); + target_1q_basis_list.add_basis(self.euler_basis); // Express the RXXGate in terms of the user-provided RXXGate equivalent gate. let mut gates = Vec::with_capacity(13); @@ -2600,14 +2602,14 @@ impl TwoQubitControlledUDecomposer { gates.push((None, smallvec![self.scale * angle], smallvec![0, 1])); if let Some(unitary_k1r) = unitary_k1r { - global_phase += unitary_k1r.global_phase; + 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; + 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])); @@ -2623,28 +2625,65 @@ impl TwoQubitControlledUDecomposer { /// Appends U_d(a, b, c) to the circuit. fn weyl_gate( &self, - py: Python, circ: &mut TwoQubitGateSequence, target_decomposed: TwoQubitWeylDecomposition, atol: f64, ) -> PyResult<()> { - let circ_a = self.to_rxx_gate(py, -2.0 * target_decomposed.a)?; + let circ_a = self.to_rxx_gate(-2.0 * target_decomposed.a)?; circ.gates.extend(circ_a.gates); let mut global_phase = circ_a.global_phase; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(self.euler_basis); + + let s_decomp = unitary_to_gate_sequence_inner( + aview2(&S_GATE), + &target_1q_basis_list, + 0, + None, + true, + None, + ); + let sdg_decomp = unitary_to_gate_sequence_inner( + aview2(&SDG_GATE), + &target_1q_basis_list, + 0, + None, + true, + None, + ); + let h_decomp = unitary_to_gate_sequence_inner( + aview2(&H_GATE), + &target_1q_basis_list, + 0, + None, + true, + None, + ); + // translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate. if (target_decomposed.b).abs() > atol { - let circ_b = self.to_rxx_gate(py, -2.0 * target_decomposed.b)?; + let circ_b = self.to_rxx_gate(-2.0 * target_decomposed.b)?; global_phase += circ_b.global_phase; - circ.gates - .push((Some(StandardGate::SdgGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::SdgGate), smallvec![], smallvec![1])); + if let Some(sdg_decomp) = sdg_decomp { + global_phase += 2.0 * sdg_decomp.global_phase; + for gate in sdg_decomp.gates.into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, 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 let Some(s_decomp) = s_decomp { + global_phase += 2.0 * s_decomp.global_phase; + for gate in s_decomp.gates.into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } } // # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. @@ -2656,36 +2695,57 @@ impl TwoQubitControlledUDecomposer { // circuit if c < 0. let mut gamma = -2.0 * target_decomposed.c; if gamma <= 0.0 { - let circ_c = self.to_rxx_gate(py, gamma)?; + let circ_c = self.to_rxx_gate(gamma)?; global_phase += circ_c.global_phase; - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, 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])); + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } } else { // invert the circuit above gamma *= -1.0; - let circ_c = self.to_rxx_gate(py, gamma)?; + let circ_c = self.to_rxx_gate(gamma)?; global_phase -= circ_c.global_phase; - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } for gate in circ_c.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params, inv_gate_qubits) = - self.invert_2q_gate(py, gate)?; + self.invert_2q_gate(gate)?; circ.gates .push((inv_gate_name, inv_gate_params, inv_gate_qubits)); } - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } } } @@ -2695,18 +2755,16 @@ impl TwoQubitControlledUDecomposer { /// Returns the Weyl decomposition in circuit form. /// Note: atol is passed to OneQubitEulerDecomposer. - fn call_inner( + pub fn call_inner( &self, - py: Python, unitary: ArrayView2, - atol: f64, + atol: Option, ) -> 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); + target_1q_basis_list.add_basis(self.euler_basis); let c1r = target_decomposed.K1r.view(); let c2r = target_decomposed.K2r.view(); @@ -2741,17 +2799,17 @@ impl TwoQubitControlledUDecomposer { gates, global_phase, }; - self.weyl_gate(py, &mut gates1, target_decomposed, atol)?; + self.weyl_gate(&mut gates1, target_decomposed, atol.unwrap_or(DEFAULT_ATOL))?; global_phase += gates1.global_phase; if let Some(unitary_c1r) = unitary_c1r { - global_phase -= unitary_c1r.global_phase; + 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; + global_phase += unitary_c1l.global_phase; for gate in unitary_c1l.gates.into_iter() { gates1.gates.push((Some(gate.0), gate.1, smallvec![1])); } @@ -2760,19 +2818,9 @@ 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`. - #[new] - #[pyo3(signature=(rxx_equivalent_gate))] - pub fn new(py: Python, rxx_equivalent_gate: RXXEquivalent) -> PyResult { + /// Initialize the KAK decomposition. + pub fn new_inner(rxx_equivalent_gate: RXXEquivalent, euler_basis: &str) -> PyResult { let atol = DEFAULT_ATOL; let test_angles = [0.2, 0.3, PI2]; @@ -2788,14 +2836,17 @@ impl TwoQubitControlledUDecomposer { } } RXXEquivalent::CustomPython(gate_cls) => { - if gate_cls.bind(py).call1((test_angle,)).ok().is_none() { + let takes_param = Python::with_gil(|py: Python| { + gate_cls.bind(py).call1((test_angle,)).ok().is_none() + }); + if takes_param { return Err(QiskitError::new_err( "Equivalent gate needs to take exactly 1 angle parameter.", )); } } }; - let mat = rxx_equivalent_gate.matrix(py, test_angle)?; + let mat = rxx_equivalent_gate.matrix(test_angle)?; let decomp = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; let mat_rxx = StandardGate::RXXGate @@ -2836,17 +2887,35 @@ impl TwoQubitControlledUDecomposer { Ok(TwoQubitControlledUDecomposer { scale, rxx_equivalent_gate, + euler_basis: EulerBasis::__new__(euler_basis)?, }) } +} - #[pyo3(signature=(unitary, atol))] +#[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. + /// euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer` + /// for 1Q synthesis. + /// Raises: + /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. + #[new] + #[pyo3(signature=(rxx_equivalent_gate, euler_basis="ZXZ"))] + pub fn new(rxx_equivalent_gate: RXXEquivalent, euler_basis: &str) -> PyResult { + TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, euler_basis) + } + + #[pyo3(signature=(unitary, atol=None))] fn __call__( &self, py: Python, unitary: PyReadonlyArray2, - atol: f64, + atol: Option, ) -> PyResult { - let sequence = self.call_inner(py, unitary.as_array(), atol)?; + let sequence = self.call_inner(unitary.as_array(), atol)?; match &self.rxx_equivalent_gate { RXXEquivalent::Standard(rxx_gate) => CircuitData::from_standard_gates( py, diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 2721c58c9237..b36715bd2cf9 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -27,14 +27,14 @@ use smallvec::{smallvec, SmallVec}; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::{IntoPyDict, PyDict, PyString}; +use pyo3::types::{IntoPyDict, PyDict, PyString, PyType}; use pyo3::wrap_pyfunction; use pyo3::Python; use qiskit_circuit::converters::{circuit_to_dag, QuantumCircuitData}; use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; use qiskit_circuit::imports; -use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; +use qiskit_circuit::operations::{Operation, OperationRef, Param, PyGate, StandardGate}; use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation}; use qiskit_circuit::Qubit; @@ -44,7 +44,8 @@ use crate::euler_one_qubit_decomposer::{ use crate::nlayout::PhysicalQubit; use crate::target_transpiler::{NormalOperation, Target}; use crate::two_qubit_decompose::{ - TwoQubitBasisDecomposer, TwoQubitGateSequence, TwoQubitWeylDecomposition, + RXXEquivalent, TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer, TwoQubitGateSequence, + TwoQubitWeylDecomposition, }; use crate::QiskitError; @@ -53,10 +54,12 @@ const PI4: f64 = PI / 4.; #[derive(Clone, Debug)] enum DecomposerType { - TwoQubitBasisDecomposer(Box), - XXDecomposer(PyObject), + TwoQubitBasis(Box), + TwoQubitControlledU(Box), + XX(PyObject), } +#[derive(Clone, Debug)] struct DecomposerElement { decomposer: DecomposerType, gate: NormalOperation, @@ -72,6 +75,7 @@ struct TwoQubitUnitarySequence { // then we know TwoQubitBasisDecomposer is an ideal decomposition and there is // no need to bother trying the XXDecomposer. static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; +static PARAM_SET: [&str; 8] = ["rzz", "rxx", "ryy", "rzx", "crx", "cry", "crz", "cphase"]; fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet { let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); @@ -133,17 +137,56 @@ fn apply_synth_sequence( ) -> PyResult<()> { let mut instructions = Vec::with_capacity(sequence.gate_sequence.gates().len()); for (gate, params, qubit_ids) in sequence.gate_sequence.gates() { - let gate_node = match gate { - None => sequence.decomp_gate.operation.standard_gate(), - Some(gate) => *gate, + let packed_op = match gate { + None => &sequence.decomp_gate.operation, + Some(gate) => &PackedOperation::from_standard_gate(*gate), }; let mapped_qargs: Vec = qubit_ids.iter().map(|id| out_qargs[*id as usize]).collect(); let new_params: Option>> = match gate { Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), - None => Some(Box::new(sequence.decomp_gate.params.clone())), + None => { + if !sequence.decomp_gate.params.is_empty() + && matches!(sequence.decomp_gate.params[0], Param::Float(_)) + { + Some(Box::new(sequence.decomp_gate.params.clone())) + } else { + Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())) + } + } + }; + + let new_op: PackedOperation = match packed_op.py_copy(py)?.view() { + OperationRef::Gate(gate) => { + gate.gate.setattr( + py, + "params", + new_params + .as_deref() + .map(SmallVec::as_slice) + .unwrap_or(&[]) + .iter() + .map(|param| param.clone_ref(py)) + .collect::>(), + )?; + Box::new(PyGate { + gate: gate.gate.clone(), + qubits: gate.qubits, + clbits: gate.clbits, + params: gate.params, + op_name: gate.op_name.clone(), + }) + .into() + } + OperationRef::StandardGate(_) => packed_op.clone(), + _ => { + return Err(QiskitError::new_err( + "Decomposed gate sequence contains unexpected operations.", + )) + } }; + let instruction = PackedInstruction { - op: PackedOperation::from_standard_gate(gate_node), + op: new_op, qubits: out_dag.qargs_interner.insert(&mapped_qargs), clbits: out_dag.cargs_interner.get_default(), params: new_params, @@ -407,8 +450,18 @@ fn run_2q_unitary_synthesis( coupling_edges, target, )?; + match decomposer_item.decomposer { - DecomposerType::TwoQubitBasisDecomposer(_) => { + DecomposerType::TwoQubitBasis(_) => { + let synth = synth_su4_sequence( + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + } + DecomposerType::TwoQubitControlledU(_) => { let synth = synth_su4_sequence( &unitary, decomposer_item, @@ -417,7 +470,7 @@ fn run_2q_unitary_synthesis( )?; apply_synth_sequence(py, out_dag, out_qargs, &synth)?; } - DecomposerType::XXDecomposer(_) => { + DecomposerType::XX(_) => { let synth = synth_su4_dag( py, &unitary, @@ -442,7 +495,34 @@ fn run_2q_unitary_synthesis( target, )?; match &decomposer.decomposer { - DecomposerType::TwoQubitBasisDecomposer(_) => { + DecomposerType::TwoQubitBasis(_) => { + let sequence = + synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; + let scoring_info = + sequence + .gate_sequence + .gates() + .iter() + .map(|(gate, params, qubit_ids)| { + let inst_qubits = + qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); + match gate { + Some(gate) => ( + gate.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + None => ( + sequence.decomp_gate.operation.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + } + }); + let synth_error_from_target = synth_error(py, scoring_info, target); + synth_errors_sequence.push((sequence, synth_error_from_target)); + } + DecomposerType::TwoQubitControlledU(_) => { let sequence = synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; let scoring_info = @@ -460,12 +540,7 @@ fn run_2q_unitary_synthesis( inst_qubits, ), None => ( - sequence - .decomp_gate - .operation - .standard_gate() - .name() - .to_string(), + sequence.decomp_gate.operation.name().to_string(), Some(params.iter().map(|p| Param::Float(*p)).collect()), inst_qubits, ), @@ -474,7 +549,7 @@ fn run_2q_unitary_synthesis( let synth_error_from_target = synth_error(py, scoring_info, target); synth_errors_sequence.push((sequence, synth_error_from_target)); } - DecomposerType::XXDecomposer(_) => { + DecomposerType::XX(_) => { let synth_dag = synth_su4_dag( py, &unitary, @@ -543,6 +618,8 @@ fn get_2q_decomposers_from_target( let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); let mut available_2q_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); let mut available_2q_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); + let mut available_2q_param_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); + let mut available_2q_param_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); let mut qubit_gate_map = IndexMap::new(); @@ -565,28 +642,10 @@ fn get_2q_decomposers_from_target( } #[inline] - fn replace_parametrized_gate(mut op: NormalOperation) -> NormalOperation { - if let Some(std_gate) = op.operation.try_standard_gate() { - match std_gate.name() { - "rxx" => { - if let Param::ParameterExpression(_) = op.params[0] { - op.params[0] = Param::Float(PI2) - } - } - "rzx" => { - if let Param::ParameterExpression(_) = op.params[0] { - op.params[0] = Param::Float(PI4) - } - } - "rzz" => { - if let Param::ParameterExpression(_) = op.params[0] { - op.params[0] = Param::Float(PI2) - } - } - _ => (), - } - } - op + fn check_parametrized_gate(op: &NormalOperation) -> bool { + // The gate counts as parametrized if there is any + // non-float parameter + !op.params.iter().all(|p| matches!(p, Param::Float(_))) } for (q_pair, gates) in qubit_gate_map { @@ -602,8 +661,21 @@ fn get_2q_decomposers_from_target( if op.operation.num_qubits() != 2 { continue; } - available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); - + if check_parametrized_gate(op) { + available_2q_param_basis.insert(key, op.clone()); + if target.contains_key(key) { + available_2q_param_props.insert( + key, + match &target[key].get(Some(q_pair)) { + Some(Some(props)) => (props.duration, props.error), + _ => (None, None), + }, + ); + } else { + continue; + } + } + available_2q_basis.insert(key, op.clone()); if target.contains_key(key) { available_2q_props.insert( key, @@ -620,7 +692,8 @@ fn get_2q_decomposers_from_target( } } } - if available_2q_basis.is_empty() { + + if available_2q_basis.is_empty() && available_2q_param_basis.is_empty() { return Err(QiskitError::new_err( "Target has no gates available on qubits to synthesize over.", )); @@ -655,7 +728,6 @@ fn get_2q_decomposers_from_target( } } - // Iterate over 1q and 2q supercontrolled basis, append TwoQubitBasisDecomposers let supercontrolled_basis: IndexMap<&str, NormalOperation> = available_2q_basis .iter() .filter(|(_, v)| is_supercontrolled(v)) @@ -680,26 +752,67 @@ fn get_2q_decomposers_from_target( )?; decomposers.push(DecomposerElement { - decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), + decomposer: DecomposerType::TwoQubitBasis(Box::new(decomposer)), gate: gate.clone(), }); } } // If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer - // is an ideal decomposition and there is no need to bother calculating the XX embodiments - // or try the XX decomposer + // is an ideal decomposition and there is no need to try other decomposers let available_basis_set: IndexSet<&str> = available_2q_basis.keys().copied().collect(); #[inline] fn check_goodbye(basis_set: &IndexSet<&str>) -> bool { - basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) + !basis_set.is_empty() && basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) } if check_goodbye(&available_basis_set) { return Ok(Some(decomposers)); } + for basis_1q in &available_1q_basis { + for (_basis_2q, gate) in available_2q_param_basis.iter() { + let rxx_equivalent_gate = if let Some(std_gate) = gate.operation.try_standard_gate() { + RXXEquivalent::Standard(std_gate) + } else { + let module = PyModule::import(py, "builtins")?; + let py_type = module.getattr("type")?; + let gate_type = py_type + .call1((gate.clone().into_pyobject(py)?,))? + .downcast_into::()? + .unbind(); + + RXXEquivalent::CustomPython(gate_type) + }; + + match TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, basis_1q) { + Ok(decomposer) => { + decomposers.push(DecomposerElement { + decomposer: DecomposerType::TwoQubitControlledU(Box::new(decomposer)), + gate: gate.clone(), + }); + } + Err(_) => continue, + }; + } + } + + // If our 2q basis gates are a subset of PARAM_SET, then we will use the TwoQubitControlledUDecomposer + // and there is no need to try other decomposers + + let available_basis_param_set: IndexSet<&str> = + available_2q_param_basis.keys().copied().collect(); + + #[inline] + fn check_parametrized_goodbye(basis_set: &IndexSet<&str>) -> bool { + !basis_set.is_empty() && basis_set.iter().all(|gate| PARAM_SET.contains(gate)) + } + + if check_parametrized_goodbye(&available_basis_param_set) { + return Ok(Some(decomposers)); + } + // Let's now look for possible controlled decomposers (i.e. XXDecomposer) let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis .iter() @@ -787,7 +900,7 @@ fn get_2q_decomposers_from_target( .extract::()?; decomposers.push(DecomposerElement { - decomposer: DecomposerType::XXDecomposer(decomposer.into()), + decomposer: DecomposerType::XX(decomposer.into()), gate: decomposer_gate, }); } @@ -887,8 +1000,10 @@ fn synth_su4_sequence( approximation_degree: Option, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + let synth = if let DecomposerType::TwoQubitBasis(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else if let DecomposerType::TwoQubitControlledU(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None)? } else { unreachable!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") }; @@ -947,8 +1062,10 @@ fn reversed_synth_su4_sequence( let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); - let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + let synth = if let DecomposerType::TwoQubitBasis(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else if let DecomposerType::TwoQubitControlledU(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None)? } else { unreachable!( "reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer." @@ -982,7 +1099,7 @@ fn synth_su4_dag( approximation_degree: Option, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let synth_dag = if let DecomposerType::XX(decomposer) = &decomposer_2q.decomposer { let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] .into_iter() .collect(); @@ -1048,7 +1165,7 @@ fn reversed_synth_su4_dag( let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); - let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let synth_dag = if let DecomposerType::XX(decomposer) = &decomposer_2q.decomposer { let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] .into_iter() .collect(); diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index a86ec6681400..4b29c434b72c 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -119,6 +119,7 @@ TwoQubitBasisDecomposer XXDecomposer TwoQubitWeylDecomposition + TwoQubitControlledUDecomposer .. autofunction:: two_qubit_cnot_decompose @@ -200,6 +201,7 @@ TwoQubitBasisDecomposer, two_qubit_cnot_decompose, TwoQubitWeylDecomposition, + TwoQubitControlledUDecomposer, ) from .multi_controlled import ( synth_mcmt_vchain, diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 79a444e6220c..4af8c3a7eef6 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -270,32 +270,46 @@ class TwoQubitControlledUDecomposer: :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate that is locally equivalent to an :class:`.RXXGate`.""" - def __init__(self, rxx_equivalent_gate: Type[Gate]): + def __init__(self, rxx_equivalent_gate: Type[Gate], euler_basis: str = "ZXZ"): r"""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. + :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. + Valid options are [:class:`.RZZGate`, :class:`.RXXGate`, :class:`.RYYGate`, + :class:`.RZXGate`, :class:`.CPhaseGate`, :class:`.CRXGate`, :class:`.CRYGate`, + :class:`.CRZGate`]. + euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer` + for 1Q synthesis. + Valid options are [``'ZXZ'``, ``'ZYZ'``, ``'XYX'``, ``'XZX'``, ``'U'``, ``'U3'``, + ``'U321'``, ``'U1X'``, ``'PSX'``, ``'ZSX'``, ``'ZSXX'``, ``'RR'``]. + Raises: QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. """ if rxx_equivalent_gate._standard_gate is not None: self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( - rxx_equivalent_gate._standard_gate + rxx_equivalent_gate._standard_gate, euler_basis ) else: self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( - rxx_equivalent_gate + rxx_equivalent_gate, euler_basis ) self.rxx_equivalent_gate = rxx_equivalent_gate self.scale = self._inner_decomposition.scale + self.euler_basis = euler_basis - def __call__(self, unitary: Operator | np.ndarray, *, atol=DEFAULT_ATOL) -> QuantumCircuit: + def __call__( + self, unitary: Operator | np.ndarray, approximate=False, use_dag=False, *, 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. """ circ_data = self._inner_decomposition(np.asarray(unitary, dtype=complex), atol) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index ab66b38bd993..3dfd6caff65c 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -38,6 +38,7 @@ RXXGate, RZXGate, RZZGate, + RYYGate, ECRGate, RXGate, SXGate, @@ -50,6 +51,10 @@ U3Gate, RYGate, RGate, + CRXGate, + CRYGate, + CRZGate, + CPhaseGate, ) from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.dagcircuit.dagcircuit import DAGCircuit @@ -61,6 +66,7 @@ from qiskit.synthesis.two_qubit.two_qubit_decompose import ( TwoQubitBasisDecomposer, TwoQubitWeylDecomposition, + TwoQubitControlledUDecomposer, ) from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.coupling import CouplingMap @@ -87,16 +93,32 @@ "u3": U3Gate._standard_gate, "ry": RYGate._standard_gate, "r": RGate._standard_gate, + "rzz": RZZGate._standard_gate, + "ryy": RYYGate._standard_gate, + "rxx": RXXGate._standard_gate, + "rzx": RXXGate._standard_gate, + "cp": CPhaseGate._standard_gate, + "crx": RXXGate._standard_gate, + "cry": RXXGate._standard_gate, + "crz": RXXGate._standard_gate, } +KAK_GATE_PARAM_NAMES = { + "rxx": RXXGate, + "rzz": RZZGate, + "ryy": RYYGate, + "rzx": RZXGate, + "cphase": CPhaseGate, + "crx": CRXGate, + "cry": CRYGate, + "crz": CRZGate, +} KAK_GATE_NAMES = { "cx": CXGate(), "cz": CZGate(), "iswap": iSwapGate(), - "rxx": RXXGate(pi / 2), "ecr": ECRGate(), - "rzx": RZXGate(pi / 4), # typically pi/6 is also available } GateNameToGate = get_standard_gate_name_mapping() @@ -105,9 +127,14 @@ def _choose_kak_gate(basis_gates): """Choose the first available 2q gate to use in the KAK decomposition.""" kak_gate = None - kak_gates = set(basis_gates or []).intersection(KAK_GATE_NAMES.keys()) - if kak_gates: - kak_gate = KAK_GATE_NAMES[kak_gates.pop()] + kak_gates = sorted(set(basis_gates or []).intersection(KAK_GATE_NAMES.keys())) + kak_gates_params = sorted(set(basis_gates or []).intersection(KAK_GATE_PARAM_NAMES.keys())) + + if kak_gates_params: + kak_gate = KAK_GATE_PARAM_NAMES[kak_gates_params[0]] + + elif kak_gates: + kak_gate = KAK_GATE_NAMES[kak_gates[0]] return kak_gate @@ -150,14 +177,9 @@ def _decomposer_2q_from_basis_gates(basis_gates, pulse_optimize=None, approximat kak_gate = _choose_kak_gate(basis_gates) euler_basis = _choose_euler_basis(basis_gates) basis_fidelity = approximation_degree or 1.0 - if isinstance(kak_gate, RZXGate): - backup_optimizer = TwoQubitBasisDecomposer( - CXGate(), - basis_fidelity=basis_fidelity, - euler_basis=euler_basis, - pulse_optimize=pulse_optimize, - ) - decomposer2q = XXDecomposer(euler_basis=euler_basis, backup_optimizer=backup_optimizer) + + if kak_gate in KAK_GATE_PARAM_NAMES.values(): + decomposer2q = TwoQubitControlledUDecomposer(kak_gate, euler_basis) elif kak_gate is not None: decomposer2q = TwoQubitBasisDecomposer( kak_gate, diff --git a/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml b/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml new file mode 100644 index 000000000000..a55fd7f3ea52 --- /dev/null +++ b/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml @@ -0,0 +1,37 @@ +--- +features_synthesis: + - | + Add a :class:`.TwoQubitControlledUDecomposer` that decomposes any two-qubit unitary + in terms of basis two-qubit fractional gates, such as :class:`.RZZGate` + (or two-gates gates which are locally equivalent to :class:`.RZZGate` up to single qubit gates). + + For example:: + + from qiskit.circuit.library import RZZGate + from qiskit.synthesis import TwoQubitControlledUDecomposer + from qiskit.quantum_info import random_unitary + + unitary = random_unitary(4, seed=1) + decomposer = TwoQubitControlledUDecomposer(RZZGate, euler_basis="ZXZ") + circ = decomposer(unitary) + circ.draw(output='mpl') + +features_transpiler: + - | + Added support for two-qubit fractional basis gates, such as :class:`.RZZGate`, to the + :class:`.UnitarySynthesis` transpiler pass. The decomposition is done using the + :class:`.TwoQubitControlledUDecomposer`, and supports both standard and custom basis gates. + + For example:: + + from qiskit import QuantumCircuit + from qiskit.quantum_info import random_unitary + from qiskit.transpiler.passes import UnitarySynthesis + from qiskit.converters import circuit_to_dag, dag_to_circuit + + unitary = random_unitary(4, seed=1) + qc = QuantumCircuit(2) + qc.append(unitary, [0, 1]) + dag = circuit_to_dag(qc) + circ = UnitarySynthesis(basis_gates=['rzz', 'rx', 'rz']).run(dag) + dag_to_circuit(circ).draw(output='mpl') diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 4bf2b437d05c..5c707c00d884 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1276,7 +1276,7 @@ def test_block_collection_reduces_1q_gate(self, basis_gates, gate_counts): basis_gates=[ ["u3", "cx"], ["rx", "rz", "iswap"], - ["rx", "ry", "rxx"], + ["ry", "rz", "rxx"], ], ) def test_translation_method_synthesis(self, optimization_level, basis_gates): diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index b26a049b5567..98e20d21c009 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -1426,14 +1426,32 @@ def test_approx_supercontrolled_decompose_phase_3_use_random(self, seed, delta=0 class TestTwoQubitControlledUDecompose(CheckDecompositions): """Test TwoQubitControlledUDecomposer() for exact decompositions and raised exceptions""" - @combine(seed=range(10), name="seed_{seed}") - def test_correct_unitary(self, seed): + @combine( + seed=range(5), + gate=[RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate], + euler_basis=[ + "ZYZ", + "ZXZ", + "XYX", + "XZX", + "RR", + "U", + "U3", + "U321", + "PSX", + "ZSX", + "ZSXX", + "U1X", + ], + name="seed_{seed}", + ) + def test_correct_unitary(self, seed, gate, euler_basis): """Verify unitary for different gates in the decomposition""" unitary = random_unitary(4, seed=seed) - for gate in [RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate]: - decomposer = TwoQubitControlledUDecomposer(gate) - circ = decomposer(unitary) - self.assertEqual(Operator(unitary), Operator(circ)) + + decomposer = TwoQubitControlledUDecomposer(gate, euler_basis) + circ = decomposer(unitary) + self.assertEqual(Operator(unitary), Operator(circ)) def test_not_rxx_equivalent(self): """Test that an exception is raised if the gate is not equivalent to an RXXGate""" diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index bc9e218e79cc..d93d6800985c 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -17,6 +17,7 @@ """ import unittest +import math import numpy as np import scipy from ddt import ddt, data @@ -25,6 +26,7 @@ from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.circuit.library import quantum_volume +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.passes import UnitarySynthesis from qiskit.quantum_info.operators import Operator @@ -58,6 +60,7 @@ RZZGate, RXXGate, PauliEvolutionGate, + CPhaseGate, ) from qiskit.quantum_info import SparsePauliOp from qiskit.circuit import Measure @@ -130,7 +133,8 @@ def test_empty_basis_gates(self): @data( ["u3", "cx"], ["u1", "u2", "u3", "cx"], - ["rx", "ry", "rxx"], + ["ry", "rz", "rxx"], + ["rx", "rz", "rzz"], ["rx", "rz", "iswap"], ["u3", "rx", "rz", "cz", "iswap"], ) @@ -740,19 +744,110 @@ def test_iswap_no_cx_synthesis_succeeds(self): result_qc = dag_to_circuit(result_dag) self.assertTrue(np.allclose(Operator(result_qc.to_gate()).to_matrix(), cxmat)) - def test_parameterized_basis_gate_in_target(self): - """Test synthesis with parameterized RXX gate.""" + @combine(is_random=[True, False], param_gate=[RXXGate, RZZGate, CPhaseGate]) + def test_parameterized_basis_gate_in_target(self, is_random, param_gate): + """Test synthesis with parameterized RZZ/RXX gate.""" theta = Parameter("θ") lam = Parameter("λ") + phi = Parameter("ϕ") target = Target(num_qubits=2) target.add_instruction(RZGate(lam)) - target.add_instruction(RXGate(theta)) - target.add_instruction(RXXGate(theta)) + target.add_instruction(RXGate(phi)) + target.add_instruction(param_gate(theta)) qc = QuantumCircuit(2) + if is_random: + qc.unitary(random_unitary(4, seed=1234), [0, 1]) qc.cp(np.pi / 2, 0, 1) qc_transpiled = transpile(qc, target=target, optimization_level=3, seed_transpiler=42) opcount = qc_transpiled.count_ops() - self.assertTrue(set(opcount).issubset({"rz", "rx", "rxx"})) + self.assertTrue(set(opcount).issubset({"rz", "rx", param_gate(theta).name})) + self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + + def test_custom_parameterized_gate_in_target(self): + """Test synthesis with custom parameterized gate in target.""" + + 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" + + theta = Parameter("θ") + lam = Parameter("λ") + phi = Parameter("ϕ") + + target = Target(num_qubits=2) + target.add_instruction(RZGate(lam)) + target.add_instruction(RXGate(phi)) + target.add_instruction(CustomXXGate(theta)) + + qc = QuantumCircuit(2) + qc.unitary(random_unitary(4, seed=1234), [0, 1]) + qc_transpiled = UnitarySynthesis(target=target)(qc) + opcount = qc_transpiled.count_ops() + self.assertTrue(set(opcount).issubset({"rz", "rx", "MyCustomXXGate"})) + + self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + + def test_custom_parameterized_gate_in_target_skips(self): + """Test that synthesis is skipped with custom parameterized + gate in target that is not RXX equivalent.""" + + 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]) + + theta = Parameter("θ") + lam = Parameter("λ") + phi = Parameter("ϕ") + + target = Target(num_qubits=2) + target.add_instruction(RZGate(lam)) + target.add_instruction(RXGate(phi)) + target.add_instruction(CustomXYGate(theta)) + + qc = QuantumCircuit(2) + qc.unitary(random_unitary(4, seed=1234), [0, 1]) + qc_transpiled = UnitarySynthesis(target=target)(qc) + opcount = qc_transpiled.count_ops() + self.assertTrue(set(opcount).issubset({"unitary"})) + self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + + @data( + ["rx", "ry", "rxx"], + ["rx", "rz", "rzz"], + ) + def test_parameterized_backend(self, basis_gates): + """Test synthesis with parameterized backend.""" + backend = GenericBackendV2(3, basis_gates=basis_gates, seed=0) + qc = QuantumCircuit(3) + qc.unitary(random_unitary(4, seed=1234), [0, 1]) + qc.unitary(random_unitary(4, seed=4321), [0, 2]) + qc.cp(np.pi / 2, 0, 1) + qc_transpiled = transpile(qc, backend, optimization_level=3, seed_transpiler=42) + opcount = qc_transpiled.count_ops() + self.assertTrue(set(opcount).issubset(basis_gates)) self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) @data(1, 2, 3)