Source code for qiskit.transpiler.passes.optimization.normalize_rx_angle
# 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
[docs]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 arbitrary-angle RX)
"""
super().__init__()
self.target = target
self.resolution_in_radian = resolution_in_radian
self.already_generated = {}
[docs] 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.
"""
if (angles := self.already_generated.get(qubit)) is None:
self.already_generated[qubit] = np.array([original_angle])
return original_angle
similar_angles = angles[
np.isclose(angles, original_angle, atol=self.resolution_in_radian / 2)
]
if similar_angles.size == 0:
self.already_generated[qubit] = np.append(angles, original_angle)
return original_angle
return float(similar_angles[0])
[docs] 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