From c3c6c3d9338bc2a91953473a240802d7d2bdb6b4 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Nov 2024 03:09:05 +0900 Subject: [PATCH 01/20] add plugin pass to support non-calibrated rzz angles --- qiskit_ibm_runtime/ibm_backend.py | 4 +- .../transpiler/passes/basis/__init__.py | 1 + .../transpiler/passes/basis/fold_rzz_angle.py | 185 ++++++++++++++++++ qiskit_ibm_runtime/transpiler/plugin.py | 41 +++- setup.py | 1 + .../passes/basis/test_fold_rzz_angle.py | 63 ++++++ 6 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py create mode 100644 test/unit/transpiler/passes/basis/test_fold_rzz_angle.py diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py index eaced006f..067242227 100644 --- a/qiskit_ibm_runtime/ibm_backend.py +++ b/qiskit_ibm_runtime/ibm_backend.py @@ -919,7 +919,9 @@ def close_session(self) -> None: def get_translation_stage_plugin(self) -> str: """Return the default translation stage plugin name for IBM backends.""" - return "ibm_dynamic_circuits" + if not self.options.use_fractional_gates: + return "ibm_dynamic_circuits" + return "ibm_fractional" class IBMRetiredBackend(IBMBackend): diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py b/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py index f771fced1..dbeac2851 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py @@ -13,3 +13,4 @@ """Passes to layout circuits to IBM backend's instruction sets.""" from .convert_id_to_delay import ConvertIdToDelay +from .fold_rzz_angle import FoldRzzAngle diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py new file mode 100644 index 000000000..7ae760442 --- /dev/null +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -0,0 +1,185 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Pass to wrap Rzz gate angle in calibrated range of 0-pi/2.""" + +from typing import Tuple +from math import pi + +from qiskit.circuit.library.standard_gates import RZZGate, RZGate, XGate +from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.circuit import Qubit +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.basepasses import TransformationPass + +import numpy as np + + +class FoldRzzAngle(TransformationPass): + """Fold Rzz gate angle into calibrated range of 0-pi/2 with + local gate tweaks. + + This pass preserves the number of Rzz gates, but it may add + extra single qubit gate operations. + These gates might be reduced by the following + single qubit optimization passes. + """ + + def run(self, dag: DAGCircuit) -> DAGCircuit: + # Currently control flow ops and Rzz cannot live in the same circuit. + # Once it's supported, we need to recursively check subroutines. + for node in dag.op_nodes(): + if not isinstance(node.op, RZZGate) or isinstance( + angle := node.op.params[0], ParameterExpression + ): + continue + wrap_angle = np.angle(np.exp(1j * angle)) + if -pi <= wrap_angle <= -pi / 2: + replace = _quad3(wrap_angle, node.qargs) + elif -pi / 2 < wrap_angle < 0: + replace = _quad4(wrap_angle, node.qargs) + elif pi / 2 < wrap_angle <= pi: + replace = _quad2(wrap_angle, node.qargs) + else: + if angle != wrap_angle: + dag.substitute_node( + node, + RZZGate(wrap_angle), + inplace=True, + ) + continue + dag.substitute_node_with_dag(node, replace) + return dag + + +def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + """Handle angle between (pi/2, pi]. + + Circuit is transformed into following form: + + ┌───────┐┌───┐ ┌───┐ + q_0: ┤ Rz(π) ├┤ X ├─■──────────┤ X ├ + ├───────┤└───┘ │ZZ(π - θ) └───┘ + q_1: ┤ Rz(π) ├──────■─────────────── + └───────┘ + + Returns: + New dag to replace Rzz gate. + """ + new_dag = DAGCircuit() + new_dag.add_qubits(qubits=qubits) + new_dag.apply_operation_back( + RZGate(pi), + qargs=(qubits[0],), + cargs=(), + check=False, + ) + new_dag.apply_operation_back( + RZGate(pi), + qargs=(qubits[1],), + cargs=(), + check=False, + ) + if not np.isclose(new_angle := (pi - angle), 0.0): + new_dag.apply_operation_back( + XGate(), + qargs=(qubits[0],), + cargs=(), + check=False, + ) + new_dag.apply_operation_back( + RZZGate(new_angle), + qargs=qubits, + cargs=(), + check=False, + ) + new_dag.apply_operation_back( + XGate(), + qargs=(qubits[0],), + cargs=(), + check=False, + ) + return new_dag + + +def _quad3(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + """Handle angle between [-pi, -pi/2]. + + Circuit is transformed into following form: + + ┌───────┐ + q_0: ┤ Rz(π) ├─■─────────────── + ├───────┤ │ZZ(π - Abs(θ)) + q_1: ┤ Rz(π) ├─■─────────────── + └───────┘ + + Returns: + New dag to replace Rzz gate. + """ + new_dag = DAGCircuit() + new_dag.add_qubits(qubits=qubits) + new_dag.apply_operation_back( + RZGate(pi), + qargs=(qubits[0],), + cargs=(), + check=False, + ) + new_dag.apply_operation_back( + RZGate(pi), + qargs=(qubits[1],), + cargs=(), + check=False, + ) + if not np.isclose(new_angle := (pi - np.abs(angle)), 0.0): + new_dag.apply_operation_back( + RZZGate(new_angle), + qargs=qubits, + cargs=(), + check=False, + ) + return new_dag + + +def _quad4(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + """Handle angle between (-pi/2, 0). + + Circuit is transformed into following form: + + ┌───┐ ┌───┐ + q_0: ┤ X ├─■───────────┤ X ├ + └───┘ │ZZ(Abs(θ)) └───┘ + q_1: ──────■──────────────── + + Returns: + New dag to replace Rzz gate. + """ + new_dag = DAGCircuit() + new_dag.add_qubits(qubits=qubits) + new_dag.apply_operation_back( + XGate(), + qargs=(qubits[0],), + cargs=(), + check=False, + ) + new_dag.apply_operation_back( + RZZGate(abs(angle)), + qargs=qubits, + cargs=(), + check=False, + ) + new_dag.apply_operation_back( + XGate(), + qargs=(qubits[0],), + cargs=(), + check=False, + ) + return new_dag diff --git a/qiskit_ibm_runtime/transpiler/plugin.py b/qiskit_ibm_runtime/transpiler/plugin.py index 077534395..afe9ad6c5 100644 --- a/qiskit_ibm_runtime/transpiler/plugin.py +++ b/qiskit_ibm_runtime/transpiler/plugin.py @@ -20,8 +20,9 @@ from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes import ConvertConditionsToIfOps -from qiskit_ibm_runtime.transpiler.passes.basis.convert_id_to_delay import ( +from qiskit_ibm_runtime.transpiler.passes.basis import ( ConvertIdToDelay, + FoldRzzAngle, ) @@ -96,3 +97,41 @@ def pass_manager( plugin_passes += [ConvertConditionsToIfOps()] return PassManager(plugin_passes) + translator_pm + + +class IBMFractionalTranslationPlugin(PassManagerStagePlugin): + """A translation stage plugin for targeting Qiskit circuits + to IBM Quantum system with fractional gate support. + + Currently coexistense of fractional gate operations and + dynamic circuit is not assumed. + """ + + def pass_manager( + self, + pass_manager_config: PassManagerConfig, + optimization_level: Optional[int] = None, + ) -> PassManager: + """Build IBMTranslationPlugin PassManager.""" + + translator_pm = common.generate_translation_passmanager( + target=pass_manager_config.target, + basis_gates=pass_manager_config.basis_gates, + approximation_degree=pass_manager_config.approximation_degree, + coupling_map=pass_manager_config.coupling_map, + backend_props=pass_manager_config.backend_properties, + unitary_synthesis_method=pass_manager_config.unitary_synthesis_method, + unitary_synthesis_plugin_config=pass_manager_config.unitary_synthesis_plugin_config, + hls_config=pass_manager_config.hls_config, + ) + + instruction_durations = pass_manager_config.instruction_durations + pre_passes = [] + post_passes = [] + target = pass_manager_config.target or pass_manager_config.basis_gates + if instruction_durations and not "id" in target: + pre_passes.append(ConvertIdToDelay(instruction_durations)) + if "rzz" in target: + # Apply this pass after SU4 is translated. + post_passes.append(FoldRzzAngle()) + return PassManager(pre_passes) + translator_pm + PassManager(post_passes) diff --git a/setup.py b/setup.py index 2d29eedcd..7275e3fc1 100644 --- a/setup.py +++ b/setup.py @@ -82,6 +82,7 @@ "qiskit.transpiler.translation": [ "ibm_backend = qiskit_ibm_runtime.transpiler.plugin:IBMTranslationPlugin", "ibm_dynamic_circuits = qiskit_ibm_runtime.transpiler.plugin:IBMDynamicTranslationPlugin", + "ibm_fractional = qiskit_ibm_runtime.transpiler.plugin:IBMFractionalTranslationPlugin", ] }, extras_require={"visualization": ["plotly>=5.23.0"]}, diff --git a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py new file mode 100644 index 000000000..0c0768fd4 --- /dev/null +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -0,0 +1,63 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test folding Rzz angle into calibrated range.""" + +from math import pi +from ddt import ddt, named_data + +from qiskit.circuit import QuantumCircuit +from qiskit.quantum_info import Operator +from qiskit.transpiler.passmanager import PassManager +from qiskit.circuit.parameter import Parameter + +from qiskit_ibm_runtime.transpiler.passes.basis import FoldRzzAngle +from .....ibm_test_case import IBMTestCase + + +@ddt +class TestFoldRzzAngle(IBMTestCase): + """Test FoldRzzAngle pass""" + + @named_data( + ("large_positive_number", 12345), + ("large_negative_number", -12345), + ("pi/2_pos", pi / 2), + ("pi/2_neg", -pi / 2), + ("pi_pos", pi), + ("pi_neg", -pi), + ("quad1", 0.1), + ("quad2", pi / 2 + 0.1), + ("quad3", -pi + 0.1), + ("quad4", -0.1), + ) + def test_folding_rzz_angles(self, angle): + """Test folding gate angle into calibrated range.""" + qc = QuantumCircuit(2) + qc.rzz(angle, 0, 1) + pm = PassManager([FoldRzzAngle()]) + isa = pm.run(qc) + + self.assertTrue(Operator.from_circuit(qc).equiv(Operator.from_circuit(isa))) + for inst_data in isa.data: + if inst_data.operation.name == "rzz": + fold_angle = inst_data.operation.params[0] + self.assertGreaterEqual(fold_angle, 0.0) + self.assertLessEqual(fold_angle, pi / 2) + + def test_folding_rzz_angle_unbound(self): + """Test skip folding unbound gate angle.""" + qc = QuantumCircuit(2) + qc.rzz(Parameter("θ"), 0, 1) + pm = PassManager([FoldRzzAngle()]) + isa = pm.run(qc) + self.assertEqual(qc, isa) From 5ce4cd2d949e8507b66e53e1c00c8909d300e344 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Nov 2024 03:25:33 +0900 Subject: [PATCH 02/20] add release note --- release-notes/unreleased/2043.feat.rst | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 release-notes/unreleased/2043.feat.rst diff --git a/release-notes/unreleased/2043.feat.rst b/release-notes/unreleased/2043.feat.rst new file mode 100644 index 000000000..39cd96056 --- /dev/null +++ b/release-notes/unreleased/2043.feat.rst @@ -0,0 +1,9 @@ +Add new transpiler translation plugin :class:`IBMFractionalTranslationPlugin` +and a pass :class:`FoldRzzAngle`. +This plugin is automatically appled for backends +retrieved with the `use_fractional_gates` optin, +and the folding pass is added when the backend target includes the `RZZ` gate. + +New pass modifies the input quantum circuit so that all `RZZ` gates in the +circuit has an angle parameter within [0, pi/2] which is supported +by IBM Quantum processors. \ No newline at end of file From 55668ed2e4d1ff88d26b3557c8e364e62bd32b90 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Nov 2024 20:50:14 +0900 Subject: [PATCH 03/20] update phase check logic --- .../transpiler/passes/basis/fold_rzz_angle.py | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index 7ae760442..15d387585 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -38,25 +38,31 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # Currently control flow ops and Rzz cannot live in the same circuit. # Once it's supported, we need to recursively check subroutines. for node in dag.op_nodes(): - if not isinstance(node.op, RZZGate) or isinstance( - angle := node.op.params[0], ParameterExpression - ): + if not isinstance(node.op, RZZGate): + continue + angle = node.op.params[0] + if isinstance(angle, ParameterExpression) or 0 <= angle <= pi / 2: + # Angle is unbound parameter or calibrated value. continue wrap_angle = np.angle(np.exp(1j * angle)) - if -pi <= wrap_angle <= -pi / 2: + if 0 <= wrap_angle <= pi / 2: + # In the first quadrant after phase wrapping. + # We just need to remove 2pi offset. + dag.substitute_node( + node, + RZZGate(wrap_angle), + inplace=True, + ) + continue + elif pi /2 < wrap_angle <= pi: + # In the second quadrant. + replace = _quad2(wrap_angle, node.qargs) + elif -pi <= wrap_angle <= - pi / 2: + # In the third quadrant. replace = _quad3(wrap_angle, node.qargs) elif -pi / 2 < wrap_angle < 0: - replace = _quad4(wrap_angle, node.qargs) - elif pi / 2 < wrap_angle <= pi: - replace = _quad2(wrap_angle, node.qargs) - else: - if angle != wrap_angle: - dag.substitute_node( - node, - RZZGate(wrap_angle), - inplace=True, - ) - continue + # In the forth quadrant. + replace = _quad4(wrap_angle, node.qargs) dag.substitute_node_with_dag(node, replace) return dag From a45251afcdd2d24ca44ae5913e54a5e7398014e0 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Nov 2024 21:07:33 +0900 Subject: [PATCH 04/20] add more document --- .../transpiler/passes/basis/fold_rzz_angle.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index 15d387585..6ac4eb4d5 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -28,10 +28,26 @@ class FoldRzzAngle(TransformationPass): """Fold Rzz gate angle into calibrated range of 0-pi/2 with local gate tweaks. - This pass preserves the number of Rzz gates, but it may add - extra single qubit gate operations. - These gates might be reduced by the following - single qubit optimization passes. + In the IBM Quantum ISA, the instruction Rzz(theta) has + valid "theta" value of [0, pi/2] and any instruction outside + this range becomes non-ISA operation for the quantum backend. + The transpiler pass discovers such non-ISA Rzz gates + and folds the gate angle into the calibrated range + with addition of single qubit gates while preserving + logical equivalency of the input quantum circuit. + Added local gates might be efficiently merged into + neighboring single qubit gates by the following single qubit + optimization passes. + + This pass allows the Qiskit users to naively use the Rzz gates + with angle of arbitrary real numbers. + + .. note:: + This pass doesn't transform the circuit when the + Rzz gate angle is unbound parameter. + In this case, the user must assign gate angle before + transpile, or be responsible for choosing parameters + from the calibrated range of [0, pi/2]. """ def run(self, dag: DAGCircuit) -> DAGCircuit: @@ -54,15 +70,17 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: inplace=True, ) continue - elif pi /2 < wrap_angle <= pi: + if pi / 2 < wrap_angle <= pi: # In the second quadrant. replace = _quad2(wrap_angle, node.qargs) - elif -pi <= wrap_angle <= - pi / 2: + elif -pi <= wrap_angle <= -pi / 2: # In the third quadrant. replace = _quad3(wrap_angle, node.qargs) elif -pi / 2 < wrap_angle < 0: # In the forth quadrant. - replace = _quad4(wrap_angle, node.qargs) + replace = _quad4(wrap_angle, node.qargs) + else: + raise RuntimeError("Unreacheable.") dag.substitute_node_with_dag(node, replace) return dag From 0bc77b941eb9245ecff3729aae45a13ad592bc3d Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Nov 2024 21:13:53 +0900 Subject: [PATCH 05/20] add more angle test --- .../transpiler/passes/basis/test_fold_rzz_angle.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py index 0c0768fd4..c36f6fc65 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -14,6 +14,7 @@ from math import pi from ddt import ddt, named_data +import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.quantum_info import Operator @@ -61,3 +62,14 @@ def test_folding_rzz_angle_unbound(self): pm = PassManager([FoldRzzAngle()]) isa = pm.run(qc) self.assertEqual(qc, isa) + + def test_identity(self): + """Create identity circuit with angles with [-4pi, 4pi] and transpile.""" + # Angle sum is zero + angles = pi * np.linspace(-4, 4, 17) + qc = QuantumCircuit(2) + for angle in angles: + qc.rzz(angle, 0, 1) + pm = PassManager([FoldRzzAngle()]) + isa = pm.run(qc) + self.assertTrue(Operator.from_label("II").equiv(Operator.from_circuit(isa))) From 013a6b45f54138587ee95ec7ead0e31250d58570 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Nov 2024 22:03:26 +0900 Subject: [PATCH 06/20] support control flow --- .../transpiler/passes/basis/fold_rzz_angle.py | 38 +++++++++++++++++-- .../passes/basis/test_fold_rzz_angle.py | 29 +++++++++++++- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index 6ac4eb4d5..4cf624410 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -15,9 +15,10 @@ from typing import Tuple from math import pi +from qiskit.converters import dag_to_circuit, circuit_to_dag from qiskit.circuit.library.standard_gates import RZZGate, RZGate, XGate from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.circuit import Qubit +from qiskit.circuit import Qubit, ControlFlowOp from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass @@ -51,15 +52,44 @@ class FoldRzzAngle(TransformationPass): """ def run(self, dag: DAGCircuit) -> DAGCircuit: - # Currently control flow ops and Rzz cannot live in the same circuit. - # Once it's supported, we need to recursively check subroutines. + self._run_inner(dag) + return dag + + def _run_inner(self, dag: DAGCircuit) -> DAGCircuit: + """Mutate the input dag to fix non-ISA Rzz angles.""" + modified = False for node in dag.op_nodes(): + if isinstance(node.op, ControlFlowOp): + modified_blocks = False + new_blocks = [] + for block in node.op.blocks: + block_dag = circuit_to_dag(block) + if self._run_inner(block_dag): + # Store circuit with Rzz modification + new_blocks.append(dag_to_circuit(block_dag)) + modified_blocks = True + else: + # Keep original circuit to save memory + new_blocks.append(block) + if modified_blocks: + dag.substitute_node( + node, + node.op.replace_blocks(new_blocks), + inplace=True, + ) + modified = True + continue + if not isinstance(node.op, RZZGate): continue + angle = node.op.params[0] if isinstance(angle, ParameterExpression) or 0 <= angle <= pi / 2: # Angle is unbound parameter or calibrated value. continue + + # Modify circuit around Rzz gate to address non-ISA angles. + modified = True wrap_angle = np.angle(np.exp(1j * angle)) if 0 <= wrap_angle <= pi / 2: # In the first quadrant after phase wrapping. @@ -82,7 +112,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: else: raise RuntimeError("Unreacheable.") dag.substitute_node_with_dag(node, replace) - return dag + return modified def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: diff --git a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py index c36f6fc65..10662ed44 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -62,7 +62,7 @@ def test_folding_rzz_angle_unbound(self): pm = PassManager([FoldRzzAngle()]) isa = pm.run(qc) self.assertEqual(qc, isa) - + def test_identity(self): """Create identity circuit with angles with [-4pi, 4pi] and transpile.""" # Angle sum is zero @@ -73,3 +73,30 @@ def test_identity(self): pm = PassManager([FoldRzzAngle()]) isa = pm.run(qc) self.assertTrue(Operator.from_label("II").equiv(Operator.from_circuit(isa))) + + def test_controlflow(self): + """Test non-ISA Rzz gates inside/outside a control flow branch.""" + qc = QuantumCircuit(2, 1) + qc.rzz(-0.2, 0, 1) + with qc.if_test((0, 1)): # pylint: disable=not-context-manager + qc.rzz(-0.1, 0, 1) + with qc.if_test((0, 1)): # pylint: disable=not-context-manager + qc.rzz(-0.3, 0, 1) + + pm = PassManager([FoldRzzAngle()]) + isa = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.x(0) + expected.rzz(0.2, 0, 1) + expected.x(0) + with expected.if_test((0, 1)): # pylint: disable=not-context-manager + expected.x(0) + expected.rzz(0.1, 0, 1) + expected.x(0) + with expected.if_test((0, 1)): # pylint: disable=not-context-manager + expected.x(0) + expected.rzz(0.3, 0, 1) + expected.x(0) + + self.assertEqual(isa, expected) From c3c2af2e6592ac031040b419ad1ddf3481272b66 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Nov 2024 22:12:49 +0900 Subject: [PATCH 07/20] Update qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py Co-authored-by: Yael Ben-Haim --- qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index 4cf624410..b99fc5efd 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -45,7 +45,7 @@ class FoldRzzAngle(TransformationPass): .. note:: This pass doesn't transform the circuit when the - Rzz gate angle is unbound parameter. + Rzz gate angle is an unbound parameter. In this case, the user must assign gate angle before transpile, or be responsible for choosing parameters from the calibrated range of [0, pi/2]. From 8ad894759ef280419537aa9ca9af368586568293 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Nov 2024 22:12:54 +0900 Subject: [PATCH 08/20] Update qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py Co-authored-by: Yael Ben-Haim --- qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index b99fc5efd..f59716957 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -46,7 +46,7 @@ class FoldRzzAngle(TransformationPass): .. note:: This pass doesn't transform the circuit when the Rzz gate angle is an unbound parameter. - In this case, the user must assign gate angle before + In this case, the user must assign a gate angle before transpile, or be responsible for choosing parameters from the calibrated range of [0, pi/2]. """ From 5285946534e38a3294bb85246ff2744f41f7e28a Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Nov 2024 22:13:06 +0900 Subject: [PATCH 09/20] Update qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py Co-authored-by: Yael Ben-Haim --- qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index f59716957..bcfcd97b0 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -47,7 +47,7 @@ class FoldRzzAngle(TransformationPass): This pass doesn't transform the circuit when the Rzz gate angle is an unbound parameter. In this case, the user must assign a gate angle before - transpile, or be responsible for choosing parameters + transpilation, or be responsible for choosing parameters from the calibrated range of [0, pi/2]. """ From 75d86d609779b14b62421aab9578886fd6b6de22 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Mon, 25 Nov 2024 11:39:08 -0500 Subject: [PATCH 10/20] Update documentation wording Co-authored-by: Yael Ben-Haim --- .../transpiler/passes/basis/fold_rzz_angle.py | 8 ++++---- qiskit_ibm_runtime/transpiler/plugin.py | 6 +++--- release-notes/unreleased/2043.feat.rst | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index bcfcd97b0..14342221b 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -31,7 +31,7 @@ class FoldRzzAngle(TransformationPass): In the IBM Quantum ISA, the instruction Rzz(theta) has valid "theta" value of [0, pi/2] and any instruction outside - this range becomes non-ISA operation for the quantum backend. + this range becomes a non-ISA operation for the quantum backend. The transpiler pass discovers such non-ISA Rzz gates and folds the gate angle into the calibrated range with addition of single qubit gates while preserving @@ -55,7 +55,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: self._run_inner(dag) return dag - def _run_inner(self, dag: DAGCircuit) -> DAGCircuit: + def _run_inner(self, dag: DAGCircuit) -> bool: """Mutate the input dag to fix non-ISA Rzz angles.""" modified = False for node in dag.op_nodes(): @@ -85,7 +85,7 @@ def _run_inner(self, dag: DAGCircuit) -> DAGCircuit: angle = node.op.params[0] if isinstance(angle, ParameterExpression) or 0 <= angle <= pi / 2: - # Angle is unbound parameter or calibrated value. + # Angle is an unbound parameter or a calibrated value. continue # Modify circuit around Rzz gate to address non-ISA angles. @@ -118,7 +118,7 @@ def _run_inner(self, dag: DAGCircuit) -> DAGCircuit: def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: """Handle angle between (pi/2, pi]. - Circuit is transformed into following form: + Circuit is transformed into the following form: ┌───────┐┌───┐ ┌───┐ q_0: ┤ Rz(π) ├┤ X ├─■──────────┤ X ├ diff --git a/qiskit_ibm_runtime/transpiler/plugin.py b/qiskit_ibm_runtime/transpiler/plugin.py index afe9ad6c5..b268b6a4e 100644 --- a/qiskit_ibm_runtime/transpiler/plugin.py +++ b/qiskit_ibm_runtime/transpiler/plugin.py @@ -101,10 +101,10 @@ def pass_manager( class IBMFractionalTranslationPlugin(PassManagerStagePlugin): """A translation stage plugin for targeting Qiskit circuits - to IBM Quantum system with fractional gate support. + to IBM Quantum systems with fractional gate support. - Currently coexistense of fractional gate operations and - dynamic circuit is not assumed. + Currently coexistence of fractional gate operations and + dynamic circuits is not assumed. """ def pass_manager( diff --git a/release-notes/unreleased/2043.feat.rst b/release-notes/unreleased/2043.feat.rst index 39cd96056..4bfd12f90 100644 --- a/release-notes/unreleased/2043.feat.rst +++ b/release-notes/unreleased/2043.feat.rst @@ -1,9 +1,9 @@ -Add new transpiler translation plugin :class:`IBMFractionalTranslationPlugin` +Added a new transpiler translation plugin :class:`IBMFractionalTranslationPlugin` and a pass :class:`FoldRzzAngle`. -This plugin is automatically appled for backends +This plugin is automatically applied for backends retrieved with the `use_fractional_gates` optin, and the folding pass is added when the backend target includes the `RZZ` gate. -New pass modifies the input quantum circuit so that all `RZZ` gates in the -circuit has an angle parameter within [0, pi/2] which is supported +The new pass modifies the input quantum circuit, so that all `RZZ` gates in the +circuit have an angle parameter within [0, pi/2] which is supported by IBM Quantum processors. \ No newline at end of file From ec478c4ed8a668f85daeeda695d80872ed3c9000 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 27 Nov 2024 10:57:22 +0200 Subject: [PATCH 11/20] update test to require the same phase --- .../transpiler/passes/basis/test_fold_rzz_angle.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py index 10662ed44..963aacb65 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -36,10 +36,14 @@ class TestFoldRzzAngle(IBMTestCase): ("pi/2_neg", -pi / 2), ("pi_pos", pi), ("pi_neg", -pi), - ("quad1", 0.1), - ("quad2", pi / 2 + 0.1), - ("quad3", -pi + 0.1), - ("quad4", -0.1), + ("quad1_no_wrap", 0.1), + ("quad2_no_wrap", pi / 2 + 0.1), + ("quad3_no_wrap", -pi + 0.1), + ("quad4_no_wrap", -0.1), + ("quad1_wrap", 2 * pi + 0.1), + ("quad2_wrap", - 3 * pi / 2 + 0.1), + ("quad3_wrap", pi + 0.1), + ("quad4_wrap", 2 * pi - 0.1) ) def test_folding_rzz_angles(self, angle): """Test folding gate angle into calibrated range.""" @@ -48,7 +52,7 @@ def test_folding_rzz_angles(self, angle): pm = PassManager([FoldRzzAngle()]) isa = pm.run(qc) - self.assertTrue(Operator.from_circuit(qc).equiv(Operator.from_circuit(isa))) + self.assertEqual(Operator.from_circuit(qc), Operator.from_circuit(isa)) for inst_data in isa.data: if inst_data.operation.name == "rzz": fold_angle = inst_data.operation.params[0] From 13524bcf8628910298dec15bf319eea2ff3890da Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 27 Nov 2024 14:26:54 +0200 Subject: [PATCH 12/20] fix quad2 no wrap --- .../transpiler/passes/basis/fold_rzz_angle.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index 14342221b..aaf677303 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -16,7 +16,7 @@ from math import pi from qiskit.converters import dag_to_circuit, circuit_to_dag -from qiskit.circuit.library.standard_gates import RZZGate, RZGate, XGate +from qiskit.circuit.library.standard_gates import RZZGate, RZGate, XGate, GlobalPhaseGate from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit import Qubit, ControlFlowOp from qiskit.dagcircuit import DAGCircuit @@ -131,6 +131,7 @@ def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: """ new_dag = DAGCircuit() new_dag.add_qubits(qubits=qubits) + new_dag.apply_operation_back(GlobalPhaseGate(pi / 2)) new_dag.apply_operation_back( RZGate(pi), qargs=(qubits[0],), @@ -140,26 +141,22 @@ def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: new_dag.apply_operation_back( RZGate(pi), qargs=(qubits[1],), - cargs=(), check=False, ) if not np.isclose(new_angle := (pi - angle), 0.0): new_dag.apply_operation_back( XGate(), qargs=(qubits[0],), - cargs=(), check=False, ) new_dag.apply_operation_back( RZZGate(new_angle), qargs=qubits, - cargs=(), check=False, ) new_dag.apply_operation_back( XGate(), qargs=(qubits[0],), - cargs=(), check=False, ) return new_dag @@ -184,20 +181,17 @@ def _quad3(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: new_dag.apply_operation_back( RZGate(pi), qargs=(qubits[0],), - cargs=(), check=False, ) new_dag.apply_operation_back( RZGate(pi), qargs=(qubits[1],), - cargs=(), check=False, ) if not np.isclose(new_angle := (pi - np.abs(angle)), 0.0): new_dag.apply_operation_back( RZZGate(new_angle), qargs=qubits, - cargs=(), check=False, ) return new_dag @@ -221,19 +215,16 @@ def _quad4(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: new_dag.apply_operation_back( XGate(), qargs=(qubits[0],), - cargs=(), check=False, ) new_dag.apply_operation_back( RZZGate(abs(angle)), qargs=qubits, - cargs=(), check=False, ) new_dag.apply_operation_back( XGate(), qargs=(qubits[0],), - cargs=(), check=False, ) return new_dag From 38a9d95df7d989e61c17da5f44be5349b5bad3f5 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 27 Nov 2024 14:33:34 +0200 Subject: [PATCH 13/20] fix quad3 no wrap --- qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index aaf677303..ada163606 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -178,6 +178,7 @@ def _quad3(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: """ new_dag = DAGCircuit() new_dag.add_qubits(qubits=qubits) + new_dag.apply_operation_back(GlobalPhaseGate(- pi / 2)) new_dag.apply_operation_back( RZGate(pi), qargs=(qubits[0],), From 88c89ea0f519f29710f21dccd4c1737710caecd9 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 27 Nov 2024 14:52:43 +0200 Subject: [PATCH 14/20] apply a global phase of pi for a 2pi angle offset --- .../transpiler/passes/basis/fold_rzz_angle.py | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index ada163606..55e51a3e7 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -92,15 +92,9 @@ def _run_inner(self, dag: DAGCircuit) -> bool: modified = True wrap_angle = np.angle(np.exp(1j * angle)) if 0 <= wrap_angle <= pi / 2: - # In the first quadrant after phase wrapping. - # We just need to remove 2pi offset. - dag.substitute_node( - node, - RZZGate(wrap_angle), - inplace=True, - ) - continue - if pi / 2 < wrap_angle <= pi: + # In the first quadrant. + replace = _quad1(wrap_angle, node.qargs) + elif pi / 2 < wrap_angle <= pi: # In the second quadrant. replace = _quad2(wrap_angle, node.qargs) elif -pi <= wrap_angle <= -pi / 2: @@ -111,10 +105,30 @@ def _run_inner(self, dag: DAGCircuit) -> bool: replace = _quad4(wrap_angle, node.qargs) else: raise RuntimeError("Unreacheable.") + if not np.isclose(angle, wrap_angle): + replace.apply_operation_back(GlobalPhaseGate(pi)) dag.substitute_node_with_dag(node, replace) return modified +def _quad1(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + """Handle angle between [0, pi/2]. + + Circuit is not transformed - the Rzz gate is calibrated for the angle. + + Returns: + A new dag with the same Rzz gate. + """ + new_dag = DAGCircuit() + new_dag.add_qubits(qubits=qubits) + new_dag.apply_operation_back( + RZZGate(angle), + qargs=qubits, + check=False, + ) + return new_dag + + def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: """Handle angle between (pi/2, pi]. From c7e6ceaaacf5da4e375a254ee226cdbf2539a176 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 27 Nov 2024 14:58:32 +0200 Subject: [PATCH 15/20] black --- qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py | 2 +- test/unit/transpiler/passes/basis/test_fold_rzz_angle.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index 55e51a3e7..17958caa9 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -192,7 +192,7 @@ def _quad3(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: """ new_dag = DAGCircuit() new_dag.add_qubits(qubits=qubits) - new_dag.apply_operation_back(GlobalPhaseGate(- pi / 2)) + new_dag.apply_operation_back(GlobalPhaseGate(-pi / 2)) new_dag.apply_operation_back( RZGate(pi), qargs=(qubits[0],), diff --git a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py index 963aacb65..bbd3c417d 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -41,9 +41,9 @@ class TestFoldRzzAngle(IBMTestCase): ("quad3_no_wrap", -pi + 0.1), ("quad4_no_wrap", -0.1), ("quad1_wrap", 2 * pi + 0.1), - ("quad2_wrap", - 3 * pi / 2 + 0.1), + ("quad2_wrap", -3 * pi / 2 + 0.1), ("quad3_wrap", pi + 0.1), - ("quad4_wrap", 2 * pi - 0.1) + ("quad4_wrap", 2 * pi - 0.1), ) def test_folding_rzz_angles(self, angle): """Test folding gate angle into calibrated range.""" From 7ec8a81d94977c205c4b747d180318503fe1405d Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Mon, 2 Dec 2024 10:55:35 +0200 Subject: [PATCH 16/20] bug fix --- .../transpiler/passes/basis/fold_rzz_angle.py | 5 +++-- .../transpiler/passes/basis/test_fold_rzz_angle.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index 17958caa9..e2a39e79e 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -56,7 +56,8 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: return dag def _run_inner(self, dag: DAGCircuit) -> bool: - """Mutate the input dag to fix non-ISA Rzz angles.""" + """Mutate the input dag to fix non-ISA Rzz angles. + Return true if the dag was modified.""" modified = False for node in dag.op_nodes(): if isinstance(node.op, ControlFlowOp): @@ -105,7 +106,7 @@ def _run_inner(self, dag: DAGCircuit) -> bool: replace = _quad4(wrap_angle, node.qargs) else: raise RuntimeError("Unreacheable.") - if not np.isclose(angle, wrap_angle): + if pi < angle % (4 * pi) < 3 * pi: replace.apply_operation_back(GlobalPhaseGate(pi)) dag.substitute_node_with_dag(node, replace) return modified diff --git a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py index bbd3c417d..70213875a 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -30,8 +30,6 @@ class TestFoldRzzAngle(IBMTestCase): """Test FoldRzzAngle pass""" @named_data( - ("large_positive_number", 12345), - ("large_negative_number", -12345), ("pi/2_pos", pi / 2), ("pi/2_neg", -pi / 2), ("pi_pos", pi), @@ -40,10 +38,14 @@ class TestFoldRzzAngle(IBMTestCase): ("quad2_no_wrap", pi / 2 + 0.1), ("quad3_no_wrap", -pi + 0.1), ("quad4_no_wrap", -0.1), - ("quad1_wrap", 2 * pi + 0.1), - ("quad2_wrap", -3 * pi / 2 + 0.1), - ("quad3_wrap", pi + 0.1), - ("quad4_wrap", 2 * pi - 0.1), + ("quad1_2pi_wrap", 2 * pi + 0.1), + ("quad2_2pi_wrap", -3 * pi / 2 + 0.1), + ("quad3_2pi_wrap", pi + 0.1), + ("quad4_2pi_wrap", 2 * pi - 0.1), + ("quad1_12pi_wrap", -12 * pi + 0.1), + ("quad2_12pi_wrap", 23 * pi / 2 + 0.1), + ("quad3_12pi_wrap", 11 * pi + 0.1), + ("quad4_12pi_wrap", -12 * pi - 0.1), ) def test_folding_rzz_angles(self, angle): """Test folding gate angle into calibrated range.""" From bae92098305ade8ab2b9aeaf7a114cccba8b55d8 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Tue, 3 Dec 2024 11:47:27 +0200 Subject: [PATCH 17/20] wrote test_fractional_plugin --- .../passes/basis/test_fold_rzz_angle.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py index 70213875a..1ec404d71 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -17,11 +17,13 @@ import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.quantum_info import Operator -from qiskit.transpiler.passmanager import PassManager from qiskit.circuit.parameter import Parameter +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit.quantum_info import Operator from qiskit_ibm_runtime.transpiler.passes.basis import FoldRzzAngle +from qiskit_ibm_runtime.fake_provider import FakeFractionalBackend from .....ibm_test_case import IBMTestCase @@ -106,3 +108,21 @@ def test_controlflow(self): expected.x(0) self.assertEqual(isa, expected) + + def test_fractional_plugin(self): + """Verify that a pass manager created for a fractional backend applies the rzz folding + pass""" + + circ = QuantumCircuit(2) + circ.rzz(7, 0, 1) + + pm = generate_preset_pass_manager( + optimization_level=0, + backend=FakeFractionalBackend(), + translation_method="ibm_fractional", + ) + isa_circ = pm.run(circ) + + self.assertEqual(isa_circ.data[0].operation.name, "global_phase") + self.assertEqual(isa_circ.data[1].operation.name, "rzz") + self.assertTrue(np.isclose(isa_circ.data[1].operation.params[0], 7 - 2 * pi)) From 4b08e7e09588aa6352ecb893008ae56527b47aa2 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Tue, 3 Dec 2024 12:08:20 +0200 Subject: [PATCH 18/20] made the _quad functions static methods of the class --- .../transpiler/passes/basis/fold_rzz_angle.py | 238 +++++++++--------- 1 file changed, 119 insertions(+), 119 deletions(-) diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py index e2a39e79e..a7996d235 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -94,16 +94,16 @@ def _run_inner(self, dag: DAGCircuit) -> bool: wrap_angle = np.angle(np.exp(1j * angle)) if 0 <= wrap_angle <= pi / 2: # In the first quadrant. - replace = _quad1(wrap_angle, node.qargs) + replace = self._quad1(wrap_angle, node.qargs) elif pi / 2 < wrap_angle <= pi: # In the second quadrant. - replace = _quad2(wrap_angle, node.qargs) + replace = self._quad2(wrap_angle, node.qargs) elif -pi <= wrap_angle <= -pi / 2: # In the third quadrant. - replace = _quad3(wrap_angle, node.qargs) + replace = self._quad3(wrap_angle, node.qargs) elif -pi / 2 < wrap_angle < 0: # In the forth quadrant. - replace = _quad4(wrap_angle, node.qargs) + replace = self._quad4(wrap_angle, node.qargs) else: raise RuntimeError("Unreacheable.") if pi < angle % (4 * pi) < 3 * pi: @@ -111,136 +111,136 @@ def _run_inner(self, dag: DAGCircuit) -> bool: dag.substitute_node_with_dag(node, replace) return modified + @staticmethod + def _quad1(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + """Handle angle between [0, pi/2]. -def _quad1(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: - """Handle angle between [0, pi/2]. + Circuit is not transformed - the Rzz gate is calibrated for the angle. - Circuit is not transformed - the Rzz gate is calibrated for the angle. - - Returns: - A new dag with the same Rzz gate. - """ - new_dag = DAGCircuit() - new_dag.add_qubits(qubits=qubits) - new_dag.apply_operation_back( - RZZGate(angle), - qargs=qubits, - check=False, - ) - return new_dag - - -def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: - """Handle angle between (pi/2, pi]. - - Circuit is transformed into the following form: - - ┌───────┐┌───┐ ┌───┐ - q_0: ┤ Rz(π) ├┤ X ├─■──────────┤ X ├ - ├───────┤└───┘ │ZZ(π - θ) └───┘ - q_1: ┤ Rz(π) ├──────■─────────────── - └───────┘ - - Returns: - New dag to replace Rzz gate. - """ - new_dag = DAGCircuit() - new_dag.add_qubits(qubits=qubits) - new_dag.apply_operation_back(GlobalPhaseGate(pi / 2)) - new_dag.apply_operation_back( - RZGate(pi), - qargs=(qubits[0],), - cargs=(), - check=False, - ) - new_dag.apply_operation_back( - RZGate(pi), - qargs=(qubits[1],), - check=False, - ) - if not np.isclose(new_angle := (pi - angle), 0.0): + Returns: + A new dag with the same Rzz gate. + """ + new_dag = DAGCircuit() + new_dag.add_qubits(qubits=qubits) new_dag.apply_operation_back( - XGate(), + RZZGate(angle), + qargs=qubits, + check=False, + ) + return new_dag + + @staticmethod + def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + """Handle angle between (pi/2, pi]. + + Circuit is transformed into the following form: + + ┌───────┐┌───┐ ┌───┐ + q_0: ┤ Rz(π) ├┤ X ├─■──────────┤ X ├ + ├───────┤└───┘ │ZZ(π - θ) └───┘ + q_1: ┤ Rz(π) ├──────■─────────────── + └───────┘ + + Returns: + New dag to replace Rzz gate. + """ + new_dag = DAGCircuit() + new_dag.add_qubits(qubits=qubits) + new_dag.apply_operation_back(GlobalPhaseGate(pi / 2)) + new_dag.apply_operation_back( + RZGate(pi), qargs=(qubits[0],), + cargs=(), check=False, ) new_dag.apply_operation_back( - RZZGate(new_angle), - qargs=qubits, + RZGate(pi), + qargs=(qubits[1],), + check=False, + ) + if not np.isclose(new_angle := (pi - angle), 0.0): + new_dag.apply_operation_back( + XGate(), + qargs=(qubits[0],), + check=False, + ) + new_dag.apply_operation_back( + RZZGate(new_angle), + qargs=qubits, + check=False, + ) + new_dag.apply_operation_back( + XGate(), + qargs=(qubits[0],), + check=False, + ) + return new_dag + + @staticmethod + def _quad3(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + """Handle angle between [-pi, -pi/2]. + + Circuit is transformed into following form: + + ┌───────┐ + q_0: ┤ Rz(π) ├─■─────────────── + ├───────┤ │ZZ(π - Abs(θ)) + q_1: ┤ Rz(π) ├─■─────────────── + └───────┘ + + Returns: + New dag to replace Rzz gate. + """ + new_dag = DAGCircuit() + new_dag.add_qubits(qubits=qubits) + new_dag.apply_operation_back(GlobalPhaseGate(-pi / 2)) + new_dag.apply_operation_back( + RZGate(pi), + qargs=(qubits[0],), + check=False, + ) + new_dag.apply_operation_back( + RZGate(pi), + qargs=(qubits[1],), check=False, ) + if not np.isclose(new_angle := (pi - np.abs(angle)), 0.0): + new_dag.apply_operation_back( + RZZGate(new_angle), + qargs=qubits, + check=False, + ) + return new_dag + + @staticmethod + def _quad4(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + """Handle angle between (-pi/2, 0). + + Circuit is transformed into following form: + + ┌───┐ ┌───┐ + q_0: ┤ X ├─■───────────┤ X ├ + └───┘ │ZZ(Abs(θ)) └───┘ + q_1: ──────■──────────────── + + Returns: + New dag to replace Rzz gate. + """ + new_dag = DAGCircuit() + new_dag.add_qubits(qubits=qubits) new_dag.apply_operation_back( XGate(), qargs=(qubits[0],), check=False, ) - return new_dag - - -def _quad3(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: - """Handle angle between [-pi, -pi/2]. - - Circuit is transformed into following form: - - ┌───────┐ - q_0: ┤ Rz(π) ├─■─────────────── - ├───────┤ │ZZ(π - Abs(θ)) - q_1: ┤ Rz(π) ├─■─────────────── - └───────┘ - - Returns: - New dag to replace Rzz gate. - """ - new_dag = DAGCircuit() - new_dag.add_qubits(qubits=qubits) - new_dag.apply_operation_back(GlobalPhaseGate(-pi / 2)) - new_dag.apply_operation_back( - RZGate(pi), - qargs=(qubits[0],), - check=False, - ) - new_dag.apply_operation_back( - RZGate(pi), - qargs=(qubits[1],), - check=False, - ) - if not np.isclose(new_angle := (pi - np.abs(angle)), 0.0): new_dag.apply_operation_back( - RZZGate(new_angle), + RZZGate(abs(angle)), qargs=qubits, check=False, ) - return new_dag - - -def _quad4(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: - """Handle angle between (-pi/2, 0). - - Circuit is transformed into following form: - - ┌───┐ ┌───┐ - q_0: ┤ X ├─■───────────┤ X ├ - └───┘ │ZZ(Abs(θ)) └───┘ - q_1: ──────■──────────────── - - Returns: - New dag to replace Rzz gate. - """ - new_dag = DAGCircuit() - new_dag.add_qubits(qubits=qubits) - new_dag.apply_operation_back( - XGate(), - qargs=(qubits[0],), - check=False, - ) - new_dag.apply_operation_back( - RZZGate(abs(angle)), - qargs=qubits, - check=False, - ) - new_dag.apply_operation_back( - XGate(), - qargs=(qubits[0],), - check=False, - ) - return new_dag + new_dag.apply_operation_back( + XGate(), + qargs=(qubits[0],), + check=False, + ) + return new_dag From 9c88806345c059030d2ba49180d51b5c24385f44 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 4 Dec 2024 10:03:44 +0200 Subject: [PATCH 19/20] Update release-notes/unreleased/2043.feat.rst Co-authored-by: Will Shanks --- release-notes/unreleased/2043.feat.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/release-notes/unreleased/2043.feat.rst b/release-notes/unreleased/2043.feat.rst index 4bfd12f90..03f08e4ae 100644 --- a/release-notes/unreleased/2043.feat.rst +++ b/release-notes/unreleased/2043.feat.rst @@ -1,9 +1,9 @@ -Added a new transpiler translation plugin :class:`IBMFractionalTranslationPlugin` -and a pass :class:`FoldRzzAngle`. +Added a new transpiler translation plugin :class:`~.IBMFractionalTranslationPlugin` +and a pass :class:`~.FoldRzzAngle`. This plugin is automatically applied for backends -retrieved with the `use_fractional_gates` optin, -and the folding pass is added when the backend target includes the `RZZ` gate. +retrieved with the ``use_fractional_gates`` opt-in, +and the folding pass is added when the backend target includes the ``RZZ`` gate. -The new pass modifies the input quantum circuit, so that all `RZZ` gates in the +The new pass modifies the input quantum circuit, so that all ``RZZ`` gates in the circuit have an angle parameter within [0, pi/2] which is supported by IBM Quantum processors. \ No newline at end of file From 5c38a6e8f352164a7b757f294d95df53f3db3689 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 4 Dec 2024 10:05:44 +0200 Subject: [PATCH 20/20] deleted a test --- .../transpiler/passes/basis/test_fold_rzz_angle.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py index 1ec404d71..a9d035e8f 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -71,17 +71,6 @@ def test_folding_rzz_angle_unbound(self): isa = pm.run(qc) self.assertEqual(qc, isa) - def test_identity(self): - """Create identity circuit with angles with [-4pi, 4pi] and transpile.""" - # Angle sum is zero - angles = pi * np.linspace(-4, 4, 17) - qc = QuantumCircuit(2) - for angle in angles: - qc.rzz(angle, 0, 1) - pm = PassManager([FoldRzzAngle()]) - isa = pm.run(qc) - self.assertTrue(Operator.from_label("II").equiv(Operator.from_circuit(isa))) - def test_controlflow(self): """Test non-ISA Rzz gates inside/outside a control flow branch.""" qc = QuantumCircuit(2, 1)