bloqade-circuit 0.6.0__py3-none-any.whl → 0.6.2__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.
- bloqade/cirq_utils/__init__.py +2 -0
- bloqade/cirq_utils/noise/__init__.py +11 -0
- bloqade/cirq_utils/noise/_two_zone_utils.py +531 -0
- bloqade/cirq_utils/noise/conflict_graph.py +166 -0
- bloqade/cirq_utils/noise/model.py +544 -0
- bloqade/cirq_utils/noise/transform.py +57 -0
- bloqade/cirq_utils/parallelize.py +4 -2
- bloqade/qasm2/_qasm_loading.py +4 -1
- bloqade/qasm2/parse/lowering.py +11 -3
- bloqade/squin/cirq/lowering.py +53 -5
- bloqade/squin/lowering.py +29 -2
- {bloqade_circuit-0.6.0.dist-info → bloqade_circuit-0.6.2.dist-info}/METADATA +1 -1
- {bloqade_circuit-0.6.0.dist-info → bloqade_circuit-0.6.2.dist-info}/RECORD +15 -10
- {bloqade_circuit-0.6.0.dist-info → bloqade_circuit-0.6.2.dist-info}/WHEEL +0 -0
- {bloqade_circuit-0.6.0.dist-info → bloqade_circuit-0.6.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
from typing import Iterable, Sequence
|
|
2
|
+
from dataclasses import field, dataclass
|
|
3
|
+
|
|
4
|
+
import cirq
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from bloqade.qasm2.dialects.noise import MoveNoiseModelABC
|
|
8
|
+
|
|
9
|
+
from . import _two_zone_utils
|
|
10
|
+
from ..parallelize import parallelize
|
|
11
|
+
from .conflict_graph import OneZoneConflictGraph
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class GeminiNoiseModelABC(cirq.NoiseModel, MoveNoiseModelABC):
|
|
16
|
+
"""Abstract base class for all Gemini noise models."""
|
|
17
|
+
|
|
18
|
+
check_input_circuit: bool = True
|
|
19
|
+
"""Determine whether or not to verify that the circuit only contains native gates.
|
|
20
|
+
|
|
21
|
+
**Caution**: Disabling this for circuits containing non-native gates may lead to incorrect results!
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def validate_moments(moments: Iterable[cirq.Moment]):
|
|
27
|
+
allowed_target_gates: frozenset[cirq.GateFamily] = cirq.CZTargetGateset().gates
|
|
28
|
+
|
|
29
|
+
for moment in moments:
|
|
30
|
+
for operation in moment:
|
|
31
|
+
if not isinstance(operation, cirq.Operation):
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
gate = operation.gate
|
|
35
|
+
for allowed_family in allowed_target_gates:
|
|
36
|
+
if gate in allowed_family:
|
|
37
|
+
break
|
|
38
|
+
else:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Noise model only supported for circuits containing native gates part of the CZTargetGateSet, but encountered {operation} in moment {moment}! "
|
|
41
|
+
"To solve this error you can either use the `bloqade.cirq_utils.noise.transform` method setting `to_target_gateset = True` "
|
|
42
|
+
"or use the `bloqade.cirq_utils.transpile` method to convert the circuit before applying the noise model."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def parallel_cz_errors(
|
|
46
|
+
self, ctrls: list[int], qargs: list[int], rest: list[int]
|
|
47
|
+
) -> dict[tuple[float, float, float, float], list[int]]:
|
|
48
|
+
raise NotImplementedError(
|
|
49
|
+
"This noise model doesn't support rewrites on bloqade kernels, but should be used with cirq."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def mover_pauli_rates(self) -> tuple[float, float, float]:
|
|
54
|
+
return (self.mover_px, self.mover_py, self.mover_pz)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def sitter_pauli_rates(self) -> tuple[float, float, float]:
|
|
58
|
+
return (self.sitter_px, self.sitter_py, self.sitter_pz)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def global_pauli_rates(self) -> tuple[float, float, float]:
|
|
62
|
+
return (self.global_px, self.global_py, self.global_pz)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def local_pauli_rates(self) -> tuple[float, float, float]:
|
|
66
|
+
return (self.local_px, self.local_py, self.local_pz)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def cz_paired_pauli_rates(self) -> tuple[float, float, float]:
|
|
70
|
+
return (
|
|
71
|
+
self.cz_paired_gate_px,
|
|
72
|
+
self.cz_paired_gate_py,
|
|
73
|
+
self.cz_paired_gate_pz,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def cz_unpaired_pauli_rates(self) -> tuple[float, float, float]:
|
|
78
|
+
return (
|
|
79
|
+
self.cz_unpaired_gate_px,
|
|
80
|
+
self.cz_unpaired_gate_py,
|
|
81
|
+
self.cz_unpaired_gate_pz,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
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
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class GeminiOneZoneNoiseModel(GeminiOneZoneNoiseModelABC):
|
|
141
|
+
"""
|
|
142
|
+
A Cirq-compatible noise model for a one-zone implementation of the Gemini architecture.
|
|
143
|
+
|
|
144
|
+
This model introduces custom asymmetric depolarizing noise for both single- and two-qubit gates
|
|
145
|
+
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
|
|
147
|
+
qubits not actively involved in a gate operation.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def _single_qubit_moment_noise_ops(
|
|
151
|
+
self, moment: cirq.Moment, system_qubits: Sequence[cirq.Qid]
|
|
152
|
+
) -> tuple[list, list]:
|
|
153
|
+
"""
|
|
154
|
+
Helper function to determine the noise operations for a single qubit moment.
|
|
155
|
+
|
|
156
|
+
:param moment: The current cirq.Moment being evaluated.
|
|
157
|
+
:param system_qubits: All qubits in the circuit.
|
|
158
|
+
:return: A tuple containing gate noise operations and move noise operations for the given moment.
|
|
159
|
+
"""
|
|
160
|
+
# Check if the moment only contains single qubit gates
|
|
161
|
+
assert np.all([len(op.qubits) == 1 for op in moment.operations])
|
|
162
|
+
# Check if single qubit gate is global or local
|
|
163
|
+
gate_params = [
|
|
164
|
+
[op.gate.axis_phase_exponent, op.gate.x_exponent, op.gate.z_exponent]
|
|
165
|
+
for op in moment.operations
|
|
166
|
+
]
|
|
167
|
+
gate_params = np.array(gate_params)
|
|
168
|
+
|
|
169
|
+
test_params = [
|
|
170
|
+
[
|
|
171
|
+
moment.operations[0].gate.axis_phase_exponent,
|
|
172
|
+
moment.operations[0].gate.x_exponent,
|
|
173
|
+
moment.operations[0].gate.z_exponent,
|
|
174
|
+
]
|
|
175
|
+
for _ in moment.operations
|
|
176
|
+
]
|
|
177
|
+
test_params = np.array(test_params)
|
|
178
|
+
|
|
179
|
+
gated_qubits = [
|
|
180
|
+
op.qubits[0]
|
|
181
|
+
for op in moment.operations
|
|
182
|
+
if not (
|
|
183
|
+
np.isclose(op.gate.x_exponent, 0) and np.isclose(op.gate.z_exponent, 0)
|
|
184
|
+
)
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
is_global = np.all(np.isclose(gate_params, test_params)) and set(
|
|
188
|
+
gated_qubits
|
|
189
|
+
) == set(system_qubits)
|
|
190
|
+
|
|
191
|
+
if is_global:
|
|
192
|
+
p_x = self.global_px
|
|
193
|
+
p_y = self.global_py
|
|
194
|
+
p_z = self.global_pz
|
|
195
|
+
else:
|
|
196
|
+
p_x = self.local_px
|
|
197
|
+
p_y = self.local_py
|
|
198
|
+
p_z = self.local_pz
|
|
199
|
+
|
|
200
|
+
if p_x == p_y == p_z:
|
|
201
|
+
gate_noise_op = cirq.depolarize(p_x + p_y + p_z).on_each(gated_qubits)
|
|
202
|
+
else:
|
|
203
|
+
gate_noise_op = cirq.asymmetric_depolarize(
|
|
204
|
+
p_x=p_x, p_y=p_y, p_z=p_z
|
|
205
|
+
).on_each(gated_qubits)
|
|
206
|
+
|
|
207
|
+
return [gate_noise_op], []
|
|
208
|
+
|
|
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
|
+
def noisy_moment(self, moment, system_qubits):
|
|
336
|
+
# Moment with original ops
|
|
337
|
+
original_moment = moment
|
|
338
|
+
|
|
339
|
+
# Check if the moment is empty
|
|
340
|
+
if len(moment.operations) == 0:
|
|
341
|
+
move_noise_ops = []
|
|
342
|
+
gate_noise_ops = []
|
|
343
|
+
# Check if the moment contains 1-qubit gates or 2-qubit gates
|
|
344
|
+
elif len(moment.operations[0].qubits) == 1:
|
|
345
|
+
gate_noise_ops, move_noise_ops = self._single_qubit_moment_noise_ops(
|
|
346
|
+
moment, system_qubits
|
|
347
|
+
)
|
|
348
|
+
elif len(moment.operations[0].qubits) == 2:
|
|
349
|
+
control_qubits = [op.qubits[0] for op in moment.operations]
|
|
350
|
+
target_qubits = [op.qubits[1] for op in moment.operations]
|
|
351
|
+
gated_qubits = control_qubits + target_qubits
|
|
352
|
+
idle_atoms = list(set(system_qubits) - set(gated_qubits))
|
|
353
|
+
|
|
354
|
+
move_noise_ops = [
|
|
355
|
+
cirq.asymmetric_depolarize(*self.mover_pauli_rates).on_each(
|
|
356
|
+
control_qubits
|
|
357
|
+
),
|
|
358
|
+
cirq.asymmetric_depolarize(*self.sitter_pauli_rates).on_each(
|
|
359
|
+
target_qubits + idle_atoms
|
|
360
|
+
),
|
|
361
|
+
] # In this setting, we assume a 1 zone scheme where the controls move to the targets.
|
|
362
|
+
|
|
363
|
+
# Add correlated noise channels for entangling pairs
|
|
364
|
+
two_qubit_pauli = self.two_qubit_pauli
|
|
365
|
+
gate_noise_ops = [
|
|
366
|
+
two_qubit_pauli.on_each([c, t])
|
|
367
|
+
for c, t in zip(control_qubits, target_qubits)
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
# In this 1 zone scheme, all unpaired atoms are in the entangling zone.
|
|
371
|
+
idle_depolarize = cirq.asymmetric_depolarize(
|
|
372
|
+
*self.cz_unpaired_pauli_rates
|
|
373
|
+
).on_each(idle_atoms)
|
|
374
|
+
|
|
375
|
+
gate_noise_ops.append(idle_depolarize)
|
|
376
|
+
else:
|
|
377
|
+
raise ValueError(
|
|
378
|
+
"Moment contains operations with more than 2 qubits, which is not supported."
|
|
379
|
+
"Correlated measurements should be added after the noise model is applied."
|
|
380
|
+
)
|
|
381
|
+
if move_noise_ops == []:
|
|
382
|
+
move_noise_moments = []
|
|
383
|
+
else:
|
|
384
|
+
move_noise_moments = [cirq.Moment(move_noise_ops)]
|
|
385
|
+
gate_noise_moment = cirq.Moment(gate_noise_ops)
|
|
386
|
+
|
|
387
|
+
return [
|
|
388
|
+
*move_noise_moments,
|
|
389
|
+
original_moment,
|
|
390
|
+
gate_noise_moment,
|
|
391
|
+
*move_noise_moments,
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@dataclass(frozen=True)
|
|
396
|
+
class GeminiOneZoneNoiseModelConflictGraphMoves(GeminiOneZoneNoiseModel):
|
|
397
|
+
"""
|
|
398
|
+
A Cirq noise model that uses a conflict graph to schedule moves in a one-zone Gemini architecture.
|
|
399
|
+
|
|
400
|
+
Assumes that the qubits are cirq.GridQubits, such that the assignment of row, column coordinates define the initial
|
|
401
|
+
geometry. An SLM site at the two qubit interaction distance is also assumed next to each cirq.GridQubit to allow
|
|
402
|
+
for multiple moves before a single Rydberg pulse is applied for a parallel CZ.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
max_parallel_movers: int = 10000
|
|
406
|
+
|
|
407
|
+
def noisy_moment(self, moment, system_qubits):
|
|
408
|
+
# Moment with original ops
|
|
409
|
+
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."
|
|
413
|
+
# Check if the moment is empty
|
|
414
|
+
if len(moment.operations) == 0:
|
|
415
|
+
move_moments = []
|
|
416
|
+
gate_noise_ops = []
|
|
417
|
+
# Check if the moment contains 1-qubit gates or 2-qubit gates
|
|
418
|
+
elif len(moment.operations[0].qubits) == 1:
|
|
419
|
+
gate_noise_ops, _ = self._single_qubit_moment_noise_ops(
|
|
420
|
+
moment, system_qubits
|
|
421
|
+
)
|
|
422
|
+
move_moments = []
|
|
423
|
+
elif len(moment.operations[0].qubits) == 2:
|
|
424
|
+
cg = OneZoneConflictGraph(moment)
|
|
425
|
+
schedule = cg.get_move_schedule(mover_limit=self.max_parallel_movers)
|
|
426
|
+
move_moments = []
|
|
427
|
+
for move_moment_idx, movers in schedule.items():
|
|
428
|
+
control_qubits = list(movers)
|
|
429
|
+
target_qubits = list(
|
|
430
|
+
set(
|
|
431
|
+
[op.qubits[0] for op in moment.operations]
|
|
432
|
+
+ [op.qubits[1] for op in moment.operations]
|
|
433
|
+
)
|
|
434
|
+
- movers
|
|
435
|
+
)
|
|
436
|
+
gated_qubits = control_qubits + target_qubits
|
|
437
|
+
idle_atoms = list(set(system_qubits) - set(gated_qubits))
|
|
438
|
+
|
|
439
|
+
move_noise_ops = [
|
|
440
|
+
cirq.asymmetric_depolarize(*self.mover_pauli_rates).on_each(
|
|
441
|
+
control_qubits
|
|
442
|
+
),
|
|
443
|
+
cirq.asymmetric_depolarize(*self.sitter_pauli_rates).on_each(
|
|
444
|
+
target_qubits + idle_atoms
|
|
445
|
+
),
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
move_moments.append(cirq.Moment(move_noise_ops))
|
|
449
|
+
|
|
450
|
+
control_qubits = [op.qubits[0] for op in moment.operations]
|
|
451
|
+
target_qubits = [op.qubits[1] for op in moment.operations]
|
|
452
|
+
gated_qubits = control_qubits + target_qubits
|
|
453
|
+
idle_atoms = list(set(system_qubits) - set(gated_qubits))
|
|
454
|
+
|
|
455
|
+
gate_noise_ops = [
|
|
456
|
+
cirq.asymmetric_depolarize(*self.cz_paired_pauli_rates).on_each(
|
|
457
|
+
gated_qubits
|
|
458
|
+
),
|
|
459
|
+
cirq.asymmetric_depolarize(*self.cz_unpaired_pauli_rates).on_each(
|
|
460
|
+
idle_atoms
|
|
461
|
+
),
|
|
462
|
+
] # In this 1 zone scheme, all unpaired atoms are in the entangling zone.
|
|
463
|
+
else:
|
|
464
|
+
raise ValueError(
|
|
465
|
+
"Moment contains operations with more than 2 qubits, which is not supported."
|
|
466
|
+
"Correlated measurements should be added after the noise model is applied."
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
gate_noise_moment = cirq.Moment(gate_noise_ops)
|
|
470
|
+
|
|
471
|
+
return [
|
|
472
|
+
*move_moments,
|
|
473
|
+
original_moment,
|
|
474
|
+
gate_noise_moment,
|
|
475
|
+
*(move_moments[::-1]),
|
|
476
|
+
]
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@dataclass(frozen=True)
|
|
480
|
+
class GeminiTwoZoneNoiseModel(GeminiNoiseModelABC):
|
|
481
|
+
def noisy_moments(
|
|
482
|
+
self, moments: Iterable[cirq.Moment], system_qubits: Sequence[cirq.Qid]
|
|
483
|
+
) -> Sequence[cirq.OP_TREE]:
|
|
484
|
+
"""Adds possibly stateful noise to a series of moments.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
moments: The moments to add noise to.
|
|
488
|
+
system_qubits: A list of all qubits in the system.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
A sequence of OP_TREEEs, with the k'th tree corresponding to the
|
|
492
|
+
noisy operations for the k'th moment.
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
if self.check_input_circuit:
|
|
496
|
+
self.validate_moments(moments)
|
|
497
|
+
|
|
498
|
+
moments = list(moments)
|
|
499
|
+
|
|
500
|
+
if len(moments) == 0:
|
|
501
|
+
return []
|
|
502
|
+
|
|
503
|
+
nqubs = len(system_qubits)
|
|
504
|
+
noisy_moment_list = []
|
|
505
|
+
|
|
506
|
+
prev_moment: cirq.Moment | None = None
|
|
507
|
+
|
|
508
|
+
# TODO: clean up error getters so they return a list moments rather than circuits
|
|
509
|
+
for i in range(len(moments)):
|
|
510
|
+
noisy_moment_list.extend(
|
|
511
|
+
[
|
|
512
|
+
moment
|
|
513
|
+
for moment in _two_zone_utils.get_move_error_channel_two_zoned(
|
|
514
|
+
moments[i],
|
|
515
|
+
prev_moment,
|
|
516
|
+
np.array(self.mover_pauli_rates),
|
|
517
|
+
np.array(self.sitter_pauli_rates),
|
|
518
|
+
nqubs,
|
|
519
|
+
).moments
|
|
520
|
+
if len(moment) > 0
|
|
521
|
+
]
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
noisy_moment_list.append(moments[i])
|
|
525
|
+
|
|
526
|
+
noisy_moment_list.extend(
|
|
527
|
+
[
|
|
528
|
+
moment
|
|
529
|
+
for moment in _two_zone_utils.get_gate_error_channel(
|
|
530
|
+
moments[i],
|
|
531
|
+
np.array(self.local_pauli_rates),
|
|
532
|
+
np.array(self.global_pauli_rates),
|
|
533
|
+
np.array(
|
|
534
|
+
self.cz_paired_pauli_rates + self.cz_paired_pauli_rates
|
|
535
|
+
),
|
|
536
|
+
np.array(self.cz_unpaired_pauli_rates),
|
|
537
|
+
).moments
|
|
538
|
+
if len(moment) > 0
|
|
539
|
+
]
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
prev_moment = moments[i]
|
|
543
|
+
|
|
544
|
+
return noisy_moment_list
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import cirq
|
|
2
|
+
|
|
3
|
+
from .model import GeminiOneZoneNoiseModel, GeminiOneZoneNoiseModelABC
|
|
4
|
+
from ..parallelize import transpile, parallelize
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def transform_circuit(
|
|
8
|
+
circuit: cirq.Circuit,
|
|
9
|
+
to_native_gateset: bool = True,
|
|
10
|
+
model: cirq.NoiseModel | None = None,
|
|
11
|
+
parallelize_circuit: bool = False,
|
|
12
|
+
) -> cirq.Circuit:
|
|
13
|
+
"""Transform an input circuit into one with the native gateset with noise operations added.
|
|
14
|
+
|
|
15
|
+
Noise operations will be added to all qubits in circuit.all_qubits(), regardless of whether the output of the
|
|
16
|
+
circuit optimizers contain all the qubits.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
circuit (cirq.Circuit): The input circuit.
|
|
20
|
+
|
|
21
|
+
Keyword Arguments:
|
|
22
|
+
to_native_gateset (bool): Whether or not to convert the input circuit to one using the native set of gates (`cirq.CZTargetGateset`)
|
|
23
|
+
only. Defaults to `True`. Note, that if you use an input circuit that has gates different from this gateset and don't convert it,
|
|
24
|
+
may lead to incorrect results and errors.
|
|
25
|
+
model (cirq.NoiseModel): The cirq noise model to apply to the circuit. Usually, you want to use one of the ones supplied in this submodule,
|
|
26
|
+
such as `GeminiOneZoneNoiseModel`.
|
|
27
|
+
parallelize_circuit (bool): Whether or not to parallelize the circuit as much as possible after it's been converted to the native gateset.
|
|
28
|
+
Defaults to `False`.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
cirq.Circuit:
|
|
32
|
+
The resulting noisy circuit.
|
|
33
|
+
"""
|
|
34
|
+
if model is None:
|
|
35
|
+
model = GeminiOneZoneNoiseModel(parallelize_circuit=parallelize_circuit)
|
|
36
|
+
|
|
37
|
+
# only parallelize here if we aren't parallelizing inside a one-zone model
|
|
38
|
+
parallelize_circuit_here = parallelize_circuit and not isinstance(
|
|
39
|
+
model, GeminiOneZoneNoiseModelABC
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
system_qubits = sorted(circuit.all_qubits())
|
|
43
|
+
# Transform to CZ + PhasedXZ gateset.
|
|
44
|
+
if to_native_gateset and not parallelize_circuit_here:
|
|
45
|
+
native_circuit = transpile(circuit)
|
|
46
|
+
elif parallelize_circuit_here:
|
|
47
|
+
native_circuit = parallelize(circuit)
|
|
48
|
+
else:
|
|
49
|
+
native_circuit = circuit
|
|
50
|
+
|
|
51
|
+
# Add noise
|
|
52
|
+
noisy_circuit = cirq.Circuit()
|
|
53
|
+
for op_tree in model.noisy_moments(native_circuit, system_qubits):
|
|
54
|
+
# Keep moments aligned
|
|
55
|
+
noisy_circuit += cirq.Circuit(op_tree)
|
|
56
|
+
|
|
57
|
+
return noisy_circuit
|
|
@@ -39,6 +39,8 @@ def transpile(circuit: cirq.Circuit) -> cirq.Circuit:
|
|
|
39
39
|
"""
|
|
40
40
|
# Convert to CZ target gate set.
|
|
41
41
|
circuit2 = cirq.optimize_for_target_gateset(circuit, gateset=cirq.CZTargetGateset())
|
|
42
|
+
circuit2 = cirq.drop_empty_moments(circuit2)
|
|
43
|
+
|
|
42
44
|
missing_qubits = circuit.all_qubits() - circuit2.all_qubits()
|
|
43
45
|
|
|
44
46
|
for qubit in missing_qubits:
|
|
@@ -374,9 +376,9 @@ def parallelize(
|
|
|
374
376
|
"""
|
|
375
377
|
hyperparameters = _get_hyperparameters(hyperparameters)
|
|
376
378
|
|
|
379
|
+
# Transpile the circuit to a native CZ gate set.
|
|
380
|
+
transpiled_circuit = transpile(circuit)
|
|
377
381
|
if auto_tag:
|
|
378
|
-
# Transpile the circuit to a native CZ gate set.
|
|
379
|
-
transpiled_circuit = transpile(circuit)
|
|
380
382
|
# Annotate the circuit with topological information
|
|
381
383
|
# to improve parallelization
|
|
382
384
|
transpiled_circuit, group_weights = auto_similarity(
|
bloqade/qasm2/_qasm_loading.py
CHANGED
|
@@ -82,7 +82,7 @@ def loads(
|
|
|
82
82
|
body=body,
|
|
83
83
|
)
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
mt = ir.Method(
|
|
86
86
|
mod=None,
|
|
87
87
|
py_func=None,
|
|
88
88
|
sym_name=kernel_name,
|
|
@@ -91,6 +91,9 @@ def loads(
|
|
|
91
91
|
code=code,
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
+
mt.verify()
|
|
95
|
+
return mt
|
|
96
|
+
|
|
94
97
|
|
|
95
98
|
def loadfile(
|
|
96
99
|
qasm_file: str | pathlib.Path,
|
bloqade/qasm2/parse/lowering.py
CHANGED
|
@@ -202,7 +202,13 @@ class QASM2(lowering.LoweringABC[ast.Node]):
|
|
|
202
202
|
|
|
203
203
|
then_body = if_frame.curr_region
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
# NOTE: create empty else body
|
|
206
|
+
else_body = ir.Block(stmts=[scf.Yield()])
|
|
207
|
+
else_body.args.append_from(types.Bool)
|
|
208
|
+
|
|
209
|
+
state.current_frame.push(
|
|
210
|
+
scf.IfElse(cond, then_body=then_body, else_body=else_body)
|
|
211
|
+
)
|
|
206
212
|
|
|
207
213
|
def branch_next_if_not_terminated(self, frame: lowering.Frame):
|
|
208
214
|
"""Branch to the next block if the current block is not terminated.
|
|
@@ -381,6 +387,8 @@ class QASM2(lowering.LoweringABC[ast.Node]):
|
|
|
381
387
|
QubitType for _ in node.qparams
|
|
382
388
|
]
|
|
383
389
|
|
|
390
|
+
self_name = node.name + "_self"
|
|
391
|
+
|
|
384
392
|
with state.frame(
|
|
385
393
|
stmts=node.body,
|
|
386
394
|
finalize_next=False,
|
|
@@ -390,7 +398,7 @@ class QASM2(lowering.LoweringABC[ast.Node]):
|
|
|
390
398
|
types.Generic(
|
|
391
399
|
ir.Method, types.Tuple.where(tuple(arg_types)), types.NoneType
|
|
392
400
|
),
|
|
393
|
-
name=
|
|
401
|
+
name=self_name,
|
|
394
402
|
)
|
|
395
403
|
|
|
396
404
|
for arg_type, arg_name in zip(arg_types, arg_names):
|
|
@@ -422,7 +430,7 @@ class QASM2(lowering.LoweringABC[ast.Node]):
|
|
|
422
430
|
py_func=None,
|
|
423
431
|
sym_name=node.name,
|
|
424
432
|
dialects=self.dialects,
|
|
425
|
-
arg_names=[*node.cparams, *node.qparams],
|
|
433
|
+
arg_names=[self_name, *node.cparams, *node.qparams],
|
|
426
434
|
code=gate_func,
|
|
427
435
|
)
|
|
428
436
|
state.current_frame.globals[node.name] = mt
|