-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add NormalizeRXAngle and RXCalibrationBuilder passes (#10634)
* Added NormalizeRXAngle and RXCalibrationBuilder passes to build single-pulse RX gate calibrations * Fixed a typo in documentation * Removed all cross-references in the documentation * Added URL links back * Added cross references back to the release note * Shorten the cross references in the release note * Fixed normalize_rx_angle.py based on the comments * Update qiskit/transpiler/passes/optimization/normalize_rx_angle.py Edit the doc so that it looks nicer in html Co-authored-by: Naoki Kanazawa <[email protected]> * Reformat the doc of NormalizeRXAngle * Try to pass the doc test * Addressed feedback and comments from the code review * Yet another attempt to fix docs error * Fix docs * Add 'atol=resolution/2' to np.isclose() tests for SX and X * fix typo * Elaborate on quantize_angles() * Add a demo with a QuantumVolume circuit * Check pulse.pulse_type==Drag instead of isinstance(pulse, Drag) --------- Co-authored-by: Naoki Kanazawa <[email protected]>
- Loading branch information
1 parent
9843fbf
commit fc74ab9
Showing
9 changed files
with
590 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
# 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, ScalableSymbolicPulse | ||
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). | ||
It is recommended to place this pass in the post-optimization stage of a passmanager. | ||
A simple demo: | ||
.. code-block:: python | ||
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 | ||
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)] | ||
|
||
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, | ||
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]: | ||
""" | ||
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=self.target.get_calibration("sx", tuple(qubits)).channels[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: 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, | ||
) | ||
|
||
return new_rx_sched |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
157 changes: 157 additions & 0 deletions
157
qiskit/transpiler/passes/optimization/normalize_rx_angle.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
# 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 by assigning the same value for the angles | ||
that differ within 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): | ||
"""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): | ||
"""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 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. | ||
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 | ||
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 | ||
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``. | ||
Args: | ||
dag (DAGCircuit): The DAG to be optimized. | ||
Returns: | ||
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 isinstance(op_node.op, RXGate): | ||
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, 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) | ||
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: | ||
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 | ||
) | ||
|
||
# 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 |
25 changes: 25 additions & 0 deletions
25
releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
--- | ||
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 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 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. |
Oops, something went wrong.