Skip to content

Commit

Permalink
Add option to unitary synthesis plugin interface for user config (#7252)
Browse files Browse the repository at this point in the history
* Add option to unitary synthesis plugin interface for user config

This commit adds a new option to the unitary synthesis plugin interface
for plugins to accept free form user config. Two potential synthesis
plugins both have asked for an interface where a user can pass
configuration options through to the plugin to tune how the plugin runs.
To enable this, this commit adds a new kwarg to transpile() to pass a
configuration dictionary straight through to the plugin. As this is a
custom thing for each plugin the burden is on the plugin author to
define how this dictionary is used, implement using it, and documenting
it's use.

* Only pass config to non-default plugins
  • Loading branch information
mtreinish authored Nov 11, 2021
1 parent aac9917 commit 2db1685
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 1 deletion.
22 changes: 22 additions & 0 deletions qiskit/compiler/transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def transpile(
callback: Optional[Callable[[BasePass, DAGCircuit, float, PropertySet, int], Any]] = None,
output_name: Optional[Union[str, List[str]]] = None,
unitary_synthesis_method: str = "default",
unitary_synthesis_plugin_config: dict = None,
) -> Union[QuantumCircuit, List[QuantumCircuit]]:
"""Transpile one or more circuits, according to some desired transpilation targets.
Expand Down Expand Up @@ -220,6 +221,14 @@ def callback_func(**kwargs):
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.
unitary_synthesis_plugin_config: An optional configuration dictionary
that will be passed directly to the unitary synthesis plugin. By
default this setting will have no effect as the default unitary
synthesis method does not take custom configuration. This should
only be necessary when a unitary synthesis plugin is specified with
the ``unitary_synthesis`` argument. As this is custom for each
unitary synthesis plugin refer to the plugin documentation for how
to use this option.
Returns:
The transpiled circuit(s).
Expand Down Expand Up @@ -301,6 +310,7 @@ def callback_func(**kwargs):
output_name,
timing_constraints,
unitary_synthesis_method,
unitary_synthesis_plugin_config,
)

_check_circuits_coupling_map(circuits, transpile_args, backend)
Expand Down Expand Up @@ -484,6 +494,7 @@ def _parse_transpile_args(
output_name,
timing_constraints,
unitary_synthesis_method,
unitary_synthesis_plugin_config,
) -> 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
Expand Down Expand Up @@ -519,6 +530,9 @@ def _parse_transpile_args(
unitary_synthesis_method = _parse_unitary_synthesis_method(
unitary_synthesis_method, num_circuits
)
unitary_synthesis_plugin_config = _parse_unitary_plugin_config(
unitary_synthesis_plugin_config, 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)
Expand Down Expand Up @@ -554,6 +568,7 @@ def _parse_transpile_args(
"backend_num_qubits": backend_num_qubits,
"faulty_qubits_map": faulty_qubits_map,
"unitary_synthesis_method": unitary_synthesis_method,
"unitary_synthesis_plugin_config": unitary_synthesis_plugin_config,
}
):
transpile_args = {
Expand All @@ -572,6 +587,7 @@ def _parse_transpile_args(
timing_constraints=kwargs["timing_constraints"],
seed_transpiler=kwargs["seed_transpiler"],
unitary_synthesis_method=kwargs["unitary_synthesis_method"],
unitary_synthesis_plugin_config=kwargs["unitary_synthesis_plugin_config"],
),
"optimization_level": kwargs["optimization_level"],
"output_name": kwargs["output_name"],
Expand Down Expand Up @@ -837,6 +853,12 @@ def _parse_unitary_synthesis_method(unitary_synthesis_method, num_circuits):
return unitary_synthesis_method


def _parse_unitary_plugin_config(unitary_synthesis_plugin_config, num_circuits):
if not isinstance(unitary_synthesis_plugin_config, list):
unitary_synthesis_plugin_config = [unitary_synthesis_plugin_config] * num_circuits
return unitary_synthesis_plugin_config


def _parse_seed_transpiler(seed_transpiler, num_circuits):
if not isinstance(seed_transpiler, list):
seed_transpiler = [seed_transpiler] * num_circuits
Expand Down
10 changes: 10 additions & 0 deletions qiskit/transpiler/passes/synthesis/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ def run(self, unitary, **options):
expose multiple plugins if necessary. The name ``default`` is used by Qiskit
itself and can't be used in a plugin.
Unitary Synthesis Plugin Configuration
''''''''''''''''''''''''''''''''''''''
For some unitary synthesis plugins that expose multiple options and tunables
the plugin interface has an option for users to provide a free form
configuration dictionary. This will be passed through to the ``run()`` method
as the ``config`` kwarg. 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.
Using Plugins
=============
Expand Down
9 changes: 8 additions & 1 deletion qiskit/transpiler/passes/synthesis/unitary_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def __init__(
synth_gates: Union[List[str], None] = None,
method: str = "default",
min_qubits: int = None,
plugin_config: dict = None,
):
"""Synthesize unitaries over some basis gates.
Expand Down Expand Up @@ -161,6 +162,11 @@ def __init__(
min_qubits: The minimum number of qubits in the unitary to synthesize. If this is set
and the unitary is less than the specified number of qubits it will not be
synthesized.
plugin_config: Optional extra configuration arguments (as a dict)
which are passed directly to the specified unitary synthesis
plugin. By default this will have no effect as the default
plugin has no extra arguments. Refer to the documentation of
your unitary synthesis plugin on how to use this.
"""
super().__init__()
self._basis_gates = set(basis_gates or ())
Expand All @@ -172,6 +178,7 @@ def __init__(
self._backend_props = backend_props
self._pulse_optimize = pulse_optimize
self._natural_direction = natural_direction
self._plugin_config = plugin_config
if synth_gates:
self._synth_gates = synth_gates
else:
Expand Down Expand Up @@ -205,7 +212,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit:
return dag

plugin_method = self.plugins.ext_plugins[self.method].obj
plugin_kwargs = {}
plugin_kwargs = {"config": self._plugin_config}
_gate_lengths = _gate_errors = None
dag_bit_indices = {}

Expand Down
2 changes: 2 additions & 0 deletions qiskit/transpiler/passmanager_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def __init__(
seed_transpiler=None,
timing_constraints=None,
unitary_synthesis_method="default",
unitary_synthesis_plugin_config=None,
):
"""Initialize a PassManagerConfig object
Expand Down Expand Up @@ -80,6 +81,7 @@ def __init__(
self.seed_transpiler = seed_transpiler
self.timing_constraints = timing_constraints
self.unitary_synthesis_method = unitary_synthesis_method
self.unitary_synthesis_plugin_config = unitary_synthesis_plugin_config

@classmethod
def from_backend(cls, backend, **pass_manager_options):
Expand Down
5 changes: 5 additions & 0 deletions qiskit/transpiler/preset_passmanagers/level0.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
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
unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config

# 1. Choose an initial layout if not set by user (default: trivial layout)
_given_layout = SetLayout(initial_layout)
Expand Down Expand Up @@ -123,6 +124,7 @@ def _choose_layout_condition(property_set):
backend_props=backend_properties,
method=unitary_synthesis_method,
min_qubits=3,
plugin_config=unitary_synthesis_plugin_config,
),
Unroll3qOrMore(),
]
Expand Down Expand Up @@ -166,6 +168,7 @@ def _swap_condition(property_set):
coupling_map=coupling_map,
backend_props=backend_properties,
method=unitary_synthesis_method,
plugin_config=unitary_synthesis_plugin_config,
),
UnrollCustomDefinitions(sel, basis_gates),
BasisTranslator(sel, basis_gates),
Expand All @@ -179,6 +182,7 @@ def _swap_condition(property_set):
backend_props=backend_properties,
method=unitary_synthesis_method,
min_qubits=3,
plugin_config=unitary_synthesis_plugin_config,
),
Unroll3qOrMore(),
Collect2qBlocks(),
Expand All @@ -190,6 +194,7 @@ def _swap_condition(property_set):
coupling_map=coupling_map,
backend_props=backend_properties,
method=unitary_synthesis_method,
plugin_config=unitary_synthesis_plugin_config,
),
]
else:
Expand Down
4 changes: 4 additions & 0 deletions qiskit/transpiler/preset_passmanagers/level1.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
backend_properties = pass_manager_config.backend_properties
approximation_degree = pass_manager_config.approximation_degree
unitary_synthesis_method = pass_manager_config.unitary_synthesis_method
unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config
timing_constraints = pass_manager_config.timing_constraints or TimingConstraints()

