From 3476ef065e507be880c205b44f77d56d2c44bccb Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 24 Mar 2022 03:44:47 +0900 Subject: [PATCH 1/5] Replace AlignMeasures with ConstrainedReschedule for pulse alignment (#7762) * Replace AlignMeasures with ConstrainedReschedule New pass considers both pulse and acquire alignment constraints, thus it is a drop-in-replacement of the AlignMeasures. Note that ordering of passes in the preset pass managers is updated because new pass is implemented as analysis pass, that only updates `node_start_time` in the property set, thus it should be executed before the padding passes. * wording edits Co-authored-by: Ali Javadi-Abhari * split files and separate duration validation from reschedule * lint * add reno * add reno * fix unittests wip * update property set initialization * move latencies to propery set and separately consider qreg and creg overlap in rescheduler. * Add set io latency pass to set creg latencies to property set and copy them to circuit attributes. Schedulign passes are combined in a single ``scheduling`` folder. * alignment bug fix * update unittests * move padding to own module as well Co-authored-by: Ali Javadi-Abhari Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/transpiler/basepasses.py | 4 + qiskit/transpiler/passes/__init__.py | 8 +- .../transpiler/passes/scheduling/__init__.py | 14 +- .../passes/scheduling/alignments/__init__.py | 81 +++++ .../scheduling/alignments/align_measures.py | 39 +++ .../scheduling/alignments/check_durations.py | 75 ++++ .../alignments/pulse_gate_validation.py | 97 ++++++ .../scheduling/alignments/reschedule.py | 237 +++++++++++++ .../scheduling/instruction_alignment.py | 319 ------------------ .../passes/scheduling/padding/__init__.py | 16 + .../scheduling/{ => padding}/base_padding.py | 0 .../{ => padding}/dynamical_decoupling.py | 3 +- .../scheduling/{ => padding}/pad_delay.py | 3 +- .../passes/scheduling/scheduling/__init__.py | 17 + .../scheduling/{ => scheduling}/alap.py | 9 +- .../scheduling/{ => scheduling}/asap.py | 11 +- .../{ => scheduling}/base_scheduler.py | 30 +- .../scheduling/scheduling/set_io_latency.py | 64 ++++ .../transpiler/preset_passmanagers/level0.py | 96 +++--- .../transpiler/preset_passmanagers/level1.py | 95 +++--- .../transpiler/preset_passmanagers/level2.py | 96 +++--- .../transpiler/preset_passmanagers/level3.py | 95 +++--- qiskit/transpiler/runningpassmanager.py | 2 + ...ion-alignment-passes-ef0f20d4f89f95f3.yaml | 31 ++ ...ade-alap-asap-passes-bcacc0f1053c9828.yaml | 29 +- .../transpiler/test_instruction_alignments.py | 239 ++++++++++--- .../test_scheduling_padding_pass.py | 39 ++- 27 files changed, 1172 insertions(+), 577 deletions(-) create mode 100644 qiskit/transpiler/passes/scheduling/alignments/__init__.py create mode 100644 qiskit/transpiler/passes/scheduling/alignments/align_measures.py create mode 100644 qiskit/transpiler/passes/scheduling/alignments/check_durations.py create mode 100644 qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py create mode 100644 qiskit/transpiler/passes/scheduling/alignments/reschedule.py delete mode 100644 qiskit/transpiler/passes/scheduling/instruction_alignment.py create mode 100644 qiskit/transpiler/passes/scheduling/padding/__init__.py rename qiskit/transpiler/passes/scheduling/{ => padding}/base_padding.py (100%) rename qiskit/transpiler/passes/scheduling/{ => padding}/dynamical_decoupling.py (99%) rename qiskit/transpiler/passes/scheduling/{ => padding}/pad_delay.py (97%) create mode 100644 qiskit/transpiler/passes/scheduling/scheduling/__init__.py rename qiskit/transpiler/passes/scheduling/{ => scheduling}/alap.py (94%) rename qiskit/transpiler/passes/scheduling/{ => scheduling}/asap.py (92%) rename qiskit/transpiler/passes/scheduling/{ => scheduling}/base_scheduler.py (88%) create mode 100644 qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py create mode 100644 releasenotes/notes/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index df63818d6675..832b79e148a5 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -132,6 +132,10 @@ def __call__(self, circuit, property_set=None): if self.property_set["layout"]: result_circuit._layout = self.property_set["layout"] + if self.property_set["clbit_write_latency"] is not None: + result_circuit._clbit_write_latency = self.property_set["clbit_write_latency"] + if self.property_set["conditional_latency"] is not None: + result_circuit._conditional_latency = self.property_set["conditional_latency"] return result_circuit diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index f341c9fef310..547fe798c13b 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -104,8 +104,11 @@ ALAPSchedule ASAPSchedule DynamicalDecoupling + ConstrainedReschedule AlignMeasures ValidatePulseGates + InstructionDurationCheck + SetIOLatency Circuit Analysis ================ @@ -227,9 +230,12 @@ from .scheduling import ALAPSchedule from .scheduling import ASAPSchedule from .scheduling import DynamicalDecoupling -from .scheduling import AlignMeasures +from .scheduling import AlignMeasures # Deprecated from .scheduling import ValidatePulseGates from .scheduling import PadDelay +from .scheduling import ConstrainedReschedule +from .scheduling import InstructionDurationCheck +from .scheduling import SetIOLatency # additional utility passes from .utils import CheckMap diff --git a/qiskit/transpiler/passes/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/__init__.py index 339012e25969..6a7a95cb8be7 100644 --- a/qiskit/transpiler/passes/scheduling/__init__.py +++ b/qiskit/transpiler/passes/scheduling/__init__.py @@ -12,9 +12,13 @@ """Module containing circuit scheduling passes.""" -from .alap import ALAPSchedule -from .asap import ASAPSchedule +from .scheduling import ALAPSchedule, ASAPSchedule, SetIOLatency from .time_unit_conversion import TimeUnitConversion -from .instruction_alignment import AlignMeasures, ValidatePulseGates -from .pad_delay import PadDelay -from .dynamical_decoupling import DynamicalDecoupling +from .padding import PadDelay, DynamicalDecoupling +from .alignments import InstructionDurationCheck, ValidatePulseGates, ConstrainedReschedule + +# For backward compability +from . import alignments as instruction_alignments + +# TODO Deprecated pass. Will be removed after deprecation period. +from .alignments import AlignMeasures diff --git a/qiskit/transpiler/passes/scheduling/alignments/__init__.py b/qiskit/transpiler/passes/scheduling/alignments/__init__.py new file mode 100644 index 000000000000..513144937ab5 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/__init__.py @@ -0,0 +1,81 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Validation and optimization for hardware instruction alignment constraints. + +This is a control electronics aware analysis pass group. + +In many quantum computing architectures gates (instructions) are implemented with +shaped analog stimulus signals. These signals are digitally stored in the +waveform memory of the control electronics and converted into analog voltage signals +by electronic components called digital to analog converters (DAC). + +In a typical hardware implementation of superconducting quantum processors, +a single qubit instruction is implemented by a +microwave signal with the duration of around several tens of ns with a per-sample +time resolution of ~0.1-10ns, as reported by ``backend.configuration().dt``. +In such systems requiring higher DAC bandwidth, control electronics often +defines a `pulse granularity`, in other words a data chunk, to allow the DAC to +perform the signal conversion in parallel to gain the bandwidth. + +A control electronics, i.e. micro-architecture, of the real quantum backend may +impose some constraints on the start time of microinstructions. +In Qiskit SDK, the duration of :class:`qiskit.circuit.Delay` can take arbitrary +value in units of dt, thus circuits involving delays may violate the constraints, +which may result in failure in the circuit execution on the backend. + +There are two alignment constraint values reported by your quantum backend. +In addition, if you want to define a custom instruction as a pulse gate, i.e. calibration, +the underlying pulse instruction should satisfy other two waveform constraints. + +Pulse alignment constraint + + This value is reported by ``timing_constraints["pulse_alignment"]`` in the backend + configuration in units of dt. The start time of the all pulse instruction should be + multiple of this value. Violation of this constraint may result in the + backend execution failure. + + In most of the senarios, the scheduled start time of ``DAGOpNode`` corresponds to the + start time of the underlying pulse instruction composing the node operation. + However, this assumption can be intentionally broken by defining a pulse gate, + i.e. calibration, with the schedule involving pre-buffer, i.e. some random pulse delay + followed by a pulse instruction. Because this pass is not aware of such edge case, + the user must take special care of pulse gates if any. + +Acquire alignment constraint + + This value is reported by ``timing_constraints["acquire_alignment"]`` in the backend + configuration in units of dt. The start time of the :class:`~qiskit.circuit.Measure` + instruction should be multiple of this value. + +Granularity constraint + + This value is reported by ``timing_constraints["granularity"]`` in the backend + configuration in units of dt. This is the constraint for a single pulse :class:`Play` + instruction that may constitute your pulse gate. + The length of waveform samples should be multipel of this constraint value. + Violation of this constraint may result in failue in backend execution. + +Minimum pulse length constraint + + This value is reported by ``timing_constraints["min_length"]`` in the backend + configuration in units of dt. This is the constraint for a single pulse :class:`Play` + instruction that may constitute your pulse gate. + The length of waveform samples should be greater than this constraint value. + Violation of this constraint may result in failue in backend execution. + +""" + +from .check_durations import InstructionDurationCheck +from .pulse_gate_validation import ValidatePulseGates +from .reschedule import ConstrainedReschedule +from .align_measures import AlignMeasures diff --git a/qiskit/transpiler/passes/scheduling/alignments/align_measures.py b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py new file mode 100644 index 000000000000..0318d6988847 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py @@ -0,0 +1,39 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Deprecated. Measurement alignment.""" + +import warnings + +from qiskit.transpiler.passes.scheduling.alignments.reschedule import ConstrainedReschedule + + +class AlignMeasures: + """Deprecated. Measurement alignment.""" + + def __new__(cls, alignment=1) -> ConstrainedReschedule: + """Create new pass. + + Args: + alignment: Integer number representing the minimum time resolution to + trigger measure instruction in units of ``dt``. This value depends on + the control electronics of your quantum processor. + + Returns: + ConstrainedReschedule instance that is a drop-in-replacement of this class. + """ + warnings.warn( + f"{cls.__name__} has been deprecated as of Qiskit 20.0. " + f"Use ConstrainedReschedule pass instead.", + FutureWarning, + ) + return ConstrainedReschedule(acquire_alignment=alignment) diff --git a/qiskit/transpiler/passes/scheduling/alignments/check_durations.py b/qiskit/transpiler/passes/scheduling/alignments/check_durations.py new file mode 100644 index 000000000000..b2cc90b0d152 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/check_durations.py @@ -0,0 +1,75 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""A pass to check if input circuit requires reschedule.""" + +from qiskit.circuit.delay import Delay +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.basepasses import AnalysisPass + + +class InstructionDurationCheck(AnalysisPass): + """Duration validation pass for reschedule. + + This pass investigates the input quantum circuit and checks if the circuit requres + rescheduling for execution. Note that this pass can be triggered without scheduling. + This pass only checks the duration of delay instructions and user defined pulse gates, + which report duration values without pre-scheduling. + + This pass assumes backend supported instructions, i.e. basis gates, have no violation + of the hardware alignment constraints, which is true in general. + """ + + def __init__( + self, + acquire_alignment: int = 1, + pulse_alignment: int = 1, + ): + """Create new duration validation pass. + + The alignment values depend on the control electronics of your quantum processor. + + Args: + acquire_alignment: Integer number representing the minimum time resolution to + trigger acquisition instruction in units of ``dt``. + pulse_alignment: Integer number representing the minimum time resolution to + trigger gate instruction in units of ``dt``. + """ + super().__init__() + self.acquire_align = acquire_alignment + self.pulse_align = pulse_alignment + + def run(self, dag: DAGCircuit): + """Run duration validation passes. + + Args: + dag: DAG circuit to check instruction durations. + """ + self.property_set["reschedule_required"] = False + + # Rescheduling is not necessary + if self.acquire_align == 1 and self.pulse_align == 1: + return + + # Check delay durations + for delay_node in dag.op_nodes(Delay): + dur = delay_node.op.duration + if not (dur % self.acquire_align == 0 and dur % self.pulse_align == 0): + self.property_set["reschedule_required"] = True + return + + # Check custom gate durations + for inst_defs in dag.calibrations.values(): + for caldef in inst_defs.values(): + dur = caldef.duration + if not (dur % self.acquire_align == 0 and dur % self.pulse_align == 0): + self.property_set["reschedule_required"] = True + return diff --git a/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py b/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py new file mode 100644 index 000000000000..697d36fa71b6 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py @@ -0,0 +1,97 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Analysis passes for hardware alignment constraints.""" + +from qiskit.dagcircuit import DAGCircuit +from qiskit.pulse import Play +from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.transpiler.exceptions import TranspilerError + + +class ValidatePulseGates(AnalysisPass): + """Check custom gate length. + + This is a control electronics aware analysis pass. + + Quantum gates (instructions) are often implemented with shaped analog stimulus signals. + These signals may be digitally stored in the waveform memory of the control electronics + and converted into analog voltage signals by electronic components known as + digital to analog converters (DAC). + + In Qiskit SDK, we can define the pulse-level implementation of custom quantum gate + instructions, as a `pulse gate + `__, + thus user gates should satisfy all waveform memory constraints imposed by the backend. + + This pass validates all attached calibration entries and raises ``TranspilerError`` to + kill the transpilation process if any invalid calibration entry is found. + This pass saves users from waiting until job execution time to get an invalid pulse error from + the backend control electronics. + """ + + def __init__( + self, + granularity: int = 1, + min_length: int = 1, + ): + """Create new pass. + + Args: + granularity: Integer number representing the minimum time resolution to + define the pulse gate length in units of ``dt``. This value depends on + the control electronics of your quantum processor. + min_length: Integer number representing the minimum data point length to + define the pulse gate in units of ``dt``. This value depends on + the control electronics of your quantum processor. + """ + super().__init__() + self.granularity = granularity + self.min_length = min_length + + def run(self, dag: DAGCircuit): + """Run the pulse gate validation attached to ``dag``. + + Args: + dag: DAG to be validated. + + Returns: + DAGCircuit: DAG with consistent timing and op nodes annotated with duration. + + Raises: + TranspilerError: When pulse gate violate pulse controller constraints. + """ + if self.granularity == 1 and self.min_length == 1: + # we can define arbitrary length pulse with dt resolution + return + + for gate, insts in dag.calibrations.items(): + for qubit_param_pair, schedule in insts.items(): + for _, inst in schedule.instructions: + if isinstance(inst, Play): + pulse = inst.pulse + if pulse.duration % self.granularity != 0: + raise TranspilerError( + f"Pulse duration is not multiple of {self.granularity}. " + "This pulse cannot be played on the specified backend. " + f"Please modify the duration of the custom gate pulse {pulse.name} " + f"which is associated with the gate {gate} of " + f"qubit {qubit_param_pair[0]}." + ) + if pulse.duration < self.min_length: + raise TranspilerError( + f"Pulse gate duration is less than {self.min_length}. " + "This pulse cannot be played on the specified backend. " + f"Please modify the duration of the custom gate pulse {pulse.name} " + f"which is associated with the gate {gate} of " + "qubit {qubit_param_pair[0]}." + ) diff --git a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py new file mode 100644 index 000000000000..ab22b0080a9d --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py @@ -0,0 +1,237 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Rescheduler pass to adjust node start times.""" + +from typing import List + +from qiskit.circuit.gate import Gate +from qiskit.circuit.measure import Measure +from qiskit.dagcircuit import DAGCircuit, DAGOpNode, DAGOutNode +from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.transpiler.exceptions import TranspilerError + + +class ConstrainedReschedule(AnalysisPass): + """Rescheduler pass that updates node start times to conform to the hardware alignments. + + This pass shifts DAG node start times previously scheduled with one of + the scheduling passes, e.g. :class:`ASAPSchedule` or :class:`ALAPSchedule`, + so that every instruction start time satisfies alignment constraints. + + Examples: + + We assume executing the following circuit on a backend with 16 dt of acquire alignment. + + .. parsed-literal:: + + ┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├ + └───┘└────────────────┘└╥┘ + c: 1/════════════════════════╩═ + 0 + + Note that delay of 100 dt induces a misalignment of 4 dt at the measurement. + This pass appends an extra 12 dt time shift to the input circuit. + + .. parsed-literal:: + + ┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├ + └───┘└────────────────┘└╥┘ + c: 1/════════════════════════╩═ + 0 + + Notes: + + Your backend may execute circuits violating these alignment constraints. + However, you may obtain erroneous measurement result because of the + untracked phase originating in the instruction misalignment. + """ + + def __init__( + self, + acquire_alignment: int = 1, + pulse_alignment: int = 1, + ): + """Create new rescheduler pass. + + The alignment values depend on the control electronics of your quantum processor. + + Args: + acquire_alignment: Integer number representing the minimum time resolution to + trigger acquisition instruction in units of ``dt``. + pulse_alignment: Integer number representing the minimum time resolution to + trigger gate instruction in units of ``dt``. + """ + super().__init__() + self.acquire_align = acquire_alignment + self.pulse_align = pulse_alignment + + @classmethod + def _get_next_gate(cls, dag: DAGCircuit, node: DAGOpNode) -> List[DAGOpNode]: + """Get next non-delay nodes. + + Args: + dag: DAG circuit to be rescheduled with constraints. + node: Current node. + + Returns: + A list of non-delay successors. + """ + op_nodes = [] + for next_node in dag.successors(node): + if isinstance(next_node, DAGOutNode): + continue + op_nodes.append(next_node) + + return op_nodes + + def _push_node_back(self, dag: DAGCircuit, node: DAGOpNode, shift: int): + """Update start time of current node. Successors are also shifted to avoid overlap. + + .. note:: + + This logic assumes the all bits in the qregs and cregs synchronously start and end, + i.e. occupy the same time slot, but qregs and cregs can take + different time slot due to classical I/O latencies. + + Args: + dag: DAG circuit to be rescheduled with constraints. + node: Current node. + shift: Amount of required time shift. + """ + node_start_time = self.property_set["node_start_time"] + conditional_latency = self.property_set.get("conditional_latency", 0) + clbit_write_latency = self.property_set.get("clbit_write_latency", 0) + + # Compute shifted t1 of this node separately for qreg and creg + this_t0 = node_start_time[node] + new_t1q = this_t0 + node.op.duration + shift + this_qubits = set(node.qargs) + if isinstance(node.op, Measure): + # creg access ends at the end of instruction + new_t1c = new_t1q + this_clbits = set(node.cargs) + else: + if node.op.condition_bits: + # conditional access ends at the beginning of node start time + new_t1c = this_t0 + shift + this_clbits = set(node.op.condition_bits) + else: + new_t1c = None + this_clbits = set() + + # Check successors for overlap + for next_node in self._get_next_gate(dag, node): + # Compute next node start time separately for qreg and creg + next_t0q = node_start_time[next_node] + next_qubits = set(next_node.qargs) + if isinstance(next_node.op, Measure): + # creg access starts after write latency + next_t0c = next_t0q + clbit_write_latency + next_clbits = set(next_node.cargs) + else: + if next_node.op.condition_bits: + # conditional access starts before node start time + next_t0c = next_t0q - conditional_latency + next_clbits = set(next_node.op.condition_bits) + else: + next_t0c = None + next_clbits = set() + # Compute overlap if there is qubits overlap + if any(this_qubits & next_qubits): + qreg_overlap = new_t1q - next_t0q + else: + qreg_overlap = 0 + # Compute overlap if there is clbits overlap + if any(this_clbits & next_clbits): + creg_overlap = new_t1c - next_t0c + else: + creg_overlap = 0 + # Shift next node if there is finite overlap in either in qubits or clbits + overlap = max(qreg_overlap, creg_overlap) + if overlap > 0: + self._push_node_back(dag, next_node, overlap) + + # Update start time of this node after all overlaps are resolved + node_start_time[node] += shift + + def run(self, dag: DAGCircuit): + """Run rescheduler. + + This pass should perform rescheduling to satisfy: + + - All DAGOpNode are placed at start time satisfying hardware alignment constraints. + - The end time of current does not overlap with the start time of successor nodes. + - Compiler directives are not necessary satisfying the constraints. + + Assumptions: + + - Topological order and absolute time order of DAGOpNode are consistent. + - All bits in either qargs or cargs associated with node synchronously start. + - Start time of qargs and cargs may different due to I/O latency. + + Based on the configurations above, rescheduler pass takes following strategy. + + 1. Scan node from the beginning, i.e. from left of the circuit. The rescheduler + calls ``node_start_time`` from the property set, + and retrieves the scheduled start time of current node. + 2. If the start time of the node violates the alignment constraints, + the scheduler increases the start time until it satisfies the constraint. + 3. Check overlap with successor nodes. If any overlap occurs, the rescheduler + recursively pushs the successor nodes backward towards the end of the wire. + Note that shifted location doesn't need to satisfy the constraints, + thus it will be a minimum delay to resolve the overlap with the ancestor node. + 4. Repeat 1-3 until the node at the end of the wire. This will resolve + all misalignment without creating overlap between the nodes. + + Args: + dag: DAG circuit to be rescheduled with constraints. + + Raises: + TranspilerError: If circuit is not scheduled. + """ + + if "node_start_time" not in self.property_set: + raise TranspilerError( + f"The input circuit {dag.name} is not scheduled. Call one of scheduling passes " + f"before running the {self.__class__.__name__} pass." + ) + + node_start_time = self.property_set["node_start_time"] + + for node in dag.topological_op_nodes(): + if node_start_time[node] == 0: + # Every instruction can start at t=0 + continue + + if isinstance(node.op, Gate): + alignment = self.pulse_align + elif isinstance(node.op, Measure): + alignment = self.acquire_align + else: + # Directive or delay. These can start at arbitrary time. + continue + + try: + misalignment = node_start_time[node] % alignment + if misalignment == 0: + continue + shift = max(0, alignment - misalignment) + except KeyError as ex: + raise TranspilerError( + f"Start time of {repr(node)} is not found. This node is likely added after " + "this circuit is scheduled. Run scheduler again." + ) from ex + if shift > 0: + self._push_node_back(dag, node, shift) diff --git a/qiskit/transpiler/passes/scheduling/instruction_alignment.py b/qiskit/transpiler/passes/scheduling/instruction_alignment.py deleted file mode 100644 index 838e372e63b2..000000000000 --- a/qiskit/transpiler/passes/scheduling/instruction_alignment.py +++ /dev/null @@ -1,319 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Align measurement instructions.""" -import itertools -import warnings -from collections import defaultdict -from typing import List, Union - -from qiskit.circuit.delay import Delay -from qiskit.circuit.instruction import Instruction -from qiskit.circuit.measure import Measure -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit import DAGCircuit -from qiskit.pulse import Play -from qiskit.transpiler.basepasses import TransformationPass, AnalysisPass -from qiskit.transpiler.exceptions import TranspilerError - - -class AlignMeasures(TransformationPass): - """Measurement alignment. - - This is a control electronics aware optimization pass. - - In many quantum computing architectures gates (instructions) are implemented with - shaped analog stimulus signals. These signals are digitally stored in the - waveform memory of the control electronics and converted into analog voltage signals - by electronic components called digital to analog converters (DAC). - - In a typical hardware implementation of superconducting quantum processors, - a single qubit instruction is implemented by a - microwave signal with the duration of around several tens of ns with a per-sample - time resolution of ~0.1-10ns, as reported by ``backend.configuration().dt``. - In such systems requiring higher DAC bandwidth, control electronics often - defines a `pulse granularity`, in other words a data chunk, to allow the DAC to - perform the signal conversion in parallel to gain the bandwidth. - - Measurement alignment is required if a backend only allows triggering ``measure`` - instructions at a certain multiple value of this pulse granularity. - This value is usually provided by ``backend.configuration().timing_constraints``. - - In Qiskit SDK, the duration of delay can take arbitrary value in units of ``dt``, - thus circuits involving delays may violate the above alignment constraint (i.e. misalignment). - This pass shifts measurement instructions to a new time position to fix the misalignment, - by inserting extra delay right before the measure instructions. - The input of this pass should be scheduled :class:`~qiskit.dagcircuit.DAGCircuit`, - thus one should select one of the scheduling passes - (:class:`~qiskit.transpiler.passes.ALAPSchedule` or - :class:`~qiskit.trasnpiler.passes.ASAPSchedule`) before calling this. - - Examples: - We assume executing the following circuit on a backend with ``alignment=16``. - - .. parsed-literal:: - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - Note that delay of 100 dt induces a misalignment of 4 dt at the measurement. - This pass appends an extra 12 dt time shift to the input circuit. - - .. parsed-literal:: - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - This pass always inserts a positive delay before measurements - rather than reducing other delays. - - Notes: - The Backend may allow users to execute circuits violating the alignment constraint. - However, it may return meaningless measurement data mainly due to the phase error. - """ - - def __init__(self, alignment: int = 1): - """Create new pass. - - Args: - alignment: Integer number representing the minimum time resolution to - trigger measure instruction in units of ``dt``. This value depends on - the control electronics of your quantum processor. - """ - super().__init__() - self.alignment = alignment - - def run(self, dag: DAGCircuit): - """Run the measurement alignment pass on `dag`. - - Args: - dag (DAGCircuit): DAG to be checked. - - Returns: - DAGCircuit: DAG with consistent timing and op nodes annotated with duration. - - Raises: - TranspilerError: If circuit is not scheduled. - """ - time_unit = self.property_set["time_unit"] - - if not _check_alignment_required(dag, self.alignment, Measure): - # return input as-is to avoid unnecessary scheduling. - # because following procedure regenerate new DAGCircuit, - # we should avoid continuing if not necessary from performance viewpoint. - return dag - - # if circuit is not yet scheduled, schedule with ALAP method - if dag.duration is None: - raise TranspilerError( - f"This circuit {dag.name} may involve a delay instruction violating the " - "pulse controller alignment. To adjust instructions to " - "right timing, you should call one of scheduling passes first. " - "This is usually done by calling transpiler with scheduling_method='alap'." - ) - - # the following lines are basically copied from ASAPSchedule pass - # - # * some validations for non-scheduled nodes are dropped, since we assume scheduled input - # * pad_with_delay is called only with non-delay node to avoid consecutive delay - new_dag = dag._copy_circuit_metadata() - - qubit_time_available = defaultdict(int) # to track op start time - qubit_stop_times = defaultdict(int) # to track delay start time for padding - clbit_readable = defaultdict(int) - clbit_writeable = defaultdict(int) - - def pad_with_delays(qubits: List[int], until, unit) -> None: - """Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``.""" - for q in qubits: - if qubit_stop_times[q] < until: - idle_duration = until - qubit_stop_times[q] - new_dag.apply_operation_back(Delay(idle_duration, unit), [q]) - - for node in dag.topological_op_nodes(): - # choose appropriate clbit available time depending on op - clbit_time_available = ( - clbit_writeable if isinstance(node.op, Measure) else clbit_readable - ) - # correction to change clbit start time to qubit start time - delta = node.op.duration if isinstance(node.op, Measure) else 0 - start_time = max( - itertools.chain( - (qubit_time_available[q] for q in node.qargs), - (clbit_time_available[c] - delta for c in node.cargs + node.op.condition_bits), - ) - ) - - if isinstance(node.op, Measure): - if start_time % self.alignment != 0: - start_time = ((start_time // self.alignment) + 1) * self.alignment - - if not isinstance(node.op, Delay): # exclude delays for combining consecutive delays - pad_with_delays(node.qargs, until=start_time, unit=time_unit) - new_dag.apply_operation_back(node.op, node.qargs, node.cargs) - - stop_time = start_time + node.op.duration - # update time table - for q in node.qargs: - qubit_time_available[q] = stop_time - if not isinstance(node.op, Delay): - qubit_stop_times[q] = stop_time - for c in node.cargs: # measure - clbit_writeable[c] = clbit_readable[c] = stop_time - for c in node.op.condition_bits: # conditional op - clbit_writeable[c] = max(start_time, clbit_writeable[c]) - - working_qubits = qubit_time_available.keys() - circuit_duration = max(qubit_time_available[q] for q in working_qubits) - pad_with_delays(new_dag.qubits, until=circuit_duration, unit=time_unit) - - new_dag.name = dag.name - new_dag.metadata = dag.metadata - - # set circuit duration and unit to indicate it is scheduled - new_dag.duration = circuit_duration - new_dag.unit = time_unit - - return new_dag - - -class ValidatePulseGates(AnalysisPass): - """Check custom gate length. - - This is a control electronics aware analysis pass. - - Quantum gates (instructions) are often implemented with shaped analog stimulus signals. - These signals may be digitally stored in the waveform memory of the control electronics - and converted into analog voltage signals by electronic components known as - digital to analog converters (DAC). - - In Qiskit SDK, we can define the pulse-level implementation of custom quantum gate - instructions, as a `pulse gate - `__, - thus user gates should satisfy all waveform memory constraints imposed by the backend. - - This pass validates all attached calibration entries and raises ``TranspilerError`` to - kill the transpilation process if any invalid calibration entry is found. - This pass saves users from waiting until job execution time to get an invalid pulse error from - the backend control electronics. - """ - - def __init__( - self, - granularity: int = 1, - min_length: int = 1, - ): - """Create new pass. - - Args: - granularity: Integer number representing the minimum time resolution to - define the pulse gate length in units of ``dt``. This value depends on - the control electronics of your quantum processor. - min_length: Integer number representing the minimum data point length to - define the pulse gate in units of ``dt``. This value depends on - the control electronics of your quantum processor. - """ - super().__init__() - self.granularity = granularity - self.min_length = min_length - - def run(self, dag: DAGCircuit): - """Run the measurement alignment pass on `dag`. - - Args: - dag (DAGCircuit): DAG to be checked. - - Returns: - DAGCircuit: DAG with consistent timing and op nodes annotated with duration. - - Raises: - TranspilerError: When pulse gate violate pulse controller constraints. - """ - if self.granularity == 1 and self.min_length == 1: - # we can define arbitrary length pulse with dt resolution - return - - for gate, insts in dag.calibrations.items(): - for qubit_param_pair, schedule in insts.items(): - for _, inst in schedule.instructions: - if isinstance(inst, Play): - pulse = inst.pulse - if pulse.duration % self.granularity != 0: - raise TranspilerError( - f"Pulse duration is not multiple of {self.granularity}. " - "This pulse cannot be played on the specified backend. " - f"Please modify the duration of the custom gate pulse {pulse.name} " - f"which is associated with the gate {gate} of " - f"qubit {qubit_param_pair[0]}." - ) - if pulse.duration < self.min_length: - raise TranspilerError( - f"Pulse gate duration is less than {self.min_length}. " - "This pulse cannot be played on the specified backend. " - f"Please modify the duration of the custom gate pulse {pulse.name} " - f"which is associated with the gate {gate} of " - "qubit {qubit_param_pair[0]}." - ) - - -def _check_alignment_required( - dag: DAGCircuit, - alignment: int, - instructions: Union[Instruction, List[Instruction]], -) -> bool: - """Check DAG nodes and return a boolean representing if instruction scheduling is necessary. - - Args: - dag: DAG circuit to check. - alignment: Instruction alignment condition. - instructions: Target instructions. - - Returns: - If instruction scheduling is necessary. - """ - if not isinstance(instructions, list): - instructions = [instructions] - - if alignment == 1: - # disable alignment if arbitrary t0 value can be used - return False - - if all(len(dag.op_nodes(inst)) == 0 for inst in instructions): - # disable alignment if target instruction is not involved - return False - - # check delay durations - for delay_node in dag.op_nodes(Delay): - duration = delay_node.op.duration - if isinstance(duration, ParameterExpression): - # duration is parametrized: - # raise user warning if backend alignment is not 1. - warnings.warn( - f"Parametrized delay with {repr(duration)} is found in circuit {dag.name}. " - f"This backend requires alignment={alignment}. " - "Please make sure all assigned values are multiple values of the alignment.", - UserWarning, - ) - else: - # duration is bound: - # check duration and trigger alignment if it violates constraint - if duration % alignment != 0: - return True - - # disable alignment if all delays are multiple values of the alignment - return False diff --git a/qiskit/transpiler/passes/scheduling/padding/__init__.py b/qiskit/transpiler/passes/scheduling/padding/__init__.py new file mode 100644 index 000000000000..f6f09caac487 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/padding/__init__.py @@ -0,0 +1,16 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Scheduling pass to fill idle times with gate sequence.""" + +from .dynamical_decoupling import DynamicalDecoupling +from .pad_delay import PadDelay diff --git a/qiskit/transpiler/passes/scheduling/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py similarity index 100% rename from qiskit/transpiler/passes/scheduling/base_padding.py rename to qiskit/transpiler/passes/scheduling/padding/base_padding.py diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py similarity index 99% rename from qiskit/transpiler/passes/scheduling/dynamical_decoupling.py rename to qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index d7425abb3bed..9b54dbbce995 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -25,7 +25,8 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.passes.optimization import Optimize1qGates -from qiskit.transpiler.passes.scheduling.base_padding import BasePadding + +from .base_padding import BasePadding class DynamicalDecoupling(BasePadding): diff --git a/qiskit/transpiler/passes/scheduling/pad_delay.py b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py similarity index 97% rename from qiskit/transpiler/passes/scheduling/pad_delay.py rename to qiskit/transpiler/passes/scheduling/padding/pad_delay.py index 3dc868e67732..c188247e2b26 100644 --- a/qiskit/transpiler/passes/scheduling/pad_delay.py +++ b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py @@ -15,7 +15,8 @@ from qiskit.circuit import Qubit from qiskit.circuit.delay import Delay from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOutNode -from qiskit.transpiler.passes.scheduling.base_padding import BasePadding + +from .base_padding import BasePadding class PadDelay(BasePadding): diff --git a/qiskit/transpiler/passes/scheduling/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/scheduling/__init__.py new file mode 100644 index 000000000000..7c737dc20e0e --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/scheduling/__init__.py @@ -0,0 +1,17 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Scheduling pass to assign instruction start time.""" + +from .asap import ASAPSchedule +from .alap import ALAPSchedule +from .set_io_latency import SetIOLatency diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/scheduling/alap.py similarity index 94% rename from qiskit/transpiler/passes/scheduling/alap.py rename to qiskit/transpiler/passes/scheduling/scheduling/alap.py index 980c917e9dca..983eeb0d0a7d 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/alap.py @@ -14,7 +14,7 @@ from qiskit.circuit import Measure from qiskit.transpiler.exceptions import TranspilerError -from .base_scheduler import BaseScheduler +from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler class ALAPSchedule(BaseScheduler): @@ -40,6 +40,9 @@ def run(self, dag): if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ALAP schedule runs on physical circuits only") + conditional_latency = self.property_set.get("conditional_latency", 0) + clbit_write_latency = self.property_set.get("clbit_write_latency", 0) + node_start_time = dict() idle_before = {q: 0 for q in dag.qubits + dag.clbits} bit_indices = {bit: index for index, bit in enumerate(dag.qubits)} @@ -82,7 +85,7 @@ def run(self, dag): t0 = max(t0q, t0c - op_duration) t1 = t0 + op_duration for clbit in node.op.condition_bits: - idle_before[clbit] = t1 + self.conditional_latency + idle_before[clbit] = t1 + conditional_latency else: t0 = t0q t1 = t0 + op_duration @@ -103,7 +106,7 @@ def run(self, dag): # |t0 + (duration - clbit_write_latency) # for clbit in node.cargs: - idle_before[clbit] = t0 + (op_duration - self.clbit_write_latency) + idle_before[clbit] = t0 + (op_duration - clbit_write_latency) else: # It happens to be directives such as barrier t0 = max(idle_before[bit] for bit in node.qargs + node.cargs) diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/scheduling/asap.py similarity index 92% rename from qiskit/transpiler/passes/scheduling/asap.py rename to qiskit/transpiler/passes/scheduling/scheduling/asap.py index e0b5cdad1544..469d7b7f510d 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/asap.py @@ -14,7 +14,7 @@ from qiskit.circuit import Measure from qiskit.transpiler.exceptions import TranspilerError -from .base_scheduler import BaseScheduler +from qiskit.transpiler.passes.scheduling.scheduling.base_scheduler import BaseScheduler class ASAPSchedule(BaseScheduler): @@ -40,6 +40,9 @@ def run(self, dag): if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ASAP schedule runs on physical circuits only") + conditional_latency = self.property_set.get("conditional_latency", 0) + clbit_write_latency = self.property_set.get("clbit_write_latency", 0) + node_start_time = dict() idle_after = {q: 0 for q in dag.qubits + dag.clbits} bit_indices = {q: index for index, q in enumerate(dag.qubits)} @@ -69,8 +72,8 @@ def run(self, dag): # C ▒▒▒░░░▒▒░░░ # |t0q - conditional_latency # - t0c = max(t0q - self.conditional_latency, t0c) - t1c = t0c + self.conditional_latency + t0c = max(t0q - conditional_latency, t0c) + t1c = t0c + conditional_latency for bit in node.op.condition_bits: # Lock clbit until state is read idle_after[bit] = t1c @@ -111,7 +114,7 @@ def run(self, dag): # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ # |t0c' = t0c + clbit_write_latency # - t0 = max(t0q, t0c - self.clbit_write_latency) + t0 = max(t0q, t0c - clbit_write_latency) t1 = t0 + op_duration for clbit in node.cargs: idle_after[clbit] = t1 diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py similarity index 88% rename from qiskit/transpiler/passes/scheduling/base_scheduler.py rename to qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index d582822b53d6..5cedbd0e1ae5 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -140,11 +140,10 @@ class BaseScheduler(AnalysisPass): The former parameter determines the delay of the register write-access from the beginning of the measure instruction t0, and another parameter determines the delay of conditional gate operation from t0 which comes from the register read-access. + These information might be found in the backend configuration and then should + be copied to the pass manager property set before the pass is called. - Since we usually expect topological ordering and time ordering are identical - without the context of microarchitecture, both latencies are set to zero by default. - In this case, ``Measure`` instruction immediately locks the register C. - Under this configuration, the `alap`-scheduled circuit of above example may become + By default latencies, the `alap`-scheduled circuit of above example may become .. parsed-literal:: @@ -208,36 +207,15 @@ class BaseScheduler(AnalysisPass): CONDITIONAL_SUPPORTED = (Gate, Delay) - def __init__( - self, - durations: InstructionDurations, - clbit_write_latency: int = 0, - conditional_latency: int = 0, - ): + def __init__(self, durations: InstructionDurations): """Scheduler initializer. Args: durations: Durations of instructions to be used in scheduling - clbit_write_latency: A control flow constraints. Because standard superconducting - quantum processor implement dispersive QND readout, the actual data transfer - to the clbit happens after the round-trip stimulus signal is buffered - and discriminated into quantum state. - The interval ``[t0, t0 + clbit_write_latency]`` is regarded as idle time - for clbits associated with the measure instruction. - This defaults to 0 dt which is identical to Qiskit Pulse scheduler. - conditional_latency: A control flow constraints. This value represents - a latency of reading a classical register for the conditional operation. - The gate operation occurs after this latency. This appears as a delay - in front of the DAGOpNode of the gate. - This defaults to 0 dt. """ super().__init__() self.durations = durations - # Control flow constraints. - self.clbit_write_latency = clbit_write_latency - self.conditional_latency = conditional_latency - # Ensure op node durations are attached and in consistent unit self.requires.append(TimeUnitConversion(durations)) diff --git a/qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py b/qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py new file mode 100644 index 000000000000..38fa9f1258d8 --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/scheduling/set_io_latency.py @@ -0,0 +1,64 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Set classical IO latency information to circuit.""" + +from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.dagcircuit import DAGCircuit + + +class SetIOLatency(AnalysisPass): + """Set IOLatency information to the input circuit. + + The ``clbit_write_latency`` and ``conditional_latency`` are added to + the property set of pass manager. These information can be shared among the passes + that perform scheduling on instructions acting on classical registers. + + Once these latencies are added to the property set, this information + is also copied to the output circuit object as protected attributes, + so that it can be utilized outside the transilation, + for example, the timeline visualization can use latency to accurately show + time occupation by instructions on the classical registers. + """ + + def __init__( + self, + clbit_write_latency: int = 0, + conditional_latency: int = 0, + ): + """Create pass with latency information. + + Args: + clbit_write_latency: A control flow constraints. Because standard superconducting + quantum processor implement dispersive QND readout, the actual data transfer + to the clbit happens after the round-trip stimulus signal is buffered + and discriminated into quantum state. + The interval ``[t0, t0 + clbit_write_latency]`` is regarded as idle time + for clbits associated with the measure instruction. + This defaults to 0 dt which is identical to Qiskit Pulse scheduler. + conditional_latency: A control flow constraints. This value represents + a latency of reading a classical register for the conditional operation. + The gate operation occurs after this latency. This appears as a delay + in front of the DAGOpNode of the gate. + This defaults to 0 dt. + """ + super().__init__() + self._conditional_latency = conditional_latency + self._clbit_write_latency = clbit_write_latency + + def run(self, dag: DAGCircuit): + """Add IO latency information. + + Args: + dag: Input DAG circuit. + """ + self.property_set["conditional_latency"] = self._conditional_latency + self.property_set["clbit_write_latency"] = self._clbit_write_latency diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 83b88c5bd369..fe4d3bfc5e22 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -46,7 +46,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule -from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ConstrainedReschedule +from qiskit.transpiler.passes import InstructionDurationCheck from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import PadDelay @@ -214,39 +215,6 @@ def _direction_condition(property_set): _direction = [GateDirection(coupling_map, target)] - # 7. Unify all durations (either SI, or convert to dt if known) - # Schedule the circuit only when scheduling_method is supplied - _time_unit_setup = [ContainsInstruction("delay")] - _time_unit_conversion = [TimeUnitConversion(instruction_durations)] - - def _contains_delay(property_set): - return property_set["contains_delay"] - - _scheduling = [] - if scheduling_method: - _scheduling += _time_unit_conversion - if scheduling_method in {"alap", "as_late_as_possible"}: - _scheduling += [ALAPSchedule(instruction_durations), PadDelay()] - elif scheduling_method in {"asap", "as_soon_as_possible"}: - _scheduling += [ASAPSchedule(instruction_durations), PadDelay()] - else: - raise TranspilerError("Invalid scheduling method %s." % scheduling_method) - - # 8. Call measure alignment. Should come after scheduling. - if ( - timing_constraints.granularity != 1 - or timing_constraints.min_length != 1 - or timing_constraints.acquire_alignment != 1 - ): - _alignments = [ - ValidatePulseGates( - granularity=timing_constraints.granularity, min_length=timing_constraints.min_length - ), - AlignMeasures(alignment=timing_constraints.acquire_alignment), - ] - else: - _alignments = [] - # Build pass manager pm0 = PassManager() if coupling_map or initial_layout: @@ -265,10 +233,62 @@ def _contains_delay(property_set): pm0.append(_unroll) if inst_map and inst_map.has_custom_gate(): pm0.append(PulseGates(inst_map=inst_map)) + + # 7. Unify all durations (either SI, or convert to dt if known) + # Schedule the circuit only when scheduling_method is supplied + # Apply alignment analysis regardless of scheduling for delay validation. if scheduling_method: - pm0.append(_scheduling) + # Do scheduling after unit conversion. + scheduler = { + "alap": ALAPSchedule, + "as_late_as_possible": ALAPSchedule, + "asap": ASAPSchedule, + "as_soon_as_possible": ASAPSchedule, + } + pm0.append(TimeUnitConversion(instruction_durations)) + try: + pm0.append(scheduler[scheduling_method](instruction_durations)) + except KeyError as ex: + raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex elif instruction_durations: - pm0.append(_time_unit_setup) - pm0.append(_time_unit_conversion, condition=_contains_delay) - pm0.append(_alignments) + # No scheduling. But do unit conversion for delays. + def _contains_delay(property_set): + return property_set["contains_delay"] + + pm0.append(ContainsInstruction("delay")) + pm0.append(TimeUnitConversion(instruction_durations), condition=_contains_delay) + if ( + timing_constraints.granularity != 1 + or timing_constraints.min_length != 1 + or timing_constraints.acquire_alignment != 1 + or timing_constraints.pulse_alignment != 1 + ): + # Run alignment analysis regardless of scheduling. + + def _require_alignment(property_set): + return property_set["reschedule_required"] + + pm0.append( + InstructionDurationCheck( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ) + ) + pm0.append( + ConstrainedReschedule( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ), + condition=_require_alignment, + ) + pm0.append( + ValidatePulseGates( + granularity=timing_constraints.granularity, + min_length=timing_constraints.min_length, + ) + ) + if scheduling_method: + # Call padding pass if circuit is scheduled + pm0.append(PadDelay()) + return pm0 diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 3f894484f1a6..4c08c1cdce8f 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -53,7 +53,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule -from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ConstrainedReschedule +from qiskit.transpiler.passes import InstructionDurationCheck from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import PadDelay @@ -286,39 +287,6 @@ def _opt_control(property_set): _opt = [Optimize1qGatesDecomposition(basis_gates), CXCancellation()] - # 10. Unify all durations (either SI, or convert to dt if known) - # Schedule the circuit only when scheduling_method is supplied - _time_unit_setup = [ContainsInstruction("delay")] - _time_unit_conversion = [TimeUnitConversion(instruction_durations)] - - def _contains_delay(property_set): - return property_set["contains_delay"] - - _scheduling = [] - if scheduling_method: - _scheduling += _time_unit_conversion - if scheduling_method in {"alap", "as_late_as_possible"}: - _scheduling += [ALAPSchedule(instruction_durations), PadDelay()] - elif scheduling_method in {"asap", "as_soon_as_possible"}: - _scheduling += [ASAPSchedule(instruction_durations), PadDelay()] - else: - raise TranspilerError("Invalid scheduling method %s." % scheduling_method) - - # 11. Call measure alignment. Should come after scheduling. - if ( - timing_constraints.granularity != 1 - or timing_constraints.min_length != 1 - or timing_constraints.acquire_alignment != 1 - ): - _alignments = [ - ValidatePulseGates( - granularity=timing_constraints.granularity, min_length=timing_constraints.min_length - ), - AlignMeasures(alignment=timing_constraints.acquire_alignment), - ] - else: - _alignments = [] - # Build pass manager pm1 = PassManager() if coupling_map or initial_layout: @@ -341,11 +309,62 @@ def _contains_delay(property_set): pm1.append(_opt + _unroll + _depth_check + _size_check, do_while=_opt_control) if inst_map and inst_map.has_custom_gate(): pm1.append(PulseGates(inst_map=inst_map)) + + # 10. Unify all durations (either SI, or convert to dt if known) + # Schedule the circuit only when scheduling_method is supplied + # Apply alignment analysis regardless of scheduling for delay validation. if scheduling_method: - pm1.append(_scheduling) + # Do scheduling after unit conversion. + scheduler = { + "alap": ALAPSchedule, + "as_late_as_possible": ALAPSchedule, + "asap": ASAPSchedule, + "as_soon_as_possible": ASAPSchedule, + } + pm1.append(TimeUnitConversion(instruction_durations)) + try: + pm1.append(scheduler[scheduling_method](instruction_durations)) + except KeyError as ex: + raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex elif instruction_durations: - pm1.append(_time_unit_setup) - pm1.append(_time_unit_conversion, condition=_contains_delay) - pm1.append(_alignments) + # No scheduling. But do unit conversion for delays. + def _contains_delay(property_set): + return property_set["contains_delay"] + + pm1.append(ContainsInstruction("delay")) + pm1.append(TimeUnitConversion(instruction_durations), condition=_contains_delay) + if ( + timing_constraints.granularity != 1 + or timing_constraints.min_length != 1 + or timing_constraints.acquire_alignment != 1 + or timing_constraints.pulse_alignment != 1 + ): + # Run alignment analysis regardless of scheduling. + + def _require_alignment(property_set): + return property_set["reschedule_required"] + + pm1.append( + InstructionDurationCheck( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ) + ) + pm1.append( + ConstrainedReschedule( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ), + condition=_require_alignment, + ) + pm1.append( + ValidatePulseGates( + granularity=timing_constraints.granularity, + min_length=timing_constraints.min_length, + ) + ) + if scheduling_method: + # Call padding pass if circuit is scheduled + pm1.append(PadDelay()) return pm1 diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 092ac6b0af32..53b2a3e64ce1 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -53,7 +53,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule -from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ConstrainedReschedule +from qiskit.transpiler.passes import InstructionDurationCheck from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import PadDelay @@ -274,39 +275,6 @@ def _opt_control(property_set): CommutativeCancellation(basis_gates=basis_gates), ] - # 9. Unify all durations (either SI, or convert to dt if known) - # Schedule the circuit only when scheduling_method is supplied - _time_unit_setup = [ContainsInstruction("delay")] - _time_unit_conversion = [TimeUnitConversion(instruction_durations)] - - def _contains_delay(property_set): - return property_set["contains_delay"] - - _scheduling = [] - if scheduling_method: - _scheduling += _time_unit_conversion - if scheduling_method in {"alap", "as_late_as_possible"}: - _scheduling += [ALAPSchedule(instruction_durations), PadDelay()] - elif scheduling_method in {"asap", "as_soon_as_possible"}: - _scheduling += [ASAPSchedule(instruction_durations), PadDelay()] - else: - raise TranspilerError("Invalid scheduling method %s." % scheduling_method) - - # 10. Call measure alignment. Should come after scheduling. - if ( - timing_constraints.granularity != 1 - or timing_constraints.min_length != 1 - or timing_constraints.acquire_alignment != 1 - ): - _alignments = [ - ValidatePulseGates( - granularity=timing_constraints.granularity, min_length=timing_constraints.min_length - ), - AlignMeasures(alignment=timing_constraints.acquire_alignment), - ] - else: - _alignments = [] - # Build pass manager pm2 = PassManager() if coupling_map or initial_layout: @@ -330,10 +298,62 @@ def _contains_delay(property_set): if inst_map and inst_map.has_custom_gate(): pm2.append(PulseGates(inst_map=inst_map)) + + # 9. Unify all durations (either SI, or convert to dt if known) + # Schedule the circuit only when scheduling_method is supplied + # Apply alignment analysis regardless of scheduling for delay validation. if scheduling_method: - pm2.append(_scheduling) + # Do scheduling after unit conversion. + scheduler = { + "alap": ALAPSchedule, + "as_late_as_possible": ALAPSchedule, + "asap": ASAPSchedule, + "as_soon_as_possible": ASAPSchedule, + } + pm2.append(TimeUnitConversion(instruction_durations)) + try: + pm2.append(scheduler[scheduling_method](instruction_durations)) + except KeyError as ex: + raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex elif instruction_durations: - pm2.append(_time_unit_setup) - pm2.append(_time_unit_conversion, condition=_contains_delay) - pm2.append(_alignments) + # No scheduling. But do unit conversion for delays. + def _contains_delay(property_set): + return property_set["contains_delay"] + + pm2.append(ContainsInstruction("delay")) + pm2.append(TimeUnitConversion(instruction_durations), condition=_contains_delay) + if ( + timing_constraints.granularity != 1 + or timing_constraints.min_length != 1 + or timing_constraints.acquire_alignment != 1 + or timing_constraints.pulse_alignment != 1 + ): + # Run alignment analysis regardless of scheduling. + + def _require_alignment(property_set): + return property_set["reschedule_required"] + + pm2.append( + InstructionDurationCheck( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ) + ) + pm2.append( + ConstrainedReschedule( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ), + condition=_require_alignment, + ) + pm2.append( + ValidatePulseGates( + granularity=timing_constraints.granularity, + min_length=timing_constraints.min_length, + ) + ) + if scheduling_method: + # Call padding pass if circuit is scheduled + pm2.append(PadDelay()) + return pm2 diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 94f341d911b5..b338f8b1b5e7 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -56,7 +56,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule -from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ConstrainedReschedule +from qiskit.transpiler.passes import InstructionDurationCheck from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import PadDelay @@ -284,39 +285,6 @@ def _opt_control(property_set): CommutativeCancellation(), ] - # 9. Unify all durations (either SI, or convert to dt if known) - # Schedule the circuit only when scheduling_method is supplied - _time_unit_setup = [ContainsInstruction("delay")] - _time_unit_conversion = [TimeUnitConversion(instruction_durations)] - - def _contains_delay(property_set): - return property_set["contains_delay"] - - _scheduling = [] - if scheduling_method: - _scheduling += _time_unit_conversion - if scheduling_method in {"alap", "as_late_as_possible"}: - _scheduling += [ALAPSchedule(instruction_durations), PadDelay()] - elif scheduling_method in {"asap", "as_soon_as_possible"}: - _scheduling += [ASAPSchedule(instruction_durations), PadDelay()] - else: - raise TranspilerError("Invalid scheduling method %s." % scheduling_method) - - # 10. Call measure alignment. Should come after scheduling. - if ( - timing_constraints.granularity != 1 - or timing_constraints.min_length != 1 - or timing_constraints.acquire_alignment != 1 - ): - _alignments = [ - ValidatePulseGates( - granularity=timing_constraints.granularity, min_length=timing_constraints.min_length - ), - AlignMeasures(alignment=timing_constraints.acquire_alignment), - ] - else: - _alignments = [] - # Build pass manager pm3 = PassManager() pm3.append(_unroll3q) @@ -352,11 +320,62 @@ def _contains_delay(property_set): pm3.append(_opt + _unroll + _depth_check + _size_check, do_while=_opt_control) if inst_map and inst_map.has_custom_gate(): pm3.append(PulseGates(inst_map=inst_map)) + + # 9. Unify all durations (either SI, or convert to dt if known) + # Schedule the circuit only when scheduling_method is supplied + # Apply alignment analysis regardless of scheduling for delay validation. if scheduling_method: - pm3.append(_scheduling) + # Do scheduling after unit conversion. + scheduler = { + "alap": ALAPSchedule, + "as_late_as_possible": ALAPSchedule, + "asap": ASAPSchedule, + "as_soon_as_possible": ASAPSchedule, + } + pm3.append(TimeUnitConversion(instruction_durations)) + try: + pm3.append(scheduler[scheduling_method](instruction_durations)) + except KeyError as ex: + raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex elif instruction_durations: - pm3.append(_time_unit_setup) - pm3.append(_time_unit_conversion, condition=_contains_delay) - pm3.append(_alignments) + # No scheduling. But do unit conversion for delays. + def _contains_delay(property_set): + return property_set["contains_delay"] + + pm3.append(ContainsInstruction("delay")) + pm3.append(TimeUnitConversion(instruction_durations), condition=_contains_delay) + if ( + timing_constraints.granularity != 1 + or timing_constraints.min_length != 1 + or timing_constraints.acquire_alignment != 1 + or timing_constraints.pulse_alignment != 1 + ): + # Run alignment analysis regardless of scheduling. + + def _require_alignment(property_set): + return property_set["reschedule_required"] + + pm3.append( + InstructionDurationCheck( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ) + ) + pm3.append( + ConstrainedReschedule( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ), + condition=_require_alignment, + ) + pm3.append( + ValidatePulseGates( + granularity=timing_constraints.granularity, + min_length=timing_constraints.min_length, + ) + ) + if scheduling_method: + # Call padding pass if circuit is scheduled + pm3.append(PadDelay()) return pm3 diff --git a/qiskit/transpiler/runningpassmanager.py b/qiskit/transpiler/runningpassmanager.py index 5e4e5ff02b15..43af938d1879 100644 --- a/qiskit/transpiler/runningpassmanager.py +++ b/qiskit/transpiler/runningpassmanager.py @@ -129,6 +129,8 @@ def run(self, circuit, output_name=None, callback=None): else: circuit.name = name circuit._layout = self.property_set["layout"] + circuit._clbit_write_latency = self.property_set["clbit_write_latency"] + circuit._conditional_latency = self.property_set["conditional_latency"] return circuit diff --git a/releasenotes/notes/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml b/releasenotes/notes/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml new file mode 100644 index 000000000000..3c134ef5a68d --- /dev/null +++ b/releasenotes/notes/update-instruction-alignment-passes-ef0f20d4f89f95f3.yaml @@ -0,0 +1,31 @@ +--- +features: + - | + New pass group :mod:`qiskit.transpiler.passes.scheduling.alignments` has been added. + This consists of :class:`ConstrainedReschedule`, :class:`ValidatePulseGates`, + and :class:`InstructionDurationCheck`. The new pass :class:`ConstrainedReschedule` + considers both hardware alignment constraints of ``pulse_alignment`` and ``acquire_alignment``, + which is a drop-in replacement of :class:`AlignMeasures`. +upgrade: + - | + The ordering of scheduling-relevant passes in the preset pass manager has been upgraded. + Note that now scheduling passes and alignment passes are all sub-class of the + :class:`AnalysisPass` that only updates the property set of the pass manager, + in which new property set ``node_start_time`` is created there to perform + these operation on nodes scheduled on the absolute time. + + Previously, the pass chain had been implemented as ``scheduling -> alignment`` + which were both transform passes thus there were multiple DAG instance regeneration. + In addition, scheduling occured in each pass to obtain instruction start time. + + Now this chain becomes ``scheduling -> alignment -> padding`` + where the DAG replacement only occurs at the end, i.e. at the ``padding`` pass. + Note that who wants to design a new pass manager must insert one of the padding passes + at the end of the pass chain. Without the padding pass, the scheduled nodes + are not reflected in the target code since the QASM payload has no notion of + the scheduling for underlying instructions. +deprecations: + - | + Alignment pass :class:`AlignMeasures` has been deprecated and will be removed. + The constructor of this pass now internally calls :class:`ConstrainedReschedule` + and returns the instance of its instance. diff --git a/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml b/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml index 6a958d5ba268..4d9f597e49f1 100644 --- a/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml +++ b/releasenotes/notes/upgrade-alap-asap-passes-bcacc0f1053c9828.yaml @@ -1,22 +1,30 @@ --- +features: + - | + New pass :class:`SetIOLatency` has been added. This pass takes + ``clbit_write_latency`` and ``conditional_latency`` and set these values to + property set of the pass manager. + Following passes that perform scheduling on instruction acting on classical resgisters + may consider these classical I/O latencies to provide more presice scheduling on + dynamic circuit. upgrade: - | The circuit scheduling passes :class:`~qiskit.transpiler.passes.scheduling.asap.ASAPSchedule` and :class:`~qiskit.transpiler.passes.scheduling.alap.ALAPSchedule` have been upgraded. - Now these passes can be instantiated with two new parameters - ``clbit_write_latency`` and ``conditional_latency``, which allows scheduler to - more carefully schedule instructions with classical feedback. - The former option represents a latency of clbit access from the begging of the - measurement instruction, and the other represents a latency of the - conditional gate operation from the first conditional clbit read-access. + + Now these passes consider I/O latencies for measurement and conditional operations. + These information should be set by :class:`SetIOLatency` pass in advance. + The I/O latency to write classical register is represneted by ``clbit_write_latency``, + and one for reading the register is presented by ``conditional_latency``. + These properties are loaded from backend and set to the pass manager's property set. The standard behavior of these passes has been also upgraded to align timing ordering with the topological ordering of the DAG nodes. This change may affect the scheduling outcome if it includes conditional operations, or simultaneously measuring two qubits with the same classical register (edge-case). - To reproduce conventional behavior, create a pass manager with one of - scheduling passes with ``clbit_write_latency`` identical to the measurement instruction length. + To reproduce conventional behavior, set ``clbit_write_latency`` identical + to the measurement instruction length. The following example may clearly explain the change of scheduler spec. @@ -38,7 +46,7 @@ upgrade: from qiskit import QuantumCircuit from qiskit.transpiler import InstructionDurations, PassManager - from qiskit.transpiler.passes import ALAPSchedule, PadDelay + from qiskit.transpiler.passes import ALAPSchedule, PadDelay, SetIOLatency from qiskit.visualization.timeline import draw circuit = QuantumCircuit(3, 1) @@ -51,7 +59,8 @@ upgrade: pm = PassManager( [ - ALAPSchedule(durations, clbit_write_latency=800, conditional_latency=0), + SetIOLatency(clbit_write_latency=800, conditional_latency=0), + ALAPSchedule(durations), PadDelay(), ] ) diff --git a/test/python/transpiler/test_instruction_alignments.py b/test/python/transpiler/test_instruction_alignments.py index 233fea1efd43..845566d17682 100644 --- a/test/python/transpiler/test_instruction_alignments.py +++ b/test/python/transpiler/test_instruction_alignments.py @@ -18,9 +18,13 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import ( AlignMeasures, + InstructionDurationCheck, + ConstrainedReschedule, ValidatePulseGates, ALAPSchedule, + ASAPSchedule, PadDelay, + SetIOLatency, ) @@ -75,13 +79,10 @@ def test_t1_experiment_type(self): [ # reproduce old behavior of 0.20.0 before #7655 # currently default write latency is 0 - ALAPSchedule( - durations=self.instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ), + SetIOLatency(clbit_write_latency=1600, conditional_latency=0), + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(acquire_alignment=16), PadDelay(), - AlignMeasures(alignment=16), ] ) @@ -129,13 +130,10 @@ def test_hanh_echo_experiment_type(self): [ # reproduce old behavior of 0.20.0 before #7655 # currently default write latency is 0 - ALAPSchedule( - durations=self.instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ), + SetIOLatency(clbit_write_latency=1600, conditional_latency=0), + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(acquire_alignment=16), PadDelay(), - AlignMeasures(alignment=16), ] ) @@ -187,13 +185,10 @@ def test_mid_circuit_measure(self): [ # reproduce old behavior of 0.20.0 before #7655 # currently default write latency is 0 - ALAPSchedule( - durations=self.instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ), + SetIOLatency(clbit_write_latency=1600, conditional_latency=0), + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(acquire_alignment=16), PadDelay(), - AlignMeasures(alignment=16), ] ) @@ -256,13 +251,10 @@ def test_mid_circuit_multiq_gates(self): [ # reproduce old behavior of 0.20.0 before #7655 # currently default write latency is 0 - ALAPSchedule( - durations=self.instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ), + SetIOLatency(clbit_write_latency=1600, conditional_latency=0), + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(acquire_alignment=16), PadDelay(), - AlignMeasures(alignment=16), ] ) @@ -295,10 +287,12 @@ def test_alignment_is_not_processed(self): # pre scheduling is not necessary because alignment is skipped # this is to minimize breaking changes to existing code. - pm = PassManager(AlignMeasures(alignment=16)) - aligned_circuit = pm.run(circuit) + pm = PassManager() + + pm.append(InstructionDurationCheck(acquire_alignment=16)) + pm.run(circuit) - self.assertEqual(aligned_circuit, circuit) + self.assertFalse(pm.property_set["reschedule_required"]) def test_circuit_using_clbit(self): """Test a circuit with instructions using a common clbit. @@ -342,13 +336,10 @@ def test_circuit_using_clbit(self): [ # reproduce old behavior of 0.20.0 before #7655 # currently default write latency is 0 - ALAPSchedule( - durations=self.instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ), - PadDelay(), - AlignMeasures(alignment=16), + SetIOLatency(clbit_write_latency=1600, conditional_latency=0), + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(acquire_alignment=16), + PadDelay(fill_very_end=False), ] ) @@ -363,19 +354,174 @@ def test_circuit_using_clbit(self): ref_circuit.delay(432, 2, unit="dt") # 2032 - 1600 ref_circuit.measure(0, 0) ref_circuit.x(1).c_if(0, 1) - ref_circuit.delay(160, 0, unit="dt") ref_circuit.measure(2, 0) self.assertEqual(aligned_circuit, ref_circuit) + def test_programmed_delay_preserved(self): + """Intentionally programmed delay will be kept after reschedule. + + No delay + ++++++++ + + (input) + ┌────────────────┐┌───┐ ░ ┌───┐ + q_0: ┤ Delay(100[dt]) ├┤ X ├─░─┤ X ├ + ├────────────────┤└───┘ ░ └───┘ + q_1: ┤ Delay(272[dt]) ├──────░────── + └────────────────┘ ░ + + (aligned) + ┌────────────────┐┌───┐ ░ ┌───┐ + q_0: ┤ Delay(112[dt]) ├┤ X ├─░─┤ X ├ + ├────────────────┤└───┘ ░ └───┘ + q_1: ┤ Delay(272[dt]) ├──────░────── + └────────────────┘ ░ + + With delay (intentional post buffer) + ++++++++++++++++++++++++++++++++++++ + + (input) ... this is identical to no delay pattern without reschedule + ┌────────────────┐┌───┐┌───────────────┐ ░ ┌───┐ + q_0: ┤ Delay(100[dt]) ├┤ X ├┤ Delay(10[dt]) ├─░─┤ X ├ + ├────────────────┤└───┘└───────────────┘ ░ └───┘ + q_1: ┤ Delay(272[dt]) ├───────────────────────░────── + └────────────────┘ ░ + + (aligned) + ┌────────────────┐┌───┐┌───────────────┐ ░ ┌──────────────┐┌───┐ + q_0: ┤ Delay(112[dt]) ├┤ X ├┤ Delay(10[dt]) ├─░─┤ Delay(6[dt]) ├┤ X ├ + ├────────────────┤└───┘└───────────────┘ ░ └──────────────┘└───┘ + q_1: ┤ Delay(282[dt]) ├───────────────────────░────────────────────── + └────────────────┘ ░ + + """ + + pm = PassManager( + [ + ASAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(pulse_alignment=16), + PadDelay(fill_very_end=False), + ] + ) + + pm_only_schedule = PassManager( + [ + ASAPSchedule(durations=self.instruction_durations), + PadDelay(fill_very_end=False), + ] + ) + + circuit_no_delay = QuantumCircuit(2) + circuit_no_delay.delay(100, 0, unit="dt") + circuit_no_delay.x(0) # q0 ends here at t = 260, t = 260 - 272 is free + circuit_no_delay.delay(160 + 112, 1, unit="dt") + circuit_no_delay.barrier() # q0 and q1 is aligned here at t = 272 dt + circuit_no_delay.x(0) + + ref_no_delay = QuantumCircuit(2) + ref_no_delay.delay(112, 0, unit="dt") + ref_no_delay.x(0) + ref_no_delay.delay(160 + 100 + 12, 1, unit="dt") # this t0 doesn't change + ref_no_delay.barrier() + ref_no_delay.x(0) # no buffer + + self.assertEqual(pm.run(circuit_no_delay), ref_no_delay) + + circuit_with_delay = QuantumCircuit(2) + circuit_with_delay.delay(100, 0, unit="dt") + circuit_with_delay.x(0) # q0 ends here at t = 260 + circuit_with_delay.delay(10, 0, unit="dt") # intentional post buffer of 10 dt to next X(0) + circuit_with_delay.delay(160 + 112, 1, unit="dt") # q0 and q1 is aligned here at t = 272 dt + circuit_with_delay.barrier() + circuit_with_delay.x(0) + + ref_with_delay = QuantumCircuit(2) + ref_with_delay.delay(112, 0, unit="dt") + ref_with_delay.x(0) + ref_with_delay.delay(10, 0, unit="dt") # this delay survive + ref_with_delay.delay(160 + 100 + 12 + 10, 1, unit="dt") + ref_with_delay.barrier() + ref_with_delay.delay(6, 0, unit="dt") # extra delay for next X0 + ref_with_delay.x(0) # at least 10dt buffer is preserved + + self.assertEqual(pm.run(circuit_with_delay), ref_with_delay) + + # check if circuit is identical without reschedule + self.assertEqual( + pm_only_schedule.run(circuit_no_delay), + pm_only_schedule.run(circuit_with_delay), + ) + + def test_both_pulse_and_acquire_alignment(self): + """Test when both acquire and pulse alignment are specified. + + (input) + ┌────────────────┐┌───┐┌───────────────┐┌─┐ + q: ┤ Delay(100[dt]) ├┤ X ├┤ Delay(10[dt]) ├┤M├ + └────────────────┘└───┘└───────────────┘└╥┘ + c: 1/═════════════════════════════════════════╩═ + 0 + + (aligned) + ┌────────────────┐┌───┐┌───────────────┐┌─┐ + q: ┤ Delay(112[dt]) ├┤ X ├┤ Delay(16[dt]) ├┤M├ + └────────────────┘└───┘└───────────────┘└╥┘ + c: 1/═════════════════════════════════════════╩═ + 0 + """ + pm = PassManager( + [ + ALAPSchedule(durations=self.instruction_durations), + ConstrainedReschedule(pulse_alignment=16, acquire_alignment=16), + PadDelay(fill_very_end=False), + ] + ) + + circuit = QuantumCircuit(1, 1) + circuit.delay(100, 0, unit="dt") + circuit.x(0) + circuit.delay(10, 0, unit="dt") + circuit.measure(0, 0) + + ref_circ = QuantumCircuit(1, 1) + ref_circ.delay(112, 0, unit="dt") + ref_circ.x(0) + ref_circ.delay(16, 0, unit="dt") + ref_circ.measure(0, 0) + + self.assertEqual(pm.run(circuit), ref_circ) + + def test_deprecated_align_measure(self): + """Test if old AlignMeasures can be still used and warning is raised.""" + circuit = QuantumCircuit(1, 1) + circuit.x(0) + circuit.delay(100) + circuit.measure(0, 0) + + with self.assertWarns(FutureWarning): + pm_old = PassManager( + [ + ALAPSchedule(durations=self.instruction_durations), + AlignMeasures(alignment=16), + PadDelay(fill_very_end=False), + ] + ) + + pm_new = PassManager( + [ + ALAPSchedule(durations=self.instruction_durations), + AlignMeasures(alignment=16), + PadDelay(fill_very_end=False), + ] + ) + + self.assertEqual(pm_old.run(circuit), pm_new.run(circuit)) + class TestPulseGateValidation(QiskitTestCase): """A test for pulse gate validation pass.""" - def setUp(self): - super().setUp() - self.pulse_gate_validation_pass = ValidatePulseGates(granularity=16, min_length=64) - def test_invalid_pulse_duration(self): """Kill pass manager if invalid pulse gate is found.""" @@ -390,8 +536,9 @@ def test_invalid_pulse_duration(self): circuit.x(0) circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) + pm.run(circuit) def test_short_pulse_duration(self): """Kill pass manager if invalid pulse gate is found.""" @@ -407,8 +554,9 @@ def test_short_pulse_duration(self): circuit.x(0) circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) + pm.run(circuit) def test_short_pulse_duration_multiple_pulse(self): """Kill pass manager if invalid pulse gate is found.""" @@ -428,8 +576,9 @@ def test_short_pulse_duration_multiple_pulse(self): circuit.x(0) circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) + pm.run(circuit) def test_valid_pulse_duration(self): """No error raises if valid calibration is provided.""" @@ -445,7 +594,8 @@ def test_valid_pulse_duration(self): circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) # just not raise an error - self.pulse_gate_validation_pass(circuit) + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) + pm.run(circuit) def test_no_calibration(self): """No error raises if no calibration is addedd.""" @@ -454,4 +604,5 @@ def test_no_calibration(self): circuit.x(0) # just not raise an error - self.pulse_gate_validation_pass(circuit) + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) + pm.run(circuit) diff --git a/test/python/transpiler/test_scheduling_padding_pass.py b/test/python/transpiler/test_scheduling_padding_pass.py index e3dfc2809167..174b2798674d 100644 --- a/test/python/transpiler/test_scheduling_padding_pass.py +++ b/test/python/transpiler/test_scheduling_padding_pass.py @@ -19,7 +19,7 @@ from qiskit.pulse import Schedule, Play, Constant, DriveChannel from qiskit.test import QiskitTestCase from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule, PadDelay +from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule, PadDelay, SetIOLatency from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError @@ -444,10 +444,18 @@ def test_measure_after_c_if_on_edge_locking(self): # lock at the end edge actual_asap = PassManager( - [ASAPSchedule(durations, clbit_write_latency=1000), PadDelay()] + [ + SetIOLatency(clbit_write_latency=1000), + ASAPSchedule(durations), + PadDelay(), + ] ).run(qc) actual_alap = PassManager( - [ALAPSchedule(durations, clbit_write_latency=1000), PadDelay()] + [ + SetIOLatency(clbit_write_latency=1000), + ALAPSchedule(durations), + PadDelay(), + ] ).run(qc) # start times of 2nd measure depends on ASAP/ALAP @@ -496,19 +504,19 @@ def test_active_reset_circuit(self, write_lat, cond_lat): qc.x(0).c_if(0, 1) durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) + actual_asap = PassManager( [ - ASAPSchedule( - durations, clbit_write_latency=write_lat, conditional_latency=cond_lat - ), + SetIOLatency(clbit_write_latency=write_lat, conditional_latency=cond_lat), + ASAPSchedule(durations), PadDelay(), ] ).run(qc) + actual_alap = PassManager( [ - ALAPSchedule( - durations, clbit_write_latency=write_lat, conditional_latency=cond_lat - ), + SetIOLatency(clbit_write_latency=write_lat, conditional_latency=cond_lat), + ALAPSchedule(durations), PadDelay(), ] ).run(qc) @@ -630,10 +638,19 @@ def test_random_complicated_circuit(self): ) actual_asap = PassManager( - [ASAPSchedule(durations, clbit_write_latency=100, conditional_latency=200), PadDelay()] + [ + SetIOLatency(clbit_write_latency=100, conditional_latency=200), + ASAPSchedule(durations), + PadDelay(), + ] ).run(qc) + actual_alap = PassManager( - [ALAPSchedule(durations, clbit_write_latency=100, conditional_latency=200), PadDelay()] + [ + SetIOLatency(clbit_write_latency=100, conditional_latency=200), + ALAPSchedule(durations), + PadDelay(), + ] ).run(qc) expected_asap = QuantumCircuit(3, 1) From 5dc12ceba79f672048a998ab55d467a9540a9f0b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 23 Mar 2022 16:09:38 -0400 Subject: [PATCH 2/5] Add CODEOWNERS for the primitives submodule (#7810) The primitives submodule was recently added in the #7723. As the primary authors of this new submodule were @ikkoham and @t-imamichi and they are the most familiar with its operation and how it was written this commit adds both of them as codeowners for this submodule. As codeowners they have merge permission on the submodule as well as a formal responsibility to review and maintain it. Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0ad6b7413e61..219df3897a0a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -32,6 +32,7 @@ qpy/ @Qiskit/terra-core @nkanazawa1989 pulse/ @Qiskit/terra-core @eggerdj @nkanazawa1989 @danpuzzuoli scheduler/ @Qiskit/terra-core @eggerdj @nkanazawa1989 @danpuzzuoli visualization/ @Qiskit/terra-core @nonhermitian @nkanazawa1989 +primitives/ @Qiskit/terra-core @ikkoham @t-imamichi # Override the release notes directories to have _no_ code owners, so any review # from somebody with write access is acceptable. /releasenotes/notes From 95c2aa29c3f4f697028f0d643da6b250d665c8f1 Mon Sep 17 00:00:00 2001 From: georgios-ts <45130028+georgios-ts@users.noreply.github.com> Date: Wed, 23 Mar 2022 23:57:23 +0200 Subject: [PATCH 3/5] Rework `BasisTranslator` for faster basis search. (#7211) * Rework `BasisTranslator` for faster basis search. Closes #5539. This commit reworks the logic of `BasisTranslator._basis_search` to perform a modified BFS (instead of A* search) while searching for a set of transformation that maps source basis to target basis. In more detail, we build a directed graph `G = (V, E)` where `V` is the set of gates in the library and we add an edge `(ga, gb)` if there is an equivalence rule in the library `R = gb -> C_gb` and `ga in C_gb` (in the edge we also attach the rule `R`). Then, we do a BFS starting from the gates in the target basis. We traverse an edge `(ga, gb, R)` only if we have previously visited all the gates appearing in the right side of `R`. We terminate early if we've reached all gates in the original circuit. Otherwise, we can't map the given circuit in this basis. The performance improvement here mainly shows up when we attempt to target an unreachable basis, since in other cases the previously used A* search was already fast and after all, the real bottleneck of `BasisTranslator` for larger circuits is when we actually apply the basis transformation rules. * switch from bfs to dijkstra search for more efficient output circuits * lint * move visitor class outside of `_basis_search` function * set comprehension Co-authored-by: Jake Lishman * fix `expanded_target` if 1q-non global op * adjust test case * docstring and log messages improvements * fix failing tests * release note Co-authored-by: Jake Lishman Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../standard_gates/equivalence_library.py | 4 +- .../passes/basis/basis_translator.py | 283 +++++++++++------- ...ork-basis-translator-a83dc46cbc71c3b1.yaml | 7 + .../transpiler/test_basis_translator.py | 18 +- 4 files changed, 194 insertions(+), 118 deletions(-) create mode 100644 releasenotes/notes/rework-basis-translator-a83dc46cbc71c3b1.yaml diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index 1e702b651442..51a6db1991ca 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -584,13 +584,13 @@ # q_1: ┤ Sx ├ q_1: ─────┤1 ├┤ sx^0.5 ├───── # └────┘ └───────────┘└────────┘ q = QuantumRegister(2, "q") -csx_to_zx45 = QuantumCircuit(q, global_phase=pi / 8) +csx_to_zx45 = QuantumCircuit(q, global_phase=pi / 4) for inst, qargs, cargs in [ (XGate(), [q[0]], []), (RZXGate(pi / 4), [q[0], q[1]], []), (TdgGate(), [q[0]], []), (XGate(), [q[0]], []), - (SXGate().power(0.5), [q[1]], []), + (RXGate(pi / 4), [q[1]], []), ]: csx_to_zx45.append(inst, qargs, cargs) _sel.add_equivalence(CSXGate(), csx_to_zx45) diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index c1341d74f61a..a379c8249ae3 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -15,14 +15,13 @@ import time import logging -from heapq import heappush, heappop from itertools import zip_longest -from itertools import count as iter_count from collections import defaultdict -import numpy as np +import retworkx from qiskit.circuit import Gate, ParameterVector, QuantumRegister +from qiskit.circuit.equivalence import Key from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError @@ -38,11 +37,10 @@ class BasisTranslator(TransformationPass): This pass operates in several steps: * Determine the source basis from the input circuit. - * Perform an A* search over basis sets, starting from the source basis and - targeting the device's target_basis, with edges discovered from the - provided EquivalenceLibrary. The heuristic used by the A* search is the - number of distinct circuit basis gates not in the target_basis, plus the - number of distinct device basis gates not used in the current basis. + * Perform a Dijkstra search over basis sets, starting from the device's + target_basis new gates are being generated using the rules from the provided + EquivalenceLibrary and the search stops if all gates in the source basis have + been generated. * The found path, as a set of rules from the EquivalenceLibrary, is composed into a set of gate replacement rules. * The composed replacement rules are applied in-place to each op node which @@ -143,7 +141,7 @@ def run(self, dag): # do an extra non-local search for this op to ensure we include any # single qubit operation for (1,) as valid. This pattern also holds # true for > 2q ops too (so for 4q operations we need to check for 3q, 2q, - # and 1q opertaions in the same manner) + # and 1q operations in the same manner) if qargs in self._qargs_with_non_global_operation or any( frozenset(qargs).issuperset(incomplete_qargs) for incomplete_qargs in self._qargs_with_non_global_operation @@ -162,13 +160,11 @@ def run(self, dag): # Search for a path from source to target basis. search_start_time = time.time() - basis_transforms = _basis_search( - self._equiv_lib, source_basis, target_basis, _basis_heuristic - ) + basis_transforms = _basis_search(self._equiv_lib, source_basis, target_basis) qarg_local_basis_transforms = {} for qarg, local_source_basis in qargs_local_source_basis.items(): - expanded_target = target_basis | self._qargs_with_non_global_operation[qarg] + expanded_target = set(target_basis) # For any multiqubit operation that contains a subset of qubits that # has a non-local operation, include that non-local operation in the # search. This matches with the check we did above to include those @@ -177,6 +173,8 @@ def run(self, dag): for non_local_qarg, local_basis in self._qargs_with_non_global_operation.items(): if qarg.issuperset(non_local_qarg): expanded_target |= local_basis + else: + expanded_target |= self._qargs_with_non_global_operation[tuple(qarg)] logger.info( "Performing BasisTranslator search from source basis %s to target " @@ -185,10 +183,20 @@ def run(self, dag): expanded_target, qarg, ) - qarg_local_basis_transforms[qarg] = _basis_search( - self._equiv_lib, local_source_basis, expanded_target, _basis_heuristic + local_basis_transforms = _basis_search( + self._equiv_lib, local_source_basis, expanded_target ) + if local_basis_transforms is None: + raise TranspilerError( + "Unable to map source basis {} to target basis {} on qarg {} " + "over library {}.".format( + local_source_basis, expanded_target, qarg, self._equiv_lib + ) + ) + + qarg_local_basis_transforms[qarg] = local_basis_transforms + search_end_time = time.time() logger.info( "Basis translation path search completed in %.3fs.", search_end_time - search_start_time @@ -288,22 +296,100 @@ def replace_node(node, instr_map): return dag -def _basis_heuristic(basis, target): - """Simple metric to gauge distance between two bases as the number of - elements in the symmetric difference of the circuit basis and the device - basis. - """ - return len({gate_name for gate_name, gate_num_qubits in basis} ^ target) +class StopIfBasisRewritable(Exception): + """Custom exception that signals `retworkx.dijkstra_search` to stop.""" + + +class BasisSearchVisitor(retworkx.visit.DijkstraVisitor): + """Handles events emitted during `retworkx.dijkstra_search`.""" + + def __init__(self, graph, source_basis, target_basis, num_gates_for_rule): + self.graph = graph + self.target_basis = set(target_basis) + self._source_gates_remain = set(source_basis) + self._num_gates_remain_for_rule = dict(num_gates_for_rule) + self._basis_transforms = [] + self._predecessors = dict() + self._opt_cost_map = dict() + + def discover_vertex(self, v, score): + gate = self.graph[v] + self._source_gates_remain.discard(gate) + self._opt_cost_map[gate] = score + rule = self._predecessors.get(gate, None) + if rule is not None: + logger.debug( + "Gate %s generated using rule \n%s\n with total cost of %s.", + gate.name, + rule.circuit, + score, + ) + self._basis_transforms.append((gate.name, gate.num_qubits, rule.params, rule.circuit)) + # we can stop the search if we have found all gates in the original ciruit. + if not self._source_gates_remain: + # if we start from source gates and apply `basis_transforms` in reverse order, we'll end + # up with gates in the target basis. Note though that `basis_transforms` may include + # additional transformations that are not required to map our source gates to the given + # target basis. + self._basis_transforms.reverse() + raise StopIfBasisRewritable + + def examine_edge(self, edge): + _, target, edata = edge + if edata is None: + return + + index = edata["index"] + self._num_gates_remain_for_rule[index] -= 1 + + target = self.graph[target] + # if there are gates in this `rule` that we have not yet generated, we can't apply + # this `rule`. if `target` is already in basis, it's not beneficial to use this rule. + if self._num_gates_remain_for_rule[index] > 0 or target in self.target_basis: + raise retworkx.visit.PruneSearch + + def edge_relaxed(self, edge): + _, target, edata = edge + if edata is not None: + gate = self.graph[target] + self._predecessors[gate] = edata["rule"] + + def edge_cost(self, edge): + """Returns the cost of an edge. + + This function computes the cost of this edge rule by summing + the costs of all gates in the rule equivalence circuit. In the + end, we need to subtract the cost of the source since `dijkstra` + will later add it. + """ + + if edge is None: + # the target of the edge is a gate in the target basis, + # so we return a default value of 1. + return 1 + cost_tot = 0 + rule = edge["rule"] + for gate, qargs, _ in rule.circuit: + key = Key(name=gate.name, num_qubits=len(qargs)) + cost_tot += self._opt_cost_map[key] -def _basis_search(equiv_lib, source_basis, target_basis, heuristic): + source = edge["source"] + return cost_tot - self._opt_cost_map[source] + + @property + def basis_transforms(self): + """Returns the gate basis transforms.""" + return self._basis_transforms + + +def _basis_search(equiv_lib, source_basis, target_basis): """Search for a set of transformations from source_basis to target_basis. Args: equiv_lib (EquivalenceLibrary): Source of valid translations source_basis (Set[Tuple[gate_name: str, gate_num_qubits: int]]): Starting basis. target_basis (Set[gate_name: str]): Target basis. - heuristic (Callable[[source_basis, target_basis], int]): distance heuristic. Returns: Optional[List[Tuple[gate, equiv_params, equiv_circuit]]]: List of (gate, @@ -312,100 +398,75 @@ def _basis_search(equiv_lib, source_basis, target_basis, heuristic): was found. """ - source_basis = frozenset(source_basis) - target_basis = frozenset(target_basis) - - open_set = set() # Bases found but not yet inspected. - closed_set = set() # Bases found and inspected. - - # Priority queue for inspection order of open_set. Contains Tuple[priority, count, basis] - open_heap = [] - - # Map from bases in closed_set to predecessor with lowest cost_from_source. - # Values are Tuple[prev_basis, gate_name, params, circuit]. - came_from = {} - - basis_count = iter_count() # Used to break ties in priority. - - open_set.add(source_basis) - heappush(open_heap, (0, next(basis_count), source_basis)) - - # Map from basis to lowest found cost from source. - cost_from_source = defaultdict(lambda: np.inf) - cost_from_source[source_basis] = 0 - - # Map from basis to cost_from_source + heuristic. - est_total_cost = defaultdict(lambda: np.inf) - est_total_cost[source_basis] = heuristic(source_basis, target_basis) - logger.debug("Begining basis search from %s to %s.", source_basis, target_basis) - while open_set: - _, _, current_basis = heappop(open_heap) - - if current_basis in closed_set: - # When we close a node, we don't remove it from the heap, - # so skip here. - continue - - if {gate_name for gate_name, gate_num_qubits in current_basis}.issubset(target_basis): - # Found target basis. Construct transform path. - rtn = [] - last_basis = current_basis - while last_basis != source_basis: - prev_basis, gate_name, gate_num_qubits, params, equiv = came_from[last_basis] - - rtn.append((gate_name, gate_num_qubits, params, equiv)) - last_basis = prev_basis - rtn.reverse() - - logger.debug("Transformation path:") - for gate_name, gate_num_qubits, params, equiv in rtn: - logger.debug("%s/%s => %s\n%s", gate_name, gate_num_qubits, params, equiv) - return rtn - - logger.debug("Inspecting basis %s.", current_basis) - open_set.remove(current_basis) - closed_set.add(current_basis) - - for gate_name, gate_num_qubits in current_basis: - if gate_name in target_basis: - continue - - equivs = equiv_lib._get_equivalences((gate_name, gate_num_qubits)) - - basis_remain = current_basis - {(gate_name, gate_num_qubits)} - neighbors = [ + source_basis = { + (gate_name, gate_num_qubits) + for gate_name, gate_num_qubits in source_basis + if gate_name not in target_basis + } + + # if source basis is empty, no work to be done. + if not source_basis: + return [] + + all_gates_in_lib = set() + + graph = retworkx.PyDiGraph() + nodes_to_indices = dict() + num_gates_for_rule = dict() + + def lazy_setdefault(key): + if key not in nodes_to_indices: + nodes_to_indices[key] = graph.add_node(key) + return nodes_to_indices[key] + + rcounter = 0 # running sum of the number of equivalence rules in the library. + for key in equiv_lib._get_all_keys(): + target = lazy_setdefault(key) + all_gates_in_lib.add(key) + for equiv in equiv_lib._get_equivalences(key): + sources = { + Key(name=gate.name, num_qubits=len(qargs)) for gate, qargs, _ in equiv.circuit + } + all_gates_in_lib |= sources + edges = [ ( - frozenset( - basis_remain - | {(inst.name, inst.num_qubits) for inst, qargs, cargs in equiv.data} - ), - params, - equiv, + lazy_setdefault(source), + target, + {"index": rcounter, "rule": equiv, "source": source}, ) - for params, equiv in equivs + for source in sources ] - # Weight total path length of transformation weakly. - tentative_cost_from_source = cost_from_source[current_basis] + 1e-3 - - for neighbor, params, equiv in neighbors: - if neighbor in closed_set: - continue - - if tentative_cost_from_source >= cost_from_source[neighbor]: - continue - - open_set.add(neighbor) - came_from[neighbor] = (current_basis, gate_name, gate_num_qubits, params, equiv) - cost_from_source[neighbor] = tentative_cost_from_source - est_total_cost[neighbor] = tentative_cost_from_source + heuristic( - neighbor, target_basis - ) - heappush(open_heap, (est_total_cost[neighbor], next(basis_count), neighbor)) - - return None + num_gates_for_rule[rcounter] = len(sources) + graph.add_edges_from(edges) + rcounter += 1 + + # This is only neccessary since gates in target basis are currently reported by + # their names and we need to have in addition the number of qubits they act on. + target_basis_keys = [ + key + for gate in target_basis + for key in filter(lambda key, name=gate: key.name == name, all_gates_in_lib) + ] + + vis = BasisSearchVisitor(graph, source_basis, target_basis_keys, num_gates_for_rule) + # we add a dummy node and connect it with gates in the target basis. + # we'll start the search from this dummy node. + dummy = graph.add_node("dummy starting node") + graph.add_edges_from_no_data([(dummy, nodes_to_indices[key]) for key in target_basis_keys]) + rtn = None + try: + retworkx.digraph_dijkstra_search(graph, [dummy], vis.edge_cost, vis) + except StopIfBasisRewritable: + rtn = vis.basis_transforms + + logger.debug("Transformation path:") + for gate_name, gate_num_qubits, params, equiv in rtn: + logger.debug("%s/%s => %s\n%s", gate_name, gate_num_qubits, params, equiv) + + return rtn def _compose_transforms(basis_transforms, source_basis, source_dag): diff --git a/releasenotes/notes/rework-basis-translator-a83dc46cbc71c3b1.yaml b/releasenotes/notes/rework-basis-translator-a83dc46cbc71c3b1.yaml new file mode 100644 index 000000000000..c90a7e60fca7 --- /dev/null +++ b/releasenotes/notes/rework-basis-translator-a83dc46cbc71c3b1.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The basis search strategy in :class:`~.BasisTranslator` transpiler pass + has been modified into a variant of Dijkstra search which greatly improves + the runtime performance of the pass when attempting to target an unreachable + basis. diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index cb3e10d22107..3dd001d00346 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -946,12 +946,20 @@ def test_2q_with_non_global_1q(self): bt_pass = BasisTranslator(std_eqlib, target_basis=None, target=self.target) output = bt_pass(qc) - expected = QuantumCircuit(2, global_phase=pi / 2) - expected.rz(pi / 2, 1) + # We need a second run of BasisTranslator to correct gates outside of + # the target basis. This is a known isssue, see: + # https://qiskit.org/documentation/release_notes.html#release-notes-0-19-0-known-issues + output = bt_pass(output) + expected = QuantumCircuit(2) + expected.rz(pi, 1) + expected.sx(1) + expected.rz(3 * pi / 2, 1) expected.sx(1) - expected.rz(pi / 2, 1) + expected.rz(3 * pi, 1) expected.cx(0, 1) - expected.rz(pi / 2, 1) + expected.rz(pi, 1) + expected.sx(1) + expected.rz(3 * pi / 2, 1) expected.sx(1) - expected.rz(pi / 2, 1) + expected.rz(3 * pi, 1) self.assertEqual(output, expected) From bf0136cf9eaea19851f1a9f9a81f211108510258 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Wed, 23 Mar 2022 19:45:33 -0400 Subject: [PATCH 4/5] Add XXMinusYYGate (#7799) * add XXMinusYYGate * update docs and release note * move tests to existing files * update docs and release note Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../library/standard_gates/__init__.py | 2 + .../library/standard_gates/xx_minus_yy.py | 170 ++++++++++++++++++ .../add-xxminusyy-gate-63e6530c23500de9.yaml | 7 + .../circuit/test_extensions_standard.py | 78 +++++++- test/python/circuit/test_gate_definitions.py | 11 ++ 5 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 qiskit/circuit/library/standard_gates/xx_minus_yy.py create mode 100644 releasenotes/notes/add-xxminusyy-gate-63e6530c23500de9.yaml diff --git a/qiskit/circuit/library/standard_gates/__init__.py b/qiskit/circuit/library/standard_gates/__init__.py index af1a76cc33ec..c3d464d66879 100644 --- a/qiskit/circuit/library/standard_gates/__init__.py +++ b/qiskit/circuit/library/standard_gates/__init__.py @@ -49,6 +49,7 @@ RZGate RZZGate RZXGate + XXMinusYYGate XXPlusYYGate ECRGate SGate @@ -80,6 +81,7 @@ from .rz import RZGate, CRZGate from .rzz import RZZGate from .rzx import RZXGate +from .xx_minus_yy import XXMinusYYGate from .xx_plus_yy import XXPlusYYGate from .ecr import ECRGate from .s import SGate, SdgGate diff --git a/qiskit/circuit/library/standard_gates/xx_minus_yy.py b/qiskit/circuit/library/standard_gates/xx_minus_yy.py new file mode 100644 index 000000000000..a718ff59be50 --- /dev/null +++ b/qiskit/circuit/library/standard_gates/xx_minus_yy.py @@ -0,0 +1,170 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Two-qubit XX-YY gate.""" + +from typing import Optional + +import numpy as np + +from qiskit.circuit.gate import Gate +from qiskit.circuit.library.standard_gates.ry import RYGate +from qiskit.circuit.library.standard_gates.rz import RZGate +from qiskit.circuit.library.standard_gates.s import SdgGate, SGate +from qiskit.circuit.library.standard_gates.sx import SXdgGate, SXGate +from qiskit.circuit.library.standard_gates.x import CXGate +from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.qasm import pi + + +class XXMinusYYGate(Gate): + r"""XX-YY interaction gate. + + A 2-qubit parameterized XX-YY interaction. Its action is to induce + a coherent rotation by some angle between :math:`|00\rangle` and :math:`|11\rangle`. + + **Circuit Symbol:** + + .. parsed-literal:: + + ┌───────────────┐ + q_0: ┤0 ├ + │ {XX-YY}(θ,β) │ + q_1: ┤1 ├ + └───────────────┘ + + **Matrix Representation:** + + .. math:: + + \newcommand{\th}{\frac{\theta}{2}} + + R_{XX-YY}(\theta, \beta) q_0, q_1 = + RZ_1(\beta) \cdot exp(-i \frac{\theta}{2} \frac{XX-YY}{2}) \cdot RZ_1(-\beta) = + \begin{pmatrix} + \cos(\th) & 0 & 0 & -i\sin(\th)e^{-i\beta} \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + -i\sin(\th)e^{i\beta} & 0 & 0 & \cos(\th) + \end{pmatrix} + + .. note:: + + In Qiskit's convention, higher qubit indices are more significant + (little endian convention). In the above example we apply the gate + on (q_0, q_1) which results in adding the (optional) phase defined + by :math:`beta` on q_1. Instead, if we apply it on (q_1, q_0), the + phase is added on q_0. If :math:`beta` is set to its default value + of :math:`0`, the gate is equivalent in big and little endian. + + .. parsed-literal:: + + ┌───────────────┐ + q_0: ┤1 ├ + │ {XX-YY}(θ,β) │ + q_1: ┤0 ├ + └───────────────┘ + + .. math:: + + \newcommand{\th}{\frac{\theta}{2}} + + R_{XX-YY}(\theta, \beta) q_1, q_0 = + RZ_0(\beta) \cdot exp(-i \frac{\theta}{2} \frac{XX-YY}{2}) \cdot RZ_0(-\beta) = + \begin{pmatrix} + \cos(\th) & 0 & 0 & -i\sin(\th)e^{i\beta} \\ + 0 & 1 & 0 & 0 \\ + 0 & 0 & 1 & 0 \\ + -i\sin(\th)e^{-i\beta} & 0 & 0 & \cos(\th) + \end{pmatrix} + """ + + def __init__( + self, + theta: ParameterValueType, + beta: ParameterValueType = 0, + label: Optional[str] = "{XX-YY}", + ): + """Create new XX-YY gate. + + Args: + theta: The rotation angle. + beta: The phase angle. + label: The label of the gate. + """ + super().__init__("xx_minus_yy", 2, [theta, beta], label=label) + + def _define(self): + """ + gate xx_minus_yy(theta, beta) a, b { + rz(-beta) b; + rz(-pi/2) a; + sx a; + rz(pi/2) a; + s b; + cx a, b; + ry(theta/2) a; + ry(-theta/2) b; + cx a, b; + sdg b; + rz(-pi/2) a; + sxdg a; + rz(pi/2) a; + rz(beta) b; + } + """ + theta, beta = self.params + register = QuantumRegister(2, "q") + circuit = QuantumCircuit(register, name=self.name) + a, b = register + rules = [ + (RZGate(-beta), [b], []), + (RZGate(-pi / 2), [a], []), + (SXGate(), [a], []), + (RZGate(pi / 2), [a], []), + (SGate(), [b], []), + (CXGate(), [a, b], []), + (RYGate(theta / 2), [a], []), + (RYGate(-theta / 2), [b], []), + (CXGate(), [a, b], []), + (SdgGate(), [b], []), + (RZGate(-pi / 2), [a], []), + (SXdgGate(), [a], []), + (RZGate(pi / 2), [a], []), + (RZGate(beta), [b], []), + ] + for instr, qargs, cargs in rules: + circuit._append(instr, qargs, cargs) + + self.definition = circuit + + def inverse(self): + """Inverse gate.""" + theta, beta = self.params + return XXMinusYYGate(-theta, beta) + + def __array__(self, dtype=None): + """Gate matrix.""" + theta, beta = self.params + cos = np.cos(theta / 2) + sin = np.sin(theta / 2) + return np.array( + [ + [cos, 0, 0, -1j * sin * np.exp(-1j * beta)], + [0, 1, 0, 0], + [0, 0, 1, 0], + [-1j * sin * np.exp(1j * beta), 0, 0, cos], + ], + dtype=dtype, + ) diff --git a/releasenotes/notes/add-xxminusyy-gate-63e6530c23500de9.yaml b/releasenotes/notes/add-xxminusyy-gate-63e6530c23500de9.yaml new file mode 100644 index 000000000000..6516ba611606 --- /dev/null +++ b/releasenotes/notes/add-xxminusyy-gate-63e6530c23500de9.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds a gate for the XX-YY interaction, accessible via + :class:`qiskit.circuit.library.XXMinusYYGate`. This gate can be used + to implement the bSwap gate and its powers. It also arises + in the simulation of superconducting fermionic models. \ No newline at end of file diff --git a/test/python/circuit/test_extensions_standard.py b/test/python/circuit/test_extensions_standard.py index f8d37e9a9e3c..152b73e6b90e 100644 --- a/test/python/circuit/test_extensions_standard.py +++ b/test/python/circuit/test_extensions_standard.py @@ -16,13 +16,27 @@ from inspect import signature import warnings +import numpy as np +from scipy.linalg import expm +from ddt import data, ddt, unpack + from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, execute from qiskit.qasm import pi from qiskit.exceptions import QiskitError from qiskit.circuit.exceptions import CircuitError from qiskit.test import QiskitTestCase from qiskit.circuit import Gate, ControlledGate -from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, CU1Gate, CU3Gate +from qiskit.circuit.library import ( + U1Gate, + U2Gate, + U3Gate, + CU1Gate, + CU3Gate, + XXMinusYYGate, + RZGate, + XGate, + YGate, +) from qiskit import BasicAer from qiskit.quantum_info import Pauli from qiskit.quantum_info.operators.predicates import matrix_equal, is_unitary_matrix @@ -978,6 +992,7 @@ def test_z_reg_inv(self): self.assertEqual(instruction_set.instructions[2].params, []) +@ddt class TestStandard2Q(QiskitTestCase): """Standard Extension Test. Gates with two Qubits""" @@ -1325,6 +1340,67 @@ def test_swap_reg_reg_inv(self): self.assertEqual(instruction_set.qargs[1], [self.qr[1], self.qr2[1]]) self.assertEqual(instruction_set.instructions[2].params, []) + @unpack + @data( + (0, 0, np.eye(4)), + ( + np.pi / 2, + np.pi / 2, + np.array( + [ + [np.sqrt(2) / 2, 0, 0, -np.sqrt(2) / 2], + [0, 1, 0, 0], + [0, 0, 1, 0], + [np.sqrt(2) / 2, 0, 0, np.sqrt(2) / 2], + ] + ), + ), + ( + np.pi, + np.pi / 2, + np.array([[0, 0, 0, -1], [0, 1, 0, 0], [0, 0, 1, 0], [1, 0, 0, 0]]), + ), + ( + 2 * np.pi, + np.pi / 2, + np.array([[-1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]]), + ), + ( + np.pi / 2, + np.pi, + np.array( + [ + [np.sqrt(2) / 2, 0, 0, 1j * np.sqrt(2) / 2], + [0, 1, 0, 0], + [0, 0, 1, 0], + [1j * np.sqrt(2) / 2, 0, 0, np.sqrt(2) / 2], + ] + ), + ), + (4 * np.pi, 0, np.eye(4)), + ) + def test_xx_minus_yy_matrix(self, theta: float, beta: float, expected: np.ndarray): + """Test XX-YY matrix.""" + gate = XXMinusYYGate(theta, beta) + np.testing.assert_allclose(np.array(gate), expected, atol=1e-7) + + def test_xx_minus_yy_exponential_formula(self): + """Test XX-YY exponential formula.""" + theta, beta = np.random.uniform(-10, 10, size=2) + theta = np.pi / 2 + beta = 0.0 + gate = XXMinusYYGate(theta, beta) + x = np.array(XGate()) + y = np.array(YGate()) + xx = np.kron(x, x) + yy = np.kron(y, y) + rz1 = np.kron(np.array(RZGate(beta)), np.eye(2)) + np.testing.assert_allclose( + np.array(gate), + rz1 @ expm(-0.25j * theta * (xx - yy)) @ rz1.T.conj(), + atol=1e-7, + ) + class TestStandard3Q(QiskitTestCase): """Standard Extension Test. Gates with three Qubits""" diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 2a46f59a2069..cc584eacf067 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -62,6 +62,7 @@ SXdgGate, CSXGate, RVGate, + XXMinusYYGate, ) from qiskit.circuit.library.standard_gates.equivalence_library import ( @@ -173,6 +174,16 @@ def test_rv_zero(self): rv = RVGate(0, 0, 0) self.assertTrue(np.array_equal(rv.to_matrix(), np.array([[1, 0], [0, 1]]))) + def test_xx_minus_yy_definition(self): + """Test XX-YY gate decomposition.""" + theta, beta = np.random.uniform(-10, 10, size=2) + gate = XXMinusYYGate(theta, beta) + circuit = QuantumCircuit(2) + circuit.append(gate, [0, 1]) + decomposed_circuit = circuit.decompose() + self.assertTrue(len(decomposed_circuit) > len(circuit)) + self.assertTrue(Operator(circuit).equiv(Operator(decomposed_circuit), atol=1e-7)) + @ddt class TestStandardGates(QiskitTestCase): From 5c61d8f2df5a2255e1f31a53be79a45a1abe0cd1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Mar 2022 17:08:46 -0400 Subject: [PATCH 5/5] Pin jinja2 in CI (#7815) The recent jinja2 release is breaking the tutorials ci job. This is because something in the nbsphinx, jupyter, sphinx pipeline is incompatible with the new version. These issues have been reported upstream to jinja2 (and promptly closed as won't fix) so until the docs build toolchain is upgraded to work with the new version this commit pins the jinja2 version. --- constraints.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/constraints.txt b/constraints.txt index 0577f4d686a9..3cb8598cb345 100644 --- a/constraints.txt +++ b/constraints.txt @@ -8,3 +8,8 @@ jsonschema==3.2.0 # as we won't get matplotlib upgrades by default, this constraint likely can't # be removed until we can unpin matplotlib. pyparsing<3.0.0 + +# Jinja2 3.1.0 is incompatible with sphinx and/or jupyter until they are updated +# to work with the new jinja version (the jinja maintainers aren't going to +# fix things) pin to the previous working version. +jinja2==3.0.3