qubasic 0.1.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.
Files changed (48) hide show
  1. qbasic.py +113 -0
  2. qbasic_core/__init__.py +80 -0
  3. qbasic_core/__main__.py +6 -0
  4. qbasic_core/analysis.py +179 -0
  5. qbasic_core/backend.py +76 -0
  6. qbasic_core/classic.py +419 -0
  7. qbasic_core/control_flow.py +576 -0
  8. qbasic_core/debug.py +327 -0
  9. qbasic_core/demos.py +356 -0
  10. qbasic_core/display.py +274 -0
  11. qbasic_core/engine.py +126 -0
  12. qbasic_core/engine_state.py +109 -0
  13. qbasic_core/errors.py +37 -0
  14. qbasic_core/exec_context.py +24 -0
  15. qbasic_core/executor.py +861 -0
  16. qbasic_core/expression.py +228 -0
  17. qbasic_core/file_io.py +457 -0
  18. qbasic_core/gates.py +284 -0
  19. qbasic_core/help_text.py +167 -0
  20. qbasic_core/io_protocol.py +33 -0
  21. qbasic_core/locc.py +10 -0
  22. qbasic_core/locc_commands.py +221 -0
  23. qbasic_core/locc_display.py +61 -0
  24. qbasic_core/locc_engine.py +195 -0
  25. qbasic_core/locc_execution.py +389 -0
  26. qbasic_core/memory.py +369 -0
  27. qbasic_core/mock_backend.py +66 -0
  28. qbasic_core/noise_mixin.py +96 -0
  29. qbasic_core/parser.py +564 -0
  30. qbasic_core/patterns.py +186 -0
  31. qbasic_core/profiler.py +156 -0
  32. qbasic_core/program_mgmt.py +369 -0
  33. qbasic_core/protocol.py +77 -0
  34. qbasic_core/py.typed +0 -0
  35. qbasic_core/scope.py +74 -0
  36. qbasic_core/screen.py +115 -0
  37. qbasic_core/state_display.py +60 -0
  38. qbasic_core/statements.py +387 -0
  39. qbasic_core/strings.py +107 -0
  40. qbasic_core/subs.py +261 -0
  41. qbasic_core/sweep.py +82 -0
  42. qbasic_core/terminal.py +1697 -0
  43. qubasic-0.1.0.dist-info/METADATA +736 -0
  44. qubasic-0.1.0.dist-info/RECORD +48 -0
  45. qubasic-0.1.0.dist-info/WHEEL +5 -0
  46. qubasic-0.1.0.dist-info/entry_points.txt +2 -0
  47. qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
  48. qubasic-0.1.0.dist-info/top_level.txt +2 -0