# 1. Use trivial layout if no layout given
Expand Down Expand Up @@ -142,6 +143,7 @@ def _not_perfect_yet(property_set):
method=unitary_synthesis_method,
backend_props=backend_properties,
min_qubits=3,
plugin_config=unitary_synthesis_plugin_config,
),
Unroll3qOrMore(),
]
Expand Down Expand Up @@ -187,6 +189,7 @@ def _swap_condition(property_set):
coupling_map=coupling_map,
method=unitary_synthesis_method,
backend_props=backend_properties,
plugin_config=unitary_synthesis_plugin_config,
),
UnrollCustomDefinitions(sel, basis_gates),
BasisTranslator(sel, basis_gates),
Expand All @@ -212,6 +215,7 @@ def _swap_condition(property_set):
coupling_map=coupling_map,
method=unitary_synthesis_method,
backend_props=backend_properties,
plugin_config=unitary_synthesis_plugin_config,
),
]
else:
Expand Down
5 changes: 5 additions & 0 deletions qiskit/transpiler/preset_passmanagers/level2.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
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()
unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config

# 1. Search for a perfect layout, or choose a dense layout, if no layout given
_given_layout = SetLayout(initial_layout)
Expand Down Expand Up @@ -176,6 +177,7 @@ def _csp_not_found_match(property_set):
backend_props=backend_properties,
method=unitary_synthesis_method,
min_qubits=3,
plugin_config=unitary_synthesis_plugin_config,
),
Unroll3qOrMore(),
]
Expand Down Expand Up @@ -221,6 +223,7 @@ def _swap_condition(property_set):
coupling_map=coupling_map,
backend_props=backend_properties,
method=unitary_synthesis_method,
plugin_config=unitary_synthesis_plugin_config,
),
UnrollCustomDefinitions(sel, basis_gates),
BasisTranslator(sel, basis_gates),
Expand All @@ -235,6 +238,7 @@ def _swap_condition(property_set):
coupling_map=coupling_map,
backend_props=backend_properties,
method=unitary_synthesis_method,
plugin_config=unitary_synthesis_plugin_config,
min_qubits=3,
),
Unroll3qOrMore(),
Expand All @@ -246,6 +250,7 @@ def _swap_condition(property_set):
coupling_map=coupling_map,
backend_props=backend_properties,
method=unitary_synthesis_method,
plugin_config=unitary_synthesis_plugin_config,
),
]
else:
Expand Down
6 changes: 6 additions & 0 deletions qiskit/transpiler/preset_passmanagers/level3.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
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()
unitary_synthesis_plugin_config = pass_manager_config.unitary_synthesis_plugin_config

