qc-parallelizer 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qc_parallelizer/__init__.py +1 -0
- qc_parallelizer/base.py +35 -0
- qc_parallelizer/generic/backendtools.py +14 -0
- qc_parallelizer/generic/circuittools.py +240 -0
- qc_parallelizer/generic/generic.py +45 -0
- qc_parallelizer/generic/layouts.py +246 -0
- qc_parallelizer/packing.py +298 -0
- qc_parallelizer/parallelizer.py +302 -0
- qc_parallelizer/postprocessing.py +176 -0
- qc_parallelizer/transpiling.py +227 -0
- qc_parallelizer-1.0.0.dist-info/METADATA +97 -0
- qc_parallelizer-1.0.0.dist-info/RECORD +14 -0
- qc_parallelizer-1.0.0.dist-info/WHEEL +5 -0
- qc_parallelizer-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .parallelizer import describe, execute, rearrange
|
qc_parallelizer/base.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import qiskit
|
|
2
|
+
import qiskit.circuit
|
|
3
|
+
import qiskit.providers
|
|
4
|
+
import qiskit.result
|
|
5
|
+
import qiskit.transpiler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Types:
|
|
9
|
+
Layout = list | dict | qiskit.transpiler.Layout
|
|
10
|
+
Backend = qiskit.providers.BackendV2
|
|
11
|
+
Result = qiskit.result.result.Result
|
|
12
|
+
Qubit = qiskit.circuit.Qubit
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Exceptions:
|
|
16
|
+
class MissingParameter(Exception):
|
|
17
|
+
"""A required parameter or part of a parameter is missing."""
|
|
18
|
+
|
|
19
|
+
class MissingInformation(Exception):
|
|
20
|
+
"""
|
|
21
|
+
Required information is missing from data. For example, a circuit object was passed that
|
|
22
|
+
does not contain required metadata.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
class ParameterConflict(Exception):
|
|
26
|
+
"""Two or more passed parameters or parts of parameters are in mutual conflict."""
|
|
27
|
+
|
|
28
|
+
class CircuitBackendCompatibility(Exception):
|
|
29
|
+
"""One or more given circuits cannot be executed on any given backends."""
|
|
30
|
+
|
|
31
|
+
class InvalidLayout(Exception):
|
|
32
|
+
"""
|
|
33
|
+
One or more given circuit layouts are not valid. This can be due to incompleteness or
|
|
34
|
+
duplicate/overlapping definitions.
|
|
35
|
+
"""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import qiskit.providers
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_neighbor_sets(backend: qiskit.providers.BackendV2) -> list[set[int]]:
|
|
5
|
+
"""
|
|
6
|
+
Returns sets of physical neighbors in the backend's topology.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
edges = backend.coupling_map.get_edges()
|
|
10
|
+
neighbors = [set() for _ in range(backend.num_qubits)]
|
|
11
|
+
for from_, to in edges:
|
|
12
|
+
neighbors[from_].add(to)
|
|
13
|
+
neighbors[to].add(from_)
|
|
14
|
+
return neighbors
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import qiskit
|
|
5
|
+
|
|
6
|
+
from . import layouts
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def count_gates(circuit: qiskit.QuantumCircuit):
|
|
10
|
+
"""
|
|
11
|
+
Returns gate counts for each qubit in the given circuit, including measurements, but excluding
|
|
12
|
+
barriers. For example, for a circuit that looks like this:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
┌───┐ ┌─┐
|
|
16
|
+
q_0: ┤ H ├───────■──┤M├───
|
|
17
|
+
├───┤┌───┐┌─┴─┐└╥┘┌─┐
|
|
18
|
+
q_1: ┤ X ├┤ H ├┤ X ├─╫─┤M├
|
|
19
|
+
├───┤└┬─┬┘└───┘ ║ └╥┘
|
|
20
|
+
q_2: ┤ H ├─┤M├───────╫──╫─
|
|
21
|
+
└───┘ └╥┘ ║ ║
|
|
22
|
+
c: 3/═══════╩════════╩══╩═
|
|
23
|
+
2 0 1
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The returned counts would look like this:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
{q_0: 3, q_1: 4, q_2: 2}
|
|
30
|
+
```
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
gate_count = {qubit: 0 for qubit in circuit.qubits}
|
|
34
|
+
for operation, qubits, _ in circuit.data:
|
|
35
|
+
if operation.name == "barrier":
|
|
36
|
+
continue
|
|
37
|
+
for qubit in qubits:
|
|
38
|
+
gate_count[qubit] += 1
|
|
39
|
+
return gate_count
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def remove_idle_qubits(
|
|
43
|
+
circuit: qiskit.QuantumCircuit,
|
|
44
|
+
layout: layouts.QILayout | None = None,
|
|
45
|
+
allow_layout_replacement: bool = True,
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Removes idle qubits from a circuit. An idle qubit is a qubit that no operations, including
|
|
49
|
+
measurements, touch during the execution of the circuit. A layout for the circuit may also be
|
|
50
|
+
provided via the `layout` parameter, which will be updated to not contain the idle qubits.
|
|
51
|
+
|
|
52
|
+
Registers are immutable, so the operation is done by reconstructing the circuit gate by gate
|
|
53
|
+
with a reduced set of registers. Register names are lost in the process, and all remaining
|
|
54
|
+
active qubits are placed in a new quantum register.
|
|
55
|
+
|
|
56
|
+
If the circuit contains layout information, which is the case after transpilation, this function
|
|
57
|
+
will read that information to determine original register names. The resulting pruned circuit
|
|
58
|
+
will not contain layout information, since removing qubits violates some assumptions that other
|
|
59
|
+
functionality in Qiskit relies on. If you wish to force this function to ignore the layout data,
|
|
60
|
+
though, set `allow_layout_replacement` to `False` - this will use original register names, but
|
|
61
|
+
not the embedded layout.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
if circuit.layout is not None and allow_layout_replacement:
|
|
65
|
+
layout = layouts.QILayout.from_circuit(circuit)
|
|
66
|
+
|
|
67
|
+
gate_count = count_gates(circuit)
|
|
68
|
+
idle_indices = {index for index, qubit in enumerate(circuit.qubits) if gate_count[qubit] == 0}
|
|
69
|
+
if len(idle_indices) == 0:
|
|
70
|
+
if layout is None:
|
|
71
|
+
return circuit
|
|
72
|
+
return circuit, layout
|
|
73
|
+
|
|
74
|
+
active_qubits = [
|
|
75
|
+
qubit for index, qubit in enumerate(circuit.qubits) if index not in idle_indices
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
qreg_mapping = {}
|
|
79
|
+
new_qreg = qiskit.QuantumRegister(len(active_qubits))
|
|
80
|
+
for new_index, old_qubit in enumerate(active_qubits):
|
|
81
|
+
qreg_mapping[old_qubit] = new_qreg[new_index]
|
|
82
|
+
|
|
83
|
+
new_circuit = qiskit.QuantumCircuit(
|
|
84
|
+
new_qreg,
|
|
85
|
+
*circuit.cregs,
|
|
86
|
+
name=circuit.name,
|
|
87
|
+
global_phase=circuit.global_phase,
|
|
88
|
+
metadata=circuit.metadata,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
for operation, qubits, clbits in circuit.data:
|
|
92
|
+
new_qubits = [qreg_mapping[qubit] for qubit in qubits if qubit in qreg_mapping]
|
|
93
|
+
if len(new_qubits) != len(qubits):
|
|
94
|
+
# Some operations need to be adjusted. Currently, this is only the case for barriers,
|
|
95
|
+
# since they "operate" on registers, but they do not affect a qubit's activity. So, if
|
|
96
|
+
# we encounter a barrier that was placed partially on active qubits, we lower the qubit
|
|
97
|
+
# count. This does not seem to have any side effects.
|
|
98
|
+
|
|
99
|
+
operation = operation.copy()
|
|
100
|
+
operation.num_qubits = len(new_qubits)
|
|
101
|
+
new_circuit.append(operation, new_qubits, clbits)
|
|
102
|
+
|
|
103
|
+
if layout is not None:
|
|
104
|
+
new_layout = layout.copy()
|
|
105
|
+
|
|
106
|
+
# Since we are dealing with indices, which, inherently, keep pointing at the same index even
|
|
107
|
+
# if the underlying array shifts, we must iterate in decreasing order to not invalidate
|
|
108
|
+
# later indices
|
|
109
|
+
modified = False
|
|
110
|
+
for index in sorted(idle_indices, reverse=True):
|
|
111
|
+
if index in new_layout.vkeys:
|
|
112
|
+
new_layout.remove(virt=index, decrement_keys=True)
|
|
113
|
+
modified = True
|
|
114
|
+
|
|
115
|
+
# If the layout was not modified, the new object is discarded and the old layout is returned
|
|
116
|
+
# instead - this avoids unnecessary copies in memory
|
|
117
|
+
return new_circuit, (new_layout if modified else layout)
|
|
118
|
+
|
|
119
|
+
return new_circuit
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def pad_to_width(circuit: qiskit.QuantumCircuit, width: int, in_place=True):
|
|
123
|
+
"""
|
|
124
|
+
Pads a circuit with unused qubits to the specified width. The newly added padding qubits will be
|
|
125
|
+
placed in a new single quantum register, called "padding".
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
num_padding = width - circuit.num_qubits
|
|
129
|
+
if num_padding > 0:
|
|
130
|
+
if not in_place:
|
|
131
|
+
circuit = circuit.copy()
|
|
132
|
+
circuit.add_register(qiskit.QuantumRegister(num_padding, name="padding"))
|
|
133
|
+
return circuit
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_neighbor_sets(circuit: qiskit.QuantumCircuit) -> list[set[int]]:
|
|
137
|
+
"""
|
|
138
|
+
Returns a list of sets of indices that represent all neighbors that qubits have in the given
|
|
139
|
+
circuit. For non-transpiled circuits, this may also mean other than physically immediate
|
|
140
|
+
neighbors - as a counterexample, a qubit that interacts with every other qubit will have all
|
|
141
|
+
other qubits in its neighbor set, even if that is physically impossible.
|
|
142
|
+
|
|
143
|
+
From another point of view, each set represents which qubits the corresponding qubit interacts
|
|
144
|
+
with during the execution of the circuit. Barriers are not counted as interaction.
|
|
145
|
+
|
|
146
|
+
For example, for the circuit below,
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
q_0: ──■────■────■──
|
|
150
|
+
│ ┌─┴─┐ │
|
|
151
|
+
q_1: ──■──┤ X ├──┼──
|
|
152
|
+
┌─┴─┐└───┘ │
|
|
153
|
+
q_2: ┤ X ├───────┼──
|
|
154
|
+
└───┘ ┌─┴─┐
|
|
155
|
+
q_3: ──────────┤ X ├
|
|
156
|
+
└───┘
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
this function returns
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
[{1, 2, 3}, {0, 2}, {0, 1}, {0}]
|
|
163
|
+
```
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
neighbors = [set() for _ in range(circuit.num_qubits)]
|
|
167
|
+
for operation, qubits, _ in circuit.data:
|
|
168
|
+
if operation.name == "barrier":
|
|
169
|
+
continue
|
|
170
|
+
qubit_indices = [circuit.find_bit(qb).index for qb in qubits]
|
|
171
|
+
for i, qb_i in enumerate(qubit_indices):
|
|
172
|
+
for qb_j in qubit_indices[i + 1 :]:
|
|
173
|
+
neighbors[qb_i].add(qb_j)
|
|
174
|
+
neighbors[qb_j].add(qb_i)
|
|
175
|
+
return neighbors
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def combine_for_backend(
|
|
179
|
+
circuits: list[tuple[qiskit.QuantumCircuit, Any, layouts.QILayout]],
|
|
180
|
+
backend: qiskit.providers.BackendV2,
|
|
181
|
+
name: str | None = None,
|
|
182
|
+
):
|
|
183
|
+
"""
|
|
184
|
+
This function combines multiple circuits into a larger host circuit in a backend-aware manner.
|
|
185
|
+
It requires a list of circuits, each of which must be represented by a (circuit, metadata,
|
|
186
|
+
layout) triple. The returned circuit's width is the same as the backend size.
|
|
187
|
+
|
|
188
|
+
The second element of the triple (metadata) is embedded into the circuit's "internal metadata".
|
|
189
|
+
This allows for internal metadata to be stored, such as the index of the circuit in some
|
|
190
|
+
original ordering, while preserving the actual metadata of the circuit.
|
|
191
|
+
|
|
192
|
+
The resulting circuit will contain metadata about the hosted circuits that the user should not
|
|
193
|
+
touch.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
host_circuit = qiskit.QuantumCircuit(
|
|
197
|
+
backend.num_qubits,
|
|
198
|
+
metadata={"_hosted_circuits": []},
|
|
199
|
+
name=name or f"{backend.num_qubits}-qubit host",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
for index, (subcircuit, metadata, layout) in enumerate(circuits):
|
|
203
|
+
creg_mapping = {}
|
|
204
|
+
for old_reg in subcircuit.cregs:
|
|
205
|
+
new_reg = qiskit.ClassicalRegister(old_reg.size, name=f"circ{index}.{old_reg.name}")
|
|
206
|
+
for i in range(old_reg.size):
|
|
207
|
+
creg_mapping[old_reg[i]] = new_reg[i]
|
|
208
|
+
host_circuit.add_register(new_reg)
|
|
209
|
+
|
|
210
|
+
host_circuit.metadata["_hosted_circuits"].append(
|
|
211
|
+
{
|
|
212
|
+
"name": subcircuit.name,
|
|
213
|
+
"original_metadata": subcircuit.metadata,
|
|
214
|
+
"internal_metadata": metadata,
|
|
215
|
+
"registers": {
|
|
216
|
+
"clbit": {"sizes": {f"{reg.name}": reg.size for reg in subcircuit.cregs}},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
qreg_indices = {
|
|
222
|
+
virt_qubit: subcircuit.find_bit(virt_qubit).index for virt_qubit in subcircuit.qubits
|
|
223
|
+
}
|
|
224
|
+
qreg_mapping = {
|
|
225
|
+
virt_qubit: host_circuit.qubits[layout[qreg_indices[virt_qubit]]]
|
|
226
|
+
for virt_qubit in subcircuit.qubits
|
|
227
|
+
if qreg_indices[virt_qubit] in layout
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for operation, qubits, clbits in subcircuit.data:
|
|
231
|
+
if any(qubit not in qreg_mapping for qubit in qubits):
|
|
232
|
+
print(f"oh noes! instruction '{operation.name}' skipped") # TODO: warn
|
|
233
|
+
continue
|
|
234
|
+
host_circuit.append(
|
|
235
|
+
operation,
|
|
236
|
+
[qreg_mapping[qubit] for qubit in qubits],
|
|
237
|
+
[creg_mapping[clbit] for clbit in clbits],
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return host_circuit
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
def get_connected_qubit_sets(
|
|
2
|
+
neighbors: list[set[int]],
|
|
3
|
+
) -> list[set[int]]:
|
|
4
|
+
"""
|
|
5
|
+
Returns a list of sets of connected qubits. Qubits are separated into two different sets if
|
|
6
|
+
there is no path along the gates/couplers between them.
|
|
7
|
+
|
|
8
|
+
The `neighbors` parameter can be computed with `get_neighbot_sets` from either `circuit_tools`
|
|
9
|
+
or `backend_tools`, depending on which you are working with.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
not_seen = set(range(len(neighbors)))
|
|
13
|
+
connected_sets = []
|
|
14
|
+
|
|
15
|
+
while len(not_seen) > 0:
|
|
16
|
+
seen = set()
|
|
17
|
+
search_set = {not_seen.pop()}
|
|
18
|
+
while len(search_set) > 0:
|
|
19
|
+
qubit_index = search_set.pop()
|
|
20
|
+
seen.add(qubit_index)
|
|
21
|
+
for nb in neighbors[qubit_index]:
|
|
22
|
+
if nb in not_seen:
|
|
23
|
+
not_seen.remove(nb)
|
|
24
|
+
search_set.add(nb)
|
|
25
|
+
connected_sets.append(seen)
|
|
26
|
+
|
|
27
|
+
return connected_sets
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_edges(
|
|
31
|
+
neighbors: list[set[int]],
|
|
32
|
+
) -> set[tuple[int, int]]:
|
|
33
|
+
"""
|
|
34
|
+
Returns a set of edges between qubits. If there is a gate (virtual) or coupler (physical)
|
|
35
|
+
between two qubits, there is an edge between them.
|
|
36
|
+
|
|
37
|
+
Edges are not duplicated for both directions, but only one edge is returned per edge with the
|
|
38
|
+
indices sorted in ascending order.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
edges = set()
|
|
42
|
+
for from_, nb_set in enumerate(neighbors):
|
|
43
|
+
for to in nb_set:
|
|
44
|
+
edges.add((min(from_, to), max(from_, to)))
|
|
45
|
+
return edges
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import heapq
|
|
2
|
+
import itertools
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import qiskit
|
|
6
|
+
import qiskit.dagcircuit
|
|
7
|
+
import qiskit.transpiler
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def layout_to_dict(
|
|
11
|
+
layout: qiskit.transpiler.Layout | list | dict | None,
|
|
12
|
+
circuit: qiskit.QuantumCircuit,
|
|
13
|
+
) -> dict[int, int]:
|
|
14
|
+
"""
|
|
15
|
+
Given a qubit layout of basically any form that Qiskit understands, this function normalizes it
|
|
16
|
+
into a dictionary that contains virtual-physical index mappings. That is, keys represent the
|
|
17
|
+
virtual qubit indices in the circuit, and values represent the physical qubit indices in the
|
|
18
|
+
backend.
|
|
19
|
+
|
|
20
|
+
Note that if the layout cannot be resolved, an empty dict (`{}`) is returned, not None.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
if isinstance(layout, list):
|
|
24
|
+
# The Qiskit parser does not have information about registers, so we have to handle this
|
|
25
|
+
# case separately
|
|
26
|
+
# TODO: a list of something else, like Qubit objects, might also be given
|
|
27
|
+
layout = qiskit.transpiler.Layout.from_intlist(layout, *circuit.qregs)
|
|
28
|
+
else:
|
|
29
|
+
# If not a list, we let the default parser do its thing - this handles Layout objects,
|
|
30
|
+
# dicts, and other formats
|
|
31
|
+
layout = qiskit.compiler.transpiler._parse_initial_layout(layout)
|
|
32
|
+
# If there is no layout, return an empty mapping
|
|
33
|
+
if layout is None:
|
|
34
|
+
return {}
|
|
35
|
+
# Finally, return a mapping from virtual indices to physical indices
|
|
36
|
+
return {
|
|
37
|
+
circuit.find_bit(qubit).index: physical_index
|
|
38
|
+
for qubit, physical_index in layout.get_virtual_bits().items()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class QILayout:
|
|
43
|
+
"""
|
|
44
|
+
Class for representing one-to-one qubit index mappings between virtual circuit indices and
|
|
45
|
+
physical backend indices. Resembles Qiskit's Layout class, but has a restricted and improved
|
|
46
|
+
set of features.
|
|
47
|
+
|
|
48
|
+
Note on terminology: in this context, a layout contains a mapping. While the two terms might be
|
|
49
|
+
used interchangeably, "layout" refers to a one-to-one mapping that is specifically used to map
|
|
50
|
+
physical qubits to virtual qubits or vice versa, while "mapping" refers to the dictionary
|
|
51
|
+
instance(s) that represent it. At least in this class.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_layout(
|
|
56
|
+
cls,
|
|
57
|
+
layout: qiskit.transpiler.Layout | list | dict | None,
|
|
58
|
+
circuit: qiskit.QuantumCircuit,
|
|
59
|
+
):
|
|
60
|
+
return cls(v2p=layout_to_dict(layout, circuit))
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_circuit(cls, circuit: qiskit.QuantumCircuit):
|
|
64
|
+
initial_layout = circuit.layout.initial_virtual_layout()
|
|
65
|
+
return cls(
|
|
66
|
+
p2v={
|
|
67
|
+
p: circuit.layout.input_qubit_mapping[v]
|
|
68
|
+
for p, v in initial_layout.get_physical_bits().items()
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_property_set(
|
|
74
|
+
cls,
|
|
75
|
+
property_set: dict[str, Any],
|
|
76
|
+
dag: qiskit.dagcircuit.DAGCircuit | None = None,
|
|
77
|
+
):
|
|
78
|
+
if property_set["original_qubit_indices"] and property_set["layout"]:
|
|
79
|
+
return cls(
|
|
80
|
+
p2v={
|
|
81
|
+
p: property_set["original_qubit_indices"][v]
|
|
82
|
+
for p, v in property_set["layout"].get_physical_bits().items()
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
elif property_set["layout"] and dag is not None:
|
|
86
|
+
return cls(
|
|
87
|
+
p2v={
|
|
88
|
+
p: dag.find_bit(qubit).index
|
|
89
|
+
for p, qubit in property_set["layout"].get_physical_bits().items()
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
return cls()
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_trivial(cls, num_qubits: int):
|
|
96
|
+
return cls(v2p={i: i for i in range(num_qubits)})
|
|
97
|
+
|
|
98
|
+
def __init__(self, v2p: dict[int, int] | None = None, p2v: dict[int, int] | None = None):
|
|
99
|
+
if v2p is None and p2v is None:
|
|
100
|
+
self._v2p: dict[int, int] = {}
|
|
101
|
+
self._p2v: dict[int, int] = {}
|
|
102
|
+
elif v2p is not None:
|
|
103
|
+
self._v2p: dict[int, int] = v2p
|
|
104
|
+
self._p2v: dict[int, int] = {}
|
|
105
|
+
for k, v in v2p.items():
|
|
106
|
+
if v is not None:
|
|
107
|
+
self._p2v[v] = k
|
|
108
|
+
elif p2v is not None:
|
|
109
|
+
self._v2p: dict[int, int] = {}
|
|
110
|
+
self._p2v: dict[int, int] = p2v
|
|
111
|
+
for k, v in p2v.items():
|
|
112
|
+
if v is not None:
|
|
113
|
+
self._v2p[v] = k
|
|
114
|
+
else:
|
|
115
|
+
raise ValueError("only up to one mapping may be provided as an initializer")
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def size(self):
|
|
119
|
+
return len(self._v2p)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def vkeys(self):
|
|
123
|
+
return self._v2p.keys()
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def pkeys(self):
|
|
127
|
+
return self._p2v.keys()
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def v2p(self):
|
|
131
|
+
return {v: p for v, p in self._v2p.items() if self._p2v[p] is not None}
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def p2v(self):
|
|
135
|
+
return {p: v for p, v in self._p2v.items() if v is not None}
|
|
136
|
+
|
|
137
|
+
def copy(self):
|
|
138
|
+
return QILayout(p2v={**self._p2v})
|
|
139
|
+
|
|
140
|
+
def add(self, virt: int, phys: int):
|
|
141
|
+
self._v2p[virt] = phys
|
|
142
|
+
self._p2v[phys] = virt
|
|
143
|
+
|
|
144
|
+
def remove(
|
|
145
|
+
self,
|
|
146
|
+
virt: int | None = None,
|
|
147
|
+
phys: int | None = None,
|
|
148
|
+
decrement_keys: bool = False,
|
|
149
|
+
):
|
|
150
|
+
if virt is not None:
|
|
151
|
+
phys = self._v2p[virt]
|
|
152
|
+
elif phys is not None:
|
|
153
|
+
virt = self._p2v[phys]
|
|
154
|
+
if virt in self._v2p:
|
|
155
|
+
del self._v2p[virt]
|
|
156
|
+
if phys in self._p2v:
|
|
157
|
+
del self._p2v[phys]
|
|
158
|
+
if decrement_keys:
|
|
159
|
+
new_v2p = {}
|
|
160
|
+
for other_virt in list(self._v2p.keys()):
|
|
161
|
+
if other_virt > virt:
|
|
162
|
+
phys = self._v2p[other_virt]
|
|
163
|
+
del self._v2p[other_virt]
|
|
164
|
+
new_virt = other_virt - 1
|
|
165
|
+
new_v2p[new_virt] = phys
|
|
166
|
+
self._p2v[phys] = new_virt
|
|
167
|
+
self._v2p = {**self._v2p, **new_v2p}
|
|
168
|
+
|
|
169
|
+
def block(self, phys: int | set[int]):
|
|
170
|
+
"""
|
|
171
|
+
Blocks a set of physical indices. The index will be reported as blocked when calling
|
|
172
|
+
`{is, get}_blocked` and will not appear on `v2p` or `p2v`.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
if isinstance(phys, int):
|
|
176
|
+
phys = {phys}
|
|
177
|
+
for i in phys:
|
|
178
|
+
self._p2v[i] = None
|
|
179
|
+
|
|
180
|
+
def with_entry(self, virt: int, phys: int):
|
|
181
|
+
return QILayout(p2v={**self._p2v, phys: virt})
|
|
182
|
+
|
|
183
|
+
def with_blocked(self, phys: int | set[int]):
|
|
184
|
+
copy = self.copy()
|
|
185
|
+
copy.block(phys)
|
|
186
|
+
return copy
|
|
187
|
+
|
|
188
|
+
def is_blocked(self, phys: int):
|
|
189
|
+
return phys in self._p2v and self._p2v[phys] is None
|
|
190
|
+
|
|
191
|
+
def get_blocked(self):
|
|
192
|
+
return {p for p, v in self._p2v.items() if v is None}
|
|
193
|
+
|
|
194
|
+
def insert_blocked_indices(self, phys_set: set[int]):
|
|
195
|
+
"""
|
|
196
|
+
Similar to `block`, but increments other physical indices as if these qubits were inserted
|
|
197
|
+
into the backend.
|
|
198
|
+
"""
|
|
199
|
+
new_phys_indices = {p: p for p in self._p2v.keys()}
|
|
200
|
+
|
|
201
|
+
for b in sorted(phys_set):
|
|
202
|
+
for p in new_phys_indices:
|
|
203
|
+
if new_phys_indices[p] >= b:
|
|
204
|
+
new_phys_indices[p] += 1
|
|
205
|
+
|
|
206
|
+
for p, v in sorted(self._p2v.items(), reverse=True):
|
|
207
|
+
del self._p2v[p]
|
|
208
|
+
self._p2v[new_phys_indices[p]] = v
|
|
209
|
+
for v, p in list(self._v2p.items()):
|
|
210
|
+
self._v2p[v] = new_phys_indices[p]
|
|
211
|
+
for p in phys_set:
|
|
212
|
+
self._p2v[p] = None
|
|
213
|
+
|
|
214
|
+
def to_qiskit_layout(self, circuit: qiskit.QuantumCircuit) -> qiskit.transpiler.Layout:
|
|
215
|
+
"""
|
|
216
|
+
Converts to a Qiskit Layout object. Discards blocked indices.
|
|
217
|
+
"""
|
|
218
|
+
return qiskit.transpiler.Layout({circuit.qubits[v]: p for v, p in self._v2p.items()})
|
|
219
|
+
|
|
220
|
+
def __repr__(self):
|
|
221
|
+
return f"QILayout(p2v={self._p2v.__repr__()})"
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _p2v_sort_key(item):
|
|
225
|
+
p, v = item
|
|
226
|
+
return (v is None, p)
|
|
227
|
+
|
|
228
|
+
def __str__(self):
|
|
229
|
+
return (
|
|
230
|
+
"{"
|
|
231
|
+
+ ", ".join(
|
|
232
|
+
(f"!p_{p}" if v is None else f"v_{v} ~ p_{p}")
|
|
233
|
+
for p, v in sorted(self._p2v.items(), key=self._p2v_sort_key)
|
|
234
|
+
)
|
|
235
|
+
+ "}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def _iqmstr(self):
|
|
239
|
+
return (
|
|
240
|
+
"{"
|
|
241
|
+
+ ", ".join(
|
|
242
|
+
(f"!QB{p + 1}" if v is None else f"QB{p + 1}: {v}")
|
|
243
|
+
for p, v in sorted(self._p2v.items(), key=self._p2v_sort_key)
|
|
244
|
+
)
|
|
245
|
+
+ "}"
|
|
246
|
+
)
|