Skip to content
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

Add NormalizeRXAngle and RXCalibrationBuilder passes #10634

Merged
merged 19 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions qiskit/transpiler/passes/calibration/rx_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,8 +30,25 @@ 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)
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.
Expand All @@ -56,9 +73,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.
Expand All @@ -84,7 +98,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"],
Expand All @@ -101,7 +115,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].
Expand All @@ -110,7 +124,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
68 changes: 36 additions & 32 deletions qiskit/transpiler/passes/optimization/normalize_rx_angle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -63,8 +77,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
Expand All @@ -78,19 +95,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]
Expand All @@ -102,14 +118,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)
Expand All @@ -122,18 +130,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
Expand Down
17 changes: 6 additions & 11 deletions releasenotes/notes/single-pulse-rx-cal-347aadcee7bfe60b.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 4 additions & 6 deletions test/python/transpiler/test_calibrationbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
32 changes: 12 additions & 20 deletions test/python/transpiler/test_normalize_rx_angle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down