Skip to main contentIBM Quantum Documentation Preview
This is a preview build of IBM Quantumâ„¢ documentation. Refer to docs.quantum.ibm.com for the official documentation.

Create a transpiler plugin

Package versions

The code on this page was developed using the following requirements. We recommend using these versions or newer.

qiskit[all]~=1.2.4
qiskit-aer~=0.15.1
qiskit-ibm-runtime~=0.31.0
qiskit-serverless~=0.17.1
qiskit-ibm-catalog~=0.1

Creating a transpiler plugin is a great way to share your transpilation code with the wider Qiskit community, allowing other users to benefit from the functionality you've developed. Thank you for your interest in contributing to the Qiskit community!

Before you create a transpiler plugin, you need to decide what kind of plugin is appropriate for your situation. There are three kinds of transpiler plugins:

  • Transpiler stage plugin. Choose this if you are defining a pass manager that can be substituted for one of the 6 stages of a preset staged pass manager.
  • Unitary synthesis plugin. Choose this if your transpilation code takes as input a unitary matrix (represented as a Numpy array) and outputs a description of a quantum circuit implementing that unitary.
  • High-level synthesis plugin. Choose this if your transpilation code takes as input a "high-level object" such as a Clifford operator or a linear function and outputs a description of a quantum circuit implementing that high-level object. High-level objects are represented by subclasses of the Operation class.

Once you've determined which kind of plugin to create, follow these steps to create the plugin:

  1. Create a subclass of the appropriate abstract plugin class:
  2. Expose the class as a setuptools entry point in the package metadata, typically by editing the pyproject.toml, setup.cfg, or setup.py file for your Python package.

There is no limit to the number of plugins a single package can define, but each plugin must have a unique name. The Qiskit SDK itself includes a number of plugins, whose names are also reserved. The reserved names are:

  • Transpiler stage plugins: See this table.
  • Unitary synthesis plugins: default, aqc, sk
  • High-level synthesis plugins:
Operation classOperation nameReserved names
Cliffordclifforddefault, ag, bm, greedy, layers, lnn
LinearFunctionlinear_functiondefault, kms, pmh
PermutationGatepermutationdefault, kms, basic, acg, token_swapper

In the next sections, we show examples of these steps for the different types of plugins. In these examples, we assume that we are creating a Python package called my_qiskit_plugin. For information on creating Python packages, you can check out this tutorial from the Python website.


Example: Create a transpiler stage plugin

