From 92344c250840f279694f26594969a8c6d9093782 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Tue, 15 Aug 2023 21:35:41 +0900 Subject: [PATCH 01/18] Added NormalizeRXAngle and RXCalibrationBuilder passes to build single-pulse RX gate calibrations --- qiskit/transpiler/passes/__init__.py | 4 + .../transpiler/passes/calibration/__init__.py | 1 + .../transpiler/passes/calibration/builders.py | 1 + .../passes/calibration/rx_builder.py | 116 ++++++++++++++ .../passes/optimization/__init__.py | 1 + .../passes/optimization/normalize_rx_angle.py | 149 ++++++++++++++++++ .../single-pulse-rx-cal-347aadcee7bfe60b.yaml | 30 ++++ .../transpiler/test_calibrationbuilder.py | 92 ++++++++++- .../transpiler/test_normalize_rx_angle.py | 147 +++++++++++++++++ 9 files changed, 539 insertions(+), 2 deletions(-) create mode 100644 qiskit/transpiler/passes/calibration/rx_builder.py create mode 100644 qiskit/transpiler/passes/optimization/normalize_rx_angle.py create mode 100644 releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml create mode 100644 test/python/transpiler/test_normalize_rx_angle.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 73e6ea055165..d5886990e345 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -89,6 +89,7 @@ EchoRZXWeylDecomposition ResetAfterMeasureSimplification OptimizeCliffords + NormalizeRXAngle Calibration ============= @@ -99,6 +100,7 @@ PulseGates RZXCalibrationBuilder RZXCalibrationBuilderNoEcho + RXCalibrationBuilder Scheduling ============= @@ -234,6 +236,7 @@ from .optimization import CollectCliffords from .optimization import ResetAfterMeasureSimplification from .optimization import OptimizeCliffords +from .optimization import NormalizeRXAngle # circuit analysis from .analysis import ResourceEstimation @@ -258,6 +261,7 @@ from .calibration import PulseGates from .calibration import RZXCalibrationBuilder from .calibration import RZXCalibrationBuilderNoEcho +from .calibration import RXCalibrationBuilder # circuit scheduling from .scheduling import TimeUnitConversion diff --git a/qiskit/transpiler/passes/calibration/__init__.py b/qiskit/transpiler/passes/calibration/__init__.py index 10810c1cdff1..990249373f3f 100644 --- a/qiskit/transpiler/passes/calibration/__init__.py +++ b/qiskit/transpiler/passes/calibration/__init__.py @@ -14,3 +14,4 @@ from .pulse_gate import PulseGates from .rzx_builder import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho +from .rx_builder import RXCalibrationBuilder diff --git a/qiskit/transpiler/passes/calibration/builders.py b/qiskit/transpiler/passes/calibration/builders.py index 93adb4f945f1..49c2c6427317 100644 --- a/qiskit/transpiler/passes/calibration/builders.py +++ b/qiskit/transpiler/passes/calibration/builders.py @@ -17,3 +17,4 @@ # pylint: disable=unused-import from .pulse_gate import PulseGates from .rzx_builder import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho +from .rx_builder import RXCalibrationBuilder diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py new file mode 100644 index 000000000000..5d40af378c15 --- /dev/null +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -0,0 +1,116 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Add single-pulse RX calibrations that are bootstrapped from the SX calibration.""" + +from typing import Union +from functools import lru_cache +import numpy as np + +from qiskit.circuit import Instruction +from qiskit.pulse import Schedule, ScheduleBlock, builder, DriveChannel +from qiskit.pulse.channels import Channel +from qiskit.pulse.library.symbolic_pulses import Drag +from qiskit.transpiler.passes.calibration.base_builder import CalibrationBuilder +from qiskit.transpiler import Target +from qiskit.circuit.library.standard_gates import RXGate +from qiskit.exceptions import QiskitError + + +class RXCalibrationBuilder(CalibrationBuilder): + """Add single-pulse RX calibrations that are bootstrapped from the SX calibration. + + .. note: + Requirement: NormalizeRXAngles pass (one of the optimization passes). + + References: + [1]: Gokhale et al. (2020), Optimized Quantum Compilation for + Near-Term Algorithms with OpenPulse. + `arXiv:2004.11205 ` + """ + + def __init__( + self, + target: Target = None, + ): + """Bootstrap single-pulse RX gate calibrations from the + (hardware-calibrated) SX gate calibration. + + Args: + target (Target): Should contain a SX calibration that will be + used for bootstrapping RX calibrations. + """ + from qiskit.transpiler.passes.optimization import NormalizeRXAngle + + super().__init__() + self.target = target + self.already_generated = {} + self.requires = [NormalizeRXAngle(self.target)] + + if self.target.instruction_schedule_map() is None: + raise QiskitError("Calibrations can only be added to Pulse-enabled backends") + + def supported(self, node_op: Instruction, qubits: list) -> bool: + """ + Check if the calibration for SX gate exists. + """ + return isinstance(node_op, RXGate) and self.target.has_calibration("sx", tuple(qubits)) + + def get_calibration(self, node_op: Instruction, qubits: list) -> Union[Schedule, ScheduleBlock]: + """ + Generate RX calibration for the rotation angle specified in node_op. + """ + # already within [0, pi] by NormalizeRXAngles pass + angle = node_op.params[0] + + try: + angle = float(angle) + except TypeError as ex: + raise QiskitError("Target rotation angle is not assigned.") from ex + + params = ( + self.target.get_calibration("sx", tuple(qubits)) + .instructions[0][1] + .pulse.parameters.copy() + ) + new_rx_sched = _create_rx_sched( + rx_angle=angle, + channel_identifier=DriveChannel(qubits[0]), + duration=params["duration"], + amp=params["amp"], + sigma=params["sigma"], + beta=params["beta"], + ) + + return new_rx_sched + + +@lru_cache +def _create_rx_sched( + rx_angle: float, + duration: int, + amp: float, + sigma: float, + beta: float, + channel_identifier: Channel, +): + """Generates (and caches) pulse calibrations for RX gates. + Assumes that the rotation angle is in [0, pi]. + """ + new_amp = rx_angle / (np.pi / 2) * amp + with builder.build() as new_rx_sched: + builder.play( + Drag(duration=duration, amp=new_amp, sigma=sigma, beta=beta, angle=0), + channel=channel_identifier, + ) + + return new_rx_sched diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 6b87415194db..291fd9aec58b 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -35,3 +35,4 @@ from .reset_after_measure_simplification import ResetAfterMeasureSimplification from .optimize_cliffords import OptimizeCliffords from .collect_cliffords import CollectCliffords +from .normalize_rx_angle import NormalizeRXAngle diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py new file mode 100644 index 000000000000..3477b0f9cfbb --- /dev/null +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -0,0 +1,149 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. + +"""Performs three optimizations to reduce the number of pulse calibrations for +the single-pulse RX gates: +Wrap RX Gate rotation angles into [0, pi] by sandwiching them with RZ gates. +Convert RX(pi/2) to SX, and RX(pi) to X if the calibrations exist in the target. +Quantize the RX rotation angles using a resolution provided by the user. +""" + +import numpy as np + +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.dagcircuit import DAGCircuit +from qiskit.circuit.library.standard_gates import RXGate, RZGate, SXGate, XGate + + +class NormalizeRXAngle(TransformationPass): + """Wrap RX Gate rotation angles into [0, pi] by sandwiching them with RZ gates. + This will help reduce the size of calibration data, + as we won't have to keep separate, phase-flipped calibrations for negative rotation angles. + Moreover, if the calibrations exist in the target, convert RX(pi/2) to SX, and RX(pi) to X. + This will allow us to exploit the more accurate, hardware-calibrated pulses. + Lastly, quantize the RX rotation angles using a resolution provided by the user. + """ + + def __init__(self, target=None, resolution_in_radian=0): + """NormalizeRXAngle initializer. + + Args: + target (Target): The :class:`~.Target` representing the target backend. + If the target contains SX and X calibrations, this pass will replace the + corresponding RX gates with SX and X gates. + resolution_in_radian (float): Resolution for RX rotation angle quantization. + If set to zero, this pass won't modify the rotation angles in the given DAG. + (=Provides aribitary-angle RX) + """ + super().__init__() + self.target = target + self.resolution_in_radian = resolution_in_radian + self.already_generated = {} + + def quantize_angles(self, qubit, original_angle): + """Quantize the RX rotation angles using a resolution provided by the user. + + Args: + qubit (Qubit): This will be the dict key to access the list of quantized rotation angles. + original_angle (float): Original rotation angle, before quantization. + + Returns: + float: Quantized angle. + """ + + # check if there is already a calibration for a simliar angle + try: + angles = self.already_generated[qubit] # 1d ndarray of already generated angles + quantized_angle = float( + angles[np.where(np.abs(angles - original_angle) < (self.resolution_in_radian / 2))] + ) + except KeyError: + quantized_angle = original_angle + self.already_generated[qubit] = np.array([quantized_angle]) + except TypeError: + quantized_angle = original_angle + self.already_generated[qubit] = np.append( + self.already_generated[qubit], quantized_angle + ) + + return quantized_angle + + def run(self, dag): + """Run the NormalizeRXAngle pass on `dag`. This pass consists of three parts: + normalize_rx_angles(), convert_to_hardware_sx_x(), quantize_rx_angles(). + + Args: + dag (DAGCircuit): The DAG to be optimized. + + Returns: + DAGCircuit: A DAG where all RX rotation angles are within [0, pi]. + """ + + # Iterate over all op_nodes and replace RX if eligible for modification. + for op_node in dag.op_nodes(): + if not (op_node.op.name == "rx"): + continue + + raw_theta = op_node.op.params[0] + wrapped_theta = np.arctan2(np.sin(raw_theta), np.cos(raw_theta)) # [-pi, pi] + + if self.resolution_in_radian: + wrapped_theta = self.quantize_angles(op_node.qargs[0], wrapped_theta) + + half_pi_rotation = np.isclose(abs(wrapped_theta), np.pi / 2) + pi_rotation = np.isclose(abs(wrapped_theta), np.pi) + + # get the physical qubit index to look up the SX or X calibrations + qubit = dag.find_bit(op_node.qargs[0]).index if half_pi_rotation | pi_rotation else None + try: + qubit = int(qubit) + find_bit_succeeded = True + except TypeError: + find_bit_succeeded = False + + should_modify_node = ( + (wrapped_theta != raw_theta) + or (wrapped_theta < 0) + or half_pi_rotation + or pi_rotation + ) + + if should_modify_node: + mini_dag = DAGCircuit() + mini_dag.add_qubits(op_node.qargs) + + # new X-rotation gate with angle in [0, pi] + if ( + half_pi_rotation + and find_bit_succeeded + and self.target.has_calibration("sx", (qubit,)) + ): + mini_dag.apply_operation_back(SXGate(), qargs=op_node.qargs) + elif ( + pi_rotation + and find_bit_succeeded + and self.target.has_calibration("x", (qubit,)) + ): + mini_dag.apply_operation_back(XGate(), qargs=op_node.qargs) + else: + mini_dag.apply_operation_back( + RXGate(np.abs(wrapped_theta)), qargs=op_node.qargs + ) + + # sandwich with RZ if the intended rotation angle was negative + if wrapped_theta < 0: + mini_dag.apply_operation_front(RZGate(np.pi), qargs=op_node.qargs) + mini_dag.apply_operation_back(RZGate(-np.pi), qargs=op_node.qargs) + + dag.substitute_node_with_dag(node=op_node, input_dag=mini_dag, wires=op_node.qargs) + + return dag diff --git a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml new file mode 100644 index 000000000000..b873deda523f --- /dev/null +++ b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml @@ -0,0 +1,30 @@ +--- +features: + - | + Two new transpiler passes are added to generate single-pulse RX gate calibrations on the fly. + These single-pulse RX calibrations will reduce the gate time in half, as described in + P.Gokhale et al, Optimized Quantum Compilation for Near-Term Algorithms with OpenPulse + (2020), arXiv:2004.11205. + + To reduce the amount of RX calibration data that needs to be generated, + :class:`qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` + performs three optimizations: wrapping RX gate rotation angles to [0, pi], + replacing RX(pi/2) and RX(pi) with SX and X gates, and quantizing the rotation angles. + This pass is required to be run before + :class:`qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder`, + which generates RX calibrations on the fly. + + The details of the transpiler passes are as follows: + :class:`qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` wraps + RX gate rotation angles to [0, pi] by replacing an RX gate with negative rotation angle, RX(-theta), + with a sequence: RZ(pi)-RX(theta)-RZ(-pi). Moreover, the pass replaces RX(pi/2) with SX gate, + and RX(pi) with X gate. This will enable us to exploit the more accurate, hardware-calibrated + pulses. Lastly, the pass quantizes the rotation angles using a user-provided resolution. + If the resolution is set to 0, this pass will not perform any quantization. + :class:`qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder` + generates RX calibrations on the fly. The pulse calibrations are bootstrapped from + the SX gate calibration in the target. + The amplitude is linearly scaled to achieve the desired arbitrary rotation angle. + Such single-pulse calibrations reduces the gate time in half, compared to the + conventional sequence that consists of two SX pulses. + There could be an improvement in fidelity due to this reduction in gate time. \ No newline at end of file diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py index 7baa39c8ad6a..9b43920d9770 100644 --- a/test/python/transpiler/test_calibrationbuilder.py +++ b/test/python/transpiler/test_calibrationbuilder.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Test the RZXCalibrationBuilderNoEcho.""" +"""Test the CalibrationBuilder subclasses.""" from math import pi, erf @@ -19,9 +19,10 @@ from qiskit.converters import circuit_to_dag from qiskit import circuit, schedule, QiskitError -from qiskit.circuit.library.standard_gates import SXGate, RZGate +from qiskit.circuit.library.standard_gates import SXGate, RZGate, RXGate from qiskit.providers.fake_provider import FakeHanoi # TODO - include FakeHanoiV2, FakeSherbrooke from qiskit.providers.fake_provider import FakeArmonk +from qiskit.providers.fake_provider import FakeBelemV2 from qiskit.pulse import ( ControlChannel, DriveChannel, @@ -30,6 +31,7 @@ Play, InstructionScheduleMap, Schedule, + Drag, ) from qiskit.pulse import builder from qiskit.pulse.transforms import target_qobj_transform @@ -38,7 +40,12 @@ from qiskit.transpiler.passes.calibration.builders import ( RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho, + RXCalibrationBuilder, ) +from qiskit.transpiler import Target, InstructionProperties +from qiskit.dagcircuit import DAGOpNode +from qiskit.circuit import Parameter +from qiskit import QuantumCircuit class TestCalibrationBuilder(QiskitTestCase): @@ -428,3 +435,84 @@ def test_pass_alive_with_dcx_ish(self): # User warning that says q0 q1 is invalid cal_qc = PassManager(pass_).run(rzx_qc) self.assertEqual(cal_qc, rzx_qc) + + +@ddt +class TestRXCalibrationBuilder(QiskitTestCase): + """Test RXCalibrationBuilder.""" + + def compute_correct_rx_amplitude(self, rx_theta: float, sx_amp: float): + """A helper function to compute the amplitude of the bootstrapped RX pulse.""" + return sx_amp * (np.abs(rx_theta) / (0.5 * np.pi)) + + def test_not_supported_if_no_sx_schedule(self): + """Test that supported() returns False when the target does not have SX calibration.""" + empty_target = Target() + tp = RXCalibrationBuilder(empty_target) + qubits = (0,) + node_op = DAGOpNode(RXGate(0.5), qubits, []) + self.assertFalse(tp.supported(node_op, qubits)) + + def test_raises_error_when_rotation_angle_not_assigned(self): + """Test that get_calibration() fails when the RX gate's rotation angle is + an unassigned Parameter, not a number. + The QiskitError occurs while trying to typecast the Parameter into a float. + """ + backend = FakeBelemV2() + tp = RXCalibrationBuilder(backend.target) + qubits = (0,) + rx = RXGate(Parameter("theta")) + with self.assertRaises(QiskitError): + tp.get_calibration(rx, qubits) + + # Note: These input data values should be within [0, pi] because + # the required NormalizeRXAngles pass ensures that. + @data(0, np.pi / 3, (2 / 3) * np.pi) + def test_pulse_schedule(self, theta: float): + """Test that get_calibration() returns a schedule with correct amplitude.""" + backend = FakeBelemV2() + dummy_target = Target() + sx_amp, sx_beta, sx_sigma, sx_duration, sx_angle = 0.6, 2, 40, 160, 0.5 + with builder.build(backend=backend) as dummy_sx_cal: + builder.play( + Drag( + amp=sx_amp, beta=sx_beta, sigma=sx_sigma, duration=sx_duration, angle=sx_angle + ), + DriveChannel(0), + ) + dummy_target.add_instruction( + SXGate(), {(0,): InstructionProperties(calibration=dummy_sx_cal)} + ) + + tp = RXCalibrationBuilder(dummy_target) + test = tp.get_calibration(RXGate(theta), qubits=(0,)) + + with builder.build(backend=backend) as correct_rx_schedule: + builder.play( + Drag( + amp=self.compute_correct_rx_amplitude(rx_theta=theta, sx_amp=sx_amp), + beta=sx_beta, + sigma=sx_sigma, + duration=sx_duration, + angle=0, + ), + channel=DriveChannel(0), + ) + + self.assertEqual(test, correct_rx_schedule) + + def test_with_normalizerxangles(self): + """Checks that this pass works well with the NormalizeRXAngles pass.""" + backend = FakeBelemV2() + # NormalizeRXAngle pass should also be included because it's a required pass. + pm = PassManager(RXCalibrationBuilder(backend.target)) + + qc = QuantumCircuit(1) + qc.rx(np.pi / 3, 0) + qc.rx(np.pi / 2, 0) + qc.rx(np.pi, 0) + + # Only RX(pi/3) should get a rx calibration. + # The others should be converted to SX and X + tc = pm.run(qc) + self.assertEqual(len(tc.calibrations["rx"]), 1) diff --git a/test/python/transpiler/test_normalize_rx_angle.py b/test/python/transpiler/test_normalize_rx_angle.py new file mode 100644 index 000000000000..88136664b825 --- /dev/null +++ b/test/python/transpiler/test_normalize_rx_angle.py @@ -0,0 +1,147 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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 the NormalizeRXAngle pass""" + +import unittest +import numpy as np +from ddt import ddt, named_data + +from qiskit import QuantumCircuit + +from qiskit.transpiler.passes.optimization.normalize_rx_angle import ( + NormalizeRXAngle, +) +from qiskit.test import QiskitTestCase +from qiskit.providers.fake_provider import FakeBelemV2 +from qiskit.transpiler import Target + + +@ddt +class TestNormalizeRXAngle(QiskitTestCase): + """Tests the NormalizeRXAngle pass.""" + + @staticmethod + def count_gate_number(gate, circuit): + """Count the number of a specific gate type in a circuit""" + if gate not in QuantumCircuit.count_ops(circuit): + gate_number = 0 + else: + gate_number = QuantumCircuit.count_ops(circuit)[gate] + return gate_number + + def test_not_convert_to_X_if_no_calib_in_target(self): + """Check that RX(pi) is NOT converted to X, + if X calibration is not present in the target""" + empty_target = Target() + tp = NormalizeRXAngle(target=empty_target) + + qc = QuantumCircuit(1) + qc.rx(90, 0) + + transpiled_circ = tp(qc) + self.assertEqual(self.count_gate_number("x", transpiled_circ), 0) + + def test_SX_conversion_works(self): + """Check that RX(pi/2) is converted to SX, + if SX calibration is present in the target""" + backend = FakeBelemV2() + tp = NormalizeRXAngle(target=backend.target) + + qc = QuantumCircuit(1) + qc.rx(np.pi / 2, 0) + + transpiled_circ = tp(qc) + self.assertEqual(self.count_gate_number("sx", transpiled_circ), 1) + + @named_data({"name": "RX(-pi/3)=RZ(pi)-RX(pi/3)-RZ(-pi)", "rx_angle": (-1 / 3) * np.pi}) + def test_RZ_added_for_negative_rotation_angles(self, rx_angle): + """Check that RZ is added before and after RX, + if RX rotation angle is negative""" + + backend = FakeBelemV2() + tp = NormalizeRXAngle(target=backend.target) + + # circuit to transpiler and test + qc = QuantumCircuit(1) + qc.rx(rx_angle, 0) + transpiled_circ = tp(qc) + + # circuit to show the correct answer + qc_ref = QuantumCircuit(1) + qc_ref.rz(np.pi, 0) + qc_ref.rx(np.abs(rx_angle), 0) + qc_ref.rz(-np.pi, 0) + + self.assertQuantumCircuitEqual(transpiled_circ, qc_ref) + + @named_data( + {"name": "-0.3pi", "raw_theta": -0.3 * np.pi, "correct_wrapped_theta": 0.3 * np.pi}, + {"name": "1.7pi", "raw_theta": 1.7 * np.pi, "correct_wrapped_theta": 0.3 * np.pi}, + {"name": "2.2pi", "raw_theta": 2.2 * np.pi, "correct_wrapped_theta": 0.2 * np.pi}, + ) + def test_angle_wrapping_works(self, raw_theta, correct_wrapped_theta): + """Check that RX rotation angles are correctly wrapped to [0, pi]""" + backend = FakeBelemV2() + tp = NormalizeRXAngle(target=backend.target) + + # circuit to transpile and test + qc = QuantumCircuit(1) + qc.rx(raw_theta, 0) + + transpiled_circuit = tp(qc) + wrapped_theta = transpiled_circuit.get_instructions("rx")[0].operation.params[0] + self.assertAlmostEqual(wrapped_theta, correct_wrapped_theta) + + @named_data( + { + "name": "angles are within resolution", + "resolution": 0.1, + "rx_angles": [0.3, 0.303], + "correct_num_of_cals": 1, + }, + { + "name": "angles are not within resolution", + "resolution": 0.1, + "rx_angles": [0.2, 0.4], + "correct_num_of_cals": 2, + }, + { + "name": "same angle three times", + "resolution": 0.1, + "rx_angles": [0.2, 0.2, 0.2], + "correct_num_of_cals": 1, + }, + ) + def test_quantize_angles(self, resolution, rx_angles, correct_num_of_cals): + """Test that quantize_angles() adds a new calibration only if + the requested angle is not in the vicinity of the already generated angles. + """ + backend = FakeBelemV2() + tp = NormalizeRXAngle(backend.target, resolution_in_radian=resolution) + + qc = QuantumCircuit(1) + for rx_angle in rx_angles: + qc.rx(rx_angle, 0) + transpiled_circuit = tp(qc) + + angles = [ + inst.operation.params[0] + for inst in transpiled_circuit.data + if inst.operation.name == "rx" + ] + angles_without_duplicate = list(dict.fromkeys(angles)) + self.assertEqual(len(angles_without_duplicate), correct_num_of_cals) + + +if __name__ == "__main__": + unittest.main() From 5e23f55001351a90d0ef1834763a1ea3e5a8bf6f Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Tue, 22 Aug 2023 16:34:50 +0900 Subject: [PATCH 02/18] Fixed a typo in documentation --- qiskit/transpiler/passes/calibration/rx_builder.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py index 5d40af378c15..a79b38834911 100644 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2023. # # 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 @@ -29,13 +29,12 @@ class RXCalibrationBuilder(CalibrationBuilder): """Add single-pulse RX calibrations that are bootstrapped from the SX calibration. - .. note: + .. note:: Requirement: NormalizeRXAngles pass (one of the optimization passes). References: - [1]: Gokhale et al. (2020), Optimized Quantum Compilation for - Near-Term Algorithms with OpenPulse. - `arXiv:2004.11205 ` + * [1]: Gokhale et al. (2020), Optimized Quantum Compilation for + Near-Term Algorithms with OpenPulse. https://arxiv.org/abs/2004.11205 """ def __init__( From acb5ee9b69134bd30b6e3f001780aa9ce496dbc0 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Wed, 23 Aug 2023 11:21:39 +0900 Subject: [PATCH 03/18] Removed all cross-references in the documentation --- qiskit/transpiler/passes/calibration/rx_builder.py | 4 ++-- .../transpiler/passes/optimization/normalize_rx_angle.py | 4 ++-- .../notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml | 8 ++++---- requirements-dev.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py index a79b38834911..0d2c5e6a1b3f 100644 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -32,9 +32,9 @@ class RXCalibrationBuilder(CalibrationBuilder): .. note:: Requirement: NormalizeRXAngles pass (one of the optimization passes). - References: + References * [1]: Gokhale et al. (2020), Optimized Quantum Compilation for - Near-Term Algorithms with OpenPulse. https://arxiv.org/abs/2004.11205 + Near-Term Algorithms with OpenPulse. https://arxiv.org/abs/2004.11205 """ def __init__( diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py index 3477b0f9cfbb..ef0819617e85 100644 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -37,7 +37,7 @@ def __init__(self, target=None, resolution_in_radian=0): """NormalizeRXAngle initializer. Args: - target (Target): The :class:`~.Target` representing the target backend. + target (Target): The Target of the backend to run the circuit on. If the target contains SX and X calibrations, this pass will replace the corresponding RX gates with SX and X gates. resolution_in_radian (float): Resolution for RX rotation angle quantization. @@ -78,7 +78,7 @@ def quantize_angles(self, qubit, original_angle): return quantized_angle def run(self, dag): - """Run the NormalizeRXAngle pass on `dag`. This pass consists of three parts: + """Run the NormalizeRXAngle pass on dag. This pass consists of three parts: normalize_rx_angles(), convert_to_hardware_sx_x(), quantize_rx_angles(). Args: diff --git a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml index b873deda523f..ce947e82e4f2 100644 --- a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml +++ b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml @@ -7,21 +7,21 @@ features: (2020), arXiv:2004.11205. To reduce the amount of RX calibration data that needs to be generated, - :class:`qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` + qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle performs three optimizations: wrapping RX gate rotation angles to [0, pi], replacing RX(pi/2) and RX(pi) with SX and X gates, and quantizing the rotation angles. This pass is required to be run before - :class:`qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder`, + qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder, which generates RX calibrations on the fly. The details of the transpiler passes are as follows: - :class:`qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` wraps + qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle wraps RX gate rotation angles to [0, pi] by replacing an RX gate with negative rotation angle, RX(-theta), with a sequence: RZ(pi)-RX(theta)-RZ(-pi). Moreover, the pass replaces RX(pi/2) with SX gate, and RX(pi) with X gate. This will enable us to exploit the more accurate, hardware-calibrated pulses. Lastly, the pass quantizes the rotation angles using a user-provided resolution. If the resolution is set to 0, this pass will not perform any quantization. - :class:`qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder` + qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder generates RX calibrations on the fly. The pulse calibrations are bootstrapped from the SX gate calibration in the target. The amplitude is linearly scaled to achieve the desired arbitrary rotation angle. diff --git a/requirements-dev.txt b/requirements-dev.txt index fe363cba1f44..810a58f1e7de 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -31,7 +31,7 @@ ddt>=1.2.0,!=1.4.0,!=1.4.3 # components of Terra use some of its optional dependencies in order to document # themselves. These are the requirements that are _only_ required for the docs # build, and are not used by Terra itself. -Sphinx>=6.0 +Sphinx>=6.0,<7.2 qiskit-sphinx-theme~=1.14.0 sphinx-design>=0.2.0 nbsphinx~=0.9.2 From c681760c4379290e54b46d86865dfa2d707c7c68 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Wed, 23 Aug 2023 15:49:11 +0900 Subject: [PATCH 04/18] Added URL links back --- qiskit/transpiler/passes/calibration/rx_builder.py | 3 ++- qiskit/transpiler/passes/optimization/normalize_rx_angle.py | 4 ++-- releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml | 2 +- requirements-dev.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py index 0d2c5e6a1b3f..c4da32d5c982 100644 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -34,7 +34,8 @@ class RXCalibrationBuilder(CalibrationBuilder): References * [1]: Gokhale et al. (2020), Optimized Quantum Compilation for - Near-Term Algorithms with OpenPulse. https://arxiv.org/abs/2004.11205 + Near-Term Algorithms with OpenPulse. + `arXiv:2004.11205 ` """ def __init__( diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py index ef0819617e85..443799f7ede8 100644 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -37,7 +37,7 @@ def __init__(self, target=None, resolution_in_radian=0): """NormalizeRXAngle initializer. Args: - target (Target): The Target of the backend to run the circuit on. + target (Target): The :class:`~.Target` representing the target backend. If the target contains SX and X calibrations, this pass will replace the corresponding RX gates with SX and X gates. resolution_in_radian (float): Resolution for RX rotation angle quantization. @@ -78,7 +78,7 @@ def quantize_angles(self, qubit, original_angle): return quantized_angle def run(self, dag): - """Run the NormalizeRXAngle pass on dag. This pass consists of three parts: + """Run the NormalizeRXAngle pass on ``dag``. This pass consists of three parts: normalize_rx_angles(), convert_to_hardware_sx_x(), quantize_rx_angles(). Args: diff --git a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml index ce947e82e4f2..7105154fc258 100644 --- a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml +++ b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml @@ -4,7 +4,7 @@ features: Two new transpiler passes are added to generate single-pulse RX gate calibrations on the fly. These single-pulse RX calibrations will reduce the gate time in half, as described in P.Gokhale et al, Optimized Quantum Compilation for Near-Term Algorithms with OpenPulse - (2020), arXiv:2004.11205. + (2020), `arXiv:2004.11205 `. To reduce the amount of RX calibration data that needs to be generated, qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle diff --git a/requirements-dev.txt b/requirements-dev.txt index 810a58f1e7de..792f74080e8b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -32,7 +32,7 @@ ddt>=1.2.0,!=1.4.0,!=1.4.3 # themselves. These are the requirements that are _only_ required for the docs # build, and are not used by Terra itself. Sphinx>=6.0,<7.2 -qiskit-sphinx-theme~=1.14.0 +qiskit-sphinx-theme~=1.15.0 sphinx-design>=0.2.0 nbsphinx~=0.9.2 nbconvert~=7.7.1 From 79a44d8f3e6b5fb1978acfb7cdada1d0051d47f9 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Wed, 23 Aug 2023 17:22:33 +0900 Subject: [PATCH 05/18] Added cross references back to the release note --- qiskit/transpiler/passes/calibration/rx_builder.py | 2 +- .../notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py index c4da32d5c982..1460c2049c05 100644 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -34,7 +34,7 @@ class RXCalibrationBuilder(CalibrationBuilder): References * [1]: Gokhale et al. (2020), Optimized Quantum Compilation for - Near-Term Algorithms with OpenPulse. + Near-Term Algorithms with OpenPulse. `arXiv:2004.11205 ` """ diff --git a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml index 7105154fc258..07dd32d150ab 100644 --- a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml +++ b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml @@ -7,21 +7,21 @@ features: (2020), `arXiv:2004.11205 `. To reduce the amount of RX calibration data that needs to be generated, - qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle + :class:`qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` performs three optimizations: wrapping RX gate rotation angles to [0, pi], replacing RX(pi/2) and RX(pi) with SX and X gates, and quantizing the rotation angles. This pass is required to be run before - qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder, + :class:`qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder`, which generates RX calibrations on the fly. The details of the transpiler passes are as follows: - qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle wraps + :class:`qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` wraps RX gate rotation angles to [0, pi] by replacing an RX gate with negative rotation angle, RX(-theta), with a sequence: RZ(pi)-RX(theta)-RZ(-pi). Moreover, the pass replaces RX(pi/2) with SX gate, and RX(pi) with X gate. This will enable us to exploit the more accurate, hardware-calibrated pulses. Lastly, the pass quantizes the rotation angles using a user-provided resolution. If the resolution is set to 0, this pass will not perform any quantization. - qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder + :class:`qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder` generates RX calibrations on the fly. The pulse calibrations are bootstrapped from the SX gate calibration in the target. The amplitude is linearly scaled to achieve the desired arbitrary rotation angle. From 2b0664c1f6be43e0b4dcced3e4fea95d267e10a7 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Thu, 24 Aug 2023 10:43:59 +0900 Subject: [PATCH 06/18] Shorten the cross references in the release note --- .../notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml index 07dd32d150ab..1bad28ca02ca 100644 --- a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml +++ b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml @@ -7,21 +7,21 @@ features: (2020), `arXiv:2004.11205 `. To reduce the amount of RX calibration data that needs to be generated, - :class:`qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` + :class:`~qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` performs three optimizations: wrapping RX gate rotation angles to [0, pi], replacing RX(pi/2) and RX(pi) with SX and X gates, and quantizing the rotation angles. This pass is required to be run before - :class:`qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder`, + :class:`~qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder`, which generates RX calibrations on the fly. The details of the transpiler passes are as follows: - :class:`qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` wraps + :class:`~qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` wraps RX gate rotation angles to [0, pi] by replacing an RX gate with negative rotation angle, RX(-theta), with a sequence: RZ(pi)-RX(theta)-RZ(-pi). Moreover, the pass replaces RX(pi/2) with SX gate, and RX(pi) with X gate. This will enable us to exploit the more accurate, hardware-calibrated pulses. Lastly, the pass quantizes the rotation angles using a user-provided resolution. If the resolution is set to 0, this pass will not perform any quantization. - :class:`qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder` + :class:`~qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder` generates RX calibrations on the fly. The pulse calibrations are bootstrapped from the SX gate calibration in the target. The amplitude is linearly scaled to achieve the desired arbitrary rotation angle. From fe1fc0612e455d3ec8b2574857d01a94dee9adf4 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Thu, 31 Aug 2023 00:30:13 +0900 Subject: [PATCH 07/18] Fixed normalize_rx_angle.py based on the comments --- .../passes/optimization/normalize_rx_angle.py | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py index 443799f7ede8..67948fc293fe 100644 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -63,8 +63,11 @@ def quantize_angles(self, qubit, original_angle): # check if there is already a calibration for a simliar angle try: angles = self.already_generated[qubit] # 1d ndarray of already generated angles - quantized_angle = float( - angles[np.where(np.abs(angles - original_angle) < (self.resolution_in_radian / 2))] + similar_angle = angles[ + np.isclose(angles, original_angle, atol=self.resolution_in_radian / 2) + ] + quantized_angle = ( + float(similar_angle[0]) if len(similar_angle) > 1 else float(similar_angle) ) except KeyError: quantized_angle = original_angle @@ -78,19 +81,18 @@ def quantize_angles(self, qubit, original_angle): return quantized_angle def run(self, dag): - """Run the NormalizeRXAngle pass on ``dag``. This pass consists of three parts: - normalize_rx_angles(), convert_to_hardware_sx_x(), quantize_rx_angles(). + """Run the NormalizeRXAngle pass on ``dag``. Args: dag (DAGCircuit): The DAG to be optimized. Returns: - DAGCircuit: A DAG where all RX rotation angles are within [0, pi]. + DAGCircuit: A DAG with RX gate calibration. """ # Iterate over all op_nodes and replace RX if eligible for modification. for op_node in dag.op_nodes(): - if not (op_node.op.name == "rx"): + if not isinstance(op_node.op, RXGate): continue raw_theta = op_node.op.params[0] @@ -102,14 +104,6 @@ def run(self, dag): half_pi_rotation = np.isclose(abs(wrapped_theta), np.pi / 2) pi_rotation = np.isclose(abs(wrapped_theta), np.pi) - # get the physical qubit index to look up the SX or X calibrations - qubit = dag.find_bit(op_node.qargs[0]).index if half_pi_rotation | pi_rotation else None - try: - qubit = int(qubit) - find_bit_succeeded = True - except TypeError: - find_bit_succeeded = False - should_modify_node = ( (wrapped_theta != raw_theta) or (wrapped_theta < 0) @@ -122,18 +116,14 @@ def run(self, dag): mini_dag.add_qubits(op_node.qargs) # new X-rotation gate with angle in [0, pi] - if ( - half_pi_rotation - and find_bit_succeeded - and self.target.has_calibration("sx", (qubit,)) - ): - mini_dag.apply_operation_back(SXGate(), qargs=op_node.qargs) - elif ( - pi_rotation - and find_bit_succeeded - and self.target.has_calibration("x", (qubit,)) - ): - mini_dag.apply_operation_back(XGate(), qargs=op_node.qargs) + if half_pi_rotation: + physical_qubit_idx = dag.find_bit(op_node.qargs[0]).index + if self.target.instruction_supported("sx", (physical_qubit_idx,)): + mini_dag.apply_operation_back(SXGate(), qargs=op_node.qargs) + elif pi_rotation: + physical_qubit_idx = dag.find_bit(op_node.qargs[0]).index + if self.target.instruction_supported("x", (physical_qubit_idx,)): + mini_dag.apply_operation_back(XGate(), qargs=op_node.qargs) else: mini_dag.apply_operation_back( RXGate(np.abs(wrapped_theta)), qargs=op_node.qargs From 9244800c825cf3411475acaee477744cca15d694 Mon Sep 17 00:00:00 2001 From: jaeunkim <43903969+jaeunkim@users.noreply.github.com> Date: Thu, 31 Aug 2023 09:52:56 +0900 Subject: [PATCH 08/18] Update qiskit/transpiler/passes/optimization/normalize_rx_angle.py Edit the doc so that it looks nicer in html Co-authored-by: Naoki Kanazawa --- .../passes/optimization/normalize_rx_angle.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py index 67948fc293fe..61d0df88f05e 100644 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -25,12 +25,26 @@ class NormalizeRXAngle(TransformationPass): - """Wrap RX Gate rotation angles into [0, pi] by sandwiching them with RZ gates. - This will help reduce the size of calibration data, - as we won't have to keep separate, phase-flipped calibrations for negative rotation angles. - Moreover, if the calibrations exist in the target, convert RX(pi/2) to SX, and RX(pi) to X. - This will allow us to exploit the more accurate, hardware-calibrated pulses. - Lastly, quantize the RX rotation angles using a resolution provided by the user. + """Normalize theta parameter of RXGate instruction. + + The parameter normalization is performed with following steps. + + 1) Wrap RX Gate theta into [0, pi]. When theta is negative value, the gate is + decomposed into the following sequence. + + .. code-block:: + + ┌───────┐┌─────────┐┌────────┐ + q: ┤ Rz(π) ├┤ Rx(|θ|) ├┤ Rz(-π) ├ + └───────┘└─────────┘└────────┘ + + 2) If the operation is supported by target, convert RX(pi/2) to SX, and RX(pi) to X. + + 3) Quantize theta value according to the user-specified resolution. + + This will help reduce the size of calibration data sent over the wire, + and allow us to exploit the more accurate, hardware-calibrated pulses. + Note that pulse calibration might be attached per each rotation angle. """ def __init__(self, target=None, resolution_in_radian=0): From 60fb2c4917fff0245e8b5bfd15e48c3782bbfaaf Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Thu, 31 Aug 2023 11:16:51 +0900 Subject: [PATCH 09/18] Reformat the doc of NormalizeRXAngle --- .../passes/optimization/normalize_rx_angle.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py index 61d0df88f05e..68e90448921e 100644 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -26,22 +26,22 @@ class NormalizeRXAngle(TransformationPass): """Normalize theta parameter of RXGate instruction. - + The parameter normalization is performed with following steps. - - 1) Wrap RX Gate theta into [0, pi]. When theta is negative value, the gate is + + 1) Wrap RX Gate theta into [0, pi]. When theta is negative value, the gate is decomposed into the following sequence. - + .. code-block:: - + ┌───────┐┌─────────┐┌────────┐ q: ┤ Rz(π) ├┤ Rx(|θ|) ├┤ Rz(-π) ├ └───────┘└─────────┘└────────┘ - + 2) If the operation is supported by target, convert RX(pi/2) to SX, and RX(pi) to X. - + 3) Quantize theta value according to the user-specified resolution. - + This will help reduce the size of calibration data sent over the wire, and allow us to exploit the more accurate, hardware-calibrated pulses. Note that pulse calibration might be attached per each rotation angle. From 87f15dfacc4ae5fb97fe4fc186ec719fe9c5ca55 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Thu, 31 Aug 2023 11:48:25 +0900 Subject: [PATCH 10/18] Try to pass the doc test --- qiskit/transpiler/passes/optimization/normalize_rx_angle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py index 68e90448921e..801f8fd423c4 100644 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -34,9 +34,9 @@ class NormalizeRXAngle(TransformationPass): .. code-block:: - ┌───────┐┌─────────┐┌────────┐ - q: ┤ Rz(π) ├┤ Rx(|θ|) ├┤ Rz(-π) ├ - └───────┘└─────────┘└────────┘ + ┌───────┐┌─────────┐┌────────┐ + q: ┤ Rz(π) ├┤ Rx(|θ|) ├┤ Rz(-π) ├ + └───────┘└─────────┘└────────┘ 2) If the operation is supported by target, convert RX(pi/2) to SX, and RX(pi) to X. From 6d6a0592764ae210acf483dcd2856c9454241472 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Fri, 1 Sep 2023 22:09:06 +0900 Subject: [PATCH 11/18] Addressed feedback and comments from the code review --- .../passes/calibration/rx_builder.py | 26 +++++++++++---- .../single-pulse-rx-cal-347aadcee7bfe60b.yaml | 17 ++++------ requirements-dev.txt | 4 +-- .../transpiler/test_calibrationbuilder.py | 10 +++--- .../transpiler/test_normalize_rx_angle.py | 32 +++++++------------ 5 files changed, 43 insertions(+), 46 deletions(-) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py index 1460c2049c05..d205adee230e 100644 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -17,7 +17,7 @@ import numpy as np from qiskit.circuit import Instruction -from qiskit.pulse import Schedule, ScheduleBlock, builder, DriveChannel +from qiskit.pulse import Schedule, ScheduleBlock, builder from qiskit.pulse.channels import Channel from qiskit.pulse.library.symbolic_pulses import Drag from qiskit.transpiler.passes.calibration.base_builder import CalibrationBuilder @@ -32,6 +32,21 @@ class RXCalibrationBuilder(CalibrationBuilder): .. note:: Requirement: NormalizeRXAngles pass (one of the optimization passes). + It is recommended to place this pass in the post-optimization stage of a passmanager. + A simple demo: + + .. code-block:: python + backend = FakeBelemV2() + pm = PassManager(RXCalibrationBuilder(backend.target)) + qc = QuantumCircuit(1) + angles = [0.1, 0.2, 0.3, 0.4] + for angle in angles: + qc.rx(angle, 0) + + # run the pass and check that new calibrations are generated + transpiled_circuit = pm.run(qc) + print(transpiled_circuit.calibrations["rx"]) + References * [1]: Gokhale et al. (2020), Optimized Quantum Compilation for Near-Term Algorithms with OpenPulse. @@ -56,9 +71,6 @@ def __init__( self.already_generated = {} self.requires = [NormalizeRXAngle(self.target)] - if self.target.instruction_schedule_map() is None: - raise QiskitError("Calibrations can only be added to Pulse-enabled backends") - def supported(self, node_op: Instruction, qubits: list) -> bool: """ Check if the calibration for SX gate exists. @@ -84,7 +96,7 @@ def get_calibration(self, node_op: Instruction, qubits: list) -> Union[Schedule, ) new_rx_sched = _create_rx_sched( rx_angle=angle, - channel_identifier=DriveChannel(qubits[0]), + channel=self.target.get_calibration("sx", tuple(qubits)).channels[0], duration=params["duration"], amp=params["amp"], sigma=params["sigma"], @@ -101,7 +113,7 @@ def _create_rx_sched( amp: float, sigma: float, beta: float, - channel_identifier: Channel, + channel: Channel, ): """Generates (and caches) pulse calibrations for RX gates. Assumes that the rotation angle is in [0, pi]. @@ -110,7 +122,7 @@ def _create_rx_sched( with builder.build() as new_rx_sched: builder.play( Drag(duration=duration, amp=new_amp, sigma=sigma, beta=beta, angle=0), - channel=channel_identifier, + channel=channel, ) return new_rx_sched diff --git a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml index 1bad28ca02ca..6261d86e4a95 100644 --- a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml +++ b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml @@ -14,17 +14,12 @@ features: :class:`~qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder`, which generates RX calibrations on the fly. - The details of the transpiler passes are as follows: - :class:`~qiskit.transpiler.passes.optimization.normalize_rx_angle.NormalizeRXAngle` wraps - RX gate rotation angles to [0, pi] by replacing an RX gate with negative rotation angle, RX(-theta), - with a sequence: RZ(pi)-RX(theta)-RZ(-pi). Moreover, the pass replaces RX(pi/2) with SX gate, - and RX(pi) with X gate. This will enable us to exploit the more accurate, hardware-calibrated - pulses. Lastly, the pass quantizes the rotation angles using a user-provided resolution. - If the resolution is set to 0, this pass will not perform any quantization. - :class:`~qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder` - generates RX calibrations on the fly. The pulse calibrations are bootstrapped from - the SX gate calibration in the target. + The optimizations performed by `NormalizeRXAngle` reduce the amount of calibration data and + enable us to take advantage of the more accurate, hardware-calibrated + pulses. The calibrations generated by `RXCalibrationBuilder` are bootstrapped from + the SX gate calibration, which should be already present in the target. The amplitude is linearly scaled to achieve the desired arbitrary rotation angle. - Such single-pulse calibrations reduces the gate time in half, compared to the + + Such single-pulse calibrations reduces the RX gate time in half, compared to the conventional sequence that consists of two SX pulses. There could be an improvement in fidelity due to this reduction in gate time. \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 792f74080e8b..fe363cba1f44 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -31,8 +31,8 @@ ddt>=1.2.0,!=1.4.0,!=1.4.3 # components of Terra use some of its optional dependencies in order to document # themselves. These are the requirements that are _only_ required for the docs # build, and are not used by Terra itself. -Sphinx>=6.0,<7.2 -qiskit-sphinx-theme~=1.15.0 +Sphinx>=6.0 +qiskit-sphinx-theme~=1.14.0 sphinx-design>=0.2.0 nbsphinx~=0.9.2 nbconvert~=7.7.1 diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py index 9b43920d9770..faee6ef0fd43 100644 --- a/test/python/transpiler/test_calibrationbuilder.py +++ b/test/python/transpiler/test_calibrationbuilder.py @@ -18,7 +18,8 @@ from ddt import data, ddt from qiskit.converters import circuit_to_dag -from qiskit import circuit, schedule, QiskitError +from qiskit import circuit, schedule, QiskitError, QuantumCircuit +from qiskit.circuit import Parameter from qiskit.circuit.library.standard_gates import SXGate, RZGate, RXGate from qiskit.providers.fake_provider import FakeHanoi # TODO - include FakeHanoiV2, FakeSherbrooke from qiskit.providers.fake_provider import FakeArmonk @@ -35,17 +36,14 @@ ) from qiskit.pulse import builder from qiskit.pulse.transforms import target_qobj_transform +from qiskit.dagcircuit import DAGOpNode from qiskit.test import QiskitTestCase -from qiskit.transpiler import PassManager +from qiskit.transpiler import PassManager, Target, InstructionProperties from qiskit.transpiler.passes.calibration.builders import ( RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho, RXCalibrationBuilder, ) -from qiskit.transpiler import Target, InstructionProperties -from qiskit.dagcircuit import DAGOpNode -from qiskit.circuit import Parameter -from qiskit import QuantumCircuit class TestCalibrationBuilder(QiskitTestCase): diff --git a/test/python/transpiler/test_normalize_rx_angle.py b/test/python/transpiler/test_normalize_rx_angle.py index 88136664b825..b3669b841afe 100644 --- a/test/python/transpiler/test_normalize_rx_angle.py +++ b/test/python/transpiler/test_normalize_rx_angle.py @@ -24,22 +24,14 @@ from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import FakeBelemV2 from qiskit.transpiler import Target +from qiskit.circuit.library.standard_gates import SXGate @ddt class TestNormalizeRXAngle(QiskitTestCase): """Tests the NormalizeRXAngle pass.""" - @staticmethod - def count_gate_number(gate, circuit): - """Count the number of a specific gate type in a circuit""" - if gate not in QuantumCircuit.count_ops(circuit): - gate_number = 0 - else: - gate_number = QuantumCircuit.count_ops(circuit)[gate] - return gate_number - - def test_not_convert_to_X_if_no_calib_in_target(self): + def test_not_convert_to_x_if_no_calib_in_target(self): """Check that RX(pi) is NOT converted to X, if X calibration is not present in the target""" empty_target = Target() @@ -49,37 +41,37 @@ def test_not_convert_to_X_if_no_calib_in_target(self): qc.rx(90, 0) transpiled_circ = tp(qc) - self.assertEqual(self.count_gate_number("x", transpiled_circ), 0) + self.assertEqual(transpiled_circ.count_ops().get("x", 0), 0) - def test_SX_conversion_works(self): + def test_sx_conversion_works(self): """Check that RX(pi/2) is converted to SX, if SX calibration is present in the target""" - backend = FakeBelemV2() - tp = NormalizeRXAngle(target=backend.target) + target = Target() + target.add_instruction(SXGate(), properties={(0,): None}) + tp = NormalizeRXAngle(target=target) qc = QuantumCircuit(1) qc.rx(np.pi / 2, 0) transpiled_circ = tp(qc) - self.assertEqual(self.count_gate_number("sx", transpiled_circ), 1) + self.assertEqual(transpiled_circ.count_ops().get("sx", 0), 1) - @named_data({"name": "RX(-pi/3)=RZ(pi)-RX(pi/3)-RZ(-pi)", "rx_angle": (-1 / 3) * np.pi}) - def test_RZ_added_for_negative_rotation_angles(self, rx_angle): + def test_rz_added_for_negative_rotation_angles(self): """Check that RZ is added before and after RX, if RX rotation angle is negative""" backend = FakeBelemV2() tp = NormalizeRXAngle(target=backend.target) - # circuit to transpiler and test + # circuit to transpile and test qc = QuantumCircuit(1) - qc.rx(rx_angle, 0) + qc.rx((-1 / 3) * np.pi, 0) transpiled_circ = tp(qc) # circuit to show the correct answer qc_ref = QuantumCircuit(1) qc_ref.rz(np.pi, 0) - qc_ref.rx(np.abs(rx_angle), 0) + qc_ref.rx(np.pi / 3, 0) qc_ref.rz(-np.pi, 0) self.assertQuantumCircuitEqual(transpiled_circ, qc_ref) From 35e5b93e7fba5102397e801bbcbf136617d84e7c Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Fri, 1 Sep 2023 23:10:58 +0900 Subject: [PATCH 12/18] Yet another attempt to fix docs error --- qiskit/transpiler/passes/calibration/rx_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py index d205adee230e..2f87861c0dae 100644 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -36,6 +36,7 @@ class RXCalibrationBuilder(CalibrationBuilder): A simple demo: .. code-block:: python + backend = FakeBelemV2() pm = PassManager(RXCalibrationBuilder(backend.target)) qc = QuantumCircuit(1) From 3011bb9ff50cb8e292cd3666c89d8a5aea105418 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Fri, 1 Sep 2023 23:52:15 +0900 Subject: [PATCH 13/18] Fix docs --- qiskit/transpiler/passes/calibration/rx_builder.py | 3 ++- releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py index 2f87861c0dae..2dfac55b63b4 100644 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -30,13 +30,14 @@ class RXCalibrationBuilder(CalibrationBuilder): """Add single-pulse RX calibrations that are bootstrapped from the SX calibration. .. note:: + Requirement: NormalizeRXAngles pass (one of the optimization passes). It is recommended to place this pass in the post-optimization stage of a passmanager. A simple demo: .. code-block:: python - + backend = FakeBelemV2() pm = PassManager(RXCalibrationBuilder(backend.target)) qc = QuantumCircuit(1) diff --git a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml index 6261d86e4a95..c6cc05c907cd 100644 --- a/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml +++ b/releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml @@ -14,9 +14,9 @@ features: :class:`~qiskit.transpiler.passes.calibration.rx_builder.RXCalibrationBuilder`, which generates RX calibrations on the fly. - The optimizations performed by `NormalizeRXAngle` reduce the amount of calibration data and + The optimizations performed by ``NormalizeRXAngle`` reduce the amount of calibration data and enable us to take advantage of the more accurate, hardware-calibrated - pulses. The calibrations generated by `RXCalibrationBuilder` are bootstrapped from + pulses. The calibrations generated by ``RXCalibrationBuilder`` are bootstrapped from the SX gate calibration, which should be already present in the target. The amplitude is linearly scaled to achieve the desired arbitrary rotation angle. From 1ff6c50aa6a7ce2e9aa5acda02ec2cf819dd2bb1 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Sat, 16 Sep 2023 16:45:18 +0900 Subject: [PATCH 14/18] Add 'atol=resolution/2' to np.isclose() tests for SX and X --- qiskit/transpiler/passes/optimization/normalize_rx_angle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py index 801f8fd423c4..04f643b4f365 100644 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -115,8 +115,8 @@ def run(self, dag): if self.resolution_in_radian: wrapped_theta = self.quantize_angles(op_node.qargs[0], wrapped_theta) - half_pi_rotation = np.isclose(abs(wrapped_theta), np.pi / 2) - pi_rotation = np.isclose(abs(wrapped_theta), np.pi) + half_pi_rotation = np.isclose(abs(wrapped_theta), np.pi / 2, atol=self.resolution_in_radians / 2) + pi_rotation = np.isclose(abs(wrapped_theta), np.pi, atol=self.resolution_in_radians / 2) should_modify_node = ( (wrapped_theta != raw_theta) From e56bb20833a0199ae7c9b432e7cd13cd13cb07dd Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Sat, 16 Sep 2023 16:51:50 +0900 Subject: [PATCH 15/18] fix typo --- qiskit/transpiler/passes/optimization/normalize_rx_angle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py index 04f643b4f365..8e917dab001d 100644 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -115,8 +115,8 @@ def run(self, dag): if self.resolution_in_radian: wrapped_theta = self.quantize_angles(op_node.qargs[0], wrapped_theta) - half_pi_rotation = np.isclose(abs(wrapped_theta), np.pi / 2, atol=self.resolution_in_radians / 2) - pi_rotation = np.isclose(abs(wrapped_theta), np.pi, atol=self.resolution_in_radians / 2) + half_pi_rotation = np.isclose(abs(wrapped_theta), np.pi / 2, atol=self.resolution_in_radian / 2) + pi_rotation = np.isclose(abs(wrapped_theta), np.pi, atol=self.resolution_in_radian / 2) should_modify_node = ( (wrapped_theta != raw_theta) From aa0270c4549ed6d8627f76c56c8a1cdb87bc7d60 Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Sun, 17 Sep 2023 18:25:50 +0900 Subject: [PATCH 16/18] Elaborate on quantize_angles() --- .../passes/optimization/normalize_rx_angle.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py index 8e917dab001d..33b381fc2283 100644 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py @@ -14,7 +14,8 @@ the single-pulse RX gates: Wrap RX Gate rotation angles into [0, pi] by sandwiching them with RZ gates. Convert RX(pi/2) to SX, and RX(pi) to X if the calibrations exist in the target. -Quantize the RX rotation angles using a resolution provided by the user. +Quantize the RX rotation angles by assigning the same value for the angles +that differ within a resolution provided by the user. """ import numpy as np @@ -64,7 +65,8 @@ def __init__(self, target=None, resolution_in_radian=0): self.already_generated = {} def quantize_angles(self, qubit, original_angle): - """Quantize the RX rotation angles using a resolution provided by the user. + """Quantize the RX rotation angles by assigning the same value for the angles + that differ within a resolution provided by the user. Args: qubit (Qubit): This will be the dict key to access the list of quantized rotation angles. @@ -115,7 +117,9 @@ def run(self, dag): if self.resolution_in_radian: wrapped_theta = self.quantize_angles(op_node.qargs[0], wrapped_theta) - half_pi_rotation = np.isclose(abs(wrapped_theta), np.pi / 2, atol=self.resolution_in_radian / 2) + half_pi_rotation = np.isclose( + abs(wrapped_theta), np.pi / 2, atol=self.resolution_in_radian / 2 + ) pi_rotation = np.isclose(abs(wrapped_theta), np.pi, atol=self.resolution_in_radian / 2) should_modify_node = ( From 12cbeb004e7b3aa03bcb24fe62a2eccddd90d2fd Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Sun, 17 Sep 2023 22:45:49 +0900 Subject: [PATCH 17/18] Add a demo with a QuantumVolume circuit --- .../passes/calibration/rx_builder.py | 51 ++++++++++++++----- .../transpiler/test_calibrationbuilder.py | 12 +++++ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py index 2dfac55b63b4..a7bfcb63c3dd 100644 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -38,16 +38,36 @@ class RXCalibrationBuilder(CalibrationBuilder): .. code-block:: python - backend = FakeBelemV2() - pm = PassManager(RXCalibrationBuilder(backend.target)) - qc = QuantumCircuit(1) - angles = [0.1, 0.2, 0.3, 0.4] - for angle in angles: - qc.rx(angle, 0) - - # run the pass and check that new calibrations are generated - transpiled_circuit = pm.run(qc) - print(transpiled_circuit.calibrations["rx"]) + from qiskit.providers.fake_provider import FakeBelemV2 + from qiskit.transpiler import PassManager, PassManagerConfig + from qiskit.transpiler.preset_passmanagers import level_1_pass_manager + from qiskit.circuit import Parameter + from qiskit.circuit.library import QuantumVolume + from qiskit.circuit.library.standard_gates import RXGate + + from calibration.rx_builder import RXCalibrationBuilder + + qv = QuantumVolume(4, 4, seed=1004) + + # Transpiling with single pulse RX gates enabled + backend_with_single_pulse_rx = FakeBelemV2() + rx_inst_props = {} + for i in range(backend_with_single_pulse_rx.num_qubits): + rx_inst_props[(i,)] = None + backend_with_single_pulse_rx.target.add_instruction(RXGate(Parameter("theta")), rx_inst_props) + config_with_rx = PassManagerConfig.from_backend(backend=backend_with_single_pulse_rx) + pm_with_rx = level_1_pass_manager(pass_manager_config=config_with_rx) + rx_builder = RXCalibrationBuilder(target=backend_with_single_pulse_rx.target) + pm_with_rx.post_optimization = PassManager([rx_builder]) + transpiled_circ_with_single_pulse_rx = pm_with_rx.run(qv) + transpiled_circ_with_single_pulse_rx.count_ops() + + # Conventional transpilation: each RX gate is decomposed into a sequence with two SX gates + original_backend = FakeBelemV2() + original_config = PassManagerConfig.from_backend(backend=original_backend) + original_pm = level_1_pass_manager(pass_manager_config=original_config) + original_transpiled_circ = original_pm.run(qv) + original_transpiled_circ.count_ops() References * [1]: Gokhale et al. (2020), Optimized Quantum Compilation for @@ -75,9 +95,16 @@ def __init__( def supported(self, node_op: Instruction, qubits: list) -> bool: """ - Check if the calibration for SX gate exists. + Check if the calibration for SX gate exists and it's a single DRAG pulse. """ - return isinstance(node_op, RXGate) and self.target.has_calibration("sx", tuple(qubits)) + return ( + isinstance(node_op, RXGate) + and self.target.has_calibration("sx", tuple(qubits)) + and (len(self.target.get_calibration("sx", tuple(qubits)).instructions) == 1) + and isinstance( + self.target.get_calibration("sx", tuple(qubits)).instructions[0][1].pulse, Drag + ) + ) def get_calibration(self, node_op: Instruction, qubits: list) -> Union[Schedule, ScheduleBlock]: """ diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py index faee6ef0fd43..45ada1789a88 100644 --- a/test/python/transpiler/test_calibrationbuilder.py +++ b/test/python/transpiler/test_calibrationbuilder.py @@ -33,6 +33,7 @@ InstructionScheduleMap, Schedule, Drag, + Square, ) from qiskit.pulse import builder from qiskit.pulse.transforms import target_qobj_transform @@ -451,6 +452,17 @@ def test_not_supported_if_no_sx_schedule(self): node_op = DAGOpNode(RXGate(0.5), qubits, []) self.assertFalse(tp.supported(node_op, qubits)) + def test_not_supported_if_sx_not_drag(self): + """Test that supported() returns False when the default SX calibration is not a DRAG.""" + target = Target() + with builder.build() as square_sx_cal: + builder.play(Square(amp=0.1, duration=160, phase=0), DriveChannel(0)) + target.add_instruction(SXGate(), {(0,): InstructionProperties(calibration=square_sx_cal)}) + tp = RXCalibrationBuilder(target) + qubits = (0,) + node_op = DAGOpNode(RXGate(0.5), qubits, []) + self.assertFalse(tp.supported(node_op, qubits)) + def test_raises_error_when_rotation_angle_not_assigned(self): """Test that get_calibration() fails when the RX gate's rotation angle is an unassigned Parameter, not a number. From 9a5337fcda4b0177fa295978015aff13d3bdc2dc Mon Sep 17 00:00:00 2001 From: Jaeun Kim Date: Thu, 21 Sep 2023 00:39:20 +0900 Subject: [PATCH 18/18] Check pulse.pulse_type==Drag instead of isinstance(pulse, Drag) --- qiskit/transpiler/passes/calibration/rx_builder.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py index a7bfcb63c3dd..1aff88ec802f 100644 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -17,7 +17,7 @@ import numpy as np from qiskit.circuit import Instruction -from qiskit.pulse import Schedule, ScheduleBlock, builder +from qiskit.pulse import Schedule, ScheduleBlock, builder, ScalableSymbolicPulse from qiskit.pulse.channels import Channel from qiskit.pulse.library.symbolic_pulses import Drag from qiskit.transpiler.passes.calibration.base_builder import CalibrationBuilder @@ -102,8 +102,11 @@ def supported(self, node_op: Instruction, qubits: list) -> bool: and self.target.has_calibration("sx", tuple(qubits)) and (len(self.target.get_calibration("sx", tuple(qubits)).instructions) == 1) and isinstance( - self.target.get_calibration("sx", tuple(qubits)).instructions[0][1].pulse, Drag + self.target.get_calibration("sx", tuple(qubits)).instructions[0][1].pulse, + ScalableSymbolicPulse, ) + and self.target.get_calibration("sx", tuple(qubits)).instructions[0][1].pulse.pulse_type + == "Drag" ) def get_calibration(self, node_op: Instruction, qubits: list) -> Union[Schedule, ScheduleBlock]: