diff --git a/pyproject.toml b/pyproject.toml index 137f85530a2b..06db15d3cc24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,7 @@ sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevS "permutation.kms" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:KMSSynthesisPermutation" "permutation.basic" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:BasicSynthesisPermutation" "permutation.acg" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:ACGSynthesisPermutation" +"permutation.token_swapper" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:TokenSwapperSynthesisPermutation" [project.entry-points."qiskit.transpiler.init"] default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultInitPassManager" diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index da10a8102887..130cb97b7c91 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -15,6 +15,8 @@ from typing import Optional, Union, List, Tuple +import rustworkx as rx + from qiskit.circuit.operation import Operation from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.basepasses import TransformationPass @@ -25,6 +27,7 @@ from qiskit.transpiler.coupling import CouplingMap from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper from qiskit.circuit.annotated_operation import ( AnnotatedOperation, @@ -297,7 +300,7 @@ def _recursively_handle_op( # Try to apply plugin mechanism decomposition = self._synthesize_op_using_plugins(op, qubits) - if decomposition: + if decomposition is not None: return decomposition, True # Handle annotated operations @@ -644,3 +647,80 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** """Run synthesis for the given Permutation.""" decomposition = synth_permutation_acg(high_level_object.pattern) return decomposition + + +class TokenSwapperSynthesisPermutation(HighLevelSynthesisPlugin): + """The permutation synthesis plugin based on the token swapper algorithm. + + This plugin name is :``permutation.token_swapper`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + In more detail, this plugin is used to synthesize objects of type `PermutationGate`. + When synthesis succeeds, the plugin outputs a quantum circuit consisting only of swap + gates. When synthesis does not succeed, the plugin outputs `None`. + + If either `coupling_map` or `qubits` is None, then the synthesized circuit + is not required to adhere to connectivity constraints, as is the case + when the synthesis is done before layout/routing. + + On the other hand, if both `coupling_map` and `qubits` are specified, the synthesized + circuit is supposed to adhere to connectivity constraints. At the moment, the + plugin only creates swap gates between qubits in `qubits`, i.e. it does not use + any other qubits in the coupling map (if such synthesis is not possible, the + plugin outputs `None`). + + The plugin supports the following plugin-specific options: + + * trials: The number of trials for the token swapper to perform the mapping. The + circuit with the smallest number of SWAPs is returned. + * seed: The argument to the token swapper specifying the seed for random trials. + * parallel_threshold: The argument to the token swapper specifying the number of nodes + in the graph beyond which the algorithm will use parallel processing. + + For more details on the token swapper algorithm, see to the paper: + `arXiv:1902.09102 `__. + + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given Permutation.""" + + trials = options.get("trials", 5) + seed = options.get("seed", 0) + parallel_threshold = options.get("parallel_threshold", 50) + + pattern = high_level_object.pattern + pattern_as_dict = {j: i for i, j in enumerate(pattern)} + + # When the plugin is called from the HighLevelSynthesis transpiler pass, + # the coupling map already takes target into account. + if coupling_map is None or qubits is None: + # The abstract synthesis uses a fully connected coupling map, allowing + # arbitrary connections between qubits. + used_coupling_map = CouplingMap.from_full(len(pattern)) + else: + # The concrete synthesis uses the coupling map restricted to the set of + # qubits over which the permutation gate is defined. If we allow using other + # qubits in the coupling map, replacing the node in the DAGCircuit that + # defines this PermutationGate by the DAG corresponding to the constructed + # decomposition becomes problematic. Note that we allow the reduced + # coupling map to be disconnected. + used_coupling_map = coupling_map.reduce(qubits, check_if_connected=False) + + graph = used_coupling_map.graph.to_undirected() + swapper = ApproximateTokenSwapper(graph, seed=seed) + + try: + swapper_result = swapper.map( + pattern_as_dict, trials, parallel_threshold=parallel_threshold + ) + except rx.InvalidMapping: + swapper_result = None + + if swapper_result is not None: + decomposition = QuantumCircuit(len(graph.node_indices())) + for swap in swapper_result: + decomposition.swap(*swap) + return decomposition + + return None diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index 69505c127438..f5024e4ef419 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -284,6 +284,36 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** will return a list of all the installed Clifford synthesis plugins. +Available Plugins +----------------- + +High-level synthesis plugins that are directly available in Qiskit include plugins +for synthesizing :class:`.Clifford` objects, :class:`.LinearFunction` objects, and +:class:`.PermutationGate` objects. +Some of these plugins implicitly target all-to-all connectivity. This is not a +practical limitation since +:class:`~qiskit.transpiler.passes.synthesis.high_level_synthesis.HighLevelSynthesis` +typically runs before layout and routing, which will ensure that the final circuit +adheres to the device connectivity by inserting additional SWAP gates. A good example +is the permutation synthesis plugin ``ACGSynthesisPermutation`` which can synthesize +any permutation with at most 2 layers of SWAP gates. +On the other hand, some plugins implicitly target linear connectivity. +Typically, the synthesizing circuits have larger depth and the number of gates, +however no additional SWAP gates would be inserted if the following layout pass chose a +consecutive line of qubits inside the topology of the device. A good example of this is +the permutation synthesis plugin ``KMSSynthesisPermutation`` which can synthesize any +permutation of ``n`` qubits in depth ``n``. Typically, it is difficult to know in advance +which of the two approaches: synthesizing circuits for all-to-all connectivity and +inserting SWAP gates vs. synthesizing circuits for linear connectivity and inserting less +or no SWAP gates lead a better final circuit, so it likely makes sense to try both and +see which gives better results. +Finally, some plugins can target a given connectivity, and hence should be run after the +layout is set. In this case the synthesized circuit automatically adheres to +the topology of the device. A good example of this is the permutation synthesis plugin +``TokenSwapperSynthesisPermutation`` which is able to synthesize arbitrary permutations +with respect to arbitrary coupling maps. +For more detail, please refer to description of each individual plugin. + Plugin API ========== @@ -306,7 +336,6 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** HighLevelSynthesisPlugin HighLevelSynthesisPluginManager high_level_synthesis_plugin_names - """ import abc diff --git a/releasenotes/notes/add-token-swapper-synthesis-plugin-4ed5009f5f21519d.yaml b/releasenotes/notes/add-token-swapper-synthesis-plugin-4ed5009f5f21519d.yaml new file mode 100644 index 000000000000..00a69a46c1af --- /dev/null +++ b/releasenotes/notes/add-token-swapper-synthesis-plugin-4ed5009f5f21519d.yaml @@ -0,0 +1,44 @@ +--- +upgrade: + - | + Qiskit 1.0 now requires version 0.14.0 of ``rustworkx``. +features: + - | + Added a new :class:`.HighLevelSynthesisPlugin` for :class:`.PermutationGate` + objects based on Qiskit's token swapper algorithm. To use this plugin, + specify ``token_swapper`` when defining high-level-synthesis config. + + This synthesis plugin is able to run before or after the layout is set. + When synthesis succeeds, the plugin outputs a quantum circuit consisting only of + swap gates. When synthesis does not succeed, the plugin outputs `None`. + + The following code illustrates how the new plugin can be run:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import PermutationGate + from qiskit.transpiler import PassManager, CouplingMap + from qiskit.transpiler.passes.synthesis.high_level_synthesis import HighLevelSynthesis, HLSConfig + + # This creates a circuit with a permutation gate. + qc = QuantumCircuit(8) + perm_gate = PermutationGate([0, 1, 4, 3, 2]) + qc.append(perm_gate, [3, 4, 5, 6, 7]) + + # This defines the coupling map. + coupling_map = CouplingMap.from_ring(8) + + # This high-level-synthesis config specifies that we want to use + # the "token_swapper" plugin for synthesizing permutation gates, + # with the option to use 10 trials. + synthesis_config = HLSConfig(permutation=[("token_swapper", {"trials": 10})]) + + # This creates the pass manager that runs high-level-synthesis on our circuit. + # The option use_qubit_indices=True indicates that synthesis run after the layout is set, + # and hence should preserve the specified coupling map. + pm = PassManager( + HighLevelSynthesis( + synthesis_config, coupling_map=coupling_map, target=None, use_qubit_indices=True + ) + ) + + qc_transpiled = pm.run(qc) diff --git a/requirements.txt b/requirements.txt index 5c3dd9c879b6..e34fdfe36b2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -rustworkx>=0.13.0 +rustworkx>=0.14.0 numpy>=1.17,<2 scipy>=1.5 sympy>=1.3 diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 962a3b87b66e..ea5f5ee7e4d6 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -13,8 +13,7 @@ """ Tests the interface for HighLevelSynthesis transpiler pass. """ - - +import itertools import unittest.mock import numpy as np from qiskit.circuit import ( @@ -42,12 +41,15 @@ from qiskit.circuit.library.generalized_gates import LinearFunction from qiskit.quantum_info import Clifford from qiskit.test import QiskitTestCase +from qiskit.transpiler.passes.synthesis.plugin import ( + HighLevelSynthesisPlugin, + HighLevelSynthesisPluginManager, +) from qiskit.compiler import transpile from qiskit.exceptions import QiskitError from qiskit.converters import dag_to_circuit, circuit_to_dag, circuit_to_instruction from qiskit.transpiler import PassManager, TranspilerError, CouplingMap, Target from qiskit.transpiler.passes.basis import BasisTranslator -from qiskit.transpiler.passes.synthesis.plugin import HighLevelSynthesisPlugin from qiskit.transpiler.passes.synthesis.high_level_synthesis import HighLevelSynthesis, HLSConfig from qiskit.circuit.annotated_operation import ( AnnotatedOperation, @@ -501,6 +503,188 @@ def test_qubits_get_passed_to_plugins(self): pm_use_qubits_true.run(qc) +class TestTokenSwapperPermutationPlugin(QiskitTestCase): + """Tests for the token swapper plugin for synthesizing permutation gates.""" + + def test_token_swapper_in_known_plugin_names(self): + """Test that "token_swapper" is an available synthesis plugin for permutation gates.""" + self.assertIn( + "token_swapper", HighLevelSynthesisPluginManager().method_names("permutation") + ) + + def test_abstract_synthesis(self): + """Test abstract synthesis of a permutation gate (either the coupling map or the set + of qubits over which the permutation is defined is not specified). + """ + + # Permutation gate + # 4->0, 6->1, 3->2, 7->3, 1->4, 2->5, 0->6, 5->7 + perm = PermutationGate([4, 6, 3, 7, 1, 2, 0, 5]) + + # Circuit with permutation gate + qc = QuantumCircuit(8) + qc.append(perm, range(8)) + + # Synthesize circuit using the token swapper plugin + synthesis_config = HLSConfig(permutation=[("token_swapper", {"trials": 10, "seed": 1})]) + qc_transpiled = PassManager(HighLevelSynthesis(synthesis_config)).run(qc) + + # Construct the expected quantum circuit + # From the description below we can see that + # 0->6, 1->4, 2->5, 3->2, 4->0, 5->2->3->7, 6->0->4->1, 7->3 + qc_expected = QuantumCircuit(8) + qc_expected.swap(2, 5) + qc_expected.swap(0, 6) + qc_expected.swap(2, 3) + qc_expected.swap(0, 4) + qc_expected.swap(1, 4) + qc_expected.swap(3, 7) + + self.assertEqual(qc_transpiled, qc_expected) + + def test_concrete_synthesis(self): + """Test concrete synthesis of a permutation gate (we have both the coupling map and the + set of qubits over which the permutation gate is defined; moreover, the coupling map may + have more qubits than the permutation gate). + """ + + # Permutation gate + perm = PermutationGate([0, 1, 4, 3, 2]) + + # Circuit with permutation gate + qc = QuantumCircuit(8) + qc.append(perm, [3, 4, 5, 6, 7]) + + coupling_map = CouplingMap.from_ring(8) + + synthesis_config = HLSConfig(permutation=[("token_swapper", {"trials": 10})]) + qc_transpiled = PassManager( + HighLevelSynthesis( + synthesis_config, coupling_map=coupling_map, target=None, use_qubit_indices=True + ) + ).run(qc) + + qc_expected = QuantumCircuit(8) + qc_expected.swap(6, 7) + qc_expected.swap(5, 6) + qc_expected.swap(6, 7) + self.assertEqual(qc_transpiled, qc_expected) + + def test_concrete_synthesis_over_disconnected_qubits(self): + """Test concrete synthesis of a permutation gate over a disconnected set of qubits, + when synthesis is possible. + """ + + # Permutation gate + perm = PermutationGate([1, 0, 3, 2]) + + # Circuit with permutation gate + qc = QuantumCircuit(10) + qc.append(perm, [3, 2, 7, 8]) + + coupling_map = CouplingMap.from_ring(10) + + synthesis_config = HLSConfig(permutation=[("token_swapper", {"trials": 10})]) + qc_transpiled = PassManager( + HighLevelSynthesis( + synthesis_config, coupling_map=coupling_map, target=None, use_qubit_indices=True + ) + ).run(qc) + + qc_expected = QuantumCircuit(10) + qc_expected.swap(2, 3) + qc_expected.swap(7, 8) + + # Even though the permutation is over a disconnected set of qubits, the synthesis + # is possible. + self.assertEqual(qc_transpiled, qc_expected) + + def test_concrete_synthesis_is_not_possible(self): + """Test concrete synthesis of a permutation gate over a disconnected set of qubits, + when synthesis is not possible. + """ + + # Permutation gate + perm = PermutationGate([0, 2, 1, 3]) + + # Circuit with permutation gate + qc = QuantumCircuit(10) + qc.append(perm, [3, 2, 7, 8]) + + coupling_map = CouplingMap.from_ring(10) + + synthesis_config = HLSConfig(permutation=[("token_swapper", {"trials": 10})]) + qc_transpiled = PassManager( + HighLevelSynthesis( + synthesis_config, coupling_map=coupling_map, target=None, use_qubit_indices=True + ) + ).run(qc) + + # The synthesis is not possible. In this case the plugin should return `None` + # and `HighLevelSynthesis` should not change the original circuit. + self.assertEqual(qc_transpiled, qc) + + def test_abstract_synthesis_all_permutations(self): + """Test abstract synthesis of permutation gates, varying permutation gate patterns.""" + + edges = [(0, 1), (1, 0), (1, 2), (2, 1), (1, 3), (3, 1), (3, 4), (4, 3)] + + coupling_map = CouplingMap() + for i in range(5): + coupling_map.add_physical_qubit(i) + for edge in edges: + coupling_map.add_edge(*edge) + + synthesis_config = HLSConfig(permutation=[("token_swapper", {"trials": 10})]) + pm = PassManager( + HighLevelSynthesis( + synthesis_config, coupling_map=coupling_map, target=None, use_qubit_indices=False + ) + ) + + for pattern in itertools.permutations(range(4)): + qc = QuantumCircuit(5) + qc.append(PermutationGate(pattern), [2, 0, 3, 1]) + self.assertIn("permutation", qc.count_ops()) + + qc_transpiled = pm.run(qc) + self.assertNotIn("permutation", qc_transpiled.count_ops()) + + self.assertEqual(Operator(qc), Operator(qc_transpiled)) + + def test_concrete_synthesis_all_permutations(self): + """Test concrete synthesis of permutation gates, varying permutation gate patterns.""" + + edges = [(0, 1), (1, 0), (1, 2), (2, 1), (1, 3), (3, 1), (3, 4), (4, 3)] + + coupling_map = CouplingMap() + for i in range(5): + coupling_map.add_physical_qubit(i) + for edge in edges: + coupling_map.add_edge(*edge) + + synthesis_config = HLSConfig(permutation=[("token_swapper", {"trials": 10})]) + pm = PassManager( + HighLevelSynthesis( + synthesis_config, coupling_map=coupling_map, target=None, use_qubit_indices=True + ) + ) + + for pattern in itertools.permutations(range(4)): + + qc = QuantumCircuit(5) + qc.append(PermutationGate(pattern), [2, 0, 3, 1]) + self.assertIn("permutation", qc.count_ops()) + + qc_transpiled = pm.run(qc) + self.assertNotIn("permutation", qc_transpiled.count_ops()) + self.assertEqual(Operator(qc), Operator(qc_transpiled)) + + for inst in qc_transpiled: + qubits = tuple(qc_transpiled.find_bit(q).index for q in inst.qubits) + self.assertIn(qubits, edges) + + class TestHighLevelSynthesisModifiers(QiskitTestCase): """Tests for high-level-synthesis pass."""