bloqade-circuit 0.2.3__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bloqade-circuit might be problematic. Click here for more details.

Files changed (101) hide show
  1. bloqade/analysis/address/impls.py +3 -2
  2. bloqade/pyqrack/device.py +1 -3
  3. bloqade/pyqrack/noise/native.py +8 -8
  4. bloqade/pyqrack/qasm2/core.py +4 -1
  5. bloqade/pyqrack/squin/op.py +7 -0
  6. bloqade/pyqrack/squin/qubit.py +5 -27
  7. bloqade/pyqrack/squin/runtime.py +18 -0
  8. bloqade/pyqrack/squin/wire.py +4 -22
  9. bloqade/pyqrack/task.py +13 -5
  10. bloqade/qasm2/__init__.py +1 -0
  11. bloqade/qasm2/_qasm_loading.py +151 -0
  12. bloqade/qasm2/dialects/core/__init__.py +9 -1
  13. bloqade/qasm2/dialects/expr/__init__.py +18 -1
  14. bloqade/{noise/native → qasm2/dialects/noise}/__init__.py +1 -7
  15. bloqade/qasm2/dialects/noise/_dialect.py +3 -0
  16. bloqade/{noise → qasm2/dialects/noise}/fidelity.py +4 -4
  17. bloqade/qasm2/dialects/noise/model.py +278 -0
  18. bloqade/{noise/native → qasm2/dialects/noise}/stmts.py +1 -1
  19. bloqade/qasm2/dialects/uop/__init__.py +39 -3
  20. bloqade/qasm2/dialects/uop/schedule.py +1 -1
  21. bloqade/qasm2/emit/impls/__init__.py +1 -0
  22. bloqade/qasm2/emit/impls/noise.py +89 -0
  23. bloqade/qasm2/emit/main.py +23 -4
  24. bloqade/qasm2/emit/target.py +19 -4
  25. bloqade/qasm2/noise.py +67 -0
  26. bloqade/qasm2/parse/__init__.py +7 -4
  27. bloqade/qasm2/parse/lowering.py +20 -130
  28. bloqade/qasm2/parse/qasm2.lark +1 -1
  29. bloqade/qasm2/passes/__init__.py +1 -0
  30. bloqade/qasm2/passes/fold.py +6 -0
  31. bloqade/qasm2/passes/glob.py +12 -8
  32. bloqade/qasm2/passes/noise.py +27 -16
  33. bloqade/qasm2/passes/parallel.py +9 -0
  34. bloqade/qasm2/passes/unroll_if.py +25 -0
  35. bloqade/qasm2/rewrite/__init__.py +3 -0
  36. bloqade/qasm2/rewrite/desugar.py +3 -2
  37. bloqade/qasm2/rewrite/native_gates.py +67 -4
  38. bloqade/qasm2/rewrite/noise/__init__.py +0 -0
  39. bloqade/qasm2/rewrite/{heuristic_noise.py → noise/heuristic_noise.py} +32 -62
  40. bloqade/{noise/native/rewrite.py → qasm2/rewrite/noise/remove_noise.py} +2 -2
  41. bloqade/qasm2/rewrite/split_ifs.py +66 -0
  42. bloqade/qbraid/lowering.py +8 -8
  43. bloqade/squin/__init__.py +7 -1
  44. bloqade/squin/analysis/nsites/__init__.py +1 -0
  45. bloqade/squin/analysis/nsites/impls.py +16 -1
  46. bloqade/squin/groups.py +4 -4
  47. bloqade/squin/lowering.py +27 -0
  48. bloqade/squin/noise/__init__.py +7 -26
  49. bloqade/squin/noise/_wrapper.py +25 -0
  50. bloqade/squin/op/__init__.py +34 -159
  51. bloqade/squin/op/_wrapper.py +105 -0
  52. bloqade/squin/op/stdlib.py +62 -0
  53. bloqade/squin/op/stmts.py +10 -0
  54. bloqade/squin/passes/__init__.py +1 -0
  55. bloqade/squin/passes/stim.py +68 -0
  56. bloqade/squin/qubit.py +32 -37
  57. bloqade/squin/rewrite/__init__.py +11 -0
  58. bloqade/squin/rewrite/desugar.py +65 -0
  59. bloqade/squin/rewrite/qubit_to_stim.py +61 -0
  60. bloqade/squin/rewrite/squin_measure.py +73 -0
  61. bloqade/squin/rewrite/stim_rewrite_util.py +153 -0
  62. bloqade/squin/rewrite/wire_identity_elimination.py +24 -0
  63. bloqade/squin/rewrite/wire_to_stim.py +52 -0
  64. bloqade/squin/rewrite/wrap_analysis.py +72 -0
  65. bloqade/squin/wire.py +5 -22
  66. bloqade/stim/__init__.py +40 -5
  67. bloqade/stim/_wrappers.py +18 -12
  68. bloqade/stim/dialects/__init__.py +1 -5
  69. bloqade/stim/dialects/{aux → auxiliary}/__init__.py +13 -1
  70. bloqade/stim/dialects/{aux → auxiliary}/emit.py +18 -3
  71. bloqade/stim/dialects/{aux → auxiliary}/stmts/__init__.py +1 -0
  72. bloqade/stim/dialects/{aux → auxiliary}/stmts/annotate.py +8 -0
  73. bloqade/stim/dialects/collapse/__init__.py +13 -2
  74. bloqade/stim/dialects/collapse/{emit.py → emit_str.py} +4 -2
  75. bloqade/stim/dialects/collapse/stmts/pp_measure.py +1 -1
  76. bloqade/stim/dialects/gate/__init__.py +16 -1
  77. bloqade/stim/dialects/gate/emit.py +10 -3
  78. bloqade/stim/dialects/gate/stmts/base.py +1 -1
  79. bloqade/stim/dialects/gate/stmts/pp.py +1 -1
  80. bloqade/stim/dialects/noise/emit.py +33 -2
  81. bloqade/stim/dialects/noise/stmts.py +29 -0
  82. bloqade/stim/emit/__init__.py +1 -1
  83. bloqade/stim/groups.py +4 -2
  84. bloqade/stim/parse/__init__.py +1 -0
  85. bloqade/stim/parse/lowering.py +686 -0
  86. {bloqade_circuit-0.2.3.dist-info → bloqade_circuit-0.4.0.dist-info}/METADATA +5 -3
  87. {bloqade_circuit-0.2.3.dist-info → bloqade_circuit-0.4.0.dist-info}/RECORD +95 -77
  88. bloqade/noise/__init__.py +0 -2
  89. bloqade/noise/native/_dialect.py +0 -3
  90. bloqade/noise/native/_wrappers.py +0 -34
  91. bloqade/noise/native/model.py +0 -346
  92. bloqade/qasm2/dialects/noise.py +0 -16
  93. bloqade/squin/rewrite/measure_desugar.py +0 -33
  94. /bloqade/stim/dialects/{aux → auxiliary}/_dialect.py +0 -0
  95. /bloqade/stim/dialects/{aux → auxiliary}/interp.py +0 -0
  96. /bloqade/stim/dialects/{aux → auxiliary}/lowering.py +0 -0
  97. /bloqade/stim/dialects/{aux → auxiliary}/stmts/const.py +0 -0
  98. /bloqade/stim/dialects/{aux → auxiliary}/types.py +0 -0
  99. /bloqade/stim/emit/{stim.py → stim_str.py} +0 -0
  100. {bloqade_circuit-0.2.3.dist-info → bloqade_circuit-0.4.0.dist-info}/WHEEL +0 -0
  101. {bloqade_circuit-0.2.3.dist-info → bloqade_circuit-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,278 @@
1
+ import abc
2
+ from dataclasses import field, dataclass
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class MoveNoiseModelABC(abc.ABC):
7
+ """Abstract base class for noise based on atom movement.
8
+
9
+ This class defines the interface for a noise model. The gate noise is calculated from the parameters
10
+ provided in this dataclass which can be updated when inheriting from this class. The move error is
11
+ calculated by implementing the parallel_cz_errors method which takes a set of ctrl and qarg qubits
12
+ and returns a noise model for all the qubits. The noise model is a dictionary with the keys being the
13
+ error rates for the qubits and the values being the list of qubits that the error rate applies to.
14
+
15
+ Once implemented the class can be used with the NoisePass to analyze a circuit and apply the noise
16
+ model to the circuit.
17
+
18
+ NOTE: This model is not guaranteed to be supported long-term in bloqade. We will be
19
+ moving towards a more general approach to noise modeling in the future.
20
+
21
+ """
22
+
23
+ # parameters for gate noise
24
+
25
+ local_px: float = field(default=4.102e-04, kw_only=True)
26
+ """The error probability for a Pauli-X error during a local single qubit gate operation."""
27
+ local_py: float = field(default=4.102e-04, kw_only=True)
28
+ """The error probability for a Pauli-Y error during a local single qubit gate operation."""
29
+ local_pz: float = field(default=4.112e-04, kw_only=True)
30
+ """The error probability for a Pauli-Z error during a local single qubit gate operation."""
31
+ local_loss_prob: float = field(default=0.0, kw_only=True)
32
+ """The error probability for a loss during a local single qubit gate operation."""
33
+
34
+ local_unaddressed_px: float = field(default=2.000e-07, kw_only=True)
35
+ """The error probability for a Pauli-X error during a local single qubit gate operation when the qubit is not addressed."""
36
+ local_unaddressed_py: float = field(default=2.000e-07, kw_only=True)
37
+ """The error probability for a Pauli-Y error during a local single qubit gate operation when the qubit is not addressed."""
38
+ local_unaddressed_pz: float = field(default=1.200e-06, kw_only=True)
39
+ """The error probability for a Pauli-Z error during a local single qubit gate operation when the qubit is not addressed."""
40
+ local_unaddressed_loss_prob: float = field(default=0.0, kw_only=True)
41
+ """The error probability for a loss during a local single qubit gate operation when the qubit is not addressed."""
42
+
43
+ global_px: float = field(default=6.500e-05, kw_only=True)
44
+ """The error probability for a Pauli-X error during a global single qubit gate operation."""
45
+ global_py: float = field(default=6.500e-05, kw_only=True)
46
+ """The error probability for a Pauli-Y error during a global single qubit gate operation."""
47
+ global_pz: float = field(default=6.500e-05, kw_only=True)
48
+ """The error probability for a Pauli-Z error during a global single qubit gate operation."""
49
+ global_loss_prob: float = field(default=0.0, kw_only=True)
50
+ """The error probability for a loss during a global single qubit gate operation."""
51
+
52
+ cz_paired_gate_px: float = field(default=6.549e-04, kw_only=True)
53
+ """The error probability for a Pauli-X error during CZ gate operation when two qubits are within blockade radius."""
54
+ cz_paired_gate_py: float = field(default=6.549e-04, kw_only=True)
55
+ """The error probability for a Pauli-Y error during CZ gate operation when two qubits are within blockade radius."""
56
+ cz_paired_gate_pz: float = field(default=3.184e-03, kw_only=True)
57
+ """The error probability for a Pauli-Z error during CZ gate operation when two qubits are within blockade radius."""
58
+ cz_gate_loss_prob: float = field(default=0.0, kw_only=True)
59
+ """The error probability for a loss during CZ gate operation when two qubits are within blockade radius."""
60
+
61
+ cz_unpaired_gate_px: float = field(default=5.149e-04, kw_only=True)
62
+ """The error probability for Pauli-X error during CZ gate operation when another qubit is not within blockade radius."""
63
+ cz_unpaired_gate_py: float = field(default=5.149e-04, kw_only=True)
64
+ """The error probability for Pauli-Y error during CZ gate operation when another qubit is not within blockade radius."""
65
+ cz_unpaired_gate_pz: float = field(default=2.185e-03, kw_only=True)
66
+ """The error probability for Pauli-Z error during CZ gate operation when another qubit is not within blockade radius."""
67
+ cz_unpaired_loss_prob: float = field(default=0.0, kw_only=True)
68
+ """The error probability for a loss during CZ gate operation when another qubit is not within blockade radius."""
69
+
70
+ # parameters for move noise
71
+
72
+ mover_px: float = field(default=8.060e-04, kw_only=True)
73
+ """Probability of X error occurring on a moving qubit during a move operation"""
74
+ mover_py: float = field(default=8.060e-04, kw_only=True)
75
+ """Probability of Y error occurring on a moving qubit during a move operation"""
76
+ mover_pz: float = field(default=2.458e-03, kw_only=True)
77
+ """Probability of Z error occurring on a moving qubit during a move operation"""
78
+ move_loss_prob: float = field(default=0.0, kw_only=True)
79
+ """Probability of loss occurring on a moving qubit during a move operation"""
80
+
81
+ sitter_px: float = field(default=3.066e-04, kw_only=True)
82
+ """Probability of X error occurring on a stationary qubit during a move operation"""
83
+ sitter_py: float = field(default=3.066e-04, kw_only=True)
84
+ """Probability of Y error occurring on a stationary qubit during a move operation"""
85
+ sitter_pz: float = field(default=4.639e-04, kw_only=True)
86
+ """Probability of Z error occurring on a stationary qubit during a move operation"""
87
+ sit_loss_prob: float = field(default=0.0, kw_only=True)
88
+ """Probability of loss occurring on a stationary qubit during a move operation"""
89
+
90
+ @property
91
+ def cz_paired_errors(
92
+ self,
93
+ ) -> tuple[float, float, float, float]:
94
+ """Returns the error rates for a CZ gate."""
95
+ return (
96
+ self.cz_paired_gate_px,
97
+ self.cz_paired_gate_py,
98
+ self.cz_paired_gate_pz,
99
+ self.cz_gate_loss_prob,
100
+ )
101
+
102
+ @property
103
+ def cz_unpaired_errors(
104
+ self,
105
+ ) -> tuple[float, float, float, float]:
106
+ """Returns the error rates for a CZ gate."""
107
+ return (
108
+ self.cz_unpaired_gate_px,
109
+ self.cz_unpaired_gate_py,
110
+ self.cz_unpaired_gate_pz,
111
+ self.cz_unpaired_loss_prob,
112
+ )
113
+
114
+ @property
115
+ def local_errors(
116
+ self,
117
+ ) -> tuple[float, float, float, float]:
118
+ """Returns the error rates for a local single qubit gate."""
119
+ return (
120
+ self.local_px,
121
+ self.local_py,
122
+ self.local_pz,
123
+ self.local_loss_prob,
124
+ )
125
+
126
+ @property
127
+ def local_unaddressed_errors(
128
+ self,
129
+ ) -> tuple[float, float, float, float]:
130
+ """Returns the error rates for a local single qubit gate."""
131
+ return (
132
+ self.local_unaddressed_px,
133
+ self.local_unaddressed_py,
134
+ self.local_unaddressed_pz,
135
+ self.local_unaddressed_loss_prob,
136
+ )
137
+
138
+ @property
139
+ def global_errors(
140
+ self,
141
+ ) -> tuple[float, float, float, float]:
142
+ """Returns the error rates for a global single qubit gate."""
143
+ return (
144
+ self.global_px,
145
+ self.global_py,
146
+ self.global_pz,
147
+ self.global_loss_prob,
148
+ )
149
+
150
+ @property
151
+ def sitter_errors(
152
+ self,
153
+ ) -> tuple[float, float, float, float]:
154
+ """Returns the error rates for a move operation."""
155
+ return (
156
+ self.sitter_px,
157
+ self.sitter_py,
158
+ self.sitter_pz,
159
+ self.sit_loss_prob,
160
+ )
161
+
162
+ @abc.abstractmethod
163
+ def parallel_cz_errors(
164
+ self, ctrls: list[int], qargs: list[int], rest: list[int]
165
+ ) -> dict[tuple[float, float, float, float], list[int]]:
166
+ """Takes a set of ctrls and qargs and returns a noise model for all qubits."""
167
+ pass
168
+
169
+ @classmethod
170
+ def join_binary_probs(cls, p1: float, *args: float) -> float:
171
+ """Merge the probabilities of an event happening if the event can only happen once.
172
+
173
+ For example, finding the effective probability of losing an atom from multiple sources, since
174
+ a qubit can only be lost once. This is done by using the formula:
175
+
176
+ p = p1 * (1 - p2) + p2 * (1 - p1)
177
+
178
+ applied recursively to all the probabilities in the list.
179
+
180
+ Args:
181
+ p1 (float): The probability of the event happening.
182
+ arg (float): The probabilities of the event happening from other sources.
183
+
184
+ Returns:
185
+ float: The effective probability of the event happening.
186
+
187
+ """
188
+ if len(args) == 0:
189
+ return p1
190
+ else:
191
+ p2 = cls.join_binary_probs(*args)
192
+ return p1 * (1 - p2) + p2 * (1 - p1)
193
+
194
+
195
+ @dataclass(frozen=True)
196
+ class TwoRowZoneModel(MoveNoiseModelABC):
197
+ """This model assumes that the qubits are arranged in a single storage row with a row corresponding to a gate zone below it.
198
+
199
+ The CZ gate noise is calculated using the following heuristic: The idle error is calculated by the total duration required
200
+ to do the move and entangle the qubits. Not every pair can be entangled at the same time, so we first deconflict the qargs
201
+ by finding subsets in which both the ctrl and the qarg qubits are in ascending order. This breaks the pairs into
202
+ groups that can be moved and entangled separately. We then take each group and assign each pair to a gate zone slot. The
203
+ slots are allocated by starting from the middle of the atoms and moving outwards making sure to keep the ctrl qubits in
204
+ ascending order. The time to move a group is calculated by finding the maximum travel distance of the qarg and ctrl qubits
205
+ and dviding by the move speed. The total move time is the sum of all the group move times. The error rate for all the qubits
206
+ is then calculated by using the poisson_pauli_prob function. An additional error for the pick operation is calculated by
207
+ joining the binary probabilities of the pick operation and the move operation.
208
+
209
+ """
210
+
211
+ @property
212
+ def move_errors(
213
+ self,
214
+ ) -> tuple[float, float, float, float]:
215
+ """Returns the error rates for a move operation."""
216
+ return (
217
+ self.mover_px / 2,
218
+ self.mover_py / 2,
219
+ self.mover_pz / 2,
220
+ self.move_loss_prob / 2,
221
+ )
222
+
223
+ def deconflict(
224
+ self, ctrls: list[int], qargs: list[int]
225
+ ) -> list[tuple[tuple[int, ...], tuple[int, ...]]]:
226
+ """Return a list of groups of ctrl and qarg qubits that can be moved and entangled separately."""
227
+ # sort by ctrl qubit first to guarantee that they will be in ascending order
228
+ sorted_pairs = sorted(zip(ctrls, qargs))
229
+
230
+ groups: list[list[tuple[int, int]]] = []
231
+ # group by qarg only putting it in a group if the qarg is greater than the last qarg in the group
232
+ # thus ensuring that the qargs are in ascending order
233
+ while len(sorted_pairs) > 0:
234
+ ctrl, qarg = sorted_pairs.pop(0)
235
+
236
+ found = False
237
+ for group in groups:
238
+ if group[-1][1] < qarg:
239
+ group.append((ctrl, qarg))
240
+ found = True
241
+ break
242
+ if not found:
243
+ groups.append([(ctrl, qarg)])
244
+
245
+ new_groups: list[tuple[tuple[int, ...], tuple[int, ...]]] = []
246
+
247
+ for group in groups:
248
+ ctrl, qarg = zip(*group)
249
+ ctrl = tuple(ctrl)
250
+ qarg = tuple(qarg)
251
+ new_groups.append((ctrl, qarg))
252
+
253
+ return new_groups
254
+
255
+ def parallel_cz_errors(
256
+ self, ctrls: list[int], qargs: list[int], rest: list[int]
257
+ ) -> dict[tuple[float, float, float, float], list[int]]:
258
+ """Apply parallel gates by moving ctrl qubits to qarg qubits."""
259
+ groups = self.deconflict(ctrls, qargs)
260
+ movers = ctrls + qargs
261
+ num_moves = len(groups)
262
+ # ignore order O(p^2) errors since they are small
263
+ effective_move_errors = (
264
+ self.move_errors[0] + self.sitter_errors[0] * (num_moves - 1),
265
+ self.move_errors[1] + self.sitter_errors[1] * (num_moves - 1),
266
+ self.move_errors[2] + self.sitter_errors[2] * (num_moves - 1),
267
+ self.move_errors[3] + self.sitter_errors[3] * (num_moves - 1),
268
+ )
269
+ effective_sitter_errors = (
270
+ self.sitter_errors[0] * num_moves,
271
+ self.sitter_errors[1] * num_moves,
272
+ self.sitter_errors[2] * num_moves,
273
+ self.sitter_errors[3] * num_moves,
274
+ )
275
+ result = {effective_move_errors: list(movers)}
276
+ result.setdefault(effective_sitter_errors, []).extend(rest)
277
+
278
+ return result
@@ -4,7 +4,7 @@ from kirin import ir, types, lowering
4
4
  from kirin.decl import info, statement
5
5
  from kirin.dialects import ilist
6
6
 
7
- from bloqade.qasm2.types import QubitType
7
+ from bloqade.types import QubitType
8
8
 
9
9
  from ._dialect import dialect
10
10
 
@@ -1,4 +1,40 @@
1
- from . import _emit as _emit, stmts as stmts
2
- from .stmts import * # noqa: F403
1
+ from . import _emit as _emit, stmts as stmts, schedule as schedule
2
+ from .stmts import (
3
+ CH as CH,
4
+ CU as CU,
5
+ CX as CX,
6
+ CY as CY,
7
+ CZ as CZ,
8
+ RX as RX,
9
+ RY as RY,
10
+ RZ as RZ,
11
+ SX as SX,
12
+ U1 as U1,
13
+ U2 as U2,
14
+ CCX as CCX,
15
+ CRX as CRX,
16
+ CRY as CRY,
17
+ CRZ as CRZ,
18
+ CSX as CSX,
19
+ CU1 as CU1,
20
+ CU3 as CU3,
21
+ RXX as RXX,
22
+ RZZ as RZZ,
23
+ H as H,
24
+ S as S,
25
+ T as T,
26
+ X as X,
27
+ Y as Y,
28
+ Z as Z,
29
+ Id as Id,
30
+ Sdag as Sdag,
31
+ Swap as Swap,
32
+ Tdag as Tdag,
33
+ CSwap as CSwap,
34
+ SXdag as SXdag,
35
+ UGate as UGate,
36
+ Barrier as Barrier,
37
+ SingleQubitGate as SingleQubitGate,
38
+ TwoQubitCtrlGate as TwoQubitCtrlGate,
39
+ )
3
40
  from ._dialect import dialect as dialect
4
- from .schedule import * # noqa: F403
@@ -8,7 +8,7 @@ from ._dialect import dialect
8
8
 
9
9
 
10
10
  @dialect.register(key="qasm2.schedule.dag")
11
- class UOp(interp.MethodTable):
11
+ class UOpSchedule(interp.MethodTable):
12
12
 
13
13
  @interp.impl(stmts.Id)
14
14
  @interp.impl(stmts.SXdag)
@@ -0,0 +1 @@
1
+ from .noise import NativeNoise as NativeNoise
@@ -0,0 +1,89 @@
1
+ from typing import Any
2
+
3
+ from kirin import interp
4
+ from kirin.dialects import ilist
5
+
6
+ from bloqade.qasm2.parse import ast
7
+ from bloqade.qasm2.dialects import noise
8
+ from bloqade.qasm2.emit.gate import EmitQASM2Gate, EmitQASM2Frame
9
+
10
+
11
+ @noise.dialect.register(key="emit.qasm2.gate")
12
+ class NativeNoise(interp.MethodTable):
13
+
14
+ def _convert(self, node: ast.Bit | ast.Name) -> str:
15
+ if isinstance(node, ast.Bit):
16
+ return f"{node.name.id}[{node.addr}]"
17
+ else:
18
+ return f"{node.id}"
19
+
20
+ @interp.impl(noise.CZPauliChannel)
21
+ def emit_czp(
22
+ self,
23
+ emit: EmitQASM2Gate,
24
+ frame: EmitQASM2Frame,
25
+ stmt: noise.CZPauliChannel,
26
+ ):
27
+ paired: bool = stmt.paired
28
+ px_ctrl: float = stmt.px_ctrl
29
+ py_ctrl: float = stmt.py_ctrl
30
+ pz_ctrl: float = stmt.pz_ctrl
31
+ px_qarg: float = stmt.pz_qarg
32
+ py_qarg: float = stmt.py_qarg
33
+ pz_qarg: float = stmt.pz_qarg
34
+ ctrls: ilist.IList[ast.Bit, Any] = frame.get(stmt.ctrls)
35
+ qargs: ilist.IList[ast.Bit, Any] = frame.get(stmt.qargs)
36
+ frame.body.append(
37
+ ast.Comment(
38
+ text=f"noise.CZPauliChannel(paired={paired}, p_ctrl=[x:{px_ctrl}, y:{py_ctrl}, z:{pz_ctrl}], p_qarg[x:{px_qarg}, y:{py_qarg}, z:{pz_qarg}])"
39
+ )
40
+ )
41
+ frame.body.append(
42
+ ast.Comment(
43
+ text=f" -: ctrls: {', '.join([self._convert(q) for q in ctrls])}"
44
+ )
45
+ )
46
+ frame.body.append(
47
+ ast.Comment(
48
+ text=f" -: qargs: {', '.join([self._convert(q) for q in qargs])}"
49
+ )
50
+ )
51
+ return ()
52
+
53
+ @interp.impl(noise.AtomLossChannel)
54
+ def emit_loss(
55
+ self,
56
+ emit: EmitQASM2Gate,
57
+ frame: EmitQASM2Frame,
58
+ stmt: noise.AtomLossChannel,
59
+ ):
60
+ prob: float = stmt.prob
61
+ qargs: ilist.IList[ast.Bit, Any] = frame.get(stmt.qargs)
62
+ frame.body.append(ast.Comment(text=f"noise.Atomloss(p={prob})"))
63
+ frame.body.append(
64
+ ast.Comment(
65
+ text=f" -: qargs: {', '.join([self._convert(q) for q in qargs])}"
66
+ )
67
+ )
68
+ return ()
69
+
70
+ @interp.impl(noise.PauliChannel)
71
+ def emit_pauli(
72
+ self,
73
+ emit: EmitQASM2Gate,
74
+ frame: EmitQASM2Frame,
75
+ stmt: noise.PauliChannel,
76
+ ):
77
+ px: float = stmt.px
78
+ py: float = stmt.py
79
+ pz: float = stmt.pz
80
+ qargs: ilist.IList[ast.Bit, Any] = frame.get(stmt.qargs)
81
+ frame.body.append(
82
+ ast.Comment(text=f"noise.PauliChannel(px={px}, py={py}, pz={pz})")
83
+ )
84
+ frame.body.append(
85
+ ast.Comment(
86
+ text=f" -: qargs: {', '.join([self._convert(q) for q in qargs])}"
87
+ )
88
+ )
89
+ return ()
@@ -5,8 +5,11 @@ from kirin.dialects import cf, scf, func
5
5
  from kirin.ir.dialect import Dialect as Dialect
6
6
 
7
7
  from bloqade.qasm2.parse import ast
8
+ from bloqade.qasm2.dialects.uop import SingleQubitGate, TwoQubitCtrlGate
9
+ from bloqade.qasm2.dialects.expr import GateFunction
8
10
 
9
11
  from .base import EmitQASM2Base, EmitQASM2Frame
12
+ from ..dialects.core.stmts import Reset, Measure
10
13
 
11
14
 
12
15
  @dataclass
@@ -22,12 +25,10 @@ class Func(interp.MethodTable):
22
25
  def emit_func(
23
26
  self, emit: EmitQASM2Main, frame: EmitQASM2Frame, stmt: func.Function
24
27
  ):
25
- from bloqade.qasm2.dialects import glob, noise, parallel
28
+ from bloqade.qasm2.dialects import glob, parallel
26
29
 
27
30
  emit.run_ssacfg_region(frame, stmt.body, ())
28
- if emit.dialects.data.intersection(
29
- (parallel.dialect, glob.dialect, noise.dialect)
30
- ):
31
+ if emit.dialects.data.intersection((parallel.dialect, glob.dialect)):
31
32
  header = ast.Kirin([dialect.name for dialect in emit.dialects])
32
33
  else:
33
34
  header = ast.OPENQASM(ast.Version(2, 0))
@@ -94,6 +95,24 @@ class Scf(interp.MethodTable):
94
95
 
95
96
  cond = emit.assert_node(ast.Cmp, frame.get(stmt.cond))
96
97
 
98
+ # NOTE: we need exactly one of those in the then body in order to emit valid QASM2
99
+ AllowedThenType = SingleQubitGate | TwoQubitCtrlGate | Measure | Reset
100
+
101
+ then_stmts = stmt.then_body.blocks[0].stmts
102
+ uop_stmts = 0
103
+ for s in then_stmts:
104
+ if isinstance(s, AllowedThenType):
105
+ uop_stmts += 1
106
+ continue
107
+
108
+ if isinstance(s, func.Invoke):
109
+ uop_stmts += isinstance(s.callee.code, GateFunction)
110
+
111
+ if uop_stmts != 1:
112
+ raise interp.InterpreterError(
113
+ "Cannot lower if-statement: QASM2 only allows exactly one quantum operation in the body."
114
+ )
115
+
97
116
  with emit.new_frame(stmt) as then_frame:
98
117
  then_frame.entries.update(frame.entries)
99
118
  emit.emit_block(then_frame, stmt.then_body.blocks[0])
@@ -11,6 +11,7 @@ from bloqade.qasm2.passes.glob import GlobalToParallel
11
11
  from bloqade.qasm2.passes.py2qasm import Py2QASM
12
12
  from bloqade.qasm2.passes.parallel import ParallelToUOp
13
13
 
14
+ from . import impls as impls # register the tables
14
15
  from .gate import EmitQASM2Gate
15
16
  from .main import EmitQASM2Main
16
17
 
@@ -27,6 +28,8 @@ class QASM2:
27
28
  allow_parallel: bool = False,
28
29
  allow_global: bool = False,
29
30
  custom_gate: bool = True,
31
+ unroll_ifs: bool = True,
32
+ allow_noise: bool = True,
30
33
  ) -> None:
31
34
  """Initialize the QASM2 target.
32
35
 
@@ -43,9 +46,14 @@ class QASM2:
43
46
  qelib1 (bool):
44
47
  Include the `include "qelib1.inc"` line in the resulting QASM2 AST that's
45
48
  submitted to qBraid. Defaults to `True`.
49
+
46
50
  custom_gate (bool):
47
51
  Include the custom gate definitions in the resulting QASM2 AST. Defaults to `True`. If `False`, all the qasm2.gate will be inlined.
48
52
 
53
+ unroll_ifs (bool):
54
+ Unrolls if statements with multiple qasm2 statements in the body in order to produce valid qasm2 output, which only allows a single
55
+ operation in an if body. Defaults to `True`.
56
+
49
57
 
50
58
 
51
59
  """
@@ -58,6 +66,7 @@ class QASM2:
58
66
  self.custom_gate = custom_gate
59
67
  self.allow_parallel = allow_parallel
60
68
  self.allow_global = allow_global
69
+ self.unroll_ifs = unroll_ifs
61
70
 
62
71
  if allow_parallel:
63
72
  self.main_target = self.main_target.add(qasm2.dialects.parallel)
@@ -67,7 +76,11 @@ class QASM2:
67
76
  self.main_target = self.main_target.add(qasm2.dialects.glob)
