From b32942e0733ddaeaf3aab5c72bf34c9acac22d95 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 11:08:02 +0000 Subject: [PATCH] Restore accidently deleted scheduling passes (#11184) (#11189) In #10754 3 legacy scheduling passes were accidently deleted. These passes were incorrectly identified as deprecated, however they were never marked as deprecating just pending future deprecation. They were intended to be be promoted from a pending deprecation to a full deprecation in #8023 but we never took that step because there were objections at the time as they still served a purpose. #10754 likely missed this as the only indication in the deprecation decorator was a kwarg that said `pending=True`, and this was the only indication that these passes weren't actually deprecated yet. This commit restores these passes on the 0.45.0 branch in the interest of unblocking the 0.45.0 release ASAP. We can handle forward porting this PR to main as needed after the 0.45.0 release is tagged. (cherry picked from commit aa272e9ef7e500a8ac10d70775b3871646169046) Co-authored-by: Matthew Treinish --- qiskit/transpiler/passes/__init__.py | 8 + .../transpiler/passes/scheduling/__init__.py | 4 + qiskit/transpiler/passes/scheduling/alap.py | 155 ++++ .../passes/scheduling/alignments/__init__.py | 1 + .../scheduling/alignments/align_measures.py | 256 ++++++ qiskit/transpiler/passes/scheduling/asap.py | 177 ++++ .../passes/scheduling/dynamical_decoupling.py | 293 +++++++ ...oved_deprecated_0.21-6c93f7bbc50ae40e.yaml | 31 +- .../test_instruction_alignments.py | 310 ++++++- .../legacy_scheduling/test_scheduling_pass.py | 811 ++++++++++++++++++ 10 files changed, 2020 insertions(+), 26 deletions(-) create mode 100644 qiskit/transpiler/passes/scheduling/alap.py create mode 100644 qiskit/transpiler/passes/scheduling/alignments/align_measures.py create mode 100644 qiskit/transpiler/passes/scheduling/asap.py create mode 100644 qiskit/transpiler/passes/scheduling/dynamical_decoupling.py create mode 100644 test/python/transpiler/legacy_scheduling/test_scheduling_pass.py 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()