In this example, we create a transpiler stage plugin for the layout stage (see Transpiler stages for a description of the 6 stages of Qiskit's built-in transpilation pipeline). Our plugin simply runs VF2Layout for a number of trials that depends on the requested optimization level.

First, we create a subclass of PassManagerStagePlugin. There is one method we need to implement, called pass_manager. This method takes as input a PassManagerConfig and returns the pass manager that we are defining. The PassManagerConfig object stores information about the target backend, such as its coupling map and basis gates.

# This import is needed for python versions prior to 3.10
from __future__ import annotations
 
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import VF2Layout
from qiskit.transpiler.passmanager_config import PassManagerConfig
from qiskit.transpiler.preset_passmanagers import common
from qiskit.transpiler.preset_passmanagers.plugin import (
    PassManagerStagePlugin,
)
 
 
class MyLayoutPlugin(PassManagerStagePlugin):
    def pass_manager(
        self,
        pass_manager_config: PassManagerConfig,
        optimization_level: int | None = None,
    ) -> PassManager:
        layout_pm = PassManager(
            [
                VF2Layout(
                    coupling_map=pass_manager_config.coupling_map,
                    properties=pass_manager_config.backend_properties,
                    max_trials=optimization_level * 10 + 1,
                    target=pass_manager_config.target,
                )
            ]
        )
        layout_pm += common.generate_embed_passmanager(
            pass_manager_config.coupling_map
        )
        return layout_pm

Now, we expose the plugin by adding an entry point in our Python package metadata. Here, we assume that the class we defined is exposed in a module called my_qiskit_plugin, for example by being imported in the __init__.py file of the my_qiskit_plugin module. We edit the pyproject.toml, setup.cfg, or setup.py file of our package (depending on which kind of file you chose to store your Python project metadata):

[project.entry-points."qiskit.transpiler.layout"]
"my_layout" = "my_qiskit_plugin:MyLayoutPlugin"

See the table of transpiler plugin stages for the entry points and expectations for each transpiler stage.

To check that your plugin is successfully detected by Qiskit, install your plugin package and follow the instructions at Transpiler plugins for listing installed plugins, and ensure that your plugin appears in the list:

from qiskit.transpiler.preset_passmanagers.plugin import list_stage_plugins
 
list_stage_plugins("layout")

Output:

['default', 'dense', 'sabre', 'trivial']

If our example plugin were installed, then the name my_layout would appear in this list.

If you want to use a built-in transpiler stage as the starting point for your transpiler stage plugin, you can obtain the pass manager for a built-in transpiler stage using PassManagerStagePluginManager. The following code cell shows how to do this to obtain the built-in optimization stage for optimization level 3.

from qiskit.transpiler.preset_passmanagers.plugin import (
    PassManagerStagePluginManager,
)
 
# Initialize the plugin manager
plugin_manager = PassManagerStagePluginManager()
 
# Here we create a pass manager config to use as an example.
# Instead, you should use the pass manager config that you already received as input
# to the pass_manager method of your PassManagerStagePlugin.
pass_manager_config = PassManagerConfig()
 
# Obtain the desired built-in transpiler stage
optimization = plugin_manager.get_passmanager_stage(
    "optimization", "default", pass_manager_config, optimization_level=3
)

Example: Create a unitary synthesis plugin

In this example, we'll create a unitary synthesis plugin that simply uses the built-in UnitarySynthesis transpilation pass to synthesize a gate. Of course, your own plugin will do something more interesting than that.

The UnitarySynthesisPlugin class defines the interface and contract for unitary synthesis plugins. The primary method is run, which takes as input a Numpy array storing a unitary matrix and returns a DAGCircuit representing the circuit synthesized from that unitary matrix. In addition to the run method, there are a number of property methods that need to be defined. See UnitarySynthesisPlugin for documentation of all required properties.

Let's create our UnitarySynthesisPlugin subclass:

import numpy as np
from qiskit.circuit import QuantumCircuit, QuantumRegister
from qiskit.converters import circuit_to_dag
from qiskit.dagcircuit.dagcircuit import DAGCircuit
from qiskit.quantum_info import Operator
from qiskit.transpiler.passes import UnitarySynthesis
from qiskit.transpiler.passes.synthesis.plugin import UnitarySynthesisPlugin
 
 
class MyUnitarySynthesisPlugin(UnitarySynthesisPlugin):
    @property
    def supports_basis_gates(self):
        # Returns True if the plugin can target a list of basis gates
        return True
 
    @property
    def supports_coupling_map(self):
        # Returns True if the plugin can synthesize for a given coupling map
        return False
 
    @property
    def supports_natural_direction(self):
        # Returns True if the plugin supports a toggle for considering
        # directionality of 2-qubit gates
        return False
 
    @property
    def supports_pulse_optimize(self):
        # Returns True if the plugin can optimize pulses during synthesis
        return False
 
    @property
    def supports_gate_lengths(self):
        # Returns True if the plugin can accept information about gate lengths
        return False
 
    @property
    def supports_gate_errors(self):
        # Returns True if the plugin can accept information about gate errors
        return False
 
    @property
    def supports_gate_lengths_by_qubit(self):
        # Returns True if the plugin can accept information about gate lengths
        # (The format of the input differs from supports_gate_lengths)
        return False
 
    @property
    def supports_gate_errors_by_qubit(self):
        # Returns True if the plugin can accept information about gate errors
        # (The format of the input differs from supports_gate_errors)
        return False
 
    @property
    def min_qubits(self):
        # Returns the minimum number of qubits the plugin supports
        return None
 
    @property
    def max_qubits(self):
        # Returns the maximum number of qubits the plugin supports
        return None
 
    @property
    def supported_bases(self):
        # Returns a dictionary of supported bases for synthesis
        return None
 
    def run(self, unitary: np.ndarray, **options) -> DAGCircuit:
        basis_gates = options["basis_gates"]
        synth_pass = UnitarySynthesis(basis_gates, min_qubits=3)
        qubits = QuantumRegister(3)
        circuit = QuantumCircuit(qubits)
        circuit.append(Operator(unitary).to_instruction(), qubits)
        dag_circuit = synth_pass.run(circuit_to_dag(circuit))
        return dag_circuit

If you find that the inputs available to the run method are insufficient for your purposes, please open an issue explaining your requirements. Changes to the plugin interface, such as adding additional optional inputs, will be done in a backward compatible way so that they do not require changes from existing plugins.

Note

All methods prefixed with supports_ are reserved on a UnitarySynthesisPlugin derived class as part of the interface. You should not define any custom supports_* methods on a subclass that are not defined in the abstract class.

Now, we expose the plugin by adding an entry point in our Python package metadata. Here, we assume that the class we defined is exposed in a module called my_qiskit_plugin, for example by being imported in the __init__.py file of the my_qiskit_plugin module. We edit the pyproject.toml, setup.cfg, or setup.py file of our package:

[project.entry-points."qiskit.unitary_synthesis"]
"my_unitary_synthesis" = "my_qiskit_plugin:MyUnitarySynthesisPlugin"

As before, if your project uses setup.cfg or setup.py instead of pyproject.toml, see the setuptools documentation for how to adapt these lines for your situation.

To check that your plugin is successfully detected by Qiskit, install your plugin package and follow the instructions at Transpiler plugins for listing installed plugins, and ensure that your plugin appears in the list:

from qiskit.transpiler.passes.synthesis import unitary_synthesis_plugin_names
 
unitary_synthesis_plugin_names()

Output:

['aqc', 'default', 'sk']

If our example plugin were installed, then the name my_unitary_synthesis would appear in this list.

To accommodate unitary synthesis plugins that expose multiple options, the plugin interface has an option for users to provide a free-form configuration dictionary. This will be passed to the run method via the options keyword argument. If your plugin has these configuration options, you should clearly document them.


Example: Create a high-level synthesis plugin

In this example, we'll create a high-level synthesis plugin that simply uses the built-in synth_clifford_bm function to synthesize a Clifford operator.

The HighLevelSynthesisPlugin class defines the interface and contract for high-level synthesis plugins. The primary method is run. The positional argument high_level_object is an Operation representing the "high-level" object to be synthesized. For example, it could be a LinearFunction or a Clifford. The following keyword arguments are present:

  • 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
  • coupling_map only specifies the coupling map, and is only used when target is not specified.
  • qubits specifies the list of qubits over which the high-level object is defined, in case the synthesis is done on the physical circuit. A 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.
  • options, a free-form configuration dictionary for plugin-specific options. If your plugin has these configuration options you should clearly document them.

The run method returns a QuantumCircuit representing the circuit synthesized from that high-level object. It is also allowed to return None, indicating that the plugin is unable to synthesize the given high-level object. The actual synthesis of high-level objects is performed by the HighLevelSynthesis transpiler pass.

In addition to the run method, there are a number of property methods that need to be defined. See HighLevelSynthesisPlugin for documentation of all required properties.

Let's define our HighLevelSynthesisPlugin subclass:

from qiskit.synthesis import synth_clifford_bm
from qiskit.transpiler.passes.synthesis.plugin import HighLevelSynthesisPlugin
 
 
class MyCliffordSynthesisPlugin(HighLevelSynthesisPlugin):
    def run(
        self,
        high_level_object,
        coupling_map=None,
        target=None,
        qubits=None,
        **options,
    ) -> QuantumCircuit:
        if high_level_object.num_qubits <= 3:
            return synth_clifford_bm(high_level_object)
        else:
            return None

This plugin synthesizes objects of type Clifford that have at most 3 qubits, using the synth_clifford_bm method.

Now, we expose the plugin by adding an entry point in our Python package metadata. Here, we assume that the class we defined is exposed in a module called my_qiskit_plugin, for example by being imported in the __init__.py file of the my_qiskit_plugin module. We edit the pyproject.toml, setup.cfg, or setup.py file of our package:

[project.entry-points."qiskit.synthesis"]
"clifford.my_clifford_synthesis" = "my_qiskit_plugin:MyCliffordSynthesisPlugin"

The name consists of two parts separated by a dot (.):

  • The name of the type of Operation that the plugin synthesizes (in this case, clifford). Note that this string corresponds to the name attribute of the Operation class, and not the name of the class itself.
  • The name of the plugin (in this case, special).

As before, if your project uses setup.cfg or setup.py instead of pyproject.toml, see the setuptools documentation for how to adapt these lines for your situation.

To check that your plugin is successfully detected by Qiskit, install your plugin package and follow the instructions at Transpiler plugins for listing installed plugins, and ensure that your plugin appears in the list:

from qiskit.transpiler.passes.synthesis import (
    high_level_synthesis_plugin_names,
)
 
high_level_synthesis_plugin_names("clifford")

Output:

['ag', 'bm', 'default', 'greedy', 'layers', 'lnn']

If our example plugin were installed, then the name my_clifford_synthesis would appear in this list.

Recommendation