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

Token swapper permutation synthesis plugin #10657

Merged
merged 29 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4181428
trying to implement permutation synthesis plugin based on token swapper
alexanderivrii Aug 14, 2023
ace8693
improving plugin and tests
alexanderivrii Aug 17, 2023
3f948d2
pylint fixes
alexanderivrii Aug 18, 2023
1d6ee58
Merge branch 'main' into token-swapper-permutation-plugin
alexanderivrii Aug 18, 2023
5858b3d
Merge branch 'token-swapper-permutation-plugin' of github.com:alexand…
alexanderivrii Aug 18, 2023
74ccc4b
exposing seed and parallel_threshold
alexanderivrii Aug 18, 2023
a689b02
release notes
alexanderivrii Aug 18, 2023
e725b37
clarification comment
alexanderivrii Aug 18, 2023
e3e2c3f
Merge branch 'main' into token-swapper-permutation-plugin
alexanderivrii Sep 15, 2023
816ac1e
Merge branch 'main' into token-swapper-permutation-plugin
alexanderivrii Sep 20, 2023
a4b97c7
improved support for disconnected coupling maps
alexanderivrii Sep 20, 2023
2eb7075
unused import
alexanderivrii Sep 20, 2023
8677dcb
fix arxiv reference
alexanderivrii Oct 5, 2023
e15d739
Merge branch 'main' into token-swapper-permutation-plugin
alexanderivrii Oct 18, 2023
ecd793e
Merge branch 'token-swapper-permutation-plugin' of github.com:alexand…
alexanderivrii Oct 18, 2023
9dd1f85
minor fix
alexanderivrii Oct 18, 2023
73e80a7
fix merge
alexanderivrii Oct 18, 2023
3389c31
more merge fixes
alexanderivrii Oct 18, 2023
fb8956d
Merge branch 'main' into token-swapper-permutation-plugin
alexanderivrii Oct 23, 2023
e111e63
fixing imports
alexanderivrii Oct 23, 2023
ef4e877
Merge branch 'main' into token-swapper-permutation-plugin
alexanderivrii Jan 25, 2024
e9b4766
updating toml file
alexanderivrii Jan 25, 2024
24ae130
additional fixes
alexanderivrii Jan 25, 2024
8e65d74
better way to find the position in the circuit
alexanderivrii Jan 25, 2024
f06dc65
Merge branch 'main' into token-swapper-permutation-plugin
alexanderivrii Jan 30, 2024
5e9ff47
bump rustworkx version to 0.14.0
alexanderivrii Jan 30, 2024
7886221
doc and autosummary improvements
alexanderivrii Jan 30, 2024
c3bd53b
Update plugin docs configuration
mtreinish Jan 30, 2024
b617f6f
Remove autosummary for available plugins list
mtreinish Jan 30, 2024
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
68 changes: 67 additions & 1 deletion qiskit/transpiler/passes/synthesis/high_level_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
"""Synthesize higher-level objects."""

from typing import Optional
import rustworkx as rx

from qiskit.circuit import QuantumCircuit
from qiskit.converters import circuit_to_dag
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.target import Target
from qiskit.transpiler.coupling import CouplingMap
from qiskit.dagcircuit.dagcircuit import DAGCircuit
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.exceptions import TranspilerError, CouplingError
from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper

