Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to unitary synthesis plugin interface for user config #7252

Merged
merged 2 commits into from
Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()