Create a transpiler plugin
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:
- Create a subclass of the appropriate abstract plugin class:
- PassManagerStagePlugin for a transpiler stage plugin,
- UnitarySynthesisPlugin for a unitary synthesis plugin, and
- HighLevelSynthesisPlugin for a high-level synthesis plugin.
- Expose the class as a setuptools entry point in the package metadata, typically by editing the
pyproject.toml
,setup.cfg
, orsetup.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 class | Operation name | Reserved names |
---|---|---|
Clifford | clifford | default , ag , bm , greedy , layers , lnn |
LinearFunction | linear_function | default , kms , pmh |
PermutationGate | permutation | default , 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"
[options.entry_points]
qiskit.transpiler.layout =
my_layout = my_qiskit_plugin:MyLayoutPlugin
from setuptools import setup
setup(
# ...,
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:
[2] :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.
[3] :from qiskit.transpiler.passmanager_config import PassManagerConfig
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:
[4] :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.
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"
[options.entry_points]
qiskit.unitary_synthesis =
my_unitary_synthesis = my_qiskit_plugin:MyUnitarySynthesisPlugin
from setuptools import setup
setup(
# ...,
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:
[5] :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 oncoupling_map
only specifies the coupling map, and is only used whentarget
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 ofNone
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:
[6] :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"
[options.entry_points]
qiskit.synthesis =
clifford.my_clifford_synthesis = my_qiskit_plugin:MyCliffordSynthesisPlugin
from setuptools import setup
setup(
# ...,
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 thename
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:
[7] :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.
- Submit your plugin to the Qiskit Ecosystem!.
- Check out the tutorials on IBM Quantum Learning for examples of transpiling and running quantum circuits.