bloqade-circuit 0.4.4__py3-none-any.whl → 0.5.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.
Potentially problematic release.
This version of bloqade-circuit might be problematic. Click here for more details.
- bloqade/cirq_utils/__init__.py +7 -0
- bloqade/cirq_utils/lineprog.py +295 -0
- bloqade/cirq_utils/parallelize.py +400 -0
- bloqade/pyqrack/squin/op.py +7 -2
- bloqade/pyqrack/squin/runtime.py +4 -2
- bloqade/qasm2/dialects/expr/stmts.py +2 -20
- bloqade/qasm2/parse/lowering.py +1 -0
- bloqade/qasm2/passes/parallel.py +18 -0
- bloqade/qasm2/rewrite/__init__.py +1 -0
- bloqade/qasm2/rewrite/parallel_to_glob.py +82 -0
- bloqade/squin/__init__.py +1 -0
- bloqade/squin/_typeinfer.py +20 -0
- bloqade/squin/analysis/nsites/impls.py +6 -1
- bloqade/squin/cirq/__init__.py +74 -9
- bloqade/squin/cirq/emit/noise.py +49 -0
- bloqade/squin/cirq/emit/runtime.py +9 -1
- bloqade/squin/cirq/lowering.py +46 -27
- bloqade/squin/noise/_wrapper.py +9 -2
- bloqade/squin/noise/rewrite.py +3 -3
- bloqade/squin/op/__init__.py +1 -0
- bloqade/squin/op/_wrapper.py +4 -0
- bloqade/squin/op/stmts.py +20 -2
- bloqade/squin/qubit.py +8 -5
- bloqade/squin/rewrite/__init__.py +1 -0
- bloqade/squin/rewrite/canonicalize.py +60 -0
- bloqade/squin/rewrite/desugar.py +52 -5
- bloqade/squin/types.py +8 -0
- bloqade/squin/wire.py +91 -5
- bloqade/stim/__init__.py +1 -0
- bloqade/stim/_wrappers.py +4 -0
- bloqade/stim/dialects/noise/emit.py +1 -0
- bloqade/stim/dialects/noise/stmts.py +5 -0
- bloqade/stim/passes/squin_to_stim.py +16 -1
- bloqade/stim/rewrite/__init__.py +1 -0
- bloqade/stim/rewrite/qubit_to_stim.py +10 -6
- bloqade/stim/rewrite/squin_noise.py +120 -0
- bloqade/stim/rewrite/util.py +44 -9
- bloqade/stim/rewrite/wire_to_stim.py +8 -3
- {bloqade_circuit-0.4.4.dist-info → bloqade_circuit-0.5.0.dist-info}/METADATA +4 -2
- {bloqade_circuit-0.4.4.dist-info → bloqade_circuit-0.5.0.dist-info}/RECORD +42 -33
- {bloqade_circuit-0.4.4.dist-info → bloqade_circuit-0.5.0.dist-info}/WHEEL +0 -0
- {bloqade_circuit-0.4.4.dist-info → bloqade_circuit-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Union
|
|
3
|
+
from collections import Counter
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import scipy.sparse
|
|
7
|
+
from qpsolvers import solve_qp
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Variable:
|
|
11
|
+
def __add__(
|
|
12
|
+
self, other: Union["Variable", "Expression", float, int]
|
|
13
|
+
) -> "Expression":
|
|
14
|
+
if isinstance(other, Variable):
|
|
15
|
+
return Expression({self: 1}) + Expression({other: 1})
|
|
16
|
+
elif isinstance(other, Expression):
|
|
17
|
+
return Expression({self: 1}) + other
|
|
18
|
+
elif isinstance(other, (float, int)):
|
|
19
|
+
return Expression({self: 1}) + Expression({None: other})
|
|
20
|
+
else:
|
|
21
|
+
raise TypeError(f"Cannot add {type(other)} to Variable")
|
|
22
|
+
|
|
23
|
+
def __radd__(self, left: float | int) -> "Expression":
|
|
24
|
+
return self.__add__(left)
|
|
25
|
+
|
|
26
|
+
def __sub__(
|
|
27
|
+
self, other: Union["Variable", "Expression", float, int]
|
|
28
|
+
) -> "Expression":
|
|
29
|
+
if isinstance(other, Variable):
|
|
30
|
+
return Expression({self: 1}) - Expression({other: 1})
|
|
31
|
+
elif isinstance(other, Expression):
|
|
32
|
+
return Expression({self: 1}) - other
|
|
33
|
+
elif isinstance(other, (float, int)):
|
|
34
|
+
return Expression({self: 1}) - Expression({None: other})
|
|
35
|
+
else:
|
|
36
|
+
raise TypeError(f"Cannot subtract {type(other)} from Variable")
|
|
37
|
+
|
|
38
|
+
def __rsub__(self, left: float | int) -> "Expression":
|
|
39
|
+
return self.__sub__(left)
|
|
40
|
+
|
|
41
|
+
def __neg__(self) -> "Expression":
|
|
42
|
+
return 0 - self
|
|
43
|
+
|
|
44
|
+
def __mul__(self, factor: float | int) -> "Expression":
|
|
45
|
+
if not isinstance(factor, (float, int)):
|
|
46
|
+
raise TypeError("Cannot multiply by non-numeric type")
|
|
47
|
+
return Expression({self: factor})
|
|
48
|
+
|
|
49
|
+
def __rmul__(self, factor: float | int) -> "Expression":
|
|
50
|
+
return self.__mul__(factor)
|
|
51
|
+
|
|
52
|
+
def __truediv__(self, factor: float | int) -> "Expression":
|
|
53
|
+
if not isinstance(factor, (float, int)):
|
|
54
|
+
raise TypeError("Cannot divide by non-numeric type")
|
|
55
|
+
return Expression({self: 1 / factor})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclasses.dataclass(frozen=True)
|
|
59
|
+
class Expression:
|
|
60
|
+
coeffs: dict[Variable | None, float]
|
|
61
|
+
|
|
62
|
+
def get(self, key: Variable | None) -> float:
|
|
63
|
+
if key in self.coeffs:
|
|
64
|
+
return self.coeffs[key]
|
|
65
|
+
else:
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
def __getitem__(self, key: Variable | None) -> float:
|
|
69
|
+
return self.get(key)
|
|
70
|
+
|
|
71
|
+
def __add__(
|
|
72
|
+
self, other: Union["Expression", "Variable", float, int]
|
|
73
|
+
) -> "Expression":
|
|
74
|
+
if isinstance(other, Variable):
|
|
75
|
+
coeff = {key: val for key, val in self.coeffs.items()}
|
|
76
|
+
coeff[other] = coeff.get(other, 0) + 1
|
|
77
|
+
return Expression(coeffs=coeff)
|
|
78
|
+
elif isinstance(other, Expression):
|
|
79
|
+
coeff = {}
|
|
80
|
+
for key in set(self.coeffs.keys()).union(other.coeffs.keys()):
|
|
81
|
+
coeff[key] = self.coeffs.get(key, 0) + other.coeffs.get(key, 0)
|
|
82
|
+
return Expression(coeffs=coeff)
|
|
83
|
+
elif isinstance(other, (float, int)):
|
|
84
|
+
coeff = {key: val for key, val in self.coeffs.items()}
|
|
85
|
+
coeff[None] = coeff.get(None, 0) + other
|
|
86
|
+
return Expression(coeffs=coeff)
|
|
87
|
+
|
|
88
|
+
def __radd__(self, left: float | int) -> "Expression":
|
|
89
|
+
return self.__add__(left)
|
|
90
|
+
|
|
91
|
+
def __sub__(
|
|
92
|
+
self, other: Union["Expression", "Variable", float, int]
|
|
93
|
+
) -> "Expression":
|
|
94
|
+
if isinstance(other, Variable):
|
|
95
|
+
coeff = {key: val for key, val in self.coeffs.items()}
|
|
96
|
+
coeff[other] = coeff.get(other, 0) - 1
|
|
97
|
+
return Expression(coeffs=coeff)
|
|
98
|
+
elif isinstance(other, Expression):
|
|
99
|
+
coeff = {}
|
|
100
|
+
for key in set(self.coeffs.keys()).union(other.coeffs.keys()):
|
|
101
|
+
coeff[key] = self.coeffs.get(key, 0) - other.coeffs.get(key, 0)
|
|
102
|
+
return Expression(coeffs=coeff)
|
|
103
|
+
elif isinstance(other, (float, int)):
|
|
104
|
+
coeff = {key: val for key, val in self.coeffs.items()}
|
|
105
|
+
coeff[None] = coeff.get(None, 0) - other
|
|
106
|
+
return Expression(coeffs=coeff)
|
|
107
|
+
|
|
108
|
+
def __rsub__(self, left: float | int) -> "Expression":
|
|
109
|
+
return self.__sub__(left)
|
|
110
|
+
|
|
111
|
+
def __neg__(self) -> "Expression":
|
|
112
|
+
return 0 - self
|
|
113
|
+
|
|
114
|
+
def __mul__(self, factor: float | int) -> "Expression":
|
|
115
|
+
if not isinstance(factor, (float, int)):
|
|
116
|
+
raise TypeError("Cannot multiply by non-numeric type")
|
|
117
|
+
coeff = {key: factor * val for key, val in self.coeffs.items()}
|
|
118
|
+
return Expression(coeffs=coeff)
|
|
119
|
+
|
|
120
|
+
def __rmul__(self, factor: float | int) -> "Expression":
|
|
121
|
+
return self.__mul__(factor)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Solution(dict[Variable, float]): ...
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclasses.dataclass(frozen=False)
|
|
128
|
+
class LPProblem:
|
|
129
|
+
constraints_eqz: list[Expression] = dataclasses.field(default_factory=list)
|
|
130
|
+
constraints_gez: list[Expression] = dataclasses.field(default_factory=list)
|
|
131
|
+
linear_objective: Expression = dataclasses.field(
|
|
132
|
+
default_factory=lambda: Expression({})
|
|
133
|
+
)
|
|
134
|
+
quadratic_objective: list[Expression] = dataclasses.field(default_factory=list)
|
|
135
|
+
|
|
136
|
+
def add_gez(self, expr: Expression):
|
|
137
|
+
if isinstance(expr, (Expression, Variable)):
|
|
138
|
+
self.constraints_gez.append(expr)
|
|
139
|
+
elif expr < 0:
|
|
140
|
+
raise RuntimeError("No solution found")
|
|
141
|
+
|
|
142
|
+
def add_lez(self, expr: Expression):
|
|
143
|
+
if isinstance(expr, (Expression, Variable)):
|
|
144
|
+
self.constraints_gez.append(-expr)
|
|
145
|
+
elif expr > 0:
|
|
146
|
+
raise RuntimeError("No solution found")
|
|
147
|
+
|
|
148
|
+
def add_eqz(self, expr: Expression):
|
|
149
|
+
if isinstance(expr, (Expression, Variable)):
|
|
150
|
+
self.constraints_eqz.append(expr)
|
|
151
|
+
elif expr != 0:
|
|
152
|
+
raise RuntimeError("No solution found")
|
|
153
|
+
|
|
154
|
+
def add_linear(self, expr: Expression):
|
|
155
|
+
if isinstance(expr, (Expression, Variable)):
|
|
156
|
+
self.linear_objective += expr
|
|
157
|
+
else:
|
|
158
|
+
print("LP Program Warning: no variables in linear objective term")
|
|
159
|
+
|
|
160
|
+
def add_quadratic(self, expr: Expression):
|
|
161
|
+
if isinstance(expr, (Expression, Variable)):
|
|
162
|
+
self.quadratic_objective.append(expr)
|
|
163
|
+
else:
|
|
164
|
+
print("LP Program Warning: No variables in quadratic objective term")
|
|
165
|
+
|
|
166
|
+
def add_abs(self, expr: Expression):
|
|
167
|
+
"""
|
|
168
|
+
Use a slack variable to add an absolute value constraint.
|
|
169
|
+
"""
|
|
170
|
+
slack = Variable()
|
|
171
|
+
self.add_gez(slack - expr)
|
|
172
|
+
self.add_gez(slack + expr)
|
|
173
|
+
self.add_gez(1.0 * slack)
|
|
174
|
+
self.add_linear(1.0 * slack)
|
|
175
|
+
|
|
176
|
+
def __repr__(self):
|
|
177
|
+
payload = "A Linear programming problem with {:} inequalies, {:} equalities, {:} quadratic".format(
|
|
178
|
+
len(self.constraints_gez),
|
|
179
|
+
len(self.constraints_eqz),
|
|
180
|
+
len(self.quadratic_objective),
|
|
181
|
+
)
|
|
182
|
+
return payload
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def basis(self) -> list[Variable]:
|
|
186
|
+
all_vars = Counter()
|
|
187
|
+
for expr in self.constraints_eqz:
|
|
188
|
+
all_vars.update(expr.coeffs.keys())
|
|
189
|
+
for expr in self.constraints_gez:
|
|
190
|
+
all_vars.update(expr.coeffs.keys())
|
|
191
|
+
for expr in self.quadratic_objective:
|
|
192
|
+
all_vars.update(expr.coeffs.keys())
|
|
193
|
+
all_vars.update(self.linear_objective.coeffs.keys())
|
|
194
|
+
all_vars.pop(None, None) # The constant term is not a variable
|
|
195
|
+
all_vars = list(all_vars.keys())
|
|
196
|
+
return all_vars
|
|
197
|
+
|
|
198
|
+
def __add__(self, other: "LPProblem") -> "LPProblem":
|
|
199
|
+
if not isinstance(other, LPProblem):
|
|
200
|
+
raise TypeError(f"Cannot add {type(other)} to LPProblem")
|
|
201
|
+
return LPProblem(
|
|
202
|
+
constraints_eqz=self.constraints_eqz + other.constraints_eqz,
|
|
203
|
+
constraints_gez=self.constraints_gez + other.constraints_gez,
|
|
204
|
+
linear_objective=self.linear_objective + other.linear_objective,
|
|
205
|
+
quadratic_objective=self.quadratic_objective + other.quadratic_objective,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def solve(self) -> Solution:
|
|
209
|
+
|
|
210
|
+
basis = self.basis
|
|
211
|
+
if len(basis) == 0:
|
|
212
|
+
return Solution()
|
|
213
|
+
|
|
214
|
+
# Generate the sparse matrix for each value...
|
|
215
|
+
# Inequality constraints
|
|
216
|
+
A_gez_dat = []
|
|
217
|
+
A_gez_i = []
|
|
218
|
+
A_gez_j = []
|
|
219
|
+
B_gez_dat = []
|
|
220
|
+
for k in range(len(self.constraints_gez)):
|
|
221
|
+
for key, val in self.constraints_gez[k].coeffs.items():
|
|
222
|
+
if key is not None:
|
|
223
|
+
A_gez_dat.append(val)
|
|
224
|
+
A_gez_i.append(k)
|
|
225
|
+
A_gez_j.append(basis.index(key))
|
|
226
|
+
B_gez_dat.append(self.constraints_gez[k].coeffs.get(None, 0))
|
|
227
|
+
|
|
228
|
+
# Equality constraints
|
|
229
|
+
A_eqz_dat = []
|
|
230
|
+
A_eqz_i = []
|
|
231
|
+
A_eqz_j = []
|
|
232
|
+
B_eqz_dat = []
|
|
233
|
+
for k in range(len(self.constraints_eqz)):
|
|
234
|
+
for key, val in self.constraints_eqz[k].coeffs.items():
|
|
235
|
+
if key is not None:
|
|
236
|
+
A_eqz_dat.append(val)
|
|
237
|
+
A_eqz_i.append(k)
|
|
238
|
+
A_eqz_j.append(basis.index(key))
|
|
239
|
+
B_eqz_dat.append(self.constraints_eqz[k].get(None))
|
|
240
|
+
|
|
241
|
+
# Linear objective
|
|
242
|
+
C_dat = [self.linear_objective.coeffs.get(key, 0) for key in basis]
|
|
243
|
+
|
|
244
|
+
# Quadratic objective
|
|
245
|
+
Q_dat = []
|
|
246
|
+
Q_i = []
|
|
247
|
+
Q_j = []
|
|
248
|
+
for k in range(len(self.quadratic_objective)):
|
|
249
|
+
for key1, val1 in self.quadratic_objective[k].coeffs.items():
|
|
250
|
+
for key2, val2 in self.quadratic_objective[k].coeffs.items():
|
|
251
|
+
if key1 is not None and key2 is not None:
|
|
252
|
+
Q_dat.append(val1 * val2)
|
|
253
|
+
Q_i.append(basis.index(key1))
|
|
254
|
+
Q_j.append(basis.index(key2))
|
|
255
|
+
elif key1 is None and key2 is not None:
|
|
256
|
+
C_dat[basis.index(key2)] += 0.5 * val1 * val2
|
|
257
|
+
elif key1 is not None and key2 is None:
|
|
258
|
+
C_dat[basis.index(key1)] += 0.5 * val1 * val2
|
|
259
|
+
elif key1 is None and key2 is None:
|
|
260
|
+
# This is the constant term and can be ignored
|
|
261
|
+
pass
|
|
262
|
+
else:
|
|
263
|
+
raise RuntimeError("I should never get here")
|
|
264
|
+
|
|
265
|
+
A_gez_mat = scipy.sparse.coo_matrix(
|
|
266
|
+
(A_gez_dat, (A_gez_i, A_gez_j)), shape=(len(B_gez_dat), len(basis))
|
|
267
|
+
).tocsc()
|
|
268
|
+
B_gez_vec = np.array(B_gez_dat)
|
|
269
|
+
|
|
270
|
+
A_eqz_mat = scipy.sparse.coo_matrix(
|
|
271
|
+
(A_eqz_dat, (A_eqz_i, A_eqz_j)), shape=(len(B_eqz_dat), len(basis))
|
|
272
|
+
).tocsc()
|
|
273
|
+
B_eqz_vec = np.array(B_eqz_dat)
|
|
274
|
+
|
|
275
|
+
C_vec = np.array(C_dat)
|
|
276
|
+
|
|
277
|
+
Q_mat = scipy.sparse.coo_matrix(
|
|
278
|
+
(Q_dat, (Q_i, Q_j)), shape=(len(basis), len(basis))
|
|
279
|
+
).tocsc()
|
|
280
|
+
|
|
281
|
+
# The quadratic problem uses slightly different variable symbols than
|
|
282
|
+
# scipy, which is why the letters are all off...
|
|
283
|
+
solution = solve_qp(
|
|
284
|
+
P=Q_mat,
|
|
285
|
+
q=C_vec,
|
|
286
|
+
G=-A_gez_mat,
|
|
287
|
+
h=B_gez_vec,
|
|
288
|
+
A=A_eqz_mat,
|
|
289
|
+
b=B_eqz_vec,
|
|
290
|
+
solver="clarabel",
|
|
291
|
+
)
|
|
292
|
+
if solution is None:
|
|
293
|
+
raise RuntimeError("No solution found")
|
|
294
|
+
|
|
295
|
+
return Solution(zip(basis, solution))
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
from typing import TypeVar, Hashable, Iterable
|
|
2
|
+
from itertools import combinations
|
|
3
|
+
|
|
4
|
+
import cirq
|
|
5
|
+
import networkx as nx
|
|
6
|
+
from cirq.ops.gate_operation import GateOperation
|
|
7
|
+
from cirq.contrib.circuitdag.circuit_dag import Unique, CircuitDag
|
|
8
|
+
|
|
9
|
+
from .lineprog import Variable, LPProblem
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def can_be_parallel(
|
|
13
|
+
op1: cirq.GateOperation, op2: cirq.GateOperation, tol: float = 1e-14
|
|
14
|
+
) -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Heuristic similarity function to determine if two operations are similar enough
|
|
17
|
+
to be grouped together in parallel execution.
|
|
18
|
+
"""
|
|
19
|
+
are_disjoint = len(set(op1.qubits).intersection(op2.qubits)) == 0
|
|
20
|
+
if not are_disjoint:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
# Check if both operations are CZ gates
|
|
24
|
+
both_cz = op1.gate == cirq.CZ and op2.gate == cirq.CZ
|
|
25
|
+
|
|
26
|
+
both_phased_xz = isinstance(op1.gate, cirq.PhasedXZGate) and isinstance(
|
|
27
|
+
op2.gate, cirq.PhasedXZGate
|
|
28
|
+
)
|
|
29
|
+
equal_unitaries = cirq.equal_up_to_global_phase(
|
|
30
|
+
cirq.unitary(op1.gate), cirq.unitary(op2.gate), atol=tol
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return (both_phased_xz and equal_unitaries) or both_cz
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def transpile(circuit: cirq.Circuit) -> cirq.Circuit:
|
|
37
|
+
"""
|
|
38
|
+
Transpile a circuit to a native CZ gate set of {CZ, PhXZ}.
|
|
39
|
+
"""
|
|
40
|
+
# Convert to CZ target gate set.
|
|
41
|
+
circuit2 = cirq.optimize_for_target_gateset(circuit, gateset=cirq.CZTargetGateset())
|
|
42
|
+
missing_qubits = circuit.all_qubits() - circuit2.all_qubits()
|
|
43
|
+
|
|
44
|
+
for qubit in missing_qubits:
|
|
45
|
+
circuit2.append(
|
|
46
|
+
cirq.PhasedXZGate(x_exponent=0, z_exponent=0, axis_phase_exponent=0).on(
|
|
47
|
+
qubit
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return circuit2
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def moment_similarity(
|
|
55
|
+
circuit: cirq.Circuit, weight: float
|
|
56
|
+
) -> tuple[cirq.Circuit, dict[Hashable, float]]:
|
|
57
|
+
"""
|
|
58
|
+
Associate every gate in each moment with a similarity group.
|
|
59
|
+
|
|
60
|
+
Inputs:
|
|
61
|
+
circuit - a cirq.Circuit to be analyzed.
|
|
62
|
+
weight: float - the weight to assign to each block of gates.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
[0] - the cirq.Circuit with each gate annotated with topological similarity tags.
|
|
66
|
+
[1] - a dictionary mapping each tag to its weight, where the key is the tag and the value is the weight.
|
|
67
|
+
"""
|
|
68
|
+
new_moments = []
|
|
69
|
+
weights = {}
|
|
70
|
+
|
|
71
|
+
for moment_index, moment in enumerate(circuit.moments):
|
|
72
|
+
tag = f"MOMENT:{moment_index}"
|
|
73
|
+
new_moments.append([gate.with_tags(tag) for gate in moment.operations])
|
|
74
|
+
weights[tag] = weight
|
|
75
|
+
return cirq.Circuit(new_moments), weights
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def block_similarity(
|
|
79
|
+
circuit: cirq.Circuit, weight: float, block_id: int
|
|
80
|
+
) -> tuple[cirq.Circuit, dict[Hashable, float]]:
|
|
81
|
+
"""
|
|
82
|
+
Associate every gate in a circuit with a similarity group.
|
|
83
|
+
|
|
84
|
+
Inputs:
|
|
85
|
+
circuit - a cirq.Circuit to be analyzed.
|
|
86
|
+
weight: float - the weight to assign to each block of gates.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
[0] - the cirq.Circuit with each gate annotated with topological similarity tags.
|
|
90
|
+
[1] - a dictionary mapping each tag to its weight, where the key is the tag and the value is the weight.
|
|
91
|
+
"""
|
|
92
|
+
new_moments = []
|
|
93
|
+
weights = {}
|
|
94
|
+
tag = f"BLOCK:{block_id}"
|
|
95
|
+
for moment in circuit.moments:
|
|
96
|
+
new_moments.append([gate.with_tags(tag) for gate in moment.operations])
|
|
97
|
+
weights[tag] = weight
|
|
98
|
+
return cirq.Circuit(new_moments), weights
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def auto_similarity(
|
|
102
|
+
circuit: cirq.Circuit, weight_1q: float, weight_2q: float
|
|
103
|
+
) -> tuple[cirq.Circuit, dict[Hashable, float]]:
|
|
104
|
+
"""
|
|
105
|
+
Automatically tag the circuit with topological basis group labels,
|
|
106
|
+
where each group is a pair of gates that can be executed in parallel.
|
|
107
|
+
|
|
108
|
+
Inputs:
|
|
109
|
+
circuit - a cirq.Circuit to be analyzed. This should be CZ + PhaseXZGate, otherwise no annotation will occur.
|
|
110
|
+
weight_1q: float - the weight to assign to single-qubit gates.
|
|
111
|
+
weight_2q: float - the weight to assign to two-qubit gates.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
[0] - the cirq.Circuit with each gate annotated with topological similarity tags.
|
|
115
|
+
[1] - a dictionary mapping each tag to its weight, where the key is the tag and the value is the weight.
|
|
116
|
+
"""
|
|
117
|
+
flattened_circuit: list[GateOperation] = list(cirq.flatten_op_tree(circuit))
|
|
118
|
+
weights = {}
|
|
119
|
+
for i in range(len(flattened_circuit)):
|
|
120
|
+
for j in range(i + 1, len(flattened_circuit)):
|
|
121
|
+
op1 = flattened_circuit[i]
|
|
122
|
+
op2 = flattened_circuit[j]
|
|
123
|
+
if can_be_parallel(op1, op2):
|
|
124
|
+
# Add tags to both operations
|
|
125
|
+
tag = f"AUTO:{i}"
|
|
126
|
+
flattened_circuit[i] = op1.with_tags(tag)
|
|
127
|
+
flattened_circuit[j] = op2.with_tags(tag)
|
|
128
|
+
if len(op1.qubits) == 1:
|
|
129
|
+
weights[tag] = weight_1q
|
|
130
|
+
elif len(op1.qubits) == 2:
|
|
131
|
+
weights[tag] = weight_2q
|
|
132
|
+
else:
|
|
133
|
+
raise RuntimeError("Unsupported gate type")
|
|
134
|
+
return cirq.Circuit(flattened_circuit), weights
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def no_similarity(circuit: cirq.Circuit) -> cirq.Circuit:
|
|
138
|
+
"""
|
|
139
|
+
Removes all tags from the circuit
|
|
140
|
+
|
|
141
|
+
Inputs:
|
|
142
|
+
circuit: cirq.Circuit - the circuit to remove tags from.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
[0] - cirq.Circuit - the circuit with all tags removed.
|
|
146
|
+
"""
|
|
147
|
+
new_moments = []
|
|
148
|
+
for moment in circuit.moments:
|
|
149
|
+
new_moments.append([gate.untagged for gate in moment.operations])
|
|
150
|
+
return cirq.Circuit(new_moments)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def to_dag_circuit(circuit: cirq.Circuit, can_reorder=None) -> nx.DiGraph:
|
|
154
|
+
"""
|
|
155
|
+
Convert a cirq.Circuit to a directed acyclic graph (DAG) representation.
|
|
156
|
+
This is useful for analyzing the circuit structure and dependencies.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
circuit: cirq.Circuit - the circuit to convert.
|
|
160
|
+
can_reorder: function - a function that checks if two operations can be reordered.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
[0] - nx.DiGraph - the directed acyclic graph representation of the circuit.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def reorder_check(
|
|
167
|
+
op1, op2
|
|
168
|
+
): # can reorder iff both are CZ, or intersection is empty
|
|
169
|
+
if op1.gate == cirq.CZ and op2.gate == cirq.CZ:
|
|
170
|
+
return True
|
|
171
|
+
else:
|
|
172
|
+
return len(set(op1.qubits).intersection(op2.qubits)) == 0
|
|
173
|
+
|
|
174
|
+
# Turn into DAG
|
|
175
|
+
directed = CircuitDag.from_circuit(
|
|
176
|
+
circuit, can_reorder=reorder_check if can_reorder is None else can_reorder
|
|
177
|
+
)
|
|
178
|
+
return nx.transitive_reduction(directed)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
NodeType = TypeVar("NodeType")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _get_hyperparameters(params: dict[str, float] | None) -> dict[str, float]:
|
|
185
|
+
"""
|
|
186
|
+
Returns a dictionary of default hyperparameters for the optimization.
|
|
187
|
+
"""
|
|
188
|
+
if params is None:
|
|
189
|
+
return {
|
|
190
|
+
"linear": 0.01,
|
|
191
|
+
"1q": 1.0,
|
|
192
|
+
"2q": 2.0,
|
|
193
|
+
"tags": 0.5,
|
|
194
|
+
}
|
|
195
|
+
else:
|
|
196
|
+
return {
|
|
197
|
+
"linear": params.get("linear", 0.01),
|
|
198
|
+
"1q": params.get("1q", 1.0),
|
|
199
|
+
"2q": params.get("2q", 1.0),
|
|
200
|
+
"tags": params.get("tags", 0.5),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def solve_epochs(
|
|
205
|
+
directed: nx.DiGraph,
|
|
206
|
+
group_weights: dict[Hashable, float],
|
|
207
|
+
hyperparameters: dict[str, float] | None = None,
|
|
208
|
+
) -> dict[Unique[cirq.GateOperation], float]:
|
|
209
|
+
"""
|
|
210
|
+
Internal function to solve the epochs using linear programming.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
hyperparameters = _get_hyperparameters(hyperparameters)
|
|
214
|
+
|
|
215
|
+
basis = {node: Variable() for node in directed.nodes}
|
|
216
|
+
|
|
217
|
+
if len(basis) == 0:
|
|
218
|
+
return {}
|
|
219
|
+
|
|
220
|
+
# ---
|
|
221
|
+
# Turn into a linear program to solve
|
|
222
|
+
# ---
|
|
223
|
+
lp = LPProblem()
|
|
224
|
+
|
|
225
|
+
# All timesteps must be positive
|
|
226
|
+
for node in directed.nodes:
|
|
227
|
+
lp.add_gez(1.0 * basis[node])
|
|
228
|
+
|
|
229
|
+
# Add ordering constraints
|
|
230
|
+
for edge in directed.edges:
|
|
231
|
+
lp.add_gez(basis[edge[1]] - basis[edge[0]] - 1.0)
|
|
232
|
+
|
|
233
|
+
all_variables = list(basis.values())
|
|
234
|
+
# Add linear objective: minimize the total time
|
|
235
|
+
objective = hyperparameters["linear"] * sum(all_variables[1:], all_variables[0])
|
|
236
|
+
|
|
237
|
+
default_weight = hyperparameters["tags"]
|
|
238
|
+
lp.add_linear(objective)
|
|
239
|
+
# Add ABS objective: similarity wants to go together.
|
|
240
|
+
for node1, node2 in combinations(directed.nodes, 2):
|
|
241
|
+
# Topological (user) similarity:
|
|
242
|
+
inter = set(node1.val.tags).intersection(set(node2.val.tags))
|
|
243
|
+
if len(inter) > 0:
|
|
244
|
+
weight = sum([group_weights.get(key, default_weight) for key in inter])
|
|
245
|
+
if weight > 0:
|
|
246
|
+
lp.add_abs((basis[node1] - basis[node2]) * weight)
|
|
247
|
+
elif weight < 0:
|
|
248
|
+
raise RuntimeError("Weights must be positive")
|
|
249
|
+
|
|
250
|
+
solution = lp.solve()
|
|
251
|
+
return {node: solution[basis[node]] for node in directed.nodes}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def generate_epochs(
|
|
255
|
+
solution: dict[NodeType, float],
|
|
256
|
+
tol=1e-2,
|
|
257
|
+
):
|
|
258
|
+
"""
|
|
259
|
+
Internal function to generate epochs from the solution of the linear program.
|
|
260
|
+
"""
|
|
261
|
+
sorted_gates = sorted(solution.items(), key=lambda x: x[1])
|
|
262
|
+
if len(sorted_gates) == 0:
|
|
263
|
+
return iter([])
|
|
264
|
+
|
|
265
|
+
gate, latest_time = sorted_gates[0]
|
|
266
|
+
current_epoch = [gate] # Start with the first gate
|
|
267
|
+
for gate, time in sorted_gates[1:]:
|
|
268
|
+
if time - latest_time < tol:
|
|
269
|
+
current_epoch.append(gate)
|
|
270
|
+
else:
|
|
271
|
+
yield current_epoch
|
|
272
|
+
current_epoch = [gate]
|
|
273
|
+
|
|
274
|
+
latest_time = time
|
|
275
|
+
|
|
276
|
+
yield current_epoch # Yield the last epoch
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def colorize(
|
|
280
|
+
epochs: Iterable[list[Unique[cirq.GateOperation]]],
|
|
281
|
+
):
|
|
282
|
+
"""
|
|
283
|
+
For each epoch, separate any 1q and 2q gates, and colorize the 2q gates
|
|
284
|
+
so that they can be executed in parallel without conflicts.
|
|
285
|
+
Args:
|
|
286
|
+
epochs: list[list[Unique[cirq.GateOperation]]] - a list of epochs, where each
|
|
287
|
+
epoch is a list of gates that can be executed in parallel.
|
|
288
|
+
|
|
289
|
+
Yields:
|
|
290
|
+
list[cirq.GateOperation] - a list of lists of gates, where each
|
|
291
|
+
inner list contains gates that can be executed in parallel.
|
|
292
|
+
|
|
293
|
+
"""
|
|
294
|
+
for epoch in epochs:
|
|
295
|
+
oneq_gates = []
|
|
296
|
+
twoq_gates = []
|
|
297
|
+
for gate in epoch:
|
|
298
|
+
if len(gate.val.qubits) == 1:
|
|
299
|
+
oneq_gates.append(gate.val)
|
|
300
|
+
elif len(gate.val.qubits) == 2:
|
|
301
|
+
twoq_gates.append(gate.val)
|
|
302
|
+
else:
|
|
303
|
+
raise RuntimeError("Unsupported gate type")
|
|
304
|
+
|
|
305
|
+
if len(oneq_gates) > 0:
|
|
306
|
+
yield oneq_gates
|
|
307
|
+
|
|
308
|
+
# twoq_gates2 = colorizer(twoq_gates)# Inlined.
|
|
309
|
+
"""
|
|
310
|
+
Implements an edge coloring algorithm on a set of simultaneous 2q gates,
|
|
311
|
+
so that they can be done in an ordered manner so that no to gates use
|
|
312
|
+
the same qubit in the same layer.
|
|
313
|
+
"""
|
|
314
|
+
graph = nx.Graph()
|
|
315
|
+
for gate in twoq_gates:
|
|
316
|
+
if len(gate.qubits) != 2 and gate.gate != cirq.CZ:
|
|
317
|
+
raise RuntimeError("Unsupported gate type")
|
|
318
|
+
graph.add_edge(gate.qubits[0], gate.qubits[1])
|
|
319
|
+
linegraph = nx.line_graph(graph)
|
|
320
|
+
|
|
321
|
+
best_colors: dict[tuple[cirq.Qid, cirq.Qid], int] = (
|
|
322
|
+
nx.algorithms.coloring.greedy_color(linegraph, strategy="largest_first")
|
|
323
|
+
)
|
|
324
|
+
best_num_colors = len(set(best_colors.values()))
|
|
325
|
+
|
|
326
|
+
for strategy in (
|
|
327
|
+
#'random_sequential',
|
|
328
|
+
"smallest_last",
|
|
329
|
+
"independent_set",
|
|
330
|
+
"connected_sequential_bfs",
|
|
331
|
+
"connected_sequential_dfs",
|
|
332
|
+
"saturation_largest_first",
|
|
333
|
+
):
|
|
334
|
+
colors: dict[tuple[cirq.Qid, cirq.Qid], int] = (
|
|
335
|
+
nx.algorithms.coloring.greedy_color(linegraph, strategy=strategy)
|
|
336
|
+
)
|
|
337
|
+
if (num_colors := len(set(colors.values()))) < best_num_colors:
|
|
338
|
+
best_num_colors = num_colors
|
|
339
|
+
best_colors = colors
|
|
340
|
+
|
|
341
|
+
twoq_gates2 = (
|
|
342
|
+
list(cirq.CZ(*k) for k, v in best_colors.items() if v == x)
|
|
343
|
+
for x in set(best_colors.values())
|
|
344
|
+
)
|
|
345
|
+
# -- end colorizer --
|
|
346
|
+
yield from twoq_gates2
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def parallelize(
|
|
350
|
+
circuit: cirq.Circuit,
|
|
351
|
+
hyperparameters: dict[str, float] | None = None,
|
|
352
|
+
auto_tag: bool = True,
|
|
353
|
+
) -> cirq.Circuit:
|
|
354
|
+
"""
|
|
355
|
+
Use linear programming to reorder a circuit so that it may be optimally be
|
|
356
|
+
run in parallel. This is done using a DAG representation, as well as a heuristic
|
|
357
|
+
similarity function to group parallelizable gates together.
|
|
358
|
+
|
|
359
|
+
Extra topological information (similarity) can be used by tagging each gate with
|
|
360
|
+
the topological basis groups that it belongs to, for example
|
|
361
|
+
> circuit.append(cirq.H(qubits[0]).with_tags(1,2,3,4))
|
|
362
|
+
represents that this gate is part of the topological basis groups 1,2,3, and 4.
|
|
363
|
+
|
|
364
|
+
Inputs:
|
|
365
|
+
circuit: cirq.Circuit - the static circuit to be optimized
|
|
366
|
+
hyperparameters: dict[str, float] - hyperparameters for the optimization
|
|
367
|
+
- "linear": float (0.01) - the linear cost of each gate
|
|
368
|
+
- "1q": float (1.0) - the quadratic cost of 1q gates
|
|
369
|
+
- "2q": float (2.0) - the quadratic cost of 2q gates
|
|
370
|
+
- "tags": float (0.5) - the default weight of the topological basis.
|
|
371
|
+
Returns:
|
|
372
|
+
cirq.Circuit - the optimized circuit, where each moment is as parallel as possible.
|
|
373
|
+
it is also broken into native CZ gate set of {CZ, PhXZ}
|
|
374
|
+
"""
|
|
375
|
+
hyperparameters = _get_hyperparameters(hyperparameters)
|
|
376
|
+
|
|
377
|
+
if auto_tag:
|
|
378
|
+
# Transpile the circuit to a native CZ gate set.
|
|
379
|
+
transpiled_circuit = transpile(circuit)
|
|
380
|
+
# Annotate the circuit with topological information
|
|
381
|
+
# to improve parallelization
|
|
382
|
+
transpiled_circuit, group_weights = auto_similarity(
|
|
383
|
+
transpiled_circuit,
|
|
384
|
+
weight_1q=hyperparameters.get("1q", 1.0),
|
|
385
|
+
weight_2q=hyperparameters.get("2q", 1.0),
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
group_weights = {}
|
|
389
|
+
epochs = colorize(
|
|
390
|
+
generate_epochs(
|
|
391
|
+
solve_epochs(
|
|
392
|
+
directed=to_dag_circuit(transpiled_circuit),
|
|
393
|
+
group_weights=group_weights,
|
|
394
|
+
hyperparameters=hyperparameters,
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
# Convert the epochs to a cirq circuit.
|
|
399
|
+
moments = map(cirq.Moment, epochs)
|
|
400
|
+
return cirq.Circuit(moments)
|