diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index 4357f85940f7..86fa679b38f6 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -11,41 +11,20 @@ # that they have been altered from the originals. """ALAP Scheduling.""" -import itertools -from collections import defaultdict -from typing import List - -from qiskit.circuit import Delay, Measure -from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.circuit import Delay, Qubit, Measure from qiskit.dagcircuit import DAGCircuit -from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion +from .base_scheduler import BaseScheduler -class ALAPSchedule(TransformationPass): - """ALAP Scheduling pass, which schedules the **stop** time of instructions as late as possible. - For circuits with instructions writing or reading clbits (e.g. measurements, conditional gates), - the scheduler assumes clbits I/O operations take no time, ``measure`` locks clbits to be written - at its end and ``c_if`` locks clbits to be read at its beginning. +class ALAPSchedule(BaseScheduler): + """ALAP Scheduling pass, which schedules the **stop** time of instructions as late as possible. - Notes: - The ALAP scheduler may not schedule a circuit exactly the same as any real backend does - when the circuit contains control flows (e.g. conditional instructions). + See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseScheduler` for the + detailed behavior of the control flow operation, i.e. ``c_if``. """ - def __init__(self, durations): - """ALAPSchedule initializer. - - Args: - durations (InstructionDurations): Durations of instructions to be used in scheduling - """ - super().__init__() - self.durations = durations - # ensure op node durations are attached and in consistent unit - self.requires.append(TimeUnitConversion(durations)) - def run(self, dag): """Run the ALAPSchedule pass on `dag`. @@ -57,6 +36,7 @@ def run(self, 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") @@ -68,71 +48,95 @@ def run(self, dag): for creg in dag.cregs.values(): new_dag.add_creg(creg) - qubit_time_available = defaultdict(int) - clbit_readable = defaultdict(int) - clbit_writeable = defaultdict(int) - - def pad_with_delays(qubits: List[int], until, unit) -> None: - """Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``.""" - for q in qubits: - if qubit_time_available[q] < until: - idle_duration = until - qubit_time_available[q] - new_dag.apply_operation_front(Delay(idle_duration, unit), [q], []) - + idle_before = {q: 0 for q in dag.qubits + dag.clbits} bit_indices = {bit: index for index, bit in enumerate(dag.qubits)} for node in reversed(list(dag.topological_op_nodes())): - # validate node.op.duration - if node.op.duration is None: - indices = [bit_indices[qarg] for qarg in node.qargs] - if dag.has_calibration_for(node): - node.op.duration = dag.calibrations[node.op.name][ - (tuple(indices), tuple(float(p) for p in node.op.params)) - ].duration - - if node.op.duration is None: + op_duration = self._get_node_duration(node, bit_indices, 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"Duration of {node.op.name} on qubits {indices} is not found." + f"Conditional instruction {node.op.name} is not supported in ALAP scheduler." ) - if isinstance(node.op.duration, ParameterExpression): - indices = [bit_indices[qarg] for qarg in node.qargs] - raise TranspilerError( - f"Parameterized duration ({node.op.duration}) " - f"of {node.op.name} on qubits {indices} is not bounded." - ) - # 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 = 0 if isinstance(node.op, Measure) else node.op.duration - # must wait for op.condition_bits as well as node.cargs - start_time = max( - itertools.chain( - (qubit_time_available[q] for q in node.qargs), - (clbit_time_available[c] - delta for c in node.cargs + node.op.condition_bits), - ) - ) - - pad_with_delays(node.qargs, until=start_time, unit=time_unit) - new_dag.apply_operation_front(node.op, node.qargs, node.cargs) + 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: + new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) + idle_before[bit] = t1 - stop_time = start_time + node.op.duration - # update time table - for q in node.qargs: - qubit_time_available[q] = stop_time - for c in node.cargs: # measure - clbit_writeable[c] = clbit_readable[c] = start_time - for c in node.op.condition_bits: # conditional op - clbit_writeable[c] = max(stop_time, clbit_writeable[c]) + new_dag.apply_operation_front(node.op, node.qargs, node.cargs) - 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) + 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 + new_dag.apply_operation_front(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/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index 5c0d08dda2eb..5c3be528fb8f 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -11,41 +11,20 @@ # that they have been altered from the originals. """ASAP Scheduling.""" -import itertools -from collections import defaultdict -from typing import List - -from qiskit.circuit import Delay, Measure -from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.circuit import Delay, Qubit, Measure from qiskit.dagcircuit import DAGCircuit -from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion +from .base_scheduler import BaseScheduler -class ASAPSchedule(TransformationPass): - """ASAP Scheduling pass, which schedules the start time of instructions as early as possible.. - For circuits with instructions writing or reading clbits (e.g. measurements, conditional gates), - the scheduler assumes clbits I/O operations take no time, ``measure`` locks clbits to be written - at its end and ``c_if`` locks clbits to be read at its beginning. +class ASAPSchedule(BaseScheduler): + """ASAP Scheduling pass, which schedules the start time of instructions as early as possible.. - Notes: - The ASAP scheduler may not schedule a circuit exactly the same as any real backend does - when the circuit contains control flows (e.g. conditional instructions). + See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseScheduler` for the + detailed behavior of the control flow operation, i.e. ``c_if``. """ - def __init__(self, durations): - """ASAPSchedule initializer. - - Args: - durations (InstructionDurations): Durations of instructions to be used in scheduling - """ - super().__init__() - self.durations = durations - # ensure op node durations are attached and in consistent unit - self.requires.append(TimeUnitConversion(durations)) - def run(self, dag): """Run the ASAPSchedule pass on `dag`. @@ -57,6 +36,7 @@ def run(self, 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") @@ -69,70 +49,105 @@ def run(self, dag): for creg in dag.cregs.values(): new_dag.add_creg(creg) - qubit_time_available = defaultdict(int) - clbit_readable = defaultdict(int) - clbit_writeable = defaultdict(int) - - def pad_with_delays(qubits: List[int], until, unit) -> None: - """Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``.""" - for q in qubits: - if qubit_time_available[q] < until: - idle_duration = until - qubit_time_available[q] - new_dag.apply_operation_back(Delay(idle_duration, unit), [q]) - + idle_after = {q: 0 for q in dag.qubits + dag.clbits} bit_indices = {q: index for index, q in enumerate(dag.qubits)} for node in dag.topological_op_nodes(): - # validate node.op.duration - if node.op.duration is None: - indices = [bit_indices[qarg] for qarg in node.qargs] - if dag.has_calibration_for(node): - node.op.duration = dag.calibrations[node.op.name][ - (tuple(indices), tuple(float(p) for p in node.op.params)) - ].duration - - if node.op.duration is None: + op_duration = self._get_node_duration(node, bit_indices, 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"Duration of {node.op.name} on qubits {indices} is not found." + f"Conditional instruction {node.op.name} is not supported in ASAP scheduler." ) - if isinstance(node.op.duration, ParameterExpression): - indices = [bit_indices[qarg] for qarg in node.qargs] - raise TranspilerError( - f"Parameterized duration ({node.op.duration}) " - f"of {node.op.name} on qubits {indices} is not bounded." - ) - # 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 - # must wait for op.condition_bits as well as node.cargs - start_time = max( - itertools.chain( - (qubit_time_available[q] for q in node.qargs), - (clbit_time_available[c] - delta for c in node.cargs + node.op.condition_bits), - ) - ) - - pad_with_delays(node.qargs, until=start_time, unit=time_unit) - new_dag.apply_operation_back(node.op, node.qargs, node.cargs) + 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): + new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) + idle_after[bit] = t1 - stop_time = start_time + node.op.duration - # update time table - for q in node.qargs: - qubit_time_available[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]) + new_dag.apply_operation_back(node.op, node.qargs, node.cargs) - 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) + 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 + 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 diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py new file mode 100644 index 000000000000..b0b5581a0d79 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -0,0 +1,269 @@ +# 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. + +"""Base circuit scheduling pass.""" + +from typing import Dict +from qiskit.transpiler import InstructionDurations +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion +from qiskit.dagcircuit import DAGOpNode, DAGCircuit +from qiskit.circuit import Delay, Gate +from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.transpiler.exceptions import TranspilerError + + +class BaseScheduler(TransformationPass): + """Base scheduler pass. + + Policy of topological node ordering in scheduling + + The DAG representation of ``QuantumCircuit`` respects the node ordering also in the + classical register wires, though theoretically two conditional instructions + conditioned on the same register are commute, i.e. read-access to the + classical register doesn't change its state. + + .. parsed-literal:: + + qc = QuantumCircuit(2, 1) + qc.delay(100, 0) + qc.x(0).c_if(0, True) + qc.x(1).c_if(0, True) + + The scheduler SHOULD comply with above topological ordering policy of the DAG circuit. + Accordingly, the `asap`-scheduled circuit will become + + .. parsed-literal:: + + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── + ├────────────────┤ └─╥─┘ ┌───┐ + q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── + └────────────────┘ ║ └─╥─┘ + ┌────╨────┐┌────╨────┐ + c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ + └─────────┘└─────────┘ + + Note that this scheduling might be inefficient in some cases, + because the second conditional operation can start without waiting the delay of 100 dt. + However, such optimization should be done by another pass, + otherwise scheduling may break topological ordering of the original circuit. + + Realistic control flow scheduling respecting for microarcitecture + + In the dispersive QND readout scheme, qubit is measured with microwave stimulus to qubit (Q) + followed by resonator ring-down (depopulation). This microwave signal is recorded + in the buffer memory (B) with hardware kernel, then a discriminated (D) binary value + is moved to the classical register (C). + The sequence from t0 to t1 of the measure instruction interval might be modeled as follows: + + .. parsed-literal:: + + Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + B ░░▒▒▒▒▒▒▒▒░░░░░░░░░ + D ░░░░░░░░░░▒▒▒▒▒▒░░░ + C ░░░░░░░░░░░░░░░░▒▒░ + + However, ``QuantumCircuit`` representation is not enough accurate to represent + this model. In the circuit representation, thus ``Qubit`` is occupied by the + stimulus microwave signal during the first half of the interval, + and ``Clbit`` is only occupied at the very end of the interval. + + This precise model may induce weird edge case. + + .. parsed-literal:: + + ┌───┐ + q_0: ───┤ X ├────── + └─╥─┘ ┌─┐ + q_1: ─────╫─────┤M├ + ┌────╨────┐└╥┘ + c: 1/╡ c_0=0x1 ╞═╩═ + └─────────┘ 0 + + In this example, user may intend to measure the state of ``q_1``, after ``XGate`` is + applied to the ``q_0``. This is correct interpretation from viewpoint of + the topological node ordering, i.e. x gate node come in front of the measure node. + However, according to the measurement model above, the data in the register + is unchanged during the stimulus, thus two nodes are simultaneously operated. + If one `alap`-schedule this circuit, it may return following circuit. + + .. parsed-literal:: + + ┌────────────────┐ ┌───┐ + q_0: ┤ Delay(500[dt]) ├───┤ X ├────── + └────────────────┘ └─╥─┘ ┌─┐ + q_1: ───────────────────────╫─────┤M├ + ┌────╨────┐└╥┘ + c: 1/══════════════════╡ c_0=0x1 ╞═╩═ + └─────────┘ 0 + + Note that there is no delay on ``q_1`` wire, and the measure instruction immediately + start after t=0, while the conditional gate starts after the delay. + It looks like the topological ordering between the nodes are flipped in the scheduled view. + This behavior can be understood by considering the control flow model described above, + + .. parsed-literal:: + + : Quantum Circuit, first-measure + 0 ░░░░░░░░░░░░▒▒▒▒▒▒░ + 1 ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + + : In wire q0 + Q ░░░░░░░░░░░░░░░▒▒▒░ + C ░░░░░░░░░░░░▒▒░░░░░ + + : In wire q1 + Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + B ░░▒▒▒▒▒▒▒▒░░░░░░░░░ + D ░░░░░░░░░░▒▒▒▒▒▒░░░ + C ░░░░░░░░░░░░░░░░▒▒░ + + Since there is no qubit register (Q0, Q1) overlap, the node ordering is determined by the + shared classical register C. As you can see, the execution order is still + preserved on C, i.e. read C then apply ``XGate``, finally store the measured outcome in C. + Because ``DAGOpNode`` cannot define different durations for associated registers, + the time ordering of two nodes is inverted anyways. + + This behavior can be controlled by ``clbit_write_latency`` and ``conditional_latency``. + The former parameter determines the delay of the register write-access from + the beginning of the measure instruction t0, and another parameter determines + the delay of conditional gate operation from t0 which comes from the register read-access. + + Since we usually expect topological ordering and time ordering are identical + without the context of microarchitecture, both latencies are set to zero by default. + In this case, ``Measure`` instruction immediately locks the register C. + Under this configuration, the `alap`-scheduled circuit of above example may become + + .. parsed-literal:: + + ┌───┐ + q_0: ───┤ X ├────── + └─╥─┘ ┌─┐ + q_1: ─────╫─────┤M├ + ┌────╨────┐└╥┘ + c: 1/╡ c_0=0x1 ╞═╩═ + └─────────┘ 0 + + If the backend microarchitecture supports smart scheduling of the control flow, i.e. + it may separately schedule qubit and classical register, + insertion of the delay yields unnecessary longer total execution time. + + .. parsed-literal:: + : Quantum Circuit, first-xgate + 0 ░▒▒▒░░░░░░░░░░░░░░░ + 1 ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + + : In wire q0 + Q ░▒▒▒░░░░░░░░░░░░░░░ + C ░░░░░░░░░░░░░░░░░░░ (zero latency) + + : In wire q1 + Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + C ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ (zero latency, scheduled after C0 read-access) + + However this result is much more intuitive in the topological ordering view. + If finite conditional latency is provided, for example, 30 dt, the circuit + is scheduled as follows. + + .. parsed-literal:: + + ┌───────────────┐ ┌───┐ + q_0: ┤ Delay(30[dt]) ├───┤ X ├────── + ├───────────────┤ └─╥─┘ ┌─┐ + q_1: ┤ Delay(30[dt]) ├─────╫─────┤M├ + └───────────────┘┌────╨────┐└╥┘ + c: 1/═════════════════╡ c_0=0x1 ╞═╩═ + └─────────┘ 0 + + with the timing model: + + .. parsed-literal:: + : Quantum Circuit, first-xgate + 0 ░░▒▒▒░░░░░░░░░░░░░░░ + 1 ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + + : In wire q0 + Q ░░▒▒▒░░░░░░░░░░░░░░░ + C ░▒░░░░░░░░░░░░░░░░░░ (30dt latency) + + : In wire q1 + Q ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + C ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ + + See https://arxiv.org/abs/2102.01682 for more details. + + """ + + CONDITIONAL_SUPPORTED = (Gate, Delay) + + def __init__( + self, + durations: InstructionDurations, + clbit_write_latency: int = 0, + conditional_latency: int = 0, + ): + """Scheduler initializer. + + Args: + durations: Durations of instructions to be used in scheduling + clbit_write_latency: A control flow constraints. Because standard superconducting + quantum processor implement dispersive QND readout, the actual data transfer + to the clbit happens after the round-trip stimulus signal is buffered + and discriminated into quantum state. + The interval ``[t0, t0 + clbit_write_latency]`` is regarded as idle time + for clbits associated with the measure instruction. + This defaults to 0 dt which is identical to Qiskit Pulse scheduler. + conditional_latency: A control flow constraints. This value represents + a latency of reading a classical register for the conditional operation. + The gate operation occurs after this latency. This appears as a delay + in front of the DAGOpNode of the gate. + This defaults to 0 dt. + """ + super().__init__() + self.durations = durations + + # Control flow constraints. + self.clbit_write_latency = clbit_write_latency + self.conditional_latency = conditional_latency + + # Ensure op node durations are attached and in consistent unit + self.requires.append(TimeUnitConversion(durations)) + + @staticmethod + def _get_node_duration( + node: DAGOpNode, + bit_index_map: Dict, + dag: DAGCircuit, + ) -> int: + """A helper method to get duration from node or calibration.""" + indices = [bit_index_map[qarg] for qarg in node.qargs] + + if dag.has_calibration_for(node): + # If node has calibration, this value should be the highest priority + cal_key = tuple(indices), tuple(float(p) for p in node.op.params) + duration = dag.calibrations[node.op.name][cal_key].duration + else: + duration = node.op.duration + + if isinstance(duration, ParameterExpression): + raise TranspilerError( + f"Parameterized duration ({duration}) " + f"of {node.op.name} on qubits {indices} is not bounded." + ) + if duration is None: + raise TranspilerError(f"Duration of {node.op.name} on qubits {indices} is not found.") + + return duration + + def run(self, dag: DAGCircuit): + raise NotImplementedError diff --git a/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml b/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml new file mode 100644 index 000000000000..2199eb686514 --- /dev/null +++ b/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml @@ -0,0 +1,91 @@ +--- +upgrade: + - | + The circuit scheduling passes + :class:`~qiskit.transpiler.passes.scheduling.asap.ASAPSchedule` and + :class:`~qiskit.transpiler.passes.scheduling.alap.ALAPSchedule` have been upgraded. + Now these passes can be instantiated with two new parameters + ``clbit_write_latency`` and ``conditional_latency``, which allows scheduler to + more carefully schedule instructions with classical feedback. + The former option represents a latency of clbit access from the begging of the + measurement instruction, and the other represents a latency of the + conditional gate operation from the first conditional clbit read-access. + + The standard behavior of these passes has been also upgraded to align + timing ordering with the topological ordering of the DAG nodes. + This change may affect the scheduling outcome if it includes conditional operations, + or simultaneously measuring two qubits with the same classical register (edge-case). + To reproduce conventional behavior, create a pass manager with one of + scheduling passes with ``clbit_write_latency`` identical to the measurement instruction length. + + The following example may clearly explain the change of scheduler spec. + + .. parsed-literal:: + + ┌───┐┌─┐ + q_0: ┤ X ├┤M├────────────── + └───┘└╥┘ ┌───┐ + q_1: ──────╫────┤ X ├────── + ║ └─╥─┘ ┌─┐ + q_2: ──────╫──────╫─────┤M├ + ║ ┌────╨────┐└╥┘ + c: 1/══════╩═╡ c_0=0x1 ╞═╩═ + 0 └─────────┘ 0 + + The conventional behavior is as follows. + + .. jupyter-execute:: + + from qiskit import QuantumCircuit + from qiskit.transpiler import InstructionDurations, PassManager + from qiskit.transpiler.passes import ALAPSchedule + from qiskit.visualization.timeline import draw + + circuit = QuantumCircuit(3, 1) + circuit.x(0) + circuit.measure(0, 0) + circuit.x(1).c_if(0, 1) + circuit.measure(2, 0) + + durations = InstructionDurations([("x", None, 160), ("measure", None, 800)]) + + pm = PassManager( + ALAPSchedule(durations, clbit_write_latency=800, conditional_latency=0) + ) + draw(pm.run(circuit)) + + As you can see in the timeline view, the measurement on ``q_2`` starts before + the conditional X gate on the ``q_1``, which seems to be opposite to the + topological ordering of the node. This is also expected behavior + because clbit write-access happens at the end edge of the measure instruction, + and the read-access of the conditional gate happens the begin edge of the instruction. + Thus topological ordering is preserved on the timeslot of the classical register, + which is not captured by the timeline view. + However, this assumes a paticular microarchitecture design, and the circuit is + not necessary scheduled like this. + + By using the default configuration of passes, the circuit is schedule like below. + + .. jupyter-execute:: + + from qiskit import QuantumCircuit + from qiskit.transpiler import InstructionDurations, PassManager + from qiskit.transpiler.passes import ALAPSchedule + from qiskit.visualization.timeline import draw + + circuit = QuantumCircuit(3, 1) + circuit.x(0) + circuit.measure(0, 0) + circuit.x(1).c_if(0, 1) + circuit.measure(2, 0) + + durations = InstructionDurations([("x", None, 160), ("measure", None, 800)]) + + pm = PassManager(ALAPSchedule(durations)) + draw(pm.run(circuit)) + + Note that clbit is locked throughout the measurement instruction interval. + This behavior is designed based on the Qiskit Pulse, in which the acquire instruction takes + ``AcquireChannel`` and ``MemorySlot`` which are not allowed to overlap with other instructions, + i.e. simultaneous memory access from the different instructions is prohibited. + This also always aligns the timing ordering with the topological node ordering. diff --git a/test/python/transpiler/test_instruction_alignments.py b/test/python/transpiler/test_instruction_alignments.py index c9d09ee4af2d..4822300ffcee 100644 --- a/test/python/transpiler/test_instruction_alignments.py +++ b/test/python/transpiler/test_instruction_alignments.py @@ -44,7 +44,13 @@ def setUp(self): ] ) self.time_conversion_pass = TimeUnitConversion(inst_durations=instruction_durations) - self.scheduling_pass = ALAPSchedule(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): diff --git a/test/python/transpiler/test_scheduling_pass.py b/test/python/transpiler/test_scheduling_pass.py index d63dff60d9dc..5693ba0e4f27 100644 --- a/test/python/transpiler/test_scheduling_pass.py +++ b/test/python/transpiler/test_scheduling_pass.py @@ -14,7 +14,7 @@ import unittest -from ddt import ddt, data +from ddt import ddt, data, unpack from qiskit import QuantumCircuit from qiskit.test import QiskitTestCase from qiskit.transpiler.instruction_durations import InstructionDurations @@ -51,7 +51,7 @@ def test_alap_agree_with_reverse_asap_reverse(self): @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/7006 + See: https://github.com/Qiskit/qiskit-terra/issues/7654 (input) ┌─┐ @@ -70,7 +70,7 @@ def test_classically_controlled_gate_after_measure(self, schedule_pass): q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├─────── └─────────────────┘ ║ └─╥─┘ ║ ┌────╨────┐ - c: 1/════════════════════╩════╡ c_0 = T ╞════ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════ 0 └─────────┘ """ qc = QuantumCircuit(2, 1) @@ -83,16 +83,16 @@ def test_classically_controlled_gate_after_measure(self, schedule_pass): expected = QuantumCircuit(2, 1) expected.measure(0, 0) - expected.delay(200, 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/7006 + See: https://github.com/Qiskit/qiskit-terra/issues/7654 (input) ┌───┐┌─┐ @@ -104,13 +104,13 @@ def test_measure_after_measure(self, schedule_pass): 0 0 (scheduled) - ┌───┐ ┌─┐ - q_0: ──────┤ X ├───────┤M├─── - ┌─────┴───┴──────┐└╥┘┌─┐ - q_1: ┤ Delay(200[dt]) ├─╫─┤M├ - └────────────────┘ ║ └╥┘ - c: 1/═══════════════════╩══╩═ - 0 0 + ┌───┐ ┌─┐┌─────────────────┐ + q_0: ───────┤ X ├───────┤M├┤ Delay(1000[dt]) ├ + ┌──────┴───┴──────┐└╥┘└───────┬─┬───────┘ + q_1: ┤ Delay(1200[dt]) ├─╫─────────┤M├──────── + └─────────────────┘ ║ └╥┘ + c: 1/════════════════════╩══════════╩═════════ + 0 0 """ qc = QuantumCircuit(2, 1) qc.x(0) @@ -124,8 +124,9 @@ def test_measure_after_measure(self, schedule_pass): expected = QuantumCircuit(2, 1) expected.x(0) expected.measure(0, 0) - expected.delay(200, 1) # 2nd measure starts at the same time as 1st measure starts + expected.delay(1200, 1) expected.measure(1, 0) + expected.delay(1000, 0) self.assertEqual(expected, scheduled) @@ -146,6 +147,7 @@ def test_c_if_on_different_qubits(self, schedule_pass): 0 └─────────┘└─────────┘ (scheduled) + ┌─┐┌────────────────┐ q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─────────── ┌─────────────────┐└╥┘└─────┬───┬──────┘ @@ -154,7 +156,7 @@ def test_c_if_on_different_qubits(self, schedule_pass): q_2: ┤ Delay(1000[dt]) ├─╫─────────╫────────────┤ X ├─── └─────────────────┘ ║ ║ └─╥─┘ ║ ┌────╨────┐ ┌────╨────┐ - c: 1/════════════════════╩════╡ c_0 = T ╞════╡ c_0 = T ╞ + c: 1/════════════════════╩════╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ 0 └─────────┘ └─────────┘ """ qc = QuantumCircuit(3, 1) @@ -168,11 +170,11 @@ def test_c_if_on_different_qubits(self, schedule_pass): expected = QuantumCircuit(3, 1) expected.measure(0, 0) - expected.delay(200, 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) @@ -190,30 +192,32 @@ def test_shorter_measure_after_measure(self, schedule_pass): 0 0 (scheduled) - ┌─┐ - q_0: ──────────────────┤M├─── - ┌────────────────┐└╥┘┌─┐ - q_1: ┤ Delay(300[dt]) ├─╫─┤M├ - └────────────────┘ ║ └╥┘ - c: 1/═══════════════════╩══╩═ - 0 0 + ┌─┐┌────────────────┐ + 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)]) + 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(300, 1) + expected.delay(1000, 1) expected.measure(1, 0) + expected.delay(700, 0) self.assertEqual(expected, scheduled) - def test_measure_after_c_if(self): + @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) @@ -227,7 +231,186 @@ def test_measure_after_c_if(self): c: 1/═╩═╡ c_0 = T ╞═╩═ 0 └─────────┘ 0 - (scheduled - ASAP) + (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]) ├───────────────────── ┌─────────────────┐└╥┘└─────┬───┬──────┘ @@ -235,10 +418,10 @@ def test_measure_after_c_if(self): └─────────────────┘ ║ └─╥─┘ ┌─┐┌────────────────┐ q_2: ────────────────────╫─────────╫─────────┤M├┤ Delay(200[dt]) ├ ║ ┌────╨────┐ └╥┘└────────────────┘ - c: 1/════════════════════╩════╡ c_0 = T ╞═════╩═══════════════════ + c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═══════════════════ 0 └─────────┘ 0 - (scheduled - ALAP) + (ALAP scheduled) ┌─┐┌────────────────┐ q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─── ┌─────────────────┐└╥┘└─────┬───┬──────┘ @@ -246,8 +429,9 @@ def test_measure_after_c_if(self): └┬────────────────┤ ║ └─╥─┘ ┌─┐ q_2: ─┤ Delay(200[dt]) ├─╫─────────╫─────────┤M├ └────────────────┘ ║ ┌────╨────┐ └╥┘ - c: 1/════════════════════╩════╡ c_0 = T ╞═════╩═ + c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═ 0 └─────────┘ 0 + """ qc = QuantumCircuit(3, 1) qc.measure(0, 0) @@ -255,27 +439,284 @@ def test_measure_after_c_if(self): qc.measure(2, 0) durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - actual_asap = PassManager(ASAPSchedule(durations)).run(qc) - actual_alap = PassManager(ALAPSchedule(durations)).run(qc) + + # 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(200, 0) expected_asap.delay(1000, 1) expected_asap.x(1).c_if(0, 1) expected_asap.measure(2, 0) - expected_asap.delay(200, 2) # delay after measure on q_2 + expected_asap.delay(200, 0) + expected_asap.delay(200, 2) self.assertEqual(expected_asap, actual_asap) - expected_aslp = QuantumCircuit(3, 1) - expected_aslp.measure(0, 0) - expected_aslp.delay(200, 0) - expected_aslp.delay(1000, 1) - expected_aslp.x(1).c_if(0, 1) - expected_aslp.delay(200, 2) - expected_aslp.measure(2, 0) # delay before measure on q_2 - self.assertEqual(expected_aslp, actual_alap) + 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) if __name__ == "__main__":