From 89ca81e8f579ce9accc354f07cc914e0f33e6138 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Mon, 16 Dec 2024 04:59:29 -0600 Subject: [PATCH 01/29] TwoQubitControlledUDecomposer to _decomposer_2q_from_basis_gates --- .../two_qubit/two_qubit_decompose.py | 4 +- .../passes/synthesis/unitary_synthesis.py | 52 +++++++++++++++---- .../transpiler/test_unitary_synthesis.py | 3 +- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 79a444e6220c..28e82d870d46 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -290,7 +290,9 @@ def __init__(self, rxx_equivalent_gate: Type[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: + 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. diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index a2bd044c7341..cb0d4c44733d 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 @@ -62,6 +67,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 @@ -88,16 +94,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() @@ -107,7 +129,12 @@ 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_gates_params = set(basis_gates or []).intersection(KAK_GATE_PARAM_NAMES.keys()) + + if kak_gates_params: + kak_gate = KAK_GATE_PARAM_NAMES[kak_gates_params.pop()] + + elif kak_gates: kak_gate = KAK_GATE_NAMES[kak_gates.pop()] return kak_gate @@ -151,14 +178,17 @@ 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 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) elif kak_gate is not None: decomposer2q = TwoQubitBasisDecomposer( kak_gate, diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index e763a6206d78..ff38086b8444 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -130,7 +130,8 @@ def test_empty_basis_gates(self): @data( ["u3", "cx"], ["u1", "u2", "u3", "cx"], - ["rx", "ry", "rxx"], + ["ry", "rz", "rxx"], + ["ry", "rz", "rzz"], ["rx", "rz", "iswap"], ["u3", "rx", "rz", "cz", "iswap"], ) From 077ecb97d40641f023ee11c60e89596906ced555 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Mon, 16 Dec 2024 06:58:19 -0600 Subject: [PATCH 02/29] update (temporarily) basis gates in test --- test/python/compiler/test_transpiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 851c6817d82f..8d07fc234864 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1277,7 +1277,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): From 18b20f66a54a50b93bf05f3554dfd180c05ab871 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Wed, 18 Dec 2024 04:50:44 -0600 Subject: [PATCH 03/29] minor fix --- 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 28e82d870d46..c9430f66bd51 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -291,7 +291,7 @@ def __init__(self, rxx_equivalent_gate: Type[Gate]): self.scale = self._inner_decomposition.scale def __call__( - self, unitary: Operator | np.ndarray, approximate=False, use_dag=False, atol=DEFAULT_ATOL + self, unitary: Operator | np.ndarray, approximate=False, use_dag=False, *, atol=DEFAULT_ATOL ) -> QuantumCircuit: """Returns the Weyl decomposition in circuit form. Args: From 967da132f9b5a4631b580269b5fa9eb57907603b Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 19 Dec 2024 06:49:23 -0600 Subject: [PATCH 04/29] add EulerBasis as a parameter to TwoQubitControlledUDecomposer --- crates/accelerate/src/two_qubit_decompose.rs | 18 ++++++++++++------ .../synthesis/two_qubit/two_qubit_decompose.py | 17 +++++++++++++---- .../passes/synthesis/unitary_synthesis.py | 2 +- test/python/synthesis/test_synthesis.py | 17 +++++++++++------ .../transpiler/test_unitary_synthesis.py | 2 +- 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 4410d6f35e07..d811a76d6903 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2448,6 +2448,7 @@ impl RXXEquivalent { #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitControlledUDecomposer { rxx_equivalent_gate: RXXEquivalent, + euler_basis: EulerBasis, #[pyo3(get)] scale: f64, } @@ -2544,9 +2545,8 @@ impl TwoQubitControlledUDecomposer { 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); @@ -2687,9 +2687,8 @@ impl TwoQubitControlledUDecomposer { 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(); @@ -2751,11 +2750,17 @@ impl TwoQubitControlledUDecomposer { /// 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))] - pub fn new(py: Python, rxx_equivalent_gate: RXXEquivalent) -> PyResult { + #[pyo3(signature=(rxx_equivalent_gate, euler_basis="ZYZ"))] + pub fn new( + py: Python, + rxx_equivalent_gate: RXXEquivalent, + euler_basis: &str, + ) -> PyResult { let atol = DEFAULT_ATOL; let test_angles = [0.2, 0.3, PI2]; @@ -2819,6 +2824,7 @@ impl TwoQubitControlledUDecomposer { Ok(TwoQubitControlledUDecomposer { scale, rxx_equivalent_gate, + euler_basis: EulerBasis::__new__(euler_basis)?, }) } diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index c9430f66bd51..b7908017a8cb 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -270,34 +270,43 @@ 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 = "ZYZ"): 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. + euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer` + for 1Q synthesis. + Valid options are [``'ZYZ'``, ``'ZXZ'``, ``'XYX'``, ``'U'``, ``'U3'``, ``'U1X'``, + ``'PSX'``, ``'ZSX'``, ``'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, 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 cb0d4c44733d..fb47a29d7a5c 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -188,7 +188,7 @@ def _decomposer_2q_from_basis_gates(basis_gates, pulse_optimize=None, approximat # decomposer2q = XXDecomposer(euler_basis=euler_basis, backup_optimizer=backup_optimizer) if kak_gate in KAK_GATE_PARAM_NAMES.values(): - decomposer2q = TwoQubitControlledUDecomposer(kak_gate) + decomposer2q = TwoQubitControlledUDecomposer(kak_gate, euler_basis) elif kak_gate is not None: decomposer2q = TwoQubitBasisDecomposer( kak_gate, diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index b26a049b5567..b6e845e48a55 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -1426,14 +1426,19 @@ 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(10), + gate=[RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate], + euler_basis=["ZYZ", "ZXZ"], + 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 ff38086b8444..0e14fc7a1628 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -131,7 +131,7 @@ def test_empty_basis_gates(self): ["u3", "cx"], ["u1", "u2", "u3", "cx"], ["ry", "rz", "rxx"], - ["ry", "rz", "rzz"], + ["rx", "rz", "rzz"], ["rx", "rz", "iswap"], ["u3", "rx", "rz", "cz", "iswap"], ) From 3bb10fd18ed70b38940c5ecfea22aa4a9618ea04 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Sun, 22 Dec 2024 07:08:11 -0600 Subject: [PATCH 05/29] fix global_phase calculation in TwoQubitContolledUDecomposer --- crates/accelerate/src/two_qubit_decompose.rs | 8 ++++---- test/python/synthesis/test_synthesis.py | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index d811a76d6903..23c32cd9d114 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2583,14 +2583,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])); @@ -2727,13 +2727,13 @@ impl TwoQubitControlledUDecomposer { 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])); } diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index b6e845e48a55..98e20d21c009 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -1427,9 +1427,22 @@ class TestTwoQubitControlledUDecompose(CheckDecompositions): """Test TwoQubitControlledUDecomposer() for exact decompositions and raised exceptions""" @combine( - seed=range(10), + seed=range(5), gate=[RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate], - euler_basis=["ZYZ", "ZXZ"], + 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): From 14c7aacea7b3f0e5be8b9338ad21eecd558bf0c4 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Sun, 22 Dec 2024 08:09:35 -0600 Subject: [PATCH 06/29] add TwoQubitControlledUDecomposer to the docs --- qiskit/synthesis/__init__.py | 4 +++- qiskit/synthesis/two_qubit/two_qubit_decompose.py | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index adea95d4260c..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 @@ -147,7 +148,7 @@ Multipliers ----------- -.. autofunction:: multiplier_cumulative_h18 +.. autofunction:: multiplier_cumulative_h18 .. autofunction:: multiplier_qft_r17 """ @@ -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 b7908017a8cb..332752275866 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -276,10 +276,13 @@ def __init__(self, rxx_equivalent_gate: Type[Gate], euler_basis: str = "ZYZ"): 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. + 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 [``'ZYZ'``, ``'ZXZ'``, ``'XYX'``, ``'U'``, ``'U3'``, ``'U1X'``, - ``'PSX'``, ``'ZSX'``, ``'RR'``]. + Valid options are [``'ZYZ'``, ``'ZXZ'``, ``'XYX'``, ``'XZX'``, ``'U'``, ``'U3'``, + ``'U321'``, ``'U1X'``, ``'PSX'``, ``'ZSX'``, ``'ZSXX'``, ``'RR'``]. Raises: QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. From eeff4cd23503c0cd9ffbb678286b9c14891c69d7 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Sun, 22 Dec 2024 09:10:22 -0600 Subject: [PATCH 07/29] make the choice of kak_gate deterministic --- qiskit/transpiler/passes/synthesis/unitary_synthesis.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index fb47a29d7a5c..49c9dca1d4fa 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -128,14 +128,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()) - kak_gates_params = set(basis_gates or []).intersection(KAK_GATE_PARAM_NAMES.keys()) + 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.pop()] + kak_gate = KAK_GATE_PARAM_NAMES[kak_gates_params[0]] elif kak_gates: - kak_gate = KAK_GATE_NAMES[kak_gates.pop()] + kak_gate = KAK_GATE_NAMES[kak_gates[0]] return kak_gate From b94070a0e8619411a9e339d5c67774d8ea70ea41 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Mon, 13 Jan 2025 04:04:55 -0600 Subject: [PATCH 08/29] remove XXDecomposer from _decomposer_2q_from_basis_gates --- qiskit/transpiler/passes/synthesis/unitary_synthesis.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 49c9dca1d4fa..54c4b128b944 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -178,14 +178,6 @@ 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) From 0a1644ba404d0a2713a76b67dfb8b1568ec7ea30 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Wed, 15 Jan 2025 07:39:52 -0600 Subject: [PATCH 09/29] make call_inner pub, add Clone, Debug --- crates/accelerate/src/two_qubit_decompose.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 23c32cd9d114..580003328e12 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2445,6 +2445,7 @@ impl RXXEquivalent { } } +#[derive(Clone, Debug)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitControlledUDecomposer { rxx_equivalent_gate: RXXEquivalent, @@ -2678,7 +2679,7 @@ 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, From 48fec9b0b0abdda64b3b53a1272a6e73faf553cb Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Wed, 15 Jan 2025 07:41:06 -0600 Subject: [PATCH 10/29] add TwoQubitControlledUDecomposer to unitary_synthesis.rs --- crates/accelerate/src/unitary_synthesis.rs | 119 ++++++++++++++++----- 1 file changed, 95 insertions(+), 24 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index fa5880b1697a..34015254f0a7 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -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; @@ -54,6 +55,7 @@ const PI4: f64 = PI / 4.; #[derive(Clone, Debug)] enum DecomposerType { TwoQubitBasisDecomposer(Box), + TwoQubitControlledUDecomposer(Box), XXDecomposer(PyObject), } @@ -72,6 +74,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(); @@ -417,6 +420,15 @@ fn run_2q_unitary_synthesis( )?; apply_synth_sequence(py, out_dag, out_qargs, &synth)?; } + DecomposerType::TwoQubitControlledUDecomposer(_) => { + let synth = synth_su4_sequence( + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + } DecomposerType::XXDecomposer(_) => { let synth = synth_su4_dag( py, @@ -474,6 +486,38 @@ 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::TwoQubitControlledUDecomposer(_) => { + 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 + .standard_gate() + .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::XXDecomposer(_) => { let synth_dag = synth_su4_dag( py, @@ -543,6 +587,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(); match target.operation_names_for_qargs(Some(&qubits)) { @@ -564,28 +610,15 @@ fn get_2q_decomposers_from_target( } #[inline] - fn replace_parametrized_gate(mut op: NormalOperation) -> NormalOperation { + fn check_parametrized_gate(op: NormalOperation) -> bool { 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) - } + if PARAM_SET.contains(&std_gate.name()) { + if let Param::ParameterExpression(_) = op.params[0] { + return true; } - "rzz" => { - if let Param::ParameterExpression(_) = op.params[0] { - op.params[0] = Param::Float(PI2) - } - } - _ => (), } } - op + false } for (q_pair, gates) in qubit_gate_map { @@ -597,9 +630,21 @@ fn get_2q_decomposers_from_target( OperationRef::Standard(_) => (), _ => continue, } - - available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); - + if check_parametrized_gate(op.clone()) { + 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, @@ -616,7 +661,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.", )); @@ -651,7 +697,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)) @@ -682,10 +727,27 @@ fn get_2q_decomposers_from_target( } } + for basis_1q in &available_1q_basis { + for (_basis_2q, gate) in available_2q_param_basis.iter() { + let decomposer = TwoQubitControlledUDecomposer::new( + py, + RXXEquivalent::Standard(gate.operation.standard_gate()), + basis_1q, + )?; + + decomposers.push(DecomposerElement { + decomposer: DecomposerType::TwoQubitControlledUDecomposer(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 let available_basis_set: HashSet<&str> = available_2q_basis.keys().copied().collect(); + let available_basis_param_set: HashSet<&str> = + available_2q_param_basis.keys().copied().collect(); #[inline] fn check_goodbye(basis_set: &HashSet<&str>) -> bool { @@ -696,6 +758,15 @@ fn get_2q_decomposers_from_target( return Ok(Some(decomposers)); } + #[inline] + fn check_parametrized_goodbye(basis_set: &HashSet<&str>) -> bool { + 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() From 85e2962eee62e99ce552681b66ce5330e57dd8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 16 Jan 2025 10:55:12 +0100 Subject: [PATCH 11/29] Fix exit condition for GOODBYE_SET and PARAM_SET --- crates/accelerate/src/unitary_synthesis.rs | 31 ++++++++++++---------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 8ff1649d832f..e5324775cc3b 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -732,6 +732,19 @@ fn get_2q_decomposers_from_target( } } + // 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 try other decomposers + let available_basis_set: HashSet<&str> = available_2q_basis.keys().copied().collect(); + + #[inline] + fn check_goodbye(basis_set: &HashSet<&str>) -> bool { + !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 decomposer = TwoQubitControlledUDecomposer::new( @@ -747,25 +760,15 @@ fn get_2q_decomposers_from_target( } } - // 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 - let available_basis_set: HashSet<&str> = available_2q_basis.keys().copied().collect(); + // 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: HashSet<&str> = available_2q_param_basis.keys().copied().collect(); - #[inline] - fn check_goodbye(basis_set: &HashSet<&str>) -> bool { - basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) - } - - if check_goodbye(&available_basis_set) { - return Ok(Some(decomposers)); - } - #[inline] fn check_parametrized_goodbye(basis_set: &HashSet<&str>) -> bool { - basis_set.iter().all(|gate| PARAM_SET.contains(gate)) + !basis_set.is_empty() && basis_set.iter().all(|gate| PARAM_SET.contains(gate)) } if check_parametrized_goodbye(&available_basis_param_set) { From d7b84e9cfd6885fa9bbb002026b31893049e5d37 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 16 Jan 2025 08:40:19 -0600 Subject: [PATCH 12/29] make DEFAULT_ATOL public --- 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 580003328e12..67ada8f79928 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2454,7 +2454,7 @@ pub struct TwoQubitControlledUDecomposer { scale: f64, } -const DEFAULT_ATOL: f64 = 1e-12; +pub const DEFAULT_ATOL: f64 = 1e-12; type InverseReturn = (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>); /// Decompose two-qubit unitary in terms of a desired From ea9a2d0eb1a5697fd1431a8109cd0e18699e3b95 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 16 Jan 2025 08:41:25 -0600 Subject: [PATCH 13/29] add TwoQubitControlledUDecomposer to synth_su4_sequence --- crates/accelerate/src/unitary_synthesis.rs | 31 ++++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index e73a9392e157..9c6dc1b03ab7 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -45,7 +45,7 @@ use crate::nlayout::PhysicalQubit; use crate::target_transpiler::{NormalOperation, Target}; use crate::two_qubit_decompose::{ RXXEquivalent, TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer, TwoQubitGateSequence, - TwoQubitWeylDecomposition, + TwoQubitWeylDecomposition, DEFAULT_ATOL, }; use crate::QiskitError; @@ -413,6 +413,7 @@ fn run_2q_unitary_synthesis( match decomposer_item.decomposer { DecomposerType::TwoQubitBasisDecomposer(_) => { let synth = synth_su4_sequence( + py, &unitary, decomposer_item, preferred_dir, @@ -422,6 +423,7 @@ fn run_2q_unitary_synthesis( } DecomposerType::TwoQubitControlledUDecomposer(_) => { let synth = synth_su4_sequence( + py, &unitary, decomposer_item, preferred_dir, @@ -455,8 +457,13 @@ fn run_2q_unitary_synthesis( )?; match &decomposer.decomposer { DecomposerType::TwoQubitBasisDecomposer(_) => { - let sequence = - synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; + let sequence = synth_su4_sequence( + py, + &unitary, + decomposer, + preferred_dir, + approximation_degree, + )?; let scoring_info = sequence .gate_sequence @@ -487,8 +494,13 @@ fn run_2q_unitary_synthesis( synth_errors_sequence.push((sequence, synth_error_from_target)); } DecomposerType::TwoQubitControlledUDecomposer(_) => { - let sequence = - synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; + let sequence = synth_su4_sequence( + py, + &unitary, + decomposer, + preferred_dir, + approximation_degree, + )?; let scoring_info = sequence .gate_sequence @@ -956,6 +968,7 @@ fn preferred_direction( } fn synth_su4_sequence( + py: Python, su4_mat: &Array2, decomposer_2q: &DecomposerElement, preferred_direction: Option, @@ -964,6 +977,9 @@ fn synth_su4_sequence( let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else if let DecomposerType::TwoQubitControlledUDecomposer(decomp) = &decomposer_2q.decomposer + { + decomp.call_inner(py, su4_mat.view(), DEFAULT_ATOL)? } else { unreachable!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") }; @@ -995,6 +1011,7 @@ fn synth_su4_sequence( }; if synth_dir != preferred_dir { reversed_synth_su4_sequence( + py, su4_mat.clone(), decomposer_2q, approximation_degree, @@ -1009,6 +1026,7 @@ fn synth_su4_sequence( } fn reversed_synth_su4_sequence( + py: Python, mut su4_mat: Array2, decomposer_2q: &DecomposerElement, approximation_degree: Option, @@ -1024,6 +1042,9 @@ fn reversed_synth_su4_sequence( let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else if let DecomposerType::TwoQubitControlledUDecomposer(decomp) = &decomposer_2q.decomposer + { + decomp.call_inner(py, su4_mat.view(), DEFAULT_ATOL)? } else { unreachable!( "reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer." From 132a44fad94d6303bbb88898fd523b1f0c9879c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 20 Jan 2025 13:22:17 +0100 Subject: [PATCH 14/29] Add support for parametrized decomposer gate in apply_synth_sequence --- crates/accelerate/src/unitary_synthesis.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 9c6dc1b03ab7..1eb6d4621d39 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -140,10 +140,19 @@ fn apply_synth_sequence( None => sequence.decomp_gate.operation.standard_gate(), Some(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 instruction = PackedInstruction { op: PackedOperation::from_standard(gate_node), From b1cb5a05415ee59cc79d23da98d872dbb1bf257f Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Mon, 20 Jan 2025 07:51:58 -0600 Subject: [PATCH 15/29] change DecomposerType enum to fix clippy error --- crates/accelerate/src/unitary_synthesis.rs | 38 ++++++++++------------ 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 1eb6d4621d39..13efabbb6acc 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -54,9 +54,9 @@ const PI4: f64 = PI / 4.; #[derive(Clone, Debug)] enum DecomposerType { - TwoQubitBasisDecomposer(Box), - TwoQubitControlledUDecomposer(Box), - XXDecomposer(PyObject), + TwoQubitBasis(Box), + TwoQubitControlledU(Box), + XX(PyObject), } struct DecomposerElement { @@ -420,7 +420,7 @@ fn run_2q_unitary_synthesis( target, )?; match decomposer_item.decomposer { - DecomposerType::TwoQubitBasisDecomposer(_) => { + DecomposerType::TwoQubitBasis(_) => { let synth = synth_su4_sequence( py, &unitary, @@ -430,7 +430,7 @@ fn run_2q_unitary_synthesis( )?; apply_synth_sequence(py, out_dag, out_qargs, &synth)?; } - DecomposerType::TwoQubitControlledUDecomposer(_) => { + DecomposerType::TwoQubitControlledU(_) => { let synth = synth_su4_sequence( py, &unitary, @@ -440,7 +440,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, @@ -465,7 +465,7 @@ fn run_2q_unitary_synthesis( target, )?; match &decomposer.decomposer { - DecomposerType::TwoQubitBasisDecomposer(_) => { + DecomposerType::TwoQubitBasis(_) => { let sequence = synth_su4_sequence( py, &unitary, @@ -502,7 +502,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::TwoQubitControlledUDecomposer(_) => { + DecomposerType::TwoQubitControlledU(_) => { let sequence = synth_su4_sequence( py, &unitary, @@ -539,7 +539,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, @@ -747,7 +747,7 @@ fn get_2q_decomposers_from_target( )?; decomposers.push(DecomposerElement { - decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), + decomposer: DecomposerType::TwoQubitBasis(Box::new(decomposer)), gate: gate.clone(), }); } @@ -775,7 +775,7 @@ fn get_2q_decomposers_from_target( )?; decomposers.push(DecomposerElement { - decomposer: DecomposerType::TwoQubitControlledUDecomposer(Box::new(decomposer)), + decomposer: DecomposerType::TwoQubitControlledU(Box::new(decomposer)), gate: gate.clone(), }); } @@ -883,7 +883,7 @@ fn get_2q_decomposers_from_target( .extract::()?; decomposers.push(DecomposerElement { - decomposer: DecomposerType::XXDecomposer(decomposer.into()), + decomposer: DecomposerType::XX(decomposer.into()), gate: decomposer_gate, }); } @@ -984,10 +984,9 @@ 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::TwoQubitControlledUDecomposer(decomp) = &decomposer_2q.decomposer - { + } else if let DecomposerType::TwoQubitControlledU(decomp) = &decomposer_2q.decomposer { decomp.call_inner(py, su4_mat.view(), DEFAULT_ATOL)? } else { unreachable!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") @@ -1049,10 +1048,9 @@ 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::TwoQubitControlledUDecomposer(decomp) = &decomposer_2q.decomposer - { + } else if let DecomposerType::TwoQubitControlledU(decomp) = &decomposer_2q.decomposer { decomp.call_inner(py, su4_mat.view(), DEFAULT_ATOL)? } else { unreachable!( @@ -1087,7 +1085,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(); @@ -1153,7 +1151,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(); From 9cdf2da1e58b5ac73b0830394abd9d4968a804cd Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Mon, 20 Jan 2025 07:52:42 -0600 Subject: [PATCH 16/29] add a random unitary test to test_parametrized_basis_gate_in_target --- test/python/transpiler/test_unitary_synthesis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 9ff714500273..c624ca6a7da6 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -844,7 +844,8 @@ 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): + @data(True, False) + def test_parameterized_basis_gate_in_target(self, is_random): """Test synthesis with parameterized RXX gate.""" theta = Parameter("θ") lam = Parameter("λ") @@ -853,6 +854,8 @@ def test_parameterized_basis_gate_in_target(self): target.add_instruction(RXGate(theta)) target.add_instruction(RXXGate(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() From 1be749d195cb0ad485fb484fb8e2220da7dd1adc Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 21 Jan 2025 02:17:15 -0600 Subject: [PATCH 17/29] add public new_inner for TwoQubitControlledUDecomposer --- crates/accelerate/src/two_qubit_decompose.rs | 37 ++++++++++++-------- crates/accelerate/src/unitary_synthesis.rs | 2 +- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 67ada8f79928..15f064a8c3ef 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2743,21 +2743,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. - /// 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="ZYZ"))] - pub fn new( + /// Initialize the KAK decomposition. + pub fn new_inner( py: Python, rxx_equivalent_gate: RXXEquivalent, euler_basis: &str, @@ -2828,6 +2816,27 @@ impl TwoQubitControlledUDecomposer { euler_basis: EulerBasis::__new__(euler_basis)?, }) } +} + +#[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="ZYZ"))] + pub fn new( + py: Python, + rxx_equivalent_gate: RXXEquivalent, + euler_basis: &str, + ) -> PyResult { + TwoQubitControlledUDecomposer::new_inner(py, rxx_equivalent_gate, euler_basis) + } #[pyo3(signature=(unitary, atol))] fn __call__( diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 13efabbb6acc..7d64286f1bd0 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -768,7 +768,7 @@ fn get_2q_decomposers_from_target( for basis_1q in &available_1q_basis { for (_basis_2q, gate) in available_2q_param_basis.iter() { - let decomposer = TwoQubitControlledUDecomposer::new( + let decomposer = TwoQubitControlledUDecomposer::new_inner( py, RXXEquivalent::Standard(gate.operation.standard_gate()), basis_1q, From 1495781798aa15500aa5f5611f8f638282f6521f Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Tue, 21 Jan 2025 02:28:46 -0600 Subject: [PATCH 18/29] replace default 'ZYZ' by 'ZXZ' in TwoQubitControlledUDecomposer --- crates/accelerate/src/two_qubit_decompose.rs | 2 +- qiskit/synthesis/two_qubit/two_qubit_decompose.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 15f064a8c3ef..da16fef4fa7e 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2829,7 +2829,7 @@ impl TwoQubitControlledUDecomposer { /// Raises: /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. #[new] - #[pyo3(signature=(rxx_equivalent_gate, euler_basis="ZYZ"))] + #[pyo3(signature=(rxx_equivalent_gate, euler_basis="ZXZ"))] pub fn new( py: Python, rxx_equivalent_gate: RXXEquivalent, diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 332752275866..4af8c3a7eef6 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -270,7 +270,7 @@ 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], euler_basis: str = "ZYZ"): + def __init__(self, rxx_equivalent_gate: Type[Gate], euler_basis: str = "ZXZ"): r"""Initialize the KAK decomposition. Args: @@ -281,7 +281,7 @@ def __init__(self, rxx_equivalent_gate: Type[Gate], euler_basis: str = "ZYZ"): :class:`.CRZGate`]. euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer` for 1Q synthesis. - Valid options are [``'ZYZ'``, ``'ZXZ'``, ``'XYX'``, ``'XZX'``, ``'U'``, ``'U3'``, + Valid options are [``'ZXZ'``, ``'ZYZ'``, ``'XYX'``, ``'XZX'``, ``'U'``, ``'U3'``, ``'U321'``, ``'U1X'``, ``'PSX'``, ``'ZSX'``, ``'ZSXX'``, ``'RR'``]. Raises: From 231af7f18b4ce6b69d82c526d1c3d2488c899f91 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 23 Jan 2025 02:25:06 -0600 Subject: [PATCH 19/29] remove using py in rust functions --- crates/accelerate/src/two_qubit_decompose.rs | 52 +++++++++----------- crates/accelerate/src/unitary_synthesis.rs | 28 +++-------- 2 files changed, 28 insertions(+), 52 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index da16fef4fa7e..9888c6f1d4e6 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2431,16 +2431,16 @@ 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()) - } + }), } } } @@ -2464,7 +2464,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; @@ -2501,7 +2500,7 @@ impl TwoQubitControlledUDecomposer { .collect::>(); Ok((Some(inv_gate.0), inv_gate_params, qubits)) } - RXXEquivalent::CustomPython(gate_cls) => { + RXXEquivalent::CustomPython(gate_cls) => Python::with_gil(|py: Python| { let gate_obj = gate_cls.bind(py).call1(PyTuple::new_bound(py, params))?; let raw_inverse = gate_obj.call_method0(intern!(py, "inverse"))?; let inverse: OperationFromPython = raw_inverse.extract()?; @@ -2522,7 +2521,7 @@ impl TwoQubitControlledUDecomposer { "rxx gate inverse is not valid for this decomposer", )) } - } + }), } } } @@ -2535,14 +2534,14 @@ impl TwoQubitControlledUDecomposer { /// Circuit: Circuit equivalent to an RXXGate. /// Raises: /// QiskitError: If the circuit is not equivalent to an RXXGate. - fn to_rxx_gate(&self, py: Python, angle: f64) -> PyResult { + fn to_rxx_gate(&self, angle: f64) -> PyResult { // The user-provided RXXGate equivalent gate may be locally equivalent to the RXXGate // but with some scaling in the rotation angle. For example, RXXGate(angle) has Weyl // parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e. // :code:`self.rxx_equivalent_gate(angle)` might produce the Weyl parameters // (scale * angle, 0, 0) where scale != 1. This is the case for the CPhaseGate. - let mat = self.rxx_equivalent_gate.matrix(py, self.scale * angle)?; + let mat = self.rxx_equivalent_gate.matrix(self.scale * angle)?; let decomposer_inv = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; @@ -2607,18 +2606,17 @@ impl TwoQubitControlledUDecomposer { /// Appends U_d(a, b, c) to the circuit. fn weyl_gate( &self, - py: Python, circ: &mut TwoQubitGateSequence, target_decomposed: TwoQubitWeylDecomposition, atol: f64, ) -> PyResult<()> { - let circ_a = self.to_rxx_gate(py, -2.0 * target_decomposed.a)?; + let circ_a = self.to_rxx_gate(-2.0 * target_decomposed.a)?; circ.gates.extend(circ_a.gates); let mut global_phase = circ_a.global_phase; // translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate. if (target_decomposed.b).abs() > atol { - let circ_b = self.to_rxx_gate(py, -2.0 * target_decomposed.b)?; + let circ_b = self.to_rxx_gate(-2.0 * target_decomposed.b)?; global_phase += circ_b.global_phase; circ.gates .push((Some(StandardGate::SdgGate), smallvec![], smallvec![0])); @@ -2640,7 +2638,7 @@ impl TwoQubitControlledUDecomposer { // circuit if c < 0. let mut gamma = -2.0 * target_decomposed.c; if gamma <= 0.0 { - let circ_c = self.to_rxx_gate(py, gamma)?; + let circ_c = self.to_rxx_gate(gamma)?; global_phase += circ_c.global_phase; circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); @@ -2654,7 +2652,7 @@ impl TwoQubitControlledUDecomposer { } else { // invert the circuit above gamma *= -1.0; - let circ_c = self.to_rxx_gate(py, gamma)?; + let circ_c = self.to_rxx_gate(gamma)?; global_phase -= circ_c.global_phase; circ.gates .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); @@ -2662,7 +2660,7 @@ impl TwoQubitControlledUDecomposer { .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); for gate in circ_c.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params, inv_gate_qubits) = - self.invert_2q_gate(py, gate)?; + self.invert_2q_gate(gate)?; circ.gates .push((inv_gate_name, inv_gate_params, inv_gate_qubits)); } @@ -2681,7 +2679,6 @@ impl TwoQubitControlledUDecomposer { /// Note: atol is passed to OneQubitEulerDecomposer. pub fn call_inner( &self, - py: Python, unitary: ArrayView2, atol: f64, ) -> PyResult { @@ -2724,7 +2721,7 @@ impl TwoQubitControlledUDecomposer { gates, global_phase, }; - self.weyl_gate(py, &mut gates1, target_decomposed, atol)?; + self.weyl_gate(&mut gates1, target_decomposed, atol)?; global_phase += gates1.global_phase; if let Some(unitary_c1r) = unitary_c1r { @@ -2745,11 +2742,7 @@ impl TwoQubitControlledUDecomposer { } /// Initialize the KAK decomposition. - pub fn new_inner( - py: Python, - rxx_equivalent_gate: RXXEquivalent, - euler_basis: &str, - ) -> PyResult { + pub fn new_inner(rxx_equivalent_gate: RXXEquivalent, euler_basis: &str) -> PyResult { let atol = DEFAULT_ATOL; let test_angles = [0.2, 0.3, PI2]; @@ -2765,14 +2758,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 @@ -2830,12 +2826,8 @@ impl TwoQubitControlledUDecomposer { /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. #[new] #[pyo3(signature=(rxx_equivalent_gate, euler_basis="ZXZ"))] - pub fn new( - py: Python, - rxx_equivalent_gate: RXXEquivalent, - euler_basis: &str, - ) -> PyResult { - TwoQubitControlledUDecomposer::new_inner(py, rxx_equivalent_gate, euler_basis) + pub fn new(rxx_equivalent_gate: RXXEquivalent, euler_basis: &str) -> PyResult { + TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, euler_basis) } #[pyo3(signature=(unitary, atol))] @@ -2845,7 +2837,7 @@ impl TwoQubitControlledUDecomposer { unitary: PyReadonlyArray2, atol: f64, ) -> PyResult { - let sequence = self.call_inner(py, unitary.as_array(), atol)?; + let sequence = self.call_inner(unitary.as_array(), atol)?; match &self.rxx_equivalent_gate { RXXEquivalent::Standard(rxx_gate) => CircuitData::from_standard_gates( py, diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 7d64286f1bd0..a9c8334d3b38 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -422,7 +422,6 @@ fn run_2q_unitary_synthesis( match decomposer_item.decomposer { DecomposerType::TwoQubitBasis(_) => { let synth = synth_su4_sequence( - py, &unitary, decomposer_item, preferred_dir, @@ -432,7 +431,6 @@ fn run_2q_unitary_synthesis( } DecomposerType::TwoQubitControlledU(_) => { let synth = synth_su4_sequence( - py, &unitary, decomposer_item, preferred_dir, @@ -466,13 +464,8 @@ fn run_2q_unitary_synthesis( )?; match &decomposer.decomposer { DecomposerType::TwoQubitBasis(_) => { - let sequence = synth_su4_sequence( - py, - &unitary, - decomposer, - preferred_dir, - approximation_degree, - )?; + let sequence = + synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; let scoring_info = sequence .gate_sequence @@ -503,13 +496,8 @@ fn run_2q_unitary_synthesis( synth_errors_sequence.push((sequence, synth_error_from_target)); } DecomposerType::TwoQubitControlledU(_) => { - let sequence = synth_su4_sequence( - py, - &unitary, - decomposer, - preferred_dir, - approximation_degree, - )?; + let sequence = + synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; let scoring_info = sequence .gate_sequence @@ -769,7 +757,6 @@ fn get_2q_decomposers_from_target( for basis_1q in &available_1q_basis { for (_basis_2q, gate) in available_2q_param_basis.iter() { let decomposer = TwoQubitControlledUDecomposer::new_inner( - py, RXXEquivalent::Standard(gate.operation.standard_gate()), basis_1q, )?; @@ -977,7 +964,6 @@ fn preferred_direction( } fn synth_su4_sequence( - py: Python, su4_mat: &Array2, decomposer_2q: &DecomposerElement, preferred_direction: Option, @@ -987,7 +973,7 @@ fn synth_su4_sequence( 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(py, su4_mat.view(), DEFAULT_ATOL)? + decomp.call_inner(su4_mat.view(), DEFAULT_ATOL)? } else { unreachable!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") }; @@ -1019,7 +1005,6 @@ fn synth_su4_sequence( }; if synth_dir != preferred_dir { reversed_synth_su4_sequence( - py, su4_mat.clone(), decomposer_2q, approximation_degree, @@ -1034,7 +1019,6 @@ fn synth_su4_sequence( } fn reversed_synth_su4_sequence( - py: Python, mut su4_mat: Array2, decomposer_2q: &DecomposerElement, approximation_degree: Option, @@ -1051,7 +1035,7 @@ fn reversed_synth_su4_sequence( 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(py, su4_mat.view(), DEFAULT_ATOL)? + decomp.call_inner(su4_mat.view(), DEFAULT_ATOL)? } else { unreachable!( "reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer." From bb874c1947b9de4342cc53ed5b5748b8fb9bf4cd Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 23 Jan 2025 02:25:24 -0600 Subject: [PATCH 20/29] minor update to test --- test/python/transpiler/test_unitary_synthesis.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index c624ca6a7da6..762f8d57ce38 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -849,17 +849,18 @@ def test_parameterized_basis_gate_in_target(self, is_random): """Test synthesis with parameterized 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(RZZGate(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", "rzz"})) self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) @data(1, 2, 3) From c2a9ad452984a4880badf746e28a97d66ffd7ea3 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 23 Jan 2025 03:59:45 -0600 Subject: [PATCH 21/29] make atol optional --- crates/accelerate/src/two_qubit_decompose.rs | 10 +++++----- crates/accelerate/src/unitary_synthesis.rs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 9888c6f1d4e6..3c282e10ab1b 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -2454,7 +2454,7 @@ pub struct TwoQubitControlledUDecomposer { scale: f64, } -pub const DEFAULT_ATOL: f64 = 1e-12; +const DEFAULT_ATOL: f64 = 1e-12; type InverseReturn = (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>); /// Decompose two-qubit unitary in terms of a desired @@ -2680,7 +2680,7 @@ impl TwoQubitControlledUDecomposer { pub fn call_inner( &self, unitary: ArrayView2, - atol: f64, + atol: Option, ) -> PyResult { let target_decomposed = TwoQubitWeylDecomposition::new_inner(unitary, Some(DEFAULT_FIDELITY), None)?; @@ -2721,7 +2721,7 @@ impl TwoQubitControlledUDecomposer { gates, global_phase, }; - self.weyl_gate(&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 { @@ -2830,12 +2830,12 @@ impl TwoQubitControlledUDecomposer { TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, euler_basis) } - #[pyo3(signature=(unitary, atol))] + #[pyo3(signature=(unitary, atol=None))] fn __call__( &self, py: Python, unitary: PyReadonlyArray2, - atol: f64, + atol: Option, ) -> PyResult { let sequence = self.call_inner(unitary.as_array(), atol)?; match &self.rxx_equivalent_gate { diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index a9c8334d3b38..f8d313669a3c 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -45,7 +45,7 @@ use crate::nlayout::PhysicalQubit; use crate::target_transpiler::{NormalOperation, Target}; use crate::two_qubit_decompose::{ RXXEquivalent, TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer, TwoQubitGateSequence, - TwoQubitWeylDecomposition, DEFAULT_ATOL, + TwoQubitWeylDecomposition, }; use crate::QiskitError; @@ -973,7 +973,7 @@ fn synth_su4_sequence( 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(), DEFAULT_ATOL)? + decomp.call_inner(su4_mat.view(), None)? } else { unreachable!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") }; @@ -1035,7 +1035,7 @@ fn reversed_synth_su4_sequence( 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(), DEFAULT_ATOL)? + decomp.call_inner(su4_mat.view(), None)? } else { unreachable!( "reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer." From 106ae4a50d997a2f8d21493eb8e57cb84435ae54 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 23 Jan 2025 04:37:35 -0600 Subject: [PATCH 22/29] add a test with fractional gates in the backend --- .../transpiler/test_unitary_synthesis.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 762f8d57ce38..901b9ce9a4bc 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -846,7 +846,7 @@ def test_iswap_no_cx_synthesis_succeeds(self): @data(True, False) def test_parameterized_basis_gate_in_target(self, is_random): - """Test synthesis with parameterized RXX gate.""" + """Test synthesis with parameterized RZZ gate.""" theta = Parameter("θ") lam = Parameter("λ") phi = Parameter("ϕ") @@ -863,6 +863,22 @@ def test_parameterized_basis_gate_in_target(self, is_random): self.assertTrue(set(opcount).issubset({"rz", "rx", "rzz"})) 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) + 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) def test_qsd(self, opt): """Test that the unitary synthesis pass runs qsd successfully with a target.""" From 9131e3dda79356ddd80da13ccef5fc9b0cb831d0 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 23 Jan 2025 04:37:51 -0600 Subject: [PATCH 23/29] add release notes --- ...ates-to-unitarysynthesis-pass-f66eee29903f5639.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml 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..96f290155cc6 --- /dev/null +++ b/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml @@ -0,0 +1,10 @@ +--- +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). +features_transpiler: + - | + Add two-qubit fractional basis gates, such as :class:`.RZZGate`, to the + :class:`.UnitarySynthesis` transpiler pass. From 6a82649709a9ee23f7eef241c055f0f483f00b79 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Sun, 26 Jan 2025 08:23:40 -0600 Subject: [PATCH 24/29] enhance tests following review --- test/python/transpiler/test_unitary_synthesis.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 901b9ce9a4bc..e1bf5f4c4141 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -844,23 +844,23 @@ 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)) - @data(True, False) - def test_parameterized_basis_gate_in_target(self, is_random): - """Test synthesis with parameterized RZZ gate.""" + @combine(is_random=[True, False], param_gate=[RXXGate, RZZGate]) + 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(phi)) - target.add_instruction(RZZGate(theta)) + 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", "rzz"})) + self.assertTrue(set(opcount).issubset({"rz", "rx", param_gate(theta).name})) self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) @data( @@ -869,7 +869,7 @@ def test_parameterized_basis_gate_in_target(self, is_random): ) def test_parameterized_backend(self, basis_gates): """Test synthesis with parameterized backend.""" - backend = GenericBackendV2(3, basis_gates=basis_gates) + 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]) From 23b9e6b5572a77534e3f3508d4a64ee39ef7778c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 29 Jan 2025 17:43:36 +0100 Subject: [PATCH 25/29] Add support for non-standard parametrized gates, add new tests. TODO: address TwoQubitControlledUDecomposer issue, it appends gates outside of basis set (h/s/sdg) --- crates/accelerate/src/unitary_synthesis.rs | 71 +++++++++-------- .../transpiler/test_unitary_synthesis.py | 77 ++++++++++++++++++- 2 files changed, 111 insertions(+), 37 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 569b3ea50b40..fc75c13c1a07 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -27,7 +27,7 @@ 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; @@ -59,6 +59,7 @@ enum DecomposerType { XX(PyObject), } +#[derive(Clone, Debug)] struct DecomposerElement { decomposer: DecomposerType, gate: NormalOperation, @@ -136,11 +137,10 @@ 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), }; - 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())), @@ -155,7 +155,7 @@ fn apply_synth_sequence( } }; let instruction = PackedInstruction { - op: PackedOperation::from_standard(gate_node), + op: packed_op.clone(), qubits: out_dag.qargs_interner.insert(&mapped_qargs), clbits: out_dag.cargs_interner.get_default(), params: new_params, @@ -419,6 +419,7 @@ fn run_2q_unitary_synthesis( coupling_edges, target, )?; + match decomposer_item.decomposer { DecomposerType::TwoQubitBasis(_) => { let synth = synth_su4_sequence( @@ -481,12 +482,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, ), @@ -513,12 +509,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, ), @@ -620,15 +611,10 @@ fn get_2q_decomposers_from_target( } #[inline] - fn check_parametrized_gate(op: NormalOperation) -> bool { - if let Some(std_gate) = op.operation.try_standard_gate() { - if PARAM_SET.contains(&std_gate.name()) { - if let Param::ParameterExpression(_) = op.params[0] { - return true; - } - } - } - false + 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 { @@ -644,7 +630,7 @@ fn get_2q_decomposers_from_target( if op.operation.num_qubits() != 2 { continue; } - if check_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( @@ -756,15 +742,28 @@ fn get_2q_decomposers_from_target( for basis_1q in &available_1q_basis { for (_basis_2q, gate) in available_2q_param_basis.iter() { - let decomposer = TwoQubitControlledUDecomposer::new_inner( - RXXEquivalent::Standard(gate.operation.standard_gate()), - basis_1q, - )?; + 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) + }; - decomposers.push(DecomposerElement { - decomposer: DecomposerType::TwoQubitControlledU(Box::new(decomposer)), - gate: gate.clone(), - }); + 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, + }; } } diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index e1bf5f4c4141..18d3bbd420bb 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -19,12 +19,14 @@ import unittest import numpy as np import scipy +import math from ddt import ddt, data from qiskit import transpile, generate_preset_pass_manager from qiskit.providers.fake_provider import Fake5QV1, 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 @@ -844,7 +847,7 @@ 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)) - @combine(is_random=[True, False], param_gate=[RXXGate, RZZGate]) + @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("θ") @@ -863,6 +866,78 @@ def test_parameterized_basis_gate_in_target(self, is_random, param_gate): 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) + # TODO: fix this assertion + # opcount = qc_transpiled.count_ops() + # print(opcount) + # self.assertTrue(set(opcount).issubset({"rz", "rx", CustomXXGate.name})) + # 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"], From 5bb55b581f0852e464399e9e916eb2f2a7d3430a Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 30 Jan 2025 08:46:23 -0600 Subject: [PATCH 26/29] decompose S, Sdg, H into euler_basis --- crates/accelerate/src/two_qubit_decompose.rs | 111 ++++++++++++++----- 1 file changed, 86 insertions(+), 25 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 7e140046242e..50572cf14ecb 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}; @@ -2631,19 +2633,57 @@ impl TwoQubitControlledUDecomposer { 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(-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. @@ -2657,34 +2697,55 @@ impl TwoQubitControlledUDecomposer { 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])); - 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(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(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])); + } + } } } From f4da2dac064a0442bf1ed091679e16bc11af3cc8 Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Thu, 30 Jan 2025 08:46:37 -0600 Subject: [PATCH 27/29] update test --- test/python/transpiler/test_unitary_synthesis.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 18d3bbd420bb..39eb460e4799 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -17,9 +17,9 @@ """ import unittest +import math import numpy as np import scipy -import math from ddt import ddt, data from qiskit import transpile, generate_preset_pass_manager @@ -891,9 +891,8 @@ def __init__(self, theta, label=None): qc.unitary(random_unitary(4, seed=1234), [0, 1]) qc_transpiled = UnitarySynthesis(target=target)(qc) # TODO: fix this assertion - # opcount = qc_transpiled.count_ops() - # print(opcount) - # self.assertTrue(set(opcount).issubset({"rz", "rx", CustomXXGate.name})) + 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): From 1bd71c58ba9333f7cd72eaf2809df07986452b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 3 Feb 2025 17:25:10 +0100 Subject: [PATCH 28/29] Overwrite Python-side gate parameters as well as Rust-side parameters. --- crates/accelerate/src/unitary_synthesis.rs | 35 +++++++++++++++++-- .../transpiler/test_unitary_synthesis.py | 4 +-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index fc75c13c1a07..65a1d604e0e3 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -34,7 +34,7 @@ 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; @@ -154,8 +154,39 @@ fn apply_synth_sequence( } } }; + + 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::Standard(_) => packed_op.clone(), + _ => { + return Err(QiskitError::new_err( + "Decomposed gate sequence contains unexpected operations.", + )) + } + }; + let instruction = PackedInstruction { - op: packed_op.clone(), + op: new_op, qubits: out_dag.qargs_interner.insert(&mapped_qargs), clbits: out_dag.cargs_interner.get_default(), params: new_params, diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 39eb460e4799..6907f020f5b2 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -890,10 +890,10 @@ def __init__(self, theta, label=None): qc = QuantumCircuit(2) qc.unitary(random_unitary(4, seed=1234), [0, 1]) qc_transpiled = UnitarySynthesis(target=target)(qc) - # TODO: fix this assertion opcount = qc_transpiled.count_ops() self.assertTrue(set(opcount).issubset({"rz", "rx", "MyCustomXXGate"})) - # self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + + 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 From 55556f8f4762d960391311a76d2ecc4ee2d3222c Mon Sep 17 00:00:00 2001 From: ShellyGarion Date: Mon, 10 Feb 2025 03:15:38 -0600 Subject: [PATCH 29/29] add examples to release notes --- ...nitarysynthesis-pass-f66eee29903f5639.yaml | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) 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 index 96f290155cc6..a55fd7f3ea52 100644 --- a/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml +++ b/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml @@ -4,7 +4,34 @@ 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: - | - Add two-qubit fractional basis gates, such as :class:`.RZZGate`, to the - :class:`.UnitarySynthesis` transpiler pass. + 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')