diff --git a/docs/apidocs/ibm-provider.rst b/docs/apidocs/ibm-provider.rst index a98f08d10..7b962792b 100644 --- a/docs/apidocs/ibm-provider.rst +++ b/docs/apidocs/ibm-provider.rst @@ -10,3 +10,4 @@ qiskit-ibm-provider API Reference ibm_visualization ibm_jupyter ibm_utils + ibm_transpiler diff --git a/docs/apidocs/ibm_transpiler.rst b/docs/apidocs/ibm_transpiler.rst new file mode 100644 index 000000000..db99eb170 --- /dev/null +++ b/docs/apidocs/ibm_transpiler.rst @@ -0,0 +1,6 @@ +.. _qiskit_ibm_provider-utils: + +.. automodule:: qiskit_ibm_provider.transpiler + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/qiskit_ibm_provider/transpiler/__init__.py b/qiskit_ibm_provider/transpiler/__init__.py new file mode 100644 index 000000000..d6e62daa4 --- /dev/null +++ b/qiskit_ibm_provider/transpiler/__init__.py @@ -0,0 +1,31 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +==================================================================== +IBM Backend Transpiler Tools (:mod:`qiskit_ibm_provider.transpiler`) +==================================================================== + +A collection of transpiler tools for working with IBM Quantum's +next-generation backends that support advanced "dynamic circuit" +capabilities. Ie., circuits with support for classical +compute and control-flow/feedback based off of measurement results. + +Transpiler Passes +================== + +.. autosummary:: + :toctree: ../stubs/ + + passes + +""" diff --git a/qiskit_ibm_provider/transpiler/passes/__init__.py b/qiskit_ibm_provider/transpiler/passes/__init__.py new file mode 100644 index 000000000..0bae4b142 --- /dev/null +++ b/qiskit_ibm_provider/transpiler/passes/__init__.py @@ -0,0 +1,30 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +================================================================ +Transpiler Passes (:mod:`qiskit_ibm_provider.transpiler.passes`) +================================================================ + +.. currentmodule:: qiskit_ibm_provider.transpiler.passes + +.. autosummary:: + :toctree: ../stubs/ + + scheduling + + +""" + +# circuit scheduling +from .scheduling import DynamicCircuitScheduleAnalysis +from .scheduling import PadDelay diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py new file mode 100644 index 000000000..cae47ead2 --- /dev/null +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/__init__.py @@ -0,0 +1,85 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +==================================================================== +Scheduling (:mod:`qiskit_ibm_provider.transpiler.passes.scheduling`) +==================================================================== + +.. currentmodule:: qiskit_ibm_provider.transpiler.passes.scheduling + +A collection of scheduling passes for working with IBM Quantum's next-generation +backends that support advanced "dynamic circuit" capabilities. Ie., +circuits with support for classical control-flow/feedback based off +of measurement results. + + +Below we demonstrate how to schedule and pad a teleportation circuit with delays +for a dynamic circuit backend's execution model + + +.. jupyter-execute:: + + from qiskit import transpile + from qiskit.circuit import ClassicalRegister, QuantumCircuit, QuantumRegister + from qiskit.transpiler.instruction_durations import InstructionDurations + from qiskit.transpiler.passmanager import PassManager + + from qiskit_ibm_provider.transpiler.passes.scheduling import DynamicCircuitScheduleAnalysis, PadDelay + from qiskit.providers.fake_provider.backends.jakarta.fake_jakarta import FakeJakarta + + + backend = FakeJakarta() + + durations = InstructionDurations.from_backend(backend) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + + qr = QuantumRegister(3) + crz = ClassicalRegister(1, name="crz") + crx = ClassicalRegister(1, name="crx") + result = ClassicalRegister(1, name="result") + + teleport = QuantumCircuit(qr, crz, crx, result, name="Teleport") + + teleport.h(qr[1]) + teleport.cx(qr[1], qr[2]) + teleport.cx(qr[0], qr[1]) + teleport.h(qr[0]) + teleport.measure(qr[0], crz) + teleport.measure(qr[1], crx) + teleport.z(qr[2]).c_if(crz, 1) + teleport.x(qr[2]).c_if(crx, 1) + teleport.measure(qr[2], result) + + teleport = transpile(teleport, backend) + + scheduled_teleport = pm.run(teleport) + + scheduled_teleport.draw(output="mpl") + + +Scheduling & Dynamical Decoupling +================================= +.. autosummary:: + :toctree: ../stubs/ + + BlockBasePadder + DynamicCircuitScheduleAnalysis + PadDelay + + + +""" + +from .block_base_padder import BlockBasePadder +from .pad_delay import PadDelay +from .scheduler import DynamicCircuitScheduleAnalysis diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py new file mode 100644 index 000000000..b4dac3c15 --- /dev/null +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/block_base_padder.py @@ -0,0 +1,310 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Padding pass to fill timeslots for IBM (dynamic circuit) backends.""" + +from typing import Any, Dict, List, Optional, Union + +from qiskit.circuit import Qubit, Clbit, Instruction +from qiskit.circuit.library import Barrier +from qiskit.circuit.delay import Delay +from qiskit.dagcircuit import DAGCircuit, DAGNode +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.exceptions import TranspilerError + + +class BlockBasePadder(TransformationPass): + """The base class of padding pass. + + This pass requires one of scheduling passes to be executed before itself. + Since there are multiple scheduling strategies, the selection of scheduling + pass is left in the hands of the pass manager designer. + Once a scheduling analysis pass is run, ``node_start_time`` is generated + in the :attr:`property_set`. This information is represented by a python dictionary of + the expected instruction execution times keyed on the node instances. + Entries in the dictionary are only created for non-delay nodes. + The padding pass expects all ``DAGOpNode`` in the circuit to be scheduled. + + This base class doesn't define any sequence to interleave, but it manages + the location where the sequence is inserted, and provides a set of information necessary + to construct the proper sequence. Thus, a subclass of this pass just needs to implement + :meth:`_pad` method, in which the subclass constructs a circuit block to insert. + This mechanism removes lots of boilerplate logic to manage whole DAG circuits. + + Note that padding pass subclasses should define interleaving sequences satisfying: + + - Interleaved sequence does not change start time of other nodes + - Interleaved sequence should have total duration of the provided ``time_interval``. + + Any manipulation violating these constraints may prevent this base pass from correctly + tracking the start time of each instruction, + which may result in violation of hardware alignment constraints. + """ + + def __init__(self) -> None: + self._node_start_time = None + self._idle_after: Optional[Dict[Qubit, int]] = None + self._dag = None + self._prev_node: Optional[DAGNode] = None + self._block_duration = 0 + self._current_block_idx = 0 + self._conditional_block = False + + super().__init__() + + def run(self, dag: DAGCircuit) -> DAGCircuit: + """Run the padding pass on ``dag``. + + Args: + dag: DAG to be checked. + + Returns: + DAGCircuit: DAG with idle time filled with instructions. + + Raises: + TranspilerError: When a particular node is not scheduled, likely some transform pass + is inserted before this node is called. + """ + self._pre_runhook(dag) + + self._init_run(dag) + + # Compute fresh circuit duration from the node start time dictionary and op duration. + # Note that pre-scheduled duration may change within the alignment passes, i.e. + # if some instruction time t0 violating the hardware alignment constraint, + # the alignment pass may delay t0 and accordingly the circuit duration changes. + for node in dag.topological_op_nodes(): + if node in self._node_start_time: + if isinstance(node.op, Delay): + self._visit_delay(node) + else: + self._visit_generic(node) + + else: + raise TranspilerError( + f"Operation {repr(node)} is likely added after the circuit is scheduled. " + "Schedule the circuit again if you transformed it." + ) + + self._prev_node = node + + # terminate final block + self._terminate_block(self._block_duration, self._current_block_idx, None) + + return self._dag + + def _init_run(self, dag: DAGCircuit) -> None: + """Setup for initial run.""" + self._node_start_time = self.property_set["node_start_time"].copy() + self._idle_after = {bit: 0 for bit in dag.qubits} + self._current_block_idx = 0 + self._conditional_block = False + self._block_duration = 0 + + # Prepare DAG to pad + self._dag = DAGCircuit() + for qreg in dag.qregs.values(): + self._dag.add_qreg(qreg) + for creg in dag.cregs.values(): + self._dag.add_creg(creg) + + # Update start time dictionary for the new_dag. + # This information may be used for further scheduling tasks, + # but this is immediately invalidated because most node ids are updated in the new_dag. + self.property_set["node_start_time"].clear() + + self._dag.name = dag.name + self._dag.metadata = dag.metadata + self._dag.unit = self.property_set["time_unit"] + self._dag.calibrations = dag.calibrations + self._dag.global_phase = dag.global_phase + + self._prev_node = None + + def _pre_runhook(self, dag: DAGCircuit) -> None: + """Extra routine inserted before running the padding pass. + + Args: + dag: DAG circuit on which the sequence is applied. + + Raises: + TranspilerError: If the whole circuit or instruction is not scheduled. + """ + if "node_start_time" not in self.property_set: + raise TranspilerError( + f"The input circuit {dag.name} is not scheduled. Call one of scheduling passes " + f"before running the {self.__class__.__name__} pass." + ) + + def _pad( + self, + block_idx: int, + qubit: Qubit, + t_start: int, + t_end: int, + next_node: DAGNode, + prev_node: DAGNode, + ) -> None: + """Interleave instruction sequence in between two nodes. + + .. note:: + If a DAGOpNode is added here, it should update node_start_time property + in the property set so that the added node is also scheduled. + This is achieved by adding operation via :meth:`_apply_scheduled_op`. + + .. note:: + + This method doesn't check if the total duration of new DAGOpNode added here + is identical to the interval (``t_end - t_start``). + A developer of the pass must guarantee this is satisfied. + If the duration is greater than the interval, your circuit may be + compiled down to the target code with extra duration on the backend compiler, + which is then played normally without error. However, the outcome of your circuit + might be unexpected due to erroneous scheduling. + + Args: + block_idx: Execution block index for this node. + qubit: The wire that the sequence is applied on. + t_start: Absolute start time of this interval. + t_end: Absolute end time of this interval. + next_node: Node that follows the sequence. + prev_node: Node ahead of the sequence. + """ + raise NotImplementedError + + def _visit_delay(self, node: DAGNode) -> None: + """The padding class considers a delay instruction as idle time + rather than instruction. Delay node is not added so that + we can extract non-delay predecessors. + """ + pass + + def _visit_generic(self, node: DAGNode) -> None: + """Visit a generic node to pad.""" + # Note: t0 is the relative time with respect to the current block specified + # by block_idx. + block_idx, t0 = self._node_start_time[node] # pylint: disable=invalid-name + + # Trigger the end of a block + if block_idx > self._current_block_idx: + self._terminate_block(self._block_duration, self._current_block_idx, node) + + # This block will not be padded as it is conditional. + # See TODO below. + self._conditional_block = bool(node.op.condition_bits) + + # Now set the current block index. + self._current_block_idx = block_idx + + t1 = t0 + node.op.duration # pylint: disable=invalid-name + self._block_duration = max(self._block_duration, t1) + + for bit in node.qargs: + # Fill idle time with some sequence + if t0 - self._idle_after[bit] > 0: + # Find previous node on the wire, i.e. always the latest node on the wire + prev_node = next(self._dag.predecessors(self._dag.output_map[bit])) + self._pad( + block_idx=block_idx, + qubit=bit, + t_start=self._idle_after[bit], + t_end=t0, + next_node=node, + prev_node=prev_node, + ) + + self._idle_after[bit] = t1 + + self._apply_scheduled_op(block_idx, t0, node.op, node.qargs, node.cargs) + + def _terminate_block( + self, block_duration: int, block_idx: int, node: Optional[DAGNode] + ) -> None: + """Terminate the end of a block scheduling region.""" + # Update all other qubits as not idle so that delays are *not* + # inserted. This is because we need the delays to be inserted in + # the conditional circuit block. However, c_if currently only + # allows writing a single conditional gate. + # TODO: This should be reworked to instead apply a transformation + # pass to rewrite all ``c_if`` operations as ``if_else`` + # blocks that are in turn scheduled. + if not self._conditional_block: + self._pad_until_block_end(block_duration, block_idx) + + def _is_terminating_barrier(node: Optional[DAGNode]) -> bool: + return ( + node + and isinstance(node.op, Barrier) + and len(node.qargs) == self._dag.num_qubits() + ) + + # Only add a barrier to the end if a viable barrier is not already present on all qubits + is_terminating_barrier = _is_terminating_barrier( + self._prev_node + ) or _is_terminating_barrier(node) + if not is_terminating_barrier: + # Terminate with a barrier to be clear timing is non-deterministic + # across the barrier. + self._apply_scheduled_op( + block_idx, + block_duration, + Barrier(self._dag.num_qubits()), + self._dag.qubits, + [], + ) + + # Reset idles for the new block. + self._idle_after = {bit: 0 for bit in self._dag.qubits} + self._block_duration = 0 + self._conditional_block = False + + def _pad_until_block_end(self, block_duration: int, block_idx: int) -> None: + # Add delays until the end of circuit. + for bit in self._dag.qubits: + if block_duration - self._idle_after[bit] > 0: + node = self._dag.output_map[bit] + prev_node = next(self._dag.predecessors(node)) + self._pad( + block_idx=block_idx, + qubit=bit, + t_start=self._idle_after[bit], + t_end=block_duration, + next_node=node, + prev_node=prev_node, + ) + + def _apply_scheduled_op( + self, + block_idx: int, + t_start: int, + oper: Instruction, + qubits: Union[Qubit, List[Qubit]], + clbits: Optional[Union[Clbit, List[Clbit]]] = None, + ) -> None: + """Add new operation to DAG with scheduled information. + + This is identical to apply_operation_back + updating the node_start_time propety. + + Args: + block_idx: Execution block index for this node. + t_start: Start time of new node. + oper: New operation that is added to the DAG circuit. + qubits: The list of qubits that the operation acts on. + clbits: The list of clbits that the operation acts on. + """ + if isinstance(qubits, Qubit): + qubits = [qubits] + if isinstance(clbits, Clbit): + clbits = [clbits] + + new_node = self._dag.apply_operation_back(oper, qargs=qubits, cargs=clbits) + self.property_set["node_start_time"][new_node] = (block_idx, t_start) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py new file mode 100644 index 000000000..014b90f0d --- /dev/null +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/pad_delay.py @@ -0,0 +1,77 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Padding pass to insert Delay into empty timeslots for dynamic circuit backends.""" + +from qiskit.circuit import Qubit +from qiskit.circuit.delay import Delay +from qiskit.dagcircuit import DAGNode, DAGOutNode + +from .block_base_padder import BlockBasePadder + + +class PadDelay(BlockBasePadder): + """Padding idle time with Delay instructions. + + Consecutive delays will be merged in the output of this pass. + + .. code-block::python + + durations = InstructionDurations([("x", None, 160), ("cx", None, 800)]) + + qc = QuantumCircuit(2) + qc.delay(100, 0) + qc.x(1) + qc.cx(0, 1) + + The ASAP-scheduled circuit output may become + + .. parsed-literal:: + + ┌────────────────┐ + q_0: ┤ Delay(160[dt]) ├──■── + └─────┬───┬──────┘┌─┴─┐ + q_1: ──────┤ X ├───────┤ X ├ + └───┘ └───┘ + + Note that the additional idle time of 60dt on the ``q_0`` wire coming from the duration difference + between ``Delay`` of 100dt (``q_0``) and ``XGate`` of 160 dt (``q_1``) is absorbed in + the delay instruction on the ``q_0`` wire, i.e. in total 160 dt. + + See :class:`BlockBasePadder` pass for details. + """ + + def __init__(self, fill_very_end: bool = True): + """Create new padding delay pass. + + Args: + fill_very_end: Set ``True`` to fill the end of circuit with delay. + """ + super().__init__() + self.fill_very_end = fill_very_end + + def _pad( + self, + block_idx: int, + qubit: Qubit, + t_start: int, + t_end: int, + next_node: DAGNode, + prev_node: DAGNode, + ) -> None: + if not self.fill_very_end and isinstance(next_node, DAGOutNode): + return + + time_interval = t_end - t_start + self._apply_scheduled_op( + block_idx, t_start, Delay(time_interval, self._dag.unit), qubit + ) diff --git a/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py new file mode 100644 index 000000000..0fcd3140a --- /dev/null +++ b/qiskit_ibm_provider/transpiler/passes/scheduling/scheduler.py @@ -0,0 +1,274 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Scheduler for dynamic circuit backends.""" + +from typing import Dict, Optional, Union, Set, Tuple +import itertools + +import qiskit +from qiskit.circuit import Clbit, Measure, Qubit, Reset +from qiskit.dagcircuit import DAGCircuit, DAGNode +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler + + +class DynamicCircuitScheduleAnalysis(BaseScheduler): + """Dynamic circuits scheduling analysis pass. + + This is a scheduler designed to work for the unique scheduling constraints of the dynamic circuits + backends due to the limitations imposed by hardware. This is expected to evolve over time as the + dynamic circuit backends also change. + + In its current form this is similar to Qiskit's ASAP scheduler in which instructions + start as early as possible. + + The primary differences are that: + + * Measurements currently trigger the end of a "quantum block". The period between the end + of the block and the next is *nondeterministic* + ie., we do not know when the next block will begin (as we could be evaluating a classical + function of nondeterministic length) and therefore the + next block starts at a *relative* t=0. + * It is possible to apply gates during a measurement. + * Measurements on disjoint qubits happen simultaneously and are part of the same block. + Measurements that are not lexicographically neighbors in the generated QASM3 will + such as ``measure $0; x $1; measure $2;`` + happen in separate blocks. + + """ + + def __init__( + self, durations: qiskit.transpiler.instruction_durations.InstructionDurations + ) -> None: + """Scheduler for dynamic circuit backends. + + Args: + durations: Durations of instructions to be used in scheduling. + """ + + self._dag = None + + self._current_block_idx = 0 + + self._node_start_time: Optional[Dict[DAGNode, Tuple[int, int]]] = None + self._idle_after: Optional[Dict[Union[Qubit, Clbit], Tuple[int, int]]] = None + self._current_block_measures: Set[DAGNode] = set() + self._bit_indices: Optional[Dict[Qubit, int]] = None + + super().__init__(durations) + + def run(self, dag: DAGCircuit) -> None: + """Run the ASAPSchedule pass on `dag`. + Args: + dag (DAGCircuit): DAG to schedule. + Raises: + TranspilerError: if the circuit is not mapped on physical qubits. + TranspilerError: if conditional bit is added to non-supported instruction. + """ + self._init_run(dag) + + for node in dag.topological_op_nodes(): + self._visit_node(node) + + self.property_set["node_start_time"] = self._node_start_time + + def _init_run(self, dag: DAGCircuit) -> None: + """Setup for initial run.""" + + self._dag = dag + + if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: + raise TranspilerError("ASAP schedule runs on physical circuits only") + + self._node_start_time = {} + self._idle_after = {q: (0, 0) for q in dag.qubits + dag.clbits} + self._current_block_measures = set() + self._bit_indices = {q: index for index, q in enumerate(dag.qubits)} + + def _get_duration(self, node: DAGNode) -> int: + return super()._get_node_duration(node, self._bit_indices, self._dag) + + def _visit_node(self, node: DAGNode) -> None: + # compute t0, t1: instruction interval, note that + # t0: start time of instruction + # t1: end time of instruction + if isinstance(node.op, self.CONDITIONAL_SUPPORTED): + self._visit_conditional_node(node) + 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): + self._visit_measure(node) + elif isinstance(node.op, Reset): + self._visit_reset(node) + else: + self._visit_generic(node) + + def _visit_conditional_node(self, node: DAGNode) -> None: + """Handling case of a conditional execution. + + Conditional execution durations are currently non-deterministic. as we do not know + the time it will take to begin executing the block. We do however know the + duration of the block contents execution (provided it does not also contain + conditional executions). + + TODO: Update for support of general control-flow, not just single conditional operations. + """ + # Special processing required to resolve conditional scheduling dependencies + if node.op.condition_bits: + # Trigger the start of a conditional block + self._begin_new_circuit_block() + + op_duration = self._get_duration(node) + + t0q = max(self._idle_after[q][1] for q in node.qargs) + # conditional is bit tricky due to conditional_latency + t0c = max(self._idle_after[bit][1] 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 ▒▒▒░░░▒▒░░░ + # |t0c + # + t0c = t0q + t1c = t0c + for bit in node.op.condition_bits: + # Lock clbit until state is read + self._idle_after[bit] = (self._current_block_idx, t1c) + + # It starts after register read access + t0 = max(t0q, t1c) # pylint: disable=invalid-name + + t1 = t0 + op_duration # pylint: disable=invalid-name + self._update_idles(node, t0, t1) + + # Terminate the conditional block + self._begin_new_circuit_block() + + else: + # Fall through to generic case if not conditional + self._visit_generic(node) + + def _visit_measure(self, node: DAGNode) -> None: + """Visit a measurement node. + + Measurement currently triggers the end of a deterministically scheduled block + of instructions in IBM dynamic circuits hardware. + This means that it is possible to schedule *up to* a measurement (and during its pulses) + but the measurement will be followed by a period of indeterminism. + All measurements on disjoint qubits that lexicographically follow another + measurement will be collected and performed in parallel. A measurement on a qubit + intersecting with the set of qubits to be measured in parallel will trigger the + end of a scheduling block with said measurement occurring in a following block + which begins another grouping sequence. This behavior will change in future + backend software updates.""" + current_block_measure_qargs = self._current_block_measure_qargs() + # We handle a set of qubits here as _visit_reset currently calls + # this method and a reset may have multiple qubits. + measure_qargs = set(node.qargs) + + t0q = max( + self._idle_after[q][1] for q in measure_qargs + ) # pylint: disable=invalid-name + + # If the measurement qubits overlap, we need to start a new scheduling block. + if current_block_measure_qargs & measure_qargs: + self._begin_new_circuit_block() + t0q = 0 + # Otherwise we need to increment all measurements to start at the same time within the block. + else: + t0q = max( # pylint: disable=invalid-name + itertools.chain( + [t0q], + ( + self._node_start_time[measure][1] + for measure in self._current_block_measures + ), + ) + ) + + # Insert this measure into the block + self._current_block_measures.add(node) + + # now update all measure qarg times. + + self._current_block_measures.add(node) + + for measure in self._current_block_measures: + t0 = t0q # pylint: disable=invalid-name + bit_indices = {bit: index for index, bit in enumerate(self._dag.qubits)} + measure_duration = self.durations.get( + Measure(), [bit_indices[qarg] for qarg in node.qargs], unit="dt" + ) + t1 = t0 + measure_duration # pylint: disable=invalid-name + self._update_idles(measure, t0, t1) + + def _visit_reset(self, node: DAGNode) -> None: + """Visit a reset node. + + Reset currently triggers the end of a pulse block in IBM dynamic circuits hardware + as conditional reset is performed internally using a c_if. + This means that it is possible to schedule *up to* a reset (and during its measurement pulses) + but the reset will be followed by a period of conditional indeterminism. + All resets on disjoint qubits will be collected on the same qubits to be run simultaneously. + This means that from the perspective of scheduling resets have the same behaviour and duration + as a measurement. + """ + self._visit_measure(node) + + def _visit_generic(self, node: DAGNode) -> None: + """Visit a generic node such as a gate or barrier.""" + op_duration = self._get_duration(node) + + # If the measurement qubits overlap, we need to start a new scheduling block. + if self._current_block_measure_qargs() & set(node.qargs): + self._begin_new_circuit_block() + t0 = 0 # pylint: disable=invalid-name + else: + t0 = max( # pylint: disable=invalid-name + self._idle_after[bit][1] for bit in node.qargs + node.cargs + ) + + t1 = t0 + op_duration # pylint: disable=invalid-name + self._update_idles(node, t0, t1) + + def _update_idles( + self, node: DAGNode, t0: int, t1: int # pylint: disable=invalid-name + ) -> None: + for bit in itertools.chain(node.qargs, node.cargs): + self._idle_after[bit] = (self._current_block_idx, t1) + + self._node_start_time[node] = (self._current_block_idx, t0) + + def _begin_new_circuit_block(self) -> None: + """Create a new timed circuit block completing the previous block.""" + self._current_block_idx += 1 + self._current_block_measures = set() + self._idle_after = {q: (0, 0) for q in self._dag.qubits + self._dag.clbits} + + def _current_block_measure_qargs(self) -> Set[Qubit]: + return set( + qarg for measure in self._current_block_measures for qarg in measure.qargs + ) diff --git a/releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml b/releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml new file mode 100644 index 000000000..54ffb0bd9 --- /dev/null +++ b/releasenotes/notes/add-dynamic-circuits-scheduler-b1ae525c0b358acb.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + A scheduling analysis pass, :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.DynamicCircuitScheduleAnalysis` + has been added for Qiskit dynamic circuit (OpenQASM 3) backends. This is capable of handling + scheduling for deterministic regions of a quantum circuit and may combined with a padding pass such as + :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.PadDelay` to pad schedulable sections of a + circuit with delays. + + For an example see the :mod:`~qiskit_ibm_provider.transpiler.passes.scheduling` module's documentation. diff --git a/releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml b/releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml new file mode 100644 index 000000000..5b544a897 --- /dev/null +++ b/releasenotes/notes/add-transpiler-module-af6f530072c82f44.yaml @@ -0,0 +1,13 @@ +--- +prelude: > + A :mod:`~qiskit_ibm_provider.transpiler` module has been added. + It will contain routines that are specific to IBM hardware backends and + which consequently can not be placed directly within Qiskit Terra. +features: + - | + The module :mod:`~qiskit_ibm_provider.transpiler`` has been added. + + Primarily, it will contain all specialized Qiskit routines for running + applications on IBM's next-generation quantum devices that support dynamic + capabilities such as control-flow(feedforward) and classical + compute. diff --git a/requirements-dev.txt b/requirements-dev.txt index 83a255774..0febbfdad 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ sphinx-rtd-theme>=0.4.0 sphinx-tabs>=1.1.11 sphinx-automodapi sphinx-autodoc-typehints -matplotlib>=2.1 +matplotlib>=3.3 jupyter plotly>=4.4 ipyvuetify>=1.1 @@ -25,3 +25,5 @@ websockets>=8 black==22.3.0 coverage>=6.3 scikit-learn>=0.20.0 +ddt>=1.2.0,!=1.4.0,!=1.4.3 +pylatexenc>=1.4 diff --git a/test/integration/test_ibm_qasm_simulator.py b/test/integration/test_ibm_qasm_simulator.py index 469a9cc07..be4b29fcf 100644 --- a/test/integration/test_ibm_qasm_simulator.py +++ b/test/integration/test_ibm_qasm_simulator.py @@ -17,9 +17,9 @@ from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.compiler import transpile -from qiskit.providers.aer.noise import ( +from qiskit.providers.aer.noise import ( # pylint: disable=import-error,no-name-in-module NoiseModel, -) # pylint: disable=import-error,no-name-in-module +) from qiskit.test.reference_circuits import ReferenceCircuits from qiskit_ibm_provider import IBMBackend diff --git a/test/unit/transpiler/__init__.py b/test/unit/transpiler/__init__.py new file mode 100644 index 000000000..fdb172d36 --- /dev/null +++ b/test/unit/transpiler/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/unit/transpiler/passes/__init__.py b/test/unit/transpiler/passes/__init__.py new file mode 100644 index 000000000..fdb172d36 --- /dev/null +++ b/test/unit/transpiler/passes/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/unit/transpiler/passes/scheduling/__init__.py b/test/unit/transpiler/passes/scheduling/__init__.py new file mode 100644 index 000000000..fdb172d36 --- /dev/null +++ b/test/unit/transpiler/passes/scheduling/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/unit/transpiler/passes/scheduling/test_scheduler.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py new file mode 100644 index 000000000..ce35e73bd --- /dev/null +++ b/test/unit/transpiler/passes/scheduling/test_scheduler.py @@ -0,0 +1,524 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test the dynamic circuits scheduling analysis""" + +from qiskit import QuantumCircuit +from qiskit.pulse import Schedule, Play, Constant, DriveChannel +from qiskit.test import QiskitTestCase +from qiskit.transpiler.instruction_durations import InstructionDurations +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.exceptions import TranspilerError + +from qiskit_ibm_provider.transpiler.passes.scheduling.pad_delay import PadDelay +from qiskit_ibm_provider.transpiler.passes.scheduling.scheduler import ( + DynamicCircuitScheduleAnalysis, +) + +# pylint: disable=invalid-name + + +class TestSchedulingAndPaddingPass(QiskitTestCase): + """Tests the Scheduling passes""" + + def test_classically_controlled_gate_after_measure(self): + """Test if schedules circuits with c_if after measure with a common clbit. + See: https://github.com/Qiskit/qiskit-terra/issues/7654""" + 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([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.delay(1000, 1) + expected.measure(0, 0) + expected.barrier() + expected.x(1).c_if(0, True) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_measure_after_measure(self): + """Test if schedules circuits with measure after measure with a common clbit. + + Note: There is no delay to write into the same clbit with IBM backends.""" + 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([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.x(0) + expected.delay(200, 1) + expected.measure(0, 0) + expected.measure(1, 0) + expected.barrier() + self.assertEqual(expected, scheduled) + + def test_measure_block_end(self): + """Tests that measures trigger the end of a scheduling block and + that measurements are grouped by block.""" + qc = QuantumCircuit(3, 1) + qc.x(0) + qc.measure(0, 0) + qc.measure(1, 0) + qc.x(2) + qc.measure(1, 0) + qc.measure(2, 0) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.x(0) + expected.delay(200, 1) + expected.x(2) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.measure(1, 0) + expected.barrier() + expected.delay(1000, 0) + expected.measure(1, 0) + expected.measure(2, 0) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_c_if_on_different_qubits(self): + """Test if schedules circuits with `c_if`s on different qubits.""" + 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([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.barrier() + expected.x(1).c_if(0, True) + expected.barrier() + expected.x(2).c_if(0, True) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_shorter_measure_after_measure(self): + """Test if schedules circuits with shorter measure after measure + with a common clbit. + + Note: For dynamic circuits support we currently group measurements + to start at the same time which in turn trigger the end of a block.""" + qc = QuantumCircuit(3, 1) + qc.measure(0, 0) + qc.measure(1, 0) + + durations = InstructionDurations( + [("measure", [0], 1000), ("measure", [1], 700)] + ) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.measure(0, 0) + expected.measure(1, 0) + expected.delay(300, 1) + expected.delay(1000, 2) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_measure_after_c_if(self): + """Test if schedules circuits with c_if after measure with a common clbit. + + Note: This test is not yet correct as we should schedule the conditional block + qubits with delays as well. + """ + 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([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.barrier() + expected.x(1).c_if( + 0, 1 + ) # Not yet correct as we should insert delays for idle qubits in conditional. + expected.barrier() + expected.delay(1000, 0) + expected.measure(2, 0) + expected.delay(1000, 1) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_parallel_gate_different_length(self): + """Test circuit having two parallel instruction with different length.""" + 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([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 2) + expected.x(0) + expected.x(1) + expected.delay(200, 0) + expected.measure(0, 0) # immediately start after X gate + expected.measure(1, 1) + expected.barrier() + + self.assertEqual(scheduled, expected) + + def test_parallel_gate_different_length_with_barrier(self): + """Test circuit having two parallel instruction with different length with barrier.""" + 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([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 2) + expected.x(0) + expected.delay(200, 0) + expected.x(1) + expected.barrier() + expected.measure(0, 0) + expected.measure(1, 1) + expected.barrier() + + self.assertEqual(scheduled, expected) + + def test_measure_after_c_if_on_edge_locking(self): + """Test if 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""" + 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 + scheduled = PassManager( + [ + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(qc) + + expected = QuantumCircuit(3, 1) + expected.delay(1000, 1) + expected.delay(1000, 2) + expected.measure(0, 0) + expected.barrier() + expected.x(1).c_if(0, 1) + expected.barrier() + expected.delay(1000, 0) + expected.delay(1000, 1) + expected.measure(2, 0) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_active_reset_circuit(self): + """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.""" + 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)]) + + scheduled = PassManager( + [ + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(qc) + + expected = QuantumCircuit(1, 1) + expected.measure(0, 0) + expected.barrier() + expected.x(0).c_if(0, 1) + expected.barrier() + expected.measure(0, 0) + expected.barrier() + expected.x(0).c_if(0, 1) + expected.barrier() + expected.measure(0, 0) + expected.barrier() + expected.x(0).c_if(0, 1) + expected.barrier() + + self.assertEqual(expected, scheduled) + + 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 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).""" + 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([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.barrier() + expected.x(0).c_if(0, True) + expected.barrier() + expected.x(1).c_if(0, True) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_scheduling_with_calibration(self): + """Test if calibrated instruction can update node duration.""" + qc = QuantumCircuit(2) + qc.x(0) + qc.cx(0, 1) + qc.x(1) + qc.cx(0, 1) + + xsched = Schedule(Play(Constant(300, 0.1), DriveChannel(0))) + qc.add_calibration("x", (0,), xsched) + + durations = InstructionDurations([("x", None, 160), ("cx", None, 600)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2) + expected.x(0) + expected.delay(300, 1) + expected.cx(0, 1) + expected.x(1) + expected.delay(160, 0) + expected.cx(0, 1) + expected.add_calibration("x", (0,), xsched) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_padding_not_working_without_scheduling(self): + """Test padding fails when un-scheduled DAG is input.""" + qc = QuantumCircuit(1, 1) + qc.delay(100, 0) + qc.x(0) + qc.measure(0, 0) + + with self.assertRaises(TranspilerError): + PassManager(PadDelay()).run(qc) + + def test_no_pad_very_end_of_circuit(self): + """Test padding option that inserts no delay at the very end of circuit. + + This circuit will be unchanged after scheduling/padding.""" + qc = QuantumCircuit(2, 1) + qc.delay(100, 0) + qc.x(1) + qc.measure(0, 0) + + durations = InstructionDurations([("x", None, 160), ("measure", None, 1000)]) + + scheduled = PassManager( + [ + DynamicCircuitScheduleAnalysis(durations), + PadDelay(fill_very_end=False), + ] + ).run(qc) + + expected = qc.copy() + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_reset_terminates_block(self): + """Test if reset operations terminate the block scheduled. + + Note: For dynamic circuits support we currently group resets + to start at the same time which in turn trigger the end of a block.""" + qc = QuantumCircuit(3, 1) + qc.x(0) + qc.reset(0) + qc.reset(1) + + durations = InstructionDurations( + [ + ("x", None, 200), + ( + "reset", + [0], + 1000, + ), # ignored as only the duration of the measurement is used for scheduling + ( + "reset", + [1], + 900, + ), # ignored as only the duration of the measurement is used for scheduling + ("measure", [0], 600), + ("measure", [1], 700), + ] + ) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.x(0) + expected.delay(200, 1) + expected.delay(1200, 2) + expected.reset(0) + expected.reset(1) + expected.delay(100, 1) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_reset_merged_with_measure(self): + """Test if reset operations terminate the block scheduled. + + Note: For dynamic circuits support we currently group resets to start + at the same time which in turn trigger the end of a block.""" + qc = QuantumCircuit(3, 1) + qc.x(0) + qc.reset(0) + qc.reset(1) + + durations = InstructionDurations( + [ + ("x", None, 200), + ( + "reset", + [0], + 1000, + ), # ignored as only the duration of the measurement is used for scheduling + ( + "reset", + [1], + 900, + ), # ignored as only the duration of the measurement is used for scheduling + ("measure", [0], 600), + ("measure", [1], 700), + ] + ) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(3, 1) + expected.x(0) + expected.delay(200, 1) + expected.delay(1200, 2) + expected.reset(0) + expected.reset(1) + expected.delay(100, 1) + expected.barrier() + + self.assertEqual(expected, scheduled) + + def test_scheduling_is_idempotent(self): + """Test that padding can be applied back to back without changing the circuit.""" + 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)]) + + scheduled0 = PassManager( + [ + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(qc) + + scheduled1 = PassManager( + [ + DynamicCircuitScheduleAnalysis(durations), + PadDelay(), + ] + ).run(scheduled0) + + self.assertEqual(scheduled0, scheduled1) + + def test_gate_on_measured_qubit(self): + """Test that a gate on a previously measured qubit triggers the end of the block""" + qc = QuantumCircuit(2, 1) + qc.measure(0, 0) + qc.x(0) + qc.x(1) + + durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) + pm = PassManager([DynamicCircuitScheduleAnalysis(durations), PadDelay()]) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2, 1) + expected.x(1) + expected.delay(800, 1) + expected.measure(0, 0) + expected.barrier() + expected.x(0) + expected.delay(200, 1) + expected.barrier() + + self.assertEqual(expected, scheduled)