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.
- qbasic.py +113 -0
- qbasic_core/__init__.py +80 -0
- qbasic_core/__main__.py +6 -0
- qbasic_core/analysis.py +179 -0
- qbasic_core/backend.py +76 -0
- qbasic_core/classic.py +419 -0
- qbasic_core/control_flow.py +576 -0
- qbasic_core/debug.py +327 -0
- qbasic_core/demos.py +356 -0
- qbasic_core/display.py +274 -0
- qbasic_core/engine.py +126 -0
- qbasic_core/engine_state.py +109 -0
- qbasic_core/errors.py +37 -0
- qbasic_core/exec_context.py +24 -0
- qbasic_core/executor.py +861 -0
- qbasic_core/expression.py +228 -0
- qbasic_core/file_io.py +457 -0
- qbasic_core/gates.py +284 -0
- qbasic_core/help_text.py +167 -0
- qbasic_core/io_protocol.py +33 -0
- qbasic_core/locc.py +10 -0
- qbasic_core/locc_commands.py +221 -0
- qbasic_core/locc_display.py +61 -0
- qbasic_core/locc_engine.py +195 -0
- qbasic_core/locc_execution.py +389 -0
- qbasic_core/memory.py +369 -0
- qbasic_core/mock_backend.py +66 -0
- qbasic_core/noise_mixin.py +96 -0
- qbasic_core/parser.py +564 -0
- qbasic_core/patterns.py +186 -0
- qbasic_core/profiler.py +156 -0
- qbasic_core/program_mgmt.py +369 -0
- qbasic_core/protocol.py +77 -0
- qbasic_core/py.typed +0 -0
- qbasic_core/scope.py +74 -0
- qbasic_core/screen.py +115 -0
- qbasic_core/state_display.py +60 -0
- qbasic_core/statements.py +387 -0
- qbasic_core/strings.py +107 -0
- qbasic_core/subs.py +261 -0
- qbasic_core/sweep.py +82 -0
- qbasic_core/terminal.py +1697 -0
- qubasic-0.1.0.dist-info/METADATA +736 -0
- qubasic-0.1.0.dist-info/RECORD +48 -0
- qubasic-0.1.0.dist-info/WHEEL +5 -0
- qubasic-0.1.0.dist-info/entry_points.txt +2 -0
- qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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")
|