qbasic_core/memory.py ADDED
@@ -0,0 +1,369 @@
1
+ """QBASIC memory map — PEEK, POKE, SYS, USR, WAIT, DUMP, MAP, MONITOR, CATALOG."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+
10
+ # ═══════════════════════════════════════════════════════════════════════
11
+ # Memory Map
12
+ # ═══════════════════════════════════════════════════════════════════════
13
+ # $0000-$003F Zero Page (64 slots, SYS parameter passing)
14
+ # $0100-$01FF Qubit State (8 addresses per qubit, 32 qubits max)
15
+ # Per qubit N at $0100 + N*8:
16
+ # +0 P(|1⟩) +1 Bloch X +2 Bloch Y +3 Bloch Z
17
+ # +4 Re(α) +5 Im(α) +6 Re(β) +7 Im(β)
18
+ # $D000-$D007 QPU Config (read/write)
19
+ # $D010-$D014 QPU Status (read-only)
20
+ # $E000-$E0B0 SYS Routine Table (built-in algorithms)
21
+ # $F000-$FFFF User SYS Routines
22
+ # ═══════════════════════════════════════════════════════════════════════
23
+
24
+ QUBIT_BLOCK = 8
25
+
26
+ CFG_NAMES = {
27
+ 0xD000: 'num_qubits', 0xD001: 'shots', 0xD002: 'sim_method',
28
+ 0xD003: 'sim_device', 0xD004: 'noise_type', 0xD005: 'noise_param',
29
+ 0xD006: 'max_iterations', 0xD007: 'screen_mode',
30
+ # Performance tuning (Aer backend options)
31
+ 0xD008: 'fusion_enable', # 0/1
32
+ 0xD009: 'mps_truncation', # float threshold
33
+ 0xD00A: 'sv_parallel_threshold', # int
34
+ 0xD00B: 'es_approx_error', # float (extended stabilizer)
35
+ }
36
+
37
+ STS_NAMES = {
38
+ 0xD010: 'gate_count', 0xD011: 'circuit_depth', 0xD012: 'run_time_ms',
39
+ 0xD013: 'total_probability', 0xD014: 'entanglement_entropy',
40
+ }
41
+
42
+ SIM_METHODS_FWD = {0: 'automatic', 1: 'statevector', 2: 'stabilizer',
43
+ 3: 'matrix_product_state', 4: 'density_matrix'}
44
+ SIM_METHODS_REV = {v: k for k, v in SIM_METHODS_FWD.items()}
45
+ SIM_DEVICES_FWD = {0: 'CPU', 1: 'GPU'}
46
+ SIM_DEVICES_REV = {v: k for k, v in SIM_DEVICES_FWD.items()}
47
+
48
+ SYS_ROUTINES = {
49
+ 0xE000: 'BELL', 0xE010: 'GHZ', 0xE020: 'QFT',
50
+ 0xE030: 'GROVER', 0xE040: 'TELEPORT', 0xE050: 'DEUTSCH',
51
+ 0xE060: 'BERNSTEIN', 0xE070: 'SUPERDENSE', 0xE080: 'RANDOM',
52
+ 0xE090: 'STRESS', 0xE0A0: 'LOCC-TELEPORT', 0xE0B0: 'LOCC-COORD',
53
+ }
54
+
55
+
56
+ class MemoryMixin:
57
+ """Memory-mapped PEEK/POKE, SYS, USR, WAIT, DUMP, MAP, MONITOR, CATALOG.
58
+
59
+ Requires: TerminalProtocol — uses self.num_qubits, self.shots,
60
+ self.sim_method, self.sim_device, self._noise_model, self._max_iterations,
61
+ self.last_sv, self.variables, self.cmd_demo(), self.eval_expr().
62
+ """
63
+
64
+ def _init_memory(self) -> None:
65
+ self._zero_page: list[float] = [0.0] * 64
66
+ self._status: dict[int, float] = {a: 0.0 for a in STS_NAMES}
67
+ self._status[0xD013] = 1.0
68
+ self._user_sys: dict[int, str] = {}
69
+ self._screen_mode: int = 0
70
+ # Per-qubit noise: $D100 + qubit*2 = noise_type, +1 = noise_param
71
+ # noise_type: 0=none, 1=depolarizing, 2=amplitude_damping, 3=phase_flip
72
+ self._qubit_noise: dict[int, tuple[int, float]] = {} # qubit -> (type, param)
73
+
74
+ def _update_status(self, **kw: float) -> None:
75
+ for addr, name in STS_NAMES.items():
76
+ if name in kw:
77
+ self._status[addr] = float(kw[name])
78
+ if self.last_sv is not None:
79
+ sv = np.ascontiguousarray(self.last_sv).ravel()
80
+ self._status[0xD013] = float(np.sum(np.abs(sv) ** 2))
81
+
82
+ # ── PEEK ───────────────────────────────────────────────────────────
83
+
84
+ def _peek(self, addr: float) -> float:
85
+ a = int(addr)
86
+ if 0x0000 <= a <= 0x003F:
87
+ return self._zero_page[a]
88
+ if 0x0100 <= a <= 0x01FF:
89
+ return self._peek_qubit(a)
90
+ if 0xD100 <= a <= 0xD1FF:
91
+ qubit = (a - 0xD100) // 2
92
+ field = (a - 0xD100) % 2
93
+ ntype, nparam = self._qubit_noise.get(qubit, (0, 0.0))
94
+ return float(ntype) if field == 0 else nparam
95
+ if a in CFG_NAMES:
96
+ return self._peek_config(a)
97
+ if a in STS_NAMES:
98
+ return self._status.get(a, 0.0)
99
+ return 0.0
100
+
101
+ def _peek_qubit(self, addr: int) -> float:
102
+ off = addr - 0x0100
103
+ qubit, field = off // QUBIT_BLOCK, off % QUBIT_BLOCK
104
+ if self.last_sv is None or qubit >= self.num_qubits:
105
+ return 0.0
106
+ sv = np.ascontiguousarray(self.last_sv).ravel().reshape([2] * self.num_qubits)
107
+ ax = self.num_qubits - 1 - qubit
108
+ t = np.moveaxis(sv, ax, 0)
109
+ t0, t1 = t[0].flatten(), t[1].flatten()
110
+ rho_00 = float(np.sum(np.abs(t0) ** 2))
111
+ rho_11 = float(np.sum(np.abs(t1) ** 2))
112
+ rho_01 = complex(np.sum(np.conj(t0) * t1))
113
+ return [rho_11,
114
+ float(2 * rho_01.real),
115
+ float(-2 * rho_01.imag),
116
+ float(rho_00 - rho_11),
117
+ float(np.sqrt(max(0, rho_00))),
118
+ 0.0,
119
+ float(np.sqrt(max(0, rho_11))),
120
+ 0.0][field] if field < 8 else 0.0
121
+
122
+ def _peek_config(self, addr: int) -> float:
123
+ n = CFG_NAMES[addr]
124
+ if n == 'num_qubits': return float(self.num_qubits)
125
+ if n == 'shots': return float(self.shots)
126
+ if n == 'sim_method': return float(SIM_METHODS_REV.get(self.sim_method, 0))
127
+ if n == 'sim_device': return float(SIM_DEVICES_REV.get(self.sim_device, 0))
128
+ if n == 'noise_type': return 0.0 if self._noise_model is None else 1.0
129
+ if n == 'noise_param': return 0.0
130
+ if n == 'max_iterations': return float(self._max_iterations)
131
+ if n == 'screen_mode': return float(self._screen_mode)
132
+ if n == 'fusion_enable': return float(getattr(self, '_fusion_enable', 1))
133
+ if n == 'mps_truncation': return float(getattr(self, '_mps_truncation', 1e-16))
134
+ if n == 'sv_parallel_threshold': return float(getattr(self, '_sv_parallel_threshold', 14))
135
+ if n == 'es_approx_error': return float(getattr(self, '_es_approx_error', 0.05))
136
+ return 0.0
137
+
138
+ # ── POKE ───────────────────────────────────────────────────────────
139
+
140
+ def _poke(self, addr: float, value: float) -> None:
141
+ a, v = int(addr), float(value)
142
+ if 0x0000 <= a <= 0x003F:
143
+ self._zero_page[a] = v
144
+ return
145
+ writers = {
146
+ 0xD000: lambda: setattr(self, 'num_qubits', max(1, min(32, int(v)))),
147
+ 0xD001: lambda: setattr(self, 'shots', max(1, int(v))),
148
+ 0xD002: lambda: setattr(self, 'sim_method', SIM_METHODS_FWD.get(int(v), 'automatic')),
149
+ 0xD003: lambda: setattr(self, 'sim_device', SIM_DEVICES_FWD.get(int(v), 'CPU')),
150
+ 0xD006: lambda: setattr(self, '_max_iterations', max(1, int(v))),
151
+ 0xD007: lambda: setattr(self, '_screen_mode', int(v)),
152
+ 0xD008: lambda: setattr(self, '_fusion_enable', bool(int(v))),
153
+ 0xD009: lambda: setattr(self, '_mps_truncation', v),
154
+ 0xD00A: lambda: setattr(self, '_sv_parallel_threshold', int(v)),
155
+ 0xD00B: lambda: setattr(self, '_es_approx_error', v),
156
+ }
157
+ if a in writers:
158
+ writers[a]()
159
+ elif 0xD100 <= a <= 0xD1FF:
160
+ qubit = (a - 0xD100) // 2
161
+ field = (a - 0xD100) % 2
162
+ ntype, nparam = self._qubit_noise.get(qubit, (0, 0.0))
163
+ if field == 0:
164
+ self._qubit_noise[qubit] = (int(v), nparam)
165
+ else:
166
+ self._qubit_noise[qubit] = (ntype, v)
167
+ elif 0xD010 <= a <= 0xD01F:
168
+ self.io.writeln(f"?READ-ONLY: ${a:04X}")
169
+ elif 0x0100 <= a <= 0x01FF:
170
+ # Bloch sphere POKE — prepare qubit state via memory map
171
+ import math
172
+ off = a - 0x0100
173
+ qubit, field = off // QUBIT_BLOCK, off % QUBIT_BLOCK
174
+ if qubit >= self.num_qubits:
175
+ self.io.writeln(f"?QUBIT {qubit} OUT OF RANGE")
176
+ return
177
+ if field == 0:
178
+ # POKE P(|1⟩) — set probability via RY rotation
179
+ p1 = max(0.0, min(1.0, v))
180
+ theta = 2 * math.asin(math.sqrt(p1))
181
+ # This requires a circuit context — store for next RUN
182
+ if not hasattr(self, '_poke_state_prep'):
183
+ self._poke_state_prep = {}
184
+ self._poke_state_prep[qubit] = ('RY', theta)
185
+ self.io.writeln(f"POKE q{qubit}: P(1)={p1:.4f} -> RY({theta:.4f})")
186
+ elif field in (1, 2, 3):
187
+ # Bloch x, y, z — store target vector
188
+ if not hasattr(self, '_poke_state_prep'):
189
+ self._poke_state_prep = {}
190
+ labels = {1: 'Bx', 2: 'By', 3: 'Bz'}
191
+ self._poke_state_prep[(qubit, labels[field])] = v
192
+ self.io.writeln(f"POKE q{qubit}.{labels[field]} = {v:.4f}")
193
+ else:
194
+ self.io.writeln(f"?FIELD {field} NOT WRITABLE (use 0=P(1), 1-3=Bloch)")
195
+
196
+ # ── Commands ───────────────────────────────────────────────────────
197
+
198
+ def cmd_peek(self, rest: str) -> None:
199
+ """PEEK addr — read from memory-mapped address."""
200
+ if not rest.strip():
201
+ self.io.writeln("?USAGE: PEEK <addr>")
202
+ return
203
+ addr = self.eval_expr(rest.strip())
204
+ val = self._peek(addr)
205
+ self.io.writeln(f" ${int(addr):04X} = {val}")
206
+
207
+ def cmd_poke(self, rest: str) -> None:
208
+ """POKE addr, value — write to memory-mapped address."""
209
+ from qbasic_core.engine import RE_POKE
210
+ m = RE_POKE.match(f"POKE {rest}")
211
+ if not m:
212
+ self.io.writeln("?USAGE: POKE <addr>, <value>")
213
+ return
214
+ addr = self.eval_expr(m.group(1))
215
+ val = self.eval_expr(m.group(2))
216
+ self._poke(addr, val)
217
+
218
+ def cmd_sys(self, rest: str) -> None:
219
+ """SYS addr — execute a built-in or user routine."""
220
+ rest = rest.strip().upper()
221
+ if rest.startswith('INSTALL'):
222
+ parts = rest[7:].strip().split(',', 1)
223
+ if len(parts) != 2:
224
+ self.io.writeln("?USAGE: SYS INSTALL <addr>, <subroutine_name>")
225
+ return
226
+ addr = int(self.eval_expr(parts[0].strip()))
227
+ name = parts[1].strip()
228
+ if not (0xF000 <= addr <= 0xFFFF):
229
+ self.io.writeln(f"?USER SYS RANGE: $F000-$FFFF (got ${addr:04X})")
230
+ return
231
+ self._user_sys[addr] = name
232
+ self.io.writeln(f"INSTALLED {name} AT ${addr:04X}")
233
+ return
234
+ addr = int(self.eval_expr(rest))
235
+ if addr in SYS_ROUTINES:
236
+ self.cmd_demo(SYS_ROUTINES[addr])
237
+ elif addr in self._user_sys:
238
+ name = self._user_sys[addr]
239
+ if name in self.subroutines:
240
+ sub = self.subroutines[name]
241
+ body = sub['body'] if isinstance(sub, dict) else sub
242
+ for stmt in body:
243
+ self.process(stmt)
244
+ else:
245
+ self.io.writeln(f"?UNDEFINED ROUTINE: {name}")
246
+ else:
247
+ self.io.writeln(f"?NO ROUTINE AT ${addr:04X}")
248
+
249
+ # File-writing commands blocked from USR execution to prevent
250
+ # user-defined routines from performing uncontrolled I/O.
251
+ _USR_BLOCKED_CMDS = frozenset({'SAVE', 'EXPORT', 'CSV', 'OPEN'})
252
+
253
+ def _usr_fn(self, addr: float) -> float:
254
+ """USR(addr) — execute routine, return last measurement result."""
255
+ a = int(addr)
256
+ if a in SYS_ROUTINES:
257
+ self.cmd_demo(SYS_ROUTINES[a])
258
+ elif a in self._user_sys:
259
+ name = self._user_sys[a]
260
+ if name in self.subroutines:
261
+ sub = self.subroutines[name]
262
+ body = sub['body'] if isinstance(sub, dict) else sub
263
+ for stmt in body:
264
+ first_word = stmt.split(None, 1)[0].upper() if stmt.strip() else ''
265
+ if first_word in self._USR_BLOCKED_CMDS:
266
+ self.io.writeln(f" ?BLOCKED IN USR: {first_word}")
267
+ continue
268
+ self.process(stmt)
269
+ if self.last_counts:
270
+ top = max(self.last_counts, key=self.last_counts.get)
271
+ return float(int(top, 2))
272
+ return 0.0
273
+
274
+ def cmd_wait(self, rest: str) -> None:
275
+ """WAIT addr, mask[, value[, timeout]] — block until (PEEK(addr) AND mask) == value."""
276
+ parts = [p.strip() for p in rest.split(',')]
277
+ if len(parts) < 2:
278
+ self.io.writeln("?USAGE: WAIT <addr>, <mask>[, <value>[, <timeout>]]")
279
+ return
280
+ addr = int(self.eval_expr(parts[0]))
281
+ mask = int(self.eval_expr(parts[1]))
282
+ target = int(self.eval_expr(parts[2])) if len(parts) > 2 else mask
283
+ timeout = float(self.eval_expr(parts[3])) if len(parts) > 3 else 30.0
284
+ t0 = time.time()
285
+ while time.time() - t0 < timeout:
286
+ val = int(self._peek(addr))
287
+ if (val & mask) == target:
288
+ return
289
+ time.sleep(0.05)
290
+ self.io.writeln("?WAIT TIMEOUT")
291
+
292
+ def cmd_catalog(self) -> None:
293
+ """CATALOG — list all SYS routines with addresses."""
294
+ self.io.writeln("\n Built-in SYS Routines:")
295
+ for addr, name in sorted(SYS_ROUTINES.items()):
296
+ self.io.writeln(f" ${addr:04X} {name}")
297
+ if self._user_sys:
298
+ self.io.writeln("\n User SYS Routines:")
299
+ for addr, name in sorted(self._user_sys.items()):
300
+ self.io.writeln(f" ${addr:04X} {name}")
301
+ self.io.writeln('')
302
+
303
+ def cmd_dump(self, rest: str = '') -> None:
304
+ """DUMP [start] [end] — hex dump of memory map."""
305
+ parts = rest.split()
306
+ start = int(self.eval_expr(parts[0])) if parts else 0x0000
307
+ end = int(self.eval_expr(parts[1])) if len(parts) > 1 else start + 0x3F
308
+ self.io.writeln('')
309
+ for row_start in range(start, end + 1, 16):
310
+ vals = [self._peek(row_start + i) for i in range(16) if row_start + i <= end]
311
+ hex_part = ' '.join(f'{int(v) & 0xFF:02X}' if abs(v) < 256 else f'{v:4.1f}'[:4]
312
+ for v in vals)
313
+ self.io.writeln(f" ${row_start:04X}: {hex_part}")
314
+ self.io.writeln('')
315
+
316
+ def cmd_map(self) -> None:
317
+ """MAP — print the full memory map with current values."""
318
+ self.io.writeln("\n Memory Map:")
319
+ self.io.writeln(f" $0000-$003F Zero Page (64 slots)")
320
+ nz = sum(1 for v in self._zero_page if v != 0)
321
+ self.io.writeln(f" {nz} non-zero values")
322
+ self.io.writeln(f" $0100-$01FF Qubit State ({self.num_qubits} qubits, 8 addr/qubit)")
323
+ if self.last_sv is not None:
324
+ for q in range(min(self.num_qubits, 4)):
325
+ p1 = self._peek(0x0100 + q * 8)
326
+ bz = self._peek(0x0100 + q * 8 + 3)
327
+ self.io.writeln(f" q{q}: P(1)={p1:.3f} Bz={bz:.3f}")
328
+ if self.num_qubits > 4:
329
+ self.io.writeln(f" ... ({self.num_qubits - 4} more)")
330
+ self.io.writeln(f" $D000-$D007 QPU Config")
331
+ for addr, name in sorted(CFG_NAMES.items()):
332
+ val = self._peek_config(addr)
333
+ self.io.writeln(f" ${addr:04X} {name:16s} = {val}")
334
+ self.io.writeln(f" $D010-$D014 QPU Status (read-only)")
335
+ for addr, name in sorted(STS_NAMES.items()):
336
+ val = self._status.get(addr, 0.0)
337
+ self.io.writeln(f" ${addr:04X} {name:20s} = {val}")
338
+ self.io.writeln(f" $E000-$E0B0 SYS Routines ({len(SYS_ROUTINES)} built-in)")
339
+ self.io.writeln(f" $F000-$FFFF User SYS ({len(self._user_sys)} installed)")
340
+ self.io.writeln('')
341
+
342
+ def cmd_monitor(self) -> None:
343
+ """MONITOR — interactive hex monitor for PEEK/POKE."""
344
+ self.io.writeln("MONITOR — type address to PEEK, addr=val to POKE, Q to quit")
345
+ while True:
346
+ try:
347
+ line = self.io.read_line('* ').strip()
348
+ except (KeyboardInterrupt, EOFError):
349
+ self.io.writeln('')
350
+ break
351
+ if not line or line.upper() == 'Q':
352
+ break
353
+ if '=' in line:
354
+ parts = line.split('=', 1)
355
+ try:
356
+ addr = int(self.eval_expr(parts[0].strip()))
357
+ val = self.eval_expr(parts[1].strip())
358
+ self._poke(addr, val)
359
+ self.io.writeln(f" ${addr:04X} <- {val}")
360
+ except Exception as e:
361
+ self.io.writeln(f"?{e}")
362
+ else:
363
+ try:
364
+ addr = int(self.eval_expr(line))
365
+ val = self._peek(addr)
366
+ self.io.writeln(f" ${addr:04X} = {val}")
367
+ except Exception as e:
368
+ self.io.writeln(f"?{e}")
369
+ self.io.writeln("MONITOR OFF")
@@ -0,0 +1,66 @@
1
+ """Mock Qiskit backend for fast testing.
2
+
3
+ Produces uniform random counts without running a real simulation.
4
+ Use for tests that verify REPL behavior, not quantum correctness.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import random
10
+ from typing import Any
11
+ from unittest.mock import MagicMock
12
+
13
+
14
+ class MockResult:
15
+ """Fake Qiskit result that returns uniform counts."""
16
+
17
+ def __init__(self, n_qubits: int, shots: int):
18
+ self._n = n_qubits
19
+ self._shots = shots
20
+
21
+ def get_counts(self) -> dict[str, int]:
22
+ states = [format(i, f'0{self._n}b') for i in range(2**self._n)]
23
+ counts: dict[str, int] = {}
24
+ remaining = self._shots
25
+ for s in states[:-1]:
26
+ c = random.randint(0, remaining)
27
+ if c > 0:
28
+ counts[s] = c
29
+ remaining -= c
30
+ if remaining > 0:
31
+ counts[states[-1]] = remaining
32
+ return counts
33
+
34
+ def get_statevector(self):
35
+ import numpy as np
36
+ dim = 2 ** self._n
37
+ sv = np.full(dim, 1.0 / np.sqrt(dim), dtype=complex)
38
+ return sv
39
+
40
+ def data(self) -> dict:
41
+ """Return empty dict — no save instructions in mock."""
42
+ return {}
43
+
44
+
45
+ class MockAerSimulator:
46
+ """Drop-in replacement for AerSimulator that skips simulation."""
47
+
48
+ def __init__(self, **kwargs):
49
+ self._method = kwargs.get('method', 'automatic')
50
+
51
+ def run(self, qc, shots=1024, **kwargs):
52
+ n = qc.num_qubits if hasattr(qc, 'num_qubits') else 2
53
+ result = MockResult(n, shots)
54
+ mock_job = MagicMock()
55
+ mock_job.result.return_value = result
56
+ return mock_job
57
+
58
+
59
+ def patch_aer(monkeypatch):
60
+ """Patch AerSimulator with MockAerSimulator and transpile with identity."""
61
+ monkeypatch.setattr('qbasic_core.terminal.AerSimulator', MockAerSimulator)
62
+ monkeypatch.setattr('qbasic_core.terminal.transpile', lambda qc, backend, **kw: qc)
63
+ monkeypatch.setattr('qbasic_core.analysis.AerSimulator', MockAerSimulator)
64
+ monkeypatch.setattr('qbasic_core.analysis.transpile', lambda qc, backend, **kw: qc)
65
+ monkeypatch.setattr('qbasic_core.sweep.AerSimulator', MockAerSimulator)
66
+ monkeypatch.setattr('qbasic_core.sweep.transpile', lambda qc, backend, **kw: qc)
@@ -0,0 +1,96 @@
1
+ """QBASIC noise model configuration mixin."""
2
+
3
+
4
+ class NoiseMixin:
5
+ """Noise model configuration command for QBasicTerminal.
6
+
7
+ Requires: TerminalProtocol — uses self._noise_model, self.io.
8
+ """
9
+
10
+ def cmd_noise(self, rest: str) -> None:
11
+ """NOISE <type> <params> — set noise model.
12
+
13
+ Types:
14
+ off Disable noise
15
+ depolarizing <p> Depolarizing channel (all gates)
16
+ amplitude_damping <p> T1-like decay
17
+ phase_flip <p> T2-like dephasing
18
+ thermal <T1> <T2> <t_gate> Physical decoherence (microseconds)
19
+ readout <p0> <p1> Measurement bit-flip error
20
+ combined <p_amp> <p_phase> Amplitude + phase damping
21
+ pauli <px> <py> <pz> General Pauli channel
22
+ reset <p0> <p1> Spontaneous reset error
23
+ """
24
+ if not rest or rest.strip().upper() == 'OFF':
25
+ self._noise_model = None
26
+ self.io.writeln("NOISE OFF")
27
+ return
28
+ parts = rest.split()
29
+ ntype = parts[0].lower()
30
+ try:
31
+ from qiskit_aer.noise import (
32
+ NoiseModel, depolarizing_error, amplitude_damping_error,
33
+ phase_damping_error, thermal_relaxation_error,
34
+ phase_amplitude_damping_error, ReadoutError,
35
+ pauli_error, reset_error,
36
+ )
37
+ nm = NoiseModel()
38
+ _1q = ['h', 'x', 'y', 'z', 's', 't', 'sdg', 'tdg',
39
+ 'sx', 'rx', 'ry', 'rz', 'p', 'u', 'id']
40
+ _2q = ['cx', 'cy', 'cz', 'ch', 'swap', 'dcx', 'iswap',
41
+ 'crx', 'cry', 'crz', 'cp', 'rxx', 'ryy', 'rzz']
42
+ _3q = ['ccx', 'cswap']
43
+ if ntype == 'depolarizing':
44
+ p = float(parts[1]) if len(parts) > 1 else 0.01
45
+ nm.add_all_qubit_quantum_error(depolarizing_error(p, 1), _1q)
46
+ nm.add_all_qubit_quantum_error(depolarizing_error(p, 2), _2q)
47
+ nm.add_all_qubit_quantum_error(depolarizing_error(p, 3), _3q)
48
+ self.io.writeln(f"NOISE depolarizing p={p}")
49
+ elif ntype == 'amplitude_damping':
50
+ p = float(parts[1]) if len(parts) > 1 else 0.01
51
+ nm.add_all_qubit_quantum_error(amplitude_damping_error(p), _1q)
52
+ self.io.writeln(f"NOISE amplitude_damping p={p}")
53
+ elif ntype == 'phase_flip':
54
+ p = float(parts[1]) if len(parts) > 1 else 0.01
55
+ nm.add_all_qubit_quantum_error(phase_damping_error(p), _1q)
56
+ self.io.writeln(f"NOISE phase_flip p={p}")
57
+ elif ntype == 'thermal':
58
+ t1 = float(parts[1]) if len(parts) > 1 else 50e-6
59
+ t2 = float(parts[2]) if len(parts) > 2 else 70e-6
60
+ tg = float(parts[3]) if len(parts) > 3 else 1e-6
61
+ err = thermal_relaxation_error(t1, t2, tg)
62
+ nm.add_all_qubit_quantum_error(err, _1q)
63
+ self.io.writeln(f"NOISE thermal T1={t1} T2={t2} t_gate={tg}")
64
+ elif ntype == 'readout':
65
+ p0 = float(parts[1]) if len(parts) > 1 else 0.05
66
+ p1 = float(parts[2]) if len(parts) > 2 else 0.1
67
+ re = ReadoutError([[1 - p0, p0], [p1, 1 - p1]])
68
+ nm.add_all_qubit_readout_error(re)
69
+ self.io.writeln(f"NOISE readout p0={p0} p1={p1}")
70
+ elif ntype == 'combined':
71
+ pa = float(parts[1]) if len(parts) > 1 else 0.01
72
+ pp = float(parts[2]) if len(parts) > 2 else 0.01
73
+ nm.add_all_qubit_quantum_error(
74
+ phase_amplitude_damping_error(pa, pp), _1q)
75
+ self.io.writeln(f"NOISE combined amp={pa} phase={pp}")
76
+ elif ntype == 'pauli':
77
+ px = float(parts[1]) if len(parts) > 1 else 0.01
78
+ py = float(parts[2]) if len(parts) > 2 else 0.01
79
+ pz = float(parts[3]) if len(parts) > 3 else 0.01
80
+ pi = max(0, 1.0 - px - py - pz)
81
+ err = pauli_error([('X', px), ('Y', py), ('Z', pz), ('I', pi)])
82
+ nm.add_all_qubit_quantum_error(err, _1q)
83
+ self.io.writeln(f"NOISE pauli px={px} py={py} pz={pz}")
84
+ elif ntype == 'reset':
85
+ p0 = float(parts[1]) if len(parts) > 1 else 0.01
86
+ p1 = float(parts[2]) if len(parts) > 2 else 0.01
87
+ nm.add_all_qubit_quantum_error(reset_error(p0, p1), _1q)
88
+ self.io.writeln(f"NOISE reset p0={p0} p1={p1}")
89
+ else:
90
+ self.io.writeln(f"?UNKNOWN NOISE TYPE: {ntype}")
91
+ self.io.writeln(" Types: depolarizing, amplitude_damping, phase_flip, thermal,")
92
+ self.io.writeln(" readout, combined, pauli, reset")
93
+ return
94
+ self._noise_model = nm
95
+ except ImportError:
96
+ self.io.writeln("?Noise model requires qiskit-aer noise module")