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.
@@ -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(
@@ -82,7 +82,7 @@ def loads(
82
82
  body=body,
83
83
  )
84
84
 
85
- return ir.Method(
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,
@@ -202,7 +202,13 @@ class QASM2(lowering.LoweringABC[ast.Node]):
202
202
 
203
203
  then_body = if_frame.curr_region
204
204
 
205
- state.current_frame.push(scf.IfElse(cond, then_body=then_body))
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=node.name + "_self",
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