bloqade-circuit 0.4.5__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.

Files changed (37) hide show
  1. bloqade/cirq_utils/__init__.py +7 -0
  2. bloqade/cirq_utils/lineprog.py +295 -0
  3. bloqade/cirq_utils/parallelize.py +400 -0
  4. bloqade/pyqrack/squin/op.py +7 -2
  5. bloqade/pyqrack/squin/runtime.py +4 -2
  6. bloqade/qasm2/dialects/expr/stmts.py +2 -20
  7. bloqade/qasm2/parse/lowering.py +1 -0
  8. bloqade/qasm2/passes/parallel.py +18 -0
  9. bloqade/qasm2/rewrite/__init__.py +1 -0
  10. bloqade/qasm2/rewrite/parallel_to_glob.py +82 -0
  11. bloqade/squin/__init__.py +1 -0
  12. bloqade/squin/_typeinfer.py +20 -0
  13. bloqade/squin/analysis/nsites/impls.py +6 -1
  14. bloqade/squin/cirq/lowering.py +19 -6
  15. bloqade/squin/op/__init__.py +1 -0
  16. bloqade/squin/op/_wrapper.py +4 -0
  17. bloqade/squin/op/stmts.py +20 -2
  18. bloqade/squin/qubit.py +8 -5
  19. bloqade/squin/rewrite/__init__.py +1 -0
  20. bloqade/squin/rewrite/canonicalize.py +60 -0
  21. bloqade/squin/rewrite/desugar.py +52 -5
  22. bloqade/squin/types.py +8 -0
  23. bloqade/squin/wire.py +91 -5
  24. bloqade/stim/__init__.py +1 -0
  25. bloqade/stim/_wrappers.py +4 -0
  26. bloqade/stim/dialects/noise/emit.py +1 -0
  27. bloqade/stim/dialects/noise/stmts.py +5 -0
  28. bloqade/stim/passes/squin_to_stim.py +16 -1
  29. bloqade/stim/rewrite/__init__.py +1 -0
  30. bloqade/stim/rewrite/qubit_to_stim.py +10 -6
  31. bloqade/stim/rewrite/squin_noise.py +120 -0
  32. bloqade/stim/rewrite/util.py +44 -9
  33. bloqade/stim/rewrite/wire_to_stim.py +8 -3
  34. {bloqade_circuit-0.4.5.dist-info → bloqade_circuit-0.5.0.dist-info}/METADATA +4 -2
  35. {bloqade_circuit-0.4.5.dist-info → bloqade_circuit-0.5.0.dist-info}/RECORD +37 -29
  36. {bloqade_circuit-0.4.5.dist-info → bloqade_circuit-0.5.0.dist-info}/WHEEL +0 -0
  37. {bloqade_circuit-0.4.5.dist-info → bloqade_circuit-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,7 @@
1
+ from .parallelize import (
2
+ parallelize as parallelize,
3
+ no_similarity as no_similarity,
4
+ auto_similarity as auto_similarity,
5
+ block_similarity as block_similarity,
6
+ moment_similarity as moment_similarity,
7
+ )
@@ -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)