From 8e5981cce25861e319a5463224509326b73f2a13 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 4 Dec 2024 15:19:17 +0200 Subject: [PATCH 01/16] extended rzz pass for unbounded parameters - a very initial commit --- .../transpiler/passes/basis/fold_rzz_angle.py | 120 ++++++++++++++---- .../passes/basis/test_fold_rzz_angle.py | 37 +++++- 2 files changed, 128 insertions(+), 29 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 a7996d235..874909a88 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -14,9 +14,10 @@ from typing import Tuple from math import pi +from operator import mod from qiskit.converters import dag_to_circuit, circuit_to_dag -from qiskit.circuit.library.standard_gates import RZZGate, RZGate, XGate, GlobalPhaseGate +from qiskit.circuit.library.standard_gates import RZZGate, RZGate, XGate, GlobalPhaseGate, RXGate from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit import Qubit, ControlFlowOp from qiskit.dagcircuit import DAGCircuit @@ -42,13 +43,6 @@ class FoldRzzAngle(TransformationPass): 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 an unbound parameter. - In this case, the user must assign a gate angle before - transpilation, or be responsible for choosing parameters - from the calibrated range of [0, pi/2]. """ def run(self, dag: DAGCircuit) -> DAGCircuit: @@ -85,31 +79,107 @@ def _run_inner(self, dag: DAGCircuit) -> bool: continue angle = node.op.params[0] - if isinstance(angle, ParameterExpression) or 0 <= angle <= pi / 2: + + if not isinstance(angle, ParameterExpression) and 0 <= angle <= pi / 2: # Angle is an unbound parameter or a 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. - replace = self._quad1(wrap_angle, node.qargs) - elif pi / 2 < wrap_angle <= pi: - # In the second quadrant. - replace = self._quad2(wrap_angle, node.qargs) - elif -pi <= wrap_angle <= -pi / 2: - # In the third quadrant. - replace = self._quad3(wrap_angle, node.qargs) - elif -pi / 2 < wrap_angle < 0: - # In the forth quadrant. - replace = self._quad4(wrap_angle, node.qargs) + if isinstance(angle, ParameterExpression): + #replace = self._unbounded_parameter(angle, node.qrgs) + pass else: - raise RuntimeError("Unreacheable.") - if pi < angle % (4 * pi) < 3 * pi: - replace.apply_operation_back(GlobalPhaseGate(pi)) + replace = self._numeric_parameter(angle, node.qargs) + dag.substitute_node_with_dag(node, replace) + return modified + + # The next functions are required because sympy doesn't convert Boolean values to integers. + # symengine maybe does but I failed to find it in its documentation. + @staticmethod + def gt_op(exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: + tmp = (exp1 - exp2).sign() + + # We want to return 1 if tmp is -1 or 0, and 1 otherwise + return tmp * tmp * (tmp + 1) / 2 + + @staticmethod + def and_op(exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: + return exp1 * exp2 + + @staticmethod + def between(exp: ParameterExpression, lower: ParameterExpression, upper: ParameterExpression): + return and_op(gt_op(exp, lower), gt_op(upper, exp)) + + def _unbounded_parameter(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + wrap_angle = (angle + pi)._apply_operation(mod, 2 * pi) - pi + + pi_phase = pi * self.between(angle._apply_operation(mod, 4 * pi), pi, 3 * pi) + + quad1 = self.between(wrap_angle, 0, pi / 2) + quad2 = self.between(wrap_angle, pi / 2, pi) + quad3 = self.between(wrap_angle, - pi, - pi / 2) + quad4 = self.between(wrap_angle, - pi / 2, 0) + + global_phase = quad2 * pi / 2 + quad3 * (- pi / 2) + pi_phase * pi + rz_angle = quad2 * pi + quad3 * pi + rx_angle = quad2 * pi + quad4 * pi + rzz_angle = quad1 * wrap_angle + quad2 * (pi - wrap_angle) + quad3 * (pi + wrap_angle) + quad4 * (- wrap_angle) + + new_dag = DAGCircuit() + new_dag.add_qubits(qubits=qubits) + new_dag.apply_operation_back(GlobalPhaseGate(global_phase)) + new_dag.apply_operation_back( + RZGate(rz_angle), + qargs=(qubits[0],), + cargs=(), + check=False, + ) + new_dag.apply_operation_back( + RZGate(rz_angle), + qargs=(qubits[1],), + check=False, + ) + new_dag.apply_operation_back( + RXGate(rx_angle), + qargs=(qubits[0],), + check=False, + ) + new_dag.apply_operation_back( + RZZGate(rzz_angle), + qargs=qubits, + check=False, + ) + new_dag.apply_operation_back( + RXGate(rx_angle), + qargs=(qubits[0],), + check=False, + ) + + return new_dag + + def _numeric_parameter(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + wrap_angle = np.angle(np.exp(1j * angle)) + if 0 <= wrap_angle <= pi / 2: + # In the first quadrant. + replace = self._quad1(wrap_angle, qubits) + elif pi / 2 < wrap_angle <= pi: + # In the second quadrant. + replace = self._quad2(wrap_angle, qubits) + elif -pi <= wrap_angle <= -pi / 2: + # In the third quadrant. + replace = self._quad3(wrap_angle, qubits) + elif -pi / 2 < wrap_angle < 0: + # In the forth quadrant. + replace = self._quad4(wrap_angle, qubits) + else: + raise RuntimeError("Unreacheable.") + if pi < angle % (4 * pi) < 3 * pi: + replace.apply_operation_back(GlobalPhaseGate(pi)) + + return replace @staticmethod def _quad1(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 a9d035e8f..35d922df4 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -63,13 +63,42 @@ def test_folding_rzz_angles(self, angle): 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.""" + @named_data( + ("pi/2_pos", pi / 2), + ("pi/2_neg", -pi / 2), + ("pi_pos", pi), + ("pi_neg", -pi), + ("quad1_no_wrap", 0.1), + ("quad2_no_wrap", pi / 2 + 0.1), + ("quad3_no_wrap", -pi + 0.1), + ("quad4_no_wrap", -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_angle_unbound(self, angle): + """Test transformation in the case of an unbounded parameter""" + param = Parameter("angle") + qc = QuantumCircuit(2) - qc.rzz(Parameter("θ"), 0, 1) + qc.rzz(param, 0, 1) pm = PassManager([FoldRzzAngle()]) isa = pm.run(qc) - self.assertEqual(qc, isa) + + qc.assign_parameters({param: angle}, inplace=True) + isa.assign_parameters({param: angle}, inplace=True) + + 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] + self.assertGreaterEqual(fold_angle, 0.0) + self.assertLessEqual(fold_angle, pi / 2) def test_controlflow(self): """Test non-ISA Rzz gates inside/outside a control flow branch.""" From 70b65bb5fc51248343d9e2479ad56f2fb6513526 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 4 Dec 2024 15:19:58 +0200 Subject: [PATCH 02/16] black --- .../transpiler/passes/basis/fold_rzz_angle.py | 19 ++++++++++++------- 1 file changed, 12 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 874909a88..a83cd828f 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -87,7 +87,7 @@ def _run_inner(self, dag: DAGCircuit) -> bool: # Modify circuit around Rzz gate to address non-ISA angles. modified = True if isinstance(angle, ParameterExpression): - #replace = self._unbounded_parameter(angle, node.qrgs) + # replace = self._unbounded_parameter(angle, node.qrgs) pass else: replace = self._numeric_parameter(angle, node.qargs) @@ -95,7 +95,7 @@ def _run_inner(self, dag: DAGCircuit) -> bool: dag.substitute_node_with_dag(node, replace) return modified - + # The next functions are required because sympy doesn't convert Boolean values to integers. # symengine maybe does but I failed to find it in its documentation. @staticmethod @@ -120,13 +120,18 @@ def _unbounded_parameter(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCi quad1 = self.between(wrap_angle, 0, pi / 2) quad2 = self.between(wrap_angle, pi / 2, pi) - quad3 = self.between(wrap_angle, - pi, - pi / 2) - quad4 = self.between(wrap_angle, - pi / 2, 0) + quad3 = self.between(wrap_angle, -pi, -pi / 2) + quad4 = self.between(wrap_angle, -pi / 2, 0) - global_phase = quad2 * pi / 2 + quad3 * (- pi / 2) + pi_phase * pi + global_phase = quad2 * pi / 2 + quad3 * (-pi / 2) + pi_phase * pi rz_angle = quad2 * pi + quad3 * pi rx_angle = quad2 * pi + quad4 * pi - rzz_angle = quad1 * wrap_angle + quad2 * (pi - wrap_angle) + quad3 * (pi + wrap_angle) + quad4 * (- wrap_angle) + rzz_angle = ( + quad1 * wrap_angle + + quad2 * (pi - wrap_angle) + + quad3 * (pi + wrap_angle) + + quad4 * (-wrap_angle) + ) new_dag = DAGCircuit() new_dag.add_qubits(qubits=qubits) @@ -159,7 +164,7 @@ def _unbounded_parameter(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCi ) return new_dag - + def _numeric_parameter(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: wrap_angle = np.angle(np.exp(1j * angle)) if 0 <= wrap_angle <= pi / 2: From abb73f3d6ee1778acc420d8177e9f864fa3eaddb Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 4 Dec 2024 15:30:11 +0200 Subject: [PATCH 03/16] fix --- .../transpiler/passes/basis/fold_rzz_angle.py | 14 +++++--------- 1 file changed, 5 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 a83cd828f..ae7e64ce8 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -87,8 +87,7 @@ def _run_inner(self, dag: DAGCircuit) -> bool: # Modify circuit around Rzz gate to address non-ISA angles. modified = True if isinstance(angle, ParameterExpression): - # replace = self._unbounded_parameter(angle, node.qrgs) - pass + replace = self._unbounded_parameter(angle, node.qrgs) else: replace = self._numeric_parameter(angle, node.qargs) @@ -98,20 +97,17 @@ def _run_inner(self, dag: DAGCircuit) -> bool: # The next functions are required because sympy doesn't convert Boolean values to integers. # symengine maybe does but I failed to find it in its documentation. - @staticmethod - def gt_op(exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: + def gt_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: tmp = (exp1 - exp2).sign() # We want to return 1 if tmp is -1 or 0, and 1 otherwise return tmp * tmp * (tmp + 1) / 2 - @staticmethod - def and_op(exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: + def and_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: return exp1 * exp2 - @staticmethod - def between(exp: ParameterExpression, lower: ParameterExpression, upper: ParameterExpression): - return and_op(gt_op(exp, lower), gt_op(upper, exp)) + def between(self, exp: ParameterExpression, lower: ParameterExpression, upper: ParameterExpression): + return self.and_op(self.gt_op(exp, lower), self.gt_op(upper, exp)) def _unbounded_parameter(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: wrap_angle = (angle + pi)._apply_operation(mod, 2 * pi) - pi From 6b6fe7442d0613b1ed417ca6fdb3197973f71b50 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 4 Dec 2024 16:06:00 +0200 Subject: [PATCH 04/16] fixes --- .../transpiler/passes/basis/fold_rzz_angle.py | 30 +++++++++++++++---- .../passes/basis/test_fold_rzz_angle.py | 2 ++ 2 files changed, 27 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 ae7e64ce8..1561a75f6 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -87,7 +87,7 @@ def _run_inner(self, dag: DAGCircuit) -> bool: # Modify circuit around Rzz gate to address non-ISA angles. modified = True if isinstance(angle, ParameterExpression): - replace = self._unbounded_parameter(angle, node.qrgs) + replace = self._unbounded_parameter(angle, node.qargs) else: replace = self._numeric_parameter(angle, node.qargs) @@ -100,16 +100,26 @@ def _run_inner(self, dag: DAGCircuit) -> bool: def gt_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: tmp = (exp1 - exp2).sign() - # We want to return 1 if tmp is -1 or 0, and 1 otherwise + # We want to return 0 if tmp is -1 or 0, and 1 otherwise return tmp * tmp * (tmp + 1) / 2 + def gteq_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: + tmp = (exp1 - exp2).sign() + + # We want to return 1 if tmp is 1 or 0, and 0 otherwise + return (tmp + 1)._apply_operation(mod, 2) + tmp * tmp * (tmp + 1) / 2 + def and_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: return exp1 * exp2 - def between(self, exp: ParameterExpression, lower: ParameterExpression, upper: ParameterExpression): - return self.and_op(self.gt_op(exp, lower), self.gt_op(upper, exp)) + def between( + self, exp: ParameterExpression, lower: ParameterExpression, upper: ParameterExpression + ): + return self.and_op(self.gteq_op(exp, lower), self.gt_op(upper, exp)) - def _unbounded_parameter(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + def _unbounded_parameter( + self, angle: ParameterExpression, qubits: Tuple[Qubit, ...] + ) -> DAGCircuit: wrap_angle = (angle + pi)._apply_operation(mod, 2 * pi) - pi pi_phase = pi * self.between(angle._apply_operation(mod, 4 * pi), pi, 3 * pi) @@ -129,6 +139,16 @@ def _unbounded_parameter(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCi + quad4 * (-wrap_angle) ) + print( + wrap_angle.assign(angle, pi / 2), + pi_phase.assign(angle, pi / 2), + global_phase.assign(angle, pi / 2), + quad1.assign(angle, pi / 2), + quad2.assign(angle, pi / 2), + quad3.assign(angle, pi / 2), + quad4.assign(angle, pi / 2), + ) + new_dag = DAGCircuit() new_dag.add_qubits(qubits=qubits) new_dag.apply_operation_back(GlobalPhaseGate(global_phase)) 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 35d922df4..f511e9268 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -93,6 +93,8 @@ def test_folding_rzz_angle_unbound(self, angle): qc.assign_parameters({param: angle}, inplace=True) isa.assign_parameters({param: angle}, inplace=True) + print(isa) + self.assertEqual(Operator.from_circuit(qc), Operator.from_circuit(isa)) for inst_data in isa.data: if inst_data.operation.name == "rzz": From 0ac423e1b4691710328c50dd1b182b11614b2af8 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 5 Dec 2024 15:22:16 +0200 Subject: [PATCH 05/16] cosmetics --- 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 1561a75f6..e361102f3 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -107,7 +107,7 @@ def gteq_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> Param tmp = (exp1 - exp2).sign() # We want to return 1 if tmp is 1 or 0, and 0 otherwise - return (tmp + 1)._apply_operation(mod, 2) + tmp * tmp * (tmp + 1) / 2 + return (1 - tmp * tmp) + tmp * tmp * (tmp + 1) / 2 def and_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: return exp1 * exp2 From 264e123f5d8b162aeadd075a827d0415830583dd Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 5 Dec 2024 15:22:27 +0200 Subject: [PATCH 06/16] a fix in the test --- test/unit/transpiler/passes/basis/test_fold_rzz_angle.py | 4 +--- 1 file changed, 1 insertion(+), 3 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 f511e9268..a91304664 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -93,12 +93,10 @@ def test_folding_rzz_angle_unbound(self, angle): qc.assign_parameters({param: angle}, inplace=True) isa.assign_parameters({param: angle}, inplace=True) - print(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] + fold_angle = float(inst_data.operation.params[0].assign(param, angle)) self.assertGreaterEqual(fold_angle, 0.0) self.assertLessEqual(fold_angle, pi / 2) From 27265618e26d614aa4cd28c3d0a3fc1a5a93e0cf Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 5 Dec 2024 15:38:40 +0200 Subject: [PATCH 07/16] bug fix --- .../transpiler/passes/basis/fold_rzz_angle.py | 18 +++++++++--------- .../passes/basis/test_fold_rzz_angle.py | 5 +++++ 2 files changed, 14 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 e361102f3..6e934a447 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -121,8 +121,7 @@ def _unbounded_parameter( self, angle: ParameterExpression, qubits: Tuple[Qubit, ...] ) -> DAGCircuit: wrap_angle = (angle + pi)._apply_operation(mod, 2 * pi) - pi - - pi_phase = pi * self.between(angle._apply_operation(mod, 4 * pi), pi, 3 * pi) + pi_phase = self.between(angle._apply_operation(mod, 4 * pi), pi, 3 * pi) quad1 = self.between(wrap_angle, 0, pi / 2) quad2 = self.between(wrap_angle, pi / 2, pi) @@ -139,14 +138,15 @@ def _unbounded_parameter( + quad4 * (-wrap_angle) ) + val = 0.1 + 2 * pi print( - wrap_angle.assign(angle, pi / 2), - pi_phase.assign(angle, pi / 2), - global_phase.assign(angle, pi / 2), - quad1.assign(angle, pi / 2), - quad2.assign(angle, pi / 2), - quad3.assign(angle, pi / 2), - quad4.assign(angle, pi / 2), + wrap_angle.assign(angle, val), + pi_phase.assign(angle, val), + global_phase.assign(angle, val), + quad1.assign(angle, val), + quad2.assign(angle, val), + quad3.assign(angle, val), + quad4.assign(angle, val), ) new_dag = 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 a91304664..7e627cab6 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -93,8 +93,13 @@ def test_folding_rzz_angle_unbound(self, angle): qc.assign_parameters({param: angle}, inplace=True) isa.assign_parameters({param: angle}, inplace=True) + print(isa) + self.assertEqual(Operator.from_circuit(qc), Operator.from_circuit(isa)) + print(Operator.from_circuit(qc)) + print(Operator.from_circuit(isa)) for inst_data in isa.data: + print(inst_data.operation.name, inst_data.operation.params[0].assign(param, angle)) if inst_data.operation.name == "rzz": fold_angle = float(inst_data.operation.params[0].assign(param, angle)) self.assertGreaterEqual(fold_angle, 0.0) From 0c044550c30195f747e05c47483f6452d3861c96 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 5 Dec 2024 19:13:03 +0200 Subject: [PATCH 08/16] bug fix --- .../transpiler/passes/basis/fold_rzz_angle.py | 5 ++--- test/unit/transpiler/passes/basis/test_fold_rzz_angle.py | 8 ++++---- 2 files changed, 6 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 6e934a447..bc766b37c 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -128,7 +128,7 @@ def _unbounded_parameter( quad3 = self.between(wrap_angle, -pi, -pi / 2) quad4 = self.between(wrap_angle, -pi / 2, 0) - global_phase = quad2 * pi / 2 + quad3 * (-pi / 2) + pi_phase * pi + global_phase = quad2 * (-pi / 2) + quad3 * (-pi / 2) + quad4 * pi + pi_phase * pi rz_angle = quad2 * pi + quad3 * pi rx_angle = quad2 * pi + quad4 * pi rzz_angle = ( @@ -138,7 +138,7 @@ def _unbounded_parameter( + quad4 * (-wrap_angle) ) - val = 0.1 + 2 * pi + val = -0.1 print( wrap_angle.assign(angle, val), pi_phase.assign(angle, val), @@ -155,7 +155,6 @@ def _unbounded_parameter( new_dag.apply_operation_back( RZGate(rz_angle), qargs=(qubits[0],), - cargs=(), check=False, ) new_dag.apply_operation_back( 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 7e627cab6..bdb67290e 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -94,14 +94,14 @@ def test_folding_rzz_angle_unbound(self, angle): isa.assign_parameters({param: angle}, inplace=True) print(isa) - - self.assertEqual(Operator.from_circuit(qc), Operator.from_circuit(isa)) + print(Operator.from_circuit(qc)) print(Operator.from_circuit(isa)) + + self.assertEqual(Operator.from_circuit(qc), Operator.from_circuit(isa)) for inst_data in isa.data: - print(inst_data.operation.name, inst_data.operation.params[0].assign(param, angle)) if inst_data.operation.name == "rzz": - fold_angle = float(inst_data.operation.params[0].assign(param, angle)) + fold_angle = inst_data.operation.params[0] self.assertGreaterEqual(fold_angle, 0.0) self.assertLessEqual(fold_angle, pi / 2) From 3ffc8cd745ce5b8b39863d40967f758246792479 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 5 Dec 2024 19:13:58 +0200 Subject: [PATCH 09/16] removed dubug prints --- .../transpiler/passes/basis/fold_rzz_angle.py | 11 ----------- .../transpiler/passes/basis/test_fold_rzz_angle.py | 5 ----- 2 files changed, 16 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 bc766b37c..8506aa9b0 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -138,17 +138,6 @@ def _unbounded_parameter( + quad4 * (-wrap_angle) ) - val = -0.1 - print( - wrap_angle.assign(angle, val), - pi_phase.assign(angle, val), - global_phase.assign(angle, val), - quad1.assign(angle, val), - quad2.assign(angle, val), - quad3.assign(angle, val), - quad4.assign(angle, val), - ) - new_dag = DAGCircuit() new_dag.add_qubits(qubits=qubits) new_dag.apply_operation_back(GlobalPhaseGate(global_phase)) 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 bdb67290e..35d922df4 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -93,11 +93,6 @@ def test_folding_rzz_angle_unbound(self, angle): qc.assign_parameters({param: angle}, inplace=True) isa.assign_parameters({param: angle}, inplace=True) - print(isa) - - print(Operator.from_circuit(qc)) - print(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": From 2d5c78c5d3bce98ceb11734791838cef0a001b08 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 5 Dec 2024 19:24:38 +0200 Subject: [PATCH 10/16] lint --- .../transpiler/passes/basis/fold_rzz_angle.py | 8 ++++++++ 1 file changed, 8 insertions(+) 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 8506aa9b0..3e6efcf77 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -98,23 +98,31 @@ def _run_inner(self, dag: DAGCircuit) -> bool: # The next functions are required because sympy doesn't convert Boolean values to integers. # symengine maybe does but I failed to find it in its documentation. def gt_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: + """Return an expression which, after substitution, will be equal to 1 if `exp1` is + greater than `exp2`, and 0 otherwise""" tmp = (exp1 - exp2).sign() # We want to return 0 if tmp is -1 or 0, and 1 otherwise return tmp * tmp * (tmp + 1) / 2 def gteq_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: + """Return an expression which, after substitution, will be equal to 1 if `exp1` is + greater or equal than `exp2`, and 0 otherwise""" tmp = (exp1 - exp2).sign() # We want to return 1 if tmp is 1 or 0, and 0 otherwise return (1 - tmp * tmp) + tmp * tmp * (tmp + 1) / 2 def and_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> ParameterExpression: + """Return an expression which, after substitution, will be equal to 1 if `exp1` and `exp2` + are both 1, and 0 if at least one of then is 0""" return exp1 * exp2 def between( self, exp: ParameterExpression, lower: ParameterExpression, upper: ParameterExpression ): + """Return an expression which, after substitution, will be equal to 1 if `exp1` is + greater or equal than `lower` and smaller than `upper`, and 0 otherwise""" return self.and_op(self.gteq_op(exp, lower), self.gt_op(upper, exp)) def _unbounded_parameter( From 62c869ead62c47c415f16d914897ed8d80e1c004 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Thu, 5 Dec 2024 19:28:05 +0200 Subject: [PATCH 11/16] mypy --- 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 3e6efcf77..5208be1dc 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -120,7 +120,7 @@ def and_op(self, exp1: ParameterExpression, exp2: ParameterExpression) -> Parame def between( self, exp: ParameterExpression, lower: ParameterExpression, upper: ParameterExpression - ): + ) -> ParameterExpression: """Return an expression which, after substitution, will be equal to 1 if `exp1` is greater or equal than `lower` and smaller than `upper`, and 0 otherwise""" return self.and_op(self.gteq_op(exp, lower), self.gt_op(upper, exp)) From 917055d9f80fddd3b7e56a53f9b1f91d88f78d01 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Sun, 8 Dec 2024 17:37:17 +0200 Subject: [PATCH 12/16] don't convert to circuits with unsupported instructions --- .../transpiler/passes/basis/fold_rzz_angle.py | 46 +++++++++++++++---- qiskit_ibm_runtime/transpiler/plugin.py | 2 +- .../passes/basis/test_fold_rzz_angle.py | 24 +++++++++- 3 files changed, 60 insertions(+), 12 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 5208be1dc..d3e13ffaa 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -21,6 +21,7 @@ from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit import Qubit, ControlFlowOp from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler import Target from qiskit.transpiler.basepasses import TransformationPass import numpy as np @@ -45,6 +46,19 @@ class FoldRzzAngle(TransformationPass): with angle of arbitrary real numbers. """ + def __init__(self, target: Target | list[str] | None = None): + """ + Args: + target - either a target or only a list of basis gates, either way it can be checked + if an instruction is supported using the `in` operator, for example `"rx" in target`. + If None then we assume that there is no limit on the gates in the transpiled circuit. + """ + super().__init__() + if target is None: + self._target = ["rz", "x", "rx", "rzz"] + else: + self._target = target + def run(self, dag: DAGCircuit) -> DAGCircuit: self._run_inner(dag) return dag @@ -85,13 +99,14 @@ def _run_inner(self, dag: DAGCircuit) -> bool: continue # Modify circuit around Rzz gate to address non-ISA angles. - modified = True if isinstance(angle, ParameterExpression): replace = self._unbounded_parameter(angle, node.qargs) else: replace = self._numeric_parameter(angle, node.qargs) - dag.substitute_node_with_dag(node, replace) + if replace is not None: + dag.substitute_node_with_dag(node, replace) + modified = True return modified @@ -128,6 +143,9 @@ def between( def _unbounded_parameter( self, angle: ParameterExpression, qubits: Tuple[Qubit, ...] ) -> DAGCircuit: + if "rz" not in self._target or "rx" not in self._target or "rzz" not in self._target: + return None + wrap_angle = (angle + pi)._apply_operation(mod, 2 * pi) - pi pi_phase = self.between(angle._apply_operation(mod, 4 * pi), pi, 3 * pi) @@ -198,8 +216,7 @@ def _numeric_parameter(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCirc return replace - @staticmethod - def _quad1(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + def _quad1(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: """Handle angle between [0, pi/2]. Circuit is not transformed - the Rzz gate is calibrated for the angle. @@ -207,6 +224,9 @@ def _quad1(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: Returns: A new dag with the same Rzz gate. """ + if "rzz" not in self._target: + return None + new_dag = DAGCircuit() new_dag.add_qubits(qubits=qubits) new_dag.apply_operation_back( @@ -216,8 +236,7 @@ def _quad1(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: ) return new_dag - @staticmethod - def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + def _quad2(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: """Handle angle between (pi/2, pi]. Circuit is transformed into the following form: @@ -231,6 +250,9 @@ def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: Returns: New dag to replace Rzz gate. """ + if "rz" not in self._target or "x" not in self._target or "rzz" not in self._target: + return None + new_dag = DAGCircuit() new_dag.add_qubits(qubits=qubits) new_dag.apply_operation_back(GlobalPhaseGate(pi / 2)) @@ -263,8 +285,7 @@ def _quad2(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: ) return new_dag - @staticmethod - def _quad3(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + def _quad3(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: """Handle angle between [-pi, -pi/2]. Circuit is transformed into following form: @@ -278,6 +299,9 @@ def _quad3(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: Returns: New dag to replace Rzz gate. """ + if "rz" not in self._target or "rzz" not in self._target: + return None + new_dag = DAGCircuit() new_dag.add_qubits(qubits=qubits) new_dag.apply_operation_back(GlobalPhaseGate(-pi / 2)) @@ -299,8 +323,7 @@ def _quad3(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: ) return new_dag - @staticmethod - def _quad4(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: + def _quad4(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: """Handle angle between (-pi/2, 0). Circuit is transformed into following form: @@ -313,6 +336,9 @@ def _quad4(angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: Returns: New dag to replace Rzz gate. """ + if "x" not in self._target or "rzz" not in self._target: + return None + new_dag = DAGCircuit() new_dag.add_qubits(qubits=qubits) new_dag.apply_operation_back( diff --git a/qiskit_ibm_runtime/transpiler/plugin.py b/qiskit_ibm_runtime/transpiler/plugin.py index b268b6a4e..c90f602a4 100644 --- a/qiskit_ibm_runtime/transpiler/plugin.py +++ b/qiskit_ibm_runtime/transpiler/plugin.py @@ -133,5 +133,5 @@ def pass_manager( pre_passes.append(ConvertIdToDelay(instruction_durations)) if "rzz" in target: # Apply this pass after SU4 is translated. - post_passes.append(FoldRzzAngle()) + post_passes.append(FoldRzzAngle(target)) return PassManager(pre_passes) + translator_pm + PassManager(post_passes) 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 35d922df4..3360f4633 100644 --- a/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py +++ b/test/unit/transpiler/passes/basis/test_fold_rzz_angle.py @@ -18,12 +18,14 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.parameter import Parameter +from qiskit.circuit.library import RZZGate +from qiskit.transpiler.target import InstructionProperties 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 qiskit_ibm_runtime.fake_provider import FakeFractionalBackend, FakeSherbrooke from .....ibm_test_case import IBMTestCase @@ -144,3 +146,23 @@ def test_fractional_plugin(self): 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)) + + def test_unsupported_instructions_skip(self): + """Verify that the pass does not output gates that are not in the basis gates""" + backend = FakeSherbrooke() + backend.target.add_instruction(RZZGate(Parameter("θ")), {(0, 1): InstructionProperties()}) + + p = Parameter("p") + circ = QuantumCircuit(2) + circ.rzz(p, 0, 1) + + pm = generate_preset_pass_manager( + optimization_level=0, + backend=FakeSherbrooke(), + translation_method="ibm_fractional", + basis_gates=backend.target.operation_names, + ) + isa_circ = pm.run(circ) + + self.assertEqual(isa_circ.data[0].operation.name, "rzz") + self.assertTrue(isinstance(isa_circ.data[0].operation.params[0], Parameter)) From b55ecd030a8a41dba5631a199c268234e1cb0797 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Mon, 9 Dec 2024 11:19:14 +0200 Subject: [PATCH 13/16] aligned circuit drawings --- .../transpiler/passes/basis/fold_rzz_angle.py | 16 ++++++++-------- 1 file changed, 8 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 d3e13ffaa..0e7b05ff3 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -241,11 +241,11 @@ def _quad2(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: Circuit is transformed into the following form: - ┌───────┐┌───┐ ┌───┐ + ┌───────┐┌───┐ ┌───┐ q_0: ┤ Rz(π) ├┤ X ├─■──────────┤ X ├ - ├───────┤└───┘ │ZZ(π - θ) └───┘ + ├───────┤└───┘ │ZZ(π - θ) └───┘ q_1: ┤ Rz(π) ├──────■─────────────── - └───────┘ + └───────┘ Returns: New dag to replace Rzz gate. @@ -290,11 +290,11 @@ def _quad3(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: Circuit is transformed into following form: - ┌───────┐ + ┌───────┐ q_0: ┤ Rz(π) ├─■─────────────── - ├───────┤ │ZZ(π - Abs(θ)) + ├───────┤ │ZZ(π - Abs(θ)) q_1: ┤ Rz(π) ├─■─────────────── - └───────┘ + └───────┘ Returns: New dag to replace Rzz gate. @@ -328,9 +328,9 @@ def _quad4(self, angle: float, qubits: Tuple[Qubit, ...]) -> DAGCircuit: Circuit is transformed into following form: - ┌───┐ ┌───┐ + ┌───┐ ┌───┐ q_0: ┤ X ├─■───────────┤ X ├ - └───┘ │ZZ(Abs(θ)) └───┘ + └───┘ │ZZ(Abs(θ)) └───┘ q_1: ──────■──────────────── Returns: From 4f7b1e131a5313515af9c5fb4bca1de5d10e73c1 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Mon, 9 Dec 2024 11:35:20 +0200 Subject: [PATCH 14/16] old style typing required by CI --- qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 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 0e7b05ff3..c251875a5 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -12,7 +12,7 @@ """Pass to wrap Rzz gate angle in calibrated range of 0-pi/2.""" -from typing import Tuple +from typing import Tuple, Optional, Union from math import pi from operator import mod @@ -46,7 +46,7 @@ class FoldRzzAngle(TransformationPass): with angle of arbitrary real numbers. """ - def __init__(self, target: Target | list[str] | None = None): + def __init__(self, target: Optional[Union[Target , list[str]]] = None): """ Args: target - either a target or only a list of basis gates, either way it can be checked From afd0851342da1063798a987e01e3114d6f8473d6 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Mon, 9 Dec 2024 11:38:04 +0200 Subject: [PATCH 15/16] black --- 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 c251875a5..2bdcffd1d 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): with angle of arbitrary real numbers. """ - def __init__(self, target: Optional[Union[Target , list[str]]] = None): + def __init__(self, target: Optional[Union[Target, list[str]]] = None): """ Args: target - either a target or only a list of basis gates, either way it can be checked From 5519c7706e992ba4b74c7e7be989ffccb95a5548 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Mon, 9 Dec 2024 11:58:00 +0200 Subject: [PATCH 16/16] old style typing required by CI --- qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 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 2bdcffd1d..b9ebd52c8 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/fold_rzz_angle.py @@ -12,7 +12,7 @@ """Pass to wrap Rzz gate angle in calibrated range of 0-pi/2.""" -from typing import Tuple, Optional, Union +from typing import Tuple, Optional, Union, List from math import pi from operator import mod @@ -46,7 +46,7 @@ class FoldRzzAngle(TransformationPass): with angle of arbitrary real numbers. """ - def __init__(self, target: Optional[Union[Target, list[str]]] = None): + def __init__(self, target: Optional[Union[Target, List[str]]] = None): """ Args: target - either a target or only a list of basis gates, either way it can be checked