From 738d3e401295547de1d55c621a199bcbb60102b8 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 30 Jan 2024 23:31:13 +0200 Subject: [PATCH] Token swapper permutation synthesis plugin (#10657) * trying to implement permutation synthesis plugin based on token swapper * improving plugin and tests * pylint fixes * exposing seed and parallel_threshold * release notes * clarification comment * improved support for disconnected coupling maps * unused import * fix arxiv reference * minor fix * fix merge * more merge fixes * fixing imports * updating toml file * additional fixes * better way to find the position in the circuit * bump rustworkx version to 0.14.0 * doc and autosummary improvements * Update plugin docs configuration * Remove autosummary for available plugins list This commit removes the autosummary directives for building the documentation for the plugin classes. In the interest of time and combining it with the existing aqc docs we'll do this in a follow up for 1.0.0 after 1.0.0rc1 has been tagged. --------- Co-authored-by: Matthew Treinish --- pyproject.toml | 1 + .../passes/synthesis/high_level_synthesis.py | 82 +++++++- qiskit/transpiler/passes/synthesis/plugin.py | 31 ++- ...per-synthesis-plugin-4ed5009f5f21519d.yaml | 44 ++++ requirements.txt | 2 +- .../transpiler/test_high_level_synthesis.py | 190 +++++++++++++++++- 6 files changed, 344 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/add-token-swapper-synthesis-plugin-4ed5009f5f21519d.yaml 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 7c40a75e0b83..0a4f0f32c5a9 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."""