diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index f461f335373e..8568b8eac1a2 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -117,6 +117,10 @@ ValidatePulseGates InstructionDurationCheck SetIOLatency + ALAPSchedule + ASAPSchedule + DynamicalDecoupling + AlignMeasures Circuit Analysis ================ @@ -267,6 +271,10 @@ from .scheduling import ConstrainedReschedule from .scheduling import InstructionDurationCheck from .scheduling import SetIOLatency +from .scheduling import ALAPSchedule +from .scheduling import ASAPSchedule +from .scheduling import DynamicalDecoupling +from .scheduling import AlignMeasures # additional utility passes from .utils import CheckMap diff --git a/qiskit/transpiler/passes/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/__init__.py index 69485e88e4cf..6283faff0001 100644 --- a/qiskit/transpiler/passes/scheduling/__init__.py +++ b/qiskit/transpiler/passes/scheduling/__init__.py @@ -12,6 +12,9 @@ """Module containing circuit scheduling passes.""" +from .alap import ALAPSchedule +from .asap import ASAPSchedule +from .dynamical_decoupling import DynamicalDecoupling from .scheduling import ALAPScheduleAnalysis, ASAPScheduleAnalysis, SetIOLatency from .time_unit_conversion import TimeUnitConversion from .padding import PadDelay, PadDynamicalDecoupling @@ -21,3 +24,4 @@ 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/alap.py b/qiskit/transpiler/passes/scheduling/alap.py new file mode 100644 index 000000000000..9ee0f4988b4a --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -0,0 +1,155 @@ +# 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. + +"""ALAP Scheduling.""" + +from qiskit.circuit import Delay, Qubit, Measure +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils.deprecation import deprecate_func + +from .base_scheduler import BaseSchedulerTransform + + +class ALAPSchedule(BaseSchedulerTransform): + """ALAP Scheduling pass, which schedules the **stop** time of instructions as late as possible. + + See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseSchedulerTransform` for the + detailed behavior of the control flow operation, i.e. ``c_if``. + """ + + @deprecate_func( + additional_msg=( + "Instead, use :class:`~.ALAPScheduleAnalysis`, which is an " + "analysis pass that requires a padding pass to later modify the circuit." + ), + since="0.21.0", + pending=True, + ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def run(self, dag): + """Run the ALAPSchedule pass on `dag`. + + Args: + dag (DAGCircuit): DAG to schedule. + + Returns: + DAGCircuit: A scheduled DAG. + + Raises: + TranspilerError: if the circuit is not mapped on physical qubits. + TranspilerError: if conditional bit is added to non-supported instruction. + """ + if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: + raise TranspilerError("ALAP schedule runs on physical circuits only") + + time_unit = self.property_set["time_unit"] + new_dag = DAGCircuit() + for qreg in dag.qregs.values(): + new_dag.add_qreg(qreg) + for creg in dag.cregs.values(): + new_dag.add_creg(creg) + + idle_before = {q: 0 for q in dag.qubits + dag.clbits} + for node in reversed(list(dag.topological_op_nodes())): + op_duration = self._get_node_duration(node, dag) + + # compute t0, t1: instruction interval, note that + # t0: start time of instruction + # t1: end time of instruction + + # since this is alap scheduling, node is scheduled in reversed topological ordering + # and nodes are packed from the very end of the circuit. + # the physical meaning of t0 and t1 is flipped here. + if isinstance(node.op, self.CONDITIONAL_SUPPORTED): + t0q = max(idle_before[q] for q in node.qargs) + if node.op.condition_bits: + # conditional is bit tricky due to conditional_latency + t0c = max(idle_before[c] for c in node.op.condition_bits) + # Assume following case (t0c > t0q): + # + # |t0q + # Q ░░░░░░░░░░░░░▒▒▒ + # C ░░░░░░░░▒▒▒▒▒▒▒▒ + # |t0c + # + # In this case, there is no actual clbit read before gate. + # + # |t0q' = t0c - conditional_latency + # Q ░░░░░░░░▒▒▒░░▒▒▒ + # C ░░░░░░▒▒▒▒▒▒▒▒▒▒ + # |t1c' = t0c + conditional_latency + # + # rather than naively doing + # + # |t1q' = t0c + duration + # Q ░░░░░▒▒▒░░░░░▒▒▒ + # C ░░▒▒░░░░▒▒▒▒▒▒▒▒ + # |t1c' = t0c + duration + conditional_latency + # + t0 = max(t0q, t0c - op_duration) + t1 = t0 + op_duration + for clbit in node.op.condition_bits: + idle_before[clbit] = t1 + self.conditional_latency + else: + t0 = t0q + t1 = t0 + op_duration + else: + if node.op.condition_bits: + raise TranspilerError( + f"Conditional instruction {node.op.name} is not supported in ALAP scheduler." + ) + + if isinstance(node.op, Measure): + # clbit time is always right (alap) justified + t0 = max(idle_before[bit] for bit in node.qargs + node.cargs) + t1 = t0 + op_duration + # + # |t1 = t0 + duration + # Q ░░░░░▒▒▒▒▒▒▒▒▒▒▒ + # C ░░░░░░░░░▒▒▒▒▒▒▒ + # |t0 + (duration - clbit_write_latency) + # + for clbit in node.cargs: + idle_before[clbit] = t0 + (op_duration - self.clbit_write_latency) + else: + # It happens to be directives such as barrier + t0 = max(idle_before[bit] for bit in node.qargs + node.cargs) + t1 = t0 + op_duration + + for bit in node.qargs: + delta = t0 - idle_before[bit] + if delta > 0 and self._delay_supported(dag.find_bit(bit).index): + new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [], check=False) + idle_before[bit] = t1 + + new_dag.apply_operation_front(node.op, node.qargs, node.cargs, check=False) + + circuit_duration = max(idle_before.values()) + for bit, before in idle_before.items(): + delta = circuit_duration - before + if not (delta > 0 and isinstance(bit, Qubit)): + continue + if self._delay_supported(dag.find_bit(bit).index): + new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [], check=False) + + new_dag.name = dag.name + new_dag.metadata = dag.metadata + new_dag.calibrations = dag.calibrations + + # set circuit duration and unit to indicate it is scheduled + new_dag.duration = circuit_duration + new_dag.unit = time_unit + + return new_dag diff --git a/qiskit/transpiler/passes/scheduling/alignments/__init__.py b/qiskit/transpiler/passes/scheduling/alignments/__init__.py index a25ec01bc1cf..513144937ab5 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/__init__.py +++ b/qiskit/transpiler/passes/scheduling/alignments/__init__.py @@ -78,3 +78,4 @@ 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..668d65f6abd5 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py @@ -0,0 +1,256 @@ +# 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.""" +from __future__ import annotations +import itertools +import warnings +from collections import defaultdict +from collections.abc import Iterable +from typing import Type + +from qiskit.circuit.quantumcircuit import ClbitSpecifier, QubitSpecifier + +from qiskit.circuit.delay import Delay +from qiskit.circuit.measure import Measure +from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils.deprecation import deprecate_func + + +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. + """ + + @deprecate_func( + additional_msg=( + "Instead, use :class:`~.ConstrainedReschedule`, which performs the same function " + "but also supports aligning to additional timing constraints." + ), + since="0.21.0", + pending=True, + ) + 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_empty_like() + + qubit_time_available: dict[QubitSpecifier, int] = defaultdict(int) # to track op start time + qubit_stop_times: dict[QubitSpecifier, int] = defaultdict( + int + ) # to track delay start time for padding + clbit_readable: dict[ClbitSpecifier, int] = defaultdict(int) + clbit_writeable: dict[ClbitSpecifier, int] = defaultdict(int) + + def pad_with_delays(qubits: Iterable[QubitSpecifier], 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,), check=False) + + 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 + tuple(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, check=False) + + 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 + + +def _check_alignment_required( + dag: DAGCircuit, + alignment: int, + instructions: Type | list[Type], +) -> 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/asap.py b/qiskit/transpiler/passes/scheduling/asap.py new file mode 100644 index 000000000000..cebc32af71a8 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -0,0 +1,177 @@ +# 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. + +"""ASAP Scheduling.""" + +from qiskit.circuit import Delay, Qubit, Measure +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils.deprecation import deprecate_func + +from .base_scheduler import BaseSchedulerTransform + + +class ASAPSchedule(BaseSchedulerTransform): + """ASAP Scheduling pass, which schedules the start time of instructions as early as possible.. + + See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseSchedulerTransform` for the + detailed behavior of the control flow operation, i.e. ``c_if``. + + .. note:: + + This base class has been superseded by :class:`~.ASAPScheduleAnalysis` and + the new scheduling workflow. It will be deprecated and subsequently + removed in a future release. + """ + + @deprecate_func( + additional_msg=( + "Instead, use :class:`~.ASAPScheduleAnalysis`, which is an " + "analysis pass that requires a padding pass to later modify the circuit." + ), + since="0.21.0", + pending=True, + ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def run(self, dag): + """Run the ASAPSchedule pass on `dag`. + + Args: + dag (DAGCircuit): DAG to schedule. + + Returns: + DAGCircuit: A scheduled DAG. + + Raises: + TranspilerError: if the circuit is not mapped on physical qubits. + TranspilerError: if conditional bit is added to non-supported instruction. + """ + if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: + raise TranspilerError("ASAP schedule runs on physical circuits only") + + time_unit = self.property_set["time_unit"] + + new_dag = DAGCircuit() + for qreg in dag.qregs.values(): + new_dag.add_qreg(qreg) + for creg in dag.cregs.values(): + new_dag.add_creg(creg) + + idle_after = {q: 0 for q in dag.qubits + dag.clbits} + for node in dag.topological_op_nodes(): + op_duration = self._get_node_duration(node, dag) + + # compute t0, t1: instruction interval, note that + # t0: start time of instruction + # t1: end time of instruction + if isinstance(node.op, self.CONDITIONAL_SUPPORTED): + t0q = max(idle_after[q] for q in node.qargs) + if node.op.condition_bits: + # conditional is bit tricky due to conditional_latency + t0c = max(idle_after[bit] for bit in node.op.condition_bits) + if t0q > t0c: + # This is situation something like below + # + # |t0q + # Q ▒▒▒▒▒▒▒▒▒░░ + # C ▒▒▒░░░░░░░░ + # |t0c + # + # In this case, you can insert readout access before tq0 + # + # |t0q + # Q ▒▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒░░░▒▒░░░ + # |t0q - conditional_latency + # + t0c = max(t0q - self.conditional_latency, t0c) + t1c = t0c + self.conditional_latency + for bit in node.op.condition_bits: + # Lock clbit until state is read + idle_after[bit] = t1c + # It starts after register read access + t0 = max(t0q, t1c) + else: + t0 = t0q + t1 = t0 + op_duration + else: + if node.op.condition_bits: + raise TranspilerError( + f"Conditional instruction {node.op.name} is not supported in ASAP scheduler." + ) + + if isinstance(node.op, Measure): + # measure instruction handling is bit tricky due to clbit_write_latency + t0q = max(idle_after[q] for q in node.qargs) + t0c = max(idle_after[c] for c in node.cargs) + # Assume following case (t0c > t0q) + # + # |t0q + # Q ▒▒▒▒░░░░░░░░░░░░ + # C ▒▒▒▒▒▒▒▒░░░░░░░░ + # |t0c + # + # In this case, there is no actual clbit access until clbit_write_latency. + # The node t0 can be push backward by this amount. + # + # |t0q' = t0c - clbit_write_latency + # Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + # |t0c' = t0c + # + # rather than naively doing + # + # |t0q' = t0c + # Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒ + # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ + # |t0c' = t0c + clbit_write_latency + # + t0 = max(t0q, t0c - self.clbit_write_latency) + t1 = t0 + op_duration + for clbit in node.cargs: + idle_after[clbit] = t1 + else: + # It happens to be directives such as barrier + t0 = max(idle_after[bit] for bit in node.qargs + node.cargs) + t1 = t0 + op_duration + + # Add delay to qubit wire + for bit in node.qargs: + delta = t0 - idle_after[bit] + if ( + delta > 0 + and isinstance(bit, Qubit) + and self._delay_supported(dag.find_bit(bit).index) + ): + new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) + idle_after[bit] = t1 + + new_dag.apply_operation_back(node.op, node.qargs, node.cargs) + + circuit_duration = max(idle_after.values()) + for bit, after in idle_after.items(): + delta = circuit_duration - after + if not (delta > 0 and isinstance(bit, Qubit)): + continue + if self._delay_supported(dag.find_bit(bit).index): + new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) + + new_dag.name = dag.name + new_dag.metadata = dag.metadata + new_dag.calibrations = dag.calibrations + + # set circuit duration and unit to indicate it is scheduled + new_dag.duration = circuit_duration + new_dag.unit = time_unit + return new_dag diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py new file mode 100644 index 000000000000..87bbdbff919c --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -0,0 +1,293 @@ +# 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. + +"""Dynamical Decoupling insertion pass.""" + +import itertools + +import numpy as np +from qiskit.circuit import Gate, Delay, Reset +from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate +from qiskit.dagcircuit import DAGOpNode, DAGInNode +from qiskit.quantum_info.operators.predicates import matrix_equal +from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer +from qiskit.transpiler.passes.optimization import Optimize1qGates +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils.deprecation import deprecate_func + + +class DynamicalDecoupling(TransformationPass): + """Dynamical decoupling insertion pass. + + This pass works on a scheduled, physical circuit. It scans the circuit for + idle periods of time (i.e. those containing delay instructions) and inserts + a DD sequence of gates in those spots. These gates amount to the identity, + so do not alter the logical action of the circuit, but have the effect of + mitigating decoherence in those idle periods. + + As a special case, the pass allows a length-1 sequence (e.g. [XGate()]). + In this case the DD insertion happens only when the gate inverse can be + absorbed into a neighboring gate in the circuit (so we would still be + replacing Delay with something that is equivalent to the identity). + This can be used, for instance, as a Hahn echo. + + This pass ensures that the inserted sequence preserves the circuit exactly + (including global phase). + + .. plot:: + :include-source: + + import numpy as np + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import XGate + from qiskit.transpiler import PassManager, InstructionDurations + from qiskit.transpiler.passes import ALAPSchedule, DynamicalDecoupling + from qiskit.visualization import timeline_drawer + + # Because the legacy passes do not propagate the scheduling information correctly, it is + # necessary to run a no-op "re-schedule" before the output circuits can be drawn. + def draw(circuit): + from qiskit import transpile + + scheduled = transpile( + circuit, + optimization_level=0, + instruction_durations=InstructionDurations(), + scheduling_method="alap", + ) + return timeline_drawer(scheduled) + + circ = QuantumCircuit(4) + circ.h(0) + circ.cx(0, 1) + circ.cx(1, 2) + circ.cx(2, 3) + circ.measure_all() + durations = InstructionDurations( + [("h", 0, 50), ("cx", [0, 1], 700), ("reset", None, 10), + ("cx", [1, 2], 200), ("cx", [2, 3], 300), + ("x", None, 50), ("measure", None, 1000)] + ) + # balanced X-X sequence on all qubits + dd_sequence = [XGate(), XGate()] + pm = PassManager([ALAPSchedule(durations), + DynamicalDecoupling(durations, dd_sequence)]) + circ_dd = pm.run(circ) + draw(circ_dd) + + # Uhrig sequence on qubit 0 + n = 8 + dd_sequence = [XGate()] * n + def uhrig_pulse_location(k): + return np.sin(np.pi * (k + 1) / (2 * n + 2)) ** 2 + spacing = [] + for k in range(n): + spacing.append(uhrig_pulse_location(k) - sum(spacing)) + spacing.append(1 - sum(spacing)) + pm = PassManager( + [ + ALAPSchedule(durations), + DynamicalDecoupling(durations, dd_sequence, qubits=[0], spacing=spacing), + ] + ) + circ_dd = pm.run(circ) + draw(circ_dd) + """ + + @deprecate_func( + additional_msg=( + "Instead, use :class:`~.PadDynamicalDecoupling`, which performs the same " + "function but requires scheduling and alignment analysis passes to run prior to it." + ), + since="0.21.0", + pending=True, + ) + def __init__( + self, durations, dd_sequence, qubits=None, spacing=None, skip_reset_qubits=True, target=None + ): + """Dynamical decoupling initializer. + + Args: + durations (InstructionDurations): Durations of instructions to be + used in scheduling. + dd_sequence (list[Gate]): sequence of gates to apply in idle spots. + qubits (list[int]): physical qubits on which to apply DD. + If None, all qubits will undergo DD (when possible). + spacing (list[float]): a list of spacings between the DD gates. + The available slack will be divided according to this. + The list length must be one more than the length of dd_sequence, + and the elements must sum to 1. If None, a balanced spacing + will be used [d/2, d, d, ..., d, d, d/2]. + skip_reset_qubits (bool): if True, does not insert DD on idle + periods that immediately follow initialized/reset qubits (as + qubits in the ground state are less susceptile to decoherence). + target (Target): The :class:`~.Target` representing the target backend, if both + ``durations`` and this are specified then this argument will take + precedence and ``durations`` will be ignored. + """ + super().__init__() + self._durations = durations + self._dd_sequence = dd_sequence + self._qubits = qubits + self._spacing = spacing + self._skip_reset_qubits = skip_reset_qubits + self._target = target + if target is not None: + self._durations = target.durations() + for gate in dd_sequence: + if gate.name not in target.operation_names: + raise TranspilerError( + f"{gate.name} in dd_sequence is not supported in the target" + ) + + def run(self, dag): + """Run the DynamicalDecoupling pass on dag. + + Args: + dag (DAGCircuit): a scheduled DAG. + + Returns: + DAGCircuit: equivalent circuit with delays interrupted by DD, + where possible. + + Raises: + TranspilerError: if the circuit is not mapped on physical qubits. + """ + if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: + raise TranspilerError("DD runs on physical circuits only.") + + if dag.duration is None: + raise TranspilerError("DD runs after circuit is scheduled.") + + num_pulses = len(self._dd_sequence) + sequence_gphase = 0 + if num_pulses != 1: + if num_pulses % 2 != 0: + raise TranspilerError("DD sequence must contain an even number of gates (or 1).") + noop = np.eye(2) + for gate in self._dd_sequence: + noop = noop.dot(gate.to_matrix()) + if not matrix_equal(noop, IGate().to_matrix(), ignore_phase=True): + raise TranspilerError("The DD sequence does not make an identity operation.") + sequence_gphase = np.angle(noop[0][0]) + + if self._qubits is None: + self._qubits = set(range(dag.num_qubits())) + else: + self._qubits = set(self._qubits) + + if self._spacing: + if sum(self._spacing) != 1 or any(a < 0 for a in self._spacing): + raise TranspilerError( + "The spacings must be given in terms of fractions " + "of the slack period and sum to 1." + ) + else: # default to balanced spacing + mid = 1 / num_pulses + end = mid / 2 + self._spacing = [end] + [mid] * (num_pulses - 1) + [end] + + for qarg in list(self._qubits): + for gate in self._dd_sequence: + if not self.__gate_supported(gate, qarg): + self._qubits.discard(qarg) + break + + index_sequence_duration_map = {} + for physical_qubit in self._qubits: + dd_sequence_duration = 0 + for index, gate in enumerate(self._dd_sequence): + gate = gate.to_mutable() + self._dd_sequence[index] = gate + gate.duration = self._durations.get(gate, physical_qubit) + + dd_sequence_duration += gate.duration + index_sequence_duration_map[physical_qubit] = dd_sequence_duration + + new_dag = dag.copy_empty_like() + + for nd in dag.topological_op_nodes(): + if not isinstance(nd.op, Delay): + new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) + continue + + dag_qubit = nd.qargs[0] + physical_qubit = dag.find_bit(dag_qubit).index + if physical_qubit not in self._qubits: # skip unwanted qubits + new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) + continue + + pred = next(dag.predecessors(nd)) + succ = next(dag.successors(nd)) + if self._skip_reset_qubits: # discount initial delays + if isinstance(pred, DAGInNode) or isinstance(pred.op, Reset): + new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) + continue + + dd_sequence_duration = index_sequence_duration_map[physical_qubit] + slack = nd.op.duration - dd_sequence_duration + if slack <= 0: # dd doesn't fit + new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) + continue + + if num_pulses == 1: # special case of using a single gate for DD + u_inv = self._dd_sequence[0].inverse().to_matrix() + theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase(u_inv) + # absorb the inverse into the successor (from left in circuit) + if isinstance(succ, DAGOpNode) and isinstance(succ.op, (UGate, U3Gate)): + theta_r, phi_r, lam_r = succ.op.params + succ.op.params = Optimize1qGates.compose_u3( + theta_r, phi_r, lam_r, theta, phi, lam + ) + sequence_gphase += phase + # absorb the inverse into the predecessor (from right in circuit) + elif isinstance(pred, DAGOpNode) and isinstance(pred.op, (UGate, U3Gate)): + theta_l, phi_l, lam_l = pred.op.params + pred.op.params = Optimize1qGates.compose_u3( + theta, phi, lam, theta_l, phi_l, lam_l + ) + sequence_gphase += phase + # don't do anything if there's no single-qubit gate to absorb the inverse + else: + new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) + continue + + # insert the actual DD sequence + taus = [int(slack * a) for a in self._spacing] + unused_slack = slack - sum(taus) # unused, due to rounding to int multiples of dt + middle_index = int((len(taus) - 1) / 2) # arbitrary: redistribute to middle + taus[middle_index] += unused_slack # now we add up to original delay duration + + for tau, gate in itertools.zip_longest(taus, self._dd_sequence): + if tau > 0: + new_dag.apply_operation_back(Delay(tau), [dag_qubit], check=False) + if gate is not None: + new_dag.apply_operation_back(gate, [dag_qubit], check=False) + + new_dag.global_phase = _mod_2pi(new_dag.global_phase + sequence_gphase) + + return new_dag + + def __gate_supported(self, gate: Gate, qarg: int) -> bool: + """A gate is supported on the qubit (qarg) or not.""" + if self._target is None or self._target.instruction_supported(gate.name, qargs=(qarg,)): + return True + return False + + +def _mod_2pi(angle: float, atol: float = 0): + """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π""" + wrapped = (angle + np.pi) % (2 * np.pi) - np.pi + if abs(wrapped - np.pi) < atol: + wrapped = -np.pi + return wrapped diff --git a/releasenotes/notes/0.45/removed_deprecated_0.21-6c93f7bbc50ae40e.yaml b/releasenotes/notes/0.45/removed_deprecated_0.21-6c93f7bbc50ae40e.yaml index 5f609ad91b89..6c7b67336b80 100644 --- a/releasenotes/notes/0.45/removed_deprecated_0.21-6c93f7bbc50ae40e.yaml +++ b/releasenotes/notes/0.45/removed_deprecated_0.21-6c93f7bbc50ae40e.yaml @@ -3,42 +3,23 @@ upgrade: - | The function :func:`~qiskit.execute_function.execute` does not accept the arguments `qobj_id` and `qobj_header` any more. - Their use was deprecated in Qiskit 0.37 (with Terra 0.21), released on June 2022. + Their use was deprecated in Qiskit 0.37 (with Terra 0.21), released on June 2022. - | - The transpilation pass ``qiskit.transpiler.passes.ALAPSchedule`` is removed. It use was deprecated - in Qiskit 0.37 (with Terra 0.21), released on June 2022 and replaced by - :class:`~.transpiler.passes.ALAPScheduleAnalysis`, which is an - analysis pass. - - | - The transpilation pass ``qiskit.transpiler.passes.ASAPSchedule`` is removed. It use was deprecated - in Qiskit 0.37 (with Terra 0.21), released on June 2022. It has been superseded by - :class:`~.ASAPScheduleAnalysis` and the new scheduling workflow. - - | - The transpilation pass ``qiskit.transpiler.passes.DynamicalDecoupling`` is removed. It use was deprecated - in Qiskit 0.37 (with Terra 0.21), released on June 2022. - Instead, use :class:`~.transpiler.passes.PadDynamicalDecoupling`, which performs the same - function but requires scheduling and alignment analysis passes to run prior to it. - - | - The transpilation pass ``qiskit.transpiler.passes.AlignMeasures`` is removed. It use was deprecated - in Qiskit 0.37 (with Terra 0.21), released on June 2022. - Instead, use :class:`~.ConstrainedReschedule`, which performs the same function - and also supports aligning to additional timing constraints. - - | - The transpilation pass ``qiskit.transpiler.passes.CXDirection`` is removed. It use was deprecated + The transpilation pass ``qiskit.transpiler.passes.CXDirection`` is removed. It use was deprecated in Qiskit 0.37 (with Terra 0.21), released on June 2022. Instead, use the more generic :class:`~.GateDirection` pass. - | - The transpilation pass ``qiskit.transpiler.passes.CheckCXDirection`` is removed. It use was deprecated + The transpilation pass ``qiskit.transpiler.passes.CheckCXDirection`` is removed. It use was deprecated in Qiskit 0.37 (with Terra 0.21), released on June 2022. Instead, use the more generic :class:`~.CheckGateDirection` pass. - | The methods ``to_dict`` in the classes :class:`.pulse.transforms.AlignmentKind`, `.pulse.transforms.AlignEquispaced`, and :class:`.pulse.transforms.AlignFunc` are removed. - They were deprecated + They were deprecated in Qiskit 0.37 (with Terra 0.21), released on June 2022. - | The argument ``circuits`` in the method :meth:`qiskit.qpy.interface.dump` - is removed as its usage was deprecated + is removed as its usage was deprecated in Qiskit 0.37 (with Terra 0.21), released on June 2022. Instead, use the argument ``programs``, which behaves identically. - + diff --git a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py index feb50bab8002..efd3b86ee393 100644 --- a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py +++ b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py @@ -14,8 +14,316 @@ from qiskit import QuantumCircuit, pulse from qiskit.test import QiskitTestCase +from qiskit.transpiler import InstructionDurations from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes import ValidatePulseGates +from qiskit.transpiler.passes import ( + AlignMeasures, + ValidatePulseGates, + ALAPSchedule, + TimeUnitConversion, +) + + +class TestAlignMeasures(QiskitTestCase): + """A test for measurement alignment pass.""" + + def setUp(self): + super().setUp() + instruction_durations = InstructionDurations() + instruction_durations.update( + [ + ("rz", (0,), 0), + ("rz", (1,), 0), + ("x", (0,), 160), + ("x", (1,), 160), + ("sx", (0,), 160), + ("sx", (1,), 160), + ("cx", (0, 1), 800), + ("cx", (1, 0), 800), + ("measure", None, 1600), + ] + ) + self.time_conversion_pass = TimeUnitConversion(inst_durations=instruction_durations) + # reproduce old behavior of 0.20.0 before #7655 + # currently default write latency is 0 + self.scheduling_pass = ALAPSchedule( + durations=instruction_durations, + clbit_write_latency=1600, + conditional_latency=0, + ) + self.align_measure_pass = AlignMeasures(alignment=16) + + def test_t1_experiment_type(self): + """Test T1 experiment type circuit. + + (input) + + ┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├ + └───┘└────────────────┘└╥┘ + c: 1/════════════════════════╩═ + 0 + + (aligned) + + ┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├ + └───┘└────────────────┘└╥┘ + c: 1/════════════════════════╩═ + 0 + + This type of experiment slightly changes delay duration of interest. + However the quantization error should be less than alignment * dt. + """ + circuit = QuantumCircuit(1, 1) + circuit.x(0) + circuit.delay(100, 0, unit="dt") + circuit.measure(0, 0) + + timed_circuit = self.time_conversion_pass(circuit) + scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) + aligned_circuit = self.align_measure_pass( + scheduled_circuit, property_set={"time_unit": "dt"} + ) + + ref_circuit = QuantumCircuit(1, 1) + ref_circuit.x(0) + ref_circuit.delay(112, 0, unit="dt") + ref_circuit.measure(0, 0) + + self.assertEqual(aligned_circuit, ref_circuit) + + def test_hanh_echo_experiment_type(self): + """Test Hahn echo experiment type circuit. + + (input) + + ┌────┐┌────────────────┐┌───┐┌────────────────┐┌────┐┌─┐ + q_0: ┤ √X ├┤ Delay(100[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ √X ├┤M├ + └────┘└────────────────┘└───┘└────────────────┘└────┘└╥┘ + c: 1/══════════════════════════════════════════════════════╩═ + 0 + + (output) + + ┌────┐┌────────────────┐┌───┐┌────────────────┐┌────┐┌──────────────┐┌─┐ + q_0: ┤ √X ├┤ Delay(100[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ √X ├┤ Delay(8[dt]) ├┤M├ + └────┘└────────────────┘└───┘└────────────────┘└────┘└──────────────┘└╥┘ + c: 1/══════════════════════════════════════════════════════════════════════╩═ + 0 + + This type of experiment doesn't change duration of interest (two in the middle). + However induces slight delay less than alignment * dt before measurement. + This might induce extra amplitude damping error. + """ + circuit = QuantumCircuit(1, 1) + circuit.sx(0) + circuit.delay(100, 0, unit="dt") + circuit.x(0) + circuit.delay(100, 0, unit="dt") + circuit.sx(0) + circuit.measure(0, 0) + + timed_circuit = self.time_conversion_pass(circuit) + scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) + aligned_circuit = self.align_measure_pass( + scheduled_circuit, property_set={"time_unit": "dt"} + ) + + ref_circuit = QuantumCircuit(1, 1) + ref_circuit.sx(0) + ref_circuit.delay(100, 0, unit="dt") + ref_circuit.x(0) + ref_circuit.delay(100, 0, unit="dt") + ref_circuit.sx(0) + ref_circuit.delay(8, 0, unit="dt") + ref_circuit.measure(0, 0) + + self.assertEqual(aligned_circuit, ref_circuit) + + def test_mid_circuit_measure(self): + """Test circuit with mid circuit measurement. + + (input) + + ┌───┐┌────────────────┐┌─┐┌───────────────┐┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├┤ Delay(10[dt]) ├┤ X ├┤ Delay(120[dt]) ├┤M├ + └───┘└────────────────┘└╥┘└───────────────┘└───┘└────────────────┘└╥┘ + c: 2/════════════════════════╩══════════════════════════════════════════╩═ + 0 1 + + (output) + + ┌───┐┌────────────────┐┌─┐┌───────────────┐┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├┤ Delay(10[dt]) ├┤ X ├┤ Delay(134[dt]) ├┤M├ + └───┘└────────────────┘└╥┘└───────────────┘└───┘└────────────────┘└╥┘ + c: 2/════════════════════════╩══════════════════════════════════════════╩═ + 0 1 + + Extra delay is always added to the existing delay right before the measurement. + Delay after measurement is unchanged. + """ + circuit = QuantumCircuit(1, 2) + circuit.x(0) + circuit.delay(100, 0, unit="dt") + circuit.measure(0, 0) + circuit.delay(10, 0, unit="dt") + circuit.x(0) + circuit.delay(120, 0, unit="dt") + circuit.measure(0, 1) + + timed_circuit = self.time_conversion_pass(circuit) + scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) + aligned_circuit = self.align_measure_pass( + scheduled_circuit, property_set={"time_unit": "dt"} + ) + + ref_circuit = QuantumCircuit(1, 2) + ref_circuit.x(0) + ref_circuit.delay(112, 0, unit="dt") + ref_circuit.measure(0, 0) + ref_circuit.delay(10, 0, unit="dt") + ref_circuit.x(0) + ref_circuit.delay(134, 0, unit="dt") + ref_circuit.measure(0, 1) + + self.assertEqual(aligned_circuit, ref_circuit) + + def test_mid_circuit_multiq_gates(self): + """Test circuit with mid circuit measurement and multi qubit gates. + + (input) + + ┌───┐┌────────────────┐┌─┐ ┌─┐ + q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├──■───────■──┤M├ + └───┘└────────────────┘└╥┘┌─┴─┐┌─┐┌─┴─┐└╥┘ + q_1: ────────────────────────╫─┤ X ├┤M├┤ X ├─╫─ + ║ └───┘└╥┘└───┘ ║ + c: 2/════════════════════════╩═══════╩═══════╩═ + 0 1 0 + + (output) + + ┌───┐ ┌────────────────┐┌─┐ ┌─────────────────┐ ┌─┐» + q_0: ───────┤ X ├───────┤ Delay(112[dt]) ├┤M├──■──┤ Delay(1600[dt]) ├──■──┤M├» + ┌──────┴───┴──────┐└────────────────┘└╥┘┌─┴─┐└───────┬─┬───────┘┌─┴─┐└╥┘» + q_1: ┤ Delay(1872[dt]) ├───────────────────╫─┤ X ├────────┤M├────────┤ X ├─╫─» + └─────────────────┘ ║ └───┘ └╥┘ └───┘ ║ » + c: 2/══════════════════════════════════════╩═══════════════╩═══════════════╩═» + 0 1 0 » + « + «q_0: ─────────────────── + « ┌─────────────────┐ + «q_1: ┤ Delay(1600[dt]) ├ + « └─────────────────┘ + «c: 2/═══════════════════ + « + + Delay for the other channel paired by multi-qubit instruction is also scheduled. + Delay (1872dt) = X (160dt) + Delay (100dt + extra 12dt) + Measure (1600dt). + """ + circuit = QuantumCircuit(2, 2) + circuit.x(0) + circuit.delay(100, 0, unit="dt") + circuit.measure(0, 0) + circuit.cx(0, 1) + circuit.measure(1, 1) + circuit.cx(0, 1) + circuit.measure(0, 0) + + timed_circuit = self.time_conversion_pass(circuit) + scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) + aligned_circuit = self.align_measure_pass( + scheduled_circuit, property_set={"time_unit": "dt"} + ) + + ref_circuit = QuantumCircuit(2, 2) + ref_circuit.x(0) + ref_circuit.delay(112, 0, unit="dt") + ref_circuit.measure(0, 0) + ref_circuit.delay(160 + 112 + 1600, 1, unit="dt") + ref_circuit.cx(0, 1) + ref_circuit.delay(1600, 0, unit="dt") + ref_circuit.measure(1, 1) + ref_circuit.cx(0, 1) + ref_circuit.delay(1600, 1, unit="dt") + ref_circuit.measure(0, 0) + + self.assertEqual(aligned_circuit, ref_circuit) + + def test_alignment_is_not_processed(self): + """Test avoid pass processing if delay is aligned.""" + circuit = QuantumCircuit(2, 2) + circuit.x(0) + circuit.delay(160, 0, unit="dt") + circuit.measure(0, 0) + circuit.cx(0, 1) + circuit.measure(1, 1) + circuit.cx(0, 1) + circuit.measure(0, 0) + + # pre scheduling is not necessary because alignment is skipped + # this is to minimize breaking changes to existing code. + transpiled = self.align_measure_pass(circuit, property_set={"time_unit": "dt"}) + + self.assertEqual(transpiled, circuit) + + def test_circuit_using_clbit(self): + """Test a circuit with instructions using a common clbit. + + (input) + ┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├────────────── + └───┘└────────────────┘└╥┘ ┌───┐ + q_1: ────────────────────────╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ────────────────────────╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/════════════════════════╩═╡ c_0 = T ╞═╩═ + 0 └─────────┘ 0 + + (aligned) + ┌───┐ ┌────────────────┐┌─┐┌────────────────┐ + q_0: ───────┤ X ├───────┤ Delay(112[dt]) ├┤M├┤ Delay(160[dt]) ├─── + ┌──────┴───┴──────┐└────────────────┘└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1872[dt]) ├───────────────────╫───────┤ X ├────────── + └┬────────────────┤ ║ └─╥─┘ ┌─┐ + q_2: ─┤ Delay(432[dt]) ├───────────────────╫─────────╫─────────┤M├ + └────────────────┘ ║ ┌────╨────┐ └╥┘ + c: 1/══════════════════════════════════════╩════╡ c_0 = T ╞═════╩═ + 0 └─────────┘ 0 + + Looking at the q_0, the total schedule length T becomes + 160 (x) + 112 (aligned delay) + 1600 (measure) + 160 (delay) = 2032. + The last delay comes from ALAP scheduling called before the AlignMeasure pass, + which aligns stop times as late as possible, so the start time of x(1).c_if(0) + and the stop time of measure(0, 0) become T - 160. + """ + circuit = QuantumCircuit(3, 1) + circuit.x(0) + circuit.delay(100, 0, unit="dt") + circuit.measure(0, 0) + circuit.x(1).c_if(0, 1) + circuit.measure(2, 0) + + timed_circuit = self.time_conversion_pass(circuit) + scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) + aligned_circuit = self.align_measure_pass( + scheduled_circuit, property_set={"time_unit": "dt"} + ) + self.assertEqual(aligned_circuit.duration, 2032) + + ref_circuit = QuantumCircuit(3, 1) + ref_circuit.x(0) + ref_circuit.delay(112, 0, unit="dt") + ref_circuit.delay(1872, 1, unit="dt") # 2032 - 160 + 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) class TestPulseGateValidation(QiskitTestCase): diff --git a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py new file mode 100644 index 000000000000..2f375c46f67b --- /dev/null +++ b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py @@ -0,0 +1,811 @@ +# 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. + +"""Test the legacy Scheduling passes""" + +import unittest + +from ddt import ddt, data, unpack + +from qiskit import QuantumCircuit +from qiskit.circuit import Delay, Parameter +from qiskit.circuit.library.standard_gates import XGate, YGate, CXGate +from qiskit.test import QiskitTestCase +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule, DynamicalDecoupling +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.target import Target, InstructionProperties + + +@ddt +class TestSchedulingPass(QiskitTestCase): + """Tests the Scheduling passes""" + + def test_alap_agree_with_reverse_asap_reverse(self): + """Test if ALAP schedule agrees with doubly-reversed ASAP schedule.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.delay(500, 1) + qc.cx(0, 1) + qc.measure_all() + + durations = InstructionDurations( + [("h", 0, 200), ("cx", [0, 1], 700), ("measure", None, 1000)] + ) + + pm = PassManager(ALAPSchedule(durations)) + alap_qc = pm.run(qc) + + pm = PassManager(ASAPSchedule(durations)) + new_qc = pm.run(qc.reverse_ops()) + new_qc = new_qc.reverse_ops() + new_qc.name = new_qc.name + + self.assertEqual(alap_qc, new_qc) + + @data(ALAPSchedule, ASAPSchedule) + def test_classically_controlled_gate_after_measure(self, schedule_pass): + """Test if ALAP/ASAP schedules circuits with c_if after measure with a common clbit. + See: https://github.com/Qiskit/qiskit-terra/issues/7654 + + (input) + ┌─┐ + q_0: ┤M├─────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├─── + ║ └─╥─┘ + ║ ┌────╨────┐ + c: 1/═╩═╡ c_0 = T ╞ + 0 └─────────┘ + + (scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├ + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├─────── + └─────────────────┘ ║ └─╥─┘ + ║ ┌────╨────┐ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════ + 0 └─────────┘ + """ + qc = QuantumCircuit(2, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, True) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager(schedule_pass(durations)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.measure(0, 0) + expected.delay(1000, 1) # x.c_if starts after measure + expected.x(1).c_if(0, True) + expected.delay(200, 0) + + self.assertEqual(expected, scheduled) + + @data(ALAPSchedule, ASAPSchedule) + def test_measure_after_measure(self, schedule_pass): + """Test if ALAP/ASAP schedules circuits with measure after measure with a common clbit. + See: https://github.com/Qiskit/qiskit-terra/issues/7654 + + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + └───┘└╥┘┌─┐ + q_1: ──────╫─┤M├ + ║ └╥┘ + c: 1/══════╩══╩═ + 0 0 + + (scheduled) + ┌───┐ ┌─┐┌─────────────────┐ + q_0: ───────┤ X ├───────┤M├┤ Delay(1000[dt]) ├ + ┌──────┴───┴──────┐└╥┘└───────┬─┬───────┘ + q_1: ┤ Delay(1200[dt]) ├─╫─────────┤M├──────── + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩══════════╩═════════ + 0 0 + """ + qc = QuantumCircuit(2, 1) + qc.x(0) + qc.measure(0, 0) + qc.measure(1, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager(schedule_pass(durations)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.x(0) + expected.measure(0, 0) + expected.delay(1200, 1) + expected.measure(1, 0) + expected.delay(1000, 0) + + self.assertEqual(expected, scheduled) + + @data(ALAPSchedule, ASAPSchedule) + def test_c_if_on_different_qubits(self, schedule_pass): + """Test if ALAP/ASAP schedules circuits with `c_if`s on different qubits. + + (input) + ┌─┐ + q_0: ┤M├────────────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────────────── + ║ └─╥─┘ ┌───┐ + q_2: ─╫──────╫────────┤ X ├─── + ║ ║ └─╥─┘ + ║ ┌────╨────┐┌────╨────┐ + c: 1/═╩═╡ c_0 = T ╞╡ c_0 = T ╞ + 0 └─────────┘└─────────┘ + + (scheduled) + + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─────────── + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────────────── + ├─────────────────┤ ║ └─╥─┘ ┌───┐ + q_2: ┤ Delay(1000[dt]) ├─╫─────────╫────────────┤ X ├─── + └─────────────────┘ ║ ║ └─╥─┘ + ║ ┌────╨────┐ ┌────╨────┐ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ + 0 └─────────┘ └─────────┘ + """ + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, True) + qc.x(2).c_if(0, True) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager(schedule_pass(durations)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.measure(0, 0) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.x(1).c_if(0, True) + expected.x(2).c_if(0, True) + expected.delay(200, 0) + + self.assertEqual(expected, scheduled) + + @data(ALAPSchedule, ASAPSchedule) + def test_shorter_measure_after_measure(self, schedule_pass): + """Test if ALAP/ASAP schedules circuits with shorter measure after measure with a common clbit. + + (input) + ┌─┐ + q_0: ┤M├─── + └╥┘┌─┐ + q_1: ─╫─┤M├ + ║ └╥┘ + c: 1/═╩══╩═ + 0 0 + + (scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(700[dt]) ├ + ┌─────────────────┐└╥┘└──────┬─┬───────┘ + q_1: ┤ Delay(1000[dt]) ├─╫────────┤M├──────── + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩═════════╩═════════ + 0 0 + """ + qc = QuantumCircuit(2, 1) + qc.measure(0, 0) + qc.measure(1, 0) + + durations = InstructionDurations([("measure", [0], 1000), ("measure", [1], 700)]) + pm = PassManager(schedule_pass(durations)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.measure(0, 0) + expected.delay(1000, 1) + expected.measure(1, 0) + expected.delay(700, 0) + + self.assertEqual(expected, scheduled) + + @data(ALAPSchedule, ASAPSchedule) + def test_measure_after_c_if(self, schedule_pass): + """Test if ALAP/ASAP schedules circuits with c_if after measure with a common clbit. + + (input) + ┌─┐ + q_0: ┤M├────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ─╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/═╩═╡ c_0 = T ╞═╩═ + 0 └─────────┘ 0 + + (scheduled) + ┌─┐┌─────────────────┐ + q_0: ───────────────────┤M├┤ Delay(1000[dt]) ├────────────────── + ┌─────────────────┐└╥┘└──────┬───┬──────┘┌────────────────┐ + q_1: ┤ Delay(1000[dt]) ├─╫────────┤ X ├───────┤ Delay(800[dt]) ├ + ├─────────────────┤ ║ └─╥─┘ └──────┬─┬───────┘ + q_2: ┤ Delay(1000[dt]) ├─╫──────────╫────────────────┤M├──────── + └─────────────────┘ ║ ┌────╨────┐ └╥┘ + c: 1/════════════════════╩═════╡ c_0=0x1 ╞════════════╩═════════ + 0 └─────────┘ 0 + """ + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, 1) + qc.measure(2, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager(schedule_pass(durations)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.x(1).c_if(0, 1) + expected.measure(2, 0) + expected.delay(1000, 0) + expected.delay(800, 1) + + self.assertEqual(expected, scheduled) + + def test_parallel_gate_different_length(self): + """Test circuit having two parallel instruction with different length. + + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + ├───┤└╥┘┌─┐ + q_1: ┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c: 2/══════╩══╩═ + 0 1 + + (expected, ALAP) + ┌────────────────┐┌───┐┌─┐ + q_0: ┤ Delay(200[dt]) ├┤ X ├┤M├ + └─────┬───┬──────┘└┬─┬┘└╥┘ + q_1: ──────┤ X ├────────┤M├──╫─ + └───┘ └╥┘ ║ + c: 2/════════════════════╩═══╩═ + 1 0 + + (expected, ASAP) + ┌───┐┌─┐┌────────────────┐ + q_0: ┤ X ├┤M├┤ Delay(200[dt]) ├ + ├───┤└╥┘└──────┬─┬───────┘ + q_1: ┤ X ├─╫────────┤M├──────── + └───┘ ║ └╥┘ + c: 2/══════╩═════════╩═════════ + 0 1 + + """ + qc = QuantumCircuit(2, 2) + qc.x(0) + qc.x(1) + qc.measure(0, 0) + qc.measure(1, 1) + + durations = InstructionDurations( + [("x", [0], 200), ("x", [1], 400), ("measure", None, 1000)] + ) + pm = PassManager(ALAPSchedule(durations)) + qc_alap = pm.run(qc) + + alap_expected = QuantumCircuit(2, 2) + alap_expected.delay(200, 0) + alap_expected.x(0) + alap_expected.x(1) + alap_expected.measure(0, 0) + alap_expected.measure(1, 1) + + self.assertEqual(qc_alap, alap_expected) + + pm = PassManager(ASAPSchedule(durations)) + qc_asap = pm.run(qc) + + asap_expected = QuantumCircuit(2, 2) + asap_expected.x(0) + asap_expected.x(1) + asap_expected.measure(0, 0) # immediately start after X gate + asap_expected.measure(1, 1) + asap_expected.delay(200, 0) + + self.assertEqual(qc_asap, asap_expected) + + def test_parallel_gate_different_length_with_barrier(self): + """Test circuit having two parallel instruction with different length with barrier. + + (input) + ┌───┐┌─┐ + q_0: ┤ X ├┤M├─── + ├───┤└╥┘┌─┐ + q_1: ┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c: 2/══════╩══╩═ + 0 1 + + (expected, ALAP) + ┌────────────────┐┌───┐ ░ ┌─┐ + q_0: ┤ Delay(200[dt]) ├┤ X ├─░─┤M├─── + └─────┬───┬──────┘└───┘ ░ └╥┘┌─┐ + q_1: ──────┤ X ├─────────────░──╫─┤M├ + └───┘ ░ ║ └╥┘ + c: 2/═══════════════════════════╩══╩═ + 0 1 + + (expected, ASAP) + ┌───┐┌────────────────┐ ░ ┌─┐ + q_0: ┤ X ├┤ Delay(200[dt]) ├─░─┤M├─── + ├───┤└────────────────┘ ░ └╥┘┌─┐ + q_1: ┤ X ├───────────────────░──╫─┤M├ + └───┘ ░ ║ └╥┘ + c: 2/═══════════════════════════╩══╩═ + 0 1 + """ + qc = QuantumCircuit(2, 2) + qc.x(0) + qc.x(1) + qc.barrier() + qc.measure(0, 0) + qc.measure(1, 1) + + durations = InstructionDurations( + [("x", [0], 200), ("x", [1], 400), ("measure", None, 1000)] + ) + pm = PassManager(ALAPSchedule(durations)) + qc_alap = pm.run(qc) + + alap_expected = QuantumCircuit(2, 2) + alap_expected.delay(200, 0) + alap_expected.x(0) + alap_expected.x(1) + alap_expected.barrier() + alap_expected.measure(0, 0) + alap_expected.measure(1, 1) + + self.assertEqual(qc_alap, alap_expected) + + pm = PassManager(ASAPSchedule(durations)) + qc_asap = pm.run(qc) + + asap_expected = QuantumCircuit(2, 2) + asap_expected.x(0) + asap_expected.delay(200, 0) + asap_expected.x(1) + asap_expected.barrier() + asap_expected.measure(0, 0) + asap_expected.measure(1, 1) + + self.assertEqual(qc_asap, asap_expected) + + def test_measure_after_c_if_on_edge_locking(self): + """Test if ALAP/ASAP schedules circuits with c_if after measure with a common clbit. + + The scheduler is configured to reproduce behavior of the 0.20.0, + in which clbit lock is applied to the end-edge of measure instruction. + See https://github.com/Qiskit/qiskit-terra/pull/7655 + + (input) + ┌─┐ + q_0: ┤M├────────────── + └╥┘ ┌───┐ + q_1: ─╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ─╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/═╩═╡ c_0 = T ╞═╩═ + 0 └─────────┘ 0 + + (ASAP scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├───────────────────── + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├──────────────────────────── + └─────────────────┘ ║ └─╥─┘ ┌─┐┌────────────────┐ + q_2: ────────────────────╫─────────╫─────────┤M├┤ Delay(200[dt]) ├ + ║ ┌────╨────┐ └╥┘└────────────────┘ + c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═══════════════════ + 0 └─────────┘ 0 + + (ALAP scheduled) + ┌─┐┌────────────────┐ + q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─── + ┌─────────────────┐└╥┘└─────┬───┬──────┘ + q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────── + └┬────────────────┤ ║ └─╥─┘ ┌─┐ + q_2: ─┤ Delay(200[dt]) ├─╫─────────╫─────────┤M├ + └────────────────┘ ║ ┌────╨────┐ └╥┘ + c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═ + 0 └─────────┘ 0 + + """ + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.x(1).c_if(0, 1) + qc.measure(2, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + + # lock at the end edge + actual_asap = PassManager(ASAPSchedule(durations, clbit_write_latency=1000)).run(qc) + actual_alap = PassManager(ALAPSchedule(durations, clbit_write_latency=1000)).run(qc) + + # start times of 2nd measure depends on ASAP/ALAP + expected_asap = QuantumCircuit(3, 1) + expected_asap.measure(0, 0) + expected_asap.delay(1000, 1) + expected_asap.x(1).c_if(0, 1) + expected_asap.measure(2, 0) + expected_asap.delay(200, 0) + expected_asap.delay(200, 2) + self.assertEqual(expected_asap, actual_asap) + + expected_alap = QuantumCircuit(3, 1) + expected_alap.measure(0, 0) + expected_alap.delay(1000, 1) + expected_alap.x(1).c_if(0, 1) + expected_alap.delay(200, 2) + expected_alap.measure(2, 0) + expected_alap.delay(200, 0) + self.assertEqual(expected_alap, actual_alap) + + @data([100, 200], [500, 0], [1000, 200]) + @unpack + def test_active_reset_circuit(self, write_lat, cond_lat): + """Test practical example of reset circuit. + + Because of the stimulus pulse overlap with the previous XGate on the q register, + measure instruction is always triggered after XGate regardless of write latency. + Thus only conditional latency matters in the scheduling. + + (input) + ┌─┐ ┌───┐ ┌─┐ ┌───┐ ┌─┐ ┌───┐ + q: ┤M├───┤ X ├───┤M├───┤ X ├───┤M├───┤ X ├─── + └╥┘ └─╥─┘ └╥┘ └─╥─┘ └╥┘ └─╥─┘ + ║ ┌────╨────┐ ║ ┌────╨────┐ ║ ┌────╨────┐ + c: 1/═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞ + 0 └─────────┘ 0 └─────────┘ 0 └─────────┘ + + """ + qc = QuantumCircuit(1, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + qc.measure(0, 0) + qc.x(0).c_if(0, 1) + qc.measure(0, 0) + 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) + ).run(qc) + actual_alap = PassManager( + ALAPSchedule(durations, clbit_write_latency=write_lat, conditional_latency=cond_lat) + ).run(qc) + + expected = QuantumCircuit(1, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + expected.measure(0, 0) + if cond_lat > 0: + expected.delay(cond_lat, 0) + expected.x(0).c_if(0, 1) + + self.assertEqual(expected, actual_asap) + self.assertEqual(expected, actual_alap) + + def test_random_complicated_circuit(self): + """Test scheduling complicated circuit with control flow. + + (input) + ┌────────────────┐ ┌───┐ ░ ┌───┐ » + q_0: ┤ Delay(100[dt]) ├───┤ X ├────░──────────────────┤ X ├───» + └────────────────┘ └─╥─┘ ░ ┌───┐ └─╥─┘ » + q_1: ───────────────────────╫──────░───────┤ X ├────────╫─────» + ║ ░ ┌─┐ └─╥─┘ ║ » + q_2: ───────────────────────╫──────░─┤M├─────╫──────────╫─────» + ┌────╨────┐ ░ └╥┘┌────╨────┐┌────╨────┐» + c: 1/══════════════════╡ c_0=0x1 ╞════╩═╡ c_0=0x0 ╞╡ c_0=0x0 ╞» + └─────────┘ 0 └─────────┘└─────────┘» + « ┌────────────────┐┌───┐ + «q_0: ┤ Delay(300[dt]) ├┤ X ├─────■───── + « └────────────────┘└───┘ ┌─┴─┐ + «q_1: ────────■─────────────────┤ X ├─── + « ┌─┴─┐ ┌─┐ └─╥─┘ + «q_2: ──────┤ X ├────────┤M├──────╫───── + « └───┘ └╥┘ ┌────╨────┐ + «c: 1/════════════════════╩══╡ c_0=0x0 ╞ + « 0 └─────────┘ + + (ASAP scheduled) duration = 2800 dt + ┌────────────────┐┌────────────────┐ ┌───┐ ░ ┌─────────────────┐» + q_0: ┤ Delay(100[dt]) ├┤ Delay(100[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├» + ├────────────────┤└────────────────┘ └─╥─┘ ░ ├─────────────────┤» + q_1: ┤ Delay(300[dt]) ├───────────────────────╫──────░─┤ Delay(1200[dt]) ├» + ├────────────────┤ ║ ░ └───────┬─┬───────┘» + q_2: ┤ Delay(300[dt]) ├───────────────────────╫──────░─────────┤M├────────» + └────────────────┘ ┌────╨────┐ ░ └╥┘ » + c: 1/════════════════════════════════════╡ c_0=0x1 ╞════════════╩═════════» + └─────────┘ 0 » + « ┌───┐ ┌────────────────┐» + «q_0: ────────────────────────────────┤ X ├───┤ Delay(300[dt]) ├» + « ┌───┐ └─╥─┘ └────────────────┘» + «q_1: ───┤ X ├──────────────────────────╫─────────────■─────────» + « └─╥─┘ ┌────────────────┐ ║ ┌─┴─┐ » + «q_2: ─────╫─────┤ Delay(300[dt]) ├─────╫───────────┤ X ├───────» + « ┌────╨────┐└────────────────┘┌────╨────┐ └───┘ » + «c: 1/╡ c_0=0x0 ╞══════════════════╡ c_0=0x0 ╞══════════════════» + « └─────────┘ └─────────┘ » + « ┌───┐ ┌────────────────┐ + «q_0: ──────┤ X ├────────────■─────┤ Delay(700[dt]) ├ + « ┌─────┴───┴──────┐ ┌─┴─┐ ├────────────────┤ + «q_1: ┤ Delay(400[dt]) ├───┤ X ├───┤ Delay(700[dt]) ├ + « ├────────────────┤ └─╥─┘ └──────┬─┬───────┘ + «q_2: ┤ Delay(300[dt]) ├─────╫────────────┤M├──────── + « └────────────────┘┌────╨────┐ └╥┘ + «c: 1/══════════════════╡ c_0=0x0 ╞════════╩═════════ + « └─────────┘ 0 + + (ALAP scheduled) duration = 3100 + ┌────────────────┐┌────────────────┐ ┌───┐ ░ ┌─────────────────┐» + q_0: ┤ Delay(100[dt]) ├┤ Delay(100[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├» + ├────────────────┤└────────────────┘ └─╥─┘ ░ ├─────────────────┤» + q_1: ┤ Delay(300[dt]) ├───────────────────────╫──────░─┤ Delay(1200[dt]) ├» + ├────────────────┤ ║ ░ └───────┬─┬───────┘» + q_2: ┤ Delay(300[dt]) ├───────────────────────╫──────░─────────┤M├────────» + └────────────────┘ ┌────╨────┐ ░ └╥┘ » + c: 1/════════════════════════════════════╡ c_0=0x1 ╞════════════╩═════════» + └─────────┘ 0 » + « ┌───┐ ┌────────────────┐» + «q_0: ────────────────────────────────┤ X ├───┤ Delay(300[dt]) ├» + « ┌───┐ ┌────────────────┐ └─╥─┘ └────────────────┘» + «q_1: ───┤ X ├───┤ Delay(300[dt]) ├─────╫─────────────■─────────» + « └─╥─┘ ├────────────────┤ ║ ┌─┴─┐ » + «q_2: ─────╫─────┤ Delay(600[dt]) ├─────╫───────────┤ X ├───────» + « ┌────╨────┐└────────────────┘┌────╨────┐ └───┘ » + «c: 1/╡ c_0=0x0 ╞══════════════════╡ c_0=0x0 ╞══════════════════» + « └─────────┘ └─────────┘ » + « ┌───┐ ┌────────────────┐ + «q_0: ──────┤ X ├────────────■─────┤ Delay(700[dt]) ├ + « ┌─────┴───┴──────┐ ┌─┴─┐ ├────────────────┤ + «q_1: ┤ Delay(100[dt]) ├───┤ X ├───┤ Delay(700[dt]) ├ + « └──────┬─┬───────┘ └─╥─┘ └────────────────┘ + «q_2: ───────┤M├─────────────╫─────────────────────── + « └╥┘ ┌────╨────┐ + «c: 1/════════╩═════════╡ c_0=0x0 ╞══════════════════ + « 0 └─────────┘ + + """ + qc = QuantumCircuit(3, 1) + qc.delay(100, 0) + qc.x(0).c_if(0, 1) + qc.barrier() + qc.measure(2, 0) + qc.x(1).c_if(0, 0) + qc.x(0).c_if(0, 0) + qc.delay(300, 0) + qc.cx(1, 2) + qc.x(0) + qc.cx(0, 1).c_if(0, 0) + qc.measure(2, 0) + + durations = InstructionDurations( + [("x", None, 100), ("measure", None, 1000), ("cx", None, 200)] + ) + + actual_asap = PassManager( + ASAPSchedule(durations, clbit_write_latency=100, conditional_latency=200) + ).run(qc) + actual_alap = PassManager( + ALAPSchedule(durations, clbit_write_latency=100, conditional_latency=200) + ).run(qc) + + expected_asap = QuantumCircuit(3, 1) + expected_asap.delay(100, 0) + expected_asap.delay(100, 0) # due to conditional latency of 200dt + expected_asap.delay(300, 1) + expected_asap.delay(300, 2) + expected_asap.x(0).c_if(0, 1) + expected_asap.barrier() + expected_asap.delay(1400, 0) + expected_asap.delay(1200, 1) + expected_asap.measure(2, 0) + expected_asap.x(1).c_if(0, 0) + expected_asap.x(0).c_if(0, 0) + expected_asap.delay(300, 0) + expected_asap.x(0) + expected_asap.delay(300, 2) + expected_asap.cx(1, 2) + expected_asap.delay(400, 1) + expected_asap.cx(0, 1).c_if(0, 0) + expected_asap.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) + expected_asap.delay( + 700, 1 + ) # no creg write until 100dt. thus measure can move left by 300dt. + expected_asap.delay(300, 2) + expected_asap.measure(2, 0) + self.assertEqual(expected_asap, actual_asap) + self.assertEqual(actual_asap.duration, 3100) + + expected_alap = QuantumCircuit(3, 1) + expected_alap.delay(100, 0) + expected_alap.delay(100, 0) # due to conditional latency of 200dt + expected_alap.delay(300, 1) + expected_alap.delay(300, 2) + expected_alap.x(0).c_if(0, 1) + expected_alap.barrier() + expected_alap.delay(1400, 0) + expected_alap.delay(1200, 1) + expected_alap.measure(2, 0) + expected_alap.x(1).c_if(0, 0) + expected_alap.x(0).c_if(0, 0) + expected_alap.delay(300, 0) + expected_alap.x(0) + expected_alap.delay(300, 1) + expected_alap.delay(600, 2) + expected_alap.cx(1, 2) + expected_alap.delay(100, 1) + expected_alap.cx(0, 1).c_if(0, 0) + expected_alap.measure(2, 0) + expected_alap.delay(700, 0) + expected_alap.delay(700, 1) + self.assertEqual(expected_alap, actual_alap) + self.assertEqual(actual_alap.duration, 3100) + + def test_dag_introduces_extra_dependency_between_conditionals(self): + """Test dependency between conditional operations in the scheduling. + + In the below example circuit, the conditional x on q1 could start at time 0, + however it must be scheduled after the conditional x on q0 in ASAP scheduling. + That is because circuit model used in the transpiler passes (DAGCircuit) + interprets instructions acting on common clbits must be run in the order + given by the original circuit (QuantumCircuit). + + (input) + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├─── + └─────┬───┬──────┘ └─╥─┘ + q_1: ──────┤ X ├────────────╫───── + └─╥─┘ ║ + ┌────╨────┐ ┌────╨────┐ + c: 1/═══╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ + └─────────┘ └─────────┘ + + (ASAP scheduled) + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── + ├────────────────┤ └─╥─┘ ┌───┐ + q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── + └────────────────┘ ║ └─╥─┘ + ┌────╨────┐┌────╨────┐ + c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ + └─────────┘└─────────┘ + """ + qc = QuantumCircuit(2, 1) + qc.delay(100, 0) + qc.x(0).c_if(0, True) + qc.x(1).c_if(0, True) + + durations = InstructionDurations([("x", None, 160)]) + pm = PassManager(ASAPSchedule(durations)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.delay(100, 0) + expected.delay(100, 1) # due to extra dependency on clbits + expected.x(0).c_if(0, True) + expected.x(1).c_if(0, True) + + self.assertEqual(expected, scheduled) + + @data(ALAPSchedule, ASAPSchedule) + def test_respect_target_instruction_constraints(self, schedule_pass): + """Test if ALAP/ASAP does not pad delays for qubits that do not support delay instructions. + See: https://github.com/Qiskit/qiskit-terra/issues/9993 + """ + target = Target(dt=1) + target.add_instruction(XGate(), {(1,): InstructionProperties(duration=200)}) + # delays are not supported + + qc = QuantumCircuit(2) + qc.x(1) + + pm = PassManager(schedule_pass(target=target)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2) + expected.x(1) + # no delay on qubit 0 + + self.assertEqual(expected, scheduled) + + def test_dd_respect_target_instruction_constraints(self): + """Test if DD pass does not pad delays for qubits that do not support delay instructions + and does not insert DD gates for qubits that do not support necessary gates. + See: https://github.com/Qiskit/qiskit-terra/issues/9993 + """ + qc = QuantumCircuit(3) + qc.cx(0, 1) + qc.cx(1, 2) + + target = Target(dt=1) + # Y is partially supported (not supported on qubit 2) + target.add_instruction( + XGate(), {(q,): InstructionProperties(duration=100) for q in range(2)} + ) + target.add_instruction( + CXGate(), + { + (0, 1): InstructionProperties(duration=1000), + (1, 2): InstructionProperties(duration=1000), + }, + ) + # delays are not supported + + # No DD instructions nor delays are padded due to no delay support in the target + pm_xx = PassManager( + [ + ALAPSchedule(target=target), + DynamicalDecoupling(durations=None, dd_sequence=[XGate(), XGate()], target=target), + ] + ) + scheduled = pm_xx.run(qc) + self.assertEqual(qc, scheduled) + + # Fails since Y is not supported in the target + with self.assertRaises(TranspilerError): + PassManager( + [ + ALAPSchedule(target=target), + DynamicalDecoupling( + durations=None, + dd_sequence=[XGate(), YGate(), XGate(), YGate()], + target=target, + ), + ] + ) + + # Add delay support to the target + target.add_instruction(Delay(Parameter("t")), {(q,): None for q in range(3)}) + # No error but no DD on qubit 2 (just delay is padded) since X is not supported on it + scheduled = pm_xx.run(qc) + + expected = QuantumCircuit(3) + expected.delay(1000, [2]) + expected.cx(0, 1) + expected.cx(1, 2) + expected.delay(200, [0]) + expected.x([0]) + expected.delay(400, [0]) + expected.x([0]) + expected.delay(200, [0]) + self.assertEqual(expected, scheduled) + + +if __name__ == "__main__": + unittest.main()