# 1. Unroll to 1q or 2q gates
_unroll3q = [
Expand All @@ -116,6 +117,7 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager:
coupling_map=coupling_map,
backend_props=backend_properties,
method=unitary_synthesis_method,
plugin_config=unitary_synthesis_plugin_config,
min_qubits=3,
),
Unroll3qOrMore(),
Expand Down Expand Up @@ -221,6 +223,7 @@ def _swap_condition(property_set):
approximation_degree=approximation_degree,
coupling_map=coupling_map,
backend_props=backend_properties,
plugin_config=unitary_synthesis_plugin_config,
method=unitary_synthesis_method,
),
UnrollCustomDefinitions(sel, basis_gates),
Expand All @@ -234,6 +237,7 @@ def _swap_condition(property_set):
coupling_map=coupling_map,
backend_props=backend_properties,
method=unitary_synthesis_method,
plugin_config=unitary_synthesis_plugin_config,
min_qubits=3,
),
Unroll3qOrMore(),
Expand All @@ -245,6 +249,7 @@ def _swap_condition(property_set):
coupling_map=coupling_map,
backend_props=backend_properties,
method=unitary_synthesis_method,
plugin_config=unitary_synthesis_plugin_config,
),
]
else:
Expand Down Expand Up @@ -278,6 +283,7 @@ def _opt_control(property_set):
coupling_map=coupling_map,
backend_props=backend_properties,
method=unitary_synthesis_method,
plugin_config=unitary_synthesis_plugin_config,
),
Optimize1qGatesDecomposition(basis_gates),
CommutativeCancellation(),
Expand Down
70 changes: 70 additions & 0 deletions test/python/transpiler/test_unitary_synthesis_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import stevedore

