diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 269d01c40574..04127e1208f4 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -25,7 +25,7 @@ from qiskit.providers.backend import Backend from qiskit.providers.models import BackendProperties from qiskit.providers.models.backendproperties import Gate -from qiskit.pulse import Schedule +from qiskit.pulse import Schedule, InstructionScheduleMap from qiskit.tools.parallel import parallel_map from qiskit.transpiler import Layout, CouplingMap, PropertySet, PassManager from qiskit.transpiler.basepasses import BasePass @@ -33,13 +33,13 @@ from qiskit.transpiler.instruction_durations import InstructionDurations, InstructionDurationsType from qiskit.transpiler.passes import ApplyLayout from qiskit.transpiler.passmanager_config import PassManagerConfig -from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.preset_passmanagers import ( level_0_pass_manager, level_1_pass_manager, level_2_pass_manager, level_3_pass_manager, ) +from qiskit.transpiler.timing_constraints import TimingConstraints logger = logging.getLogger(__name__) @@ -48,6 +48,7 @@ def transpile( circuits: Union[QuantumCircuit, List[QuantumCircuit]], backend: Optional[Union[Backend, BaseBackend]] = None, basis_gates: Optional[List[str]] = None, + inst_map: Optional[List[InstructionScheduleMap]] = None, coupling_map: Optional[Union[CouplingMap, List[List[int]]]] = None, backend_properties: Optional[BackendProperties] = None, initial_layout: Optional[Union[Layout, Dict, List]] = None, @@ -85,6 +86,12 @@ def transpile( circuit may be run on any backend as long as it is compatible. basis_gates: List of basis gate names to unroll to (e.g: ``['u1', 'u2', 'u3', 'cx']``). If ``None``, do not unroll. + inst_map: Mapping of unrolled gates to pulse schedules. If this is not provided, + transpiler tries to get from the backend. If any user defined calibration + is found in the map and this is used in a circuit, transpiler attaches + the custom gate definition to the circuit. This enables one to flexibly + override the low-level instruction implementation. This feature is available + iff the backend supports the pulse gate experiment. coupling_map: Coupling map (perhaps custom) to target in mapping. Multiple formats are supported: @@ -270,6 +277,7 @@ def callback_func(**kwargs): circuits, backend, basis_gates, + inst_map, coupling_map, backend_properties, initial_layout, @@ -451,6 +459,7 @@ def _parse_transpile_args( circuits, backend, basis_gates, + inst_map, coupling_map, backend_properties, initial_layout, @@ -488,6 +497,7 @@ def _parse_transpile_args( num_circuits = len(circuits) basis_gates = _parse_basis_gates(basis_gates, backend, circuits) + inst_map = _parse_inst_map(inst_map, backend, num_circuits) faulty_qubits_map = _parse_faulty_qubits_map(backend, num_circuits) coupling_map = _parse_coupling_map(coupling_map, backend, num_circuits) backend_properties = _parse_backend_properties(backend_properties, backend, num_circuits) @@ -514,6 +524,7 @@ def _parse_transpile_args( for kwargs in _zip_dict( { "basis_gates": basis_gates, + "inst_map": inst_map, "coupling_map": coupling_map, "backend_properties": backend_properties, "initial_layout": initial_layout, @@ -535,6 +546,7 @@ def _parse_transpile_args( transpile_args = { "pass_manager_config": PassManagerConfig( basis_gates=kwargs["basis_gates"], + inst_map=kwargs["inst_map"], coupling_map=kwargs["coupling_map"], backend_properties=kwargs["backend_properties"], initial_layout=kwargs["initial_layout"], @@ -605,6 +617,19 @@ def _parse_basis_gates(basis_gates, backend, circuits): return basis_gates +def _parse_inst_map(inst_map, backend, num_circuits): + # try getting inst_map from user, else backend + if inst_map is None: + if hasattr(backend, "defaults"): + inst_map = getattr(backend.defaults(), "instruction_schedule_map", None) + + # inst_maps could be None, or single entry + if inst_map is None or isinstance(inst_map, InstructionScheduleMap): + inst_map = [inst_map] * num_circuits + + return inst_map + + def _parse_coupling_map(coupling_map, backend, num_circuits): # try getting coupling_map from user, else backend if coupling_map is None: diff --git a/qiskit/providers/models/pulsedefaults.py b/qiskit/providers/models/pulsedefaults.py index 4940b28fcb48..e5d73f80ccfa 100644 --- a/qiskit/providers/models/pulsedefaults.py +++ b/qiskit/providers/models/pulsedefaults.py @@ -15,7 +15,7 @@ import copy from typing import Any, Dict, List -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap +from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap, CalibrationPublisher from qiskit.pulse.schedule import Schedule from qiskit.qobj import PulseLibraryItem, PulseQobjInstruction from qiskit.qobj.converters import QobjToInstructionConverter @@ -205,6 +205,7 @@ def __init__( for inst in cmd_def: pulse_insts = [self.converter(inst) for inst in inst.sequence] schedule = Schedule(*pulse_insts, name=inst.name) + schedule.metadata["publisher"] = CalibrationPublisher.BACKEND_PROVIDER self.instruction_schedule_map.add(inst.name, inst.qubits, schedule) if meas_kernel is not None: diff --git a/qiskit/pulse/instruction_schedule_map.py b/qiskit/pulse/instruction_schedule_map.py index 68f168c9d50a..0044493bd25f 100644 --- a/qiskit/pulse/instruction_schedule_map.py +++ b/qiskit/pulse/instruction_schedule_map.py @@ -30,6 +30,7 @@ import functools import warnings from collections import defaultdict +from enum import IntEnum from typing import Callable, Iterable, List, Tuple, Union, Optional, NamedTuple from qiskit.circuit.instruction import Instruction @@ -43,6 +44,14 @@ ) +class CalibrationPublisher(IntEnum): + """Defines who defined schedule entry.""" + + BACKEND_PROVIDER = 0 + QISKIT = 1 + EXPERIMENT_SERVICE = 2 + + class InstructionScheduleMap: """Mapping from :py:class:`~qiskit.circuit.QuantumCircuit` :py:class:`qiskit.circuit.Instruction` names and qubits to @@ -69,6 +78,16 @@ def __init__(self): # A backwards mapping from qubit to supported instructions self._qubit_instructions = defaultdict(set) + def has_custom_gate(self) -> bool: + """Return ``True`` if the map has user provided instruction.""" + for qubit_inst in self._map.values(): + for generator in qubit_inst.values(): + metadata = getattr(generator.function, "metadata", {}) + publisher = metadata.get("publisher", CalibrationPublisher.QISKIT) + if publisher != CalibrationPublisher.BACKEND_PROVIDER: + return True + return False + @property def instructions(self) -> List[str]: """Return all instructions which have definitions. @@ -303,6 +322,10 @@ def add( "callable that outputs a schedule." ) + # add metadata + if hasattr(schedule, "metadata") and "publisher" not in schedule.metadata: + schedule.metadata["publisher"] = CalibrationPublisher.QISKIT + self._map[instruction][qubits] = Generator(schedule, signature) self._qubit_instructions[qubits].add(instruction) diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 0374c8251fa2..a5477816f554 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -76,6 +76,16 @@ CrosstalkAdaptiveSchedule TemplateOptimization +Calibration +============= + +.. autosummary:: + :toctree: ../stubs/ + + PulseGates + RZXCalibrationBuilder + RZXCalibrationBuilderNoEcho + Scheduling ============= @@ -88,8 +98,6 @@ DynamicalDecoupling AlignMeasures ValidatePulseGates - RZXCalibrationBuilder - RZXCalibrationBuilderNoEcho Circuit Analysis ================ @@ -188,12 +196,15 @@ # synthesis from .synthesis import UnitarySynthesis +# calibration +from .calibration import PulseGates +from .calibration import RZXCalibrationBuilder +from .calibration import RZXCalibrationBuilderNoEcho + # circuit scheduling from .scheduling import TimeUnitConversion from .scheduling import ALAPSchedule from .scheduling import ASAPSchedule -from .scheduling import RZXCalibrationBuilder -from .scheduling import RZXCalibrationBuilderNoEcho from .scheduling import DynamicalDecoupling from .scheduling import AlignMeasures from .scheduling import ValidatePulseGates diff --git a/qiskit/transpiler/passes/calibration/__init__.py b/qiskit/transpiler/passes/calibration/__init__.py new file mode 100644 index 000000000000..3b80fbb26530 --- /dev/null +++ b/qiskit/transpiler/passes/calibration/__init__.py @@ -0,0 +1,15 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021 +# +# 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. + +"""Module containing transpiler calibration passes.""" + +from .builders import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho, PulseGates diff --git a/qiskit/transpiler/passes/calibration/builders.py b/qiskit/transpiler/passes/calibration/builders.py new file mode 100644 index 000000000000..ee5355126538 --- /dev/null +++ b/qiskit/transpiler/passes/calibration/builders.py @@ -0,0 +1,471 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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 creators.""" + +from abc import abstractmethod +from typing import List, Union + +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.providers import basebackend +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. + """ + for node in dag.gate_nodes(): + qubits = list(dag.qubits.index(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, backend: basebackend): + """ + Initializes a RZXGate calibration builder. + + Args: + backend: Backend for which to construct the gates. + + Raises: + QiskitError: if open pulse is not supported by the backend. + """ + super().__init__() + if not backend.configuration().open_pulse: + raise QiskitError( + "Calibrations can only be added to Pulse-enabled backends, " + "but {} is not enabled with Pulse.".format(backend.name()) + ) + + self._inst_map = backend.defaults().instruction_schedule_map + self._config = backend.configuration() + self._channel_map = backend.configuration().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. + """ + 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 = theta / abs(theta) + + if target_area > gaussian_area: + width = (target_area - gaussian_area) / abs(amp) + duration = math.ceil((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 = math.ceil(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) diff --git a/qiskit/transpiler/passes/calibration/rzx_templates.py b/qiskit/transpiler/passes/calibration/rzx_templates.py new file mode 100644 index 000000000000..e6995122a68b --- /dev/null +++ b/qiskit/transpiler/passes/calibration/rzx_templates.py @@ -0,0 +1,51 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +""" +Convenience function to load RZXGate based templates. +""" + +from enum import Enum +from typing import List, Dict + +from qiskit.circuit.library.templates import rzx + + +class RZXTemplateMap(Enum): + """Mapping of instruction name to decomposition template.""" + + ZZ1 = rzx.rzx_zz1() + ZZ2 = rzx.rzx_zz2() + ZZ3 = rzx.rzx_zz3() + YZ = rzx.rzx_yz() + XZ = rzx.rzx_xz() + CY = rzx.rzx_cy() + + +def rzx_templates(template_list: List[str] = None) -> Dict: + """Convenience function to get the cost_dict and templates for template matching. + + Args: + template_list: List of instruction names. + + Returns: + Decomposition templates and cost values. + """ + if template_list is None: + template_list = ["zz1", "zz2", "zz3", "yz", "xz", "cy"] + + templates = list(map(lambda gate: RZXTemplateMap[gate.upper()].value, template_list)) + cost_dict = {"rzx": 0, "cx": 6, "rz": 0, "sx": 1, "p": 0, "h": 1, "rx": 1, "ry": 1} + + rzx_dict = {"template_list": templates, "user_cost_dict": cost_dict} + + return rzx_dict diff --git a/qiskit/transpiler/passes/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/__init__.py index fd007001b100..f8270d23da54 100644 --- a/qiskit/transpiler/passes/scheduling/__init__.py +++ b/qiskit/transpiler/passes/scheduling/__init__.py @@ -15,11 +15,5 @@ from .alap import ALAPSchedule from .asap import ASAPSchedule from .time_unit_conversion import TimeUnitConversion -from .calibration_creators import ( - CalibrationCreator, - RZXCalibrationBuilder, - RZXCalibrationBuilderNoEcho, -) from .dynamical_decoupling import DynamicalDecoupling -from .rzx_templates import rzx_templates from .instruction_alignment import AlignMeasures, ValidatePulseGates diff --git a/qiskit/transpiler/passes/scheduling/calibration_creators.py b/qiskit/transpiler/passes/scheduling/calibration_creators.py index 7b3cceb0fea7..d9c2762b45c2 100644 --- a/qiskit/transpiler/passes/scheduling/calibration_creators.py +++ b/qiskit/transpiler/passes/scheduling/calibration_creators.py @@ -10,377 +10,18 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Calibration creators.""" - -import math -from typing import List, Union -from abc import abstractmethod -import numpy as np - -from qiskit.pulse import ( - Play, - Delay, - ShiftPhase, - Schedule, - ControlChannel, - DriveChannel, - GaussianSquare, -) -from qiskit.pulse.instructions.instruction import Instruction -from qiskit.exceptions import QiskitError -from qiskit.providers import basebackend -from qiskit.dagcircuit import DAGOpNode -from qiskit.circuit.library.standard_gates import RZXGate -from qiskit.transpiler.basepasses import TransformationPass - - -class CalibrationCreator(TransformationPass): - """Abstract base class to inject calibrations into circuits.""" - - @abstractmethod - def supported(self, node_op: DAGOpNode) -> bool: - """Determine if a given name supports the calibration.""" - - @abstractmethod - def get_calibration(self, params: List, qubits: List) -> Schedule: - """Gets the calibrated schedule for the given qubits and parameters.""" - - def run(self, dag): - """Run the calibration adder pass on `dag`. - - Args: - dag (DAGCircuit): DAG to schedule. - - Returns: - DAGCircuit: A DAG with calibrations added to it. - """ - bit_indices = {bit: index for index, bit in enumerate(dag.qubits)} - - for node in dag.nodes(): - if isinstance(node, DAGOpNode): - if self.supported(node.op): - params = node.op.params - qubits = [bit_indices[qarg] for qarg in node.qargs] - - schedule = self.get_calibration(params, qubits) - - dag.add_calibration(node.op, qubits, schedule, params=params) - - return dag - - -class RZXCalibrationBuilder(CalibrationCreator): - """ - 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, backend: basebackend): - """ - Initializes a RZXGate calibration builder. - - Args: - backend: Backend for which to construct the gates. - - Raises: - QiskitError: if open pulse is not supported by the backend. - """ - super().__init__() - if not backend.configuration().open_pulse: - raise QiskitError( - "Calibrations can only be added to Pulse-enabled backends, " - "but {} is not enabled with Pulse.".format(backend.name()) - ) - - self._inst_map = backend.defaults().instruction_schedule_map - self._config = backend.configuration() - self._channel_map = backend.configuration().qubit_channel_mapping - - def supported(self, node_op: DAGOpNode) -> bool: - """ - Args: - node_op: The node from the dag dep. - - Returns: - match: True if the node is a RZXGate. - """ - 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. - """ - 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 = theta / abs(theta) - - if target_area > gaussian_area: - width = (target_area - gaussian_area) / abs(amp) - duration = math.ceil((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 = math.ceil(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, params: List, qubits: List) -> Schedule: - """ - Args: - params: Parameters 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 = 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) - - if theta == 0.0: - return rzx_theta - - crs, comp_tones = [], [] - control, target = None, None - - for time, inst in cx_sched.instructions: +# pylint: disable=unused-import - # 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", Instruction])) -> 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", Instruction])) -> 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, params: List, qubits: List) -> Schedule: - """ - Builds the calibration schedule for the RZXGate(theta) without echos. - - Args: - params: Parameters 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 = 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) - - 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 +"""Calibration creators.""" - # 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) +import warnings +from qiskit.transpiler.passes.calibration import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho - # 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))) +# TODO remove this import after sufficient deprecation period - return rzx_theta +warnings.warn( + "RZXCalibrationBuilder and RZXCalibrationBuilderNoEcho passes are moved to " + "`qiskit.transpiler.passes.calibration.builders`. " + "This import path is being deprecated.", + DeprecationWarning, +) diff --git a/qiskit/transpiler/passes/scheduling/rzx_templates.py b/qiskit/transpiler/passes/scheduling/rzx_templates.py index 54a095d066df..540d998b3b91 100644 --- a/qiskit/transpiler/passes/scheduling/rzx_templates.py +++ b/qiskit/transpiler/passes/scheduling/rzx_templates.py @@ -10,40 +10,19 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=unused-import + """ Convenience function to load RZXGate based templates. """ -from typing import List - -from qiskit.circuit.library.templates.rzx import rzx_zz1, rzx_zz2, rzx_zz3, rzx_yz, rzx_xz, rzx_cy - - -def rzx_templates(template_list: List[str] = None): - """ - Convenience function to get the cost_dict and - templates for template matching. - """ - - if template_list is None: - template_list = ["zz1", "zz2", "zz3", "yz", "xz", "cy"] - - templates = [] - if "zz1" in template_list: - templates.append(rzx_zz1()) - if "zz2" in template_list: - templates.append(rzx_zz2()) - if "zz3" in template_list: - templates.append(rzx_zz3()) - if "yz" in template_list: - templates.append(rzx_yz()) - if "xz" in template_list: - templates.append(rzx_xz()) - if "cy" in template_list: - templates.append(rzx_cy()) - - cost_dict = {"rzx": 0, "cx": 6, "rz": 0, "sx": 1, "p": 0, "h": 1, "rx": 1, "ry": 1} +import warnings +from qiskit.transpiler.passes.calibration.rzx_templates import rzx_templates - rzx_dict = {"template_list": templates, "user_cost_dict": cost_dict} +# TODO remove this import after sufficient deprecation period - return rzx_dict +warnings.warn( + "rzx_templates function is moved to `qiskit.transpiler.passes.calibration.rzx_templates`. " + "This import path is being deprecated.", + DeprecationWarning, +) diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 124a303bd08d..4705005cb9f6 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -20,6 +20,7 @@ def __init__( self, initial_layout=None, basis_gates=None, + inst_map=None, coupling_map=None, layout_method=None, routing_method=None, @@ -37,6 +38,7 @@ def __init__( initial_layout (Layout): Initial position of virtual qubits on physical qubits. basis_gates (list): List of basis gate names to unroll to. + inst_map (InstructionScheduleMap): Mapping object that maps gate to schedule. coupling_map (CouplingMap): Directed graph represented a coupling map. layout_method (str): the pass to use for choosing initial qubit @@ -59,6 +61,7 @@ def __init__( """ self.initial_layout = initial_layout self.basis_gates = basis_gates + self.inst_map = inst_map self.coupling_map = coupling_map self.layout_method = layout_method self.routing_method = routing_method diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index b2d76a88a8c2..925b11c8f9fb 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -47,6 +47,7 @@ from qiskit.transpiler.passes import ASAPSchedule from qiskit.transpiler.passes import AlignMeasures from qiskit.transpiler.passes import ValidatePulseGates +from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import Error from qiskit.transpiler import TranspilerError @@ -76,6 +77,7 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: TranspilerError: if the passmanager config is invalid. """ basis_gates = pass_manager_config.basis_gates + inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout layout_method = pass_manager_config.layout_method or "trivial" @@ -195,6 +197,8 @@ def _direction_condition(property_set): pm0.append(_direction_check) pm0.append(_direction, condition=_direction_condition) pm0.append(_unroll) + if inst_map and inst_map.has_custom_gate(): + pm0.append(PulseGates(inst_map=inst_map)) pm0.append(_scheduling) pm0.append(_alignments) return pm0 diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 5785af34ee48..6e5188b5b8a3 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -53,6 +53,7 @@ from qiskit.transpiler.passes import ASAPSchedule from qiskit.transpiler.passes import AlignMeasures from qiskit.transpiler.passes import ValidatePulseGates +from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import Error from qiskit.transpiler import TranspilerError @@ -84,6 +85,7 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: TranspilerError: if the passmanager config is invalid. """ basis_gates = pass_manager_config.basis_gates + inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout layout_method = pass_manager_config.layout_method or "dense" @@ -233,6 +235,8 @@ def _opt_control(property_set): pm1.append(_direction, condition=_direction_condition) pm1.append(_reset) pm1.append(_depth_check + _opt + _unroll, do_while=_opt_control) + if inst_map and inst_map.has_custom_gate(): + pm1.append(PulseGates(inst_map=inst_map)) pm1.append(_scheduling) pm1.append(_alignments) diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 671e62245a7f..0f4c0d3cae6c 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -55,6 +55,7 @@ from qiskit.transpiler.passes import ASAPSchedule from qiskit.transpiler.passes import AlignMeasures from qiskit.transpiler.passes import ValidatePulseGates +from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import Error from qiskit.transpiler import TranspilerError @@ -88,6 +89,7 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: TranspilerError: if the passmanager config is invalid. """ basis_gates = pass_manager_config.basis_gates + inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout layout_method = pass_manager_config.layout_method or "dense" @@ -271,6 +273,8 @@ def _opt_control(property_set): pm2.append(_direction, condition=_direction_condition) pm2.append(_reset) pm2.append(_depth_check + _opt + _unroll, do_while=_opt_control) + if inst_map and inst_map.has_custom_gate(): + pm2.append(PulseGates(inst_map=inst_map)) pm2.append(_scheduling) pm2.append(_alignments) return pm2 diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 347a2f2f762a..10ce23f9031f 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -58,6 +58,7 @@ from qiskit.transpiler.passes import ASAPSchedule from qiskit.transpiler.passes import AlignMeasures from qiskit.transpiler.passes import ValidatePulseGates +from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import Error from qiskit.transpiler import TranspilerError @@ -91,6 +92,7 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: TranspilerError: if the passmanager config is invalid. """ basis_gates = pass_manager_config.basis_gates + inst_map = pass_manager_config.inst_map coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout layout_method = pass_manager_config.layout_method or "dense" @@ -285,6 +287,8 @@ def _opt_control(property_set): pm3.append(_direction, condition=_direction_condition) pm3.append(_reset) pm3.append(_depth_check + _opt + _unroll, do_while=_opt_control) + if inst_map and inst_map.has_custom_gate(): + pm3.append(PulseGates(inst_map=inst_map)) pm3.append(_scheduling) pm3.append(_alignments) diff --git a/releasenotes/notes/add-pulse-gate-pass-dc347177ed541bcc.yaml b/releasenotes/notes/add-pulse-gate-pass-dc347177ed541bcc.yaml new file mode 100644 index 000000000000..fa5f284b67c0 --- /dev/null +++ b/releasenotes/notes/add-pulse-gate-pass-dc347177ed541bcc.yaml @@ -0,0 +1,94 @@ +--- +features: + - | + A new transpiler pass group :mod:`~qiskit.transpiler.passes.calibration` has been added. + This group consists of + + - :class:`~qiskit.transpiler.passes.calibration.creators.RZXCalibrationBuilder` + - :class:`~qiskit.transpiler.passes.calibration.creators.RZXCalibrationBuilderNoEcho` + - :func:`~qiskit.transpiler.passes.calibration.rzx_templates.rzx_templates`. + - :class:`~qiskit.transpiler.passes.calibration.creators.PulseGates` + + ``RZXCalibrationBuilder``, ``RZXCalibrationBuilderNoEcho``, and ``rzx_templates`` are + moved from :mod:`qiskit/transpiler/passes/scheduling`, whereas ``PulseGates`` + is the new pass that automatically extracts user-provided calibrations + from the instruction schedule map and attaches the gate schedule to the + given (transpiled) quantum circuit as a pulse gate. + Any transpiler pass that interacts with the pulse gate feature will be grouped to + :mod:`qiskit.transpiler.passes.calibration`. + + This pass is applied to all optimization levels from 0 to 3. + No gate implementation is updated unless the end-user explicitly overrides + the ``backend.defaults().instruction_schedule_map``. + + This pass saves users from individually calling :meth:`~qiskit.circuit.quantumcircuit.\ + QuantumCircuit#add_calibration` method for every circuit run on the hardware. + + Along with this change, a schedule was added to :class:`qiskit.pulse.instruction_schedule_map.\ + InstructionScheduleMap` and is implicitly updated with a metadata "publisher". + Backend calibrated gate schedules have a special publisher kind to avoid overriding + circuits with calibrations of already known schedules. + Usually, end-users don't need to take care of this metadata as it is applied automatically. + You can call + :py:meth:`~qiskit.pulse.instruction_schedule_map.InstructionScheduleMap#has_custom_gate` + to check if the map has custom gate calibration. + + See below code example to learn how to apply custom gate implementation + for all circuits under execution. + + .. code-block:: python + + from qiskit.test.mock import FakeGuadalupe + from qiskit import pulse, circuit, transpile + + backend = FakeGuadalupe() + + with pulse.build(backend, name="x") as x_q0: + pulse.play(pulse.Constant(160, 0.1), pulse.drive_channel(0)) + + backend.defaults().instruction_schedule_map.add("x", (0,), x_q0) + + circs = [] + for _ in range(100): + circ = circuit.QuantumCircuit(1) + circ.sx(0) + circ.rz(1.57, 0) + circ.x(0) + circ.measure_active() + circs.append(circ) + + circs = transpile(circs, backend) + circs[0].calibrations # This returns calibration only for x gate + + Note that the instruction schedule map is a mutable object. + If you override one of the entries and use that backend for other experiments, + you may accidentally update the gate definition. + + .. code-block:: python + + backend = FakeGuadalupe() + + instmap = backend.defaults().instruction_schedule_map + instmap.add("x", (0, ), my_x_gate_schedule) + + qc = QuantumCircuit(1, 1) + qc.x(0) + qc.measure(0, 0) + + qc = transpile(qc, backend) # This backend uses custom X gate + + If you want to update the gate definitions of a specific experiment, + you need to first deepcopy the instruction schedule map + and directly pass it to the transpiler. + + +deprecations: + - | + Transpiler pass reorganization regarding calibrations. + The import path for :class:`~qiskit.transpiler.passes.scheduling.calibration_creators.\ + RZXCalibrationBuilder` and :class:`~qiskit.transpiler.passes.scheduling.\ + calibration_creators.RZXCalibrationBuilderNoEcho` are deprecated. + The import path for :func:`qiskit.transpiler.passes.scheduling.rzx_templates.\ + rzx_templates.rzx_templates` is also deprecated. + + Use new import path under :mod:`~qiskit.transpiler.passes.calibration`. diff --git a/test/python/pulse/test_calibrationbuilder.py b/test/python/pulse/test_calibrationbuilder.py index 0579d8c1d10a..62097dee1bf3 100644 --- a/test/python/pulse/test_calibrationbuilder.py +++ b/test/python/pulse/test_calibrationbuilder.py @@ -27,7 +27,7 @@ DriveChannel, GaussianSquare, ) -from qiskit.transpiler.passes.scheduling.calibration_creators import ( +from qiskit.transpiler.passes.calibration.builders import ( RZXCalibrationBuilderNoEcho, ) from qiskit.test.mock import FakeAthens diff --git a/test/python/pulse/test_instruction_schedule_map.py b/test/python/pulse/test_instruction_schedule_map.py index e0a24cf0f7ba..5393d7762a8e 100644 --- a/test/python/pulse/test_instruction_schedule_map.py +++ b/test/python/pulse/test_instruction_schedule_map.py @@ -30,6 +30,7 @@ ShiftPhase, Constant, ) +from qiskit.pulse.instruction_schedule_map import CalibrationPublisher from qiskit.pulse.channels import DriveChannel from qiskit.qobj import PulseQobjInstruction from qiskit.qobj.converters import QobjToInstructionConverter @@ -549,3 +550,35 @@ def test_instmap_picklable(self): deser_instmap = pickle.loads(ser_obj) self.assertEqual(instmap, deser_instmap) + + def test_check_backend_provider_cals(self): + """Test if schedules provided by backend provider is distinguishable.""" + instmap = FakeOpenPulse2Q().defaults().instruction_schedule_map + publisher = instmap.get("u1", (0,), P0=0).metadata["publisher"] + + self.assertEqual(publisher, CalibrationPublisher.BACKEND_PROVIDER) + + def test_check_user_cals(self): + """Test if schedules provided by user is distinguishable.""" + instmap = FakeOpenPulse2Q().defaults().instruction_schedule_map + + test_u1 = Schedule() + test_u1 += ShiftPhase(Parameter("P0"), DriveChannel(0)) + + instmap.add("u1", (0,), test_u1, arguments=["P0"]) + publisher = instmap.get("u1", (0,), P0=0).metadata["publisher"] + + self.assertEqual(publisher, CalibrationPublisher.QISKIT) + + def test_has_custom_gate(self): + """Test method to check custom gate.""" + backend = FakeOpenPulse2Q() + instmap = backend.defaults().instruction_schedule_map + + self.assertFalse(instmap.has_custom_gate()) + + # add something + some_sched = Schedule() + instmap.add("u3", (0,), some_sched) + + self.assertTrue(instmap.has_custom_gate()) diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py new file mode 100644 index 000000000000..e0e96e2a4449 --- /dev/null +++ b/test/python/transpiler/test_pulse_gate_pass.py @@ -0,0 +1,203 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Transpiler pulse gate pass testing.""" + +from qiskit import pulse, circuit, transpile +from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeAthens + + +class TestPulseGate(QiskitTestCase): + """Integration test of pulse gate pass with custom backend.""" + + def setUp(self): + super().setUp() + + with pulse.build(name="sx_q0") as custom_sx_q0: + pulse.play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)) + + self.custom_sx_q0 = custom_sx_q0 + + with pulse.build(name="sx_q1") as custom_sx_q1: + pulse.play(pulse.Constant(100, 0.2), pulse.DriveChannel(1)) + + self.custom_sx_q1 = custom_sx_q1 + + self.sched_param = circuit.Parameter("P0") + + with pulse.build(name="my_gate_q0") as my_gate_q0: + pulse.shift_phase(self.sched_param, pulse.DriveChannel(0)) + pulse.play(pulse.Constant(120, 0.1), pulse.DriveChannel(0)) + + self.my_gate_q0 = my_gate_q0 + + with pulse.build(name="my_gate_q1") as my_gate_q1: + pulse.shift_phase(self.sched_param, pulse.DriveChannel(1)) + pulse.play(pulse.Constant(120, 0.2), pulse.DriveChannel(1)) + + self.my_gate_q1 = my_gate_q1 + + def test_transpile_with_bare_backend(self): + """Test transpile without custom calibrations.""" + backend = FakeAthens() + + qc = circuit.QuantumCircuit(2) + qc.sx(0) + qc.x(0) + qc.rz(0, 0) + qc.sx(1) + qc.measure_all() + + transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) + + ref_calibration = {} + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + + def test_transpile_with_custom_basis_gate(self): + """Test transpile with custom calibrations.""" + backend = FakeAthens() + backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) + backend.defaults().instruction_schedule_map.add("sx", (1,), self.custom_sx_q1) + + qc = circuit.QuantumCircuit(2) + qc.sx(0) + qc.x(0) + qc.rz(0, 0) + qc.sx(1) + qc.measure_all() + + transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) + + ref_calibration = { + "sx": { + ((0,), ()): self.custom_sx_q0, + ((1,), ()): self.custom_sx_q1, + } + } + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + + def test_transpile_with_instmap(self): + """Test providing instruction schedule map.""" + instmap = FakeAthens().defaults().instruction_schedule_map + instmap.add("sx", (0,), self.custom_sx_q0) + instmap.add("sx", (1,), self.custom_sx_q1) + + # Inst map is renewed + backend = FakeAthens() + + qc = circuit.QuantumCircuit(2) + qc.sx(0) + qc.x(0) + qc.rz(0, 0) + qc.sx(1) + qc.measure_all() + + transpiled_qc = transpile(qc, backend, inst_map=instmap, initial_layout=[0, 1]) + + ref_calibration = { + "sx": { + ((0,), ()): self.custom_sx_q0, + ((1,), ()): self.custom_sx_q1, + } + } + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + + def test_transpile_with_custom_gate(self): + """Test providing non-basis gate.""" + backend = FakeAthens() + backend.defaults().instruction_schedule_map.add( + "my_gate", (0,), self.my_gate_q0, arguments=["P0"] + ) + backend.defaults().instruction_schedule_map.add( + "my_gate", (1,), self.my_gate_q1, arguments=["P0"] + ) + + qc = circuit.QuantumCircuit(2) + qc.append(circuit.Gate("my_gate", 1, [1.0]), [0]) + qc.append(circuit.Gate("my_gate", 1, [2.0]), [1]) + + transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0, 1]) + + my_gate_q0_1_0 = self.my_gate_q0.assign_parameters({self.sched_param: 1.0}, inplace=False) + my_gate_q1_2_0 = self.my_gate_q1.assign_parameters({self.sched_param: 2.0}, inplace=False) + + ref_calibration = { + "my_gate": { + ((0,), (1.0,)): my_gate_q0_1_0, + ((1,), (2.0,)): my_gate_q1_2_0, + } + } + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + + def test_transpile_with_multiple_circuits(self): + """Test transpile with multiple circuits with custom gate.""" + backend = FakeAthens() + backend.defaults().instruction_schedule_map.add( + "my_gate", (0,), self.my_gate_q0, arguments=["P0"] + ) + + params = [0.0, 1.0, 2.0, 3.0] + circs = [] + for param in params: + qc = circuit.QuantumCircuit(1) + qc.append(circuit.Gate("my_gate", 1, [param]), [0]) + circs.append(qc) + + transpiled_qcs = transpile(circs, backend, basis_gates=["my_gate"], initial_layout=[0]) + + for param, transpiled_qc in zip(params, transpiled_qcs): + my_gate_q0_x = self.my_gate_q0.assign_parameters( + {self.sched_param: param}, inplace=False + ) + ref_calibration = {"my_gate": {((0,), (param,)): my_gate_q0_x}} + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + + def test_multiple_instructions_with_different_parameters(self): + """Test adding many instruction with different parameter binding.""" + backend = FakeAthens() + backend.defaults().instruction_schedule_map.add( + "my_gate", (0,), self.my_gate_q0, arguments=["P0"] + ) + + qc = circuit.QuantumCircuit(1) + qc.append(circuit.Gate("my_gate", 1, [1.0]), [0]) + qc.append(circuit.Gate("my_gate", 1, [2.0]), [0]) + qc.append(circuit.Gate("my_gate", 1, [3.0]), [0]) + + transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0]) + + my_gate_q0_1_0 = self.my_gate_q0.assign_parameters({self.sched_param: 1.0}, inplace=False) + my_gate_q0_2_0 = self.my_gate_q0.assign_parameters({self.sched_param: 2.0}, inplace=False) + my_gate_q0_3_0 = self.my_gate_q0.assign_parameters({self.sched_param: 3.0}, inplace=False) + + ref_calibration = { + "my_gate": { + ((0,), (1.0,)): my_gate_q0_1_0, + ((0,), (2.0,)): my_gate_q0_2_0, + ((0,), (3.0,)): my_gate_q0_3_0, + } + } + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + + def test_transpile_with_different_qubit(self): + """Test transpile with qubit without custom gate.""" + backend = FakeAthens() + backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) + + qc = circuit.QuantumCircuit(1) + qc.sx(0) + qc.measure_all() + + transpiled_qc = transpile(qc, backend, initial_layout=[3]) + + self.assertDictEqual(transpiled_qc.calibrations, {})