bloqade-circuit 0.6.2__py3-none-any.whl → 0.9.1__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.
Files changed (192) hide show
  1. bloqade/analysis/address/__init__.py +8 -4
  2. bloqade/analysis/address/analysis.py +123 -33
  3. bloqade/analysis/address/impls.py +293 -90
  4. bloqade/analysis/address/lattice.py +209 -24
  5. bloqade/analysis/fidelity/analysis.py +11 -23
  6. bloqade/analysis/measure_id/__init__.py +4 -1
  7. bloqade/analysis/measure_id/analysis.py +29 -20
  8. bloqade/analysis/measure_id/impls.py +72 -31
  9. bloqade/annotate/__init__.py +6 -0
  10. bloqade/annotate/_dialect.py +3 -0
  11. bloqade/annotate/_interface.py +22 -0
  12. bloqade/annotate/stmts.py +29 -0
  13. bloqade/annotate/types.py +13 -0
  14. bloqade/cirq_utils/__init__.py +4 -2
  15. bloqade/cirq_utils/emit/__init__.py +3 -0
  16. bloqade/cirq_utils/emit/base.py +246 -0
  17. bloqade/cirq_utils/emit/gate.py +104 -0
  18. bloqade/cirq_utils/emit/noise.py +90 -0
  19. bloqade/cirq_utils/emit/qubit.py +35 -0
  20. bloqade/cirq_utils/lowering.py +660 -0
  21. bloqade/cirq_utils/noise/__init__.py +0 -2
  22. bloqade/cirq_utils/noise/_two_zone_utils.py +7 -15
  23. bloqade/cirq_utils/noise/model.py +151 -191
  24. bloqade/cirq_utils/noise/transform.py +2 -2
  25. bloqade/cirq_utils/parallelize.py +9 -6
  26. bloqade/gemini/__init__.py +1 -0
  27. bloqade/gemini/analysis/__init__.py +3 -0
  28. bloqade/gemini/analysis/logical_validation/__init__.py +1 -0
  29. bloqade/gemini/analysis/logical_validation/analysis.py +17 -0
  30. bloqade/gemini/analysis/logical_validation/impls.py +101 -0
  31. bloqade/gemini/groups.py +67 -0
  32. bloqade/native/__init__.py +23 -0
  33. bloqade/native/_prelude.py +45 -0
  34. bloqade/native/dialects/__init__.py +0 -0
  35. bloqade/native/dialects/gate/__init__.py +2 -0
  36. bloqade/native/dialects/gate/_dialect.py +3 -0
  37. bloqade/native/dialects/gate/_interface.py +32 -0
  38. bloqade/native/dialects/gate/stmts.py +31 -0
  39. bloqade/native/stdlib/__init__.py +0 -0
  40. bloqade/native/stdlib/broadcast.py +246 -0
  41. bloqade/native/stdlib/simple.py +220 -0
  42. bloqade/native/upstream/__init__.py +4 -0
  43. bloqade/native/upstream/squin2native.py +79 -0
  44. bloqade/pyqrack/__init__.py +2 -2
  45. bloqade/pyqrack/base.py +7 -1
  46. bloqade/pyqrack/device.py +190 -4
  47. bloqade/pyqrack/native.py +49 -0
  48. bloqade/pyqrack/reg.py +6 -6
  49. bloqade/pyqrack/squin/gate/__init__.py +1 -0
  50. bloqade/pyqrack/squin/gate/gate.py +136 -0
  51. bloqade/pyqrack/squin/noise/native.py +120 -54
  52. bloqade/pyqrack/squin/qubit.py +39 -36
  53. bloqade/pyqrack/target.py +5 -4
  54. bloqade/pyqrack/task.py +114 -7
  55. bloqade/qasm2/_qasm_loading.py +3 -3
  56. bloqade/qasm2/dialects/core/address.py +21 -12
  57. bloqade/qasm2/dialects/expr/_emit.py +19 -8
  58. bloqade/qasm2/dialects/expr/stmts.py +7 -7
  59. bloqade/qasm2/dialects/noise/fidelity.py +4 -8
  60. bloqade/qasm2/dialects/noise/model.py +2 -1
  61. bloqade/qasm2/emit/base.py +16 -11
  62. bloqade/qasm2/emit/gate.py +11 -8
  63. bloqade/qasm2/emit/main.py +103 -3
  64. bloqade/qasm2/emit/target.py +9 -5
  65. bloqade/qasm2/groups.py +3 -2
  66. bloqade/qasm2/parse/lowering.py +0 -1
  67. bloqade/qasm2/passes/fold.py +14 -73
  68. bloqade/qasm2/passes/glob.py +2 -2
  69. bloqade/qasm2/passes/noise.py +1 -1
  70. bloqade/qasm2/passes/parallel.py +7 -5
  71. bloqade/qasm2/rewrite/__init__.py +0 -1
  72. bloqade/qasm2/rewrite/noise/heuristic_noise.py +7 -17
  73. bloqade/qasm2/rewrite/parallel_to_glob.py +28 -15
  74. bloqade/qasm2/rewrite/parallel_to_uop.py +2 -8
  75. bloqade/qasm2/rewrite/register.py +2 -2
  76. bloqade/qasm2/rewrite/uop_to_parallel.py +4 -2
  77. bloqade/qbraid/lowering.py +1 -0
  78. bloqade/qbraid/schema.py +2 -2
  79. bloqade/qubit/__init__.py +12 -0
  80. bloqade/qubit/_dialect.py +3 -0
  81. bloqade/qubit/_interface.py +49 -0
  82. bloqade/qubit/_prelude.py +45 -0
  83. bloqade/qubit/analysis/__init__.py +1 -0
  84. bloqade/qubit/analysis/address_impl.py +40 -0
  85. bloqade/qubit/stdlib/__init__.py +2 -0
  86. bloqade/qubit/stdlib/_new.py +34 -0
  87. bloqade/qubit/stdlib/broadcast.py +62 -0
  88. bloqade/qubit/stdlib/simple.py +59 -0
  89. bloqade/qubit/stmts.py +60 -0
  90. bloqade/rewrite/passes/__init__.py +6 -0
  91. bloqade/rewrite/passes/aggressive_unroll.py +103 -0
  92. bloqade/rewrite/passes/callgraph.py +116 -0
  93. bloqade/rewrite/passes/canonicalize_ilist.py +20 -14
  94. bloqade/rewrite/rules/split_ifs.py +18 -1
  95. bloqade/squin/__init__.py +47 -14
  96. bloqade/squin/analysis/__init__.py +0 -1
  97. bloqade/squin/analysis/schedule.py +10 -11
  98. bloqade/squin/gate/__init__.py +2 -0
  99. bloqade/squin/gate/_dialect.py +3 -0
  100. bloqade/squin/gate/_interface.py +98 -0
  101. bloqade/squin/gate/stmts.py +125 -0
  102. bloqade/squin/groups.py +5 -22
  103. bloqade/squin/noise/__init__.py +1 -10
  104. bloqade/squin/noise/_dialect.py +1 -1
  105. bloqade/squin/noise/_interface.py +45 -0
  106. bloqade/squin/noise/stmts.py +66 -28
  107. bloqade/squin/rewrite/U3_to_clifford.py +70 -51
  108. bloqade/squin/rewrite/__init__.py +0 -2
  109. bloqade/squin/rewrite/remove_dangling_qubits.py +2 -2
  110. bloqade/squin/rewrite/wrap_analysis.py +4 -35
  111. bloqade/squin/stdlib/__init__.py +0 -0
  112. bloqade/squin/stdlib/broadcast/__init__.py +34 -0
  113. bloqade/squin/stdlib/broadcast/_qubit.py +4 -0
  114. bloqade/squin/stdlib/broadcast/gate.py +260 -0
  115. bloqade/squin/stdlib/broadcast/noise.py +144 -0
  116. bloqade/squin/stdlib/simple/__init__.py +33 -0
  117. bloqade/squin/stdlib/simple/gate.py +242 -0
  118. bloqade/squin/stdlib/simple/noise.py +126 -0
  119. bloqade/stim/__init__.py +1 -0
  120. bloqade/stim/_wrappers.py +6 -0
  121. bloqade/stim/dialects/auxiliary/emit.py +19 -18
  122. bloqade/stim/dialects/collapse/emit_str.py +7 -8
  123. bloqade/stim/dialects/gate/emit.py +9 -10
  124. bloqade/stim/dialects/noise/emit.py +17 -13
  125. bloqade/stim/dialects/noise/stmts.py +5 -3
  126. bloqade/stim/emit/__init__.py +1 -0
  127. bloqade/stim/emit/impls.py +16 -0
  128. bloqade/stim/emit/stim_str.py +48 -31
  129. bloqade/stim/groups.py +12 -2
  130. bloqade/stim/parse/lowering.py +14 -17
  131. bloqade/stim/passes/__init__.py +3 -1
  132. bloqade/stim/passes/flatten.py +26 -0
  133. bloqade/stim/passes/simplify_ifs.py +16 -2
  134. bloqade/stim/passes/squin_to_stim.py +18 -60
  135. bloqade/stim/rewrite/__init__.py +3 -4
  136. bloqade/stim/rewrite/get_record_util.py +24 -0
  137. bloqade/stim/rewrite/ifs_to_stim.py +29 -31
  138. bloqade/stim/rewrite/qubit_to_stim.py +90 -41
  139. bloqade/stim/rewrite/set_detector_to_stim.py +68 -0
  140. bloqade/stim/rewrite/set_observable_to_stim.py +52 -0
  141. bloqade/stim/rewrite/squin_measure.py +11 -79
  142. bloqade/stim/rewrite/squin_noise.py +134 -108
  143. bloqade/stim/rewrite/util.py +5 -192
  144. bloqade/test_utils.py +1 -1
  145. bloqade/types.py +10 -0
  146. bloqade/validation/__init__.py +2 -0
  147. bloqade/validation/analysis/__init__.py +5 -0
  148. bloqade/validation/analysis/analysis.py +41 -0
  149. bloqade/validation/analysis/lattice.py +58 -0
  150. bloqade/validation/kernel_validation.py +77 -0
  151. {bloqade_circuit-0.6.2.dist-info → bloqade_circuit-0.9.1.dist-info}/METADATA +5 -6
  152. bloqade_circuit-0.9.1.dist-info/RECORD +265 -0
  153. bloqade/pyqrack/squin/op.py +0 -166
  154. bloqade/pyqrack/squin/runtime.py +0 -535
  155. bloqade/pyqrack/squin/wire.py +0 -51
  156. bloqade/rewrite/rules/flatten_ilist.py +0 -51
  157. bloqade/rewrite/rules/inline_getitem_ilist.py +0 -31
  158. bloqade/squin/_typeinfer.py +0 -20
  159. bloqade/squin/analysis/address_impl.py +0 -71
  160. bloqade/squin/analysis/nsites/__init__.py +0 -9
  161. bloqade/squin/analysis/nsites/analysis.py +0 -50
  162. bloqade/squin/analysis/nsites/impls.py +0 -92
  163. bloqade/squin/analysis/nsites/lattice.py +0 -49
  164. bloqade/squin/cirq/__init__.py +0 -265
  165. bloqade/squin/cirq/emit/emit_circuit.py +0 -109
  166. bloqade/squin/cirq/emit/noise.py +0 -49
  167. bloqade/squin/cirq/emit/op.py +0 -125
  168. bloqade/squin/cirq/emit/qubit.py +0 -60
  169. bloqade/squin/cirq/emit/runtime.py +0 -242
  170. bloqade/squin/cirq/lowering.py +0 -440
  171. bloqade/squin/lowering.py +0 -54
  172. bloqade/squin/noise/_wrapper.py +0 -40
  173. bloqade/squin/noise/rewrite.py +0 -111
  174. bloqade/squin/op/__init__.py +0 -41
  175. bloqade/squin/op/_dialect.py +0 -3
  176. bloqade/squin/op/_wrapper.py +0 -121
  177. bloqade/squin/op/number.py +0 -5
  178. bloqade/squin/op/rewrite.py +0 -46
  179. bloqade/squin/op/stdlib.py +0 -62
  180. bloqade/squin/op/stmts.py +0 -276
  181. bloqade/squin/op/traits.py +0 -43
  182. bloqade/squin/op/types.py +0 -26
  183. bloqade/squin/qubit.py +0 -184
  184. bloqade/squin/rewrite/canonicalize.py +0 -60
  185. bloqade/squin/rewrite/desugar.py +0 -124
  186. bloqade/squin/types.py +0 -8
  187. bloqade/squin/wire.py +0 -201
  188. bloqade/stim/rewrite/wire_identity_elimination.py +0 -24
  189. bloqade/stim/rewrite/wire_to_stim.py +0 -57
  190. bloqade_circuit-0.6.2.dist-info/RECORD +0 -234
  191. {bloqade_circuit-0.6.2.dist-info → bloqade_circuit-0.9.1.dist-info}/WHEEL +0 -0
  192. {bloqade_circuit-0.6.2.dist-info → bloqade_circuit-0.9.1.dist-info}/licenses/LICENSE +0 -0
