diff --git a/constraints.txt b/constraints.txt index 78044663fb65..e2b819502ef3 100644 --- a/constraints.txt +++ b/constraints.txt @@ -4,6 +4,7 @@ decorator==4.4.2 jax==0.2.13 jaxlib==0.1.67 networkx==2.5 +importlib-metadata==4.6.4 # jsonschema pinning needed due nbformat==5.1.3 using deprecated behaviour in # 4.0+. The pin can be removed after nbformat is updated. jsonschema==3.2.0 diff --git a/docs/apidocs/terra.rst b/docs/apidocs/terra.rst index f2a6382c3357..19f70f9ce72e 100644 --- a/docs/apidocs/terra.rst +++ b/docs/apidocs/terra.rst @@ -31,6 +31,7 @@ Qiskit Terra API Reference transpiler transpiler_passes transpiler_preset + transpiler_plugins utils opflow algorithms diff --git a/docs/apidocs/transpiler_plugins.rst b/docs/apidocs/transpiler_plugins.rst new file mode 100644 index 000000000000..33646fe2d58f --- /dev/null +++ b/docs/apidocs/transpiler_plugins.rst @@ -0,0 +1,6 @@ +.. _qiskit-transpiler-plugins: + +.. automodule:: qiskit.transpiler.passes.synthesis.plugin + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 68bba04d8264..8f8a2e2f621e 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -65,6 +65,7 @@ def transpile( pass_manager: Optional[PassManager] = None, callback: Optional[Callable[[BasePass, DAGCircuit, float, PropertySet, int], Any]] = None, output_name: Optional[Union[str, List[str]]] = None, + unitary_synthesis_method: str = "default", ) -> Union[QuantumCircuit, List[QuantumCircuit]]: """Transpile one or more circuits, according to some desired transpilation targets. @@ -215,6 +216,10 @@ def callback_func(**kwargs): output_name: A list with strings to identify the output circuits. The length of the list should be exactly the length of the ``circuits`` parameter. + unitary_synthesis_method (str): The name of the unitary synthesis + method to use. By default 'default' is used, which is the only + method included with qiskit. If you have installed any unitary + synthesis plugins you can use the name exported by the plugin. Returns: The transpiled circuit(s). @@ -249,6 +254,7 @@ def callback_func(**kwargs): routing_method=routing_method, translation_method=translation_method, approximation_degree=approximation_degree, + unitary_synthesis_method=unitary_synthesis_method, backend=backend, ) @@ -294,6 +300,7 @@ def callback_func(**kwargs): callback, output_name, timing_constraints, + unitary_synthesis_method, ) _check_circuits_coupling_map(circuits, transpile_args, backend) @@ -476,6 +483,7 @@ def _parse_transpile_args( callback, output_name, timing_constraints, + unitary_synthesis_method, ) -> List[Dict]: """Resolve the various types of args allowed to the transpile() function through duck typing, overriding args, etc. Refer to the transpile() docstring for details on @@ -508,6 +516,9 @@ def _parse_transpile_args( routing_method = _parse_routing_method(routing_method, num_circuits) translation_method = _parse_translation_method(translation_method, num_circuits) approximation_degree = _parse_approximation_degree(approximation_degree, num_circuits) + unitary_synthesis_method = _parse_unitary_synthesis_method( + unitary_synthesis_method, num_circuits + ) seed_transpiler = _parse_seed_transpiler(seed_transpiler, num_circuits) optimization_level = _parse_optimization_level(optimization_level, num_circuits) output_name = _parse_output_name(output_name, circuits) @@ -542,6 +553,7 @@ def _parse_transpile_args( "callback": callback, "backend_num_qubits": backend_num_qubits, "faulty_qubits_map": faulty_qubits_map, + "unitary_synthesis_method": unitary_synthesis_method, } ): transpile_args = { @@ -559,6 +571,7 @@ def _parse_transpile_args( approximation_degree=kwargs["approximation_degree"], timing_constraints=kwargs["timing_constraints"], seed_transpiler=kwargs["seed_transpiler"], + unitary_synthesis_method=kwargs["unitary_synthesis_method"], ), "optimization_level": kwargs["optimization_level"], "output_name": kwargs["output_name"], @@ -818,6 +831,12 @@ def _parse_approximation_degree(approximation_degree, num_circuits): return approximation_degree +def _parse_unitary_synthesis_method(unitary_synthesis_method, num_circuits): + if not isinstance(unitary_synthesis_method, list): + unitary_synthesis_method = [unitary_synthesis_method] * num_circuits + return unitary_synthesis_method + + def _parse_seed_transpiler(seed_transpiler, num_circuits): if not isinstance(seed_transpiler, list): seed_transpiler = [seed_transpiler] * num_circuits diff --git a/qiskit/test/base.py b/qiskit/test/base.py index af9708083375..b1181a9a5be9 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -221,6 +221,7 @@ def setUpClass(cls): "test.python.quantum_info.operators.channel.test_stinespring", "test.python.quantum_info.operators.symplectic.test_sparse_pauli_op", "test.python.quantum_info.operators.channel.test_ptm", + "importlib_metadata", ] for mod in allow_DeprecationWarning_modules: warnings.filterwarnings("default", category=DeprecationWarning, module=mod) diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index e1c398d93588..97f749ca3a4a 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -197,6 +197,7 @@ # synthesis from .synthesis import UnitarySynthesis +from .synthesis import unitary_synthesis_plugin_names # calibration from .calibration import PulseGates diff --git a/qiskit/transpiler/passes/synthesis/__init__.py b/qiskit/transpiler/passes/synthesis/__init__.py index 1e4dc6518fa4..57e389f44992 100644 --- a/qiskit/transpiler/passes/synthesis/__init__.py +++ b/qiskit/transpiler/passes/synthesis/__init__.py @@ -13,3 +13,4 @@ """Module containing transpiler synthesis passes.""" from .unitary_synthesis import UnitarySynthesis +from .plugin import unitary_synthesis_plugin_names diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py new file mode 100644 index 000000000000..8d8fdc585c46 --- /dev/null +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -0,0 +1,365 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +==================================================================== +Synthesis Plugins (:mod:`qiskit.transpiler.passes.synthesis.plugin`) +==================================================================== + +.. currentmodule:: qiskit.transpiler.passes.synthesis.plugin + +This module defines the plugin interfaces for the synthesis transpiler passes +in Qiskit. These provide a hook point for external python packages to implement +their own synthesis techniques and have them seamlessly exposed as opt-in +options to users when they run :func:`~qiskit.compiler.transpile`. + +The plugin interfaces are built using setuptools +`entry points `__ +which enable packages external to qiskit to advertise they include a synthesis +plugin. + +Writing Plugins +=============== + +Unitary Synthesis Plugins +------------------------- + +To write a unitary synthesis plugin there are 2 main steps. The first step is +to create a subclass of the abstract plugin class: +:class:`~qiskit.transpiler.passes.synthesis.plugin.UnitarySynthesisPlugin`. +The plugin class defines the interface and contract for unitary synthesis +plugins. The primary method is +:meth:`~qiskit.transpiler.passes.synthesis.plugin.UnitarySynthesisPlugin.run` +which takes in a single positional argument, a unitary matrix as a numpy array, +and is expected to return a :class:`~qiskit.dagcircuit.DAGCircuit` object +representing the synthesized circuit from that unitary matrix. Then to inform +the Qiskit transpiler about what information is necessary for the pass there +are several required property methods that need to be implemented such as +``supports_basis_gates`` and ``supports_coupling_map`` depending on whether the +plugin supports and/or requires that input to perform synthesis. For the full +details refer to the +:class:`~qiskit.transpiler.passes.synthesis.plugin.UnitarySynthesisPlugin` +documentation for all the required fields. An example plugin class would look +something like:: + + from qiskit.transpiler.passes.synthesis import plugin + from qiskit_plugin_pkg.synthesis import generate_dag_circuit_from_matrix + + + class SpecialUnitarySynthesis(plugin.UnitarySynthesisPlugin): + @property + def supports_basis_gates(self): + return True + + @property + def supports_coupling_map(self): + return False + + @property + def supports_natural_direction(self): + return False + + @property + def supports_pulse_optimize(self): + return False + + @property + def supports_gate_lengths(self): + return False + + @property + def supports_gate_errors(self): + return False + + @property + def min_qubits(self): + return None + + @property + def max_qubits(self): + return None + + @property + def supported_bases(self): + return None + + def run(self, unitary, **options): + basis_gates = options['basis_gates'] + dag_circuit = generate_dag_circuit_from_matrix(unitary, basis_gates) + return dag_circuit + +If for some reason the available inputs to the +:meth:`~qiskit.transpiler.passes.synthesis.plugin.UnitarySynthesisPlugin.run` +method are insufficient please open an issue and we can discuss expanding the +plugin interface with new opt-in inputs that can be added in a backwards +compatible manner for future releases. Do note though that this plugin interface +is considered stable and guaranteed to not change in a breaking manner. If +changes are needed (for example to expand the available optional input options) +it will be done in a way that will **not** require changes from existing +plugins. + +.. note:: + + All methods prefixed with ``supports_`` are reserved on a + ``UnitarySynthesisPlugin`` derived class for part of the interface. You + should not define any custom ``supports_*`` methods on a subclass that + are not defined in the abstract class. + + +The second step is to expose the +:class:`~qiskit.transpiler.passes.synthesis.plugin.UnitarySynthesisPlugin` as +a setuptools entry point in the package metadata. This is done by simply adding +an ``entry_points`` entry to the ``setuptools.setup`` call in the ``setup.py`` +for the plugin package with the necessary entry points under the +``qiskit.unitary_synthesis`` namespace. For example:: + + entry_points = { + 'qiskit.unitary_synthesis': [ + 'special = qiskit_plugin_pkg.module.plugin:SpecialUnitarySynthesis', + ] + }, + +(note that the entry point ``name = path`` is a single string not a Python +expression). There isn't a limit to the number of plugins a single package can +include as long as each plugin has a unique name. So a single package can +expose multiple plugins if necessary. The name ``default`` is used by Qiskit +itself and can't be used in a plugin. + +Using Plugins +============= + +To use a plugin all you need to do is install the package that includes a +synthesis plugin. Then Qiskit will automatically discover the installed +plugins and expose them as valid options for the appropriate +:func:`~qiskit.compiler.transpile` kwargs and pass constructors. If there are +any installed plugins which can't be loaded/imported this will be logged to +Python logging. + +To get the installed list of installed unitary synthesis plugins you can use the +:func:`qiskit.transpiler.passes.synthesis.plugin.unitary_synthesis_plugin_names` +function. + +Plugin API +========== + +Unitary Synthesis Plugins +------------------------- + +.. autosummary:: + :toctree: ../stubs/ + + UnitarySynthesisPlugin + UnitarySynthesisPluginManager + unitary_synthesis_plugin_names + +""" + +import abc + +import stevedore + + +class UnitarySynthesisPlugin(abc.ABC): + """Abstract unitary synthesis plugin class + + This abstract class defines the interface for unitary synthesis plugins. + """ + + @property + @abc.abstractmethod + def max_qubits(self): + """Return the maximum number of qubits the unitary synthesis plugin supports. + + If the size of the unitary to be synthesized exceeds this value the + ``default`` plugin will be used. If there is no upper bound return + ``None`` and all unitaries (``>= min_qubits`` if it's defined) will be + passed to this plugin when it's enabled. + """ + pass + + @property + @abc.abstractmethod + def min_qubits(self): + """Return the minimum number of qubits the unitary synthesis plugin supports. + + If the size of the unitary to be synthesized is below this value the + ``default`` plugin will be used. If there is no lower bound return + ``None`` and all unitaries (``<= max_qubits`` if it's defined) will be + passed to this plugin when it's enabled. + """ + pass + + @property + @abc.abstractmethod + def supports_basis_gates(self): + """Return whether the plugin supports taking ``basis_gates`` + + If this returns ``True`` the plugin's ``run()`` method will be + passed a ``basis_gates`` kwarg with a list of gate names the target + backend supports. For example, ``['sx', 'x', 'cx', 'id', 'rz']``.""" + pass + + @property + @abc.abstractmethod + def supports_coupling_map(self): + """Return whether the plugin supports taking ``coupling_map`` + + If this returns ``True`` the plugin's ``run()`` method will receive + one kwarg ``coupling_map``. The ``coupling_map`` kwarg will be set to a + tuple with the first element being a + :class:`~qiskit.transpiler.CouplingMap` object representing the qubit + connectivity of the target backend, the second element will be a list + of integers that represent the qubit indices in the coupling map that + unitary is on. Note that if the target backend doesn't have a coupling + map set, the ``coupling_map`` kwarg's value will be ``(None, qubit_indices)``. + """ + pass + + @property + @abc.abstractmethod + def supports_natural_direction(self): + """Return whether the plugin supports a toggle for considering + directionality of 2-qubit gates as ``natural_direction``. + + Refer to the documentation for :class:`~qiskit.transpiler.passes.UnitarySynthesis` + for the possible values and meaning of these values. + """ + pass + + @property + @abc.abstractmethod + def supports_pulse_optimize(self): + """Return whether the plugin supports a toggle to optimize pulses + during synthesis as ``pulse_optimize``. + + Refer to the documentation for :class:`~qiskit.transpiler.passes.UnitarySynthesis` + for the possible values and meaning of these values. + """ + pass + + @property + @abc.abstractmethod + def supports_gate_lengths(self): + """Return whether the plugin supports taking ``gate_lengths`` + + ``gate_lengths`` will be a dictionary in the form of + ``{gate_name: {(qubit_1, qubit_2): length}}``. For example:: + + { + 'sx': {(0,): 0.0006149355812506126, (1,): 0.0006149355812506126}, + 'cx': {(0, 1): 0.012012477900732316, (1, 0): 5.191111111111111e-07} + } + + where the ``length`` value is in units of seconds. + + Do note that this dictionary might not be complete or could be empty + as it depends on the target backend reporting gate lengths on every + gate for each qubit. + """ + pass + + @property + @abc.abstractmethod + def supports_gate_errors(self): + """Return whether the plugin supports taking ``gate_errors`` + + ``gate_errors`` will be a dictionary in the form of + ``{gate_name: {(qubit_1, qubit_2): error}}``. For example:: + + { + 'sx': {(0,): 0.0006149355812506126, (1,): 0.0006149355812506126}, + 'cx': {(0, 1): 0.012012477900732316, (1, 0): 5.191111111111111e-07} + } + + Do note that this dictionary might not be complete or could be empty + as it depends on the target backend reporting gate errors on every + gate for each qubit. The gate error rates reported in ``gate_errors`` + are provided by the target device ``Backend`` object and the exact + meaning might be different depending on the backend. + """ + pass + + @property + @abc.abstractmethod + def supported_bases(self): + """Returns a dictionary of supported bases for synthesis + + This is expected to return a dictionary where the key is a string + basis and the value is a list of gate names that the basis works in. + If the synthesis method doesn't support multiple bases this should + return ``None``. For example:: + + { + "XZX": ["rz", "rx"], + "XYX": ["rx", "ry"], + } + + If a dictionary is returned by this method the run kwargs will be + passed a parameter ``matched_basis`` which contains a list of the + basis strings (i.e. keys in the dictionary) which match the target basis + gate set for the transpilation. If no entry in the dictionary matches + the target basis gate set then the ``matched_basis`` kwarg will be set + to an empty list, and a plugin can choose how to deal with the target + basis gate set not matching the plugin's capabilities. + """ + pass + + @abc.abstractmethod + def run(self, unitary, **options): + """Run synthesis for the given unitary matrix + + Args: + unitary (numpy.ndarray): The unitary matrix to synthesize to a + :class:`~qiskit.dagcircuit.DAGCircuit` object + options: The optional kwargs that are passed based on the output + the ``support_*`` methods on the class. Refer to the + documentation for these methods on + :class:`~qiskit.transpiler.passes.synthesis.plugin.UnitarySynthesisPlugin` + to see what the keys and values are. + + Returns: + DAGCircuit: The dag circuit representation of the unitary. Alternatively, you can return + a tuple of the form ``(dag, wires)`` where ``dag`` is the dag circuit representation of + the circuit representation of the unitary and ``wires`` is the mapping wires to use for + :meth:`qiskit.dagcircuit.DAGCircuit.substitute_node_with_dag`. If you return a tuple + and ``wires`` is ``None`` this will behave just as if only a + :class:`~qiskit.dagcircuit.DAGCircuit` was returned. Additionally if this returns + ``None`` no substitution will be made. + + """ + pass + + +class UnitarySynthesisPluginManager: + """Unitary Synthesis plugin manager class + + This class tracks the installed plugins, it has a single property, + ``ext_plugins`` which contains a list of stevedore plugin objects. + """ + + def __init__(self): + self.ext_plugins = stevedore.ExtensionManager( + "qiskit.unitary_synthesis", invoke_on_load=True, propagate_map_exceptions=True + ) + + +def unitary_synthesis_plugin_names(): + """Return a list of installed unitary synthesis plugin names + + Returns: + list: A list of the installed unitary synthesis plugin names. The plugin names are valid + values for the :func:`~qiskit.compiler.transpile` kwarg ``unitary_synthesis_method``. + """ + # NOTE: This is not a shared global instance to avoid an import cycle + # at load time for the default plugin. + plugins = UnitarySynthesisPluginManager() + return plugins.ext_plugins.names() diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index cab2573cec1b..2f8739488e10 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -25,8 +25,8 @@ from qiskit.quantum_info.synthesis import one_qubit_decompose from qiskit.quantum_info.synthesis.two_qubit_decompose import TwoQubitBasisDecomposer from qiskit.circuit.library.standard_gates import iSwapGate, CXGate, CZGate, RXXGate, ECRGate +from qiskit.transpiler.passes.synthesis import plugin from qiskit.providers.models import BackendProperties -from qiskit.providers.exceptions import BackendPropertyError def _choose_kak_gate(basis_gates): @@ -53,12 +53,31 @@ def _choose_euler_basis(basis_gates): basis_set = set(basis_gates or []) for basis, gates in one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES.items(): + if set(gates).issubset(basis_set): return basis return None +def _choose_bases(basis_gates, basis_dict=None): + """Find the matching basis string keys from the list of basis gates from the backend.""" + if basis_gates is None: + basis_set = set() + else: + basis_set = set(basis_gates) + + if basis_dict is None: + basis_dict = one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES + + out_basis = [] + for basis, gates in basis_dict.items(): + if set(gates).issubset(basis_set): + out_basis.append(basis) + + return out_basis + + class UnitarySynthesis(TransformationPass): """Synthesize gates according to their basis gates.""" @@ -71,6 +90,7 @@ def __init__( pulse_optimize: Union[bool, None] = None, natural_direction: Union[bool, None] = None, synth_gates: Union[List[str], None] = None, + method: str = "default", ): """Synthesize unitaries over some basis gates. @@ -110,11 +130,14 @@ def __init__( `pulse_optimize` is False or None, default to ['unitary']. If None and `pulse_optimzie` == True, default to ['unitary', 'swap'] + method (str): The unitary synthesis method plugin to use. """ super().__init__() self._basis_gates = basis_gates self._approximation_degree = approximation_degree + self.method = method + self.plugins = plugin.UnitarySynthesisPluginManager() self._coupling_map = coupling_map self._backend_props = backend_props self._pulse_optimize = pulse_optimize @@ -137,99 +160,237 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: Output dag with UnitaryGates synthesized to target basis. Raises: - TranspilerError: - 1. pulse_optimize is True but pulse optimal decomposition is not known - for requested basis. - 2. pulse_optimize is True and natural_direction is True but a preferred - gate direction can't be determined from the coupling map or the - relative gate lengths. + TranspilerError: if a 'method' was specified for the class and is not + found in the installed plugins list. The list of installed + plugins can be queried with + :func:`~qiskit.transpiler.passes.synthesis.plugin.unitary_synthesis_plugin_names` """ + if self.method not in self.plugins.ext_plugins: + raise TranspilerError("Specified method: %s not found in plugin list" % self.method) + default_method = self.plugins.ext_plugins["default"].obj + plugin_method = self.plugins.ext_plugins[self.method].obj + if plugin_method.supports_coupling_map: + dag_bit_indices = {bit: idx for idx, bit in enumerate(dag.qubits)} + kwargs = {} + if plugin_method.supports_basis_gates: + kwargs["basis_gates"] = self._basis_gates + if plugin_method.supports_natural_direction: + kwargs["natural_direction"] = self._natural_direction + if plugin_method.supports_pulse_optimize: + kwargs["pulse_optimize"] = self._pulse_optimize + if plugin_method.supports_gate_lengths: + kwargs["gate_lengths"] = _build_gate_lengths(self._backend_props) + if plugin_method.supports_gate_errors: + kwargs["gate_errors"] = _build_gate_errors(self._backend_props) + supported_bases = plugin_method.supported_bases + if supported_bases is not None: + kwargs["matched_basis"] = _choose_bases(self._basis_gates, supported_bases) + + # Handle approximation degree as a special case for backwards compatibility, it's + # not part of the plugin interface and only something needed for the default + # pass. + default_method._approximation_degree = self._approximation_degree + if self.method == "default": + plugin_method._approximation_degree = self._approximation_degree + + for node in dag.named_nodes(*self._synth_gates): + if plugin_method.supports_coupling_map: + kwargs["coupling_map"] = ( + self._coupling_map, + [dag_bit_indices[x] for x in node.qargs], + ) + synth_dag = None + unitary = node.op.to_matrix() + n_qubits = len(node.qargs) + if (plugin_method.max_qubits is not None and n_qubits > plugin_method.max_qubits) or ( + plugin_method.min_qubits is not None and n_qubits < plugin_method.min_qubits + ): + synth_dag = default_method.run(unitary, **kwargs) + else: + synth_dag = plugin_method.run(unitary, **kwargs) + if synth_dag is not None: + if isinstance(synth_dag, tuple): + dag.substitute_node_with_dag(node, synth_dag[0], wires=synth_dag[1]) + else: + dag.substitute_node_with_dag(node, synth_dag) + return dag + + +def _build_gate_lengths(props): + gate_lengths = {} + if props: + for gate in props._gates: + gate_lengths[gate] = {} + for k, v in props._gates[gate].items(): + length = v.get("gate_length") + if length: + gate_lengths[gate][k] = length[0] + if not gate_lengths[gate]: + del gate_lengths[gate] + return gate_lengths + + +def _build_gate_errors(props): + gate_errors = {} + if props: + for gate in props._gates: + gate_errors[gate] = {} + for k, v in props._gates[gate].items(): + error = v.get("gate_error") + if error: + gate_errors[gate][k] = error[0] + if not gate_errors[gate]: + del gate_errors[gate] + return gate_errors + + +class DefaultUnitarySynthesis(plugin.UnitarySynthesisPlugin): + """The default unitary synthesis plugin.""" + + @property + def supports_basis_gates(self): + return True + + @property + def supports_coupling_map(self): + return True + + @property + def supports_natural_direction(self): + return True + + @property + def supports_pulse_optimize(self): + return True + + @property + def supports_gate_lengths(self): + return True + + @property + def supports_gate_errors(self): + return True + + @property + def max_qubits(self): + return None + + @property + def min_qubits(self): + return None + + @property + def supported_bases(self): + return None + + def run(self, unitary, **options): + # Approximation degree is set directly as an attribute on the + # instance by the UnitarySynthesis pass here as it's not part of + # plugin interface. However if for some reason it's not set assume + # it's 1. + approximation_degree = getattr(self, "_approximation_degree", 1) + basis_gates = options["basis_gates"] + coupling_map = options["coupling_map"][0] + natural_direction = options["natural_direction"] + pulse_optimize = options["pulse_optimize"] + gate_lengths = options["gate_lengths"] + gate_errors = options["gate_errors"] + qubits = options["coupling_map"][1] + + euler_basis = _choose_euler_basis(basis_gates) + kak_gate = _choose_kak_gate(basis_gates) - euler_basis = _choose_euler_basis(self._basis_gates) - kak_gate = _choose_kak_gate(self._basis_gates) decomposer1q, decomposer2q = None, None if euler_basis is not None: decomposer1q = one_qubit_decompose.OneQubitEulerDecomposer(euler_basis) if kak_gate is not None: decomposer2q = TwoQubitBasisDecomposer( - kak_gate, euler_basis=euler_basis, pulse_optimize=self._pulse_optimize + kak_gate, euler_basis=euler_basis, pulse_optimize=pulse_optimize ) - for node in dag.named_nodes(*self._synth_gates): - if self._basis_gates and node.name in self._basis_gates: - continue - synth_dag = None - wires = None - if len(node.qargs) == 1: - if decomposer1q is None: - continue - synth_dag = circuit_to_dag(decomposer1q._decompose(node.op.to_matrix())) - elif len(node.qargs) == 2: - if decomposer2q is None: - continue - synth_dag, wires = self._synth_natural_direction(node, dag, decomposer2q) - else: - synth_dag = circuit_to_dag(isometry.Isometry(node.op.to_matrix(), 0, 0).definition) - - dag.substitute_node_with_dag(node, synth_dag, wires=wires) + synth_dag = None + wires = None + if unitary.shape == (2, 2): + if decomposer1q is None: + return None + synth_dag = circuit_to_dag(decomposer1q._decompose(unitary)) + elif unitary.shape == (4, 4): + if decomposer2q is None: + return None + synth_dag, wires = self._synth_natural_direction( + unitary, + coupling_map, + qubits, + decomposer2q, + gate_lengths, + gate_errors, + natural_direction, + approximation_degree, + pulse_optimize, + ) + else: + synth_dag = circuit_to_dag(isometry.Isometry(unitary, 0, 0).definition) - return dag + return synth_dag, wires - def _synth_natural_direction(self, node, dag, decomposer2q): - layout = self.property_set["layout"] + def _synth_natural_direction( + self, + su4_mat, + coupling_map, + qubits, + decomposer2q, + gate_lengths, + gate_errors, + natural_direction, + approximation_degree, + pulse_optimize, + ): preferred_direction = None synth_direction = None physical_gate_fidelity = None wires = None - dag_qubit_index = {qubit: index for index, qubit in enumerate(dag.qubits)} - if self._natural_direction in {None, True} and layout and self._coupling_map: - neighbors0 = self._coupling_map.neighbors(dag_qubit_index[node.qargs[0]]) - zero_one = dag_qubit_index[node.qargs[1]] in neighbors0 - neighbors1 = self._coupling_map.neighbors(dag_qubit_index[node.qargs[1]]) - one_zero = dag_qubit_index[node.qargs[0]] in neighbors1 + if natural_direction in {None, True} and coupling_map: + neighbors0 = coupling_map.neighbors(qubits[0]) + zero_one = qubits[1] in neighbors0 + neighbors1 = coupling_map.neighbors(qubits[1]) + one_zero = qubits[0] in neighbors1 if zero_one and not one_zero: preferred_direction = [0, 1] if one_zero and not zero_one: preferred_direction = [1, 0] if ( - self._natural_direction in {None, True} + natural_direction in {None, True} and preferred_direction is None - and layout - and self._backend_props + and gate_lengths + and gate_errors ): len_0_1 = inf len_1_0 = inf - try: - len_0_1 = self._backend_props.gate_length( - decomposer2q.gate.name, - [dag_qubit_index[node.qargs[0]], dag_qubit_index[node.qargs[1]]], - ) - except BackendPropertyError: - pass - try: - len_1_0 = self._backend_props.gate_length( - decomposer2q.gate.name, - [dag_qubit_index[node.qargs[1]], dag_qubit_index[node.qargs[0]]], - ) - except BackendPropertyError: - pass - - if len_0_1 < len_1_0: - preferred_direction = [0, 1] - elif len_1_0 < len_0_1: - preferred_direction = [1, 0] - if preferred_direction: - physical_gate_fidelity = 1 - self._backend_props.gate_error( - "cx", [dag_qubit_index[node.qargs[i]] for i in preferred_direction] - ) - if self._natural_direction is True and preferred_direction is None: + twoq_gate_lengths = gate_lengths.get(decomposer2q.gate.name) + if twoq_gate_lengths: + len_0_1 = twoq_gate_lengths.get((qubits[0], qubits[1]), inf) + len_1_0 = twoq_gate_lengths.get((qubits[1], qubits[0]), inf) + if len_0_1 < len_1_0: + preferred_direction = [0, 1] + elif len_1_0 < len_0_1: + preferred_direction = [1, 0] + if preferred_direction: + twoq_gate_errors = gate_errors.get("cx") + gate_error = twoq_gate_errors.get( + (qubits[preferred_direction[0]], qubits[preferred_direction[1]]) + ) + if gate_error: + physical_gate_fidelity = 1 - gate_error + if natural_direction is True and preferred_direction is None: raise TranspilerError( - f"No preferred direction of {node.name} gate " + f"No preferred direction of gate on qubits {qubits} " "could be determined from coupling map or " "gate lengths." ) - basis_fidelity = self._approximation_degree or physical_gate_fidelity - su4_mat = node.op.to_matrix() + if approximation_degree is not None: + basis_fidelity = approximation_degree + else: + basis_fidelity = physical_gate_fidelity synth_circ = decomposer2q(su4_mat, basis_fidelity=basis_fidelity) synth_dag = circuit_to_dag(synth_circ) @@ -243,7 +404,7 @@ def _synth_natural_direction(self, node, dag, decomposer2q): ] if ( preferred_direction - and self._pulse_optimize in {True, None} + and pulse_optimize in {True, None} and synth_direction != preferred_direction ): su4_mat_mm = deepcopy(su4_mat) diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 4705005cb9f6..1da250f71e1a 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -31,6 +31,7 @@ def __init__( approximation_degree=None, seed_transpiler=None, timing_constraints=None, + unitary_synthesis_method="default", ): """Initialize a PassManagerConfig object @@ -58,6 +59,9 @@ def __init__( seed_transpiler (int): Sets random seed for the stochastic parts of the transpiler. timing_constraints (TimingConstraints): Hardware time alignment restrictions. + unitary_synthesis_method (str): The string method to use for the + :class:`~qiskit.transpiler.passes.UnitarySynthesis` pass. Will + search installed plugins for a valid method. """ self.initial_layout = initial_layout self.basis_gates = basis_gates @@ -72,3 +76,4 @@ def __init__( self.approximation_degree = approximation_degree self.seed_transpiler = seed_transpiler self.timing_constraints = timing_constraints + self.unitary_synthesis_method = unitary_synthesis_method diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 925b11c8f9fb..f598f5d5d8dc 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -89,6 +89,7 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: backend_properties = pass_manager_config.backend_properties approximation_degree = pass_manager_config.approximation_degree timing_constraints = pass_manager_config.timing_constraints or TimingConstraints() + unitary_synthesis_method = pass_manager_config.unitary_synthesis_method # 1. Choose an initial layout if not set by user (default: trivial layout) _given_layout = SetLayout(initial_layout) @@ -111,7 +112,16 @@ def _choose_layout_condition(property_set): _embed = [FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()] # 3. Decompose so only 1-qubit and 2-qubit gates remain - _unroll3q = Unroll3qOrMore() + _unroll3q = [ + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_properties, + method=unitary_synthesis_method, + ), + Unroll3qOrMore(), + ] # 4. Swap to fit the coupling map _swap_check = CheckMap(coupling_map) @@ -151,7 +161,13 @@ def _swap_condition(property_set): Unroll3qOrMore(), Collect2qBlocks(), ConsolidateBlocks(basis_gates=basis_gates), - UnitarySynthesis(basis_gates, approximation_degree=approximation_degree), + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_properties, + method=unitary_synthesis_method, + ), ] else: raise TranspilerError("Invalid translation method %s." % translation_method) diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 6e5188b5b8a3..0cff8a5f5dbd 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -96,6 +96,7 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: seed_transpiler = pass_manager_config.seed_transpiler backend_properties = pass_manager_config.backend_properties approximation_degree = pass_manager_config.approximation_degree + unitary_synthesis_method = pass_manager_config.unitary_synthesis_method timing_constraints = pass_manager_config.timing_constraints or TimingConstraints() # 1. Use trivial layout if no layout given @@ -131,7 +132,16 @@ def _not_perfect_yet(property_set): _embed = [FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()] # 4. Decompose so only 1-qubit and 2-qubit gates remain - _unroll3q = Unroll3qOrMore() + _unroll3q = [ + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + method=unitary_synthesis_method, + backend_props=backend_properties, + ), + Unroll3qOrMore(), + ] # 5. Swap to fit the coupling map _swap_check = CheckMap(coupling_map) @@ -175,6 +185,7 @@ def _swap_condition(property_set): basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, + method=unitary_synthesis_method, backend_props=backend_properties, ), ] diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 0f4c0d3cae6c..929e9b270c45 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -100,6 +100,7 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: seed_transpiler = pass_manager_config.seed_transpiler backend_properties = pass_manager_config.backend_properties approximation_degree = pass_manager_config.approximation_degree + unitary_synthesis_method = pass_manager_config.unitary_synthesis_method timing_constraints = pass_manager_config.timing_constraints or TimingConstraints() # 1. Search for a perfect layout, or choose a dense layout, if no layout given @@ -165,7 +166,16 @@ def _csp_not_found_match(property_set): _embed = [FullAncillaAllocation(coupling_map), EnlargeWithAncilla(), ApplyLayout()] # 3. Unroll to 1q or 2q gates - _unroll3q = Unroll3qOrMore() + _unroll3q = [ + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_properties, + method=unitary_synthesis_method, + ), + Unroll3qOrMore(), + ] # 4. Swap to fit the coupling map _swap_check = CheckMap(coupling_map) @@ -210,6 +220,7 @@ def _swap_condition(property_set): approximation_degree=approximation_degree, coupling_map=coupling_map, backend_props=backend_properties, + method=unitary_synthesis_method, ), ] else: diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 10ce23f9031f..8f6cecb7be62 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -103,10 +103,20 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: seed_transpiler = pass_manager_config.seed_transpiler backend_properties = pass_manager_config.backend_properties approximation_degree = pass_manager_config.approximation_degree + unitary_synthesis_method = pass_manager_config.unitary_synthesis_method timing_constraints = pass_manager_config.timing_constraints or TimingConstraints() # 1. Unroll to 1q or 2q gates - _unroll3q = Unroll3qOrMore() + _unroll3q = [ + UnitarySynthesis( + basis_gates, + approximation_degree=approximation_degree, + coupling_map=coupling_map, + backend_props=backend_properties, + method=unitary_synthesis_method, + ), + Unroll3qOrMore(), + ] # 2. Layout on good qubits if calibration info available, otherwise on dense links _given_layout = SetLayout(initial_layout) @@ -213,6 +223,7 @@ def _swap_condition(property_set): approximation_degree=approximation_degree, coupling_map=coupling_map, backend_props=backend_properties, + method=unitary_synthesis_method, ), ] else: @@ -245,6 +256,7 @@ def _opt_control(property_set): approximation_degree=approximation_degree, coupling_map=coupling_map, backend_props=backend_properties, + method=unitary_synthesis_method, ), Optimize1qGatesDecomposition(basis_gates), CommutativeCancellation(), diff --git a/releasenotes/notes/unitary-synthesis-plugin-a5ec21a1906149fa.yaml b/releasenotes/notes/unitary-synthesis-plugin-a5ec21a1906149fa.yaml new file mode 100644 index 000000000000..660d3e85d236 --- /dev/null +++ b/releasenotes/notes/unitary-synthesis-plugin-a5ec21a1906149fa.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + Introduced a new unitary synthesis plugin interface which is used to enable + using alternative synthesis techniques included in external packages + seamlessly with the :class:`~qiskit.transpiler.passes.UnitarySynthesis` + transpiler pass. Users can select a plugin to use when calling + :func:`~qiskit.compiler.transpile` by setting the + ``unitary_synthesis_method`` kwarg to the plugin's name. A full list of + installed plugins can be found using the + :func:`qiskit.transpiler.passes.synthesis.plugin.unitary_synthesis_plugin_names` + function. For example, if you installed a package that includes a synthesis + plugin named ``special_synth`` you could use it with:: + + from qiskit import transpile + + transpile(qc, unitary_synthesis_method='special_synth', optimization_level=3) + + this will replace all uses of the :class:`~qiskit.transpiler.passes.UnitarySynthesis` + with the method included in the external package that exports the ``special_synth`` + plugin. + + The plugin interface is built around setuptools + `entry points `__ + which enables packages external to Qiskit to advertise they include a + synthesis plugin. For details on writing a new plugin refer to the + :mod:`qiskit.transpiler.passes.synthesis.plugin` module documentation. +upgrade: + - | + A new dependency `stevedore `__ has + been added to the requirements list. This is required by qiskit-terra as + it's used to build the unitary synthesis plugin interface. diff --git a/requirements.txt b/requirements.txt index 01f487675cb6..866111a812b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,5 +8,6 @@ sympy>=1.3 dill>=0.3 python-constraint>=1.4 python-dateutil>=2.8.0 +stevedore>=3.0.0 symengine>=0.8 ; platform_machine == 'x86_64' or platform_machine == 'aarch64' or platform_machine == 'ppc64le' or platform_machine == 'amd64' or platform_machine == 'arm64' tweedledum>=1.1,<2.0 diff --git a/setup.py b/setup.py index a111229993b1..ce30211419bf 100755 --- a/setup.py +++ b/setup.py @@ -140,4 +140,9 @@ }, ext_modules=cythonize(EXT_MODULES), zip_safe=False, + entry_points={ + "qiskit.unitary_synthesis": [ + "default = qiskit.transpiler.passes.synthesis.unitary_synthesis:DefaultUnitarySynthesis", + ] + }, )