diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index c3b56c3bc7cc..5750c7fa6260 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 2023. # # 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 @@ -13,9 +13,12 @@ """Synthesize higher-level objects.""" +from typing import Optional from qiskit.converters import circuit_to_dag from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.target import Target +from qiskit.transpiler.coupling import CouplingMap from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError @@ -119,7 +122,27 @@ class HighLevelSynthesis(TransformationPass): ``default`` methods for all other high-level objects, including ``op_a``-objects. """ - def __init__(self, hls_config=None): + def __init__( + self, + hls_config: Optional[HLSConfig] = None, + coupling_map: Optional[CouplingMap] = None, + target: Optional[Target] = None, + use_qubit_indices: bool = False, + ): + """ + HighLevelSynthesis initializer. + + Args: + hls_config: Optional, the high-level-synthesis config that specifies synthesis methods + and parameters for various high-level-objects in the circuit. If it is not specified, + the default synthesis methods and parameters will be used. + coupling_map: Optional, directed graph represented as a coupling map. + target: Optional, the backend target to use for this pass. If it is specified, + it will be used instead of the coupling map. + use_qubit_indices: a flag indicating whether this synthesis pass is running before or after + the layout is set, that is, whether the qubit indices of higher-level-objects correspond + to qubit indices on the target backend. + """ super().__init__() if hls_config is not None: @@ -129,6 +152,11 @@ def __init__(self, hls_config=None): # to synthesize Operations (when available). self.hls_config = HLSConfig(True) self.hls_plugin_manager = HighLevelSynthesisPluginManager() + self._coupling_map = coupling_map + self._target = target + self._use_qubit_indices = use_qubit_indices + if target is not None: + self._coupling_map = self._target.build_coupling_map() def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the HighLevelSynthesis pass on `dag`. @@ -141,7 +169,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: Raises: TranspilerError: when the specified synthesis method is not available. """ - for node in dag.op_nodes(): if node.name in self.hls_config.methods.keys(): # the operation's name appears in the user-provided config, @@ -187,9 +214,17 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: else: plugin_method = plugin_specifier - # ToDo: similarly to UnitarySynthesis, we should pass additional parameters - # e.g. coupling_map to the synthesis algorithm. - decomposition = plugin_method.run(node.op, **plugin_args) + qubits = ( + [dag.find_bit(x).index for x in node.qargs] if self._use_qubit_indices else None + ) + + decomposition = plugin_method.run( + node.op, + coupling_map=self._coupling_map, + target=self._target, + qubits=qubits, + **plugin_args, + ) # The synthesis methods that are not suited for the given higher-level-object # will return None, in which case the next method in the list will be used. @@ -211,7 +246,7 @@ class DefaultSynthesisClifford(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" decomposition = synth_clifford_full(high_level_object) return decomposition @@ -224,7 +259,7 @@ class AGSynthesisClifford(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" decomposition = synth_clifford_ag(high_level_object) return decomposition @@ -241,7 +276,7 @@ class BMSynthesisClifford(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" if high_level_object.num_qubits <= 3: decomposition = synth_clifford_bm(high_level_object) @@ -258,7 +293,7 @@ class GreedySynthesisClifford(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" decomposition = synth_clifford_greedy(high_level_object) return decomposition @@ -272,7 +307,7 @@ class LayerSynthesisClifford(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" decomposition = synth_clifford_layers(high_level_object) return decomposition @@ -287,7 +322,7 @@ class LayerLnnSynthesisClifford(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" decomposition = synth_clifford_depth_lnn(high_level_object) return decomposition @@ -300,7 +335,7 @@ class DefaultSynthesisLinearFunction(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given LinearFunction.""" decomposition = synth_cnot_count_full_pmh(high_level_object.linear) return decomposition @@ -313,7 +348,7 @@ class KMSSynthesisLinearFunction(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given LinearFunction.""" decomposition = synth_cnot_depth_line_kms(high_level_object.linear) return decomposition @@ -326,7 +361,7 @@ class PMHSynthesisLinearFunction(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given LinearFunction.""" decomposition = synth_cnot_count_full_pmh(high_level_object.linear) return decomposition @@ -339,7 +374,7 @@ class KMSSynthesisPermutation(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" decomposition = synth_permutation_depth_lnn_kms(high_level_object.pattern) return decomposition @@ -352,7 +387,7 @@ class BasicSynthesisPermutation(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" decomposition = synth_permutation_basic(high_level_object.pattern) return decomposition @@ -365,7 +400,7 @@ class ACGSynthesisPermutation(HighLevelSynthesisPlugin): an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. """ - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" decomposition = synth_permutation_acg(high_level_object.pattern) return decomposition diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index 8ecbc4e08c77..3a801061de01 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -166,11 +166,25 @@ def run(self, unitary, **options): which defines the interface and contract for high-level synthesis plugins. The primary method is :meth:`~qiskit.transpiler.passes.synthesis.plugin.HighLevelSynthesisPlugin.run`. -It takes in a single positional argument, a "higher-level-object" to be -synthesized, which is any object of type :class:`~qiskit.circuit.Operation` +The positional argument ``high_level_object`` specifies the "higher-level-object" to +be synthesized, which is any object of type :class:`~qiskit.circuit.Operation` (including, for example, :class:`~qiskit.circuit.library.generalized_gates.linear_function.LinearFunction` or :class:`~qiskit.quantum_info.operators.symplectic.clifford.Clifford`). +The keyword argument ``target`` specifies the target backend, allowing the plugin +to access all target-specific information, +such as the coupling map, the supported gate set, and so on. The keyword argument +``coupling_map`` only specifies the coupling map, and is only used when ``target`` +is not specified. +The keyword argument ``qubits`` specifies the list of qubits over which the +higher-level-object is defined, in case the synthesis is done on the physical circuit. +The value of ``None`` indicates that the layout has not yet been chosen and the physical qubits +in the target or coupling map that this operation is operating on has not yet been determined. +Additionally, plugin-specific options and tunables can be specified via ``options``, +which is a free form configuration dictionary. +If your plugin has these configuration options you +should clearly document how a user should specify these configuration options +and how they're used as it's a free form field. The method :meth:`~qiskit.transpiler.passes.synthesis.plugin.HighLevelSynthesisPlugin.run` is expected to return a :class:`~qiskit.circuit.QuantumCircuit` object @@ -180,11 +194,6 @@ def run(self, unitary, **options): The actual synthesis of higher-level objects is performed by :class:`~qiskit.transpiler.passes.synthesis.high_level_synthesis.HighLevelSynthesis` transpiler pass. -In the near future, -:class:`~qiskit.transpiler.passes.synthesis.plugin.HighLevelSynthesisPlugin` -will be extended with additional information necessary to run this transpiler -pass, for instance whether the plugin supports and/or requires ``coupling_map`` -to perform synthesis. For the full details refer to the :class:`~qiskit.transpiler.passes.synthesis.plugin.HighLevelSynthesisPlugin` documentation for all the required fields. An example plugin class would look @@ -196,7 +205,7 @@ def run(self, unitary, **options): class SpecialSynthesisClifford(HighLevelSynthesisPlugin): - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): if higher_level_object.num_qubits <= 3: return synth_clifford_bm(high_level_object) else: @@ -544,13 +553,18 @@ class HighLevelSynthesisPlugin(abc.ABC): """ @abc.abstractmethod - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Operation. Args: high_level_object (Operation): The Operation to synthesize to a - :class:`~qiskit.dagcircuit.DAGCircuit` object - options: The optional kwargs. + :class:`~qiskit.dagcircuit.DAGCircuit` object. + coupling_map (CouplingMap): The coupling map of the backend + in case synthesis is done on a physical circuit. + target (Target): A target representing the target backend. + qubits (list): List of qubits over which the operation is defined + in case synthesis is done on a physical circuit. + options: Additional method-specific optional kwargs. Returns: QuantumCircuit: The quantum circuit representation of the Operation diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 56ffb74f5415..f02fa7e63665 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -217,7 +217,11 @@ def generate_unroll_3q( target=target, ) ) - unroll_3q.append(HighLevelSynthesis(hls_config=hls_config)) + unroll_3q.append( + HighLevelSynthesis( + hls_config=hls_config, coupling_map=None, target=target, use_qubit_indices=False + ) + ) unroll_3q.append(Unroll3qOrMore(target=target, basis_gates=basis_gates)) return unroll_3q @@ -422,7 +426,12 @@ def generate_translation_passmanager( method=unitary_synthesis_method, target=target, ), - HighLevelSynthesis(hls_config=hls_config), + HighLevelSynthesis( + hls_config=hls_config, + coupling_map=coupling_map, + target=target, + use_qubit_indices=True, + ), UnrollCustomDefinitions(sel, basis_gates=basis_gates, target=target), BasisTranslator(sel, basis_gates, target), ] @@ -440,7 +449,12 @@ def generate_translation_passmanager( min_qubits=3, target=target, ), - HighLevelSynthesis(hls_config=hls_config), + HighLevelSynthesis( + hls_config=hls_config, + coupling_map=coupling_map, + target=target, + use_qubit_indices=True, + ), Unroll3qOrMore(target=target, basis_gates=basis_gates), Collect2qBlocks(), Collect1qRuns(), @@ -456,7 +470,12 @@ def generate_translation_passmanager( method=unitary_synthesis_method, target=target, ), - HighLevelSynthesis(hls_config=hls_config), + HighLevelSynthesis( + hls_config=hls_config, + coupling_map=coupling_map, + target=target, + use_qubit_indices=True, + ), ] else: raise TranspilerError("Invalid translation method %s." % method) diff --git a/releasenotes/notes/add-target-to-hls.yaml b/releasenotes/notes/add-target-to-hls.yaml new file mode 100644 index 000000000000..27fd80050728 --- /dev/null +++ b/releasenotes/notes/add-target-to-hls.yaml @@ -0,0 +1,28 @@ +--- +features: + - | + Added the arguments ``coupling_map``, ``target`` and ``use_qubit_indices`` to + :class:`.HighLevelSynthesis` transpiler pass. The argument ``target`` specifies + the target backend, allowing the synthesis plugins called within the pass to + access all target-specific information, such as the coupling map and the supported + gate set. The argument ``coupling_map`` only specifies the coupling map, and is only + used when ``target`` is not specified. The argument ``use_qubit_indices`` indicates + whether the high-level-synthesis pass is running before or after the layout is set, + that is, whether the qubit indices of higher-level-objects correspond to qubit indices + on the target backend. + - | + Added the arguments ``coupling_map``, ``target`` and ``qubits`` to :class:`.HighLevelSynthesisPlugin`. + The positional argument ``target`` specifies the target backend, allowing the plugin + to access all target-specific information, + such as the coupling map, the supported gate set, and so on. The positional argument + ``coupling_map`` only specifies the coupling map, and is only used when ``target`` + is not specified. + The positional argument ``qubits`` specifies the list of qubits over which the + higher-level-object is defined, in case the synthesis is done on the physical circuit. + The value of ``None`` indicates that the layout has not yet been chosen. + + This enables a cleaner separation of synthesis plugins options into general interface options + for plugins (that is, ``coupling_map``, ``target``, and ``qubits``) and into plugin-specific options + (a free form configuration dictionary specified via ``options``). It is worthwhile to note that this + change is backward-compatible, if the options ``coupling_map``, etc. are not explicitly added to + the plugin's ``run()`` method, they will appear as part of ``options``. diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 19da830955f0..a387d7b6a0b9 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -19,9 +19,10 @@ from qiskit.circuit import QuantumCircuit, Operation from qiskit.test import QiskitTestCase -from qiskit.transpiler import PassManager +from qiskit.transpiler import PassManager, TranspilerError, CouplingMap from qiskit.transpiler.passes.synthesis.plugin import HighLevelSynthesisPlugin from qiskit.transpiler.passes.synthesis.high_level_synthesis import HighLevelSynthesis, HLSConfig +from qiskit.providers.fake_provider.fake_backend_v2 import FakeBackend5QV2 # In what follows, we create two simple operations OpA and OpB, that potentially mimic @@ -72,7 +73,7 @@ def num_clbits(self): class OpADefaultSynthesisPlugin(HighLevelSynthesisPlugin): """The default synthesis for opA""" - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): qc = QuantumCircuit(1) qc.id(0) return qc @@ -81,7 +82,7 @@ def run(self, high_level_object, **options): class OpARepeatSynthesisPlugin(HighLevelSynthesisPlugin): """The repeat synthesis for opA""" - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): if "n" not in options.keys(): return None @@ -94,7 +95,7 @@ def run(self, high_level_object, **options): class OpBSimpleSynthesisPlugin(HighLevelSynthesisPlugin): """The simple synthesis for OpB""" - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): qc = QuantumCircuit(2) qc.cx(0, 1) return qc @@ -110,7 +111,7 @@ class OpBAnotherSynthesisPlugin(HighLevelSynthesisPlugin): def __init__(self, num_swaps=1): self.num_swaps = num_swaps - def run(self, high_level_object, **options): + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): num_swaps = options.get("num_swaps", self.num_swaps) qc = QuantumCircuit(2) @@ -119,6 +120,28 @@ def run(self, high_level_object, **options): return qc +class OpAPluginNeedsCouplingMap(HighLevelSynthesisPlugin): + """Synthesis plugins for OpA that needs a coupling map to be run.""" + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if coupling_map is None: + raise TranspilerError("Coupling map should be specified!") + qc = QuantumCircuit(1) + qc.id(0) + return qc + + +class OpAPluginNeedsQubits(HighLevelSynthesisPlugin): + """Synthesis plugins for OpA that needs ``qubits`` to be specified.""" + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if qubits is None: + raise TranspilerError("Qubits should be specified!") + qc = QuantumCircuit(1) + qc.id(0) + return qc + + class MockPluginManager: """Mocks the functionality of HighLevelSynthesisPluginManager, without actually depending on the stevedore extension manager. @@ -129,9 +152,14 @@ def __init__(self): "op_a.default": OpADefaultSynthesisPlugin, "op_a.repeat": OpARepeatSynthesisPlugin, "op_b.simple": OpBSimpleSynthesisPlugin, + "op_a.needs_coupling_map": OpAPluginNeedsCouplingMap, + "op_a.needs_qubits": OpAPluginNeedsQubits, } - self.plugins_by_op = {"op_a": ["default", "repeat"], "op_b": ["simple"]} + self.plugins_by_op = { + "op_a": ["default", "repeat", "needs_coupling_map", "needs_qubits"], + "op_b": ["simple"], + } def method_names(self, op_name): """Returns plugin methods for op_name.""" @@ -364,6 +392,75 @@ def test_synthesis_using_alternate_short_form(self): ops = tqc.count_ops() self.assertEqual(ops["swap"], 6) + def test_coupling_map_gets_passed_to_plugins(self): + """Check that passing coupling map works correctly.""" + qc = self.create_circ() + mock_plugin_manager = MockPluginManager + with unittest.mock.patch( + "qiskit.transpiler.passes.synthesis.high_level_synthesis.HighLevelSynthesisPluginManager", + wraps=mock_plugin_manager, + ): + hls_config = HLSConfig(op_a=["needs_coupling_map"]) + pm_bad = PassManager([HighLevelSynthesis(hls_config=hls_config)]) + pm_good = PassManager( + [ + HighLevelSynthesis( + hls_config=hls_config, coupling_map=CouplingMap.from_line(qc.num_qubits) + ) + ] + ) + + # HighLevelSynthesis is initialized without a coupling map, but calling a plugin that + # raises a TranspilerError without the coupling map. + with self.assertRaises(TranspilerError): + pm_bad.run(qc) + + # Now HighLevelSynthesis is initialized with a coupling map. + pm_good.run(qc) + + def test_target_gets_passed_to_plugins(self): + """Check that passing target (and constructing coupling map from the target) + works correctly. + """ + qc = self.create_circ() + mock_plugin_manager = MockPluginManager + with unittest.mock.patch( + "qiskit.transpiler.passes.synthesis.high_level_synthesis.HighLevelSynthesisPluginManager", + wraps=mock_plugin_manager, + ): + hls_config = HLSConfig(op_a=["needs_coupling_map"]) + pm_good = PassManager( + [HighLevelSynthesis(hls_config=hls_config, target=FakeBackend5QV2().target)] + ) + + # HighLevelSynthesis is initialized with target. + pm_good.run(qc) + + def test_qubits_get_passed_to_plugins(self): + """Check that setting ``use_qubit_indices`` works correctly.""" + qc = self.create_circ() + mock_plugin_manager = MockPluginManager + with unittest.mock.patch( + "qiskit.transpiler.passes.synthesis.high_level_synthesis.HighLevelSynthesisPluginManager", + wraps=mock_plugin_manager, + ): + hls_config = HLSConfig(op_a=["needs_qubits"]) + pm_use_qubits_false = PassManager( + [HighLevelSynthesis(hls_config=hls_config, use_qubit_indices=False)] + ) + pm_use_qubits_true = PassManager( + [HighLevelSynthesis(hls_config=hls_config, use_qubit_indices=True)] + ) + + # HighLevelSynthesis is initialized with use_qubit_indices=False, which means synthesis + # plugin should see qubits=None and raise a transpiler error. + with self.assertRaises(TranspilerError): + pm_use_qubits_false.run(qc) + + # HighLevelSynthesis is initialized with use_qubit_indices=True, which means synthesis + # plugin should see qubits and complete without errors. + pm_use_qubits_true.run(qc) + if __name__ == "__main__": unittest.main()