-
Notifications
You must be signed in to change notification settings - Fork 3k
Add NormalizeRXAngle and RXCalibrationBuilder passes #10634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
92344c2
5e23f55
acb5ee9
c681760
79a44d8
2b0664c
fe1fc06
9244800
60fb2c4
87f15df
6d6a059
35e5b93
3011bb9
1ff6c50
e56bb20
aa0270c
12cbeb0
dac2c64
9a5337f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| # 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. | ||
|
|
||
| """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 <https://arxiv.org/abs/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: | ||
|
jaeunkim marked this conversation as resolved.
Outdated
|
||
| 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]), | ||
|
jaeunkim marked this conversation as resolved.
Outdated
|
||
| duration=params["duration"], | ||
| amp=params["amp"], | ||
| sigma=params["sigma"], | ||
| beta=params["beta"], | ||
| ) | ||
|
|
||
| return new_rx_sched | ||
|
|
||
|
|
||
| @lru_cache | ||
| def _create_rx_sched( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably fine, but it makes me a little nervous. I would do more checking to confirm that the default calibration is a single Drag pulse.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added two tests in the def supported(self, node_op: Instruction, qubits: list) -> bool:
"""
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))
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
)
)However, I can't decide whether we should allow non-DRAG pulses too or not.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be okay to check for Also,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you @wshanks, I'm trying to cover the case where the original This is an example of an unusually complicated sx schedule: d0 = pulse.DriveChannel(0)
with pulse.build() as complicated_sx_cal:
pulse.play(Drag(duration=100, amp=0.1, sigma=40, beta=0.2, angle=10), d0)
pulse.delay(50, d0)
pulse.play(
GaussianSquare(duration=130, amp=0.3, sigma=30, risefall_sigma_ratio=2, angle=30), d0
)This is my attempt: # goal: copy a ScheduleBlock, but halve the amplitude.
with pulse.build() as rx_cal:
for i in range(len(complicated_sx_cal.instructions)):
if isinstance(complicated_sx_cal.instructions[i][1], Play) and isinstance(
complicated_sx_cal.instructions[i][1].pulse, ScalableSymbolicPulse
):
# caveat: assumed that all the params are immutable (checked that Parameter is immutable)
params = complicated_sx_cal.instructions[i][1].pulse.parameters.copy()
halved_amp = params.pop("amp") * 0.5
duration = params.pop("duration")
angle = params.pop("angle", 0)
pulse.play(
ScalableSymbolicPulse(
complicated_sx_cal.instructions[i][1].pulse.pulse_type,
duration=duration,
amp=halved_amp,
angle=angle,
parameters=params,
),
channel=complicated_sx_cal.channels[0]
)
else:
complicated_sx_cal.instructions[i][1] #??Questions:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I print out the above However, I get
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, we can see what @nkanazawa1989 thinks. One option is not to try to handle such a complicated case, but to choose either to give an error or ignore it. A complicated schedule seems not too likely to come up, and either giving an error or ignoring the gate would avoid the case of assigning the wrong schedule (i.e. assigning a Drag with amplitude based on the first pulse amplitude if the schedule really has multiple instructions). I think it's pretty tricky to walk through a new_sched = copy.deepcopy(sched)
for block in sched.blocks:
if isinstance(block, Play) and isinstance(block.pulse, ScalableSymbolicPulse):
new = Play(ScalableSymbolicPulse(pulse_type=block.pulse.pulse_type, ...<a lot of options to copy>), block.channel)
new_sched.replace(block, new)but that only covers the top level blocks. You would also need to check
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, I agree that it's reasonable to reject a schedule with multiple pulses. To deal with any type of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copying a p = pulse.Drag(100, 0.5, 10, 0.1)
p2 = pulse.ScalableSymbolicPulse(
pulse_type=p.pulse_type,
duration=p.duration,
amp=p.amp,
angle=p.angle,
parameters={k: v for k, v in p.parameters.items() if k not in ("amp", "angle", "duration")},
limit_amplitude=p.limit_amplitude,
envelope=p.envelope,
constraints=p.constraints,
valid_amp_conditions=p.valid_amp_conditions,
)It would be nice to be able to copy the pulse and mutate the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I agree that the current version is fine if you don't want to support
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the discussion 😊 I updated the Drag check according to your suggestion. (In other words, the current version supports only the calibrations with a single Drag pulse) |
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
wshanks marked this conversation as resolved.
Outdated
|
||
| """ | ||
|
|
||
| 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. | ||
| """ | ||
|
nkanazawa1989 marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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))] | ||
|
nkanazawa1989 marked this conversation as resolved.
Outdated
|
||
| ) | ||
| except KeyError: | ||
| quantized_angle = original_angle | ||
| self.already_generated[qubit] = np.array([quantized_angle]) | ||
| except TypeError: | ||
| quantized_angle = original_angle | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess current logic causes an edge case. For example, pass_ = NormalizeRXAngle(target, resolution_in_radian=0.1)
pass_.quantize_angles(0, 1.23)
pass_.quantize_angles(0, 1.24)
pass_.quantize_angles(0, 1.235) # What happens here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, more than one value will return 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)(Note: the |
||
| 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(). | ||
|
jaeunkim marked this conversation as resolved.
Outdated
|
||
|
|
||
| Args: | ||
| dag (DAGCircuit): The DAG to be optimized. | ||
|
|
||
| Returns: | ||
| DAGCircuit: A DAG where all RX rotation angles are within [0, pi]. | ||
|
jaeunkim marked this conversation as resolved.
Outdated
|
||
| """ | ||
|
|
||
| # 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"): | ||
|
jaeunkim marked this conversation as resolved.
Outdated
|
||
| 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) | ||
|
jaeunkim marked this conversation as resolved.
Outdated
|
||
|
|
||
| # 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 | ||
|
jaeunkim marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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,)) | ||
|
jaeunkim marked this conversation as resolved.
Outdated
|
||
| ): | ||
| 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: | ||
|
nkanazawa1989 marked this conversation as resolved.
|
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <https://arxiv.org/abs/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. | ||
|
jaeunkim marked this conversation as resolved.
|
||
| 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. | ||

Uh oh!
There was an error while loading. Please reload this page.