From ae1c03658bf3d7277f4e1fb297711ab82778136f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 10 Nov 2021 11:30:13 -0500 Subject: [PATCH 1/2] 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. --- qiskit/compiler/transpiler.py | 22 +++++++++++++++++++ qiskit/transpiler/passes/synthesis/plugin.py | 10 +++++++++ .../passes/synthesis/unitary_synthesis.py | 9 +++++++- qiskit/transpiler/passmanager_config.py | 2 ++ .../transpiler/preset_passmanagers/level0.py | 5 +++++ .../transpiler/preset_passmanagers/level1.py | 4 ++++ .../transpiler/preset_passmanagers/level2.py | 5 +++++ .../transpiler/preset_passmanagers/level3.py | 6 +++++ 8 files changed, 62 insertions(+), 1 deletion(-) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 8f8a2e2f621e..143aa63a5c2b 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -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. @@ -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). @@ -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) @@ -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 @@ -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) @@ -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 = { @@ -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"], @@ -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 diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index 8d8fdc585c46..bb733c8979af 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -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 ============= diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index bf470068b5cd..f1ff716456b5 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -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. @@ -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 ()) @@ -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: @@ -267,7 +274,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: self._coupling_map, [dag_bit_indices[x] for x in node.qargs], ) - synth_dag = method.run(unitary, **kwargs) + synth_dag = method.run(unitary, config=self._plugin_config, **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]) diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 7f0afffcc89c..12343f9c6ec3 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -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 @@ -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): diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 7b24f1ceff06..421376f8fe79 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -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) @@ -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(), ] @@ -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), @@ -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(), @@ -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: diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index c7308ae969c7..0d4d7cc422f2 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -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 @@ -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(), ] @@ -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), @@ -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: diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 30f26e518253..7008002472b4 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -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) @@ -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(), ] @@ -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), @@ -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(), @@ -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: diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index e43192f5c9f6..2ca784e608dc 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -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 = [ @@ -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(), @@ -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), @@ -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(), @@ -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: @@ -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(), From a67482be0d503d82851c0c9b9b2f7f1a35930030 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 10 Nov 2021 17:10:17 -0500 Subject: [PATCH 2/2] Only pass config to non-default plugins --- .../passes/synthesis/unitary_synthesis.py | 4 +- .../test_unitary_synthesis_plugin.py | 70 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index f1ff716456b5..49d7db314481 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -212,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 = {} @@ -274,7 +274,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: self._coupling_map, [dag_bit_indices[x] for x in node.qargs], ) - synth_dag = method.run(unitary, config=self._plugin_config, **kwargs) + synth_dag = 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]) diff --git a/test/python/transpiler/test_unitary_synthesis_plugin.py b/test/python/transpiler/test_unitary_synthesis_plugin.py index c67c86fe8d25..ca00f5ea5672 100644 --- a/test/python/transpiler/test_unitary_synthesis_plugin.py +++ b/test/python/transpiler/test_unitary_synthesis_plugin.py @@ -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 @@ -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()