bloqade-circuit 0.6.8__py3-none-any.whl → 0.7.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/analysis/measure_id/analysis.py +10 -11
- bloqade/analysis/measure_id/impls.py +15 -2
- bloqade/cirq_utils/noise/__init__.py +0 -2
- bloqade/cirq_utils/noise/_two_zone_utils.py +7 -15
- bloqade/cirq_utils/noise/model.py +141 -188
- bloqade/cirq_utils/noise/transform.py +2 -2
- bloqade/pyqrack/squin/qubit.py +4 -2
- bloqade/pyqrack/squin/runtime.py +14 -6
- bloqade/squin/cirq/emit/op.py +37 -5
- bloqade/squin/cirq/emit/qubit.py +4 -4
- bloqade/squin/cirq/emit/runtime.py +0 -15
- bloqade/squin/cirq/lowering.py +3 -9
- bloqade/squin/gate.py +7 -0
- bloqade/squin/lowering.py +26 -0
- bloqade/squin/noise/__init__.py +0 -1
- bloqade/squin/noise/_wrapper.py +2 -6
- bloqade/squin/noise/rewrite.py +0 -11
- bloqade/squin/noise/stmts.py +2 -14
- bloqade/squin/op/_wrapper.py +4 -4
- bloqade/squin/op/stmts.py +33 -9
- bloqade/squin/op/types.py +104 -2
- bloqade/squin/qubit.py +27 -40
- bloqade/squin/rewrite/desugar.py +44 -66
- bloqade/stim/passes/squin_to_stim.py +21 -4
- bloqade/stim/rewrite/ifs_to_stim.py +6 -1
- bloqade/stim/rewrite/qubit_to_stim.py +1 -1
- bloqade/stim/rewrite/squin_noise.py +9 -7
- bloqade/stim/rewrite/util.py +15 -3
- {bloqade_circuit-0.6.8.dist-info → bloqade_circuit-0.7.0.dist-info}/METADATA +1 -1
- {bloqade_circuit-0.6.8.dist-info → bloqade_circuit-0.7.0.dist-info}/RECORD +32 -32
- {bloqade_circuit-0.6.8.dist-info → bloqade_circuit-0.7.0.dist-info}/WHEEL +0 -0
- {bloqade_circuit-0.6.8.dist-info → bloqade_circuit-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import TypeVar
|
|
2
2
|
from dataclasses import field, dataclass
|
|
3
3
|
|
|
4
|
-
from kirin import ir
|
|
4
|
+
from kirin import ir
|
|
5
5
|
from kirin.analysis import ForwardExtra, const
|
|
6
6
|
from kirin.analysis.forward import ForwardFrame
|
|
7
7
|
|
|
@@ -37,20 +37,19 @@ class MeasurementIDAnalysis(ForwardExtra[MeasureIDFrame, MeasureId]):
|
|
|
37
37
|
# NOTE: we do not support dynamic calls here, thus no need to propagate method object
|
|
38
38
|
return self.run_callable(method.code, (self.lattice.bottom(),) + args)
|
|
39
39
|
|
|
40
|
-
T = TypeVar("T")
|
|
41
|
-
|
|
42
40
|
# Xiu-zhe (Roger) Luo came up with this in the address analysis,
|
|
43
|
-
# reused here for convenience
|
|
41
|
+
# reused here for convenience (now modified to be a bit more graceful)
|
|
44
42
|
# TODO: Remove this function once upgrade to kirin 0.18 happens,
|
|
45
43
|
# method is built-in to interpreter then
|
|
46
|
-
|
|
44
|
+
|
|
45
|
+
T = TypeVar("T")
|
|
46
|
+
|
|
47
|
+
def get_const_value(
|
|
48
|
+
self, input_type: type[T], value: ir.SSAValue
|
|
49
|
+
) -> type[T] | None:
|
|
47
50
|
if isinstance(hint := value.hints.get("const"), const.Value):
|
|
48
51
|
data = hint.data
|
|
49
52
|
if isinstance(data, input_type):
|
|
50
53
|
return hint.data
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
raise interp.InterpreterError(
|
|
55
|
-
f"Expected constant value <type = {input_type}>, got {value}"
|
|
56
|
-
)
|
|
54
|
+
|
|
55
|
+
return None
|
|
@@ -103,10 +103,23 @@ class PyIndexing(interp.MethodTable):
|
|
|
103
103
|
def getitem(
|
|
104
104
|
self, interp: MeasurementIDAnalysis, frame: interp.Frame, stmt: py.GetItem
|
|
105
105
|
):
|
|
106
|
-
|
|
106
|
+
|
|
107
|
+
idx_or_slice = interp.get_const_value((int, slice), stmt.index)
|
|
108
|
+
if idx_or_slice is None:
|
|
109
|
+
return (InvalidMeasureId(),)
|
|
110
|
+
|
|
111
|
+
# hint = stmt.index.hints.get("const")
|
|
112
|
+
# if hint is None or not isinstance(hint, const.Value):
|
|
113
|
+
# return (InvalidMeasureId(),)
|
|
114
|
+
|
|
107
115
|
obj = frame.get(stmt.obj)
|
|
108
116
|
if isinstance(obj, MeasureIdTuple):
|
|
109
|
-
|
|
117
|
+
if isinstance(idx_or_slice, slice):
|
|
118
|
+
return (MeasureIdTuple(data=obj.data[idx_or_slice]),)
|
|
119
|
+
elif isinstance(idx_or_slice, int):
|
|
120
|
+
return (obj.data[idx_or_slice],)
|
|
121
|
+
else:
|
|
122
|
+
return (InvalidMeasureId(),)
|
|
110
123
|
# just propagate these down the line
|
|
111
124
|
elif isinstance(obj, (AnyMeasureId, NotMeasureId)):
|
|
112
125
|
return (obj,)
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
from .model import (
|
|
4
4
|
GeminiOneZoneNoiseModel as GeminiOneZoneNoiseModel,
|
|
5
5
|
GeminiTwoZoneNoiseModel as GeminiTwoZoneNoiseModel,
|
|
6
|
-
GeminiOneZoneNoiseModelABC as GeminiOneZoneNoiseModelABC,
|
|
7
|
-
GeminiOneZoneNoiseModelCorrelated as GeminiOneZoneNoiseModelCorrelated,
|
|
8
6
|
GeminiOneZoneNoiseModelConflictGraphMoves as GeminiOneZoneNoiseModelConflictGraphMoves,
|
|
9
7
|
)
|
|
10
8
|
from .transform import transform_circuit as transform_circuit
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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,47 @@ 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
|
+
if (
|
|
60
|
+
self.cz_paired_correlated_rates is None
|
|
61
|
+
and self.cz_paired_error_probabilities is None
|
|
62
|
+
):
|
|
63
|
+
# NOTE: no input, set to default value; weird setattr for frozen dataclass
|
|
64
|
+
object.__setattr__(
|
|
65
|
+
self,
|
|
66
|
+
"cz_paired_error_probabilities",
|
|
67
|
+
_default_cz_paired_correlated_rates(),
|
|
68
|
+
)
|
|
69
|
+
elif (
|
|
70
|
+
self.cz_paired_correlated_rates is not None
|
|
71
|
+
and self.cz_paired_correlated_rates is None
|
|
72
|
+
):
|
|
73
|
+
|
|
74
|
+
if self.cz_paired_correlated_rates.shape != (4, 4):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"Expected a 4x4 array of probabilities for cz_paired_correlated_rates"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# NOTE: convert array to dict
|
|
80
|
+
object.__setattr__(
|
|
81
|
+
self,
|
|
82
|
+
"cz_paired_error_probabilities",
|
|
83
|
+
correlated_noise_array_to_dict(self.cz_paired_correlated_rates),
|
|
84
|
+
)
|
|
85
|
+
elif (
|
|
86
|
+
self.cz_paired_correlated_rates is not None
|
|
87
|
+
and self.cz_paired_correlated_rates is not None
|
|
88
|
+
):
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"Received both `cz_paired_correlated_rates` and `cz_paired_correlated_rates` as input. This is ambiguous, please only set one."
|
|
91
|
+
)
|
|
92
|
+
|
|
25
93
|
@staticmethod
|
|
26
94
|
def validate_moments(moments: Iterable[cirq.Moment]):
|
|
27
95
|
allowed_target_gates: frozenset[cirq.GateFamily] = cirq.CZTargetGateset().gates
|
|
@@ -81,72 +149,33 @@ class GeminiNoiseModelABC(cirq.NoiseModel, MoveNoiseModelABC):
|
|
|
81
149
|
self.cz_unpaired_gate_pz,
|
|
82
150
|
)
|
|
83
151
|
|
|
152
|
+
@property
|
|
153
|
+
def two_qubit_pauli(self) -> cirq.AsymmetricDepolarizingChannel:
|
|
154
|
+
# NOTE: if this was None it would error when instantiating self
|
|
155
|
+
# quiet the linter for the copy below
|
|
156
|
+
error_probabilities = cast(dict, self.cz_paired_error_probabilities)
|
|
84
157
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
158
|
+
# NOTE: copy dict since cirq modifies it in-place somewhere
|
|
159
|
+
return cirq.AsymmetricDepolarizingChannel(
|
|
160
|
+
error_probabilities=error_probabilities.copy()
|
|
136
161
|
)
|
|
137
162
|
|
|
138
163
|
|
|
139
164
|
@dataclass(frozen=True)
|
|
140
|
-
class GeminiOneZoneNoiseModel(
|
|
165
|
+
class GeminiOneZoneNoiseModel(GeminiNoiseModelABC):
|
|
141
166
|
"""
|
|
142
167
|
A Cirq-compatible noise model for a one-zone implementation of the Gemini architecture.
|
|
143
168
|
|
|
144
169
|
This model introduces custom asymmetric depolarizing noise for both single- and two-qubit gates
|
|
145
170
|
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,
|
|
171
|
+
atoms are in the entangling zone, errors are applied that stem from application of Rydberg error, even for
|
|
147
172
|
qubits not actively involved in a gate operation.
|
|
173
|
+
|
|
174
|
+
Note, that the noise applied to entangling pairs is correlated.
|
|
148
175
|
"""
|
|
149
176
|
|
|
177
|
+
parallelize_circuit: bool = False
|
|
178
|
+
|
|
150
179
|
def _single_qubit_moment_noise_ops(
|
|
151
180
|
self, moment: cirq.Moment, system_qubits: Sequence[cirq.Qid]
|
|
152
181
|
) -> tuple[list, list]:
|
|
@@ -206,132 +235,6 @@ class GeminiOneZoneNoiseModel(GeminiOneZoneNoiseModelABC):
|
|
|
206
235
|
|
|
207
236
|
return [gate_noise_op], []
|
|
208
237
|
|
|
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
238
|
def noisy_moment(self, moment, system_qubits):
|
|
336
239
|
# Moment with original ops
|
|
337
240
|
original_moment = moment
|
|
@@ -391,6 +294,53 @@ class GeminiOneZoneNoiseModelCorrelated(GeminiOneZoneNoiseModel):
|
|
|
391
294
|
*move_noise_moments,
|
|
392
295
|
]
|
|
393
296
|
|
|
297
|
+
def noisy_moments(
|
|
298
|
+
self, moments: Iterable[cirq.Moment], system_qubits: Sequence[cirq.Qid]
|
|
299
|
+
) -> Sequence[cirq.OP_TREE]:
|
|
300
|
+
"""Adds possibly stateful noise to a series of moments.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
moments: The moments to add noise to.
|
|
304
|
+
system_qubits: A list of all qubits in the system.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
A sequence of OP_TREEEs, with the k'th tree corresponding to the
|
|
308
|
+
noisy operations for the k'th moment.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
if self.check_input_circuit:
|
|
312
|
+
self.validate_moments(moments)
|
|
313
|
+
|
|
314
|
+
# Split into moments with only 1Q and 2Q gates
|
|
315
|
+
moments_1q = [
|
|
316
|
+
cirq.Moment([op for op in moment.operations if len(op.qubits) == 1])
|
|
317
|
+
for moment in moments
|
|
318
|
+
]
|
|
319
|
+
moments_2q = [
|
|
320
|
+
cirq.Moment([op for op in moment.operations if len(op.qubits) == 2])
|
|
321
|
+
for moment in moments
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
assert len(moments_1q) == len(moments_2q)
|
|
325
|
+
|
|
326
|
+
interleaved_moments = []
|
|
327
|
+
for idx, moment in enumerate(moments_1q):
|
|
328
|
+
interleaved_moments.append(moment)
|
|
329
|
+
interleaved_moments.append(moments_2q[idx])
|
|
330
|
+
|
|
331
|
+
interleaved_circuit = cirq.Circuit.from_moments(*interleaved_moments)
|
|
332
|
+
|
|
333
|
+
# Combine subsequent 1Q gates
|
|
334
|
+
compressed_circuit = cirq.merge_single_qubit_moments_to_phxz(
|
|
335
|
+
interleaved_circuit
|
|
336
|
+
)
|
|
337
|
+
if self.parallelize_circuit:
|
|
338
|
+
compressed_circuit = parallelize(compressed_circuit)
|
|
339
|
+
|
|
340
|
+
return self._noisy_moments_impl_moment(
|
|
341
|
+
compressed_circuit.moments, system_qubits
|
|
342
|
+
)
|
|
343
|
+
|
|
394
344
|
|
|
395
345
|
@dataclass(frozen=True)
|
|
396
346
|
class GeminiOneZoneNoiseModelConflictGraphMoves(GeminiOneZoneNoiseModel):
|
|
@@ -452,14 +402,18 @@ class GeminiOneZoneNoiseModelConflictGraphMoves(GeminiOneZoneNoiseModel):
|
|
|
452
402
|
gated_qubits = control_qubits + target_qubits
|
|
453
403
|
idle_atoms = list(set(system_qubits) - set(gated_qubits))
|
|
454
404
|
|
|
405
|
+
# Add correlated noise channels for entangling pairs
|
|
406
|
+
two_qubit_pauli = self.two_qubit_pauli
|
|
455
407
|
gate_noise_ops = [
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
408
|
+
two_qubit_pauli.on(c, t) for c, t in zip(control_qubits, target_qubits)
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
# In this 1 zone scheme, all unpaired atoms are in the entangling zone.
|
|
412
|
+
gate_noise_ops.append(
|
|
459
413
|
cirq.asymmetric_depolarize(*self.cz_unpaired_pauli_rates).on_each(
|
|
460
414
|
idle_atoms
|
|
461
415
|
),
|
|
462
|
-
|
|
416
|
+
)
|
|
463
417
|
else:
|
|
464
418
|
raise ValueError(
|
|
465
419
|
"Moment contains operations with more than 2 qubits, which is not supported."
|
|
@@ -530,10 +484,9 @@ class GeminiTwoZoneNoiseModel(GeminiNoiseModelABC):
|
|
|
530
484
|
moments[i],
|
|
531
485
|
np.array(self.local_pauli_rates),
|
|
532
486
|
np.array(self.global_pauli_rates),
|
|
533
|
-
|
|
534
|
-
self.cz_paired_pauli_rates + self.cz_paired_pauli_rates
|
|
535
|
-
),
|
|
487
|
+
self.two_qubit_pauli,
|
|
536
488
|
np.array(self.cz_unpaired_pauli_rates),
|
|
489
|
+
nqubs,
|
|
537
490
|
).moments
|
|
538
491
|
if len(moment) > 0
|
|
539
492
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import cirq
|
|
2
2
|
|
|
3
|
-
from .model import GeminiOneZoneNoiseModel
|
|
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,
|
|
39
|
+
model, GeminiOneZoneNoiseModel
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
system_qubits = sorted(circuit.all_qubits())
|
bloqade/pyqrack/squin/qubit.py
CHANGED
|
@@ -25,7 +25,7 @@ class PyQrackMethods(interp.MethodTable):
|
|
|
25
25
|
|
|
26
26
|
@interp.impl(qubit.Apply)
|
|
27
27
|
def apply(self, interp: PyQrackInterpreter, frame: interp.Frame, stmt: qubit.Apply):
|
|
28
|
-
qubits:
|
|
28
|
+
qubits: list[PyQrackQubit] = [frame.get(qbit) for qbit in stmt.qubits]
|
|
29
29
|
operator: OperatorRuntimeABC = frame.get(stmt.operator)
|
|
30
30
|
operator.apply(*qubits)
|
|
31
31
|
|
|
@@ -34,7 +34,9 @@ class PyQrackMethods(interp.MethodTable):
|
|
|
34
34
|
self, interp: PyQrackInterpreter, frame: interp.Frame, stmt: qubit.Broadcast
|
|
35
35
|
):
|
|
36
36
|
operator: OperatorRuntimeABC = frame.get(stmt.operator)
|
|
37
|
-
qubits: ilist.IList[PyQrackQubit, Any] =
|
|
37
|
+
qubits: list[ilist.IList[PyQrackQubit, Any]] = [
|
|
38
|
+
frame.get(qbit) for qbit in stmt.qubits
|
|
39
|
+
]
|
|
38
40
|
operator.broadcast_apply(qubits)
|
|
39
41
|
|
|
40
42
|
def _measure_qubit(self, qbit: PyQrackQubit, interp: PyQrackInterpreter):
|
bloqade/pyqrack/squin/runtime.py
CHANGED
|
@@ -28,17 +28,25 @@ class OperatorRuntimeABC:
|
|
|
28
28
|
) -> None:
|
|
29
29
|
raise RuntimeError(f"Can't apply controlled version of {self}")
|
|
30
30
|
|
|
31
|
-
def broadcast_apply(
|
|
31
|
+
def broadcast_apply(
|
|
32
|
+
self, qubit_lists: list[ilist.IList[PyQrackQubit, Any]], **kwargs
|
|
33
|
+
) -> None:
|
|
32
34
|
n = self.n_sites
|
|
33
35
|
|
|
34
|
-
if
|
|
36
|
+
if n != len(qubit_lists):
|
|
35
37
|
raise RuntimeError(
|
|
36
|
-
f"Cannot
|
|
38
|
+
f"Cannot apply operator of size {n} to {len(qubit_lists)} qubits!"
|
|
37
39
|
)
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
m = len(qubit_lists[0])
|
|
42
|
+
for qubit_list in qubit_lists:
|
|
43
|
+
if m != len(qubit_list):
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
"Cannot broadcast operator on qubit lists of varying length!"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
for qubits in zip(*qubit_lists):
|
|
49
|
+
self.apply(*qubits, **kwargs)
|
|
42
50
|
|
|
43
51
|
|
|
44
52
|
@dataclass(frozen=True)
|