68
77
  self.gate_target = self.gate_target.add(qasm2.dialects.glob)
69
78
 
70
- if allow_global or allow_parallel:
79
+ if allow_noise:
80
+ self.main_target = self.main_target.add(qasm2.dialects.noise)
81
+ self.gate_target = self.gate_target.add(qasm2.dialects.noise)
82
+
83
+ if allow_global or allow_parallel or allow_noise:
71
84
  self.main_target = self.main_target.add(ilist)
72
85
  self.gate_target = self.gate_target.add(ilist)
73
86
 
@@ -87,9 +100,11 @@ class QASM2:
87
100
 
88
101
  # make a cloned instance of kernel
89
102
  entry = entry.similar()
90
- QASM2Fold(entry.dialects, inline_gate_subroutine=not self.custom_gate).fixpoint(
91
- entry
92
- )
103
+ QASM2Fold(
104
+ entry.dialects,
105
+ inline_gate_subroutine=not self.custom_gate,
106
+ unroll_ifs=self.unroll_ifs,
107
+ ).fixpoint(entry)
93
108
 
94
109
  if not self.allow_global:
95
110
  # rewrite global to parallel
bloqade/qasm2/noise.py ADDED
@@ -0,0 +1,67 @@
1
+ from typing import Any
2
+
3
+ from kirin.dialects import ilist
4
+ from kirin.lowering import wraps
5
+
6
+ from .types import Qubit
7
+ from .dialects import noise
8
+ from .dialects.noise import (
9
+ TwoRowZoneModel as TwoRowZoneModel,
10
+ MoveNoiseModelABC as MoveNoiseModelABC,
11
+ )
12
+
13
+
14
+ @wraps(noise.AtomLossChannel)
15
+ def atom_loss_channel(qargs: ilist.IList[Qubit, Any] | list, *, prob: float) -> None:
16
+ """Apply an atom loss channel to a list of qubits.
17
+
18
+ Args:
19
+ qargs (ilist.IList[Qubit, Any] | list): List of qubits to apply the noise to.
20
+ prob (float): The loss probability.
21
+ """
22
+ ...
23
+
24
+
25
+ @wraps(noise.PauliChannel)
26
+ def pauli_channel(
27
+ qargs: ilist.IList[Qubit, Any] | list, *, px: float, py: float, pz: float
28
+ ) -> None:
29
+ """Apply a Pauli channel to a list of qubits.
30
+
31
+ Args:
32
+ qargs (ilist.IList[Qubit, Any] | list): List of qubits to apply the noise to.
33
+ px (float): Probability of X error.
34
+ py (float): Probability of Y error.
35
+ pz (float): Probability of Z error.
36
+ """
37
+
38
+
39
+ @wraps(noise.CZPauliChannel)
40
+ def cz_pauli_channel(
41
+ ctrls: ilist.IList[Qubit, Any] | list,
42
+ qargs: ilist.IList[Qubit, Any] | list,
43
+ *,
44
+ px_ctrl: float,
45
+ py_ctrl: float,
46
+ pz_ctrl: float,
47
+ px_qarg: float,
48
+ py_qarg: float,
49
+ pz_qarg: float,
50
+ paired: bool,
51
+ ) -> None:
52
+ """Insert noise for a CZ gate with a Pauli channel on qubits.
53
+
54
+ Args:
55
+ ctrls: List of control qubits.
56
+ qarg2: List of target qubits.
57
+ px_ctrl: Probability of X error on control qubits.
58
+ py_ctrl: Probability of Y error on control qubits.
59
+ pz_ctrl: Probability of Z error on control qubits.
60
+ px_qarg: Probability of X error on target qubits.
61
+ py_qarg: Probability of Y error on target qubits.
62
+ pz_qarg: Probability of Z error on target qubits.
63
+ paired: If True, the noise is applied to both control and target qubits
64
+ are not lost otherwise skip this error. If False Apply the noise on
65
+ the whatever qubit is not lost.
66
+ """
67
+ ...
@@ -19,19 +19,22 @@ def loadfile(file: str | pathlib.Path):
19
19
  return loads(f.read())
20
20
 
21
21
 
22
- def pprint(node: ast.Node, *, console: Console | None = None):
22
+ def pprint(node: ast.Node, *, console: Console | None = None, no_color: bool = False):
23
23
  if console:
24
- return Printer(console).visit(node)
24
+ printer = Printer(console)
25
25
  else:
26
- Printer().visit(node)
26
+ printer = Printer()
27
+ printer.console.no_color = no_color
28
+ printer.visit(node)
27
29
 
28
30
 
29
- def spprint(node: ast.Node, *, console: Console | None = None):
31
+ def spprint(node: ast.Node, *, console: Console | None = None, no_color: bool = False):
30
32
  if console:
31
33
  printer = Printer(console)
32
34
  else:
33
35
  printer = Printer()
34
36
 
37
+ printer.console.no_color = no_color
35
38
  with printer.string_io() as stream:
36
39
  printer.visit(node)
37
40
  return stream.getvalue()