@@ -264,8 +264,9 @@ def get_gate_error_channel(
264
264
  moment: cirq.Moment,
265
265
  sq_loc_rates: np.ndarray,
266
266
  sq_glob_rates: np.ndarray,
267
- cz_rates: np.ndarray,
267
+ two_qubit_pauli: cirq.Gate,
268
268
  unp_cz_rates: np.ndarray,
269
+ nqubs: int,
269
270
  ):
270
271
  """Applies gate errors to the circuit
271
272
 
@@ -273,15 +274,15 @@ def get_gate_error_channel(
273
274
  moment: A cirq.Moment object.
274
275
  sq_loc_rates: single local qubit rotation Pauli noise channel parameters (px, py, pz)
275
276
  sq_glob_rates: single global qubit rotation Pauli noise channel parameters (px,py,pz)
276
- cz_rates: two-qubit rotation Pauli noise channel parameters (ctrl_px, ctrl_py,ctrl_pz,tar_px,tar_py,tar_pz)
277
+ two_qubit_pauli: correlated two-qubit noise channel (ctrl_px, ctrl_py,ctrl_pz,tar_px,tar_py,tar_pz)
277
278
  unp_cz_rates: Pauli noise channel parameters for qubits in the gate zone and outside blockade radius
279
+ nqubs: total number of qubits
278
280
  Returns:
279
281
  A new cirq.Moment object with the gate errors applied.
280
282
  """
281
283
  # Check for the moment (layer) layout: global single qubit gates, or mixture of single qubit gates and two qubit gates
282
284
 
283
285
  gates_in_layer = extract_u3_and_cz_qargs(moment)
284
- # new_moment = cirq.Moment()
285
286
  new_moments = cirq.Circuit()
286
287
 
287
288
  if gates_in_layer["cz"] == []:
@@ -294,7 +295,7 @@ def get_gate_error_channel(
294
295
  if all(
295
296
  np.all(np.isclose(element, gates_in_layer["angles"][0]))
296
297
  for element in gates_in_layer["angles"]
297
- ):
298
+ ) and nqubs == len(gates_in_layer["u3"]):
298
299
  pauli_channel = cirq.AsymmetricDepolarizingChannel(
299
300
  p_x=sq_glob_rates[0], p_y=sq_glob_rates[1], p_z=sq_glob_rates[2]
300
301
  )
@@ -314,12 +315,6 @@ def get_gate_error_channel(
314
315
 
315
316
  else:
316
317
  # there is at least one CZ gate...
317
- ctrl_pauli_channel = cirq.AsymmetricDepolarizingChannel(
318
- p_x=cz_rates[0], p_y=cz_rates[1], p_z=cz_rates[2]
319
- )
320
- tar_pauli_channel = cirq.AsymmetricDepolarizingChannel(
321
- p_x=cz_rates[3], p_y=cz_rates[4], p_z=cz_rates[5]
322
- )
323
318
  loc_rot_pauli_channel = cirq.AsymmetricDepolarizingChannel(
324
319
  p_x=sq_loc_rates[0], p_y=sq_loc_rates[1], p_z=sq_loc_rates[2]
325
320
  )
@@ -327,18 +322,15 @@ def get_gate_error_channel(
327
322
  p_x=unp_cz_rates[0], p_y=unp_cz_rates[1], p_z=unp_cz_rates[2]
328
323
  )
329
324
 
325
+ # apply correlated noise to paired qubits
330
326
  for qub in gates_in_layer["cz"]:
331
- new_moments.append(ctrl_pauli_channel(qub[0]))
332
- new_moments.append(tar_pauli_channel(qub[1]))
333
- # new_moment=new_moment+ctrl_pauli_channel(qub[0])
334
- # new_moment=new_moment+tar_pauli_channel(qub[1])
327
+ new_moments.append(two_qubit_pauli.on(qub[0], qub[1]))
335
328
 
336
329
  for qub in gates_in_layer["u3"]:
337
330
  new_moments.append(
338
331
  unp_cz_pauli_channel(qub[0])
339
332
  ) ###qubits in the gate zone get unpaired_cz error
340
333
  new_moments.append(loc_rot_pauli_channel(qub[0]))
341
- # new_moment = new_moment + loc_rot_pauli_channel(qub[0])
342
334
 
343
335
  return new_moments
344
336
 
@@ -1,5 +1,5 @@
1
- from typing import Iterable, Sequence
2
- from dataclasses import field, dataclass
1
+ from typing import Iterable, Sequence, cast
2
+ from dataclasses import dataclass
3
3
 
4
4
  import cirq
5
5
  import numpy as np
@@ -11,6 +11,33 @@ from ..parallelize import parallelize
11
11
  from .conflict_graph import OneZoneConflictGraph
12
12
 
13
13
 
14
+ def _default_cz_paired_correlated_rates() -> dict:
15
+ rates = np.array(
16
+ [
17
+ [9.93492628e-01, 2.27472300e-04, 2.27472300e-04, 1.51277730e-03],
18
+ [2.27472300e-04, 1.42864200e-04, 1.42864200e-04, 1.43082900e-04],
19
+ [2.27472300e-04, 1.42864200e-04, 1.42864200e-04, 1.43082900e-04],
20
+ [1.51277730e-03, 1.43082900e-04, 1.43082900e-04, 1.42813990e-03],
21
+ ]
22
+ )
23
+
24
+ return correlated_noise_array_to_dict(noise_rates=rates)
25
+
26
+
27
+ def correlated_noise_array_to_dict(noise_rates: np.ndarray) -> dict:
28
+ paulis = ("I", "X", "Y", "Z")
29
+ error_probabilities = {}
30
+ for idx1, p1 in enumerate(paulis):
31
+ for idx2, p2 in enumerate(paulis):
32
+ probability = noise_rates[idx1, idx2]
33
+
34
+ if probability > 0:
35
+ key = p1 + p2
36
+ error_probabilities[key] = probability
37
+
38
+ return error_probabilities
39
+
40
+
14
41
  @dataclass(frozen=True)
15
42
  class GeminiNoiseModelABC(cirq.NoiseModel, MoveNoiseModelABC):
16
43
  """Abstract base class for all Gemini noise models."""
@@ -22,6 +49,53 @@ class GeminiNoiseModelABC(cirq.NoiseModel, MoveNoiseModelABC):
22
49
 
23
50
  """
24
51
 
52
+ cz_paired_correlated_rates: np.ndarray | None = None
53
+ """The correlated CZ error rates as a 4x4 array."""
54
+
55
+ cz_paired_error_probabilities: dict | None = None
56
+ """The correlated CZ error rates as a dictionary"""
57
+
58
+ def __post_init__(self):
59
+ is_ambiguous = (
60
+ self.cz_paired_correlated_rates is not None
61
+ and self.cz_paired_error_probabilities is not None
62
+ )
63
+ if is_ambiguous:
64
+ raise ValueError(
65
+ "Received both `cz_paired_correlated_rates` and `cz_paired_error_probabilities` as input. This is ambiguous, please only set one."
66
+ )
67
+
68
+ use_default = (
69
+ self.cz_paired_correlated_rates is None
70
+ and self.cz_paired_error_probabilities is None
71
+ )
72
+ if use_default:
73
+ # NOTE: no input, set to default value; weird setattr for frozen dataclass
74
+ object.__setattr__(
75
+ self,
76
+ "cz_paired_error_probabilities",
77
+ _default_cz_paired_correlated_rates(),
78
+ )
79
+ return
80
+
81
+ if self.cz_paired_correlated_rates is not None:
82
+ if self.cz_paired_correlated_rates.shape != (4, 4):
83
+ raise ValueError(
84
+ "Expected a 4x4 array of probabilities for cz_paired_correlated_rates"
85
+ )
86
+
87
+ # NOTE: convert array to dict
88
+ object.__setattr__(
89
+ self,
90
+ "cz_paired_error_probabilities",
91
+ correlated_noise_array_to_dict(self.cz_paired_correlated_rates),
92
+ )
93
+ return
94
+
95
+ assert (
96
+ self.cz_paired_error_probabilities is not None
97
+ ), "This error should not happen! Please report this issue."
98
+
25
99
  @staticmethod
26
100
  def validate_moments(moments: Iterable[cirq.Moment]):
27
101
  allowed_target_gates: frozenset[cirq.GateFamily] = cirq.CZTargetGateset().gates
@@ -81,72 +155,33 @@ class GeminiNoiseModelABC(cirq.NoiseModel, MoveNoiseModelABC):
81
155
  self.cz_unpaired_gate_pz,
82
156
  )
83
157
 
158
+ @property
159
+ def two_qubit_pauli(self) -> cirq.AsymmetricDepolarizingChannel:
160
+ # NOTE: if this was None it would error when instantiating self
161
+ # quiet the linter for the copy below
162
+ error_probabilities = cast(dict, self.cz_paired_error_probabilities)
84
163
 
85
- @dataclass(frozen=True)
86
- class GeminiOneZoneNoiseModelABC(GeminiNoiseModelABC):
87
- """Abstract base class for all one-zone Gemini noise models."""
88
-
89
- parallelize_circuit: bool = False
90
-
91
- def noisy_moments(
92
- self, moments: Iterable[cirq.Moment], system_qubits: Sequence[cirq.Qid]
93
- ) -> Sequence[cirq.OP_TREE]:
94
- """Adds possibly stateful noise to a series of moments.
95
-
96
- Args:
97
- moments: The moments to add noise to.
98
- system_qubits: A list of all qubits in the system.
99
-
100
- Returns:
101
- A sequence of OP_TREEEs, with the k'th tree corresponding to the
102
- noisy operations for the k'th moment.
103
- """
104
-
105
- if self.check_input_circuit:
106
- self.validate_moments(moments)
107
-
108
- # Split into moments with only 1Q and 2Q gates
109
- moments_1q = [
110
- cirq.Moment([op for op in moment.operations if len(op.qubits) == 1])
111
- for moment in moments
112
- ]
113
- moments_2q = [
114
- cirq.Moment([op for op in moment.operations if len(op.qubits) == 2])
115
- for moment in moments
116
- ]
117
-
118
- assert len(moments_1q) == len(moments_2q)
119
-
120
- interleaved_moments = []
121
- for idx, moment in enumerate(moments_1q):
122
- interleaved_moments.append(moment)
123
- interleaved_moments.append(moments_2q[idx])
124
-
125
- interleaved_circuit = cirq.Circuit.from_moments(*interleaved_moments)
126
-
127
- # Combine subsequent 1Q gates
128
- compressed_circuit = cirq.merge_single_qubit_moments_to_phxz(
129
- interleaved_circuit
130
- )
131
- if self.parallelize_circuit:
132
- compressed_circuit = parallelize(compressed_circuit)
133
-
134
- return self._noisy_moments_impl_moment(
135
- compressed_circuit.moments, system_qubits
164
+ # NOTE: copy dict since cirq modifies it in-place somewhere
165
+ return cirq.AsymmetricDepolarizingChannel(
166
+ error_probabilities=error_probabilities.copy()
136
167
  )
137
168
 
138
169
 
139
170
  @dataclass(frozen=True)
140
- class GeminiOneZoneNoiseModel(GeminiOneZoneNoiseModelABC):
171
+ class GeminiOneZoneNoiseModel(GeminiNoiseModelABC):
141
172
  """
142
173
  A Cirq-compatible noise model for a one-zone implementation of the Gemini architecture.
143
174
 
144
175
  This model introduces custom asymmetric depolarizing noise for both single- and two-qubit gates
145
176
  depending on whether operations are global, local, or part of a CZ interaction. Since the model assumes all
146
- atoms are in the entangling zone, error are applied that stem from application of Rydberg error, even for
177
+ atoms are in the entangling zone, errors are applied that stem from application of Rydberg error, even for
147
178
  qubits not actively involved in a gate operation.
179
+
180
+ Note, that the noise applied to entangling pairs is correlated.
148
181
  """
149
182
 
183
+ parallelize_circuit: bool = False
184
+
150
185
  def _single_qubit_moment_noise_ops(
151
186
  self, moment: cirq.Moment, system_qubits: Sequence[cirq.Qid]
152
187
  ) -> tuple[list, list]:
@@ -206,132 +241,6 @@ class GeminiOneZoneNoiseModel(GeminiOneZoneNoiseModelABC):
206
241
 
207
242
  return [gate_noise_op], []
208
243
 
209
- def noisy_moment(self, moment: cirq.Moment, system_qubits: Sequence[cirq.Qid]):
210
- """
211
- Applies a structured noise model to a given moment depending on the type of operations it contains.
212
-
213
- For single-qubit moments:
214
- - If all gates are identical and act on all qubits, global noise is applied.
215
- - Otherwise, local depolarizing noise is applied per qubit.
216
-
217
- For two-qubit moments:
218
- - Applies move error to move control qubits to target qubits before the gate and again to move back after
219
- the gate.
220
- - Applies gate error to control and target qubits.
221
- - Adds 1q asymmetric noise to qubits that do not participate in a gate.
222
-
223
- Args:
224
- moment: A cirq.Moment containing the original quantum operations.
225
- system_qubits: All qubits in the system (used to determine idleness and global operations).
226
-
227
- Returns:
228
- A list of cirq.Moment objects:
229
- [pre-gate move noise, original moment, post-gate move noise, gate noise moment]
230
-
231
- Raises:
232
- ValueError: If the moment contains multi-qubit gates involving >2 qubits, which are unsupported.
233
- """
234
- # Moment with original ops
235
- original_moment = moment
236
-
237
- # Check if the moment is empty
238
- if len(moment.operations) == 0:
239
- move_noise_ops = []
240
- gate_noise_ops = []
241
- # Check if the moment contains 1-qubit gates or 2-qubit gates
242
- elif len(moment.operations[0].qubits) == 1:
243
- gate_noise_ops, move_noise_ops = self._single_qubit_moment_noise_ops(
244
- moment, system_qubits
245
- )
246
- elif len(moment.operations[0].qubits) == 2:
247
- # Check if the moment only contains two qubit gates
248
- assert np.all([len(op.qubits) == 2 for op in moment.operations])
249
-
250
- control_qubits = [op.qubits[0] for op in moment.operations]
251
- target_qubits = [op.qubits[1] for op in moment.operations]
252
- gated_qubits = control_qubits + target_qubits
253
- idle_atoms = list(set(system_qubits) - set(gated_qubits))
254
-
255
- move_noise_ops = [
256
- cirq.asymmetric_depolarize(*self.mover_pauli_rates).on_each(
257
- control_qubits
258
- ),
259
- cirq.asymmetric_depolarize(*self.sitter_pauli_rates).on_each(
260
- target_qubits + idle_atoms
261
- ),
262
- ] # In this setting, we assume a 1 zone scheme where the controls move to the targets.
263
-
264
- gate_noise_ops = [
265
- cirq.asymmetric_depolarize(*self.cz_paired_pauli_rates).on_each(
266
- gated_qubits
267
- ),
268
- cirq.asymmetric_depolarize(*self.cz_unpaired_pauli_rates).on_each(
269
- idle_atoms
270
- ),
271
- ] # In this 1 zone scheme, all unpaired atoms are in the entangling zone.
272
- else:
273
- raise ValueError(
274
- "Moment contains operations with more than 2 qubits, which is not supported. "
275
- "Correlated measurements should be added after the noise model is applied."
276
- )
277
-
278
- if move_noise_ops == []:
279
- move_noise_moments = []
280
- else:
281
- move_noise_moments = [cirq.Moment(move_noise_ops)]
282
- gate_noise_moment = cirq.Moment(gate_noise_ops)
283
-
284
- return [
285
- *move_noise_moments,
286
- original_moment,
287
- gate_noise_moment,
288
- *move_noise_moments,
289
- ]
290
-
291
-
292
- def _default_cz_paired_correlated_rates() -> np.ndarray:
293
- return np.array(
294
- [
295
- [0.994000006, 0.000142857, 0.000142857, 0.001428570],
296
- [0.000142857, 0.000142857, 0.000142857, 0.000142857],
297
- [0.000142857, 0.000142857, 0.000142857, 0.000142857],
298
- [0.001428570, 0.000142857, 0.000142857, 0.001428570],
299
- ]
300
- )
301
-
302
-
303
- @dataclass(frozen=True)
304
- class GeminiOneZoneNoiseModelCorrelated(GeminiOneZoneNoiseModel):
305
- """
306
- A Cirq noise model for implementing correlated two-qubit Pauli errors in a one-zone Gemini architecture.
307
- """
308
-
309
- cz_paired_correlated_rates: np.ndarray = field(
310
- default_factory=_default_cz_paired_correlated_rates
311
- )
312
-
313
- def __post_init__(self):
314
- if self.cz_paired_correlated_rates.shape != (4, 4):
315
- raise ValueError(
316
- "Expected a 4x4 array of probabilities for cz_paired_correlated_rates"
317
- )
318
-
319
- @property
320
- def two_qubit_pauli(self) -> cirq.AsymmetricDepolarizingChannel:
321
- paulis = ("I", "X", "Y", "Z")
322
- error_probabilities = {}
323
- for idx1, p1 in enumerate(paulis):
324
- for idx2, p2 in enumerate(paulis):
325
- probability = self.cz_paired_correlated_rates[idx1, idx2]
326
-
327
- if probability > 0:
328
- key = p1 + p2
329
- error_probabilities[key] = probability
330
-
331
- return cirq.AsymmetricDepolarizingChannel(
332
- error_probabilities=error_probabilities
333
- )
334
-
335
244
  def noisy_moment(self, moment, system_qubits):
336
245
  # Moment with original ops
337
246
  original_moment = moment
@@ -391,6 +300,53 @@ class GeminiOneZoneNoiseModelCorrelated(GeminiOneZoneNoiseModel):
391
300
  *move_noise_moments,
392
301
  ]
393
302
 
303
+ def noisy_moments(
304
+ self, moments: Iterable[cirq.Moment], system_qubits: Sequence[cirq.Qid]
305
+ ) -> Sequence[cirq.OP_TREE]:
306
+ """Adds possibly stateful noise to a series of moments.
307
+
308
+ Args:
309
+ moments: The moments to add noise to.
310
+ system_qubits: A list of all qubits in the system.
311
+
312
+ Returns:
313
+ A sequence of OP_TREEEs, with the k'th tree corresponding to the
314
+ noisy operations for the k'th moment.
315
+ """
316
+
317
+ if self.check_input_circuit:
318
+ self.validate_moments(moments)
319
+
320
+ # Split into moments with only 1Q and 2Q gates
321
+ moments_1q = [
322
+ cirq.Moment([op for op in moment.operations if len(op.qubits) == 1])
323
+ for moment in moments
324
+ ]
325
+ moments_2q = [
326
+ cirq.Moment([op for op in moment.operations if len(op.qubits) == 2])
327
+ for moment in moments
328
+ ]
329
+
330
+ assert len(moments_1q) == len(moments_2q)
331
+
332
+ interleaved_moments = []
333
+ for idx, moment in enumerate(moments_1q):
334
+ interleaved_moments.append(moment)
335
+ interleaved_moments.append(moments_2q[idx])
336
+
337
+ interleaved_circuit = cirq.Circuit.from_moments(*interleaved_moments)
338
+
339
+ # Combine subsequent 1Q gates
340
+ compressed_circuit = cirq.merge_single_qubit_moments_to_phxz(
341
+ interleaved_circuit
342
+ )
343
+ if self.parallelize_circuit:
344
+ compressed_circuit = parallelize(compressed_circuit)
345
+
346
+ return self._noisy_moments_impl_moment(
347
+ compressed_circuit.moments, system_qubits
348
+ )
349
+
394
350
 
395
351
  @dataclass(frozen=True)
396
352
  class GeminiOneZoneNoiseModelConflictGraphMoves(GeminiOneZoneNoiseModel):
@@ -407,9 +363,10 @@ class GeminiOneZoneNoiseModelConflictGraphMoves(GeminiOneZoneNoiseModel):
407
363
  def noisy_moment(self, moment, system_qubits):
408
364
  # Moment with original ops
409
365
  original_moment = moment
410
- assert np.all(
411
- [isinstance(q, cirq.GridQubit) for q in system_qubits]
412
- ), "Found a qubit that is not a GridQubit."
366
+ assert np.all([isinstance(q, cirq.GridQubit) for q in system_qubits]), (
367
+ "Found a qubit that is not a GridQubit. In order for the conflict graph to know the qubit geometry, "
368
+ "all qubits in the circuit must be defined as cirq.GridQubit objects."
369
+ )
413
370
  # Check if the moment is empty
414
371
  if len(moment.operations) == 0:
415
372
  move_moments = []
@@ -452,14 +409,18 @@ class GeminiOneZoneNoiseModelConflictGraphMoves(GeminiOneZoneNoiseModel):
452
409
  gated_qubits = control_qubits + target_qubits
453
410
  idle_atoms = list(set(system_qubits) - set(gated_qubits))
454
411
 
412
+ # Add correlated noise channels for entangling pairs
413
+ two_qubit_pauli = self.two_qubit_pauli
455
414
  gate_noise_ops = [
456
- cirq.asymmetric_depolarize(*self.cz_paired_pauli_rates).on_each(
457
- gated_qubits
458
- ),
415
+ two_qubit_pauli.on(c, t) for c, t in zip(control_qubits, target_qubits)
416
+ ]
417
+
418
+ # In this 1 zone scheme, all unpaired atoms are in the entangling zone.
419
+ gate_noise_ops.append(
459
420
  cirq.asymmetric_depolarize(*self.cz_unpaired_pauli_rates).on_each(
460
421
  idle_atoms
461
422
  ),
462
- ] # In this 1 zone scheme, all unpaired atoms are in the entangling zone.
423
+ )
463
424
  else:
464
425
  raise ValueError(
465
426
  "Moment contains operations with more than 2 qubits, which is not supported."
@@ -530,10 +491,9 @@ class GeminiTwoZoneNoiseModel(GeminiNoiseModelABC):
530
491
  moments[i],
531
492
  np.array(self.local_pauli_rates),
532
493
  np.array(self.global_pauli_rates),
533
- np.array(
534
- self.cz_paired_pauli_rates + self.cz_paired_pauli_rates
535
- ),
494
+ self.two_qubit_pauli,
536
495
  np.array(self.cz_unpaired_pauli_rates),
496
+ nqubs,
537
497
  ).moments
538
498
  if len(moment) > 0
539
499
  ]
@@ -1,6 +1,6 @@
1
1
  import cirq
2
2
 
3
- from .model import GeminiOneZoneNoiseModel, GeminiOneZoneNoiseModelABC
3
+ from .model import GeminiOneZoneNoiseModel
4
4
  from ..parallelize import transpile, parallelize
5
5
 
6
6
 
@@ -36,7 +36,7 @@ def transform_circuit(
36
36
 
37
37
  # only parallelize here if we aren't parallelizing inside a one-zone model
38
38
  parallelize_circuit_here = parallelize_circuit and not isinstance(
39
- model, GeminiOneZoneNoiseModelABC
39
+ model, GeminiOneZoneNoiseModel
40
40
  )
41
41
 
42
42
  system_qubits = sorted(circuit.all_qubits())
@@ -136,7 +136,7 @@ def auto_similarity(
136
136
  return cirq.Circuit(flattened_circuit), weights
137
137
 
138
138
 
139
- def no_similarity(circuit: cirq.Circuit) -> cirq.Circuit:
139
+ def remove_tags(circuit: cirq.Circuit) -> cirq.Circuit:
140
140
  """
141
141
  Removes all tags from the circuit
142
142
 
@@ -146,10 +146,11 @@ def no_similarity(circuit: cirq.Circuit) -> cirq.Circuit:
146
146
  Returns:
147
147
  [0] - cirq.Circuit - the circuit with all tags removed.
148
148
  """
149
- new_moments = []
150
- for moment in circuit.moments:
151
- new_moments.append([gate.untagged for gate in moment.operations])
152
- return cirq.Circuit(new_moments)
149
+
150
+ def remove_tag(op: cirq.Operation, _):
151
+ return op.untagged
152
+
153
+ return cirq.map_operations(circuit, remove_tag)
153
154
 
154
155
 
155
156
  def to_dag_circuit(circuit: cirq.Circuit, can_reorder=None) -> nx.DiGraph:
@@ -399,4 +400,6 @@ def parallelize(
399
400
  )
400
401
  # Convert the epochs to a cirq circuit.
401
402
  moments = map(cirq.Moment, epochs)
402
- return cirq.Circuit(moments)
403
+ circuit = cirq.Circuit(moments)
404
+
405
+ return remove_tags(circuit)
@@ -0,0 +1 @@
1
+ from .groups import logical as logical
@@ -0,0 +1,3 @@
1
+ from .logical_validation.analysis import (
2
+ GeminiLogicalValidationAnalysis as GeminiLogicalValidationAnalysis,
3
+ )
@@ -0,0 +1 @@
1
+ from . import impls as impls, analysis as analysis # NOTE: register methods
@@ -0,0 +1,17 @@
1
+ from kirin import ir
2
+
3
+ from bloqade import squin
4
+ from bloqade.validation.analysis import ValidationFrame, ValidationAnalysis
5
+
6
+
7
+ class GeminiLogicalValidationAnalysis(ValidationAnalysis):
8
+ keys = ["gemini.validate.logical"]
9
+
10
+ first_gate = True
11
+
12
+ def eval_fallback(self, frame: ValidationFrame, node: ir.Statement):
13
+ if isinstance(node, squin.gate.stmts.Gate):
14
+ # NOTE: to validate that only the first encountered gate can be non-Clifford, we need to track this here
15
+ self.first_gate = False
16
+
17
+ return super().eval_fallback(frame, node)
@@ -0,0 +1,101 @@
1
+ from kirin import ir, interp as _interp
2
+ from kirin.analysis import const
3
+ from kirin.dialects import scf, func
4
+
5
+ from bloqade.squin import gate
6
+ from bloqade.validation.analysis import ValidationFrame
7
+ from bloqade.validation.analysis.lattice import Error
8
+
9
+ from .analysis import GeminiLogicalValidationAnalysis
10
+
11
+
12
+ @scf.dialect.register(key="gemini.validate.logical")
13
+ class __ScfGeminiLogicalValidation(_interp.MethodTable):
14
+
15
+ @_interp.impl(scf.IfElse)
16
+ def if_else(
17
+ self,
18
+ interp: GeminiLogicalValidationAnalysis,
19
+ frame: ValidationFrame,
20
+ stmt: scf.IfElse,
21
+ ):
22
+ frame.errors.append(
23
+ ir.ValidationError(
24
+ stmt, "If statements are not supported in logical Gemini programs!"
25
+ )
26
+ )
27
+ return (
28
+ Error(
29
+ message="If statements are not supported in logical Gemini programs!"
30
+ ),
31
+ )
32
+
33
+ @_interp.impl(scf.For)
34
+ def for_loop(
35
+ self,
36
+ interp: GeminiLogicalValidationAnalysis,
37
+ frame: ValidationFrame,
38
+ stmt: scf.For,
39
+ ):
40
+ if isinstance(stmt.iterable.hints.get("const"), const.Value):
41
+ return (interp.lattice.top(),)
42
+
43
+ frame.errors.append(
44
+ ir.ValidationError(
45
+ stmt,
46
+ "Non-constant iterable in for loop is not supported in Gemini logical programs!",
47
+ )
48
+ )
49
+
50
+ return (
51
+ Error(
52
+ message="Non-constant iterable in for loop is not supported in Gemini logical programs!"
53
+ ),
54
+ )
55
+
56
+
57
+ @func.dialect.register(key="gemini.validate.logical")
58
+ class __FuncGeminiLogicalValidation(_interp.MethodTable):
59
+ @_interp.impl(func.Invoke)
60
+ def invoke(
61
+ self,
62
+ interp: GeminiLogicalValidationAnalysis,
63
+ frame: ValidationFrame,
64
+ stmt: func.Invoke,
65
+ ):
66
+ frame.errors.append(
67
+ ir.ValidationError(
68
+ stmt,
69
+ "Function invocations not supported in logical Gemini program!",
70
+ help="Make sure to decorate your function with `@logical(inline = True)` or `@logical(aggressive_unroll = True)` to inline function calls",
71
+ )
72
+ )
73
+
74
+ return tuple(
75
+ Error(
76
+ message="Function invocations not supported in logical Gemini program!"
77
+ )
78
+ for _ in stmt.results
79
+ )
80
+
81
+
82
+ @gate.dialect.register(key="gemini.validate.logical")
83
+ class __GateGeminiLogicalValidation(_interp.MethodTable):
84
+ @_interp.impl(gate.stmts.U3)
85
+ def u3(
86
+ self,
87
+ interp: GeminiLogicalValidationAnalysis,
88
+ frame: ValidationFrame,
89
+ stmt: gate.stmts.U3,
90
+ ):
91
+ if interp.first_gate:
92
+ interp.first_gate = False
93
+ return ()
94
+
95
+ frame.errors.append(
96
+ ir.ValidationError(
97
+ stmt,
98
+ "U3 gate can only be used for initial state preparation, i.e. as the first gate!",
99
+ )
100
+ )
101
+ return ()