diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index d6d584de811e..ccaec1f4d224 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -595,6 +595,74 @@
+.. dropdown:: Working with preset :class:`~.PassManager` + :animate: fade-in-slide-down + + By default Qiskit includes functions to build preset :class:`~.PassManager` objects. + These preset passmangers are what get used by the :func:`~.transpile` function + for each optimization level. If you'd like to work directly with a + preset pass manager you can use the :func:`~.generate_preset_pass_manager` + function to easily generate one. For example: + + .. code-block:: python + + from qiskit.transpiler.preset_passmanager import generate_preset_pass_manager + from qiskit.providers.fake_provider import FakeLagosV2 + + backend = FakeLagosV2() + pass_manager = generate_preset_pass_manager(3, backend) + + which will generate a :class:`~.StagedPassManager` object for optimization level 3 + targetting the :class:`~.FakeLagosV2` backend (equivalent to what is used internally + by :func:`~.transpile` with ``backend=FakeLagosV2()`` and ``optimization_level=3``). + You can use this just like working with any other :class:`~.PassManager`. However, + because it is a :class:`~.StagedPassManager` it also makes it easy to compose and/or + replace stages of the pipeline. For example, if you wanted to run a custom scheduling + stage using dynamical decoupling (via the :class:`~.PadDynamicalDecoupling` pass) and + also add initial logical optimization prior to routing you would do something like + (building off the previous example): + + .. code-block:: python + + from qiskit.circuit.library import XGate, HGate, RXGate, PhaseGate, TGate, TdgGate + from qiskit.transpiler import PassManager + from qiskit.transpiler.passes import ALAPScheduleAnalysis, PadDynamicalDecoupling + from qiskit.transpiler.passes import CXCancellation, InverseCancellation + + backend_durations = backend.target.durations() + dd_sequence = [XGate(), XGate()] + scheduling_pm = PassManager([ + ALAPScheduleAnalysis(backend_durations), + PadDynamicalDecoupling(backend_durations, dd_sequence), + ]) + inverse_gate_list = [ + HGate(), + (RXGate(np.pi / 4), RXGate(-np.pi / 4)), + (PhaseGate(np.pi / 4), PhaseGate(-np.pi / 4)), + (TGate(), TdgGate()), + + ]) + logical_opt = PassManager([ + CXCancellation(), + InverseCancellation([HGate(), (RXGate(np.pi / 4), RXGate(-np.pi / 4)) + ]) + + + # Add pre-layout stage to run extra logical optimization + pass_manager.pre_layout = logical_opt + # Set scheduling stage to custom pass manager + pass_manager.scheduling = scheduling_pm + + + Then when :meth:`~.StagedPassManager.run` is called on ``pass_manager`` the + ``logical_opt`` :class:`~.PassManager` will be called prior to the ``layout`` stage + and for the ``scheduling`` stage our custom :class:`~.PassManager` + ``scheduling_pm`` will be used. + + .. raw:: html + +
+ Transpiler API ============== @@ -614,6 +682,7 @@ .. autosummary:: :toctree: ../stubs/ + StagedPassManager PassManager PassManagerConfig PropertySet @@ -669,6 +738,7 @@ from .runningpassmanager import FlowController, ConditionalController, DoWhileController from .passmanager import PassManager from .passmanager_config import PassManagerConfig +from .passmanager import StagedPassManager from .propertyset import PropertySet from .exceptions import TranspilerError, TranspilerAccessError from .fencedobjs import FencedDAGCircuit, FencedPropertySet diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 90c0858705bb..c9f2d540b60e 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -117,7 +117,7 @@ def run(self, dag): # If 1q runs are collected before consolidate those too runs = self.property_set["run_list"] or [] for run in runs: - if run[0] in all_block_gates: + if any(gate in all_block_gates for gate in run): continue if len(run) == 1 and not self._check_not_in_basis( run[0].name, run[0].qargs, global_index_map @@ -135,6 +135,11 @@ def run(self, dag): continue unitary = UnitaryGate(operator) dag.replace_block_with_op(run, unitary, {qubit: 0}, cycle_check=False) + # Clear collected blocks and runs as they are no longer valid after consolidation + if "run_list" in self.property_set: + del self.property_set["run_list"] + if "block_list" in self.property_set: + del self.property_set["block_list"] return dag def _check_not_in_basis(self, gate_name, qargs, global_index_map): diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index a206af3cf0ab..064579624c91 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -12,6 +12,8 @@ """Manager for a set of Passes and their scheduling during transpilation.""" +import io +import re from typing import Union, List, Callable, Dict, Any import dill @@ -319,3 +321,185 @@ def passes(self) -> List[Dict[str, BasePass]]: item["flow_controllers"] = {} ret.append(item) return ret + + +class StagedPassManager(PassManager): + """A Pass manager pipeline built up of individual stages + + This class enables building a compilation pipeline out of fixed stages. + Each ``StagedPassManager`` defines a list of stages which are executed in + a fixed order, and each stage is defined as a standalone :class:`~.PassManager` + instance. There are also ``pre_`` and ``post_`` stages for each defined stage. + This enables easily composing and replacing different stages and also adding + hook points to enable programmtic modifications to a pipeline. When using a staged + pass manager you are not able to modify the individual passes and are only able + to modify stages. + + By default instances of StagedPassManager define a typical full compilation + pipeline from an abstract virtual circuit to one that is optimized and + capable of running on the specified backend. The default pre-defined stages are: + + #. ``init`` - any initial passes that are run before we start embedding the circuit to the backend + #. ``layout`` - This stage runs layout and maps the virtual qubits in the + circuit to the physical qubits on a backend + #. ``routing`` - This stage runs after a layout has been run and will insert any + necessary gates to move the qubit states around until it can be run on + backend's compuling map. + #. ``translation`` - Perform the basis gate translation, in other words translate the gates + in the circuit to the target backend's basis set + #. ``optimization`` - The main optimization loop, this will typically run in a loop trying to + optimize the circuit until a condtion (such as fixed depth) is reached. + #. ``scheduling`` - Any hardware aware scheduling passes + + .. note:: + + For backwards compatibility the relative positioning of these default + stages will remain stable moving forward. However, new stages may be + added to the default stage list in between current stages. For example, + in a future release a new phase, something like ``logical_optimization``, could be added + immediately after the existing ``init`` stage in the default stage list. + This would preserve compatibility for pre-existing ``StagedPassManager`` + users as the relative positions of the stage are preserved so the behavior + will not change between releases. + + These stages will be executed in order and any stage set to ``None`` will be skipped. If + a :class:`~qiskit.transpiler.PassManager` input is being used for more than 1 stage here + (for example in the case of a :class:`~.Pass` that covers both Layout and Routing) you will want + to set that to the earliest stage in sequence that it covers. + """ + + invalid_stage_regex = re.compile( + r"\s|\+|\-|\*|\/|\\|\%|\<|\>|\@|\!|\~|\^|\&|\:|\[|\]|\{|\}|\(|\)" + ) + + def __init__(self, stages=None, **kwargs): + """Initialize a new StagedPassManager object + + Args: + stages (List[str]): An optional list of stages to use for this + instance. If this is not specified the default stages list + ``['init', 'layout', 'routing', 'translation', 'optimization', 'scheduling']`` is + used + kwargs: The initial :class:`~.PassManager` values for any stages + defined in ``stages``. If a argument is not defined the + stages will default to ``None`` indicating an empty/undefined + stage. + + Raises: + AttributeError: If a stage in the input keyword arguments is not defined. + ValueError: If an invalid stage name is specified. + """ + if stages is None: + self.stages = [ + "init", + "layout", + "routing", + "translation", + "optimization", + "scheduling", + ] + else: + invalid_stages = [ + stage for stage in stages if self.invalid_stage_regex.search(stage) is not None + ] + if invalid_stages: + with io.StringIO() as msg: + msg.write(f"The following stage names are not valid: {invalid_stages[0]}") + for invalid_stage in invalid_stages[1:]: + msg.write(f", {invalid_stage}") + raise ValueError(msg.getvalue()) + + self.stages = stages + super().__init__() + for stage in self.stages: + pre_stage = "pre_" + stage + post_stage = "post_" + stage + setattr(self, pre_stage, None) + setattr(self, stage, None) + setattr(self, post_stage, None) + for stage, pm in kwargs.items(): + if ( + stage not in self.stages + and not (stage.startswith("pre_") and stage[4:] in self.stages) + and not (stage.startswith("post_") and stage[5:] in self.stages) + ): + raise AttributeError(f"{stage} is not a valid stage.") + setattr(self, stage, pm) + self._update_passmanager() + + def _update_passmanager(self): + self._pass_sets = [] + for stage in self.stages: + # Add pre-stage PM + pre_stage_pm = getattr(self, "pre_" + stage, None) + if pre_stage_pm is not None: + self._pass_sets.extend(pre_stage_pm._pass_sets) + # Add stage PM + stage_pm = getattr(self, stage, None) + if stage_pm is not None: + self._pass_sets.extend(stage_pm._pass_sets) + # Add post-stage PM + post_stage_pm = getattr(self, "post_" + stage, None) + if post_stage_pm is not None: + self._pass_sets.extend(post_stage_pm._pass_sets) + + def __setattr__(self, attr, value): + super().__setattr__(attr, value) + if ( + attr in self.stages + or (attr.startswith("pre_") and attr[4:] not in self.stages) + or (attr.startswith("post_") and attr[5:] not in self.stages) + ): + self._update_passmanager() + + def append( + self, + passes: Union[BasePass, List[BasePass]], + max_iteration: int = None, + **flow_controller_conditions: Any, + ) -> None: + raise NotImplementedError + + def replace( + self, + index: int, + passes: Union[BasePass, List[BasePass]], + max_iteration: int = None, + **flow_controller_conditions: Any, + ) -> None: + raise NotImplementedError + + # Raise NotImplemntedError on individual pass manipulation + def remove(self, index: int) -> None: + raise NotImplementedError + + def __getitem(self, index): + self._update_passmanager() + return super().__getitem__(index) + + def __len__(self): + self._update_passmanager() + return super().__len__() + + def __setitem__(self, index, item): + raise NotImplementedError + + def __add__(self, other): + raise NotImplementedError + + def _create_running_passmanager(self) -> RunningPassManager: + self._update_passmanager() + return super()._create_running_passmanager() + + def passes(self) -> List[Dict[str, BasePass]]: + self._update_passmanager() + return super().passes() + + def run( + self, + circuits: Union[QuantumCircuit, List[QuantumCircuit]], + output_name: str = None, + callback: Callable = None, + ) -> Union[QuantumCircuit, List[QuantumCircuit]]: + self._update_passmanager() + return super().run(circuits, output_name, callback) diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index 45ddada26c71..eb2f0d2aed39 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -136,7 +136,7 @@ def generate_preset_pass_manager( to use this option. Returns: - PassManager: The preset pass manager for the given options + StagedPassManager: The preset pass manager for the given options Raises: ValueError: if an invalid value for ``optimization_level`` is passed in. diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py new file mode 100644 index 000000000000..ce01dce6a26e --- /dev/null +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -0,0 +1,388 @@ +# 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. + +# pylint: disable=invalid-name + +"""Common preset passmanager generators.""" + +from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel + +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.passes import Unroller +from qiskit.transpiler.passes import BasisTranslator +from qiskit.transpiler.passes import UnrollCustomDefinitions +from qiskit.transpiler.passes import Unroll3qOrMore +from qiskit.transpiler.passes import Collect2qBlocks +from qiskit.transpiler.passes import Collect1qRuns +from qiskit.transpiler.passes import ConsolidateBlocks +from qiskit.transpiler.passes import UnitarySynthesis +from qiskit.transpiler.passes import CheckMap +from qiskit.transpiler.passes import GateDirection +from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements +from qiskit.transpiler.passes import CheckGateDirection +from qiskit.transpiler.passes import TimeUnitConversion +from qiskit.transpiler.passes import ALAPScheduleAnalysis +from qiskit.transpiler.passes import ASAPScheduleAnalysis +from qiskit.transpiler.passes import FullAncillaAllocation +from qiskit.transpiler.passes import EnlargeWithAncilla +from qiskit.transpiler.passes import ApplyLayout +from qiskit.transpiler.passes import RemoveResetInZeroState +from qiskit.transpiler.passes import ValidatePulseGates +from qiskit.transpiler.passes import PadDelay +from qiskit.transpiler.passes import InstructionDurationCheck +from qiskit.transpiler.passes import ConstrainedReschedule +from qiskit.transpiler.passes import PulseGates +from qiskit.transpiler.passes import ContainsInstruction +from qiskit.transpiler.passes import VF2PostLayout +from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason +from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason +from qiskit.transpiler.exceptions import TranspilerError + + +def generate_unroll_3q( + target, + basis_gates=None, + approximation_degree=None, + unitary_synthesis_method="default", + unitary_synthesis_plugin_config=None, +): + """Generate an unroll >3q :class:`~qiskit.transpiler.PassManager` + + Args: + target (Target): the :class:`~.Target` object representing the backend + basis_gates (list): A list of str gate names that represent the basis + gates on the backend target + approximation_degree (float): The heuristic approximation degree to + use. Can be between 0 and 1. + unitary_synthesis_method (str): The unitary synthesis method to use + unitary_synthesis_plugin_config (dict): The optional dictionary plugin + configuration, this is plugin specific refer to the specified plugin's + documenation for how to use. + + Returns: + PassManager: The unroll 3q or more pass manager + """ + unroll_3q = PassManager() + unroll_3q.append( + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + method=unitary_synthesis_method, + min_qubits=3, + plugin_config=unitary_synthesis_plugin_config, + target=target, + ) + ) + unroll_3q.append(Unroll3qOrMore(target=target, basis_gates=basis_gates)) + return unroll_3q + + +def generate_embed_passmanager(coupling_map): + """Generate a layout embedding :class:`~qiskit.transpiler.PassManager` + + This is used to generate a :class:`~qiskit.transpiler.PassManager` object + that can be used to expand and apply an initial layout to a circuit + + Args: + coupling_map (CouplingMap): The coupling map for the backend to embed + the circuit to. + Returns: + PassManager: The embedding passmanager that assumes the layout property + set has been set in earlier stages + """ + return PassManager([FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()]) + + +def _trivial_not_perfect(property_set): + # Verify that a trivial layout is perfect. If trivial_layout_score > 0 + # the layout is not perfect. The layout is unconditionally set by trivial + # layout so we need to clear it before contuing. + return ( + property_set["trivial_layout_score"] is not None + and property_set["trivial_layout_score"] != 0 + ) + + +def _apply_post_layout_condition(property_set): + # if VF2 Post layout found a solution we need to re-apply the better + # layout. Otherwise we can skip apply layout. + return ( + property_set["VF2PostLayout_stop_reason"] is not None + and property_set["VF2PostLayout_stop_reason"] is VF2PostLayoutStopReason.SOLUTION_FOUND + ) + + +def generate_routing_passmanager( + routing_pass, + target, + coupling_map=None, + vf2_call_limit=None, + backend_properties=None, + seed_transpiler=None, + check_trivial=False, + use_barrier_before_measurement=True, +): + """Generate a routing :class:`~qiskit.transpiler.PassManager` + + Args: + routing_pass (TransformationPass): The pass which will perform the + routing + target (Target): the :class:`~.Target` object representing the backend + coupling_map (CouplingMap): The coupling map of the backend to route + for + vf2_call_limit (int): The internal call limit for the vf2 post layout + pass. If this is ``None`` the vf2 post layout will not be run. + backend_properties (BackendProperties): Properties of a backend to + synthesize for (e.g. gate fidelities). + seed_transpiler (int): Sets random seed for the stochastic parts of + the transpiler. + check_trivial (bool): If set to true this will condition running the + :class:`~.VF2PostLayout` pass after routing on whether a trivial + layout was tried and was found to not be perfect. This is only + needed if the constructed pass manager runs :class:`~.TrivialLayout` + as a first layout attempt and uses it if it's a perfect layout + (as is the case with preset pass manager level 1). + use_barrier_before_measurement (bool): If true (the default) the + :class:`~.BarrierBeforeFinalMeasurements` transpiler pass will be run prior to the + specified pass in the ``routing_pass`` argument. + Returns: + PassManager: The routing pass manager + """ + + def _run_post_layout_condition(property_set): + # If we check trivial layout and the found trivial layout was not perfect also + # ensure VF2 initial layout was not used before running vf2 post layout + if not check_trivial or _trivial_not_perfect(property_set): + vf2_stop_reason = property_set["VF2Layout_stop_reason"] + if vf2_stop_reason is None or vf2_stop_reason != VF2LayoutStopReason.SOLUTION_FOUND: + return True + return False + + routing = PassManager() + routing.append(CheckMap(coupling_map)) + + def _swap_condition(property_set): + return not property_set["is_swap_mapped"] + + if use_barrier_before_measurement: + routing.append([BarrierBeforeFinalMeasurements(), routing_pass], condition=_swap_condition) + else: + routing.append([routing_pass], condition=_swap_condition) + + if (target is not None or backend_properties is not None) and vf2_call_limit is not None: + routing.append( + VF2PostLayout( + target, + coupling_map, + backend_properties, + seed_transpiler, + call_limit=vf2_call_limit, + strict_direction=False, + ), + condition=_run_post_layout_condition, + ) + routing.append(ApplyLayout(), condition=_apply_post_layout_condition) + + return routing + + +def generate_pre_op_passmanager(target=None, coupling_map=None, remove_reset_in_zero=False): + """Generate a pre-optimization loop :class:`~qiskit.transpiler.PassManager` + + This pass manager will check to ensure that directionality from the coupling + map is respected + + Args: + target (Target): the :class:`~.Target` object representing the backend + coupling_map (CouplingMap): The coupling map to use + remove_reset_in_zero (bool): If ``True`` include the remove reset in + zero pass in the generated PassManager + Returns: + PassManager: The pass manager + + """ + pre_opt = PassManager() + if coupling_map: + pre_opt.append(CheckGateDirection(coupling_map, target=target)) + + def _direction_condition(property_set): + return not property_set["is_direction_mapped"] + + pre_opt.append([GateDirection(coupling_map, target=target)], condition=_direction_condition) + if remove_reset_in_zero: + pre_opt.append(RemoveResetInZeroState()) + return pre_opt + + +def generate_translation_passmanager( + target, + basis_gates=None, + method="translator", + approximation_degree=None, + coupling_map=None, + backend_props=None, + unitary_synthesis_method="default", + unitary_synthesis_plugin_config=None, +): + """Generate a basis translation :class:`~qiskit.transpiler.PassManager` + + Args: + target (Target): the :class:`~.Target` object representing the backend + basis_gates (list): A list of str gate names that represent the basis + gates on the backend target + method (str): The basis translation method to use + approximation_degree (float): The heuristic approximation degree to + use. Can be between 0 and 1. + coupling_map (CouplingMap): the coupling map of the backend + in case synthesis is done on a physical circuit. The + directionality of the coupling_map will be taken into + account if pulse_optimize is True/None and natural_direction + is True/None. + unitary_synthesis_plugin_config (dict): The optional dictionary plugin + configuration, this is plugin specific refer to the specified plugin's + documenation for how to use. + backend_props (BackendProperties): Properties of a backend to + synthesize for (e.g. gate fidelities). + unitary_synthesis_method (str): The unitary synthesis method to use + + Returns: + PassManager: The basis translation pass manager + + Raises: + TranspilerError: If the ``method`` kwarg is not a valid value + """ + if method == "unroller": + unroll = [Unroller(basis_gates)] + elif method == "translator": + unroll = [ + # Use unitary synthesis for basis aware decomposition of + # UnitaryGates before custom unrolling + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_props, + plugin_config=unitary_synthesis_plugin_config, + method=unitary_synthesis_method, + target=target, + ), + UnrollCustomDefinitions(sel, basis_gates), + BasisTranslator(sel, basis_gates, target), + ] + elif method == "synthesis": + unroll = [ + # # Use unitary synthesis for basis aware decomposition of + # UnitaryGates > 2q before collection + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_props, + plugin_config=unitary_synthesis_plugin_config, + method=unitary_synthesis_method, + min_qubits=3, + target=target, + ), + Unroll3qOrMore(target=target, basis_gates=basis_gates), + Collect2qBlocks(), + Collect1qRuns(), + ConsolidateBlocks(basis_gates=basis_gates, target=target), + UnitarySynthesis( + basis_gates=basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_props, + plugin_config=unitary_synthesis_plugin_config, + method=unitary_synthesis_method, + target=target, + ), + ] + else: + raise TranspilerError("Invalid translation method %s." % method) + return PassManager(unroll) + + +def generate_scheduling(instruction_durations, scheduling_method, timing_constraints, inst_map): + """Generate a post optimization scheduling :class:`~qiskit.transpiler.PassManager` + + Args: + instruction_durations (dict): The dictionary of instruction durations + scheduling_method (str): The scheduling method to use, can either be + ``'asap'``/``'as_soon_as_possible'`` or + ``'alap'``/``'as_late_as_possible'`` + timing_constraints (TimingConstraints): Hardware time alignment restrictions. + inst_map (InstructionScheduleMap): Mapping object that maps gate to schedule. + + Returns: + PassManager: The scheduling pass manager + + Raises: + TranspilerError: If the ``scheduling_method`` kwarg is not a valid value + """ + scheduling = PassManager() + if inst_map and inst_map.has_custom_gate(): + scheduling.append(PulseGates(inst_map=inst_map)) + if scheduling_method: + # Do scheduling after unit conversion. + scheduler = { + "alap": ALAPScheduleAnalysis, + "as_late_as_possible": ALAPScheduleAnalysis, + "asap": ASAPScheduleAnalysis, + "as_soon_as_possible": ASAPScheduleAnalysis, + } + scheduling.append(TimeUnitConversion(instruction_durations)) + try: + scheduling.append(scheduler[scheduling_method](instruction_durations)) + except KeyError as ex: + raise TranspilerError("Invalid scheduling method %s." % scheduling_method) from ex + elif instruction_durations: + # No scheduling. But do unit conversion for delays. + def _contains_delay(property_set): + return property_set["contains_delay"] + + scheduling.append(ContainsInstruction("delay")) + scheduling.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"] + + scheduling.append( + InstructionDurationCheck( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ) + ) + scheduling.append( + ConstrainedReschedule( + acquire_alignment=timing_constraints.acquire_alignment, + pulse_alignment=timing_constraints.pulse_alignment, + ), + condition=_require_alignment, + ) + scheduling.append( + ValidatePulseGates( + granularity=timing_constraints.granularity, + min_length=timing_constraints.min_length, + ) + ) + if scheduling_method: + # Call padding pass if circuit is scheduled + scheduling.append(PadDelay()) + + return scheduling diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 3c881cc1b306..37aadc1c4452 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -18,47 +18,24 @@ from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.passmanager import StagedPassManager -from qiskit.transpiler.passes import Unroller -from qiskit.transpiler.passes import BasisTranslator -from qiskit.transpiler.passes import UnrollCustomDefinitions -from qiskit.transpiler.passes import Unroll3qOrMore -from qiskit.transpiler.passes import CheckMap -from qiskit.transpiler.passes import GateDirection from qiskit.transpiler.passes import SetLayout from qiskit.transpiler.passes import TrivialLayout from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements from qiskit.transpiler.passes import BasicSwap from qiskit.transpiler.passes import LookaheadSwap from qiskit.transpiler.passes import StochasticSwap from qiskit.transpiler.passes import SabreSwap -from qiskit.transpiler.passes import FullAncillaAllocation -from qiskit.transpiler.passes import EnlargeWithAncilla -from qiskit.transpiler.passes import ApplyLayout -from qiskit.transpiler.passes import CheckGateDirection -from qiskit.transpiler.passes import Collect2qBlocks -from qiskit.transpiler.passes import Collect1qRuns -from qiskit.transpiler.passes import ConsolidateBlocks -from qiskit.transpiler.passes import UnitarySynthesis -from qiskit.transpiler.passes import TimeUnitConversion -from qiskit.transpiler.passes import ALAPScheduleAnalysis -from qiskit.transpiler.passes import ASAPScheduleAnalysis -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 from qiskit.transpiler.passes import Error -from qiskit.transpiler.passes import ContainsInstruction - +from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM -def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: +def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassManager: """Level 0 pass manager: no explicit optimization other than mapping to backend. This pass manager applies the user-given initial layout. If none is given, a trivial @@ -68,10 +45,6 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: The pass manager then unrolls the circuit to the desired basis, and transforms the circuit to match the coupling map. - Note: - In simulators where ``coupling_map=None``, only the unrolling and - optimization stages are done. - Args: pass_manager_config: configuration of the pass manager. @@ -98,21 +71,7 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config target = pass_manager_config.target - # 1. Decompose so only 1-qubit and 2-qubit gates remain - _unroll3q = [ - # Use unitary synthesis for basis aware decomposition of UnitaryGates - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - method=unitary_synthesis_method, - min_qubits=3, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - Unroll3qOrMore(target=target, basis_gates=basis_gates), - ] - - # 2. Choose an initial layout if not set by user (default: trivial layout) + # Choose an initial layout if not set by user (default: trivial layout) _given_layout = SetLayout(initial_layout) def _choose_layout_condition(property_set): @@ -129,27 +88,16 @@ def _choose_layout_condition(property_set): else: raise TranspilerError("Invalid layout method %s." % layout_method) - # 3. Extend dag/layout with ancillas using the full coupling map - _embed = [FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()] - - # 4. Swap to fit the coupling map - _swap_check = CheckMap(coupling_map) - - def _swap_condition(property_set): - return not property_set["is_swap_mapped"] - - def _swap_needs_basis(property_set): - return _swap_condition(property_set) and routing_method == "toqm" - - _swap = [BarrierBeforeFinalMeasurements()] + toqm_pass = False + # Choose routing pass if routing_method == "basic": - _swap += [BasicSwap(coupling_map)] + routing_pass = BasicSwap(coupling_map) elif routing_method == "stochastic": - _swap += [StochasticSwap(coupling_map, trials=20, seed=seed_transpiler)] + routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) elif routing_method == "lookahead": - _swap += [LookaheadSwap(coupling_map, search_depth=2, search_width=2)] + routing_pass = LookaheadSwap(coupling_map, search_depth=2, search_width=2) elif routing_method == "sabre": - _swap += [SabreSwap(coupling_map, heuristic="basic", seed=seed_transpiler)] + routing_pass = SabreSwap(coupling_map, heuristic="basic", seed=seed_transpiler) elif routing_method == "toqm": HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO0, latencies_from_target @@ -157,162 +105,81 @@ def _swap_needs_basis(property_set): if initial_layout: raise TranspilerError("Initial layouts are not supported with TOQM-based routing.") + toqm_pass = True # Note: BarrierBeforeFinalMeasurements is skipped intentionally since ToqmSwap # does not yet support barriers. - _swap = [ - ToqmSwap( - coupling_map, - strategy=ToqmStrategyO0( - latencies_from_target( - coupling_map, instruction_durations, basis_gates, backend_properties, target - ) - ), - ) - ] + routing_pass = ToqmSwap( + coupling_map, + strategy=ToqmStrategyO0( + latencies_from_target( + coupling_map, instruction_durations, basis_gates, backend_properties, target + ) + ), + ) elif routing_method == "none": - _swap += [ - Error( - msg=( - "No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}" - ), - action="raise", - ) - ] + routing_pass = Error( + msg="No routing method selected, but circuit is not routed to device. " + "CheckMap Error: {check_map_msg}", + action="raise", + ) else: raise TranspilerError("Invalid routing method %s." % routing_method) - # 5. Unroll to the basis - if translation_method == "unroller": - _unroll = [Unroller(basis_gates)] - elif translation_method == "translator": - from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel - - _unroll = [ - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - backend_props=backend_properties, - method=unitary_synthesis_method, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - UnrollCustomDefinitions(sel, basis_gates), - BasisTranslator(sel, basis_gates, target), - ] - elif translation_method == "synthesis": - _unroll = [ - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - backend_props=backend_properties, - method=unitary_synthesis_method, - min_qubits=3, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - Unroll3qOrMore(target=target, basis_gates=basis_gates), - Collect2qBlocks(), - Collect1qRuns(), - ConsolidateBlocks(basis_gates=basis_gates, target=target), - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - backend_props=backend_properties, - method=unitary_synthesis_method, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - ] - else: - raise TranspilerError("Invalid translation method %s." % translation_method) - - # 6. Fix any bad CX directions - _direction_check = [CheckGateDirection(coupling_map, target)] - - def _direction_condition(property_set): - return not property_set["is_direction_mapped"] - - _direction = [GateDirection(coupling_map, target)] - + unroll_3q = None # Build pass manager - pm0 = PassManager() if coupling_map or initial_layout: - pm0.append(_given_layout) - pm0.append(_unroll3q) - pm0.append(_choose_layout, condition=_choose_layout_condition) - pm0.append(_embed) - pm0.append(_swap_check) - pm0.append(_unroll, condition=_swap_needs_basis) - pm0.append(_swap, condition=_swap_condition) - pm0.append(_unroll) - if (coupling_map and not coupling_map.is_symmetric) or ( - target is not None and target.get_non_global_operation_names(strict_direction=True) - ): - pm0.append(_direction_check) - pm0.append(_direction, condition=_direction_condition) - 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: - # Do scheduling after unit conversion. - scheduler = { - "alap": ALAPScheduleAnalysis, - "as_late_as_possible": ALAPScheduleAnalysis, - "asap": ASAPScheduleAnalysis, - "as_soon_as_possible": ASAPScheduleAnalysis, - } - 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: - # 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, + unroll_3q = common.generate_unroll_3q( + target, + basis_gates, + approximation_degree, + unitary_synthesis_method, + unitary_synthesis_plugin_config, ) - pm0.append( - ValidatePulseGates( - granularity=timing_constraints.granularity, - min_length=timing_constraints.min_length, - ) + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout, condition=_choose_layout_condition) + layout += common.generate_embed_passmanager(coupling_map) + routing = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=not toqm_pass, ) - if scheduling_method: - # Call padding pass if circuit is scheduled - pm0.append(PadDelay()) + else: + layout = None + routing = None + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) + pre_routing = None + if toqm_pass: + pre_routing = translation - return pm0 + if (coupling_map and not coupling_map.is_symmetric) or ( + target is not None and target.get_non_global_operation_names(strict_direction=True) + ): + pre_opt = common.generate_pre_op_passmanager(target, coupling_map) + pre_opt += translation + else: + pre_opt = None + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) + + return StagedPassManager( + init=unroll_3q, + layout=layout, + pre_routing=pre_routing, + routing=routing, + translation=translation, + pre_optimization=pre_opt, + scheduling=sched, + ) diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index e24b40f7d875..5da52fb23052 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -18,57 +18,33 @@ from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.passmanager import StagedPassManager -from qiskit.transpiler.passes import Unroller -from qiskit.transpiler.passes import BasisTranslator -from qiskit.transpiler.passes import UnrollCustomDefinitions -from qiskit.transpiler.passes import Unroll3qOrMore from qiskit.transpiler.passes import CXCancellation -from qiskit.transpiler.passes import CheckMap -from qiskit.transpiler.passes import GateDirection from qiskit.transpiler.passes import SetLayout from qiskit.transpiler.passes import VF2Layout -from qiskit.transpiler.passes import VF2PostLayout from qiskit.transpiler.passes import TrivialLayout from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements -from qiskit.transpiler.passes import Layout2qDistance from qiskit.transpiler.passes import BasicSwap from qiskit.transpiler.passes import LookaheadSwap from qiskit.transpiler.passes import StochasticSwap from qiskit.transpiler.passes import SabreSwap -from qiskit.transpiler.passes import FullAncillaAllocation -from qiskit.transpiler.passes import EnlargeWithAncilla from qiskit.transpiler.passes import FixedPoint from qiskit.transpiler.passes import Depth from qiskit.transpiler.passes import Size -from qiskit.transpiler.passes import RemoveResetInZeroState from qiskit.transpiler.passes import Optimize1qGatesDecomposition -from qiskit.transpiler.passes import ApplyLayout -from qiskit.transpiler.passes import CheckGateDirection -from qiskit.transpiler.passes import Collect2qBlocks -from qiskit.transpiler.passes import ConsolidateBlocks -from qiskit.transpiler.passes import UnitarySynthesis -from qiskit.transpiler.passes import TimeUnitConversion -from qiskit.transpiler.passes import ALAPScheduleAnalysis -from qiskit.transpiler.passes import ASAPScheduleAnalysis -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 +from qiskit.transpiler.passes import Layout2qDistance from qiskit.transpiler.passes import Error -from qiskit.transpiler.passes import ContainsInstruction +from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason -from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM -def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: +def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassManager: """Level 1 pass manager: light optimization by simple adjacent gate collapsing. This pass manager applies the user-given initial layout. If none is given, @@ -80,10 +56,6 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: circuit to match the coupling map. Finally, optimizations in the form of adjacent gate collapse and redundant reset removal are performed. - Note: - In simulators where ``coupling_map=None``, only the unrolling and - optimization stages are done. - Args: pass_manager_config: configuration of the pass manager. @@ -110,7 +82,7 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: timing_constraints = pass_manager_config.timing_constraints or TimingConstraints() target = pass_manager_config.target - # 1. Use trivial layout if no layout given if that isn't perfect use vf2 layout + # Use trivial layout if no layout given _given_layout = SetLayout(initial_layout) def _choose_layout_condition(property_set): @@ -127,6 +99,7 @@ def _trivial_not_perfect(property_set): return True return False + # Use a better layout on densely connected qubits, if circuit needs swaps def _vf2_match_not_found(property_set): # If a layout hasn't been set by the time we run vf2 layout we need to # run layout @@ -162,21 +135,6 @@ def _vf2_match_not_found(property_set): ) ) - # 2. Decompose so only 1-qubit and 2-qubit gates remain - _unroll3q = [ - # Use unitary synthesis for basis aware decomposition of UnitaryGates - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - method=unitary_synthesis_method, - min_qubits=3, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - Unroll3qOrMore(target=target, basis_gates=basis_gates), - ] - - # 3. Use a better layout on densely connected qubits, if circuit needs swaps if layout_method == "trivial": _improve_layout = TrivialLayout(coupling_map) elif layout_method == "dense": @@ -188,27 +146,15 @@ def _vf2_match_not_found(property_set): else: raise TranspilerError("Invalid layout method %s." % layout_method) - # 4. Extend dag/layout with ancillas using the full coupling map - _embed = [FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()] - - # 5. Swap to fit the coupling map - _swap_check = CheckMap(coupling_map) - - def _swap_condition(property_set): - return not property_set["is_swap_mapped"] - - def _swap_needs_basis(property_set): - return _swap_condition(property_set) and routing_method == "toqm" - - _swap = [BarrierBeforeFinalMeasurements()] + toqm_pass = False if routing_method == "basic": - _swap += [BasicSwap(coupling_map)] + routing_pass = BasicSwap(coupling_map) elif routing_method == "stochastic": - _swap += [StochasticSwap(coupling_map, trials=20, seed=seed_transpiler)] + routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) elif routing_method == "lookahead": - _swap += [LookaheadSwap(coupling_map, search_depth=4, search_width=4)] + routing_pass = LookaheadSwap(coupling_map, search_depth=4, search_width=4) elif routing_method == "sabre": - _swap += [SabreSwap(coupling_map, heuristic="lookahead", seed=seed_transpiler)] + routing_pass = SabreSwap(coupling_map, heuristic="lookahead", seed=seed_transpiler) elif routing_method == "toqm": HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO1, latencies_from_target @@ -216,94 +162,28 @@ def _swap_needs_basis(property_set): if initial_layout: raise TranspilerError("Initial layouts are not supported with TOQM-based routing.") + toqm_pass = True # Note: BarrierBeforeFinalMeasurements is skipped intentionally since ToqmSwap # does not yet support barriers. - _swap = [ - ToqmSwap( - coupling_map, - strategy=ToqmStrategyO1( - latencies_from_target( - coupling_map, instruction_durations, basis_gates, backend_properties, target - ) - ), - ) - ] + routing_pass = ToqmSwap( + coupling_map, + strategy=ToqmStrategyO1( + latencies_from_target( + coupling_map, instruction_durations, basis_gates, backend_properties, target + ) + ), + ) elif routing_method == "none": - _swap += [ - Error( - msg=( - "No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}" - ), - action="raise", - ) - ] + routing_pass = Error( + msg="No routing method selected, but circuit is not routed to device. " + "CheckMap Error: {check_map_msg}", + action="raise", + ) else: raise TranspilerError("Invalid routing method %s." % routing_method) - # 6. Unroll to the basis - if translation_method == "unroller": - _unroll = [Unroller(basis_gates)] - elif translation_method == "translator": - from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel - - _unroll = [ - # Use unitary synthesis for basis aware decomposition of UnitaryGates before - # custom unrolling - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - method=unitary_synthesis_method, - backend_props=backend_properties, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - UnrollCustomDefinitions(sel, basis_gates), - BasisTranslator(sel, basis_gates, target), - ] - elif translation_method == "synthesis": - _unroll = [ - # Use unitary synthesis for basis aware decomposition of UnitaryGates before - # collection - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - method=unitary_synthesis_method, - backend_props=backend_properties, - min_qubits=3, - target=target, - ), - Unroll3qOrMore(target=target, basis_gates=basis_gates), - Collect2qBlocks(), - ConsolidateBlocks(basis_gates=basis_gates, target=target), - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - method=unitary_synthesis_method, - backend_props=backend_properties, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - ] - else: - raise TranspilerError("Invalid translation method %s." % translation_method) - - # 7. Fix any bad CX directions - _direction_check = [CheckGateDirection(coupling_map, target)] - - def _direction_condition(property_set): - return not property_set["is_direction_mapped"] - - _direction = [GateDirection(coupling_map, target)] - - # 8. Remove zero-state reset - _reset = RemoveResetInZeroState() - - # 9. Merge 1q rotations and cancel CNOT gates iteratively until no more change in depth - # or size of circuit + # Build optimization loop: merge 1q rotations and cancel CNOT gates iteratively + # until no more change in depth _depth_check = [Depth(), FixedPoint("depth")] _size_check = [Size(), FixedPoint("size")] @@ -312,125 +192,74 @@ def _opt_control(property_set): _opt = [Optimize1qGatesDecomposition(basis_gates), CXCancellation()] - # Build pass manager - pm1 = PassManager() + unroll_3q = None + # Build full pass manager if coupling_map or initial_layout: - pm1.append(_given_layout) - pm1.append(_unroll3q) - pm1.append(_choose_layout_0, condition=_choose_layout_condition) - pm1.append(_choose_layout_1, condition=_trivial_not_perfect) - pm1.append(_improve_layout, condition=_vf2_match_not_found) - pm1.append(_embed) - pm1.append(_swap_check) - pm1.append(_unroll, condition=_swap_needs_basis) - pm1.append(_swap, condition=_swap_condition) - if ( - (coupling_map and backend_properties) - and initial_layout is None - and pass_manager_config.layout_method is None - ): - - def _run_post_layout_condition(property_set): - if _trivial_not_perfect(property_set): - vf2_stop_reason = property_set["VF2Layout_stop_reason"] - if ( - vf2_stop_reason is None - or vf2_stop_reason != VF2LayoutStopReason.SOLUTION_FOUND - ): - return True - return False - - def _apply_post_layout_condition(property_set): - # if VF2 Post layout found a solution we need to re-apply the better - # layout. Otherwise we can skip apply layout. - if ( - property_set["VF2PostLayout_stop_reason"] is not None - and property_set["VF2PostLayout_stop_reason"] - is VF2PostLayoutStopReason.SOLUTION_FOUND - ): - return True - return False + unroll_3q = common.generate_unroll_3q( + target, + basis_gates, + approximation_degree, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout_0, condition=_choose_layout_condition) + layout.append(_choose_layout_1, condition=_trivial_not_perfect) + layout.append(_improve_layout, condition=_vf2_match_not_found) + layout += common.generate_embed_passmanager(coupling_map) + vf2_call_limit = None + if pass_manager_config.layout_method is None and pass_manager_config.initial_layout is None: + vf2_call_limit = int(5e4) # Set call limit to ~100ms with retworkx 0.10.2 + routing = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + check_trivial=True, + use_barrier_before_measurement=not toqm_pass, + ) + else: + layout = None + routing = None + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) + pre_routing = None + if toqm_pass: + pre_routing = translation - pm1.append( - VF2PostLayout( - target, - coupling_map, - backend_properties, - seed_transpiler, - call_limit=int(5e4), # Set call limit to ~100ms with retworkx 0.10.2 - strict_direction=False, - ), - condition=_run_post_layout_condition, - ) - pm1.append(ApplyLayout(), condition=_apply_post_layout_condition) - pm1.append(_unroll) if (coupling_map and not coupling_map.is_symmetric) or ( target is not None and target.get_non_global_operation_names(strict_direction=True) ): - pm1.append(_direction_check) - pm1.append(_direction, condition=_direction_condition) - pm1.append(_reset) - pm1.append(_depth_check + _size_check) - 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: - # Do scheduling after unit conversion. - scheduler = { - "alap": ALAPScheduleAnalysis, - "as_late_as_possible": ALAPScheduleAnalysis, - "asap": ASAPScheduleAnalysis, - "as_soon_as_possible": ASAPScheduleAnalysis, - } - 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: - # 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()) + pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) + else: + pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) + optimization = PassManager() + unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] + optimization.append(_depth_check + _size_check) + opt_loop = _opt + unroll + _depth_check + _size_check + optimization.append(opt_loop, do_while=_opt_control) + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) - return pm1 + return StagedPassManager( + init=unroll_3q, + layout=layout, + pre_routing=pre_routing, + routing=routing, + translation=translation, + pre_optimization=pre_optimization, + optimization=optimization, + scheduling=sched, + ) diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 5c0705745584..c2ee13ad8500 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -19,56 +19,32 @@ from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.passmanager import StagedPassManager -from qiskit.transpiler.passes import Unroller -from qiskit.transpiler.passes import BasisTranslator -from qiskit.transpiler.passes import UnrollCustomDefinitions -from qiskit.transpiler.passes import Unroll3qOrMore -from qiskit.transpiler.passes import CheckMap -from qiskit.transpiler.passes import GateDirection from qiskit.transpiler.passes import SetLayout from qiskit.transpiler.passes import VF2Layout -from qiskit.transpiler.passes import VF2PostLayout from qiskit.transpiler.passes import TrivialLayout from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements from qiskit.transpiler.passes import BasicSwap from qiskit.transpiler.passes import LookaheadSwap from qiskit.transpiler.passes import StochasticSwap from qiskit.transpiler.passes import SabreSwap -from qiskit.transpiler.passes import FullAncillaAllocation -from qiskit.transpiler.passes import EnlargeWithAncilla from qiskit.transpiler.passes import FixedPoint from qiskit.transpiler.passes import Depth from qiskit.transpiler.passes import Size -from qiskit.transpiler.passes import RemoveResetInZeroState from qiskit.transpiler.passes import Optimize1qGatesDecomposition from qiskit.transpiler.passes import CommutativeCancellation -from qiskit.transpiler.passes import ApplyLayout -from qiskit.transpiler.passes import CheckGateDirection -from qiskit.transpiler.passes import Collect2qBlocks -from qiskit.transpiler.passes import ConsolidateBlocks -from qiskit.transpiler.passes import UnitarySynthesis -from qiskit.transpiler.passes import TimeUnitConversion -from qiskit.transpiler.passes import ALAPScheduleAnalysis -from qiskit.transpiler.passes import ASAPScheduleAnalysis -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 from qiskit.transpiler.passes import Error -from qiskit.transpiler.passes import ContainsInstruction +from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason -from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM -def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: +def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassManager: """Level 2 pass manager: medium optimization by initial layout selection and gate cancellation using commutativity rules. @@ -82,10 +58,6 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: Finally, optimizations in the form of commutative gate cancellation and redundant reset removal are performed. - Note: - In simulators where ``coupling_map=None``, only the unrolling and - optimization stages are done. - Args: pass_manager_config: configuration of the pass manager. @@ -112,21 +84,7 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config target = pass_manager_config.target - # 1. Unroll to 1q or 2q gates - _unroll3q = [ - # Use unitary synthesis for basis aware decomposition of UnitaryGates - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - method=unitary_synthesis_method, - min_qubits=3, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - Unroll3qOrMore(target=target, basis_gates=basis_gates), - ] - - # 2. Search for a perfect layout, or choose a dense layout, if no layout given + # Search for a perfect layout, or choose a dense layout, if no layout given _given_layout = SetLayout(initial_layout) def _choose_layout_condition(property_set): @@ -147,7 +105,7 @@ def _vf2_match_not_found(property_set): return True return False - # 2a. Try using VF2 layout to find a perfect layout + # Try using VF2 layout to find a perfect layout _choose_layout_0 = ( [] if pass_manager_config.layout_method @@ -160,7 +118,6 @@ def _vf2_match_not_found(property_set): ) ) - # 2b. if VF2 layout doesn't converge on a solution use layout_method (dense) to get a layout if layout_method == "trivial": _choose_layout_1 = TrivialLayout(coupling_map) elif layout_method == "dense": @@ -172,123 +129,44 @@ def _vf2_match_not_found(property_set): else: raise TranspilerError("Invalid layout method %s." % layout_method) - # 3. Extend dag/layout with ancillas using the full coupling map - _embed = [FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()] - - # 4. Swap to fit the coupling map - _swap_check = CheckMap(coupling_map) - - def _swap_condition(property_set): - return not property_set["is_swap_mapped"] - - def _swap_needs_basis(property_set): - return _swap_condition(property_set) and routing_method == "toqm" - - _swap = [BarrierBeforeFinalMeasurements()] + toqm_pass = False if routing_method == "basic": - _swap += [BasicSwap(coupling_map)] + routing_pass = BasicSwap(coupling_map) elif routing_method == "stochastic": - _swap += [StochasticSwap(coupling_map, trials=20, seed=seed_transpiler)] + routing_pass = StochasticSwap(coupling_map, trials=20, seed=seed_transpiler) elif routing_method == "lookahead": - _swap += [LookaheadSwap(coupling_map, search_depth=5, search_width=5)] + routing_pass = LookaheadSwap(coupling_map, search_depth=5, search_width=5) elif routing_method == "sabre": - _swap += [SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler)] + routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler) elif routing_method == "toqm": HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO2, latencies_from_target if initial_layout: raise TranspilerError("Initial layouts are not supported with TOQM-based routing.") + toqm_pass = True # Note: BarrierBeforeFinalMeasurements is skipped intentionally since ToqmSwap # does not yet support barriers. - _swap = [ - ToqmSwap( - coupling_map, - strategy=ToqmStrategyO2( - latencies_from_target( - coupling_map, instruction_durations, basis_gates, backend_properties, target - ) - ), - ) - ] + routing_pass = ToqmSwap( + coupling_map, + strategy=ToqmStrategyO2( + latencies_from_target( + coupling_map, instruction_durations, basis_gates, backend_properties, target + ) + ), + ) elif routing_method == "none": - _swap += [ - Error( - msg=( - "No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}" - ), - action="raise", - ) - ] + routing_pass = Error( + msg="No routing method selected, but circuit is not routed to device. " + "CheckMap Error: {check_map_msg}", + action="raise", + ) else: raise TranspilerError("Invalid routing method %s." % routing_method) - # 5. Unroll to the basis - if translation_method == "unroller": - _unroll = [Unroller(basis_gates)] - elif translation_method == "translator": - from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel - - _unroll = [ - # Use unitary synthesis for basis aware decomposition of UnitaryGates before - # custom unrolling - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - backend_props=backend_properties, - method=unitary_synthesis_method, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - UnrollCustomDefinitions(sel, basis_gates), - BasisTranslator(sel, basis_gates, target), - ] - elif translation_method == "synthesis": - _unroll = [ - # Use unitary synthesis for basis aware decomposition of UnitaryGates before - # collection - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - backend_props=backend_properties, - method=unitary_synthesis_method, - plugin_config=unitary_synthesis_plugin_config, - min_qubits=3, - target=target, - ), - Unroll3qOrMore(target=target, basis_gates=basis_gates), - Collect2qBlocks(), - ConsolidateBlocks(basis_gates=basis_gates, target=target), - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - backend_props=backend_properties, - method=unitary_synthesis_method, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - ] - else: - raise TranspilerError("Invalid translation method %s." % translation_method) - - # 6. Fix any bad CX directions - _direction_check = [CheckGateDirection(coupling_map, target)] - - def _direction_condition(property_set): - return not property_set["is_direction_mapped"] - - _direction = [GateDirection(coupling_map, target)] - - # 7. Remove zero-state reset - _reset = RemoveResetInZeroState() - - # 8. 1q rotation merge and commutative cancellation iteratively until no more change in depth - # and size + # Build optimization loop: 1q rotation merge and commutative cancellation iteratively until + # no more change in depth _depth_check = [Depth(), FixedPoint("depth")] _size_check = [Size(), FixedPoint("size")] @@ -300,125 +178,70 @@ def _opt_control(property_set): CommutativeCancellation(basis_gates=basis_gates), ] + unroll_3q = None # Build pass manager - pm2 = PassManager() if coupling_map or initial_layout: - pm2.append(_given_layout) - pm2.append(_unroll3q) - pm2.append(_choose_layout_0, condition=_choose_layout_condition) - pm2.append(_choose_layout_1, condition=_vf2_match_not_found) - pm2.append(_embed) - pm2.append(_swap_check) - pm2.append(_unroll, condition=_swap_needs_basis) - pm2.append(_swap, condition=_swap_condition) - if ( - (coupling_map and backend_properties) - and initial_layout is None - and pass_manager_config.layout_method is None - ): - - def _run_post_layout_condition(property_set): - vf2_stop_reason = property_set["VF2Layout_stop_reason"] - if ( - vf2_stop_reason is not None - and vf2_stop_reason == VF2LayoutStopReason.SOLUTION_FOUND - ): - return False - return True - - def _apply_post_layout_condition(property_set): - # if VF2 Post layout found a solution we need to re-apply the better - # layout. Otherwise we can skip apply layout. - if ( - property_set["VF2PostLayout_stop_reason"] is not None - and property_set["VF2PostLayout_stop_reason"] - is VF2PostLayoutStopReason.SOLUTION_FOUND - ): - return True - return False - - pm2.append( - VF2PostLayout( - target, - coupling_map, - backend_properties, - seed_transpiler, - call_limit=int(5e6), # Set call limit to ~10 sec with retworkx 0.10.2 - strict_direction=False, - ), - condition=_run_post_layout_condition, - ) - pm2.append(ApplyLayout(), condition=_apply_post_layout_condition) - - pm2.append(_unroll) + unroll_3q = common.generate_unroll_3q( + target, + basis_gates, + approximation_degree, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout_0, condition=_choose_layout_condition) + layout.append(_choose_layout_1, condition=_vf2_match_not_found) + layout += common.generate_embed_passmanager(coupling_map) + vf2_call_limit = None + if pass_manager_config.layout_method is None and pass_manager_config.initial_layout is None: + vf2_call_limit = int(5e6) # Set call limit to ~10 sec with retworkx 0.10.2 + routing = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=not toqm_pass, + ) + else: + layout = None + routing = None + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) + pre_routing = None + if toqm_pass: + pre_routing = translation if (coupling_map and not coupling_map.is_symmetric) or ( target is not None and target.get_non_global_operation_names(strict_direction=True) ): - pm2.append(_direction_check) - pm2.append(_direction, condition=_direction_condition) - pm2.append(_reset) - - pm2.append(_depth_check + _size_check) - pm2.append(_opt + _unroll + _depth_check + _size_check, do_while=_opt_control) - - 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: - # Do scheduling after unit conversion. - scheduler = { - "alap": ALAPScheduleAnalysis, - "as_late_as_possible": ALAPScheduleAnalysis, - "asap": ASAPScheduleAnalysis, - "as_soon_as_possible": ASAPScheduleAnalysis, - } - 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: - # 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 + pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) + else: + pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) + optimization = PassManager() + unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] + optimization.append(_depth_check + _size_check) + opt_loop = _opt + unroll + _depth_check + _size_check + optimization.append(opt_loop, do_while=_opt_control) + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) + return StagedPassManager( + init=unroll_3q, + layout=layout, + pre_routing=pre_routing, + routing=routing, + translation=translation, + pre_optimization=pre_optimization, + optimization=optimization, + scheduling=sched, + ) diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 0f678c902cd2..7a0f22319ecb 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -20,27 +20,18 @@ from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.passmanager import StagedPassManager -from qiskit.transpiler.passes import Unroller -from qiskit.transpiler.passes import BasisTranslator -from qiskit.transpiler.passes import UnrollCustomDefinitions -from qiskit.transpiler.passes import Unroll3qOrMore -from qiskit.transpiler.passes import CheckMap -from qiskit.transpiler.passes import GateDirection from qiskit.transpiler.passes import SetLayout from qiskit.transpiler.passes import VF2Layout -from qiskit.transpiler.passes import VF2PostLayout from qiskit.transpiler.passes import TrivialLayout from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout from qiskit.transpiler.passes import SabreLayout -from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements from qiskit.transpiler.passes import BasicSwap from qiskit.transpiler.passes import LookaheadSwap from qiskit.transpiler.passes import StochasticSwap from qiskit.transpiler.passes import SabreSwap -from qiskit.transpiler.passes import FullAncillaAllocation -from qiskit.transpiler.passes import EnlargeWithAncilla from qiskit.transpiler.passes import FixedPoint from qiskit.transpiler.passes import Depth from qiskit.transpiler.passes import Size @@ -52,26 +43,15 @@ from qiskit.transpiler.passes import Collect2qBlocks from qiskit.transpiler.passes import ConsolidateBlocks from qiskit.transpiler.passes import UnitarySynthesis -from qiskit.transpiler.passes import ApplyLayout -from qiskit.transpiler.passes import CheckGateDirection -from qiskit.transpiler.passes import TimeUnitConversion -from qiskit.transpiler.passes import ALAPScheduleAnalysis -from qiskit.transpiler.passes import ASAPScheduleAnalysis -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 from qiskit.transpiler.passes import Error -from qiskit.transpiler.passes import ContainsInstruction +from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason -from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from qiskit.transpiler import TranspilerError from qiskit.utils.optionals import HAS_TOQM -def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: +def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassManager: """Level 3 pass manager: heavy optimization by noise adaptive qubit mapping and gate cancellation using commutativity rules and unitary synthesis. @@ -85,10 +65,6 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: Finally, optimizations in the form of commutative gate cancellation, resynthesis of two-qubit unitary blocks, and redundant reset removal are performed. - Note: - In simulators where ``coupling_map=None``, only the unrolling and - optimization stages are done. - Args: pass_manager_config: configuration of the pass manager. @@ -115,21 +91,7 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config target = pass_manager_config.target - # 1. Unroll to 1q or 2q gates - _unroll3q = [ - # Use unitary synthesis for basis aware decomposition of UnitaryGates - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - method=unitary_synthesis_method, - plugin_config=unitary_synthesis_plugin_config, - min_qubits=3, - target=target, - ), - Unroll3qOrMore(target=target, basis_gates=basis_gates), - ] - - # 2. Layout on good qubits if calibration info available, otherwise on dense links + # Layout on good qubits if calibration info available, otherwise on dense links _given_layout = SetLayout(initial_layout) def _choose_layout_condition(property_set): @@ -174,27 +136,15 @@ def _vf2_match_not_found(property_set): else: raise TranspilerError("Invalid layout method %s." % layout_method) - # 3. Extend dag/layout with ancillas using the full coupling map - _embed = [FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()] - - # 4. Swap to fit the coupling map - _swap_check = CheckMap(coupling_map) - - def _swap_condition(property_set): - return not property_set["is_swap_mapped"] - - def _swap_needs_basis(property_set): - return _swap_condition(property_set) and routing_method == "toqm" - - _swap = [BarrierBeforeFinalMeasurements()] + toqm_pass = False if routing_method == "basic": - _swap += [BasicSwap(coupling_map)] + routing_pass = BasicSwap(coupling_map) elif routing_method == "stochastic": - _swap += [StochasticSwap(coupling_map, trials=200, seed=seed_transpiler)] + routing_pass = StochasticSwap(coupling_map, trials=200, seed=seed_transpiler) elif routing_method == "lookahead": - _swap += [LookaheadSwap(coupling_map, search_depth=5, search_width=6)] + routing_pass = LookaheadSwap(coupling_map, search_depth=5, search_width=6) elif routing_method == "sabre": - _swap += [SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler)] + routing_pass = SabreSwap(coupling_map, heuristic="decay", seed=seed_transpiler) elif routing_method == "toqm": HAS_TOQM.require_now("TOQM-based routing") from qiskit_toqm import ToqmSwap, ToqmStrategyO3, latencies_from_target @@ -202,86 +152,26 @@ def _swap_needs_basis(property_set): if initial_layout: raise TranspilerError("Initial layouts are not supported with TOQM-based routing.") + toqm_pass = True # Note: BarrierBeforeFinalMeasurements is skipped intentionally since ToqmSwap # does not yet support barriers. - _swap = [ - ToqmSwap( - coupling_map, - strategy=ToqmStrategyO3( - latencies_from_target( - coupling_map, instruction_durations, basis_gates, backend_properties, target - ) - ), - ) - ] + routing_pass = ToqmSwap( + coupling_map, + strategy=ToqmStrategyO3( + latencies_from_target( + coupling_map, instruction_durations, basis_gates, backend_properties, target + ) + ), + ) elif routing_method == "none": - _swap += [ - Error( - msg=( - "No routing method selected, but circuit is not routed to device. " - "CheckMap Error: {check_map_msg}" - ), - action="raise", - ) - ] + routing_pass = Error( + msg="No routing method selected, but circuit is not routed to device. " + "CheckMap Error: {check_map_msg}", + action="raise", + ) else: raise TranspilerError("Invalid routing method %s." % routing_method) - # 5. Unroll to the basis - if translation_method == "unroller": - _unroll = [Unroller(basis_gates)] - elif translation_method == "translator": - from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel - - _unroll = [ - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - backend_props=backend_properties, - plugin_config=unitary_synthesis_plugin_config, - method=unitary_synthesis_method, - target=target, - ), - UnrollCustomDefinitions(sel, basis_gates), - BasisTranslator(sel, basis_gates, target), - ] - elif translation_method == "synthesis": - _unroll = [ - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - backend_props=backend_properties, - method=unitary_synthesis_method, - plugin_config=unitary_synthesis_plugin_config, - min_qubits=3, - target=target, - ), - Unroll3qOrMore(target=target, basis_gates=basis_gates), - Collect2qBlocks(), - ConsolidateBlocks(basis_gates=basis_gates, target=target), - UnitarySynthesis( - basis_gates, - approximation_degree=approximation_degree, - coupling_map=coupling_map, - backend_props=backend_properties, - method=unitary_synthesis_method, - plugin_config=unitary_synthesis_plugin_config, - target=target, - ), - ] - else: - raise TranspilerError("Invalid translation method %s." % translation_method) - - # 6. Fix any CX direction mismatch - _direction_check = [CheckGateDirection(coupling_map, target)] - - def _direction_condition(property_set): - return not property_set["is_direction_mapped"] - - _direction = [GateDirection(coupling_map, target)] - # 8. Optimize iteratively until no more change in depth. Removes useless gates # after reset and before measure, commutes gates and optimizes contiguous blocks. _depth_check = [Depth(), FixedPoint("depth")] @@ -290,10 +180,6 @@ def _direction_condition(property_set): def _opt_control(property_set): return (not property_set["depth_fixed_point"]) or (not property_set["size_fixed_point"]) - _reset = [RemoveResetInZeroState()] - - _meas = [OptimizeSwapBeforeMeasure(), RemoveDiagonalGatesBeforeMeasure()] - _opt = [ Collect2qBlocks(), ConsolidateBlocks(basis_gates=basis_gates, target=target), @@ -311,137 +197,87 @@ def _opt_control(property_set): ] # Build pass manager - pm3 = PassManager() - pm3.append(_unroll3q) - pm3.append(_reset + _meas) + init = common.generate_unroll_3q( + target, + basis_gates, + approximation_degree, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) + init.append(RemoveResetInZeroState()) + init.append(OptimizeSwapBeforeMeasure()) + init.append(RemoveDiagonalGatesBeforeMeasure()) if coupling_map or initial_layout: - pm3.append(_given_layout) - pm3.append(_choose_layout_0, condition=_choose_layout_condition) - pm3.append(_choose_layout_1, condition=_vf2_match_not_found) - pm3.append(_embed) - pm3.append(_swap_check) - pm3.append(_unroll, condition=_swap_needs_basis) - pm3.append(_swap, condition=_swap_condition) - if ( - (coupling_map and backend_properties) - and initial_layout is None - and pass_manager_config.layout_method is None - ): - - def _run_post_layout_condition(property_set): - vf2_stop_reason = property_set["VF2Layout_stop_reason"] - if ( - vf2_stop_reason is not None - and vf2_stop_reason == VF2LayoutStopReason.SOLUTION_FOUND - ): - return False - return True - - def _apply_post_layout_condition(property_set): - # if VF2 Post layout found a solution we need to re-apply the better - # layout. Otherwise we can skip apply layout. - if ( - property_set["VF2PostLayout_stop_reason"] is not None - and property_set["VF2PostLayout_stop_reason"] - is VF2PostLayoutStopReason.SOLUTION_FOUND - ): - return True - return False - - pm3.append( - VF2PostLayout( - target, - coupling_map, - backend_properties, - seed_transpiler, - call_limit=int(3e7), # Set call limit to ~60 sec with retworkx 0.10.2 - strict_direction=False, - ), - condition=_run_post_layout_condition, - ) - pm3.append(ApplyLayout(), condition=_apply_post_layout_condition) - - pm3.append(_unroll) + layout = PassManager() + layout.append(_given_layout) + layout.append(_choose_layout_0, condition=_choose_layout_condition) + layout.append(_choose_layout_1, condition=_vf2_match_not_found) + layout += common.generate_embed_passmanager(coupling_map) + vf2_call_limit = None + if pass_manager_config.layout_method is None and pass_manager_config.initial_layout is None: + vf2_call_limit = int(3e7) # Set call limit to ~60 sec with retworkx 0.10.2 + routing = common.generate_routing_passmanager( + routing_pass, + target, + coupling_map=coupling_map, + vf2_call_limit=vf2_call_limit, + backend_properties=backend_properties, + seed_transpiler=seed_transpiler, + use_barrier_before_measurement=not toqm_pass, + ) + else: + layout = None + routing = None + translation = common.generate_translation_passmanager( + target, + basis_gates, + translation_method, + approximation_degree, + coupling_map, + backend_properties, + unitary_synthesis_method, + unitary_synthesis_plugin_config, + ) + pre_routing = None + if toqm_pass: + pre_routing = translation + optimization = PassManager() + unroll = [pass_ for x in translation.passes() for pass_ in x["passes"]] + optimization.append(_depth_check + _size_check) if (coupling_map and not coupling_map.is_symmetric) or ( target is not None and target.get_non_global_operation_names(strict_direction=True) ): - pm3.append(_direction_check) - pm3.append(_direction, condition=_direction_condition) - pm3.append(_reset) - pm3.append(_depth_check + _size_check) + pre_optimization = common.generate_pre_op_passmanager(target, coupling_map, True) + _direction = [ + pass_ + for x in common.generate_pre_op_passmanager(target, coupling_map).passes() + for pass_ in x["passes"] + ] # For transpiling to a target we need to run GateDirection in the # optimization loop to correct for incorrect directions that might be # inserted by UnitarySynthesis which is direction aware but only via # the coupling map which with a target doesn't give a full picture if target is not None: - pm3.append( - _opt + _unroll + _depth_check + _size_check + _direction, do_while=_opt_control + optimization.append( + _opt + unroll + _depth_check + _size_check + _direction, do_while=_opt_control ) else: - pm3.append(_opt + _unroll + _depth_check + _size_check, do_while=_opt_control) + optimization.append(_opt + unroll + _depth_check + _size_check, do_while=_opt_control) else: - pm3.append(_reset) - pm3.append(_depth_check + _size_check) - 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: - # Do scheduling after unit conversion. - scheduler = { - "alap": ALAPScheduleAnalysis, - "as_late_as_possible": ALAPScheduleAnalysis, - "asap": ASAPScheduleAnalysis, - "as_soon_as_possible": ASAPScheduleAnalysis, - } - 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: - # 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 + pre_optimization = common.generate_pre_op_passmanager(remove_reset_in_zero=True) + optimization.append(_opt + unroll + _depth_check + _size_check, do_while=_opt_control) + opt_loop = _depth_check + _opt + unroll + optimization.append(opt_loop, do_while=_opt_control) + sched = common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map + ) + return StagedPassManager( + init=init, + layout=layout, + pre_routing=pre_routing, + routing=routing, + translation=translation, + pre_optimization=pre_optimization, + optimization=optimization, + scheduling=sched, + ) diff --git a/releasenotes/notes/add-full-passmanager-5a377f1b71480f72.yaml b/releasenotes/notes/add-full-passmanager-5a377f1b71480f72.yaml new file mode 100644 index 000000000000..be59f4545743 --- /dev/null +++ b/releasenotes/notes/add-full-passmanager-5a377f1b71480f72.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + Added a new class, :class:`qiskit.transpiler.StagedPassManager`, which is + a :class:`~qiskit.transpiler.PassManager` subclass that has a pipeline + with defined phases to perform circuit compilation. Each phase is a + :class:`~qiskit.transpiler.PassManager` object that will get executed + in a fixed order. For example:: + + from qiskit.transpiler.passes import * + from qiskit.transpiler import PassManager, StagedPassManager + + basis_gates = ['rx', 'ry', 'rxx'] + init = PassManager([UnitarySynthesis(basis_gates, min_qubits=3), Unroll3qOrMore()]) + translate = PassManager([Collect2qBlocks(), + ConsolidateBlocks(basis_gates=basis_gates), + UnitarySynthesis(basis_gates)]) + + staged_pm = StagedPassManager(stages=['init', 'translation'], init=init, translation=translate) diff --git a/test/python/transpiler/test_staged_passmanager.py b/test/python/transpiler/test_staged_passmanager.py new file mode 100644 index 000000000000..79045b2cd86d --- /dev/null +++ b/test/python/transpiler/test_staged_passmanager.py @@ -0,0 +1,94 @@ +# 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. + +# pylint: disable=missing-function-docstring,missing-class-docstring + +"""Test the staged passmanager logic""" + +from qiskit.transpiler import PassManager, StagedPassManager +from qiskit.transpiler.passes import Optimize1qGates, Unroller, Depth +from qiskit.test import QiskitTestCase + + +class TestStagedPassManager(QiskitTestCase): + def test_default_stages(self): + spm = StagedPassManager() + self.assertEqual( + spm.stages, ["init", "layout", "routing", "translation", "optimization", "scheduling"] + ) + spm = StagedPassManager( + init=PassManager([Optimize1qGates()]), + routing=PassManager([Unroller(["u", "cx"])]), + scheduling=PassManager([Depth()]), + ) + self.assertEqual( + [x.__class__.__name__ for passes in spm.passes() for x in passes["passes"]], + ["Optimize1qGates", "Unroller", "Depth"], + ) + + def test_inplace_edit(self): + spm = StagedPassManager(stages=["single_stage"]) + spm.single_stage = PassManager([Optimize1qGates(), Depth()]) + self.assertEqual( + [x.__class__.__name__ for passes in spm.passes() for x in passes["passes"]], + ["Optimize1qGates", "Depth"], + ) + spm.single_stage.append(Unroller(["u"])) + spm.single_stage.append(Depth()) + self.assertEqual( + [x.__class__.__name__ for passes in spm.passes() for x in passes["passes"]], + ["Optimize1qGates", "Depth", "Unroller", "Depth"], + ) + + def test_invalid_stage(self): + with self.assertRaises(AttributeError): + StagedPassManager(stages=["init"], translation=PassManager()) + + def test_pre_phase_is_valid_stage(self): + spm = StagedPassManager(stages=["init"], pre_init=PassManager([Depth()])) + self.assertEqual( + [x.__class__.__name__ for passes in spm.passes() for x in passes["passes"]], + ["Depth"], + ) + + def test_append_extend_not_implemented(self): + spm = StagedPassManager() + with self.assertRaises(NotImplementedError): + spm.append(Depth()) + with self.assertRaises(NotImplementedError): + spm += PassManager() + + def test_invalid_stages(self): + invalid_stages = [ + "two words", + "two-words", + "two+words", + "two&words", + "[two_words]", + "", + "{two_words}", + "(two_words)", + "two^words", + "two_words!", + "^two_words", + "@two_words", + "two~words", + r"two\words", + "two/words", + ] + all_stages = invalid_stages + ["two_words", "init"] + + with self.assertRaises(ValueError) as err: + StagedPassManager(all_stages) + message = str(err.exception) + for stage in invalid_stages: + self.assertIn(stage, message) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 3956da41737f..4a2f47bcdd91 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -57,7 +57,7 @@ def generate_full_circuit(): def generate_unitary_gate_circuit(): """Generate a circuit with a unitary gate.""" - unitary_circuit = QuantumCircuit(5) + unitary_circuit = QuantumCircuit(5, name="unitary_circuit") unitary_circuit.unitary(random_unitary(32, seed=100), [0, 1, 2, 3, 4]) unitary_circuit.measure_all() return unitary_circuit @@ -67,7 +67,7 @@ def generate_random_circuits(): """Generate multiple random circuits.""" random_circuits = [] for i in range(1, 15): - qc = QuantumCircuit(i) + qc = QuantumCircuit(i, name=f"random_circuit-{i}") qc.h(0) if i > 1: for j in range(i - 1): @@ -84,7 +84,9 @@ def generate_random_circuits(): def generate_string_parameters(): """Generate a circuit from pauli tensor opflow.""" - return (X ^ Y ^ Z).to_circuit_op().to_circuit() + op_circuit = (X ^ Y ^ Z).to_circuit_op().to_circuit() + op_circuit.name = "X^Y^Z" + return op_circuit def generate_register_edge_cases(): @@ -92,7 +94,7 @@ def generate_register_edge_cases(): register_edge_cases = [] # Circuit with shared bits in a register qubits = [Qubit() for _ in range(5)] - shared_qc = QuantumCircuit() + shared_qc = QuantumCircuit(name="shared_bits") shared_qc.add_bits(qubits) shared_qr = QuantumRegister(bits=qubits) shared_qc.add_register(shared_qr) @@ -109,7 +111,7 @@ def generate_register_edge_cases(): qr = QuantumRegister(name="bar", bits=qr[:3] + [Qubit(), Qubit()]) cr = ClassicalRegister(5, "foo") cr = ClassicalRegister(name="classical_bar", bits=cr[:3] + [Clbit(), Clbit()]) - hybrid_qc = QuantumCircuit(qr, cr) + hybrid_qc = QuantumCircuit(qr, cr, name="mix_standalone_bits_registers") hybrid_qc.h(0) hybrid_qc.cx(0, 1) hybrid_qc.cx(0, 2) @@ -120,7 +122,7 @@ def generate_register_edge_cases(): # Circuit with mixed standalone and shared registers qubits = [Qubit() for _ in range(5)] clbits = [Clbit() for _ in range(5)] - mixed_qc = QuantumCircuit() + mixed_qc = QuantumCircuit(name="mix_standalone_bits_with_registers") mixed_qc.add_bits(qubits) mixed_qc.add_bits(clbits) qr = QuantumRegister(bits=qubits) @@ -140,7 +142,7 @@ def generate_register_edge_cases(): qr_standalone = QuantumRegister(2, "standalone") qubits = [Qubit() for _ in range(5)] clbits = [Clbit() for _ in range(5)] - ooo_qc = QuantumCircuit() + ooo_qc = QuantumCircuit(name="out_of_order_bits") ooo_qc.add_bits(qubits) ooo_qc.add_bits(clbits) random.seed(42) @@ -166,7 +168,7 @@ def generate_register_edge_cases(): def generate_parameterized_circuit(): """Generate a circuit with parameters and parameter expressions.""" - param_circuit = QuantumCircuit(1) + param_circuit = QuantumCircuit(1, name="parameterized") theta = Parameter("theta") lam = Parameter("λ") theta_pi = 3.14159 * theta @@ -194,7 +196,7 @@ def generate_qft_circuit(): ) qubits = 3 - qft_circ = QuantumCircuit(qubits, qubits) + qft_circ = QuantumCircuit(qubits, qubits, name="QFT") qft_circ.initialize(state) qft_circ.append(QFT(qubits), range(qubits)) qft_circ.measure(range(qubits), range(qubits)) @@ -208,7 +210,7 @@ def generate_param_phase(): theta = Parameter("theta") phi = Parameter("phi") sum_param = theta + phi - qc = QuantumCircuit(5, 1, global_phase=sum_param) + qc = QuantumCircuit(5, 1, global_phase=sum_param, name="parameter_phase") qc.h(0) for i in range(4): qc.cx(i, i + 1) @@ -224,7 +226,7 @@ def generate_param_phase(): output_circuits.append(qc) # Generate circuit with Parameter global phase theta = Parameter("theta") - bell_qc = QuantumCircuit(2, global_phase=theta) + bell_qc = QuantumCircuit(2, global_phase=theta, name="bell_param_global_phase") bell_qc.h(0) bell_qc.cx(0, 1) bell_qc.measure_all() @@ -246,7 +248,7 @@ def generate_single_clbit_condition_teleportation(): # pylint: disable=invalid- def generate_parameter_vector(): """Generate tests for parameter vector element ordering.""" - qc = QuantumCircuit(11) + qc = QuantumCircuit(11, name="parameter_vector") input_params = ParameterVector("x_par", 11) user_params = ParameterVector("θ_par", 11) for i, param in enumerate(user_params): @@ -258,7 +260,7 @@ def generate_parameter_vector(): def generate_parameter_vector_expression(): # pylint: disable=invalid-name """Generate tests for parameter vector element ordering.""" - qc = QuantumCircuit(7) + qc = QuantumCircuit(7, name="vector_expansion") entanglement = [[i, i + 1] for i in range(7 - 1)] input_params = ParameterVector("x_par", 14) user_params = ParameterVector("\u03B8_par", 1) @@ -284,7 +286,7 @@ def generate_evolution_gate(): synthesis = SuzukiTrotter() evo = PauliEvolutionGate([(Z ^ I) + (I ^ Z)] * 5, time=2.0, synthesis=synthesis) - qc = QuantumCircuit(2) + qc = QuantumCircuit(2, name="pauli_evolution_circuit") qc.append(evo, range(2)) return qc @@ -295,7 +297,7 @@ def generate_control_flow_circuits(): # If instruction circuits = [] - qc = QuantumCircuit(2, 2) + qc = QuantumCircuit(2, 2, name="control_flow") qc.h(0) qc.measure(0, 0) true_body = QuantumCircuit(1) @@ -305,7 +307,7 @@ def generate_control_flow_circuits(): qc.measure(1, 1) circuits.append(qc) # If else instruction - qc = QuantumCircuit(2, 2) + qc = QuantumCircuit(2, 2, name="if_else") qc.h(0) qc.measure(0, 0) false_body = QuantumCircuit(1) @@ -315,7 +317,7 @@ def generate_control_flow_circuits(): qc.measure(1, 1) circuits.append(qc) # While loop - qc = QuantumCircuit(2, 1) + qc = QuantumCircuit(2, 1, name="while_loop") block = QuantumCircuit(2, 1) block.h(0) block.cx(0, 1) @@ -324,7 +326,7 @@ def generate_control_flow_circuits(): qc.append(while_loop, [0, 1], [0]) circuits.append(qc) # for loop range - qc = QuantumCircuit(2, 1) + qc = QuantumCircuit(2, 1, name="for_loop") body = QuantumCircuit(2, 1) body.h(0) body.cx(0, 1) @@ -334,7 +336,7 @@ def generate_control_flow_circuits(): qc.append(for_loop_op, [0, 1], [0]) circuits.append(qc) # For loop iterator - qc = QuantumCircuit(2, 1) + qc = QuantumCircuit(2, 1, name="for_loop_iterator") for_loop_op = ForLoopOp(iter(range(5)), None, body=body) qc.append(for_loop_op, [0, 1], [0]) circuits.append(qc) @@ -427,7 +429,7 @@ def assert_equal(reference, qpy, count, bind=None): sys.exit(1) # Don't compare name on bound circuits if bind is None and reference.name != qpy.name: - msg = f"Circuit {count} name mismatch {reference.name} != {qpy.name}" + msg = f"Circuit {count} name mismatch {reference.name} != {qpy.name}\n{reference}\n{qpy}" sys.stderr.write(msg) sys.exit(2) if reference.metadata != qpy.metadata: