Skip to content

Commit 65b2f27

Browse files
pehamTomburgholzer
andauthored
✨ Sub-Architectures (#106)
Added functionality for computing sub-architectures for qubit mapping based on https://arxiv.org/abs/2210.09321. Signed-off-by: burgholzer <burgholzer@me.com> Co-authored-by: burgholzer <burgholzer@me.com>
1 parent 98486ca commit 65b2f27

File tree

13 files changed

+577
-3
lines changed

13 files changed

+577
-3
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,8 @@ _In Asia and South Pacific Design Automation Conference (ASP-DAC)_, 2021.
7272
L. Burgholzer, S. Schneider, and R. Wille. Limiting the Search Space in Optimal Quantum Circuit Mapping.
7373
_In Asia and South Pacific Design Automation Conference (ASP-DAC)_, 2022.
7474

75+
[[5]](https://arxiv.org/pdf/2210.09321.pdf)
76+
T. Peham, L. Burgholzer, and R. Wille. On Optimal Subarchitectures for Quantum Circuit Mapping.
77+
_arXiv:2210.09321_, 2022.
78+
7579
[^1]: The Munich Quantum Toolkit was formerly known under the acronym _JKQ_ and developed by the [Institute for Integrated Circuits](https://iic.jku.at/eda/) at the [Johannes Kepler University Linz](https://jku.at)).

docs/source/Publications.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Furthermore, if you use any of the particular algorithms such as
88

99
- the heuristic mapping scheme using teleportation :cite:labelpar:`hillmichExlpoitingQuantumTeleportation2021`
1010
- the search space limitation techniques of the exact mapper (some of which are enabled per default) :cite:labelpar:`burgholzer2022limitingSearchSpace`
11+
- the method for finding (near-)optimal subarchitectures :cite:labelpar:`peham2022OptimalSubarchitectures`
1112

1213
please consider citing their respective papers as well. A full list of related papers is given below.
1314

docs/source/library/Library.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Library
77
Mapping
88
Architecture
99
MappingResults
10+
Subarchitectures
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Optimal Subarchitectures
2+
========================
3+
4+
To compute (near-)optimal subarchitectures of quantum computing architectures with restricted connectivity as described in :cite:labelpar:`peham2022OptimalSubarchitectures` the :code:`SubarchitectureOrder` class is provided. This class has functionality to compute the quasi-order that allows for fast computation of optimal subarchitectures.
5+
6+
Note that the initial construction of the ordering might take a while for larger architectures.
7+
8+
.. currentmodule:: mqt.qmap
9+
.. autoclass:: SubarchitectureOrder
10+
:members:
11+
12+
QMAP also provides precomputed subarchitecture libraries. The available libraries are available via:
13+
14+
.. autoattribute:: subarchitectures.precomputed_backends
15+
16+
Convenience methods are provided to import these precomputed orderings:
17+
18+
.. automethod:: subarchitectures.ibm_guadalupe_subarchitectures
19+
.. automethod:: subarchitectures.rigetti_16_subarchitectures

docs/source/refs.bib

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,11 @@ @inproceedings{boteaComplexityQuantumCircuit2018
5050
author = {Botea, A. and Kishimoto, A. and Marinescu, Radu},
5151
year = {2018},
5252
}
53+
54+
@inproceedings{peham2022OptimalSubarchitectures,
55+
title = {On Optimal Subarchitectures for Quantum Circuit Mapping},
56+
author = {Peham, Tom and Burgholzer, Lukas and Wille, Robert},
57+
booktitle = {arXiv:2210.09321},
58+
year = {2022},
59+
url = {https://arxiv.org/pdf/2210.09321.pdf},
60+
}

extern/LogicBlocks

extern/qfr

Submodule qfr updated from 76dfda6 to 6e4924a

mqt/qmap/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Method,
2929
SwapReduction,
3030
)
31+
from mqt.qmap.subarchitectures import SubarchitectureOrder
3132

3233
__all__ = [
3334
"compile",
@@ -41,4 +42,5 @@
4142
"Configuration",
4243
"MappingResults",
4344
"Architecture",
45+
"SubarchitectureOrder",
4446
]

mqt/qmap/libs/ibm_guadalupe_16.pickle

164 KB
Binary file not shown.

mqt/qmap/libs/rigetti_16.pickle

328 KB
Binary file not shown.

mqt/qmap/subarchitectures.py

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
"""
2+
Functionality for computing good subarchitectures for quantum circuit mapping.
3+
4+
This file implements the methods presented in https://arxiv.org/abs/2210.09321.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import sys
10+
11+
if sys.version_info < (3, 10, 0):
12+
import importlib_resources as resources
13+
else:
14+
from importlib import resources
15+
16+
import pickle
17+
from itertools import combinations
18+
from pathlib import Path
19+
from typing import Dict, NewType, Set, Tuple
20+
21+
import retworkx as rx
22+
from matplotlib import figure
23+
from mqt.qmap import Architecture
24+
25+
from qiskit.providers import Backend
26+
27+
PartialOrder = NewType("PartialOrder", Dict[Tuple[int, int], Set[Tuple[int, int]]])
28+
29+
#: Architectures for which precomputed orderings are available
30+
precomputed_backends = ["rigetti_16", "ibm_guadalupe_16"]
31+
32+
33+
class SubarchitectureOrder:
34+
"""Class representing partial order of Subarchitectures."""
35+
36+
__inactive_color: str = "#1f78b4"
37+
__active_color: str = "#faf18e"
38+
39+
def __init__(self) -> None:
40+
"""Construct SubarchitectureOrder with default fields."""
41+
self.arch: rx.PyGraph = rx.PyGraph()
42+
self.subarch_order: PartialOrder = PartialOrder({})
43+
self.desirable_subarchitectures: PartialOrder = PartialOrder({})
44+
self.__isomorphisms: dict[tuple[int, int], dict[tuple[int, int], dict[int, int]]] = {}
45+
46+
self.__compute_subarchs()
47+
self.__compute_subarch_order()
48+
self.__compute_desirable_subarchitectures()
49+
return
50+
51+
@classmethod
52+
def from_retworkx_graph(cls, graph: rx.PyGraph) -> SubarchitectureOrder:
53+
"""Construct SubarchitectureOrder from retworkx graph."""
54+
so = SubarchitectureOrder()
55+
so.arch = graph
56+
57+
so.__compute_subarchs()
58+
so.__compute_subarch_order()
59+
so.__compute_desirable_subarchitectures()
60+
return so
61+
62+
@classmethod
63+
def from_coupling_map(cls, coupling_map: set[tuple[int, int]] | list[tuple[int, int]]) -> SubarchitectureOrder:
64+
"""Construct SubarchitectureOrder from coupling map defined as set of tuples of connected qubits."""
65+
num_nodes = max(max(int(u), int(v)) for u, v in coupling_map)
66+
graph = rx.PyGraph()
67+
graph.add_nodes_from(list(range(num_nodes + 1)))
68+
graph.add_edges_from_no_data([tuple(edge) for edge in coupling_map])
69+
70+
return cls.from_retworkx_graph(graph)
71+
72+
@classmethod
73+
def from_backend(cls, backend: Backend) -> SubarchitectureOrder:
74+
"""Construct SubarchitectureOrder from coupling map defined by qiskit backend."""
75+
coupling_map = {(a, b) for a, b in backend.configuration().coupling_map}
76+
return cls.from_coupling_map(coupling_map)
77+
78+
@classmethod
79+
def from_qmap_architecture(cls, arch: Architecture) -> SubarchitectureOrder:
80+
"""Construct SubarchitectureOrder from qmap Architecture object."""
81+
return cls.from_coupling_map(arch.coupling_map)
82+
83+
@classmethod
84+
def from_library(cls, lib_name: str | Path) -> SubarchitectureOrder:
85+
"""Construct SubarchitectureOrder from stored library."""
86+
path = Path(lib_name).with_suffix(".pickle")
87+
with path.open("rb") as f:
88+
temp = pickle.load(f)
89+
90+
so = SubarchitectureOrder()
91+
so.__dict__.update(temp.__dict__)
92+
93+
return so
94+
95+
@classmethod
96+
def from_string(cls, path: str) -> SubarchitectureOrder:
97+
"""Construct SubarchitectureOrder from library name."""
98+
if path in precomputed_backends:
99+
ref = resources.files("mqt.qmap") / "libs" / (path + ".pickle")
100+
with resources.as_file(ref) as lib_path:
101+
return cls.from_library(lib_path)
102+
return SubarchitectureOrder()
103+
104+
def optimal_candidates(self, nqubits: int) -> list[rx.PyGraph]:
105+
"""Return optimal subarchitecture candidate.
106+
107+
nqubits : int
108+
size of circuit for which the optimal candidate should be given.
109+
"""
110+
if nqubits <= 0 or nqubits > self.arch.num_nodes():
111+
raise ValueError(
112+
"Number of qubits must not be smaller or equal 0 or larger then number of physical qubits of architecture."
113+
)
114+
115+
if nqubits == self.arch.num_nodes():
116+
return [self.arch]
117+
118+
cands = self.__cand(nqubits)
119+
trans_ord = self.__transitive_closure(self.subarch_order)
120+
ref_ord = self.__reflexive_closure(trans_ord)
121+
122+
opt_cands = set(ref_ord[next(iter(cands))])
123+
for cand in cands:
124+
opt_cands = opt_cands.intersection(set(ref_ord[cand]))
125+
126+
ordered_cands = list(opt_cands)
127+
ordered_cands.sort()
128+
for cand in ordered_cands:
129+
opt_cands = opt_cands.difference(trans_ord[cand])
130+
131+
return [self.sgs[n][i] for (n, i) in opt_cands]
132+
133+
def covering(self, nqubits: int, size: int) -> list[rx.PyGraph]:
134+
"""
135+
Return covering for nqubit circuits.
136+
137+
The size of the covering is limited by size.
138+
Note that a smaller covering might be found.
139+
"""
140+
cov = self.__cand(nqubits)
141+
po_trans = self.__transitive_closure(self.subarch_order)
142+
ref_trans_po = self.__reflexive_closure(po_trans)
143+
queue = list({el for cand in cov for el in ref_trans_po[cand]})
144+
queue.sort(reverse=True)
145+
146+
po_inv = self.__inverse_relation(po_trans)
147+
148+
while len(cov) > size:
149+
d = queue.pop()
150+
cov_d = cov.intersection(po_inv[d])
151+
if len(cov_d) > 1:
152+
cov = cov.difference(cov_d)
153+
cov.add(d)
154+
155+
return [self.sgs[n][i] for n, i in cov]
156+
157+
def store_library(self, lib_name: str | Path) -> None:
158+
"""Store ordering."""
159+
path = Path(lib_name).with_suffix(".pickle")
160+
with path.open("wb") as f:
161+
pickle.dump(self, f)
162+
163+
def draw_subarchitecture(self, subarchitecture: rx.PyGraph | tuple[int, int]) -> figure.Figure:
164+
"""Return matplotlib figure showing subarchitecture within the entire architecture.
165+
166+
Nodes that are part of the subarchitecture are drawn yellow.
167+
Nodes that are not part of the subarchitecture are drawn blue.
168+
"""
169+
if isinstance(subarchitecture, tuple):
170+
subarchitecture = self.sgs[subarchitecture[0]][subarchitecture[1]]
171+
colors = [SubarchitectureOrder.__inactive_color for node in range(self.arch.num_nodes())]
172+
for node in subarchitecture.nodes():
173+
colors[node] = SubarchitectureOrder.__active_color
174+
return rx.visualization.mpl_draw(subarchitecture, node_color=colors)
175+
176+
def draw_subarchitectures(self, subarchitectures: list[rx.PyGraph] | list[tuple[int, int]]) -> list[figure.Figure]:
177+
"""Return matplotlib figures showing subarchitectures within the entire architecture.
178+
179+
For each subarchitecture one figure is drawn.
180+
Nodes that are part of the subarchitecture are drawn yellow.
181+
Nodes that are not part of the subarchitecture are drawn blue.
182+
"""
183+
return [self.draw_subarchitecture(subarchitecture) for subarchitecture in subarchitectures]
184+
185+
def __compute_subarchs(self) -> None:
186+
self.sgs: list[list[rx.PyGraph]] = [[] for i in range(self.arch.num_nodes() + 1)]
187+
188+
for i in range(1, self.arch.num_nodes() + 1):
189+
node_combinations = combinations(range(self.arch.num_nodes()), i)
190+
for sg in (self.arch.subgraph(selected_nodes) for selected_nodes in node_combinations):
191+
if rx.is_connected(sg):
192+
new_class = True
193+
for g in self.sgs[i]:
194+
if rx.is_isomorphic(g, sg):
195+
new_class = False
196+
break
197+
if new_class:
198+
self.sgs[i].append(sg)
199+
# init orders
200+
for n in range(self.arch.num_nodes() + 1):
201+
for i in range(len(self.sgs[n])):
202+
self.subarch_order[(n, i)] = set()
203+
self.desirable_subarchitectures[(n, i)] = set()
204+
self.__isomorphisms[(n, i)] = {}
205+
206+
def __compute_subarch_order(self) -> None:
207+
for n, sgs_n in enumerate(self.sgs[:-1]):
208+
for i, sg in enumerate(sgs_n):
209+
for j, parent_sg in enumerate(self.sgs[n + 1]):
210+
matcher = rx.graph_vf2_mapping(parent_sg, sg, subgraph=True)
211+
for iso in matcher:
212+
self.subarch_order[(n, i)].add((n + 1, j))
213+
iso_rev = {}
214+
for key, val in iso.items():
215+
iso_rev[val] = key
216+
self.__isomorphisms[(n, i)][(n + 1, j)] = iso_rev
217+
break # One isomorphism suffices
218+
219+
def __complete_isos(self) -> None:
220+
for n in reversed(range(1, len(self.sgs[:-1]))):
221+
for i in range(len(self.sgs[n])):
222+
for _, i_prime in self.subarch_order[(n, i)]:
223+
self.__combine_iso_with_parent(n, i, i_prime)
224+
225+
def __combine_iso_with_parent(self, n: int, i: int, j: int) -> None:
226+
"""Combine all isomorphisms from sgs[n][i] with those from sgs[n+1][j]."""
227+
first = self.__isomorphisms[(n, i)][(n + 1, j)]
228+
for (row, k), second in self.__isomorphisms[(n + 1, j)].items():
229+
self.__isomorphisms[(n, i)][(row, k)] = SubarchitectureOrder.__combine_isos(first, second)
230+
231+
@staticmethod
232+
def __combine_isos(first: dict[int, int], second: dict[int, int]) -> dict[int, int]:
233+
combined = {}
234+
for src, img in first.items():
235+
combined[src] = second[img]
236+
return combined
237+
238+
def __transitive_closure(self, po: PartialOrder) -> PartialOrder:
239+
po_trans: PartialOrder = PartialOrder({})
240+
po_trans[self.arch.num_nodes(), 0] = set()
241+
242+
for n in reversed(range(1, len(self.sgs[:-1]))):
243+
for i in range(len(self.sgs[n])):
244+
new_rel = set(po[(n, i)])
245+
po_trans[(n, i)] = new_rel.copy()
246+
for n_prime, i_prime in po_trans[(n, i)]:
247+
new_rel = new_rel.union(po_trans[(n_prime, i_prime)])
248+
po_trans[(n, i)] = new_rel
249+
250+
return po_trans
251+
252+
def __reflexive_closure(self, po: PartialOrder) -> PartialOrder:
253+
po_ref = PartialOrder({})
254+
for k, v in po.items():
255+
v_copy = v.copy()
256+
v_copy.add(k)
257+
po_ref[k] = v_copy
258+
return po_ref
259+
260+
def __inverse_relation(self, po: PartialOrder) -> PartialOrder:
261+
po_inv = PartialOrder({})
262+
for n in range(self.arch.num_nodes() + 1):
263+
for i in range(len(self.sgs[n])):
264+
po_inv[(n, i)] = set()
265+
for k, v in po.items():
266+
for e in v:
267+
po_inv[e].add(k)
268+
return po_inv
269+
270+
def __path_order_less(self, n: int, i: int, n_prime: int, i_prime: int) -> bool:
271+
lhs = self.sgs[n][i]
272+
rhs = self.sgs[n_prime][i_prime]
273+
iso = self.__isomorphisms[(n, i)][(n_prime, i_prime)]
274+
for v in range(lhs.num_nodes()):
275+
for w in range(lhs.num_nodes()):
276+
if v is w:
277+
continue
278+
if (
279+
rx.dijkstra_shortest_path_lengths(lhs, v, lambda x: 1, goal=w)[w]
280+
> rx.dijkstra_shortest_path_lengths(rhs, iso[v], lambda x: 1, goal=iso[w])[iso[w]]
281+
):
282+
return True
283+
return False
284+
285+
def __compute_desirable_subarchitectures(self) -> None:
286+
self.__complete_isos()
287+
for n in reversed(range(1, len(self.sgs[:-1]))):
288+
for i in range(len(self.sgs[n])):
289+
val = self.__isomorphisms[(n, i)]
290+
for n_prime, i_prime in val.keys():
291+
if self.__path_order_less(n, i, n_prime, i_prime):
292+
self.desirable_subarchitectures[(n, i)].add((n_prime, i_prime))
293+
des = list(self.desirable_subarchitectures[(n, i)])
294+
des.sort()
295+
new_des: set[tuple[int, int]] = set()
296+
for j, (n_prime, i_prime) in enumerate(reversed(des)):
297+
j = len(des) - j - 1
298+
if not any([(n_prime, i_prime) in self.subarch_order[k] for k in des[:j]]):
299+
new_des.add((n_prime, i_prime))
300+
301+
self.desirable_subarchitectures[(n, i)] = new_des
302+
if len(self.desirable_subarchitectures[(n, i)]) == 0:
303+
self.desirable_subarchitectures[(n, i)].add((n, i))
304+
self.desirable_subarchitectures[self.arch.num_nodes(), 0] = {(self.arch.num_nodes(), 0)}
305+
306+
def __cand(self, nqubits: int) -> set[tuple[int, int]]:
307+
return {
308+
des for (n, i), desirables in self.desirable_subarchitectures.items() if n == nqubits for des in desirables
309+
}
310+
311+
312+
def ibm_guadalupe_subarchitectures() -> SubarchitectureOrder:
313+
"""Load the precomputed ibm guadalupe subarchitectures."""
314+
ref = resources.files("mqt.qmap") / "libs" / "ibm_guadalupe_16.pickle"
315+
with resources.as_file(ref) as path:
316+
return SubarchitectureOrder.from_library(path)
317+
318+
319+
def rigetti_16_subarchitectures() -> SubarchitectureOrder:
320+
"""Load the precomputed rigetti subarchitectures."""
321+
ref = resources.files("mqt.qmap") / "libs" / "rigetti_16.pickle"
322+
with resources.as_file(ref) as path:
323+
return SubarchitectureOrder.from_library(path)

0 commit comments

Comments
 (0)