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.
@@ -0,0 +1 @@
1
+ from .parallelizer import describe, execute, rearrange
@@ -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
+ )