diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index 86a124e32b40..ea118c359655 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -169,8 +169,12 @@ def _normalize_passes( passes = [passes] for pass_ in passes: if isinstance(pass_, FlowController): - # Normalize passes in nested FlowController - PassManager._normalize_passes(pass_.passes) + # Normalize passes in nested FlowController. + # TODO: Internal renormalisation should be the responsibility of the + # `FlowController`, but the separation between `FlowController`, + # `RunningPassManager` and `PassManager` is so muddled right now, it would be better + # to do this as part of more top-down refactoring. ---Jake, 2022-10-03. + pass_.passes = PassManager._normalize_passes(pass_.passes) elif not isinstance(pass_, BasePass): raise TranspilerError( "%s is not a BasePass or FlowController instance " % pass_.__class__ @@ -322,6 +326,19 @@ def passes(self) -> List[Dict[str, BasePass]]: ret.append(item) return ret + def to_flow_controller(self) -> FlowController: + """Linearize this manager into a single :class:`.FlowController`, so that it can be nested + inside another :class:`.PassManager`.""" + return FlowController.controller_factory( + [ + FlowController.controller_factory( + pass_set["passes"], None, **pass_set["flow_controllers"] + ) + for pass_set in self._pass_sets + ], + None, + ) + class StagedPassManager(PassManager): """A Pass manager pipeline built up of individual stages diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 3491a3e7c065..b441259869b3 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -14,11 +14,13 @@ """Common preset passmanager generators.""" +import collections from typing import Optional from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.passes import Error from qiskit.transpiler.passes import Unroller from qiskit.transpiler.passes import BasisTranslator from qiskit.transpiler.passes import UnrollCustomDefinitions @@ -51,6 +53,105 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout +_CONTROL_FLOW_OP_NAMES = {"for_loop", "if_else", "while_loop"} + +_ControlFlowState = collections.namedtuple("_ControlFlowState", ("working", "not_working")) + +# Any method neither known good nor known bad (i.e. not a Terra-internal pass) is passed through +# without error, since it is being supplied by a plugin and we don't have any knowledge of these. +_CONTROL_FLOW_STATES = { + "layout_method": _ControlFlowState( + working={"trivial", "dense"}, not_working={"sabre", "noise_adaptive"} + ), + "routing_method": _ControlFlowState( + working={"none", "stochastic"}, not_working={"sabre", "lookahead", "basic", "toqm"} + ), + # 'synthesis' is not a supported translation method because of the block-collection passes + # involved; we currently don't have a neat way to pass the information about nested blocks - the + # `UnitarySynthesis` pass itself is control-flow aware. + "translation_method": _ControlFlowState( + working={"translator", "unroller"}, not_working={"synthesis"} + ), + "optimization_method": _ControlFlowState(working=set(), not_working=set()), + "scheduling_method": _ControlFlowState(working=set(), not_working={"alap", "asap"}), +} + + +def _has_control_flow(property_set): + return any(property_set[f"contains_{x}"] for x in _CONTROL_FLOW_OP_NAMES) + + +def _without_control_flow(property_set): + return not any(property_set[f"contains_{x}"] for x in _CONTROL_FLOW_OP_NAMES) + + +def generate_control_flow_options_check( + layout_method=None, + routing_method=None, + translation_method=None, + optimization_method=None, + scheduling_method=None, +): + """Generate a pass manager that, when run on a DAG that contains control flow, fails with an + error message explaining the invalid options, and what could be used instead. + + Returns: + PassManager: a pass manager that populates the ``contains_x`` properties for each of the + control-flow operations, and raises an error if any of the given options do not support + control flow, but a circuit with control flow is given. + """ + + bad_options = [] + message = "Some options cannot be used with control flow." + for stage, given in [ + ("layout", layout_method), + ("routing", routing_method), + ("translation", translation_method), + ("optimization", optimization_method), + ("scheduling", scheduling_method), + ]: + option = stage + "_method" + method_states = _CONTROL_FLOW_STATES[option] + if given is not None and given in method_states.not_working: + if method_states.working: + message += ( + f" Got {option}='{given}', but valid values are {list(method_states.working)}." + ) + else: + message += ( + f" Got {option}='{given}', but the entire {stage} stage is not supported." + ) + bad_options.append(option) + out = PassManager() + out.append(ContainsInstruction(_CONTROL_FLOW_OP_NAMES, recurse=False)) + if not bad_options: + return out + out.append(Error(message), condition=_has_control_flow) + return out + + +def generate_error_on_control_flow(message): + """Get a pass manager that always raises an error if control flow is present in a given + circuit.""" + out = PassManager() + out.append(ContainsInstruction(_CONTROL_FLOW_OP_NAMES, recurse=False)) + out.append(Error(message), condition=_has_control_flow) + return out + + +def if_has_control_flow_else(if_present, if_absent): + """Generate a pass manager that will run the passes in ``if_present`` if the given circuit + has control-flow operations in it, and those in ``if_absent`` if it doesn't.""" + if isinstance(if_present, PassManager): + if_present = if_present.to_flow_controller() + if isinstance(if_absent, PassManager): + if_absent = if_absent.to_flow_controller() + out = PassManager() + out.append(ContainsInstruction(_CONTROL_FLOW_OP_NAMES, recurse=False)) + out.append(if_present, condition=_has_control_flow) + out.append(if_absent, condition=_without_control_flow) + return out + def generate_unroll_3q( target, @@ -111,14 +212,10 @@ def generate_embed_passmanager(coupling_map): 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 _layout_not_perfect(property_set): + """Return ``True`` if the first attempt at layout has been checked and found to be imperfect. + In this case, perfection means "does not require any swap routing".""" + return property_set["is_swap_mapped"] is not None and not property_set["is_swap_mapped"] def _apply_post_layout_condition(property_set): @@ -170,7 +267,7 @@ def generate_routing_passmanager( 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): + if not check_trivial or _layout_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 diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 5492957d2f8a..585c727d11c2 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -183,12 +183,19 @@ def _choose_layout_condition(property_set): sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=0 ) + init = common.generate_control_flow_options_check( + layout_method=layout_method, + routing_method=routing_method, + translation_method=translation_method, + optimization_method=optimization_method, + scheduling_method=scheduling_method, + ) if init_method is not None: - init = plugin_manager.get_passmanager_stage( + init += plugin_manager.get_passmanager_stage( "init", init_method, pass_manager_config, optimization_level=0 ) - else: - init = unroll_3q + elif unroll_3q is not None: + init += unroll_3q optimization = None if optimization_method is not None: optimization = plugin_manager.get_passmanager_stage( diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index d4a5695023ce..ac6360862bdc 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -32,7 +32,7 @@ from qiskit.transpiler.passes import Depth from qiskit.transpiler.passes import Size from qiskit.transpiler.passes import Optimize1qGatesDecomposition -from qiskit.transpiler.passes import Layout2qDistance +from qiskit.transpiler.passes import CheckMap from qiskit.transpiler.passes import GatesInBasis from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason @@ -72,8 +72,10 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa coupling_map = pass_manager_config.coupling_map initial_layout = pass_manager_config.initial_layout init_method = pass_manager_config.init_method - layout_method = pass_manager_config.layout_method or "sabre" - routing_method = pass_manager_config.routing_method or "sabre" + # Unlike other presets, the layout and routing defaults aren't set here because they change + # based on whether the input circuit has control flow. + layout_method = pass_manager_config.layout_method + routing_method = pass_manager_config.routing_method translation_method = pass_manager_config.translation_method or "translator" optimization_method = pass_manager_config.optimization_method scheduling_method = pass_manager_config.scheduling_method @@ -93,16 +95,10 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> StagedPassMa def _choose_layout_condition(property_set): return not property_set["layout"] - 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. - if ( - property_set["trivial_layout_score"] is not None - and property_set["trivial_layout_score"] != 0 - ): - return True - return False + def _layout_not_perfect(property_set): + """Return ``True`` if the first attempt at layout has been checked and found to be + imperfect. In this case, perfection means "does not require any swap routing".""" + return property_set["is_swap_mapped"] is not None and not property_set["is_swap_mapped"] # Use a better layout on densely connected qubits, if circuit needs swaps def _vf2_match_not_found(property_set): @@ -122,10 +118,7 @@ def _vf2_match_not_found(property_set): _choose_layout_0 = ( [] if pass_manager_config.layout_method - else [ - TrivialLayout(coupling_map), - Layout2qDistance(coupling_map, property_name="trivial_layout_score"), - ] + else [TrivialLayout(coupling_map), CheckMap(coupling_map)] ) _choose_layout_1 = ( @@ -150,6 +143,11 @@ def _vf2_match_not_found(property_set): _improve_layout = SabreLayout( coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5 ) + elif layout_method is None: + _improve_layout = common.if_has_control_flow_else( + DenseLayout(coupling_map, backend_properties, target=target), + SabreLayout(coupling_map, max_iterations=2, seed=seed_transpiler, swap_trials=5), + ).to_flow_controller() toqm_pass = False routing_pm = None @@ -185,6 +183,20 @@ def _vf2_match_not_found(property_set): check_trivial=True, use_barrier_before_measurement=not toqm_pass, ) + elif routing_method is None: + _stochastic_routing = plugin_manager.get_passmanager_stage( + "routing", + "stochastic", + pass_manager_config, + optimization_level=1, + ) + _sabre_routing = plugin_manager.get_passmanager_stage( + "routing", + "sabre", + pass_manager_config, + optimization_level=1, + ) + routing_pm = common.if_has_control_flow_else(_stochastic_routing, _sabre_routing) else: routing_pm = plugin_manager.get_passmanager_stage( "routing", @@ -214,7 +226,7 @@ def _opt_control(property_set): unitary_synthesis_plugin_config, hls_config, ) - if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre"}: + if layout_method not in {"trivial", "dense", "noise_adaptive", "sabre", None}: layout = plugin_manager.get_passmanager_stage( "layout", layout_method, pass_manager_config, optimization_level=1 ) @@ -222,7 +234,7 @@ def _opt_control(property_set): 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(_choose_layout_1, condition=_layout_not_perfect) layout.append(_improve_layout, condition=_vf2_match_not_found) layout += common.generate_embed_passmanager(coupling_map) @@ -289,12 +301,19 @@ def _unroll_condition(property_set): sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=1 ) + init = common.generate_control_flow_options_check( + layout_method=layout_method, + routing_method=routing_method, + translation_method=translation_method, + optimization_method=optimization_method, + scheduling_method=scheduling_method, + ) if init_method is not None: - init = plugin_manager.get_passmanager_stage( + init += plugin_manager.get_passmanager_stage( "init", init_method, pass_manager_config, optimization_level=1 ) - else: - init = unroll_3q + elif unroll_3q is not None: + init += unroll_3q return StagedPassManager( init=init, diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 4ec6ff375e10..9597b484a3b7 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -262,12 +262,15 @@ def _unroll_condition(property_set): sched = plugin_manager.get_passmanager_stage( "scheduling", scheduling_method, pass_manager_config, optimization_level=2 ) + init = common.generate_error_on_control_flow( + "The optimizations in optimization_level=2 do not yet support control flow." + ) if init_method is not None: - init = plugin_manager.get_passmanager_stage( + init += plugin_manager.get_passmanager_stage( "init", init_method, pass_manager_config, optimization_level=2 ) - else: - init = unroll_3q + elif unroll_3q is not None: + init += unroll_3q return StagedPassManager( init=init, diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 609c8da360a1..61080636f357 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -202,12 +202,15 @@ def _opt_control(property_set): ] # Build pass manager + init = common.generate_error_on_control_flow( + "The optimizations in optimization_level=3 do not yet support control flow." + ) if init_method is not None: - init = plugin_manager.get_passmanager_stage( - "init", init_method, pass_manager_config, optimization_level=3 + init += plugin_manager.get_passmanager_stage( + "init", init_method, pass_manager_config, optimization_level=2 ) else: - init = common.generate_unroll_3q( + init += common.generate_unroll_3q( target, basis_gates, approximation_degree, diff --git a/qiskit/transpiler/runningpassmanager.py b/qiskit/transpiler/runningpassmanager.py index ba9376b73d5e..284e38078c27 100644 --- a/qiskit/transpiler/runningpassmanager.py +++ b/qiskit/transpiler/runningpassmanager.py @@ -186,7 +186,7 @@ def _do_pass(self, pass_, dag, options): pass_.do_while = partial(pass_.do_while, self.fenced_property_set) for _pass in pass_: - self._do_pass(_pass, dag, pass_.options) + dag = self._do_pass(_pass, dag, pass_.options) else: raise TranspilerError( "Expecting type BasePass or FlowController, got %s." % type(pass_) diff --git a/releasenotes/notes/fix-nested-flow-controllers-a2a5f03eed482fa2.yaml b/releasenotes/notes/fix-nested-flow-controllers-a2a5f03eed482fa2.yaml new file mode 100644 index 000000000000..b5c719e7382a --- /dev/null +++ b/releasenotes/notes/fix-nested-flow-controllers-a2a5f03eed482fa2.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Nesting a :class:`.FlowController` inside another in a :class:`.PassManager` + could previously cause some transpiler passes to become "forgotten" during + transpilation, if the passes returned a new :class:`.DAGCircuit` rather than + mutating their input. Nested :class:`.FlowController`\ s will now affect + the transpilation correctly. diff --git a/releasenotes/notes/transpiler-control-flow-708896bfdb51961d.yaml b/releasenotes/notes/transpiler-control-flow-708896bfdb51961d.yaml new file mode 100644 index 000000000000..070e56a42a10 --- /dev/null +++ b/releasenotes/notes/transpiler-control-flow-708896bfdb51961d.yaml @@ -0,0 +1,90 @@ +--- +features: + - | + Control-flow operations are now supported through the transpiler at + optimization levels 0 and 1 (e.g. calling :func:`.transpile` or + :func:`.generate_preset_pass_manager` with keyword argument + ``optimization_level=1``). One can now construct a circuit such as + + .. code-block:: python + + from qiskit import QuantumCircuit + + qc = QuantumCircuit(2, 1) + qc.h(0) + qc.measure(0, 0) + with qc.if_test((0, True)) as else_: + qc.x(1) + with else_: + qc.y(1) + + and successfully transpile this, such as by:: + + from qiskit import transpile + from qiskit_aer import AerSimulator + + backend = AerSimulator(method="statevector") + transpiled = transpile(qc, backend) + + The available values for the keyword argument ``layout_method`` are + "trivial" and "dense". For ``routing_method``, "stochastic" and "none" are + available. Translation (``translation_method``) can be done using + "translator" or "unroller". Optimization levels 2 and 3 are not yet + supported with control flow, nor is circuit scheduling (i.e. providing a + value to ``scheduling_method``), though we intend to expand support for + these, and the other layout, routing and translation methods in subsequent + releases of Qiskit Terra. + + In order for transpilation with control-flow operations to succeed with a + backend, the backend must have the requisite control-flow operations in its + stated basis. Qiskit Aer, for example, does this. If you simply want to try + out such transpilations, consider overriding the ``basis_gates`` argument + to :func:`.transpile`. + - | + The following transpiler passes have all been taught to understand + control-flow constructs in the form of :class:`.ControlFlowOp` instructions + in a circuit: + + .. rubric:: Layout-related + + - :class:`.ApplyLayout` + - :class:`.DenseLayout` + - :class:`.EnlargeWithAncilla` + - :class:`.FullAncillaAllocation` + - :class:`.SetLayout` + - :class:`.TrivialLayout` + - :class:`.VF2Layout` + - :class:`.VF2PostLayout` + + .. rubric:: Routing-related + + - :class:`.CheckGateDirection` + - :class:`.CheckMap` + - :class:`.GateDirection` + - :class:`.StochasticSwap` + + .. rubric:: Translation-related + + - :class:`.BasisTranslator` + - :class:`.ContainsInstruction` + - :class:`.GatesInBasis` + - :class:`.UnitarySynthesis` + - :class:`.Unroll3qOrMore` + - :class:`.UnrollCustomDefinitions` + - :class:`.Unroller` + + .. rubric:: Optimization-related + + - :class:`.BarrierBeforeFinalMeasurements` + - :class:`.Depth` + - :class:`.FixedPoint` + - :class:`.Size` + - :class:`.Optimize1qGatesDecomposition` + - :class:`.CXCancellation` + - :class:`.RemoveResetInZeroState` + + These passes are most commonly used via the preset pass managers (those used + internally by :func:`.transpile` and :func:`.generate_preset_pass_manager`), + but are also available for other uses. These passes will now recurse into + control-flow operations where appropriate, updating or analysing the + internal blocks. diff --git a/test/python/transpiler/test_passmanager.py b/test/python/transpiler/test_passmanager.py index 49e96c8ca4fa..9f33ffbdcf5e 100644 --- a/test/python/transpiler/test_passmanager.py +++ b/test/python/transpiler/test_passmanager.py @@ -10,6 +10,8 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=missing-class-docstring,missing-function-docstring + """Test the passmanager logic""" import copy @@ -19,7 +21,7 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.circuit.library import U2Gate from qiskit.converters import circuit_to_dag -from qiskit.transpiler import PassManager, PropertySet +from qiskit.transpiler import PassManager, PropertySet, TransformationPass, FlowController from qiskit.transpiler.passes import CommutativeCancellation from qiskit.transpiler.passes import Optimize1qGates, Unroller from qiskit.test import QiskitTestCase @@ -114,3 +116,66 @@ def callback(**kwargs): self.assertIsInstance(calls[0]["time"], float) self.assertIsInstance(calls[0]["property_set"], PropertySet) self.assertEqual("MyCircuit", calls[1]["dag"].name) + + def test_to_flow_controller(self): + """Test that conversion to a `FlowController` works, and the result can be added to a + circuit and conditioned, with the condition only being called once.""" + + class DummyPass(TransformationPass): + def __init__(self, x): + super().__init__() + self.x = x + + def run(self, dag): + return dag + + def repeat(count): # pylint: disable=unused-argument + def condition(_): + nonlocal count + if not count: + return False + count -= 1 + return True + + return condition + + def make_inner(prefix): + inner = PassManager() + inner.append(DummyPass(f"{prefix} 1")) + inner.append(DummyPass(f"{prefix} 2"), condition=lambda _: False) + inner.append(DummyPass(f"{prefix} 3"), condition=lambda _: True) + inner.append(DummyPass(f"{prefix} 4"), do_while=repeat(1)) + return inner.to_flow_controller() + + self.assertIsInstance(make_inner("test"), FlowController) + + outer = PassManager() + outer.append(make_inner("first")) + outer.append(make_inner("second"), condition=lambda _: False) + # The intent of this `condition=repeat(1)` is to ensure that the outer condition is only + # checked once and not flattened into the inner controllers; an inner pass invalidating the + # condition should not affect subsequent passes once the initial condition was met. + outer.append(make_inner("third"), condition=repeat(1)) + + calls = [] + + def callback(pass_, **_): + self.assertIsInstance(pass_, DummyPass) + calls.append(pass_.x) + + outer.run(QuantumCircuit(), callback=callback) + + expected = [ + "first 1", + "first 3", + # it's a do-while loop, not a while, which is why the `repeat(1)` gives two calls + "first 4", + "first 4", + # If the outer pass-manager condition is called more than once, then only the first of + # the `third` passes will appear. + "third 1", + "third 3", + "third 4", + "third 4", + ] + self.assertEqual(calls, expected) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 7ca6aecdfbb7..79b8f27297d7 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -20,15 +20,15 @@ import numpy as np from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister -from qiskit.circuit import Qubit +from qiskit.circuit import Qubit, Gate, ControlFlowOp from qiskit.compiler import transpile, assemble from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspilerError +from qiskit.circuit.library import U2Gate, U3Gate, QuantumVolume, CXGate, CZGate, XGate from qiskit.transpiler.passes import ( ALAPScheduleAnalysis, PadDynamicalDecoupling, RemoveResetInZeroState, ) -from qiskit.circuit.library import U2Gate, U3Gate, XGate, QuantumVolume from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import ( FakeBelem, @@ -1227,3 +1227,148 @@ def get_translation_stage_plugin(self): for y in x["passes"] ] self.assertIn("RemoveResetInZeroState", post_translation_pass_list) + + +@ddt +class TestIntegrationControlFlow(QiskitTestCase): + """Integration tests for control-flow circuits through the preset pass managers.""" + + @data(0, 1) + def test_default_compilation(self, optimization_level): + """Test that a simple circuit with each type of control-flow passes a full transpilation + pipeline with the defaults.""" + + class CustomCX(Gate): + """Custom CX""" + + def __init__(self): + super().__init__("custom_cx", 2, []) + + def _define(self): + self._definition = QuantumCircuit(2) + self._definition.cx(0, 1) + + circuit = QuantumCircuit(6, 1) + circuit.h(0) + circuit.measure(0, 0) + circuit.cx(0, 1) + circuit.cz(0, 2) + circuit.append(CustomCX(), [1, 2], []) + with circuit.for_loop((1,)): + circuit.cx(0, 1) + circuit.cz(0, 2) + circuit.append(CustomCX(), [1, 2], []) + with circuit.if_test((circuit.clbits[0], True)) as else_: + circuit.cx(0, 1) + circuit.cz(0, 2) + circuit.append(CustomCX(), [1, 2], []) + with else_: + circuit.cx(3, 4) + circuit.cz(3, 5) + circuit.append(CustomCX(), [4, 5], []) + with circuit.while_loop((circuit.clbits[0], True)): + circuit.cx(3, 4) + circuit.cz(3, 5) + circuit.append(CustomCX(), [4, 5], []) + + coupling_map = CouplingMap.from_line(6) + + transpiled = transpile( + circuit, + basis_gates=["sx", "rz", "cx", "if_else", "for_loop", "while_loop"], + coupling_map=coupling_map, + optimization_level=optimization_level, + seed_transpiler=2022_10_04, + ) + # Tests of the complete validity of a circuit are mostly done at the indiviual pass level; + # here we're just checking that various passes do appear to have run. + self.assertIsInstance(transpiled, QuantumCircuit) + # Assert layout ran. + self.assertIsNot(getattr(transpiled, "_layout", None), None) + + def _visit_block(circuit, stack=None): + """Assert that every block contains at least one swap to imply that routing has run.""" + if stack is None: + # List of (instruction_index, block_index). + stack = () + seen_cx = 0 + for i, instruction in enumerate(circuit): + if isinstance(instruction.operation, ControlFlowOp): + for j, block in enumerate(instruction.operation.blocks): + _visit_block(block, stack + ((i, j),)) + elif isinstance(instruction.operation, CXGate): + seen_cx += 1 + # Assert unrolling ran. + self.assertNotIsInstance(instruction.operation, CustomCX) + # Assert translation ran. + self.assertNotIsInstance(instruction.operation, CZGate) + # There are three "natural" swaps in each block (one for each 2q operation), so if + # routing ran, we should see more than that. + self.assertGreater(seen_cx, 3, msg=f"no swaps in block at indices: {stack}") + + # Assert routing ran. + _visit_block(transpiled) + + @data(0, 1) + def test_allow_overriding_defaults(self, optimization_level): + """Test that the method options can be overridden.""" + circuit = QuantumCircuit(3, 1) + circuit.h(0) + circuit.measure(0, 0) + with circuit.for_loop((1,)): + circuit.h(0) + circuit.cx(0, 1) + circuit.cz(0, 2) + circuit.cx(1, 2) + + coupling_map = CouplingMap.from_line(3) + + calls = set() + + def callback(pass_, **_): + calls.add(pass_.name()) + + transpiled = transpile( + circuit, + basis_gates=["u3", "cx", "if_else", "for_loop", "while_loop"], + layout_method="trivial", + translation_method="unroller", + coupling_map=coupling_map, + optimization_level=optimization_level, + seed_transpiler=2022_10_04, + callback=callback, + ) + self.assertIsInstance(transpiled, QuantumCircuit) + self.assertIsNot(getattr(transpiled, "_layout", None), None) + + self.assertIn("TrivialLayout", calls) + self.assertIn("Unroller", calls) + self.assertNotIn("DenseLayout", calls) + self.assertNotIn("SabreLayout", calls) + self.assertNotIn("BasisTranslator", calls) + + @data(0, 1) + def test_invalid_methods_raise_on_control_flow(self, optimization_level): + """Test that trying to use an invalid method with control flow fails.""" + qc = QuantumCircuit(1) + with qc.for_loop((1,)): + qc.x(0) + + with self.assertRaisesRegex(TranspilerError, "Got layout_method="): + transpile(qc, layout_method="sabre", optimization_level=optimization_level) + with self.assertRaisesRegex(TranspilerError, "Got routing_method="): + transpile(qc, routing_method="lookahead", optimization_level=optimization_level) + with self.assertRaisesRegex(TranspilerError, "Got translation_method="): + transpile(qc, translation_method="synthesis", optimization_level=optimization_level) + with self.assertRaisesRegex(TranspilerError, "Got scheduling_method="): + transpile(qc, scheduling_method="alap", optimization_level=optimization_level) + + @data(2, 3) + def test_unsupported_levels_raise(self, optimization_level): + """Test that trying to use an invalid method with control flow fails.""" + qc = QuantumCircuit(1) + with qc.for_loop((1,)): + qc.x(0) + + with self.assertRaisesRegex(TranspilerError, "The optimizations in optimization_level="): + transpile(qc, optimization_level=optimization_level)