diff --git a/qiskit/transpiler/passes/calibration/__init__.py b/qiskit/transpiler/passes/calibration/__init__.py index 3b80fbb26530..10810c1cdff1 100644 --- a/qiskit/transpiler/passes/calibration/__init__.py +++ b/qiskit/transpiler/passes/calibration/__init__.py @@ -12,4 +12,5 @@ """Module containing transpiler calibration passes.""" -from .builders import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho, PulseGates +from .pulse_gate import PulseGates +from .rzx_builder import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho diff --git a/qiskit/transpiler/passes/calibration/base_builder.py b/qiskit/transpiler/passes/calibration/base_builder.py new file mode 100644 index 000000000000..488cc1dc2e65 --- /dev/null +++ b/qiskit/transpiler/passes/calibration/base_builder.py @@ -0,0 +1,80 @@ +# 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. + +"""Calibration builder base class.""" + +from abc import abstractmethod +from typing import List, Union + +from qiskit.circuit import Instruction as CircuitInst +from qiskit.dagcircuit import DAGCircuit +from qiskit.pulse import Schedule, ScheduleBlock +from qiskit.pulse.instruction_schedule_map import CalibrationPublisher +from qiskit.transpiler.basepasses import TransformationPass + +from .exceptions import CalibrationNotAvailable + + +class CalibrationBuilder(TransformationPass): + """Abstract base class to inject calibrations into circuits.""" + + @abstractmethod + def supported(self, node_op: CircuitInst, qubits: List) -> bool: + """Determine if a given node supports the calibration. + + Args: + node_op: Target instruction object. + qubits: Integer qubit indices to check. + + Returns: + Return ``True`` is calibration can be provided. + """ + + @abstractmethod + def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: + """Gets the calibrated schedule for the given instruction and qubits. + + Args: + node_op: Target instruction object. + qubits: Integer qubit indices to check. + + Returns: + Return Schedule of target gate instruction. + """ + + def run(self, dag: DAGCircuit) -> DAGCircuit: + """Run the calibration adder pass on `dag`. + + Args: + dag: DAG to schedule. + + Returns: + A DAG with calibrations added to it. + """ + qubit_map = {qubit: i for i, qubit in enumerate(dag.qubits)} + for node in dag.gate_nodes(): + qubits = [qubit_map[q] for q in node.qargs] + + if self.supported(node.op, qubits) and not dag.has_calibration_for(node): + # calibration can be provided and no user-defined calibration is already provided + try: + schedule = self.get_calibration(node.op, qubits) + except CalibrationNotAvailable: + # Fail in schedule generation. Just ignore. + continue + publisher = schedule.metadata.get("publisher", CalibrationPublisher.QISKIT) + + # add calibration if it is not backend default + if publisher != CalibrationPublisher.BACKEND_PROVIDER: + dag.add_calibration(gate=node.op, qubits=qubits, schedule=schedule) + + return dag diff --git a/qiskit/transpiler/passes/calibration/builders.py b/qiskit/transpiler/passes/calibration/builders.py index 29af85f17d55..93adb4f945f1 100644 --- a/qiskit/transpiler/passes/calibration/builders.py +++ b/qiskit/transpiler/passes/calibration/builders.py @@ -12,469 +12,8 @@ """Calibration creators.""" -from abc import abstractmethod -from typing import List, Union +# TODO This import path will be deprecated. -import math -import numpy as np - -from qiskit.circuit import Instruction as CircuitInst -from qiskit.circuit.library.standard_gates import RZXGate -from qiskit.dagcircuit import DAGCircuit -from qiskit.exceptions import QiskitError -from qiskit.pulse import ( - Play, - Delay, - ShiftPhase, - Schedule, - ScheduleBlock, - ControlChannel, - DriveChannel, - GaussianSquare, -) -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap, CalibrationPublisher -from qiskit.pulse.instructions.instruction import Instruction as PulseInst -from qiskit.transpiler.basepasses import TransformationPass - - -class CalibrationBuilder(TransformationPass): - """Abstract base class to inject calibrations into circuits.""" - - @abstractmethod - def supported(self, node_op: CircuitInst, qubits: List) -> bool: - """Determine if a given node supports the calibration. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return ``True`` is calibration can be provided. - """ - - @abstractmethod - def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: - """Gets the calibrated schedule for the given instruction and qubits. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return Schedule of target gate instruction. - """ - - def run(self, dag: DAGCircuit) -> DAGCircuit: - """Run the calibration adder pass on `dag`. - - Args: - dag: DAG to schedule. - - Returns: - A DAG with calibrations added to it. - """ - qubit_map = {qubit: i for i, qubit in enumerate(dag.qubits)} - for node in dag.gate_nodes(): - qubits = [qubit_map[q] for q in node.qargs] - - if self.supported(node.op, qubits) and not dag.has_calibration_for(node): - # calibration can be provided and no user-defined calibration is already provided - schedule = self.get_calibration(node.op, qubits) - publisher = schedule.metadata.get("publisher", CalibrationPublisher.QISKIT) - - # add calibration if it is not backend default - if publisher != CalibrationPublisher.BACKEND_PROVIDER: - dag.add_calibration(gate=node.op, qubits=qubits, schedule=schedule) - - return dag - - -class RZXCalibrationBuilder(CalibrationBuilder): - """ - Creates calibrations for RZXGate(theta) by stretching and compressing - Gaussian square pulses in the CX gate. This is done by retrieving (for a given pair of - qubits) the CX schedule in the instruction schedule map of the backend defaults. - The CX schedule must be an echoed cross-resonance gate optionally with rotary tones. - The cross-resonance drive tones and rotary pulses must be Gaussian square pulses. - The width of the Gaussian square pulse is adjusted so as to match the desired rotation angle. - If the rotation angle is small such that the width disappears then the amplitude of the - zero width Gaussian square pulse (i.e. a Gaussian) is reduced to reach the target rotation - angle. Additional details can be found in https://arxiv.org/abs/2012.11660. - """ - - def __init__( - self, - instruction_schedule_map: InstructionScheduleMap = None, - qubit_channel_mapping: List[List[str]] = None, - ): - """ - Initializes a RZXGate calibration builder. - - Args: - instruction_schedule_map: The :obj:`InstructionScheduleMap` object representing the - default pulse calibrations for the target backend - qubit_channel_mapping: The list mapping qubit indices to the list of - channel names that apply on that qubit. - - Raises: - QiskitError: if open pulse is not supported by the backend. - """ - super().__init__() - if instruction_schedule_map is None or qubit_channel_mapping is None: - raise QiskitError("Calibrations can only be added to Pulse-enabled backends") - - self._inst_map = instruction_schedule_map - self._channel_map = qubit_channel_mapping - - def supported(self, node_op: CircuitInst, qubits: List) -> bool: - """Determine if a given node supports the calibration. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return ``True`` is calibration can be provided. - """ - return isinstance(node_op, RZXGate) - - @staticmethod - def rescale_cr_inst(instruction: Play, theta: float, sample_mult: int = 16) -> Play: - """ - Args: - instruction: The instruction from which to create a new shortened or lengthened pulse. - theta: desired angle, pi/2 is assumed to be the angle that the pulse in the given - play instruction implements. - sample_mult: All pulses must be a multiple of sample_mult. - - Returns: - qiskit.pulse.Play: The play instruction with the stretched compressed - GaussianSquare pulse. - - Raises: - QiskitError: if the pulses are not GaussianSquare. - QiskitError: if rotation angle is not assigned. - """ - try: - theta = float(theta) - except TypeError as ex: - raise QiskitError("Target rotation angle is not assigned.") from ex - - pulse_ = instruction.pulse - if isinstance(pulse_, GaussianSquare): - amp = pulse_.amp - width = pulse_.width - sigma = pulse_.sigma - n_sigmas = (pulse_.duration - width) / sigma - - # The error function is used because the Gaussian may have chopped tails. - gaussian_area = abs(amp) * sigma * np.sqrt(2 * np.pi) * math.erf(n_sigmas) - area = gaussian_area + abs(amp) * width - - target_area = abs(theta) / (np.pi / 2.0) * area - sign = np.sign(theta) - - if target_area > gaussian_area: - width = (target_area - gaussian_area) / abs(amp) - duration = round((width + n_sigmas * sigma) / sample_mult) * sample_mult - return Play( - GaussianSquare(amp=sign * amp, width=width, sigma=sigma, duration=duration), - channel=instruction.channel, - ) - else: - amp_scale = sign * target_area / gaussian_area - duration = round(n_sigmas * sigma / sample_mult) * sample_mult - return Play( - GaussianSquare(amp=amp * amp_scale, width=0, sigma=sigma, duration=duration), - channel=instruction.channel, - ) - else: - raise QiskitError("RZXCalibrationBuilder only stretches/compresses GaussianSquare.") - - def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: - """Builds the calibration schedule for the RZXGate(theta) with echos. - - Args: - node_op: Instruction of the RZXGate(theta). I.e. params[0] is theta. - qubits: List of qubits for which to get the schedules. The first qubit is - the control and the second is the target. - - Returns: - schedule: The calibration schedule for the RZXGate(theta). - - Raises: - QiskitError: if the control and target qubits cannot be identified or the backend - does not support cx between the qubits. - """ - theta = node_op.params[0] - q1, q2 = qubits[0], qubits[1] - - if not self._inst_map.has("cx", qubits): - raise QiskitError( - "This transpilation pass requires the backend to support cx " - "between qubits %i and %i." % (q1, q2) - ) - - cx_sched = self._inst_map.get("cx", qubits=(q1, q2)) - rzx_theta = Schedule(name="rzx(%.3f)" % theta) - rzx_theta.metadata["publisher"] = CalibrationPublisher.QISKIT - - if theta == 0.0: - return rzx_theta - - crs, comp_tones = [], [] - control, target = None, None - - for time, inst in cx_sched.instructions: - - # Identify the CR pulses. - if isinstance(inst, Play) and not isinstance(inst, ShiftPhase): - if isinstance(inst.channel, ControlChannel): - crs.append((time, inst)) - - # Identify the compensation tones. - if isinstance(inst.channel, DriveChannel) and not isinstance(inst, ShiftPhase): - if isinstance(inst.pulse, GaussianSquare): - comp_tones.append((time, inst)) - target = inst.channel.index - control = q1 if target == q2 else q2 - - if control is None: - raise QiskitError("Control qubit is None.") - if target is None: - raise QiskitError("Target qubit is None.") - - echo_x = self._inst_map.get("x", qubits=control) - - # Build the schedule - - # Stretch/compress the CR gates and compensation tones - cr1 = self.rescale_cr_inst(crs[0][1], theta) - cr2 = self.rescale_cr_inst(crs[1][1], theta) - - if len(comp_tones) == 0: - comp1, comp2 = None, None - elif len(comp_tones) == 2: - comp1 = self.rescale_cr_inst(comp_tones[0][1], theta) - comp2 = self.rescale_cr_inst(comp_tones[1][1], theta) - else: - raise QiskitError( - "CX must have either 0 or 2 rotary tones between qubits %i and %i " - "but %i were found." % (control, target, len(comp_tones)) - ) - - # Build the schedule for the RZXGate - rzx_theta = rzx_theta.insert(0, cr1) - - if comp1 is not None: - rzx_theta = rzx_theta.insert(0, comp1) - - rzx_theta = rzx_theta.insert(comp1.duration, echo_x) - time = comp1.duration + echo_x.duration - rzx_theta = rzx_theta.insert(time, cr2) - - if comp2 is not None: - rzx_theta = rzx_theta.insert(time, comp2) - - time = 2 * comp1.duration + echo_x.duration - rzx_theta = rzx_theta.insert(time, echo_x) - - # Reverse direction of the ZX with Hadamard gates - if control == qubits[0]: - return rzx_theta - else: - rzc = self._inst_map.get("rz", [control], np.pi / 2) - sxc = self._inst_map.get("sx", [control]) - rzt = self._inst_map.get("rz", [target], np.pi / 2) - sxt = self._inst_map.get("sx", [target]) - h_sched = Schedule(name="hadamards") - h_sched = h_sched.insert(0, rzc) - h_sched = h_sched.insert(0, sxc) - h_sched = h_sched.insert(sxc.duration, rzc) - h_sched = h_sched.insert(0, rzt) - h_sched = h_sched.insert(0, sxt) - h_sched = h_sched.insert(sxc.duration, rzt) - rzx_theta = h_sched.append(rzx_theta) - return rzx_theta.append(h_sched) - - -class RZXCalibrationBuilderNoEcho(RZXCalibrationBuilder): - """ - Creates calibrations for RZXGate(theta) by stretching and compressing - Gaussian square pulses in the CX gate. - - The ``RZXCalibrationBuilderNoEcho`` is a variation of the - :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` pass - that creates calibrations for the cross-resonance pulses without inserting - the echo pulses in the pulse schedule. This enables exposing the echo in - the cross-resonance sequence as gates so that the transpiler can simplify them. - The ``RZXCalibrationBuilderNoEcho`` only supports the hardware-native direction - of the CX gate. - """ - - @staticmethod - def _filter_control(inst: (int, Union["Schedule", PulseInst])) -> bool: - """ - Looks for Gaussian square pulses applied to control channels. - - Args: - inst: Instructions to be filtered. - - Returns: - match: True if the instruction is a Play instruction with - a Gaussian square pulse on the ControlChannel. - """ - if isinstance(inst[1], Play): - if isinstance(inst[1].pulse, GaussianSquare) and isinstance( - inst[1].channel, ControlChannel - ): - return True - - return False - - @staticmethod - def _filter_drive(inst: (int, Union["Schedule", PulseInst])) -> bool: - """ - Looks for Gaussian square pulses applied to drive channels. - - Args: - inst: Instructions to be filtered. - - Returns: - match: True if the instruction is a Play instruction with - a Gaussian square pulse on the DriveChannel. - """ - if isinstance(inst[1], Play): - if isinstance(inst[1].pulse, GaussianSquare) and isinstance( - inst[1].channel, DriveChannel - ): - return True - - return False - - def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: - """Builds the calibration schedule for the RZXGate(theta) without echos. - - Args: - node_op: Instruction of the RZXGate(theta). I.e. params[0] is theta. - qubits: List of qubits for which to get the schedules. The first qubit is - the control and the second is the target. - - Returns: - schedule: The calibration schedule for the RZXGate(theta). - - Raises: - QiskitError: If the control and target qubits cannot be identified, or the backend - does not support a cx gate between the qubits, or the backend does not natively - support the specified direction of the cx. - """ - theta = node_op.params[0] - q1, q2 = qubits[0], qubits[1] - - if not self._inst_map.has("cx", qubits): - raise QiskitError( - "This transpilation pass requires the backend to support cx " - "between qubits %i and %i." % (q1, q2) - ) - - cx_sched = self._inst_map.get("cx", qubits=(q1, q2)) - rzx_theta = Schedule(name="rzx(%.3f)" % theta) - rzx_theta.metadata["publisher"] = CalibrationPublisher.QISKIT - - if theta == 0.0: - return rzx_theta - - control, target = None, None - - for _, inst in cx_sched.instructions: - # Identify the compensation tones. - if isinstance(inst.channel, DriveChannel) and isinstance(inst, Play): - if isinstance(inst.pulse, GaussianSquare): - target = inst.channel.index - control = q1 if target == q2 else q2 - - if control is None: - raise QiskitError("Control qubit is None.") - if target is None: - raise QiskitError("Target qubit is None.") - - if control != qubits[0]: - raise QiskitError( - "RZXCalibrationBuilderNoEcho only supports hardware-native RZX gates." - ) - - # Get the filtered Schedule instructions for the CR gates and compensation tones. - crs = cx_sched.filter(*[self._filter_control]).instructions - rotaries = cx_sched.filter(*[self._filter_drive]).instructions - - # Stretch/compress the CR gates and compensation tones. - cr = self.rescale_cr_inst(crs[0][1], 2 * theta) - rot = self.rescale_cr_inst(rotaries[0][1], 2 * theta) - - # Build the schedule for the RZXGate without the echos. - rzx_theta = rzx_theta.insert(0, cr) - rzx_theta = rzx_theta.insert(0, rot) - rzx_theta = rzx_theta.insert(0, Delay(cr.duration, DriveChannel(control))) - - return rzx_theta - - -class PulseGates(CalibrationBuilder): - """Pulse gate adding pass. - - This pass adds gate calibrations from the supplied ``InstructionScheduleMap`` - to a quantum circuit. - - This pass checks each DAG circuit node and acquires a corresponding schedule from - the instruction schedule map object that may be provided by the target backend. - Because this map is a mutable object, the end-user can provide a configured backend to - execute the circuit with customized gate implementations. - - This mapping object returns a schedule with "publisher" metadata which is an integer Enum - value representing who created the gate schedule. - If the gate schedule is provided by end-users, this pass attaches the schedule to - the DAG circuit as a calibration. - - This pass allows users to easily override quantum circuit with custom gate definitions - without directly dealing with those schedules. - - References - * [1] OpenQASM 3: A broader and deeper quantum assembly language - https://arxiv.org/abs/2104.14722 - """ - - def __init__( - self, - inst_map: InstructionScheduleMap, - ): - """Create new pass. - - Args: - inst_map: Instruction schedule map that user may override. - """ - super().__init__() - self.inst_map = inst_map - - def supported(self, node_op: CircuitInst, qubits: List) -> bool: - """Determine if a given node supports the calibration. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return ``True`` is calibration can be provided. - """ - return self.inst_map.has(instruction=node_op.name, qubits=qubits) - - def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: - """Gets the calibrated schedule for the given instruction and qubits. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return Schedule of target gate instruction. - """ - return self.inst_map.get(node_op.name, qubits, *node_op.params) +# pylint: disable=unused-import +from .pulse_gate import PulseGates +from .rzx_builder import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho diff --git a/qiskit/transpiler/passes/calibration/exceptions.py b/qiskit/transpiler/passes/calibration/exceptions.py new file mode 100644 index 000000000000..7785f49d1d30 --- /dev/null +++ b/qiskit/transpiler/passes/calibration/exceptions.py @@ -0,0 +1,22 @@ +# 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. + +"""Exception for errors raised by the calibration pass module.""" +from qiskit.exceptions import QiskitError + + +class CalibrationNotAvailable(QiskitError): + """Raised when calibration generation fails. + + .. note:: + This error is meant to caught by CalibrationBuilder and ignored. + """ diff --git a/qiskit/transpiler/passes/calibration/pulse_gate.py b/qiskit/transpiler/passes/calibration/pulse_gate.py new file mode 100644 index 000000000000..ebcbfb93ddb4 --- /dev/null +++ b/qiskit/transpiler/passes/calibration/pulse_gate.py @@ -0,0 +1,85 @@ +# 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. + +"""Instruction scheduel map reference pass.""" + +from typing import List, Union + +from qiskit.circuit import Instruction as CircuitInst +from qiskit.pulse import ( + Schedule, + ScheduleBlock, +) +from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap + +from .base_builder import CalibrationBuilder + + +class PulseGates(CalibrationBuilder): + """Pulse gate adding pass. + + This pass adds gate calibrations from the supplied ``InstructionScheduleMap`` + to a quantum circuit. + + This pass checks each DAG circuit node and acquires a corresponding schedule from + the instruction schedule map object that may be provided by the target backend. + Because this map is a mutable object, the end-user can provide a configured backend to + execute the circuit with customized gate implementations. + + This mapping object returns a schedule with "publisher" metadata which is an integer Enum + value representing who created the gate schedule. + If the gate schedule is provided by end-users, this pass attaches the schedule to + the DAG circuit as a calibration. + + This pass allows users to easily override quantum circuit with custom gate definitions + without directly dealing with those schedules. + + References + * [1] OpenQASM 3: A broader and deeper quantum assembly language + https://arxiv.org/abs/2104.14722 + """ + + def __init__( + self, + inst_map: InstructionScheduleMap, + ): + """Create new pass. + + Args: + inst_map: Instruction schedule map that user may override. + """ + super().__init__() + self.inst_map = inst_map + + def supported(self, node_op: CircuitInst, qubits: List) -> bool: + """Determine if a given node supports the calibration. + + Args: + node_op: Target instruction object. + qubits: Integer qubit indices to check. + + Returns: + Return ``True`` is calibration can be provided. + """ + return self.inst_map.has(instruction=node_op.name, qubits=qubits) + + def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: + """Gets the calibrated schedule for the given instruction and qubits. + + Args: + node_op: Target instruction object. + qubits: Integer qubit indices to check. + + Returns: + Return Schedule of target gate instruction. + """ + return self.inst_map.get(node_op.name, qubits, *node_op.params) diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py new file mode 100644 index 000000000000..6c4a6e4f2efd --- /dev/null +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -0,0 +1,387 @@ +# 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. + +"""RZX calibration builders.""" + +import math +import warnings +from typing import List, Tuple, Union + +import enum +import numpy as np +from qiskit.circuit import Instruction as CircuitInst +from qiskit.circuit.library.standard_gates import RZXGate +from qiskit.exceptions import QiskitError +from qiskit.pulse import ( + Play, + Delay, + Schedule, + ScheduleBlock, + ControlChannel, + DriveChannel, + GaussianSquare, + Waveform, +) +from qiskit.pulse.filters import filter_instructions +from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap, CalibrationPublisher + +from .base_builder import CalibrationBuilder +from .exceptions import CalibrationNotAvailable + + +class CXCalType(enum.Enum): + """Estimated calibration type of backend CX gate.""" + + ECR = "Echoed Cross Resonance" + DIRECT_CX = "Direct CX" + + +class RZXCalibrationBuilder(CalibrationBuilder): + """ + Creates calibrations for RZXGate(theta) by stretching and compressing + Gaussian square pulses in the CX gate. This is done by retrieving (for a given pair of + qubits) the CX schedule in the instruction schedule map of the backend defaults. + The CX schedule must be an echoed cross-resonance gate optionally with rotary tones. + The cross-resonance drive tones and rotary pulses must be Gaussian square pulses. + The width of the Gaussian square pulse is adjusted so as to match the desired rotation angle. + If the rotation angle is small such that the width disappears then the amplitude of the + zero width Gaussian square pulse (i.e. a Gaussian) is reduced to reach the target rotation + angle. Additional details can be found in https://arxiv.org/abs/2012.11660. + """ + + def __init__( + self, + instruction_schedule_map: InstructionScheduleMap = None, + qubit_channel_mapping: List[List[str]] = None, + verbose: bool = True, + ): + """ + Initializes a RZXGate calibration builder. + + Args: + instruction_schedule_map: The :obj:`InstructionScheduleMap` object representing the + default pulse calibrations for the target backend + qubit_channel_mapping: The list mapping qubit indices to the list of + channel names that apply on that qubit. + verbose: Set True to raise a user warning when RZX schedule cannot be built. + + Raises: + QiskitError: Instruction schedule map is not provided. + """ + super().__init__() + + if instruction_schedule_map is None: + raise QiskitError("Calibrations can only be added to Pulse-enabled backends") + + if qubit_channel_mapping: + warnings.warn( + "'qubit_channel_mapping' is no longer used. This value is ignored.", + DeprecationWarning, + ) + + self._inst_map = instruction_schedule_map + self._verbose = verbose + + def supported(self, node_op: CircuitInst, qubits: List) -> bool: + """Determine if a given node supports the calibration. + + Args: + node_op: Target instruction object. + qubits: Integer qubit indices to check. + + Returns: + Return ``True`` is calibration can be provided. + """ + return isinstance(node_op, RZXGate) and self._inst_map.has("cx", qubits) + + @staticmethod + def rescale_cr_inst(instruction: Play, theta: float, sample_mult: int = 16) -> Play: + """ + Args: + instruction: The instruction from which to create a new shortened or lengthened pulse. + theta: desired angle, pi/2 is assumed to be the angle that the pulse in the given + play instruction implements. + sample_mult: All pulses must be a multiple of sample_mult. + + Returns: + qiskit.pulse.Play: The play instruction with the stretched compressed + GaussianSquare pulse. + + Raises: + QiskitError: if rotation angle is not assigned. + """ + try: + theta = float(theta) + except TypeError as ex: + raise QiskitError("Target rotation angle is not assigned.") from ex + + # This method is called for instructions which are guaranteed to play GaussianSquare pulse + amp = instruction.pulse.amp + width = instruction.pulse.width + sigma = instruction.pulse.sigma + n_sigmas = (instruction.pulse.duration - width) / sigma + + # The error function is used because the Gaussian may have chopped tails. + gaussian_area = abs(amp) * sigma * np.sqrt(2 * np.pi) * math.erf(n_sigmas) + area = gaussian_area + abs(amp) * width + + target_area = abs(theta) / (np.pi / 2.0) * area + sign = np.sign(theta) + + if target_area > gaussian_area: + width = (target_area - gaussian_area) / abs(amp) + duration = round((width + n_sigmas * sigma) / sample_mult) * sample_mult + return Play( + GaussianSquare(amp=sign * amp, width=width, sigma=sigma, duration=duration), + channel=instruction.channel, + ) + else: + amp_scale = sign * target_area / gaussian_area + duration = round(n_sigmas * sigma / sample_mult) * sample_mult + return Play( + GaussianSquare(amp=amp * amp_scale, width=0, sigma=sigma, duration=duration), + channel=instruction.channel, + ) + + def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: + """Builds the calibration schedule for the RZXGate(theta) with echos. + + Args: + node_op: Instruction of the RZXGate(theta). I.e. params[0] is theta. + qubits: List of qubits for which to get the schedules. The first qubit is + the control and the second is the target. + + Returns: + schedule: The calibration schedule for the RZXGate(theta). + + Raises: + QiskitError: If the control and target qubits cannot be identified. + CalibrationNotAvailable: RZX schedule cannot be built for input node. + """ + theta = node_op.params[0] + + rzx_theta = Schedule(name="rzx(%.3f)" % theta) + rzx_theta.metadata["publisher"] = CalibrationPublisher.QISKIT + + if np.isclose(theta, 0.0): + return rzx_theta + + cx_sched = self._inst_map.get("cx", qubits=qubits) + cal_type, cr_tones, comp_tones = _check_calibration_type(cx_sched) + + if cal_type != CXCalType.ECR: + if self._verbose: + warnings.warn( + f"CX instruction for qubits {qubits} is likely {cal_type.value} sequence. " + "Pulse stretch for this calibration is not currently implemented. " + "RZX schedule is not generated for this qubit pair.", + UserWarning, + ) + raise CalibrationNotAvailable + + if len(comp_tones) == 0: + raise QiskitError( + f"{repr(cx_sched)} has no target compensation tones. " + "Native CR direction cannot be determined." + ) + + # Determine native direction, assuming only single drive channel per qubit. + # This guarantees channel and qubit index equality. + is_native = comp_tones[0].channel.index == qubits[1] + + stretched_cr_tones = list(map(lambda p: self.rescale_cr_inst(p, theta), cr_tones)) + stretched_comp_tones = list(map(lambda p: self.rescale_cr_inst(p, theta), comp_tones)) + + if is_native: + xgate = self._inst_map.get("x", qubits[0]) + + for cr, comp in zip(stretched_cr_tones, stretched_comp_tones): + current_dur = rzx_theta.duration + rzx_theta.insert(current_dur, cr, inplace=True) + rzx_theta.insert(current_dur, comp, inplace=True) + rzx_theta.append(xgate, inplace=True) + + else: + # Add hadamard gate to flip + xgate = self._inst_map.get("x", qubits[1]) + szc = self._inst_map.get("rz", qubits[1], np.pi / 2) + sxc = self._inst_map.get("sx", qubits[1]) + szt = self._inst_map.get("rz", qubits[0], np.pi / 2) + sxt = self._inst_map.get("sx", qubits[0]) + + # Hadamard to control + rzx_theta.insert(0, szc, inplace=True) + rzx_theta.insert(0, sxc, inplace=True) + rzx_theta.insert(sxc.duration, szc, inplace=True) + + # Hadamard to target + rzx_theta.insert(0, szt, inplace=True) + rzx_theta.insert(0, sxt, inplace=True) + rzx_theta.insert(sxt.duration, szt, inplace=True) + + for cr, comp in zip(stretched_cr_tones, stretched_comp_tones): + current_dur = rzx_theta.duration + rzx_theta.insert(current_dur, cr, inplace=True) + rzx_theta.insert(current_dur, comp, inplace=True) + rzx_theta.append(xgate, inplace=True) + + current_dur = rzx_theta.duration + + # Hadamard to control + rzx_theta.insert(current_dur, szc, inplace=True) + rzx_theta.insert(current_dur, sxc, inplace=True) + rzx_theta.insert(current_dur + sxc.duration, szc, inplace=True) + + # Hadamard to target + rzx_theta.insert(current_dur, szt, inplace=True) + rzx_theta.insert(current_dur, sxt, inplace=True) + rzx_theta.insert(current_dur + sxt.duration, szt, inplace=True) + + return rzx_theta + + +class RZXCalibrationBuilderNoEcho(RZXCalibrationBuilder): + """ + Creates calibrations for RZXGate(theta) by stretching and compressing + Gaussian square pulses in the CX gate. + + The ``RZXCalibrationBuilderNoEcho`` is a variation of the + :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` pass + that creates calibrations for the cross-resonance pulses without inserting + the echo pulses in the pulse schedule. This enables exposing the echo in + the cross-resonance sequence as gates so that the transpiler can simplify them. + The ``RZXCalibrationBuilderNoEcho`` only supports the hardware-native direction + of the CX gate. + """ + + def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: + """Builds the calibration schedule for the RZXGate(theta) without echos. + + Args: + node_op: Instruction of the RZXGate(theta). I.e. params[0] is theta. + qubits: List of qubits for which to get the schedules. The first qubit is + the control and the second is the target. + + Returns: + schedule: The calibration schedule for the RZXGate(theta). + + Raises: + QiskitError: If the control and target qubits cannot be identified, + or the backend does not natively support the specified direction of the cx. + CalibrationNotAvailable: RZX schedule cannot be built for input node. + """ + theta = node_op.params[0] + + rzx_theta = Schedule(name="rzx(%.3f)" % theta) + rzx_theta.metadata["publisher"] = CalibrationPublisher.QISKIT + + if np.isclose(theta, 0.0): + return rzx_theta + + cx_sched = self._inst_map.get("cx", qubits=qubits) + cal_type, cr_tones, comp_tones = _check_calibration_type(cx_sched) + + if cal_type != CXCalType.ECR: + if self._verbose: + warnings.warn( + f"CX instruction for qubits {qubits} is likely {cal_type.value} sequence. " + "Pulse stretch for this calibration is not currently implemented. " + "RZX schedule is not generated for this qubit pair.", + UserWarning, + ) + raise CalibrationNotAvailable + + if len(comp_tones) == 0: + raise QiskitError( + f"{repr(cx_sched)} has no target compensation tones. " + "Native CR direction cannot be determined." + ) + + # Determine native direction, assuming only single drive channel per qubit. + # This guarantees channel and qubit index equality. + is_native = comp_tones[0].channel.index == qubits[1] + + stretched_cr_tone = self.rescale_cr_inst(cr_tones[0], 2 * theta) + stretched_comp_tone = self.rescale_cr_inst(comp_tones[0], 2 * theta) + + if is_native: + # Placeholder to make pulse gate work + delay = Delay(stretched_cr_tone.duration, DriveChannel(qubits[0])) + + # This doesn't remove unwanted instruction such as ZI + # These terms are eliminated along with other gates around the pulse gate. + rzx_theta = rzx_theta.insert(0, stretched_cr_tone, inplace=True) + rzx_theta = rzx_theta.insert(0, stretched_comp_tone, inplace=True) + rzx_theta = rzx_theta.insert(0, delay, inplace=True) + + return rzx_theta + + raise QiskitError("RZXCalibrationBuilderNoEcho only supports hardware-native RZX gates.") + + +def _filter_cr_tone(time_inst_tup): + """A helper function to filter pulses on control channels.""" + valid_types = ["GaussianSquare"] + + _, inst = time_inst_tup + if isinstance(inst, Play) and isinstance(inst.channel, ControlChannel): + pulse = inst.pulse + if isinstance(pulse, Waveform) or pulse.pulse_type in valid_types: + return True + return False + + +def _filter_comp_tone(time_inst_tup): + """A helper function to filter pulses on drive channels.""" + valid_types = ["GaussianSquare"] + + _, inst = time_inst_tup + if isinstance(inst, Play) and isinstance(inst.channel, DriveChannel): + pulse = inst.pulse + if isinstance(pulse, Waveform) or pulse.pulse_type in valid_types: + return True + return False + + +def _check_calibration_type(cx_sched) -> Tuple[CXCalType, List[Play], List[Play]]: + """A helper function to check type of CR calibration. + + Args: + cx_sched: A target schedule to stretch. + + Returns: + Filtered instructions and most-likely type of calibration. + + Raises: + QiskitError: Unknown calibration type is detected. + """ + cr_tones = list( + map(lambda t: t[1], filter_instructions(cx_sched, [_filter_cr_tone]).instructions) + ) + comp_tones = list( + map(lambda t: t[1], filter_instructions(cx_sched, [_filter_comp_tone]).instructions) + ) + + if len(cr_tones) == 2 and len(comp_tones) in (0, 2): + # ECR can be implemented without compensation tone at price of lower fidelity. + # Remarkable noisy terms are usually eliminated by echo. + return CXCalType.ECR, cr_tones, comp_tones + + if len(cr_tones) == 1 and len(comp_tones) == 1: + # Direct CX must have compensation tone on target qubit. + # Otherwise, it cannot eliminate IX interaction. + return CXCalType.DIRECT_CX, cr_tones, comp_tones + + raise QiskitError( + f"{repr(cx_sched)} is undefined pulse sequence. " + "Check if this is a calibration for CX gate." + ) diff --git a/releasenotes/notes/upgrade_rzx_builder_skip_direct_cx-d0beff9b2b86ab8d.yaml b/releasenotes/notes/upgrade_rzx_builder_skip_direct_cx-d0beff9b2b86ab8d.yaml new file mode 100644 index 000000000000..a600f75f64a4 --- /dev/null +++ b/releasenotes/notes/upgrade_rzx_builder_skip_direct_cx-d0beff9b2b86ab8d.yaml @@ -0,0 +1,22 @@ +--- +upgrade: + - | + :class:`.RZXCalibrationBuilder` and :class:`.RZXCalibrationBuilderNoEcho` + have been upgraded to skip stretching CX gates implemented by + non-echoed cross resonance (ECR) sequence to avoid termination of the pass + with unexpected errors. + These passes take new argument ``verbose`` that controls warning. + If ``verbose=True`` is set, pass raises user warning when it enconters + non-ECR sequence. +deprecations: + - | + The unused argument ``qubit_channel_mapping`` in the + :class:`.RZXCalibrationBuilder` and :class:`.RZXCalibrationBuilderNoEcho` + transpiler passes have been deprecated and will be removed. + This argument is no longer used. +other: + - | + The transpiler pass module :mod:`~qiskit.transpiler.passes.calibration` has been reorganized. + :class:`.PulseGates` has been moved to :mod:`~qiskit.transpiler.passes.calibration.pulse_gates`, + and :class:`.RZXCalibrationBuilder` and :class:`.RZXCalibrationBuilderNoEcho` + have been moved to :mod:`~qiskit.transpiler.passes.calibration.rzx_builders`. diff --git a/test/python/pulse/test_calibrationbuilder.py b/test/python/pulse/test_calibrationbuilder.py index 67c0de561257..303365e01644 100644 --- a/test/python/pulse/test_calibrationbuilder.py +++ b/test/python/pulse/test_calibrationbuilder.py @@ -18,7 +18,17 @@ from ddt import data, ddt from qiskit import circuit, schedule -from qiskit.pulse import ControlChannel, Delay, DriveChannel, GaussianSquare, Play, ShiftPhase +from qiskit.pulse import ( + ControlChannel, + Delay, + DriveChannel, + GaussianSquare, + Waveform, + Play, + ShiftPhase, + InstructionScheduleMap, + Schedule, +) from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import FakeAthens from qiskit.transpiler import PassManager @@ -60,6 +70,30 @@ def test_rzx_calibration_builder_duration(self, theta: float): expected_duration = round((width + n_sigmas * sigma) / sample_mult) * sample_mult self.assertEqual(scaled.duration, expected_duration) + def test_pass_alive_with_dcx_ish(self): + """Test if the pass is not terminated by error with direct CX input.""" + cx_sched = Schedule() + # Fake direct cr + cx_sched.insert(0, Play(GaussianSquare(800, 0.2, 64, 544), ControlChannel(1)), inplace=True) + # Fake direct compensation tone + # Compensation tone doesn't have dedicated pulse class. + # So it's reported as a waveform now. + compensation_tone = Waveform(0.1 * np.ones(800, dtype=complex)) + cx_sched.insert(0, Play(compensation_tone, DriveChannel(0)), inplace=True) + + inst_map = InstructionScheduleMap() + inst_map.add("cx", (1, 0), schedule=cx_sched) + + theta = pi / 3 + rzx_qc = circuit.QuantumCircuit(2) + rzx_qc.rzx(theta, 1, 0) + + pass_ = RZXCalibrationBuilder(instruction_schedule_map=inst_map) + with self.assertWarns(UserWarning): + # User warning that says q0 q1 is invalid + cal_qc = PassManager(pass_).run(rzx_qc) + self.assertEqual(cal_qc, rzx_qc) + class TestRZXCalibrationBuilderNoEcho(TestCalibrationBuilder): """Test RZXCalibrationBuilderNoEcho.""" @@ -77,8 +111,7 @@ def test_rzx_calibration_builder(self): # apply the RZXCalibrationBuilderNoEcho. pass_ = RZXCalibrationBuilderNoEcho( - instruction_schedule_map=self.backend.defaults().instruction_schedule_map, - qubit_channel_mapping=self.backend.configuration().qubit_channel_mapping, + instruction_schedule_map=self.backend.defaults().instruction_schedule_map ) cal_qc = PassManager(pass_).run(rzx_qc) rzx_qc_duration = schedule(cal_qc, self.backend).duration @@ -138,3 +171,27 @@ def test_pulse_amp_typecasted(self): scaled_pulse = scaled.pulse self.assertIsInstance(scaled_pulse.amp, complex) + + def test_pass_alive_with_dcx_ish(self): + """Test if the pass is not terminated by error with direct CX input.""" + cx_sched = Schedule() + # Fake direct cr + cx_sched.insert(0, Play(GaussianSquare(800, 0.2, 64, 544), ControlChannel(1)), inplace=True) + # Fake direct compensation tone + # Compensation tone doesn't have dedicated pulse class. + # So it's reported as a waveform now. + compensation_tone = Waveform(0.1 * np.ones(800, dtype=complex)) + cx_sched.insert(0, Play(compensation_tone, DriveChannel(0)), inplace=True) + + inst_map = InstructionScheduleMap() + inst_map.add("cx", (1, 0), schedule=cx_sched) + + theta = pi / 3 + rzx_qc = circuit.QuantumCircuit(2) + rzx_qc.rzx(theta, 1, 0) + + pass_ = RZXCalibrationBuilderNoEcho(instruction_schedule_map=inst_map) + with self.assertWarns(UserWarning): + # User warning that says q0 q1 is invalid + cal_qc = PassManager(pass_).run(rzx_qc) + self.assertEqual(cal_qc, rzx_qc)