from qiskit.synthesis.clifford import (
synth_clifford_full,
Expand Down Expand Up @@ -404,3 +407,66 @@ 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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add this class somewhere in the toctree to render the docstring here. Right now this class (nor any of the HLS plugins) is not being included in the documentation builds. I was talking to @Cryoris offline about this a bit the other day as there isn't a unified place to document this right now things are spread out a bit too much in the organizational structure. I think for right now if you added a HLS Plugins section to the module docstring in qiskit/transpiler/passes/synthesis/plugin.py and added an autosummary for this class that'd be enough. We can refactor the organizational structure in a follow up easily enough.

"""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 constraint. At the moment, the plugin
only works when `qubits` represents a connected subset of `coupling_map` (if this is
not the case, 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.

For more details on the token swapper algorithm, see to the paper:
`arXiv:1809.03452 <https://arxiv.org/abs/1809.03452>`__.

"""

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Permutation."""

pattern = high_level_object.pattern
trials = options.get("trials", 5)

pattern_as_dict = {j: i for i, j in enumerate(pattern)}

if coupling_map is None or qubits is None:
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
# 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. Currently, we require
# this set to be connected (otherwise, replacing the node in the DAGCircuit
# that defines this PermutationGate by the DAG corresponding to the constructed
# decomposition becomes problematic); note that the method `reduce` raises an
# error if the reduced coupling map is not connected.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to handle disconnected subgraphs if we work with the graph object directly. Something like:

graph = coupling_map.graph.subgraph(qubits).to_undirected()

The only issue with that is I don't think there is any guarantees on the index ordering from PyDiGraph.subgraph(). So you might need to do:

for i in coupling_map.graph.node_indices():
    coupling_map.graph[i] = i
graph = coupling_map.graph.subgraph(qubits).to_undirected()

then you you can use graph[j] to get the original index i. That being said I don't know what the approximate token swapper does if it can't fulfill the permutation because the graph is disconnected, so this might be the correct behavior anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a very interesting suggestion. By experimenting with the approximate token swapper, it seems that if there is a way to fulfill the permutation (on a disconnected subgraph), it will do so; but if there is no such way, it will throw a rather strange error pyo3_runtime.PanicException: IndexMap: key not found.

graph = rx.PyGraph()
graph.extend_from_edge_list([(0, 1), (2, 3)])
swapper = ApproximateTokenSwapper(graph)
swapper.map({1: 0, 0: 1, 2: 3, 3: 2}, 10)  # this works fine
swapper.map({2: 0, 1: 1, 0: 2, 3: 3}, 10)  # this throws 

Question: wrapping the second in the try-except block, outputs the following message (in pycharm):

thread '<unnamed>' panicked at 'IndexMap: key not found', C:\Users\274191756\Desktop\QiskitDevelopment\rustworkx\rustworkx-core\src\token_swapper.rs:211:16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Would this be a problem?

Another question: is it safe to modify the original coupling map: coupling_map.graph[i] = i, or would it be best to make a copy of that first?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, well that's a bug in rustworkx. A PanicException is the catch all for a Rust panic or an unhandled error and we shouldn't be panicking with an otherwise valid input. Catching a PanicException isn't super straightforward because of design decision in PyO3 it inherits from BaseException (which is the parent to Exception) to make it on the same level as other low level exceptions that aren't generally recoverable. The thinking being a panic is explicitly an unhandled error so nothing in Python should be able to deal with it. So I wouldn't try to catch it, we'll have to fix it in rustworkx.

As for coupling_map.graph[i] = i we don't explicitly reserve the use of the node weights for anything and if something does depend on them (like CouplingMap.connected_components()) will overwrite it. Although doing a graph copy is probably fairly lightweight as it's just a rust clone internally. So something like coupling_map.graph.copy() is probably a good idea.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed up a fix for this in rustworkx: Qiskit/rustworkx#971

Copy link
Contributor Author

@alexanderivrii alexanderivrii Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really want to support disjoint coupling maps in this and other synthesis plugins.

First, as explained in #10657 (comment), using coupling_map.graph.subgraph(qubits) instead of coupling_map.reduce(qubits) does not preserve the index ordering. This leads to a somewhat cumbersome solution that involves remapping the desired permutation to be over the subgraph's qubits (this is the input to the token swapper) and remapping the computed swaps (the output of token swapper) to be over the original qubits. This can be done, but the code is a bit tricky. Furthermore, every other synthesis plugin that is able to support disjoint coupling maps would also need to implement a similar tricky solution.

Do I understand correctly that disconnected coupling maps are now allowed in Qiskit? So an alternative would be to add an argument (something like allow_disconnected) to CouplingMap.reduce that would not error on disconnected coupling maps.

There are a few more minor changes required for CouplingMap.reduce. As an example, let's say that the original map is a ring over 8 qubits, and we want to reduce to qubits=(1, 3), the important thing is that some of the qubits become edgeless in the reduced map. The current code would not add such edgeless qubits to the reduced coupling map (as it adds new qubits when adding edges), but we should in fact add these.

@mtreinish, if you agree with the CouplingMap.reduce suggestions above, would it make sense to add these as a part of this PR or a separate PR?

The second problem is that there is still one edge case when TokenSwapper panics when handling disconnected coupling maps (in addition to Qiskit/rustworkx#971):

def test_disjoint_graph(self):
    graph = rx.PyGraph()
    graph.add_node(0)
    graph.add_node(1)
    graph.add_node(2)
    graph.add_node(3)
    swaps = rx.graph_token_swapper(graph, {1: 0, 0: 1, 2: 3, 3: 2}, 10, seed=42)

This panics as per

thread '<unnamed>' panicked at 'index out of bounds: the len is 0 but the index is 0', C:\Users\274191756\Desktop\QiskitDevelopment\rustworkx\rustworkx-core\src\token_swapper.rs:304:40

I would be happy to look into this. One additional question is whether we need to wait till the new version of rustworkx is released (including PR 971 and possibly additional fixes) before this PR could be merged?

try:
used_coupling_map = coupling_map.reduce(qubits)
except CouplingError:
return None

graph = rx.PyGraph()
graph.extend_from_edge_list(list(used_coupling_map.get_edges()))
swapper = ApproximateTokenSwapper(graph, seed=1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should seed ben an input option here? The other similar thought is the parallel_threshold argument to ApproximateTokenSwapper.map might also be worth exposing through the options interface, although that one is less clear to me because it's mostly just a function of runtime performance and not the actual synthesis quality.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have exposed both seed and parallel_threshold.

out = list(swapper.map(pattern_as_dict, trials))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the list cast here necessary? The EdgeList return should be iterable and should work in the for loop at L470 without issue. Doing a list cast here results in double iteration over the output swap list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

decomposition = QuantumCircuit(len(graph.node_indices()))
for swap in out:
decomposition.swap(*swap)
return decomposition
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
"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",
],
"qiskit.transpiler.routing": [
"basic = qiskit.transpiler.preset_passmanagers.builtin_plugins:BasicSwapPassManager",
Expand Down
161 changes: 158 additions & 3 deletions test/python/transpiler/test_high_level_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,20 @@
"""
Tests the interface for HighLevelSynthesis transpiler pass.
"""


import itertools
import unittest.mock

from qiskit.circuit import QuantumCircuit, Operation
from qiskit.circuit.library import PermutationGate, LinearFunction
from qiskit.test import QiskitTestCase
from qiskit.transpiler import PassManager, TranspilerError, CouplingMap
from qiskit.transpiler.passes.synthesis.plugin import HighLevelSynthesisPlugin
from qiskit.transpiler.passes.synthesis.plugin import (
HighLevelSynthesisPlugin,
HighLevelSynthesisPluginManager,
)
from qiskit.transpiler.passes.synthesis.high_level_synthesis import HighLevelSynthesis, HLSConfig
from qiskit.providers.fake_provider.fake_backend_v2 import FakeBackend5QV2
from qiskit.quantum_info import Operator


# In what follows, we create two simple operations OpA and OpB, that potentially mimic
Expand Down Expand Up @@ -462,5 +466,156 @@ 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})])
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.
In this case the plugin should return `None` and `HighLevelSynthesis`
should not change the original circuit.
"""

# Permutation gate
perm = PermutationGate([4, 3, 2, 1, 0])

# Circuit with permutation gate
qc = QuantumCircuit(10)
qc.append(perm, [0, 2, 4, 6, 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)
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([q.index for q in inst.qubits])
self.assertIn(qubits, edges)


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