from qiskit.circuit import QuantumCircuit
from qiskit.converters import circuit_to_dag
from qiskit.test import QiskitTestCase
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import UnitarySynthesis
Expand Down Expand Up @@ -234,6 +235,75 @@ def test_all_keywords_passed_to_default_on_fallback(self):
self.assertIn(kwarg, call_kwargs)
self.MOCK_PLUGINS["_controllable"].run.assert_not_called()

def test_config_passed_to_non_default(self):
"""Test that a specified non-default plugin gets a config dict passed to it."""
self.MOCK_PLUGINS["_controllable"].min_qubits = 0
self.MOCK_PLUGINS["_controllable"].max_qubits = np.inf
self.MOCK_PLUGINS["_controllable"].support([])
qc = QuantumCircuit(2)
qc.unitary(np.eye(4, dtype=np.complex128), [0, 1])
return_dag = circuit_to_dag(qc)
plugin_config = {"option_a": 3.14, "option_b": False}
pm = PassManager(
[
UnitarySynthesis(
basis_gates=["u", "cx"], method="_controllable", plugin_config=plugin_config
)
]
)
with unittest.mock.patch.object(
ControllableSynthesis, "run", return_value=return_dag
) as plugin_mock:
pm.run(qc)
plugin_mock.assert_called() # pylint: disable=no-member
# This access should be `run.call_args.kwargs`, but the namedtuple access wasn't added
# until Python 3.8.
call_kwargs = plugin_mock.call_args[1] # pylint: disable=no-member
expected_kwargs = [
"config",
]
for kwarg in expected_kwargs:
self.assertIn(kwarg, call_kwargs)
self.assertEqual(call_kwargs["config"], plugin_config)

def test_config_not_passed_to_default_on_fallback(self):
"""Test that all the keywords that the default synthesis plugin needs are passed to it,
and if if config is specified it is not passed to the default."""
# Set the mock plugin to reject all keyword arguments, but also be unable to handle
# operators of any numbers of qubits. This will cause fallback to the default handler,
# which should receive a full set of keywords, still.
self.MOCK_PLUGINS["_controllable"].min_qubits = np.inf
self.MOCK_PLUGINS["_controllable"].max_qubits = 0
self.MOCK_PLUGINS["_controllable"].support([])
qc = QuantumCircuit(2)
qc.unitary(np.eye(4, dtype=np.complex128), [0, 1])
plugin_config = {"option_a": 3.14, "option_b": False}
pm = PassManager(
[
UnitarySynthesis(
basis_gates=["u", "cx"], method="_controllable", plugin_config=plugin_config
)
]
)
with self.mock_default_run_method():
pm.run(qc)
self.DEFAULT_PLUGIN.run.assert_called() # pylint: disable=no-member
# This access should be `run.call_args.kwargs`, but the namedtuple access wasn't added
# until Python 3.8.
call_kwargs = self.DEFAULT_PLUGIN.run.call_args[1] # pylint: disable=no-member
expected_kwargs = [
"basis_gates",
"coupling_map",
"gate_errors",
"gate_lengths",
"natural_direction",
"pulse_optimize",
]
for kwarg in expected_kwargs:
self.assertIn(kwarg, call_kwargs)
self.MOCK_PLUGINS["_controllable"].run.assert_not_called()
self.assertNotIn("config", call_kwargs)


if __name__ == "__main__":
unittest.main()

0 comments on commit 2db1685

Please sign in to comment.