diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index df63818d6675..832b79e148a5 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -132,6 +132,10 @@ def __call__(self, circuit, property_set=None): if self.property_set["layout"]: result_circuit._layout = self.property_set["layout"] + if self.property_set["clbit_write_latency"] is not None: + result_circuit._clbit_write_latency = self.property_set["clbit_write_latency"] + if self.property_set["conditional_latency"] is not None: + result_circuit._conditional_latency = self.property_set["conditional_latency"] return result_circuit diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index f341c9fef310..547fe798c13b 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -104,8 +104,11 @@ ALAPSchedule ASAPSchedule DynamicalDecoupling + ConstrainedReschedule AlignMeasures ValidatePulseGates + InstructionDurationCheck + SetIOLatency Circuit Analysis ================ @@ -227,9 +230,12 @@ from .scheduling import ALAPSchedule from .scheduling import ASAPSchedule from .scheduling import DynamicalDecoupling -from .scheduling import AlignMeasures +from .scheduling import AlignMeasures # Deprecated from .scheduling import ValidatePulseGates from .scheduling import PadDelay +from .scheduling import ConstrainedReschedule +from .scheduling import InstructionDurationCheck +from .scheduling import SetIOLatency # additional utility passes from .utils import CheckMap diff --git a/qiskit/transpiler/passes/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/__init__.py index 339012e25969..6a7a95cb8be7 100644 --- a/qiskit/transpiler/passes/scheduling/__init__.py +++ b/qiskit/transpiler/passes/scheduling/__init__.py @@ -12,9 +12,13 @@ """Module containing circuit scheduling passes.""" -from .alap import ALAPSchedule -from .asap import ASAPSchedule +from .scheduling import ALAPSchedule, ASAPSchedule, SetIOLatency from .time_unit_conversion import TimeUnitConversion -from .instruction_alignment import AlignMeasures, ValidatePulseGates -from .pad_delay import PadDelay -from .dynamical_decoupling import DynamicalDecoupling +from .padding import PadDelay, DynamicalDecoupling +from .alignments import InstructionDurationCheck, ValidatePulseGates, ConstrainedReschedule + +# For backward compability +from . import alignments as instruction_alignments + +# TODO Deprecated pass. Will be removed after deprecation period. +from .alignments import AlignMeasures diff --git a/qiskit/transpiler/passes/scheduling/alignments/__init__.py b/qiskit/transpiler/passes/scheduling/alignments/__init__.py new file mode 100644 index 000000000000..513144937ab5 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/__init__.py @@ -0,0 +1,81 @@ +# 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. + +"""Validation and optimization for hardware instruction alignment constraints. + +This is a control electronics aware analysis pass group. + +In many quantum computing architectures gates (instructions) are implemented with +shaped analog stimulus signals. These signals are digitally stored in the +waveform memory of the control electronics and converted into analog voltage signals +by electronic components called digital to analog converters (DAC). + +In a typical hardware implementation of superconducting quantum processors, +a single qubit instruction is implemented by a +microwave signal with the duration of around several tens of ns with a per-sample +time resolution of ~0.1-10ns, as reported by ``backend.configuration().dt``. +In such systems requiring higher DAC bandwidth, control electronics often +defines a `pulse granularity`, in other words a data chunk, to allow the DAC to +perform the signal conversion in parallel to gain the bandwidth. + +A control electronics, i.e. micro-architecture, of the real quantum backend may +impose some constraints on the start time of microinstructions. +In Qiskit SDK, the duration of :class:`qiskit.circuit.Delay` can take arbitrary +value in units of dt, thus circuits involving delays may violate the constraints, +which may result in failure in the circuit execution on the backend. + +There are two alignment constraint values reported by your quantum backend. +In addition, if you want to define a custom instruction as a pulse gate, i.e. calibration, +the underlying pulse instruction should satisfy other two waveform constraints. + +Pulse alignment constraint + + This value is reported by ``timing_constraints["pulse_alignment"]`` in the backend + configuration in units of dt. The start time of the all pulse instruction should be + multiple of this value. Violation of this constraint may result in the + backend execution failure. + + In most of the senarios, the scheduled start time of ``DAGOpNode`` corresponds to the + start time of the underlying pulse instruction composing the node operation. + However, this assumption can be intentionally broken by defining a pulse gate, + i.e. calibration, with the schedule involving pre-buffer, i.e. some random pulse delay + followed by a pulse instruction. Because this pass is not aware of such edge case, + the user must take special care of pulse gates if any. + +Acquire alignment constraint + + This value is reported by ``timing_constraints["acquire_alignment"]`` in the backend + configuration in units of dt. The start time of the :class:`~qiskit.circuit.Measure` + instruction should be multiple of this value. + +Granularity constraint + + This value is reported by ``timing_constraints["granularity"]`` in the backend + configuration in units of dt. This is the constraint for a single pulse :class:`Play` + instruction that may constitute your pulse gate. + The length of waveform samples should be multipel of this constraint value. + Violation of this constraint may result in failue in backend execution. + +Minimum pulse length constraint + + This value is reported by ``timing_constraints["min_length"]`` in the backend + configuration in units of dt. This is the constraint for a single pulse :class:`Play` + instruction that may constitute your pulse gate. + The length of waveform samples should be greater than this constraint value. + Violation of this constraint may result in failue in backend execution. + +""" + +from .check_durations import InstructionDurationCheck +from .pulse_gate_validation import ValidatePulseGates +from .reschedule import ConstrainedReschedule +from .align_measures import AlignMeasures diff --git a/qiskit/transpiler/passes/scheduling/alignments/align_measures.py b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py new file mode 100644 index 000000000000..0318d6988847 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py @@ -0,0 +1,39 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# 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. + +"""Deprecated. Measurement alignment.""" + +import warnings + +from qiskit.transpiler.passes.scheduling.alignments.reschedule import ConstrainedReschedule + + +class AlignMeasures: + """Deprecated. Measurement alignment.""" + + def __new__(cls, alignment=1) -> ConstrainedReschedule: + """Create new pass. + + Args: + alignment: Integer number representing the minimum time resolution to + trigger measure instruction in units of ``dt``. This value depends on + the control electronics of your quantum processor. + + Returns: + ConstrainedReschedule instance that is a drop-in-replacement of this class. + """ + warnings.warn( + f"{cls.__name__} has been deprecated as of Qiskit 20.0. " + f"Use ConstrainedReschedule pass instead.", + FutureWarning, + ) + return ConstrainedReschedule(acquire_alignment=alignment) diff --git a/qiskit/transpiler/passes/scheduling/alignments/check_durations.py b/qiskit/transpiler/passes/scheduling/alignments/check_durations.py new file mode 100644 index 000000000000..b2cc90b0d152 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/check_durations.py @@ -0,0 +1,75 @@ +# 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. +"""A pass to check if input circuit requires reschedule.""" + +from qiskit.circuit.delay import Delay +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.basepasses import AnalysisPass + + +class InstructionDurationCheck(AnalysisPass): + """Duration validation pass for reschedule. + + This pass investigates the input quantum circuit and checks if the circuit requres + rescheduling for execution. Note that this pass can be triggered without scheduling. + This pass only checks the duration of delay instructions and user defined pulse gates, + which report duration values without pre-scheduling. + + This pass assumes backend supported instructions, i.e. basis gates, have no violation + of the hardware alignment constraints, which is true in general. + """ + + def __init__( + self, + acquire_alignment: int = 1, + pulse_alignment: int = 1, + ): + """Create new duration validation pass. + + The alignment values depend on the control electronics of your quantum processor. + + Args: + acquire_alignment: Integer number representing the minimum time resolution to + trigger acquisition instruction in units of ``dt``. + pulse_alignment: Integer number representing the minimum time resolution to + trigger gate instruction in units of ``dt``. + """ + super().__init__() + self.acquire_align = acquire_alignment + self.pulse_align = pulse_alignment + + def run(self, dag: DAGCircuit): + """Run duration validation passes. + + Args: + dag: DAG circuit to check instruction durations. + """ + self.property_set["reschedule_required"] = False + + # Rescheduling is not necessary + if self.acquire_align == 1 and self.pulse_align == 1: + return + + # Check delay durations + for delay_node in dag.op_nodes(Delay): + dur = delay_node.op.duration + if not (dur % self.acquire_align == 0 and dur % self.pulse_align == 0): + self.property_set["reschedule_required"] = True + return + + # Check custom gate durations + for inst_defs in dag.calibrations.values(): + for caldef in inst_defs.values(): + dur = caldef.duration + if not (dur % self.acquire_align == 0 and dur % self.pulse_align == 0): + self.property_set["reschedule_required"] = True + return diff --git a/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py b/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py new file mode 100644 index 000000000000..697d36fa71b6 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py @@ -0,0 +1,97 @@ +# 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. + +"""Analysis passes for hardware alignment constraints.""" + +from qiskit.dagcircuit import DAGCircuit +from qiskit.pulse import Play +from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.transpiler.exceptions import TranspilerError + + +class ValidatePulseGates(AnalysisPass): + """Check custom gate length. + + This is a control electronics aware analysis pass. + + Quantum gates (instructions) are often implemented with shaped analog stimulus signals. + These signals may be digitally stored in the waveform memory of the control electronics + and converted into analog voltage signals by electronic components known as + digital to analog converters (DAC). + + In Qiskit SDK, we can define the pulse-level implementation of custom quantum gate + instructions, as a `pulse gate + `__, + thus user gates should satisfy all waveform memory constraints imposed by the backend. + + This pass validates all attached calibration entries and raises ``TranspilerError`` to + kill the transpilation process if any invalid calibration entry is found. + This pass saves users from waiting until job execution time to get an invalid pulse error from + the backend control electronics. + """ + + def __init__( + self, + granularity: int = 1, + min_length: int = 1, + ): + """Create new pass. + + Args: + granularity: Integer number representing the minimum time resolution to + define the pulse gate length in units of ``dt``. This value depends on + the control electronics of your quantum processor. + min_length: Integer number representing the minimum data point length to + define the pulse gate in units of ``dt``. This value depends on + the control electronics of your quantum processor. + """ + super().__init__() + self.granularity = granularity + self.min_length = min_length + + def run(self, dag: DAGCircuit): + """Run the pulse gate validation attached to ``dag``. + + Args: + dag: DAG to be validated. + + Returns: + DAGCircuit: DAG with consistent timing and op nodes annotated with duration. + + Raises: + TranspilerError: When pulse gate violate pulse controller constraints. + """ + if self.granularity == 1 and self.min_length == 1: + # we can define arbitrary length pulse with dt resolution + return + + for gate, insts in dag.calibrations.items(): + for qubit_param_pair, schedule in insts.items(): + for _, inst in schedule.instructions: + if isinstance(inst, Play): + pulse = inst.pulse + if pulse.duration % self.granularity != 0: + raise TranspilerError( + f"Pulse duration is not multiple of {self.granularity}. " + "This pulse cannot be played on the specified backend. " + f"Please modify the duration of the custom gate pulse {pulse.name} " + f"which is associated with the gate {gate} of " + f"qubit {qubit_param_pair[0]}." + ) + if pulse.duration < self.min_length: + raise TranspilerError( + f"Pulse gate duration is less than {self.min_length}. " + "This pulse cannot be played on the specified backend. " + f"Please modify the duration of the custom gate pulse {pulse.name} " + f"which is associated with the gate {gate} of " + "qubit {qubit_param_pair[0]}." + ) diff --git a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py new file mode 100644 index 000000000000..ab22b0080a9d --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py @@ -0,0 +1,237 @@ +# 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. + +"""Rescheduler pass to adjust node start times.""" + +from typing import List + +from qiskit.circuit.gate import Gate +from qiskit.circuit.measure import Measure +from qiskit.dagcircuit import DAGCircuit, DAGOpNode, DAGOutNode +from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.transpiler.exceptions import TranspilerError + + +class ConstrainedReschedule(AnalysisPass): + """Rescheduler pass that updates node start times to conform to the hardware alignments. + + This pass shifts DAG node start times previously scheduled with one of + the scheduling passes, e.g. :class:`ASAPSchedule` or :class:`ALAPSchedule`, + so that every instruction start time satisfies alignment constraints. + + Examples: + + We assume executing the following circuit on a backend with 16 dt of acquire alignment. + + .. parsed-literal:: + + ┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├ + └───┘└────────────────┘└╥┘ + c: 1/════════════════════════╩═ + 0 + + Note that delay of 100 dt induces a misalignment of 4 dt at the measurement. + This pass appends an extra 12 dt time shift to the input circuit. + + .. parsed-literal:: + + ┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├ + └───┘└────────────────┘└╥┘ + c: 1/════════════════════════╩═ + 0 + + Notes: + + Your backend may execute circuits violating these alignment constraints. + However, you may obtain erroneous measurement result because of the + untracked phase originating in the instruction misalignment. + """ + + def __init__( + self, + acquire_alignment: int = 1, + pulse_alignment: int = 1, + ): + """Create new rescheduler pass. + + The alignment values depend on the control electronics of your quantum processor. + + Args: + acquire_alignment: Integer number representing the minimum time resolution to + trigger acquisition instruction in units of ``dt``. + pulse_alignment: Integer number representing the minimum time resolution to + trigger gate instruction in units of ``dt``. + """ + super().__init__() + self.acquire_align = acquire_alignment + self.pulse_align = pulse_alignment + + @classmethod + def _get_next_gate(cls, dag: DAGCircuit, node: DAGOpNode) -> List[DAGOpNode]: + """Get next non-delay nodes. + + Args: + dag: DAG circuit to be rescheduled with constraints. + node: Current node. + + Returns: + A list of non-delay successors. + """ + op_nodes = [] + for next_node in dag.successors(node): + if isinstance(next_node, DAGOutNode): + continue + op_nodes.append(next_node) + + return op_nodes + + def _push_node_back(self, dag: DAGCircuit, node: DAGOpNode, shift: int): + """Update start time of current node. Successors are also shifted to avoid overlap. + + .. note:: + + This logic assumes the all bits in the qregs and cregs synchronously start and end, + i.e. occupy the same time slot, but qregs and cregs can take + different time slot due to classical I/O latencies. + + Args: + dag: DAG circuit to be rescheduled with constraints. + node: Current node. + shift: Amount of required time shift. + """ + node_start_time = self.property_set["node_start_time"] + conditional_latency = self.property_set.get("conditional_latency", 0) + clbit_write_latency = self.property_set.get("clbit_write_latency", 0) + + # Compute shifted t1 of this node separately for qreg and creg + this_t0 = node_start_time[node] + new_t1q = this_t0 + node.op.duration + shift + this_qubits = set(node.qargs) + if isinstance(node.op, Measure): + # creg access ends at the end of instruction + new_t1c = new_t1q + this_clbits = set(node.cargs) + else: + if node.op.condition_bits: + # conditional access ends at the beginning of node start time + new_t1c = this_t0 + shift + this_clbits = set(node.op.condition_bits) + else: + new_t1c = None + this_clbits = set() + + # Check successors for overlap + for next_node in self._get_next_gate(dag, node): + # Compute next node start time separately for qreg and creg + next_t0q = node_start_time[next_node] + next_qubits = set(next_node.qargs) + if isinstance(next_node.op, Measure): + # creg access starts after write latency + next_t0c = next_t0q + clbit_write_latency + next_clbits = set(next_node.cargs) + else: + if next_node.op.condition_bits: + # conditional access starts before node start time + next_t0c = next_t0q - conditional_latency + next_clbits = set(next_node.op.condition_bits) + else: + next_t0c = None + next_clbits = set() + # Compute overlap if there is qubits overlap + if any(this_qubits & next_qubits): + qreg_overlap = new_t1q - next_t0q + else: + qreg_overlap = 0 + # Compute overlap if there is clbits overlap + if any(this_clbits & next_clbits): + creg_overlap = new_t1c - next_t0c + else: + creg_overlap = 0 + # Shift next node if there is finite overlap in either in qubits or clbits + overlap = max(qreg_overlap, creg_overlap) + if overlap > 0: + self._push_node_back(dag, next_node, overlap) + + # Update start time of this node after all overlaps are resolved + node_start_time[node] += shift + + def run(self, dag: DAGCircuit): + """Run rescheduler. + + This pass should perform rescheduling to satisfy: + + - All DAGOpNode are placed at start time satisfying hardware alignment constraints. + - The end time of current does not overlap with the start time of successor nodes. + - Compiler directives are not necessary satisfying the constraints. + + Assumptions: + + - Topological order and absolute time order of DAGOpNode are consistent. + - All bits in either qargs or cargs associated with node synchronously start. + - Start time of qargs and cargs may different due to I/O latency. + + Based on the configurations above, rescheduler pass takes following strategy. + + 1. Scan node from the beginning, i.e. from left of the circuit. The rescheduler + calls ``node_start_time`` from the property set, + and retrieves the scheduled start time of current node. + 2. If the start time of the node violates the alignment constraints, + the scheduler increases the start time until it satisfies the constraint. + 3. Check overlap with successor nodes. If any overlap occurs, the rescheduler + recursively pushs the successor nodes backward towards the end of the wire. + Note that shifted location doesn't need to satisfy the constraints, + thus it will be a minimum delay to resolve the overlap with the ancestor node. + 4. Repeat 1-3 until the node at the end of the wire. This will resolve + all misalignment without creating overlap between the nodes. + + Args: + dag: DAG circuit to be rescheduled with constraints. + + Raises: + TranspilerError: If circuit is not scheduled. + """ + + if "node_start_time" not in self.property_set: + raise TranspilerError( + f"The input circuit {dag.name} is not scheduled. Call one of scheduling passes " + f"before running the {self.__class__.__name__} pass." + ) + + node_start_time = self.property_set["node_start_time"] + + for node in dag.topological_op_nodes(): + if node_start_time[node] == 0: + # Every instruction can start at t=0 + continue + + if isinstance(node.op, Gate): + alignment = self.pulse_align + elif isinstance(node.op, Measure): + alignment = self.acquire_align + else: + # Directive or delay. These can start at arbitrary time. + continue + + try: + misalignment = node_start_time[node] % alignment + if misalignment == 0: + continue + shift = max(0, alignment - misalignment) + except KeyError as ex: + raise TranspilerError( + f"Start time of {repr(node)} is not found. This node is likely added after " + "this circuit is scheduled. Run scheduler again." + ) from ex + if shift > 0: + self._push_node_back(dag, node, shift) diff --git a/qiskit/transpiler/passes/scheduling/instruction_alignment.py b/qiskit/transpiler/passes/scheduling/instruction_alignment.py deleted file mode 100644 index 838e372e63b2..000000000000 --- a/qiskit/transpiler/passes/scheduling/instruction_alignment.py +++ /dev/null @@ -1,319 +0,0 @@ -# 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. - -"""Align measurement instructions.""" -import itertools -import warnings -from collections import defaultdict -from typing import List, Union - -from qiskit.circuit.delay import Delay -from qiskit.circuit.instruction import Instruction -from qiskit.circuit.measure import Measure -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit import DAGCircuit -from qiskit.pulse import Play -from qiskit.transpiler.basepasses import TransformationPass, AnalysisPass -from qiskit.transpiler.exceptions import TranspilerError - - -class AlignMeasures(TransformationPass): - """Measurement alignment. - - This is a control electronics aware optimization pass. - - In many quantum computing architectures gates (instructions) are implemented with - shaped analog stimulus signals. These signals are digitally stored in the - waveform memory of the control electronics and converted into analog voltage signals - by electronic components called digital to analog converters (DAC). - - In a typical hardware implementation of superconducting quantum processors, - a single qubit instruction is implemented by a - microwave signal with the duration of around several tens of ns with a per-sample - time resolution of ~0.1-10ns, as reported by ``backend.configuration().dt``. - In such systems requiring higher DAC bandwidth, control electronics often - defines a `pulse granularity`, in other words a data chunk, to allow the DAC to - perform the signal conversion in parallel to gain the bandwidth. - - Measurement alignment is required if a backend only allows triggering ``measure`` - instructions at a certain multiple value of this pulse granularity. - This value is usually provided by ``backend.configuration().timing_constraints``. - - In Qiskit SDK, the duration of delay can take arbitrary value in units of ``dt``, - thus circuits involving delays may violate the above alignment constraint (i.e. misalignment). - This pass shifts measurement instructions to a new time position to fix the misalignment, - by inserting extra delay right before the measure instructions. - The input of this pass should be scheduled :class:`~qiskit.dagcircuit.DAGCircuit`, - thus one should select one of the scheduling passes - (:class:`~qiskit.transpiler.passes.ALAPSchedule` or - :class:`~qiskit.trasnpiler.passes.ASAPSchedule`) before calling this. - - Examples: - We assume executing the following circuit on a backend with ``alignment=16``. - - .. parsed-literal:: - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - Note that delay of 100 dt induces a misalignment of 4 dt at the measurement. - This pass appends an extra 12 dt time shift to the input circuit. - - .. parsed-literal:: - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - This pass always inserts a positive delay before measurements - rather than reducing other delays. - - Notes: - The Backend may allow users to execute circuits violating the alignment constraint. - However, it may return meaningless measurement data mainly due to the phase error. - """ - - def __init__(self, alignment: int = 1): - """Create new pass. - - Args: - alignment: Integer number representing the minimum time resolution to - trigger measure instruction in units of ``dt``. This value depends on - the control electronics of your quantum processor. - """ - super().__init__() - self.alignment = alignment - - def run(self, dag: DAGCircuit): - """Run the measurement alignment pass on `dag`. - - Args: - dag (DAGCircuit): DAG to be checked. - - Returns: - DAGCircuit: DAG with consistent timing and op nodes annotated with duration. - - Raises: - TranspilerError: If circuit is not scheduled. - """ - time_unit = self.property_set["time_unit"] - - if not _check_alignment_required(dag, self.alignment, Measure): - # return input as-is to avoid unnecessary scheduling. - # because following procedure regenerate new DAGCircuit, - # we should avoid continuing if not necessary from performance viewpoint. - return dag - - # if circuit is not yet scheduled, schedule with ALAP method - if dag.duration is None: - raise TranspilerError( - f"This circuit {dag.name} may involve a delay instruction violating the " - "pulse controller alignment. To adjust instructions to " - "right timing, you should call one of scheduling passes first. " - "This is usually done by calling transpiler with scheduling_method='alap'." - ) - - # the following lines are basically copied from ASAPSchedule pass - # - # * some validations for non-scheduled nodes are dropped, since we assume scheduled input - # * pad_with_delay is called only with non-delay node to avoid consecutive delay - new_dag = dag._copy_circuit_metadata() - - qubit_time_available = defaultdict(int) # to track op start time - qubit_stop_times = defaultdict(int) # to track delay start time for padding - clbit_readable = defaultdict(int) - clbit_writeable = defaultdict(int) - - def pad_with_delays(qubits: List[int], until, unit) -> None: - """Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``.""" - for q in qubits: - if qubit_stop_times[q] < until: - idle_duration = until - qubit_stop_times[q] - new_dag.apply_operation_back(Delay(idle_duration, unit), [q]) - - for node in dag.topological_op_nodes(): - # choose appropriate clbit available time depending on op - clbit_time_available = ( - clbit_writeable if isinstance(node.op, Measure) else clbit_readable - ) - # correction to change clbit start time to qubit start time - delta = node.op.duration if isinstance(node.op, Measure) else 0 - start_time = max( - itertools.chain( - (qubit_time_available[q] for q in node.qargs), - (clbit_time_available[c] - delta for c in node.cargs + node.op.condition_bits), - ) - ) - - if isinstance(node.op, Measure): - if start_time % self.alignment != 0: - start_time = ((start_time // self.alignment) + 1) * self.alignment - - if not isinstance(node.op, Delay): # exclude delays for combining consecutive delays - pad_with_delays(node.qargs, until=start_time, unit=time_unit) - new_dag.apply_operation_back(node.op, node.qargs, node.cargs) - - stop_time = start_time + node.op.duration - # update time table - for q in node.qargs: - qubit_time_available[q] = stop_time - if not isinstance(node.op, Delay): - qubit_stop_times[q] = stop_time - for c in node.cargs: # measure - clbit_writeable[c] = clbit_readable[c] = stop_time - for c in node.op.condition_bits: # conditional op - clbit_writeable[c] = max(start_time, clbit_writeable[c]) - - working_qubits = qubit_time_available.keys() - circuit_duration = max(qubit_time_available[q] for q in working_qubits) - pad_with_delays(new_dag.qubits, until=circuit_duration, unit=time_unit) - - new_dag.name = dag.name - new_dag.metadata = dag.metadata - - # set circuit duration and unit to indicate it is scheduled - new_dag.duration = circuit_duration - new_dag.unit = time_unit - - return new_dag - - -class ValidatePulseGates(AnalysisPass): - """Check custom gate length. - - This is a control electronics aware analysis pass. - - Quantum gates (instructions) are often implemented with shaped analog stimulus signals. - These signals may be digitally stored in the waveform memory of the control electronics - and converted into analog voltage signals by electronic components known as - digital to analog converters (DAC). - - In Qiskit SDK, we can define the pulse-level implementation of custom quantum gate - instructions, as a `pulse gate - `__, - thus user gates should satisfy all waveform memory constraints imposed by the backend. - - This pass validates all attached calibration entries and raises ``TranspilerError`` to - kill the transpilation process if any invalid calibration entry is found. - This pass saves users from waiting until job execution time to get an invalid pulse error from - the backend control electronics. - """ - - def __init__( - self, - granularity: int = 1, - min_length: int = 1, - ): - """Create new pass. - - Args: - granularity: Integer number representing the minimum time resolution to - define the pulse gate length in units of ``dt``. This value depends on - the control electronics of your quantum processor. - min_length: Integer number representing the minimum data point length to - define the pulse gate in units of ``dt``. This value depends on - the control electronics of your quantum processor. - """ - super().__init__() - self.granularity = granularity - self.min_length = min_length - - def run(self, dag: DAGCircuit): - """Run the measurement alignment pass on `dag`. - - Args: - dag (DAGCircuit): DAG to be checked. - - Returns: - DAGCircuit: DAG with consistent timing and op nodes annotated with duration. - - Raises: - TranspilerError: When pulse gate violate pulse controller constraints. - """ - if self.granularity == 1 and self.min_length == 1: - # we can define arbitrary length pulse with dt resolution - return - - for gate, insts in dag.calibrations.items(): - for qubit_param_pair, schedule in insts.items(): - for _, inst in schedule.instructions: - if isinstance(inst, Play): - pulse = inst.pulse - if pulse.duration % self.granularity != 0: - raise TranspilerError( - f"Pulse duration is not multiple of {self.granularity}. " - "This pulse cannot be played on the specified backend. " - f"Please modify the duration of the custom gate pulse {pulse.name} " - f"which is associated with the gate {gate} of " - f"qubit {qubit_param_pair[0]}." - ) - if pulse.duration < self.min_length: - raise TranspilerError( - f"Pulse gate duration is less than {self.min_length}. " - "This pulse cannot be played on the specified backend. " - f"Please modify the duration of the custom gate pulse {pulse.name} " - f"which is associated with the gate {gate} of " - "qubit {qubit_param_pair[0]}." - ) - - -def _check_alignment_required( - dag: DAGCircuit, - alignment: int, - instructions: Union[Instruction, List[Instruction]], -) -> bool: - """Check DAG nodes and return a boolean representing if instruction scheduling is necessary. - - Args: - dag: DAG circuit to check. - alignment: Instruction alignment condition. - instructions: Target instructions. - - Returns: - If instruction scheduling is necessary. - """ - if not isinstance(instructions, list): - instructions = [instructions] - - if alignment == 1: - # disable alignment if arbitrary t0 value can be used - return False - - if all(len(dag.op_nodes(inst)) == 0 for inst in instructions): - # disable alignment if target instruction is not involved - return False - - # check delay durations - for delay_node in dag.op_nodes(Delay): - duration = delay_node.op.duration - if isinstance(duration, ParameterExpression): - # duration is parametrized: - # raise user warning if backend alignment is not 1. - warnings.warn( - f"Parametrized delay with {repr(duration)} is found in circuit {dag.name}. " - f"This backend requires alignment={alignment}. " - "Please make sure all assigned values are multiple values of the alignment.", - UserWarning, - ) - else: - # duration is bound: - # check duration and trigger alignment if it violates constraint - if duration % alignment != 0: - return True - - # disable alignment if all delays are multiple values of the alignment - return False diff --git a/qiskit/transpiler/passes/scheduling/padding/__init__.py b/qiskit/transpiler/passes/scheduling/padding/__init__.py new file mode 100644 index 000000000000..f6f09caac487 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/padding/__init__.py @@ -0,0 +1,16 @@ +# 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. + +"""Scheduling pass to fill idle times with gate sequence.""" + +from .dynamical_decoupling import DynamicalDecoupling +from .pad_delay import PadDelay diff --git a/qiskit/transpiler/passes/scheduling/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py similarity index 100% rename from qiskit/transpiler/passes/scheduling/base_padding.py rename to qiskit/transpiler/passes/scheduling/padding/base_padding.py diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py similarity index 99% rename from qiskit/transpiler/passes/scheduling/dynamical_decoupling.py rename to qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index d7425abb3bed..9b54dbbce995 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -25,7 +25,8 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.passes.optimization import Optimize1qGates -from qiskit.transpiler.passes.scheduling.base_padding import BasePadding + +from .base_padding import BasePadding class DynamicalDecoupling(BasePadding): diff --git a/qiskit/transpiler/passes/scheduling/pad_delay.py b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py similarity index 97% rename from qiskit/transpiler/passes/scheduling/pad_delay.py rename to qiskit/transpiler/passes/scheduling/padding/pad_delay.py index 3dc868e67732..c188247e2b26 100644 --- a/qiskit/transpiler/passes/scheduling/pad_delay.py +++ b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py @@ -15,7 +15,8 @@ from qiskit.circuit import Qubit from qiskit.circuit.delay import Delay from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOutNode -from qiskit.transpiler.passes.scheduling.base_padding import BasePadding + +from .base_padding import BasePadding class PadDelay(BasePadding): diff --git a/qiskit/transpiler/passes/scheduling/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/scheduling/__init__.py new file mode 100644 index 000000000000..7c737dc20e0e --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/scheduling/__init__.py @@ -0,0 +1,17 @@ +# 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. + +"""Scheduling pass to assign instruction start time.""" + +from .asap import ASAPSchedule +from .alap import ALAPSchedule +from .set_io_latency import SetIOLatency diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/scheduling/alap.py similarity index 94% rename from qiskit/transpiler/passes/scheduling/alap.py rename to qiskit/transpiler/passes/scheduling/scheduling/alap.py index 980c917e9dca..983eeb0d0a7d 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/alap.py @@ -14,7 +14,7 @@ from qiskit.circuit import Measure from qiskit.transpiler.exceptions import TranspilerError -from .base_scheduler import BaseScheduler +from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler class ALAPSchedule(BaseScheduler): @@ -40,6 +40,9 @@ def run(self, dag): if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ALAP schedule runs on physical circuits only") + conditional_latency = self.property_set.get("conditional_latency", 0) + clbit_write_latency = self.property_set.get("clbit_write_latency", 0) + node_start_time = dict() idle_before = {q: 0 for q in dag.qubits + dag.clbits} bit_indices = {bit: index for index, bit in enumerate(dag.qubits)} @@ -82,7 +85,7 @@ def run(self, dag): t0 = max(t0q, t0c - op_duration) t1 = t0 + op_duration for clbit in node.op.condition_bits: - idle_before[clbit] = t1 + self.conditional_latency + idle_before[clbit] = t1 + conditional_latency else: t0 = t0q t1 = t0 + op_duration @@ -103,7 +106,7 @@ def run(self, dag): # |t0 + (duration - clbit_write_latency) # for clbit in node.cargs: - idle_before[clbit] = t0 + (op_duration - self.clbit_write_latency) + idle_before[clbit] = t0 + (op_duration - clbit_write_latency) else: # It happens to be directives such as barrier t0 = max(idle_before[bit] for bit in node.qargs + node.cargs) diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/scheduling/asap.py similarity index 92% rename from qiskit/transpiler/passes/scheduling/asap.py rename to qiskit/transpiler/passes/scheduling/scheduling/asap.py index e0b5cdad1544..469d7b7f510d 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/asap.py @@ -14,7 +14,7 @@ from qiskit.circuit import Measure from qiskit.transpiler.exceptions import TranspilerError -from .base_scheduler import BaseScheduler +from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler class ASAPSchedule(BaseScheduler): @@ -40,6 +40,9 @@ def run(self, dag): if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ASAP schedule runs on physical circuits only") + conditional_latency = self.property_set.get("conditional_latency", 0) + clbit_write_latency = self.property_set.get("clbit_write_latency", 0) + node_start_time = dict() idle_after = {q: 0 for q in dag.qubits + dag.clbits} bit_indices = {q: index for index, q in enumerate(dag.qubits)} @@ -69,8 +72,8 @@ def run(self, dag): # C ▒▒▒░░░▒▒░░░ # |t0q - conditional_latency # - t0c = max(t0q - self.conditional_latency, t0c) - t1c = t0c + self.conditional_latency + t0c = max(t0q - conditional_latency, t0c) + t1c = t0c + conditional_latency for bit in node.op.condition_bits: # Lock clbit until state is read idle_after[bit] = t1c @@ -111,7 +114,7 @@ def run(self, dag): # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ # |t0c' = t0c + clbit_write_latency # - t0 = max(t0q, t0c - self.clbit_write_latency) + t0 = max(t0q, t0c - clbit_write_latency) t1 = t0 + op_duration for clbit in node.cargs: idle_after[clbit] = t1 diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py similarity index 88% rename from qiskit/transpiler/passes/scheduling/base_scheduler.py rename to qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index d582822b53d6..5cedbd0e1ae5 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -140,11 +140,10 @@ class BaseScheduler(AnalysisPass): The former parameter determines the delay of the register write-access from the beginning of the measure instruction t0, and another parameter determines the delay of conditional gate operation from t0 which comes from the register read-access. + These information might be found in the backend configuration and then should + be copied to the pass manager property set before the pass is called. - Since we usually expect topological ordering and time ordering are identical - without the context of microarchitecture, both latencies are set to zero by default. - In this case, ``Measure`` instruction immediately locks the register C. - Under this configuration, the `alap`-scheduled circuit of above example may become + By default latencies, the `alap`-scheduled circuit of above example may become .. parsed-literal:: @@ -208,36 +207,15 @@ class BaseScheduler(AnalysisPass): CONDITIONAL_SUPPORTED = (Gate, Delay) - def __init__( - self, - durations: InstructionDurations, - clbit_write_latency: int = 0, - conditional_latency: int = 0, - ): + def __init__(self, durations: InstructionDurations): """Scheduler initializer. Args: durations: Durations of instructions to be used in scheduling - clbit_write_latency: A control flow constraints. Because standard superconducting - quantum processor implement dispersive QND readout, the actual data transfer - to the clbit happens after the round-trip stimulus signal is buffered - and discriminated into quantum state. - The interval ``[t0, t0 + clbit_write_latency]`` is regarded as idle time - for clbits associated with the measure instruction. - This defaults to 0 dt which is identical to Qiskit Pulse scheduler. - conditional_latency: A control flow constraints. This value represents - a latency of reading a classical register for the conditional operation. - The gate operation occurs after this latency. This appears as a delay - in front of the DAGOpNode of the gate. - This defaults to 0 dt. """ super().__init__() self.durations = durations - # Control flow constraints. - self.clbit_write_latency = clbit_write_latency - self.conditional_latency = conditional_latency - # Ensure op node durations are attached and in consistent unit self.requires.append(TimeUnitConversion(durations)) diff --git a/qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py b/qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py new file mode 100644 index 000000000000..38fa9f1258d8 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py @@ -0,0 +1,64 @@ +# 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. +"""Set classical IO latency information to circuit.""" + +from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.dagcircuit import DAGCircuit + + +class SetIOLatency(AnalysisPass): + """Set IOLatency information to the input circuit. + + The ``clbit_write_latency`` and ``conditional_latency`` are added to + the property set of pass manager. These information can be shared among the passes + that perform scheduling on instructions acting on classical registers. + + Once these latencies are added to the property set, this information + is also copied to the output circuit object as protected attributes, + so that it can be utilized outside the transilation, + for example, the timeline visualization can use latency to accurately show + time occupation by instructions on the classical registers. + """ + + def __init__( + self, + clbit_write_latency: int = 0, + conditional_latency: int = 0, + ): + """Create pass with latency information. + + Args: + clbit_write_latency: A control flow constraints. Because standard superconducting + quantum processor implement dispersive QND readout, the actual data transfer + to the clbit happens after the round-trip stimulus signal is buffered + and discriminated into quantum state. + The interval ``[t0, t0 + clbit_write_latency]`` is regarded as idle time + for clbits associated with the measure instruction. + This defaults to 0 dt which is identical to Qiskit Pulse scheduler. + conditional_latency: A control flow constraints. This value represents + a latency of reading a classical register for the conditional operation. + The gate operation occurs after this latency. This appears as a delay + in front of the DAGOpNode of the gate. + This defaults to 0 dt. + """ + super().__init__() + self._conditional_latency = conditional_latency + self._clbit_write_latency = clbit_write_latency + + def run(self, dag: DAGCircuit): + """Add IO latency information. + + Args: + dag: Input DAG circuit. + """ + self.property_set["conditional_latency"] = self._conditional_latency + self.property_set["clbit_write_latency"] = self._clbit_write_latency diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 83b88c5bd369..fe4d3bfc5e22 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -46,7 +46,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule -from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ConstrainedReschedule +from qiskit.transpiler.passes import InstructionDurationCheck from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import PadDelay @@ -214,39 +215,6 @@ def _direction_condition(property_set): _direction = [GateDirection(coupling_map, target)] - # 7. Unify all durations (either SI, or convert to dt if known) - # Schedule the circuit only when scheduling_method is supplied - _time_unit_setup = [ContainsInstruction("delay")] - _time_unit_conversion = [TimeUnitConversion(instruction_durations)] - - def _contains_delay(property_set): - return property_set["contains_delay"] - - _scheduling = [] - if scheduling_method: - _scheduling += _time_unit_conversion - if scheduling_method in {"alap", "as_late_as_possible"}: - _scheduling += [ALAPSchedule(instruction_durations), PadDelay()] - elif scheduling_method in {"asap", "as_soon_as_possible"}: - _scheduling += [ASAPSchedule(instruction_durations), PadDelay()] - else: - raise TranspilerError("Invalid scheduling method %s." % scheduling_method) - - # 8. Call measure alignment. Should come after scheduling. - if ( - timing_constraints.granularity != 1 - or timing_constraints.min_length != 1 - or timing_constraints.acquire_alignment != 1 - ): - _alignments = [ - ValidatePulseGates( - granularity=timing_constraints.granularity, min_length=timing_constraints.min_length - ), - AlignMeasures(alignment=timing_constraints.acquire_alignment), - ] - else: - _alignments = [] - # Build pass manager pm0 = PassManager() if coupling_map or initial_layout: @@ -265,10 +233,62 @@ def _contains_delay(property_set): pm0.append(_unroll) if inst_map and inst_map.has_custom_gate(): pm0.append(PulseGates(inst_map=inst_map)) + + # 7. Unify all durations (either SI, or convert to dt if known) + # Schedule the circuit only when scheduling_method is supplied + # Apply alignment analysis regardless of scheduling for delay validation. if scheduling_method: - pm0.append(_scheduling) + # Do scheduling after unit conversion. + scheduler = { + "alap": ALAPSchedule, + "as_late_as_possible": ALAPSchedule, + "asap": ASAPSchedule, + "as_soon_as_possible": ASAPSchedule, + } + pm0.append(TimeUnitConversion(instruction_durations)) + try: + pm0.append(scheduler[scheduling_method](instruction_durations)) + except KeyError as ex: + raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex elif instruction_durations: - pm0.append(_time_unit_setup) - pm0.append(_time_unit_conversion, condition=_contains_delay) - pm0.append(_alignments) + # No scheduling. But do unit conversion for delays. + def _contains_delay(property_set): + return property_set["contains_delay"] + + pm0.append(ContainsInstruction("delay")) + pm0.append(TimeUnitConversion(instruction_durations), condition=_contains_delay) + if ( + timing_constraints.granularity != 1 + or timing_constraints.min_length != 1 + or timing_constraints.acquire_alignment != 1 + or timing_constraints.pulse_alignment != 1 + ): + # Run alignment analysis regardless of scheduling. + + def _require_alignment(property_set): + return property_set["reschedule_required"] + + pm0.append( + InstructionDurationCheck( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ) + ) + pm0.append( + ConstrainedReschedule( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ), + condition=_require_alignment, + ) + pm0.append( + ValidatePulseGates( + granularity=timing_constraints.granularity, + min_length=timing_constraints.min_length, + ) + ) + if scheduling_method: + # Call padding pass if circuit is scheduled + pm0.append(PadDelay()) + return pm0 diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 3f894484f1a6..4c08c1cdce8f 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -53,7 +53,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule -from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ConstrainedReschedule +from qiskit.transpiler.passes import InstructionDurationCheck from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import PadDelay @@ -286,39 +287,6 @@ def _opt_control(property_set): _opt = [Optimize1qGatesDecomposition(basis_gates), CXCancellation()] - # 10. Unify all durations (either SI, or convert to dt if known) - # Schedule the circuit only when scheduling_method is supplied - _time_unit_setup = [ContainsInstruction("delay")] - _time_unit_conversion = [TimeUnitConversion(instruction_durations)] - - def _contains_delay(property_set): - return property_set["contains_delay"] - - _scheduling = [] - if scheduling_method: - _scheduling += _time_unit_conversion - if scheduling_method in {"alap", "as_late_as_possible"}: - _scheduling += [ALAPSchedule(instruction_durations), PadDelay()] - elif scheduling_method in {"asap", "as_soon_as_possible"}: - _scheduling += [ASAPSchedule(instruction_durations), PadDelay()] - else: - raise TranspilerError("Invalid scheduling method %s." % scheduling_method) - - # 11. Call measure alignment. Should come after scheduling. - if ( - timing_constraints.granularity != 1 - or timing_constraints.min_length != 1 - or timing_constraints.acquire_alignment != 1 - ): - _alignments = [ - ValidatePulseGates( - granularity=timing_constraints.granularity, min_length=timing_constraints.min_length - ), - AlignMeasures(alignment=timing_constraints.acquire_alignment), - ] - else: - _alignments = [] - # Build pass manager pm1 = PassManager() if coupling_map or initial_layout: @@ -341,11 +309,62 @@ def _contains_delay(property_set): pm1.append(_opt + _unroll + _depth_check + _size_check, do_while=_opt_control) if inst_map and inst_map.has_custom_gate(): pm1.append(PulseGates(inst_map=inst_map)) + + # 10. Unify all durations (either SI, or convert to dt if known) + # Schedule the circuit only when scheduling_method is supplied + # Apply alignment analysis regardless of scheduling for delay validation. if scheduling_method: - pm1.append(_scheduling) + # Do scheduling after unit conversion. + scheduler = { + "alap": ALAPSchedule, + "as_late_as_possible": ALAPSchedule, + "asap": ASAPSchedule, + "as_soon_as_possible": ASAPSchedule, + } + pm1.append(TimeUnitConversion(instruction_durations)) + try: + pm1.append(scheduler[scheduling_method](instruction_durations)) + except KeyError as ex: + raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex elif instruction_durations: - pm1.append(_time_unit_setup) - pm1.append(_time_unit_conversion, condition=_contains_delay) - pm1.append(_alignments) + # No scheduling. But do unit conversion for delays. + def _contains_delay(property_set): + return property_set["contains_delay"] + + pm1.append(ContainsInstruction("delay")) + pm1.append(TimeUnitConversion(instruction_durations), condition=_contains_delay) + if ( + timing_constraints.granularity != 1 + or timing_constraints.min_length != 1 + or timing_constraints.acquire_alignment != 1 + or timing_constraints.pulse_alignment != 1 + ): + # Run alignment analysis regardless of scheduling. + + def _require_alignment(property_set): + return property_set["reschedule_required"] + + pm1.append( + InstructionDurationCheck( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ) + ) + pm1.append( + ConstrainedReschedule( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ), + condition=_require_alignment, + ) + pm1.append( + ValidatePulseGates( + granularity=timing_constraints.granularity, + min_length=timing_constraints.min_length, + ) + ) + if scheduling_method: + # Call padding pass if circuit is scheduled + pm1.append(PadDelay()) return pm1 diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 092ac6b0af32..53b2a3e64ce1 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -53,7 +53,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule -from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ConstrainedReschedule +from qiskit.transpiler.passes import InstructionDurationCheck from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import PadDelay @@ -274,39 +275,6 @@ def _opt_control(property_set): CommutativeCancellation(basis_gates=basis_gates), ] - # 9. Unify all durations (either SI, or convert to dt if known) - # Schedule the circuit only when scheduling_method is supplied - _time_unit_setup = [ContainsInstruction("delay")] - _time_unit_conversion = [TimeUnitConversion(instruction_durations)] - - def _contains_delay(property_set): - return property_set["contains_delay"] - - _scheduling = [] - if scheduling_method: - _scheduling += _time_unit_conversion - if scheduling_method in {"alap", "as_late_as_possible"}: - _scheduling += [ALAPSchedule(instruction_durations), PadDelay()] - elif scheduling_method in {"asap", "as_soon_as_possible"}: - _scheduling += [ASAPSchedule(instruction_durations), PadDelay()] - else: - raise TranspilerError("Invalid scheduling method %s." % scheduling_method) - - # 10. Call measure alignment. Should come after scheduling. - if ( - timing_constraints.granularity != 1 - or timing_constraints.min_length != 1 - or timing_constraints.acquire_alignment != 1 - ): - _alignments = [ - ValidatePulseGates( - granularity=timing_constraints.granularity, min_length=timing_constraints.min_length - ), - AlignMeasures(alignment=timing_constraints.acquire_alignment), - ] - else: - _alignments = [] - # Build pass manager pm2 = PassManager() if coupling_map or initial_layout: @@ -330,10 +298,62 @@ def _contains_delay(property_set): if inst_map and inst_map.has_custom_gate(): pm2.append(PulseGates(inst_map=inst_map)) + + # 9. Unify all durations (either SI, or convert to dt if known) + # Schedule the circuit only when scheduling_method is supplied + # Apply alignment analysis regardless of scheduling for delay validation. if scheduling_method: - pm2.append(_scheduling) + # Do scheduling after unit conversion. + scheduler = { + "alap": ALAPSchedule, + "as_late_as_possible": ALAPSchedule, + "asap": ASAPSchedule, + "as_soon_as_possible": ASAPSchedule, + } + pm2.append(TimeUnitConversion(instruction_durations)) + try: + pm2.append(scheduler[scheduling_method](instruction_durations)) + except KeyError as ex: + raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex elif instruction_durations: - pm2.append(_time_unit_setup) - pm2.append(_time_unit_conversion, condition=_contains_delay) - pm2.append(_alignments) + # No scheduling. But do unit conversion for delays. + def _contains_delay(property_set): + return property_set["contains_delay"] + + pm2.append(ContainsInstruction("delay")) + pm2.append(TimeUnitConversion(instruction_durations), condition=_contains_delay) + if ( + timing_constraints.granularity != 1 + or timing_constraints.min_length != 1 + or timing_constraints.acquire_alignment != 1 + or timing_constraints.pulse_alignment != 1 + ): + # Run alignment analysis regardless of scheduling. + + def _require_alignment(property_set): + return property_set["reschedule_required"] + + pm2.append( + InstructionDurationCheck( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ) + ) + pm2.append( + ConstrainedReschedule( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ), + condition=_require_alignment, + ) + pm2.append( + ValidatePulseGates( + granularity=timing_constraints.granularity, + min_length=timing_constraints.min_length, + ) + ) + if scheduling_method: + # Call padding pass if circuit is scheduled + pm2.append(PadDelay()) + return pm2 diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 94f341d911b5..b338f8b1b5e7 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -56,7 +56,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule -from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ConstrainedReschedule +from qiskit.transpiler.passes import InstructionDurationCheck from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import PadDelay @@ -284,39 +285,6 @@ def _opt_control(property_set): CommutativeCancellation(), ] - # 9. Unify all durations (either SI, or convert to dt if known) - # Schedule the circuit only when scheduling_method is supplied - _time_unit_setup = [ContainsInstruction("delay")] - _time_unit_conversion = [TimeUnitConversion(instruction_durations)] - - def _contains_delay(property_set): - return property_set["contains_delay"] - - _scheduling = [] - if scheduling_method: - _scheduling += _time_unit_conversion - if scheduling_method in {"alap", "as_late_as_possible"}: - _scheduling += [ALAPSchedule(instruction_durations), PadDelay()] - elif scheduling_method in {"asap", "as_soon_as_possible"}: - _scheduling += [ASAPSchedule(instruction_durations), PadDelay()] - else: - raise TranspilerError("Invalid scheduling method %s." % scheduling_method) - - # 10. Call measure alignment. Should come after scheduling. - if ( - timing_constraints.granularity != 1 - or timing_constraints.min_length != 1 - or timing_constraints.acquire_alignment != 1 - ): - _alignments = [ - ValidatePulseGates( - granularity=timing_constraints.granularity, min_length=timing_constraints.min_length - ), - AlignMeasures(alignment=timing_constraints.acquire_alignment), - ] - else: - _alignments = [] - # Build pass manager pm3 = PassManager() pm3.append(_unroll3q) @@ -352,11 +320,62 @@ def _contains_delay(property_set): pm3.append(_opt + _unroll + _depth_check + _size_check, do_while=_opt_control) if inst_map and inst_map.has_custom_gate(): pm3.append(PulseGates(inst_map=inst_map)) + + # 9. Unify all durations (either SI, or convert to dt if known) + # Schedule the circuit only when scheduling_method is supplied + # Apply alignment analysis regardless of scheduling for delay validation. if scheduling_method: - pm3.append(_scheduling) + # Do scheduling after unit conversion. + scheduler = { + "alap": ALAPSchedule, + "as_late_as_possible": ALAPSchedule, + "asap": ASAPSchedule, + "as_soon_as_possible": ASAPSchedule, + } + pm3.append(TimeUnitConversion(instruction_durations)) + try: + pm3.append(scheduler[scheduling_method](instruction_durations)) + except KeyError as ex: + raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex elif instruction_durations: - pm3.append(_time_unit_setup) - pm3.append(_time_unit_conversion, condition=_contains_delay) - pm3.append(_alignments) + # No scheduling. But do unit conversion for delays. + def _contains_delay(property_set): + return property_set["contains_delay"] + + pm3.append(ContainsInstruction("delay")) + pm3.append(TimeUnitConversion(instruction_durations), condition=_contains_delay) + if ( + timing_constraints.granularity != 1 + or timing_constraints.min_length != 1 + or timing_constraints.acquire_alignment != 1 + or timing_constraints.pulse_alignment != 1 + ): + # Run alignment analysis regardless of scheduling. + + def _require_alignment(property_set): + return property_set["reschedule_required"] + + pm3.append( + InstructionDurationCheck( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ) + ) + pm3.append( + ConstrainedReschedule( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ), + condition=_require_alignment, + ) + pm3.append( + ValidatePulseGates( + granularity=timing_constraints.granularity, + min_length=timing_constraints.min_length, + ) + ) + if scheduling_method: + # Call padding pass if circuit is scheduled + pm3.append(PadDelay()) return pm3 diff --git a/qiskit/transpiler/runningpassmanager.py b/qiskit/transpiler/runningpassmanager.py index 5e4e5ff02b15..43af938d1879 100644 --- a/qiskit/transpiler/runningpassmanager.py +++ b/qiskit/transpiler/runningpassmanager.py @@ -129,6 +129,8 @@ def run(self, circuit, output_name=None, callback=None): else: circuit.name = name circuit._layout = self.property_set["layout"] + circuit._clbit_write_latency = self.property_set["clbit_write_latency"] + circuit._conditional_latency = self.property_set["conditional_latency"] return circuit diff --git a/releasenotes/notes/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml b/releasenotes/notes/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml new file mode 100644 index 000000000000..3c134ef5a68d --- /dev/null +++ b/releasenotes/notes/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml @@ -0,0 +1,31 @@ +--- +features: + - | + New pass group :mod:`qiskit.transpiler.passes.scheduling.alignments` has been added. + This consists of :class:`ConstrainedReschedule`, :class:`ValidatePulseGates`, + and :class:`InstructionDurationCheck`. The new pass :class:`ConstrainedReschedule` + considers both hardware alignment constraints of ``pulse_alignment`` and ``acquire_alignment``, + which is a drop-in replacement of :class:`AlignMeasures`. +upgrade: + - | + The ordering of scheduling-relevant passes in the preset pass manager has been upgraded. + Note that now scheduling passes and alignment passes are all sub-class of the + :class:`AnalysisPass` that only updates the property set of the pass manager, + in which new property set ``node_start_time`` is created there to perform + these operation on nodes scheduled on the absolute time. + + Previously, the pass chain had been implemented as ``scheduling -> alignment`` + which were both transform passes thus there were multiple DAG instance regeneration. + In addition, scheduling occured in each pass to obtain instruction start time. + + Now this chain becomes ``scheduling -> alignment -> padding`` + where the DAG replacement only occurs at the end, i.e. at the ``padding`` pass. + Note that who wants to design a new pass manager must insert one of the padding passes + at the end of the pass chain. Without the padding pass, the scheduled nodes + are not reflected in the target code since the QASM payload has no notion of + the scheduling for underlying instructions. +deprecations: + - | + Alignment pass :class:`AlignMeasures` has been deprecated and will be removed. + The constructor of this pass now internally calls :class:`ConstrainedReschedule` + and returns the instance of its instance. diff --git a/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml b/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml index 6a958d5ba268..4d9f597e49f1 100644 --- a/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml +++ b/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml @@ -1,22 +1,30 @@ --- +features: + - | + New pass :class:`SetIOLatency` has been added. This pass takes + ``clbit_write_latency`` and ``conditional_latency`` and set these values to + property set of the pass manager. + Following passes that perform scheduling on instruction acting on classical resgisters + may consider these classical I/O latencies to provide more presice scheduling on + dynamic circuit. upgrade: - | The circuit scheduling passes :class:`~qiskit.transpiler.passes.scheduling.asap.ASAPSchedule` and :class:`~qiskit.transpiler.passes.scheduling.alap.ALAPSchedule` have been upgraded. - Now these passes can be instantiated with two new parameters - ``clbit_write_latency`` and ``conditional_latency``, which allows scheduler to - more carefully schedule instructions with classical feedback. - The former option represents a latency of clbit access from the begging of the - measurement instruction, and the other represents a latency of the - conditional gate operation from the first conditional clbit read-access. + + Now these passes consider I/O latencies for measurement and conditional operations. + These information should be set by :class:`SetIOLatency` pass in advance. + The I/O latency to write classical register is represneted by ``clbit_write_latency``, + and one for reading the register is presented by ``conditional_latency``. + These properties are loaded from backend and set to the pass manager's property set. The standard behavior of these passes has been also upgraded to align timing ordering with the topological ordering of the DAG nodes. This change may affect the scheduling outcome if it includes conditional operations, or simultaneously measuring two qubits with the same classical register (edge-case). - To reproduce conventional behavior, create a pass manager with one of - scheduling passes with ``clbit_write_latency`` identical to the measurement instruction length. + To reproduce conventional behavior, set ``clbit_write_latency`` identical + to the measurement instruction length. The following example may clearly explain the change of scheduler spec. @@ -38,7 +46,7 @@ upgrade: from qiskit import QuantumCircuit from qiskit.transpiler import InstructionDurations, PassManager - from qiskit.transpiler.passes import ALAPSchedule, PadDelay + from qiskit.transpiler.passes import ALAPSchedule, PadDelay, SetIOLatency from qiskit.visualization.timeline import draw circuit = QuantumCircuit(3, 1) @@ -51,7 +59,8 @@ upgrade: pm = PassManager( [ - ALAPSchedule(durations, clbit_write_latency=800, conditional_latency=0), + SetIOLatency(clbit_write_latency=800, conditional_latency=0), + ALAPSchedule(durations), PadDelay(), ] ) diff --git a/test/python/transpiler/test_instruction_alignments.py b/test/python/transpiler/test_instruction_alignments.py index 233fea1efd43..845566d17682 100644 --- a/test/python/transpiler/test_instruction_alignments.py +++ b/test/python/transpiler/test_instruction_alignments.py @@ -18,9 +18,13 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import ( AlignMeasures, + InstructionDurationCheck, + ConstrainedReschedule, ValidatePulseGates, ALAPSchedule, + ASAPSchedule, PadDelay, + SetIOLatency, ) @@ -75,13 +79,10 @@ def test_t1_experiment_type(self): [ # reproduce old behavior of 0.20.0 before #7655 # currently default write latency is 0 - ALAPSchedule( - durations=self.instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ), + SetIOLatency(clbit_write_latency=1600, conditional_latency=0), + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(acquire_alignment=16), PadDelay(), - AlignMeasures(alignment=16), ] ) @@ -129,13 +130,10 @@ def test_hanh_echo_experiment_type(self): [ # reproduce old behavior of 0.20.0 before #7655 # currently default write latency is 0 - ALAPSchedule( - durations=self.instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ), + SetIOLatency(clbit_write_latency=1600, conditional_latency=0), + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(acquire_alignment=16), PadDelay(), - AlignMeasures(alignment=16), ] ) @@ -187,13 +185,10 @@ def test_mid_circuit_measure(self): [ # reproduce old behavior of 0.20.0 before #7655 # currently default write latency is 0 - ALAPSchedule( - durations=self.instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ), + SetIOLatency(clbit_write_latency=1600, conditional_latency=0), + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(acquire_alignment=16), PadDelay(), - AlignMeasures(alignment=16), ] ) @@ -256,13 +251,10 @@ def test_mid_circuit_multiq_gates(self): [ # reproduce old behavior of 0.20.0 before #7655 # currently default write latency is 0 - ALAPSchedule( - durations=self.instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ), + SetIOLatency(clbit_write_latency=1600, conditional_latency=0), + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(acquire_alignment=16), PadDelay(), - AlignMeasures(alignment=16), ] ) @@ -295,10 +287,12 @@ def test_alignment_is_not_processed(self): # pre scheduling is not necessary because alignment is skipped # this is to minimize breaking changes to existing code. - pm = PassManager(AlignMeasures(alignment=16)) - aligned_circuit = pm.run(circuit) + pm = PassManager() + + pm.append(InstructionDurationCheck(acquire_alignment=16)) + pm.run(circuit) - self.assertEqual(aligned_circuit, circuit) + self.assertFalse(pm.property_set["reschedule_required"]) def test_circuit_using_clbit(self): """Test a circuit with instructions using a common clbit. @@ -342,13 +336,10 @@ def test_circuit_using_clbit(self): [ # reproduce old behavior of 0.20.0 before #7655 # currently default write latency is 0 - ALAPSchedule( - durations=self.instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ), - PadDelay(), - AlignMeasures(alignment=16), + SetIOLatency(clbit_write_latency=1600, conditional_latency=0), + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(acquire_alignment=16), + PadDelay(fill_very_end=False), ] ) @@ -363,19 +354,174 @@ def test_circuit_using_clbit(self): ref_circuit.delay(432, 2, unit="dt") # 2032 - 1600 ref_circuit.measure(0, 0) ref_circuit.x(1).c_if(0, 1) - ref_circuit.delay(160, 0, unit="dt") ref_circuit.measure(2, 0) self.assertEqual(aligned_circuit, ref_circuit) + def test_programmed_delay_preserved(self): + """Intentionally programmed delay will be kept after reschedule. + + No delay + ++++++++ + + (input) + ┌────────────────┐┌───┐ ░ ┌───┐ + q_0: ┤ Delay(100[dt]) ├┤ X ├─░─┤ X ├ + ├────────────────┤└───┘ ░ └───┘ + q_1: ┤ Delay(272[dt]) ├──────░────── + └────────────────┘ ░ + + (aligned) + ┌────────────────┐┌───┐ ░ ┌───┐ + q_0: ┤ Delay(112[dt]) ├┤ X ├─░─┤ X ├ + ├────────────────┤└───┘ ░ └───┘ + q_1: ┤ Delay(272[dt]) ├──────░────── + └────────────────┘ ░ + + With delay (intentional post buffer) + ++++++++++++++++++++++++++++++++++++ + + (input) ... this is identical to no delay pattern without reschedule + ┌────────────────┐┌───┐┌───────────────┐ ░ ┌───┐ + q_0: ┤ Delay(100[dt]) ├┤ X ├┤ Delay(10[dt]) ├─░─┤ X ├ + ├────────────────┤└───┘└───────────────┘ ░ └───┘ + q_1: ┤ Delay(272[dt]) ├───────────────────────░────── + └────────────────┘ ░ + + (aligned) + ┌────────────────┐┌───┐┌───────────────┐ ░ ┌──────────────┐┌───┐ + q_0: ┤ Delay(112[dt]) ├┤ X ├┤ Delay(10[dt]) ├─░─┤ Delay(6[dt]) ├┤ X ├ + ├────────────────┤└───┘└───────────────┘ ░ └──────────────┘└───┘ + q_1: ┤ Delay(282[dt]) ├───────────────────────░────────────────────── + └────────────────┘ ░ + + """ + + pm = PassManager( + [ + ASAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(pulse_alignment=16), + PadDelay(fill_very_end=False), + ] + ) + + pm_only_schedule = PassManager( + [ + ASAPSchedule(durations=self.instruction_durations), + PadDelay(fill_very_end=False), + ] + ) + + circuit_no_delay = QuantumCircuit(2) + circuit_no_delay.delay(100, 0, unit="dt") + circuit_no_delay.x(0) # q0 ends here at t = 260, t = 260 - 272 is free + circuit_no_delay.delay(160 + 112, 1, unit="dt") + circuit_no_delay.barrier() # q0 and q1 is aligned here at t = 272 dt + circuit_no_delay.x(0) + + ref_no_delay = QuantumCircuit(2) + ref_no_delay.delay(112, 0, unit="dt") + ref_no_delay.x(0) + ref_no_delay.delay(160 + 100 + 12, 1, unit="dt") # this t0 doesn't change + ref_no_delay.barrier() + ref_no_delay.x(0) # no buffer + + self.assertEqual(pm.run(circuit_no_delay), ref_no_delay) + + circuit_with_delay = QuantumCircuit(2) + circuit_with_delay.delay(100, 0, unit="dt") + circuit_with_delay.x(0) # q0 ends here at t = 260 + circuit_with_delay.delay(10, 0, unit="dt") # intentional post buffer of 10 dt to next X(0) + circuit_with_delay.delay(160 + 112, 1, unit="dt") # q0 and q1 is aligned here at t = 272 dt + circuit_with_delay.barrier() + circuit_with_delay.x(0) + + ref_with_delay = QuantumCircuit(2) + ref_with_delay.delay(112, 0, unit="dt") + ref_with_delay.x(0) + ref_with_delay.delay(10, 0, unit="dt") # this delay survive + ref_with_delay.delay(160 + 100 + 12 + 10, 1, unit="dt") + ref_with_delay.barrier() + ref_with_delay.delay(6, 0, unit="dt") # extra delay for next X0 + ref_with_delay.x(0) # at least 10dt buffer is preserved + + self.assertEqual(pm.run(circuit_with_delay), ref_with_delay) + + # check if circuit is identical without reschedule + self.assertEqual( + pm_only_schedule.run(circuit_no_delay), + pm_only_schedule.run(circuit_with_delay), + ) + + def test_both_pulse_and_acquire_alignment(self): + """Test when both acquire and pulse alignment are specified. + + (input) + ┌────────────────┐┌───┐┌───────────────┐┌─┐ + q: ┤ Delay(100[dt]) ├┤ X ├┤ Delay(10[dt]) ├┤M├ + └────────────────┘└───┘└───────────────┘└╥┘ + c: 1/═════════════════════════════════════════╩═ + 0 + + (aligned) + ┌────────────────┐┌───┐┌───────────────┐┌─┐ + q: ┤ Delay(112[dt]) ├┤ X ├┤ Delay(16[dt]) ├┤M├ + └────────────────┘└───┘└───────────────┘└╥┘ + c: 1/═════════════════════════════════════════╩═ + 0 + """ + pm = PassManager( + [ + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(pulse_alignment=16, acquire_alignment=16), + PadDelay(fill_very_end=False), + ] + ) + + circuit = QuantumCircuit(1, 1) + circuit.delay(100, 0, unit="dt") + circuit.x(0) + circuit.delay(10, 0, unit="dt") + circuit.measure(0, 0) + + ref_circ = QuantumCircuit(1, 1) + ref_circ.delay(112, 0, unit="dt") + ref_circ.x(0) + ref_circ.delay(16, 0, unit="dt") + ref_circ.measure(0, 0) + + self.assertEqual(pm.run(circuit), ref_circ) + + def test_deprecated_align_measure(self): + """Test if old AlignMeasures can be still used and warning is raised.""" + circuit = QuantumCircuit(1, 1) + circuit.x(0) + circuit.delay(100) + circuit.measure(0, 0) + + with self.assertWarns(FutureWarning): + pm_old = PassManager( + [ + ALAPSchedule(durations=self.instruction_durations), + AlignMeasures(alignment=16), + PadDelay(fill_very_end=False), + ] + ) + + pm_new = PassManager( + [ + ALAPSchedule(durations=self.instruction_durations), + AlignMeasures(alignment=16), + PadDelay(fill_very_end=False), + ] + ) + + self.assertEqual(pm_old.run(circuit), pm_new.run(circuit)) + class TestPulseGateValidation(QiskitTestCase): """A test for pulse gate validation pass.""" - def setUp(self): - super().setUp() - self.pulse_gate_validation_pass = ValidatePulseGates(granularity=16, min_length=64) - def test_invalid_pulse_duration(self): """Kill pass manager if invalid pulse gate is found.""" @@ -390,8 +536,9 @@ def test_invalid_pulse_duration(self): circuit.x(0) circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) + pm.run(circuit) def test_short_pulse_duration(self): """Kill pass manager if invalid pulse gate is found.""" @@ -407,8 +554,9 @@ def test_short_pulse_duration(self): circuit.x(0) circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) + pm.run(circuit) def test_short_pulse_duration_multiple_pulse(self): """Kill pass manager if invalid pulse gate is found.""" @@ -428,8 +576,9 @@ def test_short_pulse_duration_multiple_pulse(self): circuit.x(0) circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) + pm.run(circuit) def test_valid_pulse_duration(self): """No error raises if valid calibration is provided.""" @@ -445,7 +594,8 @@ def test_valid_pulse_duration(self): circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) # just not raise an error - self.pulse_gate_validation_pass(circuit) + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) + pm.run(circuit) def test_no_calibration(self): """No error raises if no calibration is addedd.""" @@ -454,4 +604,5 @@ def test_no_calibration(self): circuit.x(0) # just not raise an error - self.pulse_gate_validation_pass(circuit) + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) + pm.run(circuit) diff --git a/test/python/transpiler/test_scheduling_padding_pass.py b/test/python/transpiler/test_scheduling_padding_pass.py index e3dfc2809167..174b2798674d 100644 --- a/test/python/transpiler/test_scheduling_padding_pass.py +++ b/test/python/transpiler/test_scheduling_padding_pass.py @@ -19,7 +19,7 @@ from qiskit.pulse import Schedule, Play, Constant, DriveChannel from qiskit.test import QiskitTestCase from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule, PadDelay +from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule, PadDelay, SetIOLatency from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError @@ -444,10 +444,18 @@ def test_measure_after_c_if_on_edge_locking(self): # lock at the end edge actual_asap = PassManager( - [ASAPSchedule(durations, clbit_write_latency=1000), PadDelay()] + [ + SetIOLatency(clbit_write_latency=1000), + ASAPSchedule(durations), + PadDelay(), + ] ).run(qc) actual_alap = PassManager( - [ALAPSchedule(durations, clbit_write_latency=1000), PadDelay()] + [ + SetIOLatency(clbit_write_latency=1000), + ALAPSchedule(durations), + PadDelay(), + ] ).run(qc) # start times of 2nd measure depends on ASAP/ALAP @@ -496,19 +504,19 @@ def test_active_reset_circuit(self, write_lat, cond_lat): qc.x(0).c_if(0, 1) durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) + actual_asap = PassManager( [ - ASAPSchedule( - durations, clbit_write_latency=write_lat, conditional_latency=cond_lat - ), + SetIOLatency(clbit_write_latency=write_lat, conditional_latency=cond_lat), + ASAPSchedule(durations), PadDelay(), ] ).run(qc) + actual_alap = PassManager( [ - ALAPSchedule( - durations, clbit_write_latency=write_lat, conditional_latency=cond_lat - ), + SetIOLatency(clbit_write_latency=write_lat, conditional_latency=cond_lat), + ALAPSchedule(durations), PadDelay(), ] ).run(qc) @@ -630,10 +638,19 @@ def test_random_complicated_circuit(self): ) actual_asap = PassManager( - [ASAPSchedule(durations, clbit_write_latency=100, conditional_latency=200), PadDelay()] + [ + SetIOLatency(clbit_write_latency=100, conditional_latency=200), + ASAPSchedule(durations), + PadDelay(), + ] ).run(qc) + actual_alap = PassManager( - [ALAPSchedule(durations, clbit_write_latency=100, conditional_latency=200), PadDelay()] + [ + SetIOLatency(clbit_write_latency=100, conditional_latency=200), + ALAPSchedule(durations), + PadDelay(), + ] ).run(qc) expected_asap = QuantumCircuit(3, 1)