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/terminal.py
ADDED
|
@@ -0,0 +1,1697 @@
|
|
|
1
|
+
"""QBASIC terminal — REPL, commands, circuit building, LOCC execution."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
|
|
9
|
+
from qiskit import QuantumCircuit, transpile
|
|
10
|
+
from qiskit_aer import AerSimulator
|
|
11
|
+
try:
|
|
12
|
+
from qiskit_aer import AerError
|
|
13
|
+
except ImportError:
|
|
14
|
+
AerError = None
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from qbasic_core.engine import (
|
|
18
|
+
GATE_TABLE, GATE_ALIASES,
|
|
19
|
+
ExecResult,
|
|
20
|
+
_np_gate_matrix, _get_ram_gb, _estimate_gb,
|
|
21
|
+
MAX_QUBITS, DEFAULT_QUBITS, DEFAULT_SHOTS, MAX_UNDO_STACK,
|
|
22
|
+
MAX_LOOP_ITERATIONS,
|
|
23
|
+
OVERHEAD_FACTOR, RAM_BUDGET_FRACTION,
|
|
24
|
+
RE_LINE_NUM, RE_DEF_SINGLE, RE_DEF_BEGIN, RE_REG_INDEX,
|
|
25
|
+
RE_AT_REG, RE_AT_REG_LINE,
|
|
26
|
+
RE_MEAS, RE_RESET, RE_UNITARY, RE_DIM, RE_INPUT,
|
|
27
|
+
RE_CTRL, RE_INV,
|
|
28
|
+
RE_GOTO_GOSUB_TARGET, RE_MEASURE_BASIS, RE_SYNDROME,
|
|
29
|
+
RE_POKE, RE_SYS, RE_OPEN, RE_CLOSE, RE_PRINT_FILE, RE_INPUT_FILE,
|
|
30
|
+
RE_LINE_INPUT, RE_LET_STR, RE_DIM_MULTI,
|
|
31
|
+
RE_DATA, RE_READ, RE_ON_GOTO, RE_ON_GOSUB,
|
|
32
|
+
RE_SELECT_CASE, RE_CASE, RE_DO, RE_LOOP_STMT, RE_EXIT,
|
|
33
|
+
RE_SUB, RE_END_SUB, RE_FUNCTION, RE_END_FUNCTION,
|
|
34
|
+
RE_CALL, RE_LOCAL, RE_STATIC_DECL, RE_SHARED,
|
|
35
|
+
RE_ON_ERROR, RE_RESUME, RE_ERROR_STMT, RE_ASSERT,
|
|
36
|
+
RE_SWAP, RE_DEF_FN, RE_OPTION_BASE,
|
|
37
|
+
RE_PRINT_USING, RE_COLOR, RE_LOCATE, RE_SCREEN, RE_LPRINT,
|
|
38
|
+
RE_ON_MEASURE, RE_ON_TIMER, RE_CHAIN, RE_MERGE,
|
|
39
|
+
RE_REDIM, RE_ERASE, RE_GET,
|
|
40
|
+
)
|
|
41
|
+
from qbasic_core.executor import ExecutorMixin
|
|
42
|
+
from qbasic_core.expression import ExpressionMixin
|
|
43
|
+
from qbasic_core.display import DisplayMixin
|
|
44
|
+
from qbasic_core.demos import DemoMixin
|
|
45
|
+
from qbasic_core.locc import LOCCMixin
|
|
46
|
+
from qbasic_core.control_flow import ControlFlowMixin
|
|
47
|
+
from qbasic_core.file_io import FileIOMixin
|
|
48
|
+
from qbasic_core.analysis import AnalysisMixin
|
|
49
|
+
from qbasic_core.sweep import SweepMixin
|
|
50
|
+
from qbasic_core.memory import MemoryMixin
|
|
51
|
+
from qbasic_core.strings import StringMixin
|
|
52
|
+
from qbasic_core.screen import ScreenMixin
|
|
53
|
+
from qbasic_core.classic import ClassicMixin
|
|
54
|
+
from qbasic_core.subs import SubroutineMixin
|
|
55
|
+
from qbasic_core.debug import DebugMixin
|
|
56
|
+
from qbasic_core.program_mgmt import ProgramMgmtMixin
|
|
57
|
+
from qbasic_core.profiler import ProfilerMixin
|
|
58
|
+
from qbasic_core.noise_mixin import NoiseMixin
|
|
59
|
+
from qbasic_core.state_display import StateDisplayMixin
|
|
60
|
+
from qbasic_core.errors import QBasicError, QBasicBuildError, QBasicRangeError
|
|
61
|
+
from qbasic_core.io_protocol import StdIOPort
|
|
62
|
+
from qbasic_core.parser import parse_stmt
|
|
63
|
+
from qbasic_core.engine_state import Engine
|
|
64
|
+
from qbasic_core.help_text import HELP_TEXT, BANNER_ART
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
68
|
+
# Named quantum states for SET_STATE
|
|
69
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
def _resolve_named_state(name: str, n_qubits: int) -> np.ndarray:
|
|
72
|
+
dim = 2 ** n_qubits
|
|
73
|
+
sv = np.zeros(dim, dtype=complex)
|
|
74
|
+
if name == '|0>':
|
|
75
|
+
sv[0] = 1.0
|
|
76
|
+
elif name == '|1>':
|
|
77
|
+
sv[min(1, dim - 1)] = 1.0
|
|
78
|
+
elif name == '|+>':
|
|
79
|
+
sv[0] = 1.0 / np.sqrt(2)
|
|
80
|
+
sv[min(1, dim - 1)] = 1.0 / np.sqrt(2)
|
|
81
|
+
elif name == '|->':
|
|
82
|
+
sv[0] = 1.0 / np.sqrt(2)
|
|
83
|
+
sv[min(1, dim - 1)] = -1.0 / np.sqrt(2)
|
|
84
|
+
elif name == '|BELL>':
|
|
85
|
+
sv[0] = 1.0 / np.sqrt(2)
|
|
86
|
+
sv[dim - 1] = 1.0 / np.sqrt(2)
|
|
87
|
+
elif name == '|GHZ>':
|
|
88
|
+
sv[0] = 1.0 / np.sqrt(2)
|
|
89
|
+
sv[dim - 1] = 1.0 / np.sqrt(2)
|
|
90
|
+
elif name == '|GHZ3>':
|
|
91
|
+
if dim >= 8:
|
|
92
|
+
sv[0] = 1.0 / np.sqrt(2)
|
|
93
|
+
sv[7] = 1.0 / np.sqrt(2)
|
|
94
|
+
else:
|
|
95
|
+
sv[0] = 1.0 # fallback to |0>
|
|
96
|
+
elif name == '|GHZ4>':
|
|
97
|
+
if dim >= 16:
|
|
98
|
+
sv[0] = 1.0 / np.sqrt(2)
|
|
99
|
+
sv[15] = 1.0 / np.sqrt(2)
|
|
100
|
+
else:
|
|
101
|
+
sv[0] = 1.0 # fallback to |0>
|
|
102
|
+
elif name in ('|W>', '|W3>'):
|
|
103
|
+
if dim >= 8:
|
|
104
|
+
sv[1] = 1.0 / np.sqrt(3)
|
|
105
|
+
sv[2] = 1.0 / np.sqrt(3)
|
|
106
|
+
sv[4] = 1.0 / np.sqrt(3)
|
|
107
|
+
else:
|
|
108
|
+
# fallback: equal superposition of available states
|
|
109
|
+
sv[:] = 1.0 / np.sqrt(dim)
|
|
110
|
+
else:
|
|
111
|
+
sv[0] = 1.0
|
|
112
|
+
return sv
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
116
|
+
# The Terminal
|
|
117
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
118
|
+
|
|
119
|
+
class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoMixin,
|
|
120
|
+
LOCCMixin, ControlFlowMixin, FileIOMixin, AnalysisMixin,
|
|
121
|
+
SweepMixin, MemoryMixin, StringMixin, ScreenMixin, ClassicMixin,
|
|
122
|
+
SubroutineMixin, DebugMixin, ProgramMgmtMixin, ProfilerMixin,
|
|
123
|
+
NoiseMixin, StateDisplayMixin):
|
|
124
|
+
# Method organization uses the mixin pattern to reduce apparent class
|
|
125
|
+
# size while keeping everything on QBasicTerminal for import compat.
|
|
126
|
+
#
|
|
127
|
+
# Mixins (defined above):
|
|
128
|
+
# ExpressionMixin — AST-based safe eval, no eval()
|
|
129
|
+
# DemoMixin — built-in demo circuits
|
|
130
|
+
#
|
|
131
|
+
# Concerns grouped by comment headers below:
|
|
132
|
+
# REPL, Commands, Run, Circuit Building,
|
|
133
|
+
# Display, File I/O, Analysis,
|
|
134
|
+
# LOCC Commands, LOCC Execution,
|
|
135
|
+
# Control Flow, Help.
|
|
136
|
+
#
|
|
137
|
+
# The Qiskit circuit-build path and the numpy LOCC path share
|
|
138
|
+
# control-flow logic via _exec_control_flow() but diverge at
|
|
139
|
+
# gate application: _apply_gate_str (Qiskit) vs _locc_apply_gate
|
|
140
|
+
# (numpy). This dualism is necessary because Qiskit does not
|
|
141
|
+
# natively support mid-circuit measurement with classical
|
|
142
|
+
# feedforward in the way LOCC protocols require.
|
|
143
|
+
# _get_parsed is inherited from Engine
|
|
144
|
+
|
|
145
|
+
def _gate_info(self, name: str) -> tuple[int, int] | None:
|
|
146
|
+
"""Look up (n_params, n_qubits) for a gate name.
|
|
147
|
+
|
|
148
|
+
Checks the global GATE_TABLE first, then instance-local custom gates.
|
|
149
|
+
Returns None if the gate is unknown.
|
|
150
|
+
"""
|
|
151
|
+
if name in GATE_TABLE:
|
|
152
|
+
return GATE_TABLE[name]
|
|
153
|
+
if name in self._custom_gates:
|
|
154
|
+
matrix = self._custom_gates[name]
|
|
155
|
+
n_qubits = int(np.log2(matrix.shape[0]))
|
|
156
|
+
return (0, n_qubits)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _sanitize_path(path: str) -> str:
|
|
161
|
+
"""Path sanitization: reject null bytes, control characters, and traversal.
|
|
162
|
+
|
|
163
|
+
Blocks directory-traversal sequences (..) and absolute paths that
|
|
164
|
+
escape the working directory. For a local REPL the user already has
|
|
165
|
+
shell access, but this prevents accidental overwrites of system files
|
|
166
|
+
and hardens INCLUDE chains against path confusion.
|
|
167
|
+
"""
|
|
168
|
+
if not path or not path.strip():
|
|
169
|
+
raise ValueError("Empty path")
|
|
170
|
+
if '\x00' in path:
|
|
171
|
+
raise ValueError("Path contains null bytes")
|
|
172
|
+
if any(ord(c) < 32 for c in path):
|
|
173
|
+
raise ValueError("Path contains control characters")
|
|
174
|
+
path = path.strip()
|
|
175
|
+
# Reject traversal sequences — the primary injection vector
|
|
176
|
+
normalized = os.path.normpath(path)
|
|
177
|
+
if '..' in normalized.split(os.sep):
|
|
178
|
+
raise ValueError("Path traversal (..) is not allowed")
|
|
179
|
+
# Reject absolute paths (Unix / or Windows drive letters)
|
|
180
|
+
if os.path.isabs(path):
|
|
181
|
+
raise ValueError("Absolute paths are not allowed")
|
|
182
|
+
# Reject UNC paths (\\server\share) and extended-length paths (\\?\)
|
|
183
|
+
if path.startswith('\\\\'):
|
|
184
|
+
raise ValueError("UNC/extended paths are not allowed")
|
|
185
|
+
return path
|
|
186
|
+
|
|
187
|
+
def __init__(self) -> None:
|
|
188
|
+
"""Initialize the QBASIC terminal with default configuration."""
|
|
189
|
+
Engine.__init__(self)
|
|
190
|
+
# Subsystem initialization (mixin state)
|
|
191
|
+
self._init_memory()
|
|
192
|
+
self._init_screen()
|
|
193
|
+
self._init_classic()
|
|
194
|
+
self._init_subs()
|
|
195
|
+
self._init_debug()
|
|
196
|
+
self._init_program_mgmt()
|
|
197
|
+
self._init_profiler()
|
|
198
|
+
self._init_file_handles()
|
|
199
|
+
|
|
200
|
+
# ── REPL ──────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
def _setup_readline(self) -> None:
|
|
203
|
+
"""Set up command history and tab completion."""
|
|
204
|
+
try:
|
|
205
|
+
import readline
|
|
206
|
+
commands = list(GATE_TABLE.keys()) + [
|
|
207
|
+
'RUN', 'LIST', 'NEW', 'SAVE', 'LOAD', 'QUBITS', 'SHOTS',
|
|
208
|
+
'METHOD', 'DEF', 'REG', 'LET', 'STEP', 'STATE', 'HIST',
|
|
209
|
+
'BLOCH', 'PROBS', 'DEMO', 'DELETE', 'RENUM', 'DEFS', 'REGS',
|
|
210
|
+
'VARS', 'HELP', 'CIRCUIT', 'LOCC', 'SEND', 'SHARE',
|
|
211
|
+
'SWEEP', 'INCLUDE', 'EXPORT', 'DECOMPOSE', 'NOISE', 'EXPECT',
|
|
212
|
+
'DENSITY', 'ENTROPY', 'CSV', 'FOR', 'NEXT', 'IF', 'THEN',
|
|
213
|
+
'ELSE', 'WHILE', 'WEND', 'GOTO', 'GOSUB', 'RETURN', 'END',
|
|
214
|
+
'MEASURE', 'BARRIER', 'REM', 'PRINT', 'INPUT', 'DIM', 'UNITARY',
|
|
215
|
+
'LOCCINFO', 'RAM', 'BYE', 'QUIT', 'EXIT',
|
|
216
|
+
]
|
|
217
|
+
def completer(text, state):
|
|
218
|
+
t = text.upper()
|
|
219
|
+
matches = [c for c in commands if c.startswith(t)]
|
|
220
|
+
matches += [s for s in self.subroutines if s.startswith(t)]
|
|
221
|
+
matches += [v for v in self.variables if v.upper().startswith(t)]
|
|
222
|
+
matches += [r for r in self.registers if r.upper().startswith(t)]
|
|
223
|
+
return matches[state] + ' ' if state < len(matches) else None
|
|
224
|
+
readline.set_completer(completer)
|
|
225
|
+
readline.parse_and_bind('tab: complete')
|
|
226
|
+
readline.set_completer_delims(' \t\n')
|
|
227
|
+
except ImportError:
|
|
228
|
+
# On Windows, readline is not bundled; try pyreadline3 as fallback
|
|
229
|
+
if sys.platform == 'win32':
|
|
230
|
+
try:
|
|
231
|
+
import pyreadline3 # noqa: F401 — registers as readline
|
|
232
|
+
import readline
|
|
233
|
+
readline.parse_and_bind('tab: complete')
|
|
234
|
+
except ImportError:
|
|
235
|
+
pass # no readline available — tab completion disabled
|
|
236
|
+
|
|
237
|
+
def repl(self) -> None:
|
|
238
|
+
# Enable VT100 escape sequences on Windows console
|
|
239
|
+
if sys.platform == 'win32':
|
|
240
|
+
try:
|
|
241
|
+
import ctypes
|
|
242
|
+
kernel32 = ctypes.windll.kernel32
|
|
243
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
self.print_banner()
|
|
247
|
+
self._setup_readline()
|
|
248
|
+
while True:
|
|
249
|
+
try:
|
|
250
|
+
line = self.io.read_line(self._prompt).strip()
|
|
251
|
+
if line:
|
|
252
|
+
self.process(line)
|
|
253
|
+
except KeyboardInterrupt:
|
|
254
|
+
self.io.writeln('')
|
|
255
|
+
continue
|
|
256
|
+
except EOFError:
|
|
257
|
+
self.io.writeln("\nBYE")
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
def process(self, line: str, *, track_undo: bool = True) -> None:
|
|
261
|
+
"""Process a line of input (numbered line or immediate command)."""
|
|
262
|
+
# Accumulate TYPE fields when a pending TYPE definition is active
|
|
263
|
+
if self._pending_type is not None:
|
|
264
|
+
self._accumulate_type_field(line)
|
|
265
|
+
return
|
|
266
|
+
# Line number -> store in program
|
|
267
|
+
m = RE_LINE_NUM.match(line)
|
|
268
|
+
if m:
|
|
269
|
+
num = int(m.group(1))
|
|
270
|
+
content = m.group(2).strip()
|
|
271
|
+
# Save undo state (skip during script/file loading)
|
|
272
|
+
if track_undo:
|
|
273
|
+
self._undo_stack.append(dict(self.program))
|
|
274
|
+
if len(self._undo_stack) > MAX_UNDO_STACK:
|
|
275
|
+
self._undo_stack.pop(0)
|
|
276
|
+
if content:
|
|
277
|
+
self.program[num] = content
|
|
278
|
+
self._parsed[num] = parse_stmt(content)
|
|
279
|
+
else:
|
|
280
|
+
if num in self.program:
|
|
281
|
+
del self.program[num]
|
|
282
|
+
self._parsed.pop(num, None)
|
|
283
|
+
self.io.writeln(f"DELETED {num}")
|
|
284
|
+
return
|
|
285
|
+
# Immediate command
|
|
286
|
+
self.dispatch(line)
|
|
287
|
+
|
|
288
|
+
_CMD_NO_ARG = {
|
|
289
|
+
'RUN': 'cmd_run', 'NEW': 'cmd_new', 'STEP': 'cmd_step',
|
|
290
|
+
'HIST': 'cmd_hist', 'PROBS': 'cmd_probs', 'DEFS': 'cmd_defs',
|
|
291
|
+
'REGS': 'cmd_regs', 'VARS': 'cmd_vars', 'HELP': 'cmd_help',
|
|
292
|
+
'CIRCUIT': 'cmd_circuit', 'DECOMPOSE': 'cmd_decompose',
|
|
293
|
+
'DENSITY': 'cmd_density', 'LOCCINFO': 'cmd_loccinfo',
|
|
294
|
+
'UNDO': 'cmd_undo', 'BENCH': 'cmd_bench', 'RAM': 'cmd_ram',
|
|
295
|
+
'BYE': '_quit', 'QUIT': '_quit', 'EXIT': '_quit',
|
|
296
|
+
# Memory
|
|
297
|
+
'MAP': 'cmd_map', 'CATALOG': 'cmd_catalog', 'MONITOR': 'cmd_monitor',
|
|
298
|
+
# Screen
|
|
299
|
+
'CLS': 'cmd_cls', 'PLAY': 'cmd_play',
|
|
300
|
+
# Debug
|
|
301
|
+
'TRON': 'cmd_tron', 'TROFF': 'cmd_troff', 'CONT': 'cmd_cont',
|
|
302
|
+
'HISTORY': 'cmd_history',
|
|
303
|
+
# Program management
|
|
304
|
+
'CHECKSUM': 'cmd_checksum',
|
|
305
|
+
# Classic
|
|
306
|
+
'RESTORE': 'cmd_restore',
|
|
307
|
+
}
|
|
308
|
+
_CMD_WITH_ARG = {
|
|
309
|
+
'LIST': 'cmd_list', 'QUBITS': 'cmd_qubits', 'SHOTS': 'cmd_shots',
|
|
310
|
+
'METHOD': 'cmd_method', 'DEF': 'cmd_def', 'REG': 'cmd_reg',
|
|
311
|
+
'LET': 'cmd_let', 'STATE': 'cmd_state', 'BLOCH': 'cmd_bloch',
|
|
312
|
+
'DEMO': 'cmd_demo', 'DELETE': 'cmd_delete', 'RENUM': 'cmd_renum',
|
|
313
|
+
'SAVE': 'cmd_save', 'LOAD': 'cmd_load', 'SWEEP': 'cmd_sweep',
|
|
314
|
+
'INCLUDE': 'cmd_include', 'EXPORT': 'cmd_export',
|
|
315
|
+
'NOISE': 'cmd_noise', 'EXPECT': 'cmd_expect',
|
|
316
|
+
'ENTROPY': 'cmd_entropy', 'CSV': 'cmd_csv', 'LOCC': 'cmd_locc',
|
|
317
|
+
'SEND': 'cmd_send', 'SHARE': 'cmd_share', 'DIR': 'cmd_dir',
|
|
318
|
+
'CLEAR': 'cmd_clear',
|
|
319
|
+
# Memory
|
|
320
|
+
'PEEK': 'cmd_peek', 'POKE': 'cmd_poke', 'SYS': 'cmd_sys', 'DUMP': 'cmd_dump', 'WAIT': 'cmd_wait',
|
|
321
|
+
# Screen
|
|
322
|
+
'SCREEN': 'cmd_screen', 'COLOR': 'cmd_color', 'LOCATE': 'cmd_locate',
|
|
323
|
+
'PROMPT': 'cmd_prompt',
|
|
324
|
+
# Debug
|
|
325
|
+
'BREAK': 'cmd_breakpoint', 'WATCH': 'cmd_watch', 'PROFILE': 'cmd_profile',
|
|
326
|
+
'REWIND': 'cmd_rewind', 'FORWARD': 'cmd_forward',
|
|
327
|
+
'STATS': 'cmd_stats',
|
|
328
|
+
# Program management
|
|
329
|
+
'AUTO': 'cmd_auto', 'EDIT': 'cmd_edit', 'COPY': 'cmd_copy',
|
|
330
|
+
'MOVE': 'cmd_move', 'FIND': 'cmd_find', 'REPLACE': 'cmd_replace',
|
|
331
|
+
'BANK': 'cmd_bank', 'CHAIN': 'cmd_chain', 'MERGE': 'cmd_merge',
|
|
332
|
+
# File handles
|
|
333
|
+
'OPEN': 'cmd_open', 'CLOSE': 'cmd_close',
|
|
334
|
+
# Module, types, network, primitives
|
|
335
|
+
'IMPORT': 'cmd_import', 'TYPE': 'cmd_type',
|
|
336
|
+
'CONNECT': 'cmd_connect', 'DISCONNECT': 'cmd_disconnect',
|
|
337
|
+
'SAMPLE': 'cmd_sample', 'ESTIMATE': 'cmd_estimate',
|
|
338
|
+
# Circuit macros
|
|
339
|
+
'CIRCUIT_DEF': 'cmd_circuit_def', 'APPLY_CIRCUIT': 'cmd_apply_circuit',
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
def dispatch(self, line: str) -> None:
|
|
343
|
+
"""Parse and execute an immediate command or gate."""
|
|
344
|
+
parts = line.split(None, 1)
|
|
345
|
+
if not parts:
|
|
346
|
+
return
|
|
347
|
+
cmd = parts[0].upper()
|
|
348
|
+
rest = parts[1].strip() if len(parts) > 1 else ''
|
|
349
|
+
|
|
350
|
+
method_name = self._CMD_WITH_ARG.get(cmd) or self._CMD_NO_ARG.get(cmd)
|
|
351
|
+
if method_name:
|
|
352
|
+
try:
|
|
353
|
+
method = getattr(self, method_name)
|
|
354
|
+
if cmd in self._CMD_WITH_ARG:
|
|
355
|
+
method(rest)
|
|
356
|
+
else:
|
|
357
|
+
method()
|
|
358
|
+
except EOFError:
|
|
359
|
+
raise
|
|
360
|
+
except QBasicError as e:
|
|
361
|
+
self.io.writeln(f"?{e.message}")
|
|
362
|
+
except Exception as e:
|
|
363
|
+
self.io.writeln(f"?ERROR: {e}")
|
|
364
|
+
else:
|
|
365
|
+
# Try as immediate gate / subroutine
|
|
366
|
+
try:
|
|
367
|
+
self.run_immediate(line)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
self.io.writeln(f"?SYNTAX ERROR: {e}")
|
|
370
|
+
|
|
371
|
+
def _quit(self) -> None:
|
|
372
|
+
raise EOFError
|
|
373
|
+
|
|
374
|
+
# ── Commands ──────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
def cmd_qubits(self, rest: str) -> None:
|
|
377
|
+
"""Set or display the number of qubits. Range: 1 to MAX_QUBITS."""
|
|
378
|
+
if not rest:
|
|
379
|
+
self.io.writeln(f"QUBITS = {self.num_qubits}")
|
|
380
|
+
return
|
|
381
|
+
n = int(rest)
|
|
382
|
+
if n < 1 or n > MAX_QUBITS:
|
|
383
|
+
from qbasic_core.errors import QBasicRangeError
|
|
384
|
+
raise QBasicRangeError(f"RANGE: 1-{MAX_QUBITS}")
|
|
385
|
+
|
|
386
|
+
self.num_qubits = n
|
|
387
|
+
self.registers.clear()
|
|
388
|
+
est = _estimate_gb(n)
|
|
389
|
+
self.io.writeln(f"{n} QUBITS ALLOCATED (~{est:.1f} GB per instance)")
|
|
390
|
+
ram = _get_ram_gb()
|
|
391
|
+
if ram:
|
|
392
|
+
total, avail = ram
|
|
393
|
+
budget = total * RAM_BUDGET_FRACTION
|
|
394
|
+
if est > avail:
|
|
395
|
+
self.io.writeln(f" WARNING: exceeds available RAM ({avail:.1f} GB free of {total:.0f} GB)")
|
|
396
|
+
elif est > budget * 0.5:
|
|
397
|
+
self.io.writeln(f" NOTE: uses {est/budget*100:.0f}% of 80% RAM budget ({budget:.1f} GB)")
|
|
398
|
+
if n >= 16 and est > 0:
|
|
399
|
+
max_par = int(budget / est)
|
|
400
|
+
if max_par > 0:
|
|
401
|
+
self.io.writeln(f" Max parallel instances in 80% budget: ~{max_par}")
|
|
402
|
+
|
|
403
|
+
def cmd_shots(self, rest: str) -> None:
|
|
404
|
+
"""Set or display the number of measurement shots."""
|
|
405
|
+
if not rest:
|
|
406
|
+
self.io.writeln(f"SHOTS = {self.shots}")
|
|
407
|
+
return
|
|
408
|
+
self.shots = max(1, int(rest))
|
|
409
|
+
self.io.writeln(f"SHOTS = {self.shots}")
|
|
410
|
+
|
|
411
|
+
def cmd_method(self, rest: str) -> None:
|
|
412
|
+
"""Set simulation method (statevector, stabilizer, MPS, ...) or device (CPU/GPU)."""
|
|
413
|
+
if not rest:
|
|
414
|
+
self.io.writeln(f"METHOD = {self.sim_method} DEVICE = {self.sim_device}")
|
|
415
|
+
methods = ['automatic', 'statevector', 'density_matrix',
|
|
416
|
+
'stabilizer', 'matrix_product_state', 'extended_stabilizer',
|
|
417
|
+
'unitary', 'superop']
|
|
418
|
+
self.io.writeln(f" methods: {', '.join(methods)}")
|
|
419
|
+
self.io.writeln(f" devices: CPU, GPU")
|
|
420
|
+
return
|
|
421
|
+
val = rest.strip().upper()
|
|
422
|
+
if val in ('GPU', 'CPU'):
|
|
423
|
+
self.sim_device = val
|
|
424
|
+
self.io.writeln(f"DEVICE = {self.sim_device}")
|
|
425
|
+
else:
|
|
426
|
+
self.sim_method = rest.strip().lower()
|
|
427
|
+
self.io.writeln(f"METHOD = {self.sim_method}")
|
|
428
|
+
|
|
429
|
+
def cmd_list(self, rest: str = '') -> None:
|
|
430
|
+
"""LIST — display program lines. LIST SUBS|VARS|ARRAYS for filtered views."""
|
|
431
|
+
arg = rest.strip().upper()
|
|
432
|
+
if arg == 'SUBS':
|
|
433
|
+
return self.cmd_list_subs()
|
|
434
|
+
if arg == 'VARS':
|
|
435
|
+
return self.cmd_list_vars()
|
|
436
|
+
if arg == 'ARRAYS':
|
|
437
|
+
return self.cmd_list_arrays()
|
|
438
|
+
if not self.program:
|
|
439
|
+
self.io.writeln("EMPTY PROGRAM")
|
|
440
|
+
return
|
|
441
|
+
lines = sorted(self.program.keys())
|
|
442
|
+
for num in lines:
|
|
443
|
+
self.io.writeln(f" {num:5d} {self.program[num]}")
|
|
444
|
+
|
|
445
|
+
def cmd_new(self, *, silent: bool = False) -> None:
|
|
446
|
+
"""NEW — clear program, subroutines, registers, and variables."""
|
|
447
|
+
self.clear()
|
|
448
|
+
if not silent:
|
|
449
|
+
self.io.writeln("READY")
|
|
450
|
+
|
|
451
|
+
def cmd_delete(self, rest: str) -> None:
|
|
452
|
+
"""DELETE <line> or DELETE <start>-<end> — remove program lines."""
|
|
453
|
+
if not rest:
|
|
454
|
+
self.io.writeln("?USAGE: DELETE <line> or DELETE <start>-<end>")
|
|
455
|
+
return
|
|
456
|
+
if '-' in rest:
|
|
457
|
+
a, b = rest.split('-')
|
|
458
|
+
a, b = int(a.strip()), int(b.strip())
|
|
459
|
+
for k in list(self.program.keys()):
|
|
460
|
+
if a <= k <= b:
|
|
461
|
+
del self.program[k]
|
|
462
|
+
self._parsed.pop(k, None)
|
|
463
|
+
self.io.writeln(f"DELETED {a}-{b}")
|
|
464
|
+
else:
|
|
465
|
+
n = int(rest)
|
|
466
|
+
if n in self.program:
|
|
467
|
+
del self.program[n]
|
|
468
|
+
self._parsed.pop(n, None)
|
|
469
|
+
self.io.writeln(f"DELETED {n}")
|
|
470
|
+
else:
|
|
471
|
+
self.io.writeln(f"?LINE {n} NOT FOUND")
|
|
472
|
+
|
|
473
|
+
def cmd_renum(self, rest: str = '') -> None:
|
|
474
|
+
"""RENUM [start] [step] — renumber lines, update GOTO/GOSUB targets."""
|
|
475
|
+
if not self.program:
|
|
476
|
+
return
|
|
477
|
+
parts = rest.split() if rest else []
|
|
478
|
+
start = int(parts[0]) if len(parts) > 0 else 10
|
|
479
|
+
step = int(parts[1]) if len(parts) > 1 else 10
|
|
480
|
+
old_lines = sorted(self.program.keys())
|
|
481
|
+
# Build mapping: old line number -> new line number
|
|
482
|
+
line_map = {}
|
|
483
|
+
for i, old in enumerate(old_lines):
|
|
484
|
+
line_map[old] = start + i * step
|
|
485
|
+
# Renumber and update GOTO/GOSUB references
|
|
486
|
+
new_prog = {}
|
|
487
|
+
for i, old in enumerate(old_lines):
|
|
488
|
+
stmt = self.program[old]
|
|
489
|
+
# Update GOTO/GOSUB targets
|
|
490
|
+
def replace_target(m):
|
|
491
|
+
target = int(m.group(2))
|
|
492
|
+
new_target = line_map.get(target, target)
|
|
493
|
+
return f"{m.group(1)} {new_target}"
|
|
494
|
+
stmt = RE_GOTO_GOSUB_TARGET.sub(replace_target, stmt)
|
|
495
|
+
|
|
496
|
+
# Update ON ... GOTO/GOSUB comma-separated target lists.
|
|
497
|
+
# RE_GOTO_GOSUB_TARGET remaps the first number after GOTO/GOSUB,
|
|
498
|
+
# but ON x GOTO 100, 200, 300 leaves the ", 200, 300" tail
|
|
499
|
+
# untouched. Walk any trailing comma-separated numbers and
|
|
500
|
+
# remap them too.
|
|
501
|
+
def _remap_on_target_list(m):
|
|
502
|
+
keyword = m.group(1) # GOTO or GOSUB
|
|
503
|
+
nums_str = m.group(2) # "10, 20, 30"
|
|
504
|
+
parts = nums_str.split(',')
|
|
505
|
+
remapped = []
|
|
506
|
+
for p in parts:
|
|
507
|
+
stripped = p.strip()
|
|
508
|
+
if stripped.isdigit():
|
|
509
|
+
n = int(stripped)
|
|
510
|
+
remapped.append(str(line_map.get(n, n)))
|
|
511
|
+
else:
|
|
512
|
+
remapped.append(p)
|
|
513
|
+
return f"{keyword} {', '.join(remapped)}"
|
|
514
|
+
stmt = re.sub(
|
|
515
|
+
r'(GOTO|GOSUB)\s+(\d+(?:\s*,\s*\d+)+)',
|
|
516
|
+
_remap_on_target_list, stmt, flags=re.IGNORECASE)
|
|
517
|
+
|
|
518
|
+
# Update ON ERROR GOTO <line-number> targets.
|
|
519
|
+
def _remap_on_error(m):
|
|
520
|
+
target = int(m.group(1))
|
|
521
|
+
return f"ON ERROR GOTO {line_map.get(target, target)}"
|
|
522
|
+
stmt = re.sub(
|
|
523
|
+
r'\bON\s+ERROR\s+GOTO\s+(\d+)', _remap_on_error, stmt,
|
|
524
|
+
flags=re.IGNORECASE)
|
|
525
|
+
|
|
526
|
+
# Update RESUME <line-number> targets.
|
|
527
|
+
def _remap_resume(m):
|
|
528
|
+
target = int(m.group(1))
|
|
529
|
+
return f"RESUME {line_map.get(target, target)}"
|
|
530
|
+
stmt = re.sub(
|
|
531
|
+
r'\bRESUME\s+(\d+)', _remap_resume, stmt,
|
|
532
|
+
flags=re.IGNORECASE)
|
|
533
|
+
|
|
534
|
+
new_prog[line_map[old]] = stmt
|
|
535
|
+
self.program = new_prog
|
|
536
|
+
self._parsed = {num: parse_stmt(s) for num, s in new_prog.items()}
|
|
537
|
+
self.io.writeln(f"RENUMBERED {len(new_prog)} LINES (start={start}, step={step})")
|
|
538
|
+
|
|
539
|
+
# cmd_save, cmd_load provided by FileIOMixin.
|
|
540
|
+
|
|
541
|
+
def cmd_def(self, rest: str) -> None:
|
|
542
|
+
"""Define a named gate sequence (subroutine), optionally parameterized."""
|
|
543
|
+
# Single-line: DEF BELL = H 0 : CX 0,1
|
|
544
|
+
# Parameterized: DEF ROTATE(angle, q) = RX angle, q : RZ angle, q
|
|
545
|
+
# Multi-line: DEF BEGIN NAME[(params)] ... DEF END
|
|
546
|
+
upper = rest.upper().strip()
|
|
547
|
+
if upper.startswith('BEGIN'):
|
|
548
|
+
return self._def_multiline(rest[5:].strip())
|
|
549
|
+
|
|
550
|
+
m = RE_DEF_SINGLE.match(rest)
|
|
551
|
+
if not m:
|
|
552
|
+
self.io.writeln("?USAGE: DEF NAME[(params)] = GATE : GATE : ...")
|
|
553
|
+
self.io.writeln(" DEF BEGIN NAME[(params)] (multi-line, end with DEF END)")
|
|
554
|
+
return
|
|
555
|
+
name = m.group(1).upper()
|
|
556
|
+
params = [p.strip() for p in m.group(2).split(',')] if m.group(2) else []
|
|
557
|
+
body = [s.strip() for s in m.group(3).split(':') if s.strip()]
|
|
558
|
+
if name in GATE_TABLE:
|
|
559
|
+
from qbasic_core.errors import QBasicSyntaxError
|
|
560
|
+
raise QBasicSyntaxError(f"CANNOT REDEFINE BUILT-IN GATE {name}")
|
|
561
|
+
self.subroutines[name] = {'body': body, 'params': params}
|
|
562
|
+
if params:
|
|
563
|
+
self.io.writeln(f"DEF {name}({', '.join(params)}) ({len(body)} gates)")
|
|
564
|
+
else:
|
|
565
|
+
self.io.writeln(f"DEF {name} ({len(body)} gates)")
|
|
566
|
+
|
|
567
|
+
def _def_multiline(self, header: str) -> None:
|
|
568
|
+
"""Read multi-line DEF block from REPL."""
|
|
569
|
+
m = re.match(r'(\w+)(?:\(([^)]*)\))?', header)
|
|
570
|
+
if not m:
|
|
571
|
+
self.io.writeln("?USAGE: DEF BEGIN NAME[(params)]")
|
|
572
|
+
return
|
|
573
|
+
name = m.group(1).upper()
|
|
574
|
+
params = [p.strip() for p in m.group(2).split(',')] if m.group(2) else []
|
|
575
|
+
if name in GATE_TABLE:
|
|
576
|
+
from qbasic_core.errors import QBasicSyntaxError
|
|
577
|
+
raise QBasicSyntaxError(f"CANNOT REDEFINE BUILT-IN GATE {name}")
|
|
578
|
+
|
|
579
|
+
body = []
|
|
580
|
+
self.io.writeln(f" DEF {name} (type gates, DEF END to finish)")
|
|
581
|
+
while True:
|
|
582
|
+
try:
|
|
583
|
+
line = self.io.read_line(' . ').strip()
|
|
584
|
+
except (KeyboardInterrupt, EOFError):
|
|
585
|
+
self.io.writeln("\n CANCELLED")
|
|
586
|
+
return
|
|
587
|
+
if line.upper() == 'DEF END' or line.upper() == 'END':
|
|
588
|
+
break
|
|
589
|
+
if line:
|
|
590
|
+
body.append(line)
|
|
591
|
+
self.subroutines[name] = {'body': body, 'params': params}
|
|
592
|
+
self.io.writeln(f"DEF {name} ({len(body)} gates)")
|
|
593
|
+
|
|
594
|
+
def cmd_type(self, rest: str) -> None:
|
|
595
|
+
"""TYPE name — define a named record type.
|
|
596
|
+
|
|
597
|
+
Usage: TYPE Point
|
|
598
|
+
x AS FLOAT
|
|
599
|
+
y AS FLOAT
|
|
600
|
+
END TYPE
|
|
601
|
+
Then: DIM p AS Point
|
|
602
|
+
LET p.x = 3.14
|
|
603
|
+
|
|
604
|
+
Works both interactively (REPL) and non-interactively (LOAD/INCLUDE).
|
|
605
|
+
In non-interactive mode, subsequent lines are routed through process()
|
|
606
|
+
which delegates to _accumulate_type_field until END TYPE is seen.
|
|
607
|
+
"""
|
|
608
|
+
from qbasic_core.engine import RE_TYPE_BEGIN
|
|
609
|
+
m = RE_TYPE_BEGIN.match(f"TYPE {rest}")
|
|
610
|
+
if not m:
|
|
611
|
+
self.io.writeln("?USAGE: TYPE <name>")
|
|
612
|
+
return
|
|
613
|
+
type_name = m.group(1).upper()
|
|
614
|
+
# Always use the accumulation path — works for both REPL and scripts.
|
|
615
|
+
# In REPL, repl() calls process() per line, which feeds _accumulate_type_field.
|
|
616
|
+
# In scripts, cmd_load/cmd_include call process() per line likewise.
|
|
617
|
+
self._pending_type = {'name': type_name, 'fields': []}
|
|
618
|
+
self.io.writeln(f" TYPE {type_name} (enter fields, END TYPE to finish)")
|
|
619
|
+
|
|
620
|
+
def _accumulate_type_field(self, line: str) -> None:
|
|
621
|
+
"""Accumulate a field for a pending TYPE definition, or finalize on END TYPE."""
|
|
622
|
+
from qbasic_core.engine import RE_TYPE_FIELD, RE_END_TYPE
|
|
623
|
+
stripped = line.strip()
|
|
624
|
+
if RE_END_TYPE.match(stripped):
|
|
625
|
+
# Finalize the type definition
|
|
626
|
+
type_name = self._pending_type['name']
|
|
627
|
+
fields = self._pending_type['fields']
|
|
628
|
+
self._pending_type = None
|
|
629
|
+
self._user_types[type_name] = fields
|
|
630
|
+
self.io.writeln(f"TYPE {type_name} ({len(fields)} fields)")
|
|
631
|
+
return
|
|
632
|
+
fm = RE_TYPE_FIELD.match(stripped)
|
|
633
|
+
if fm:
|
|
634
|
+
self._pending_type['fields'].append((fm.group(1).lower(), fm.group(2).upper()))
|
|
635
|
+
elif stripped:
|
|
636
|
+
self.io.writeln(f" ?EXPECTED: <name> AS <type>")
|
|
637
|
+
|
|
638
|
+
def cmd_circuit_def(self, rest: str) -> None:
|
|
639
|
+
"""CIRCUIT_DEF name start-end — define a circuit macro from line range."""
|
|
640
|
+
m = re.match(r'(\w+)\s+(\d+)\s*-\s*(\d+)', rest)
|
|
641
|
+
if not m:
|
|
642
|
+
self.io.writeln("?USAGE: CIRCUIT_DEF <name> <start>-<end>")
|
|
643
|
+
return
|
|
644
|
+
name = m.group(1).upper()
|
|
645
|
+
start, end = int(m.group(2)), int(m.group(3))
|
|
646
|
+
body = []
|
|
647
|
+
for ln in sorted(self.program.keys()):
|
|
648
|
+
if start <= ln <= end:
|
|
649
|
+
body.append(self.program[ln])
|
|
650
|
+
if not body:
|
|
651
|
+
self.io.writeln(f"?NO LINES IN RANGE {start}-{end}")
|
|
652
|
+
return
|
|
653
|
+
self.subroutines[name] = {'body': body, 'params': []}
|
|
654
|
+
self.io.writeln(f"CIRCUIT {name} = lines {start}-{end} ({len(body)} gates)")
|
|
655
|
+
|
|
656
|
+
def cmd_apply_circuit(self, rest: str) -> None:
|
|
657
|
+
"""APPLY_CIRCUIT name [@offset] — apply a circuit macro."""
|
|
658
|
+
m = re.match(r'(\w+)(?:\s+@(\d+))?', rest)
|
|
659
|
+
if not m:
|
|
660
|
+
self.io.writeln("?USAGE: APPLY_CIRCUIT <name> [@offset]")
|
|
661
|
+
return
|
|
662
|
+
name = m.group(1).upper()
|
|
663
|
+
offset = int(m.group(2)) if m.group(2) else 0
|
|
664
|
+
if name not in self.subroutines:
|
|
665
|
+
self.io.writeln(f"?UNDEFINED CIRCUIT: {name}")
|
|
666
|
+
return
|
|
667
|
+
call_str = f"{name} @{offset}" if offset else name
|
|
668
|
+
self.run_immediate(call_str)
|
|
669
|
+
|
|
670
|
+
def cmd_reg(self, rest: str) -> None:
|
|
671
|
+
"""REG <name> <size> — allocate a named qubit register."""
|
|
672
|
+
# REG data 3
|
|
673
|
+
parts = rest.split()
|
|
674
|
+
if len(parts) != 2:
|
|
675
|
+
self.io.writeln("?USAGE: REG <name> <size>")
|
|
676
|
+
return
|
|
677
|
+
name = parts[0].lower()
|
|
678
|
+
size = int(parts[1])
|
|
679
|
+
# Allocate starting after existing registers
|
|
680
|
+
start = sum(s for _, s in self.registers.values())
|
|
681
|
+
if start + size > self.num_qubits:
|
|
682
|
+
from qbasic_core.errors import QBasicRangeError
|
|
683
|
+
raise QBasicRangeError(f"NOT ENOUGH QUBITS (need {start+size}, have {self.num_qubits})")
|
|
684
|
+
|
|
685
|
+
self.registers[name] = (start, size)
|
|
686
|
+
self.io.writeln(f"REG {name} = qubits {start}-{start+size-1}")
|
|
687
|
+
|
|
688
|
+
def cmd_let(self, rest: str) -> None:
|
|
689
|
+
"""LET <var> = <expr> — assign a computed value to a variable."""
|
|
690
|
+
# LET angle = PI/4
|
|
691
|
+
m = re.match(r'(\w+)\s*=\s*(.*)', rest)
|
|
692
|
+
if not m:
|
|
693
|
+
self.io.writeln("?USAGE: LET <var> = <expr>")
|
|
694
|
+
return
|
|
695
|
+
name = m.group(1)
|
|
696
|
+
val = self.eval_expr(m.group(2))
|
|
697
|
+
self.variables[name] = val
|
|
698
|
+
self.io.writeln(f"{name} = {val}")
|
|
699
|
+
|
|
700
|
+
# cmd_defs, cmd_regs, cmd_vars provided by ProgramMgmtMixin.
|
|
701
|
+
|
|
702
|
+
# ── Run ───────────────────────────────────────────────────────────
|
|
703
|
+
|
|
704
|
+
def cmd_run(self) -> None:
|
|
705
|
+
"""Execute the stored program."""
|
|
706
|
+
if self.locc_mode:
|
|
707
|
+
return self._locc_run()
|
|
708
|
+
if not self.program:
|
|
709
|
+
self.io.writeln("NOTHING TO RUN")
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
t0 = time.time()
|
|
713
|
+
self._gosub_stack = []
|
|
714
|
+
self._collect_data()
|
|
715
|
+
sorted_lines = sorted(self.program.keys())
|
|
716
|
+
self._scan_subs(sorted_lines)
|
|
717
|
+
self._validate_program(sorted_lines)
|
|
718
|
+
|
|
719
|
+
# Build circuit
|
|
720
|
+
try:
|
|
721
|
+
qc, has_measure = self.build_circuit()
|
|
722
|
+
except KeyboardInterrupt:
|
|
723
|
+
self.io.writeln("\n?INTERRUPTED")
|
|
724
|
+
return
|
|
725
|
+
except Exception as e:
|
|
726
|
+
if hasattr(self, '_error_target') and self._error_target is not None:
|
|
727
|
+
self._err_code = 1
|
|
728
|
+
self._err_line = 0
|
|
729
|
+
self._in_error_handler = True
|
|
730
|
+
msg = str(e)
|
|
731
|
+
if msg.startswith('ERROR '):
|
|
732
|
+
try:
|
|
733
|
+
self._err_code = int(msg.split()[1])
|
|
734
|
+
except (IndexError, ValueError):
|
|
735
|
+
pass
|
|
736
|
+
self.variables['ERR'] = self._err_code
|
|
737
|
+
self.variables['ERL'] = self._err_line
|
|
738
|
+
self.io.writeln(f"?BUILD ERROR (trapped): {e}")
|
|
739
|
+
else:
|
|
740
|
+
self.io.writeln(f"?BUILD ERROR: {e}")
|
|
741
|
+
return
|
|
742
|
+
|
|
743
|
+
# Copy before adding measurements (for statevector extraction)
|
|
744
|
+
qc_sv = qc.copy()
|
|
745
|
+
|
|
746
|
+
# Unitary/superop methods need save instructions, not measurements
|
|
747
|
+
if self.sim_method in ('unitary', 'superop'):
|
|
748
|
+
if self.sim_method == 'unitary':
|
|
749
|
+
qc.save_unitary(label='unitary')
|
|
750
|
+
else:
|
|
751
|
+
qc.save_superop(label='superop')
|
|
752
|
+
elif has_measure:
|
|
753
|
+
qc.measure_all()
|
|
754
|
+
|
|
755
|
+
self.last_circuit = qc
|
|
756
|
+
|
|
757
|
+
# Run with shots (cache transpiled circuit if program unchanged)
|
|
758
|
+
try:
|
|
759
|
+
method = self.sim_method
|
|
760
|
+
if method == 'automatic':
|
|
761
|
+
if self.num_qubits > 28:
|
|
762
|
+
method = 'matrix_product_state'
|
|
763
|
+
elif not self._noise_model and self._is_clifford(qc):
|
|
764
|
+
method = 'stabilizer'
|
|
765
|
+
backend_opts = {'method': method}
|
|
766
|
+
if self.sim_device == 'GPU':
|
|
767
|
+
backend_opts['device'] = 'GPU'
|
|
768
|
+
if self._noise_model:
|
|
769
|
+
backend_opts['noise_model'] = self._noise_model
|
|
770
|
+
# Performance tuning from memory-mapped config
|
|
771
|
+
if hasattr(self, '_fusion_enable'):
|
|
772
|
+
backend_opts['fusion_enable'] = self._fusion_enable
|
|
773
|
+
if hasattr(self, '_mps_truncation'):
|
|
774
|
+
backend_opts['matrix_product_state_truncation_threshold'] = self._mps_truncation
|
|
775
|
+
if hasattr(self, '_sv_parallel_threshold'):
|
|
776
|
+
backend_opts['statevector_parallel_threshold'] = self._sv_parallel_threshold
|
|
777
|
+
if hasattr(self, '_es_approx_error'):
|
|
778
|
+
backend_opts['extended_stabilizer_approximation_error'] = self._es_approx_error
|
|
779
|
+
cache_key = (
|
|
780
|
+
tuple(sorted(self.program.items())),
|
|
781
|
+
self.num_qubits, method, self.sim_device,
|
|
782
|
+
id(self._noise_model),
|
|
783
|
+
getattr(self, '_fusion_enable', None),
|
|
784
|
+
getattr(self, '_mps_truncation', None),
|
|
785
|
+
getattr(self, '_sv_parallel_threshold', None),
|
|
786
|
+
getattr(self, '_es_approx_error', None),
|
|
787
|
+
)
|
|
788
|
+
if self._circuit_cache_key == cache_key and self._circuit_cache is not None:
|
|
789
|
+
qc_t, backend = self._circuit_cache
|
|
790
|
+
else:
|
|
791
|
+
backend = AerSimulator(**backend_opts)
|
|
792
|
+
qc_t = transpile(qc, backend)
|
|
793
|
+
self._circuit_cache_key = cache_key
|
|
794
|
+
self._circuit_cache = (qc_t, backend)
|
|
795
|
+
try:
|
|
796
|
+
result = backend.run(qc_t, shots=self.shots).result()
|
|
797
|
+
except KeyboardInterrupt:
|
|
798
|
+
self.io.writeln("\n?INTERRUPTED")
|
|
799
|
+
return
|
|
800
|
+
except Exception as _sim_err:
|
|
801
|
+
_err_msg = str(_sim_err).lower()
|
|
802
|
+
if (AerError is not None and isinstance(_sim_err, AerError) and 'stabilizer' in _err_msg) or \
|
|
803
|
+
('stabilizer' in _err_msg and 'invalid parameters' in _err_msg):
|
|
804
|
+
self._circuit_cache_key = None
|
|
805
|
+
self._circuit_cache = None
|
|
806
|
+
sv_opts = {k: v for k, v in backend_opts.items()
|
|
807
|
+
if k != 'method'}
|
|
808
|
+
sv_opts['method'] = 'statevector'
|
|
809
|
+
backend = AerSimulator(**sv_opts)
|
|
810
|
+
qc_t = transpile(qc, backend)
|
|
811
|
+
result = backend.run(qc_t, shots=self.shots).result()
|
|
812
|
+
else:
|
|
813
|
+
raise
|
|
814
|
+
# Extract results based on method
|
|
815
|
+
if method in ('unitary', 'superop'):
|
|
816
|
+
self.last_counts = None
|
|
817
|
+
data = result.data()
|
|
818
|
+
label = 'unitary' if method == 'unitary' else 'superop'
|
|
819
|
+
mat = data.get(label)
|
|
820
|
+
if mat is not None:
|
|
821
|
+
mat_np = np.asarray(mat)
|
|
822
|
+
self.variables[label] = mat_np
|
|
823
|
+
dim = mat_np.shape[0]
|
|
824
|
+
self.io.writeln(f"\n {label.upper()} ({dim}x{dim}):")
|
|
825
|
+
if dim <= 16:
|
|
826
|
+
for i in range(dim):
|
|
827
|
+
row = ' '.join(f"{v.real:+.3f}{v.imag:+.3f}j" for v in mat_np[i])
|
|
828
|
+
self.io.writeln(f" {row}")
|
|
829
|
+
else:
|
|
830
|
+
self.io.writeln(f" (too large to display — stored in variable '{label}')")
|
|
831
|
+
else:
|
|
832
|
+
self.last_counts = dict(result.get_counts())
|
|
833
|
+
# Extract save instruction results into BASIC variables
|
|
834
|
+
data = result.data()
|
|
835
|
+
for key, val in data.items():
|
|
836
|
+
if key.startswith('exp_'):
|
|
837
|
+
var = key[4:]
|
|
838
|
+
self.variables[var] = float(np.real(val))
|
|
839
|
+
elif key.startswith('prob_'):
|
|
840
|
+
var = key[5:]
|
|
841
|
+
self.variables[var] = val
|
|
842
|
+
if isinstance(val, np.ndarray):
|
|
843
|
+
self.arrays[var] = val.tolist()
|
|
844
|
+
elif key.startswith('amp_'):
|
|
845
|
+
var = key[4:]
|
|
846
|
+
if isinstance(val, np.ndarray):
|
|
847
|
+
self.arrays[var] = [complex(v) for v in val]
|
|
848
|
+
self.variables[var] = val
|
|
849
|
+
except Exception as e:
|
|
850
|
+
self.io.writeln(f"?RUNTIME ERROR: {e}")
|
|
851
|
+
return
|
|
852
|
+
|
|
853
|
+
# Get statevector from the measurement-free copy
|
|
854
|
+
if method not in ('unitary', 'superop'):
|
|
855
|
+
try:
|
|
856
|
+
qc_sv.save_statevector()
|
|
857
|
+
sv_backend = AerSimulator(method='statevector')
|
|
858
|
+
sv_result = sv_backend.run(transpile(qc_sv, sv_backend)).result()
|
|
859
|
+
self.last_sv = np.array(sv_result.get_statevector())
|
|
860
|
+
except Exception:
|
|
861
|
+
self.last_sv = None
|
|
862
|
+
else:
|
|
863
|
+
self.last_sv = None
|
|
864
|
+
|
|
865
|
+
dt = time.time() - t0
|
|
866
|
+
|
|
867
|
+
# Update status registers
|
|
868
|
+
depth = qc.depth()
|
|
869
|
+
n_gates = qc.size()
|
|
870
|
+
self._update_status(gate_count=n_gates, circuit_depth=depth,
|
|
871
|
+
run_time_ms=dt * 1000)
|
|
872
|
+
|
|
873
|
+
# Display results
|
|
874
|
+
self.io.writeln(f"\nRAN {len(self.program)} lines, {self.num_qubits} qubits, "
|
|
875
|
+
f"{self.shots} shots in {dt:.2f}s [depth={depth}, gates={n_gates}]")
|
|
876
|
+
if method in ('unitary', 'superop'):
|
|
877
|
+
pass # matrix already displayed above
|
|
878
|
+
elif has_measure and self.last_counts:
|
|
879
|
+
self.print_histogram(self.last_counts)
|
|
880
|
+
self._auto_display()
|
|
881
|
+
else:
|
|
882
|
+
self.io.writeln("(no MEASURE in program — use STATE or PROBS to inspect)")
|
|
883
|
+
|
|
884
|
+
def cmd_sample(self, rest: str = '') -> None:
|
|
885
|
+
"""SAMPLE [shots] — sample the current circuit using SamplerV2 primitive."""
|
|
886
|
+
if not self.program:
|
|
887
|
+
self.io.writeln("NOTHING TO SAMPLE")
|
|
888
|
+
return
|
|
889
|
+
shots = int(rest.strip()) if rest.strip() else self.shots
|
|
890
|
+
try:
|
|
891
|
+
qc, has_measure = self.build_circuit()
|
|
892
|
+
if not has_measure:
|
|
893
|
+
qc.measure_all()
|
|
894
|
+
from qiskit_aer.primitives import SamplerV2
|
|
895
|
+
sampler = SamplerV2()
|
|
896
|
+
result = sampler.run([qc], shots=shots).result()
|
|
897
|
+
# Extract counts — SamplerV2 result format varies by Qiskit version.
|
|
898
|
+
# Try the V2 get_counts() on named data attributes first, then fall
|
|
899
|
+
# back to iterating data attributes for older layouts.
|
|
900
|
+
pub_result = result[0]
|
|
901
|
+
counts = {}
|
|
902
|
+
# V2 preferred path: named classical registers expose get_counts()
|
|
903
|
+
try:
|
|
904
|
+
for attr_name in dir(pub_result.data):
|
|
905
|
+
if attr_name.startswith('_'):
|
|
906
|
+
continue
|
|
907
|
+
obj = getattr(pub_result.data, attr_name, None)
|
|
908
|
+
if obj is not None and hasattr(obj, 'get_counts'):
|
|
909
|
+
counts = dict(obj.get_counts())
|
|
910
|
+
break
|
|
911
|
+
except Exception:
|
|
912
|
+
pass
|
|
913
|
+
# Fallback: try legacy get_counts directly on data
|
|
914
|
+
if not counts:
|
|
915
|
+
try:
|
|
916
|
+
counts = dict(pub_result.data.get_counts())
|
|
917
|
+
except Exception:
|
|
918
|
+
pass
|
|
919
|
+
self.last_counts = counts
|
|
920
|
+
self.io.writeln(f"SAMPLED {shots} shots ({len(counts)} unique outcomes)")
|
|
921
|
+
if counts:
|
|
922
|
+
self.print_histogram(counts)
|
|
923
|
+
except Exception as e:
|
|
924
|
+
self.io.writeln(f"?SAMPLE ERROR: {e}")
|
|
925
|
+
|
|
926
|
+
def cmd_estimate(self, rest: str) -> None:
|
|
927
|
+
"""ESTIMATE <pauli> [qubits] — estimate observable expectation via EstimatorV2."""
|
|
928
|
+
parts = rest.split()
|
|
929
|
+
if not parts:
|
|
930
|
+
self.io.writeln("?USAGE: ESTIMATE <Z|ZZ|XY|...> [qubits]")
|
|
931
|
+
return
|
|
932
|
+
pauli_str = parts[0].upper()
|
|
933
|
+
qubits = [int(q) for q in parts[1:]] if len(parts) > 1 else list(range(len(pauli_str)))
|
|
934
|
+
try:
|
|
935
|
+
from qiskit.quantum_info import SparsePauliOp
|
|
936
|
+
from qiskit_aer.primitives import EstimatorV2
|
|
937
|
+
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
|
|
938
|
+
qc, _ = self.build_circuit()
|
|
939
|
+
full_pauli = ['I'] * self.num_qubits
|
|
940
|
+
for i, p in enumerate(pauli_str):
|
|
941
|
+
if i < len(qubits):
|
|
942
|
+
full_pauli[self.num_qubits - 1 - qubits[i]] = p
|
|
943
|
+
op = SparsePauliOp(''.join(full_pauli))
|
|
944
|
+
estimator = EstimatorV2()
|
|
945
|
+
pm = generate_preset_pass_manager(optimization_level=0)
|
|
946
|
+
qc_t = pm.run(qc)
|
|
947
|
+
result = estimator.run([(qc_t, op)]).result()
|
|
948
|
+
val = result[0].data.evs
|
|
949
|
+
self.io.writeln(f" <{pauli_str}> on qubits {qubits} = {float(val):.6f}")
|
|
950
|
+
except Exception as e:
|
|
951
|
+
self.io.writeln(f"?ESTIMATE ERROR: {e}")
|
|
952
|
+
|
|
953
|
+
def cmd_step(self) -> None:
|
|
954
|
+
"""Step through program, showing state after each line."""
|
|
955
|
+
if not self.program:
|
|
956
|
+
self.io.writeln("NOTHING TO STEP")
|
|
957
|
+
return
|
|
958
|
+
|
|
959
|
+
from qbasic_core.exec_context import ExecContext
|
|
960
|
+
from qbasic_core.scope import Scope
|
|
961
|
+
|
|
962
|
+
sorted_lines = sorted(self.program.keys())
|
|
963
|
+
qc = QuantumCircuit(self.num_qubits)
|
|
964
|
+
ctx = ExecContext(
|
|
965
|
+
sorted_lines=sorted_lines, ip=0,
|
|
966
|
+
run_vars=Scope(self.variables),
|
|
967
|
+
max_iterations=self._max_iterations, qc=qc,
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
self.io.writeln(f"STEP MODE — {len(sorted_lines)} lines, {self.num_qubits} qubits")
|
|
971
|
+
self.io.writeln("Press ENTER to advance, Q to quit\n")
|
|
972
|
+
|
|
973
|
+
while ctx.ip < len(sorted_lines):
|
|
974
|
+
ctx.iteration_count += 1
|
|
975
|
+
if ctx.iteration_count > ctx.max_iterations:
|
|
976
|
+
raise RuntimeError(f"LOOP LIMIT ({ctx.max_iterations}) — possible infinite loop")
|
|
977
|
+
line_num = sorted_lines[ctx.ip]
|
|
978
|
+
stmt = self.program[line_num]
|
|
979
|
+
parsed = self._get_parsed(line_num)
|
|
980
|
+
|
|
981
|
+
# Display current line
|
|
982
|
+
self.io.writeln(f">> {line_num:5d} {stmt}")
|
|
983
|
+
|
|
984
|
+
# Execute it
|
|
985
|
+
result = self._exec_line(stmt, parsed=parsed, ctx=ctx)
|
|
986
|
+
|
|
987
|
+
# Show state
|
|
988
|
+
try:
|
|
989
|
+
qc_tmp = qc.copy()
|
|
990
|
+
qc_tmp.save_statevector()
|
|
991
|
+
sv_b = AerSimulator(method='statevector')
|
|
992
|
+
sv_r = sv_b.run(transpile(qc_tmp, sv_b)).result()
|
|
993
|
+
sv = np.array(sv_r.get_statevector())
|
|
994
|
+
self._print_sv_compact(sv)
|
|
995
|
+
except Exception:
|
|
996
|
+
self.io.writeln(" (state unavailable)")
|
|
997
|
+
|
|
998
|
+
# Wait for input
|
|
999
|
+
try:
|
|
1000
|
+
user = self.io.read_line(" [ENTER/Q] ").strip().upper()
|
|
1001
|
+
if user == 'Q':
|
|
1002
|
+
self.io.writeln("STOPPED")
|
|
1003
|
+
return
|
|
1004
|
+
except (KeyboardInterrupt, EOFError):
|
|
1005
|
+
self.io.writeln("\nSTOPPED")
|
|
1006
|
+
return
|
|
1007
|
+
|
|
1008
|
+
if isinstance(result, int):
|
|
1009
|
+
ctx.ip = result
|
|
1010
|
+
else:
|
|
1011
|
+
ctx.ip += 1
|
|
1012
|
+
|
|
1013
|
+
self.io.writeln("DONE")
|
|
1014
|
+
|
|
1015
|
+
# run_immediate provided by ExecutorMixin.
|
|
1016
|
+
|
|
1017
|
+
def _validate_program(self, sorted_lines: list[int]) -> None:
|
|
1018
|
+
"""Pre-execution validation. Catches structural errors before running."""
|
|
1019
|
+
from qbasic_core.statements import (
|
|
1020
|
+
GotoStmt, GosubStmt, ForStmt, NextStmt, WhileStmt, WendStmt,
|
|
1021
|
+
DoStmt, LoopStmt, SubStmt, EndSubStmt, FunctionStmt, EndFunctionStmt,
|
|
1022
|
+
IfThenStmt,
|
|
1023
|
+
)
|
|
1024
|
+
line_set = set(sorted_lines)
|
|
1025
|
+
for_depth = 0
|
|
1026
|
+
while_depth = 0
|
|
1027
|
+
do_depth = 0
|
|
1028
|
+
for_var_stack: list[str] = []
|
|
1029
|
+
for ln in sorted_lines:
|
|
1030
|
+
parsed = self._get_parsed(ln)
|
|
1031
|
+
if isinstance(parsed, GotoStmt) and parsed.target not in line_set:
|
|
1032
|
+
raise RuntimeError(f"LINE {ln}: GOTO {parsed.target} — target line not found")
|
|
1033
|
+
if isinstance(parsed, GosubStmt) and parsed.target not in line_set:
|
|
1034
|
+
raise RuntimeError(f"LINE {ln}: GOSUB {parsed.target} — target line not found")
|
|
1035
|
+
# Validate GOTO/GOSUB targets embedded in IF THEN/ELSE clauses
|
|
1036
|
+
if isinstance(parsed, IfThenStmt):
|
|
1037
|
+
for clause in (parsed.then_clause, parsed.else_clause):
|
|
1038
|
+
if clause:
|
|
1039
|
+
for m in re.finditer(r'\b(?:GOTO|GOSUB)\s+(\d+)', clause, re.IGNORECASE):
|
|
1040
|
+
target = int(m.group(1))
|
|
1041
|
+
if target not in line_set:
|
|
1042
|
+
raise RuntimeError(
|
|
1043
|
+
f"LINE {ln}: {m.group(0)} (in IF THEN) — target line not found")
|
|
1044
|
+
if isinstance(parsed, ForStmt):
|
|
1045
|
+
for_depth += 1
|
|
1046
|
+
for_var_stack.append(parsed.var.upper())
|
|
1047
|
+
elif isinstance(parsed, NextStmt):
|
|
1048
|
+
for_depth -= 1
|
|
1049
|
+
# Check FOR/NEXT variable name matching
|
|
1050
|
+
if parsed.var and for_var_stack:
|
|
1051
|
+
expected = for_var_stack[-1]
|
|
1052
|
+
if parsed.var.upper() != expected:
|
|
1053
|
+
raise RuntimeError(
|
|
1054
|
+
f"LINE {ln}: NEXT {parsed.var} does not match FOR {expected}")
|
|
1055
|
+
if for_var_stack:
|
|
1056
|
+
for_var_stack.pop()
|
|
1057
|
+
elif isinstance(parsed, WhileStmt):
|
|
1058
|
+
while_depth += 1
|
|
1059
|
+
elif isinstance(parsed, WendStmt):
|
|
1060
|
+
while_depth -= 1
|
|
1061
|
+
elif isinstance(parsed, DoStmt):
|
|
1062
|
+
do_depth += 1
|
|
1063
|
+
elif isinstance(parsed, LoopStmt):
|
|
1064
|
+
do_depth -= 1
|
|
1065
|
+
if for_depth > 0:
|
|
1066
|
+
raise RuntimeError(f"Unmatched FOR: {for_depth} FOR without NEXT")
|
|
1067
|
+
if while_depth > 0:
|
|
1068
|
+
raise RuntimeError(f"Unmatched WHILE: {while_depth} WHILE without WEND")
|
|
1069
|
+
if do_depth > 0:
|
|
1070
|
+
raise RuntimeError(f"Unmatched DO: {do_depth} DO without LOOP")
|
|
1071
|
+
|
|
1072
|
+
# ── Circuit Building (provided by ExecutorMixin) ────────────────
|
|
1073
|
+
|
|
1074
|
+
# Mid-circuit measurement returns this value in Qiskit circuit-build mode.
|
|
1075
|
+
# Qiskit defers measurement to simulation time, so the variable cannot hold
|
|
1076
|
+
# the actual outcome during circuit construction. For classical feedforward,
|
|
1077
|
+
# use LOCC mode with SEND instead.
|
|
1078
|
+
MEAS_CIRCUIT_MODE_VALUE = 0
|
|
1079
|
+
|
|
1080
|
+
def _try_exec_meas(self, stmt: str, qc, run_vars: dict, *, backend=None) -> bool:
|
|
1081
|
+
"""Handle MEAS qubit -> var (mid-circuit measurement)."""
|
|
1082
|
+
m = RE_MEAS.match(stmt)
|
|
1083
|
+
if not m:
|
|
1084
|
+
return False
|
|
1085
|
+
qubit = int(self._eval_with_vars(m.group(1), run_vars))
|
|
1086
|
+
var = m.group(2)
|
|
1087
|
+
if 0 <= qubit < self.num_qubits:
|
|
1088
|
+
if not self.locc_mode:
|
|
1089
|
+
raise QBasicBuildError(
|
|
1090
|
+
"MEAS requires LOCC mode for classical feedforward. "
|
|
1091
|
+
"Use LOCC SEND instead, or use MEASURE for end-of-circuit measurement.")
|
|
1092
|
+
b = backend or qc
|
|
1093
|
+
if hasattr(b, 'add_classical_register'):
|
|
1094
|
+
cr = b.add_classical_register(f'meas_{var}')
|
|
1095
|
+
b.measure(qubit, cr[0])
|
|
1096
|
+
else:
|
|
1097
|
+
from qiskit.circuit import ClassicalRegister
|
|
1098
|
+
cr = ClassicalRegister(1, f'meas_{var}')
|
|
1099
|
+
qc.add_register(cr)
|
|
1100
|
+
qc.measure(qubit, cr[0])
|
|
1101
|
+
run_vars[var] = self.MEAS_CIRCUIT_MODE_VALUE
|
|
1102
|
+
self.variables[var] = self.MEAS_CIRCUIT_MODE_VALUE
|
|
1103
|
+
return True
|
|
1104
|
+
|
|
1105
|
+
def _try_exec_reset(self, stmt: str, qc, run_vars: dict, *, backend=None) -> bool:
|
|
1106
|
+
"""Handle RESET qubit."""
|
|
1107
|
+
m = RE_RESET.match(stmt)
|
|
1108
|
+
if not m:
|
|
1109
|
+
return False
|
|
1110
|
+
qubit = int(self._eval_with_vars(m.group(1), run_vars))
|
|
1111
|
+
if 0 <= qubit < self.num_qubits:
|
|
1112
|
+
b = backend or qc
|
|
1113
|
+
b.reset(qubit)
|
|
1114
|
+
return True
|
|
1115
|
+
|
|
1116
|
+
def _try_exec_unitary(self, stmt: str) -> bool:
|
|
1117
|
+
"""Handle UNITARY name = [[...]] gate definition."""
|
|
1118
|
+
m = RE_UNITARY.match(stmt)
|
|
1119
|
+
if not m:
|
|
1120
|
+
return False
|
|
1121
|
+
name = m.group(1).upper()
|
|
1122
|
+
try:
|
|
1123
|
+
matrix = np.array(self._parse_matrix(m.group(2)), dtype=complex)
|
|
1124
|
+
n_qubits = int(np.log2(matrix.shape[0]))
|
|
1125
|
+
if matrix.shape != (2**n_qubits, 2**n_qubits):
|
|
1126
|
+
raise ValueError("Matrix must be 2^n x 2^n")
|
|
1127
|
+
product = matrix @ matrix.conj().T
|
|
1128
|
+
if not np.allclose(product, np.eye(matrix.shape[0]), atol=1e-6):
|
|
1129
|
+
raise ValueError("Matrix is not unitary (U @ U† ≠ I, atol=1e-6). Use expressions like 1/sqrt(2) instead of truncated decimals.")
|
|
1130
|
+
self._custom_gates[name] = matrix
|
|
1131
|
+
self.io.writeln(f"UNITARY {name}: {n_qubits}-qubit gate defined")
|
|
1132
|
+
except Exception as e:
|
|
1133
|
+
self.io.writeln(f"?UNITARY ERROR: {e}")
|
|
1134
|
+
return True
|
|
1135
|
+
|
|
1136
|
+
def _try_exec_dim(self, stmt: str) -> bool:
|
|
1137
|
+
"""Handle DIM name(size) or DIM name(d1, d2, ...) array declaration."""
|
|
1138
|
+
m = RE_DIM_MULTI.match(stmt)
|
|
1139
|
+
if not m:
|
|
1140
|
+
m = RE_DIM.match(stmt)
|
|
1141
|
+
if not m:
|
|
1142
|
+
return False
|
|
1143
|
+
name = m.group(1)
|
|
1144
|
+
dims = [int(d.strip()) for d in m.group(2).split(',')]
|
|
1145
|
+
total = 1
|
|
1146
|
+
for d in dims:
|
|
1147
|
+
total *= d
|
|
1148
|
+
self.arrays[name] = [0.0] * total
|
|
1149
|
+
if len(dims) > 1:
|
|
1150
|
+
self._array_dims[name] = dims
|
|
1151
|
+
return True
|
|
1152
|
+
|
|
1153
|
+
def _try_exec_redim(self, stmt: str) -> bool:
|
|
1154
|
+
"""Handle REDIM name(size) — resize an existing array."""
|
|
1155
|
+
m = RE_REDIM.match(stmt)
|
|
1156
|
+
if not m:
|
|
1157
|
+
return False
|
|
1158
|
+
name = m.group(1)
|
|
1159
|
+
new_size = int(m.group(2))
|
|
1160
|
+
old = self.arrays.get(name, [])
|
|
1161
|
+
if isinstance(old, list):
|
|
1162
|
+
if new_size > len(old):
|
|
1163
|
+
self.arrays[name] = old + [0.0] * (new_size - len(old))
|
|
1164
|
+
else:
|
|
1165
|
+
self.arrays[name] = old[:new_size]
|
|
1166
|
+
else:
|
|
1167
|
+
self.arrays[name] = [0.0] * new_size
|
|
1168
|
+
return True
|
|
1169
|
+
|
|
1170
|
+
def _try_exec_erase(self, stmt: str) -> bool:
|
|
1171
|
+
"""Handle ERASE name — delete a specific array."""
|
|
1172
|
+
m = RE_ERASE.match(stmt)
|
|
1173
|
+
if not m:
|
|
1174
|
+
return False
|
|
1175
|
+
name = m.group(1)
|
|
1176
|
+
if name in self.arrays:
|
|
1177
|
+
del self.arrays[name]
|
|
1178
|
+
return True
|
|
1179
|
+
|
|
1180
|
+
def _try_exec_get(self, stmt: str, run_vars: dict) -> bool:
|
|
1181
|
+
"""Handle GET var — single keypress input without enter.
|
|
1182
|
+
|
|
1183
|
+
Falls back to reading one character from self.io.read_line
|
|
1184
|
+
when not running in a terminal (batch mode, tests, etc.).
|
|
1185
|
+
"""
|
|
1186
|
+
m = RE_GET.match(stmt)
|
|
1187
|
+
if not m:
|
|
1188
|
+
return False
|
|
1189
|
+
var = m.group(1)
|
|
1190
|
+
ch = ''
|
|
1191
|
+
try:
|
|
1192
|
+
import sys as _sys
|
|
1193
|
+
if _sys.stdin.isatty():
|
|
1194
|
+
if _sys.platform == 'win32':
|
|
1195
|
+
import msvcrt
|
|
1196
|
+
ch = msvcrt.getwch()
|
|
1197
|
+
else:
|
|
1198
|
+
import tty, termios
|
|
1199
|
+
fd = _sys.stdin.fileno()
|
|
1200
|
+
old_settings = termios.tcgetattr(fd)
|
|
1201
|
+
try:
|
|
1202
|
+
tty.setraw(fd)
|
|
1203
|
+
ch = _sys.stdin.read(1)
|
|
1204
|
+
finally:
|
|
1205
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
1206
|
+
else:
|
|
1207
|
+
line = self.io.read_line('')
|
|
1208
|
+
ch = line[0] if line else ''
|
|
1209
|
+
except (EOFError, KeyboardInterrupt, OSError):
|
|
1210
|
+
ch = ''
|
|
1211
|
+
if var.endswith('$'):
|
|
1212
|
+
run_vars[var] = ch
|
|
1213
|
+
self.variables[var] = ch
|
|
1214
|
+
else:
|
|
1215
|
+
run_vars[var] = float(ord(ch)) if ch else 0.0
|
|
1216
|
+
self.variables[var] = run_vars[var]
|
|
1217
|
+
return True
|
|
1218
|
+
|
|
1219
|
+
def _try_exec_input(self, stmt: str, run_vars: dict) -> bool:
|
|
1220
|
+
"""Handle INPUT "prompt", var user input with retry on bad input."""
|
|
1221
|
+
m = RE_INPUT.match(stmt)
|
|
1222
|
+
if not m:
|
|
1223
|
+
return False
|
|
1224
|
+
prompt = m.group(1) or m.group(2)
|
|
1225
|
+
var = m.group(2)
|
|
1226
|
+
for _attempt in range(3):
|
|
1227
|
+
try:
|
|
1228
|
+
val = self.io.read_line(f"{prompt}? ")
|
|
1229
|
+
if var.endswith('$'):
|
|
1230
|
+
run_vars[var] = val
|
|
1231
|
+
self.variables[var] = val
|
|
1232
|
+
else:
|
|
1233
|
+
run_vars[var] = float(val) if '.' in val else int(val)
|
|
1234
|
+
self.variables[var] = run_vars[var]
|
|
1235
|
+
return True
|
|
1236
|
+
except (EOFError, KeyboardInterrupt):
|
|
1237
|
+
run_vars[var] = 0
|
|
1238
|
+
self.variables[var] = 0
|
|
1239
|
+
return True
|
|
1240
|
+
except ValueError:
|
|
1241
|
+
self.io.writeln("?REDO FROM START")
|
|
1242
|
+
run_vars[var] = 0
|
|
1243
|
+
self.variables[var] = 0
|
|
1244
|
+
return True
|
|
1245
|
+
|
|
1246
|
+
def _try_exec_measure_basis(self, stmt: str, qc, run_vars: dict,
|
|
1247
|
+
*, backend=None) -> bool:
|
|
1248
|
+
"""Handle MEASURE_X/Y/Z qubit — measurement in a non-computational basis."""
|
|
1249
|
+
m = RE_MEASURE_BASIS.match(stmt)
|
|
1250
|
+
if not m:
|
|
1251
|
+
return False
|
|
1252
|
+
basis = m.group(1).upper()
|
|
1253
|
+
qubit = int(self._eval_with_vars(m.group(2), run_vars))
|
|
1254
|
+
if qubit < 0 or qubit >= self.num_qubits:
|
|
1255
|
+
raise ValueError(f"QUBIT {qubit} OUT OF RANGE (0-{self.num_qubits-1})")
|
|
1256
|
+
b = backend or qc
|
|
1257
|
+
if basis == 'X':
|
|
1258
|
+
b.apply_gate('H', (), [qubit]) if hasattr(b, 'apply_gate') else qc.h(qubit)
|
|
1259
|
+
elif basis == 'Y':
|
|
1260
|
+
if hasattr(b, 'apply_gate'):
|
|
1261
|
+
b.apply_gate('SDG', (), [qubit])
|
|
1262
|
+
b.apply_gate('H', (), [qubit])
|
|
1263
|
+
else:
|
|
1264
|
+
qc.sdg(qubit)
|
|
1265
|
+
qc.h(qubit)
|
|
1266
|
+
var = f"m{basis.lower()}_{qubit}"
|
|
1267
|
+
if hasattr(b, 'add_classical_register'):
|
|
1268
|
+
cr = b.add_classical_register(f'meas_{var}')
|
|
1269
|
+
b.measure(qubit, cr[0])
|
|
1270
|
+
else:
|
|
1271
|
+
from qiskit.circuit import ClassicalRegister
|
|
1272
|
+
cr = ClassicalRegister(1, f'meas_{var}')
|
|
1273
|
+
qc.add_register(cr)
|
|
1274
|
+
qc.measure(qubit, cr[0])
|
|
1275
|
+
run_vars[var] = 0
|
|
1276
|
+
self.variables[var] = 0
|
|
1277
|
+
return True
|
|
1278
|
+
|
|
1279
|
+
# _parse_syndrome provided by ExecutorMixin.
|
|
1280
|
+
|
|
1281
|
+
_PAULI_TO_CONTROLLED = {'X': 'CX', 'Y': 'CY', 'Z': 'CZ'}
|
|
1282
|
+
|
|
1283
|
+
def _try_exec_syndrome(self, stmt: str, qc, run_vars: dict,
|
|
1284
|
+
*, backend=None) -> bool:
|
|
1285
|
+
"""Handle SYNDROME — non-destructive stabilizer measurement via ancilla."""
|
|
1286
|
+
parsed = self._parse_syndrome(stmt, run_vars)
|
|
1287
|
+
if parsed is None:
|
|
1288
|
+
return False
|
|
1289
|
+
pauli_str, qubits, var = parsed
|
|
1290
|
+
anc = self.num_qubits - 1
|
|
1291
|
+
if anc in qubits:
|
|
1292
|
+
raise ValueError(
|
|
1293
|
+
f"Qubit {anc} needed as ancilla but appears in stabilizer. "
|
|
1294
|
+
f"Increase QUBITS by 1.")
|
|
1295
|
+
b = backend or qc
|
|
1296
|
+
if hasattr(b, 'apply_gate'):
|
|
1297
|
+
b.apply_gate('H', (), [anc])
|
|
1298
|
+
for p, q in zip(pauli_str, qubits):
|
|
1299
|
+
if p != 'I':
|
|
1300
|
+
b.apply_gate(self._PAULI_TO_CONTROLLED[p], (), [anc, q])
|
|
1301
|
+
b.apply_gate('H', (), [anc])
|
|
1302
|
+
cr = b.add_classical_register(f'synd_{var}')
|
|
1303
|
+
b.measure(anc, cr[0])
|
|
1304
|
+
b.reset(anc)
|
|
1305
|
+
else:
|
|
1306
|
+
qc.h(anc)
|
|
1307
|
+
for p, q in zip(pauli_str, qubits):
|
|
1308
|
+
if p != 'I':
|
|
1309
|
+
gate_method = getattr(qc, self._PAULI_TO_CONTROLLED[p].lower())
|
|
1310
|
+
gate_method(anc, q)
|
|
1311
|
+
qc.h(anc)
|
|
1312
|
+
from qiskit.circuit import ClassicalRegister
|
|
1313
|
+
cr = ClassicalRegister(1, f'synd_{var}')
|
|
1314
|
+
qc.add_register(cr)
|
|
1315
|
+
qc.measure(anc, cr[0])
|
|
1316
|
+
qc.reset(anc)
|
|
1317
|
+
run_vars[var] = 0
|
|
1318
|
+
self.variables[var] = 0
|
|
1319
|
+
return True
|
|
1320
|
+
|
|
1321
|
+
# _exec_line, _RESERVED_KEYWORDS, _RESERVED_NAMES provided by ExecutorMixin.
|
|
1322
|
+
|
|
1323
|
+
def _try_stmt_handlers(self, stmt: str, qc, run_vars: dict, *, backend=None) -> bool:
|
|
1324
|
+
"""Try statement-type handlers in order. Returns True if handled."""
|
|
1325
|
+
return (
|
|
1326
|
+
self._try_exec_meas(stmt, qc, run_vars, backend=backend)
|
|
1327
|
+
or self._try_exec_reset(stmt, qc, run_vars, backend=backend)
|
|
1328
|
+
or self._try_exec_measure_basis(stmt, qc, run_vars, backend=backend)
|
|
1329
|
+
or self._try_exec_syndrome(stmt, qc, run_vars, backend=backend)
|
|
1330
|
+
or self._try_exec_unitary(stmt)
|
|
1331
|
+
or self._try_exec_dim(stmt)
|
|
1332
|
+
or self._try_exec_redim(stmt)
|
|
1333
|
+
or self._try_exec_erase(stmt)
|
|
1334
|
+
or self._try_exec_get(stmt, run_vars)
|
|
1335
|
+
or self._try_exec_input(stmt, run_vars)
|
|
1336
|
+
or self._try_exec_poke(stmt, run_vars)
|
|
1337
|
+
or self._try_exec_sys(stmt)
|
|
1338
|
+
or self._exec_print_file(stmt, run_vars)
|
|
1339
|
+
or self._exec_input_file(stmt, run_vars)
|
|
1340
|
+
or self._exec_lprint(stmt, run_vars)
|
|
1341
|
+
or self._try_exec_line_input(stmt, run_vars)
|
|
1342
|
+
or self._try_exec_let_str(stmt, run_vars)
|
|
1343
|
+
or self._try_exec_print_using(stmt, run_vars)
|
|
1344
|
+
or self._try_exec_open_close(stmt)
|
|
1345
|
+
or self._try_exec_save_expect(stmt, qc, run_vars)
|
|
1346
|
+
or self._try_exec_save_probs(stmt, qc, run_vars)
|
|
1347
|
+
or self._try_exec_save_amps(stmt, qc, run_vars)
|
|
1348
|
+
or self._try_exec_set_state(stmt, qc)
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
def _try_exec_poke(self, stmt: str, run_vars: dict) -> bool:
|
|
1352
|
+
m = RE_POKE.match(stmt)
|
|
1353
|
+
if not m:
|
|
1354
|
+
return False
|
|
1355
|
+
addr = self._eval_with_vars(m.group(1), run_vars)
|
|
1356
|
+
val = self._eval_with_vars(m.group(2), run_vars)
|
|
1357
|
+
self._poke(addr, val)
|
|
1358
|
+
return True
|
|
1359
|
+
|
|
1360
|
+
def _try_exec_sys(self, stmt: str) -> bool:
|
|
1361
|
+
m = re.match(r'SYS\s+(.+)', stmt, re.IGNORECASE)
|
|
1362
|
+
if not m:
|
|
1363
|
+
return False
|
|
1364
|
+
self.cmd_sys(m.group(1))
|
|
1365
|
+
return True
|
|
1366
|
+
|
|
1367
|
+
def _try_exec_line_input(self, stmt: str, run_vars: dict) -> bool:
|
|
1368
|
+
m = RE_LINE_INPUT.match(stmt)
|
|
1369
|
+
if not m:
|
|
1370
|
+
return False
|
|
1371
|
+
prompt = m.group(1) or m.group(2)
|
|
1372
|
+
var = m.group(2)
|
|
1373
|
+
try:
|
|
1374
|
+
val = self.io.read_line(f"{prompt}? ")
|
|
1375
|
+
run_vars[var] = val
|
|
1376
|
+
self.variables[var] = val
|
|
1377
|
+
except (EOFError, KeyboardInterrupt):
|
|
1378
|
+
run_vars[var] = ''
|
|
1379
|
+
self.variables[var] = ''
|
|
1380
|
+
return True
|
|
1381
|
+
|
|
1382
|
+
def _try_exec_let_str(self, stmt: str, run_vars: dict) -> bool:
|
|
1383
|
+
m = RE_LET_STR.match(stmt)
|
|
1384
|
+
if not m:
|
|
1385
|
+
return False
|
|
1386
|
+
name = m.group(1)
|
|
1387
|
+
val = self._eval_string_expr(m.group(2))
|
|
1388
|
+
run_vars[name] = val
|
|
1389
|
+
self.variables[name] = val
|
|
1390
|
+
return True
|
|
1391
|
+
|
|
1392
|
+
def _try_exec_print_using(self, stmt: str, run_vars: dict) -> bool:
|
|
1393
|
+
m = RE_PRINT_USING.match(stmt)
|
|
1394
|
+
if not m:
|
|
1395
|
+
return False
|
|
1396
|
+
fmt = m.group(1)
|
|
1397
|
+
vals = [self._eval_with_vars(v.strip(), run_vars)
|
|
1398
|
+
for v in m.group(2).split(',') if v.strip()]
|
|
1399
|
+
result = fmt
|
|
1400
|
+
for val in vals:
|
|
1401
|
+
# Find the next format field (run of # and . characters)
|
|
1402
|
+
idx = result.find('#')
|
|
1403
|
+
if idx < 0:
|
|
1404
|
+
break
|
|
1405
|
+
end = idx
|
|
1406
|
+
while end < len(result) and result[end] in '#.':
|
|
1407
|
+
end += 1
|
|
1408
|
+
field = result[idx:end]
|
|
1409
|
+
field_width = len(field)
|
|
1410
|
+
dot_pos = field.find('.')
|
|
1411
|
+
if dot_pos >= 0:
|
|
1412
|
+
decimals = len(field) - dot_pos - 1
|
|
1413
|
+
formatted = f"{val:{field_width}.{decimals}f}"
|
|
1414
|
+
else:
|
|
1415
|
+
formatted = f"{val:{field_width}.0f}"
|
|
1416
|
+
result = result[:idx] + formatted.rjust(field_width) + result[end:]
|
|
1417
|
+
self.io.writeln(result)
|
|
1418
|
+
return True
|
|
1419
|
+
|
|
1420
|
+
def _try_exec_open_close(self, stmt: str) -> bool:
|
|
1421
|
+
m = RE_OPEN.match(stmt)
|
|
1422
|
+
if m:
|
|
1423
|
+
rest = stmt[4:].strip()
|
|
1424
|
+
self.cmd_open(rest)
|
|
1425
|
+
return True
|
|
1426
|
+
m = RE_CLOSE.match(stmt)
|
|
1427
|
+
if m:
|
|
1428
|
+
self.cmd_close(m.group(1))
|
|
1429
|
+
return True
|
|
1430
|
+
return False
|
|
1431
|
+
|
|
1432
|
+
def _try_exec_save_expect(self, stmt: str, qc, run_vars: dict) -> bool:
|
|
1433
|
+
"""SAVE_EXPECT <pauli> <qubits> -> <var> — inline expectation value."""
|
|
1434
|
+
from qbasic_core.engine import RE_SAVE_EXPECT
|
|
1435
|
+
m = RE_SAVE_EXPECT.match(stmt)
|
|
1436
|
+
if not m:
|
|
1437
|
+
return False
|
|
1438
|
+
pauli_str = m.group(1).upper()
|
|
1439
|
+
qubits = [int(q.strip()) for q in m.group(2).split(',') if q.strip()]
|
|
1440
|
+
var = m.group(3)
|
|
1441
|
+
try:
|
|
1442
|
+
from qiskit.quantum_info import SparsePauliOp
|
|
1443
|
+
full_pauli = ['I'] * self.num_qubits
|
|
1444
|
+
for i, p in enumerate(pauli_str):
|
|
1445
|
+
if i < len(qubits):
|
|
1446
|
+
full_pauli[self.num_qubits - 1 - qubits[i]] = p
|
|
1447
|
+
op = SparsePauliOp(''.join(full_pauli))
|
|
1448
|
+
qc.save_expectation_value(op, list(range(self.num_qubits)), label=f'exp_{var}')
|
|
1449
|
+
run_vars[var] = 0 # actual value available after simulation
|
|
1450
|
+
self.variables[var] = 0
|
|
1451
|
+
except Exception as e:
|
|
1452
|
+
self.io.writeln(f"?SAVE_EXPECT ERROR: {e}")
|
|
1453
|
+
return True
|
|
1454
|
+
|
|
1455
|
+
def _try_exec_save_probs(self, stmt: str, qc, run_vars: dict) -> bool:
|
|
1456
|
+
"""SAVE_PROBS <qubits> -> <var> — inline probability snapshot."""
|
|
1457
|
+
from qbasic_core.engine import RE_SAVE_PROBS
|
|
1458
|
+
m = RE_SAVE_PROBS.match(stmt)
|
|
1459
|
+
if not m:
|
|
1460
|
+
return False
|
|
1461
|
+
qubits = [int(q.strip()) for q in m.group(1).split(',') if q.strip()]
|
|
1462
|
+
var = m.group(2)
|
|
1463
|
+
try:
|
|
1464
|
+
qc.save_probabilities(qubits, label=f'prob_{var}')
|
|
1465
|
+
run_vars[var] = 0
|
|
1466
|
+
self.variables[var] = 0
|
|
1467
|
+
except Exception as e:
|
|
1468
|
+
self.io.writeln(f"?SAVE_PROBS ERROR: {e}")
|
|
1469
|
+
return True
|
|
1470
|
+
|
|
1471
|
+
def _try_exec_save_amps(self, stmt: str, qc, run_vars: dict) -> bool:
|
|
1472
|
+
"""SAVE_AMPS <indices> -> <var> — save specific amplitudes by index."""
|
|
1473
|
+
from qbasic_core.engine import RE_SAVE_AMPS
|
|
1474
|
+
m = RE_SAVE_AMPS.match(stmt)
|
|
1475
|
+
if not m:
|
|
1476
|
+
return False
|
|
1477
|
+
indices = [int(q.strip()) for q in m.group(1).split(',') if q.strip()]
|
|
1478
|
+
var = m.group(2)
|
|
1479
|
+
try:
|
|
1480
|
+
qc.save_amplitudes(indices, label=f'amp_{var}')
|
|
1481
|
+
run_vars[var] = 0
|
|
1482
|
+
self.variables[var] = 0
|
|
1483
|
+
except Exception as e:
|
|
1484
|
+
self.io.writeln(f"?SAVE_AMPS ERROR: {e}")
|
|
1485
|
+
return True
|
|
1486
|
+
|
|
1487
|
+
def _try_exec_set_state(self, stmt: str, qc) -> bool:
|
|
1488
|
+
"""SET_STATE <statevector> — inject custom statevector mid-circuit.
|
|
1489
|
+
|
|
1490
|
+
Accepts:
|
|
1491
|
+
SET_STATE [0.707, 0, 0, 0.707] — explicit amplitudes
|
|
1492
|
+
SET_STATE |+> — named state
|
|
1493
|
+
SET_STATE |BELL> — named entangled state
|
|
1494
|
+
SET_STATE |GHZ> — GHZ state
|
|
1495
|
+
"""
|
|
1496
|
+
from qbasic_core.engine import RE_SET_STATE
|
|
1497
|
+
m = RE_SET_STATE.match(stmt)
|
|
1498
|
+
if not m:
|
|
1499
|
+
return False
|
|
1500
|
+
try:
|
|
1501
|
+
sv_expr = m.group(1).strip()
|
|
1502
|
+
dim = 2 ** self.num_qubits
|
|
1503
|
+
# Try named states first
|
|
1504
|
+
if sv_expr.upper() in ('|0>', '|1>', '|+>', '|->', '|BELL>', '|GHZ>',
|
|
1505
|
+
'|GHZ3>', '|GHZ4>', '|W>', '|W3>'):
|
|
1506
|
+
sv_flat = _resolve_named_state(sv_expr.upper(), self.num_qubits)
|
|
1507
|
+
else:
|
|
1508
|
+
sv_list = self._parse_matrix(sv_expr)
|
|
1509
|
+
sv_flat = np.array(sv_list, dtype=complex).ravel()
|
|
1510
|
+
if len(sv_flat) != dim:
|
|
1511
|
+
raise ValueError(f"State vector length {len(sv_flat)} != 2^{self.num_qubits} = {dim}")
|
|
1512
|
+
norm = float(np.sum(np.abs(sv_flat) ** 2))
|
|
1513
|
+
if abs(norm - 1.0) > 1e-6:
|
|
1514
|
+
sv_flat = sv_flat / np.sqrt(norm)
|
|
1515
|
+
self.io.writeln(f" (normalized: ||sv||={norm:.6f} -> 1.0)")
|
|
1516
|
+
from qiskit.quantum_info import Statevector
|
|
1517
|
+
sv_obj = Statevector(sv_flat)
|
|
1518
|
+
from qiskit_aer.library import SetStatevector
|
|
1519
|
+
qc.append(SetStatevector(sv_obj), list(range(self.num_qubits)))
|
|
1520
|
+
except Exception as e:
|
|
1521
|
+
self.io.writeln(f"?SET_STATE ERROR: {e}")
|
|
1522
|
+
return True
|
|
1523
|
+
|
|
1524
|
+
# _split_colon_stmts, _substitute_vars, _expand_statement,
|
|
1525
|
+
# _offset_qubits, _apply_gate_str, _tokenize_gate, _resolve_qubit,
|
|
1526
|
+
# _GATE_DISPATCH, _apply_gate provided by ExecutorMixin.
|
|
1527
|
+
|
|
1528
|
+
# ── Display ───────────────────────────────────────────────────────
|
|
1529
|
+
|
|
1530
|
+
def _try_quantum_print(self, text: str, run_vars: dict) -> str | None:
|
|
1531
|
+
"""Handle quantum PRINT expressions. Returns formatted string or None."""
|
|
1532
|
+
upper = text.strip().upper()
|
|
1533
|
+
# PRINT @REG — Dirac notation for LOCC register
|
|
1534
|
+
if self.locc_mode and self.locc and upper.startswith('@'):
|
|
1535
|
+
reg = upper[1:].strip()
|
|
1536
|
+
if reg in self.locc.names:
|
|
1537
|
+
sv = self.locc.get_sv(reg)
|
|
1538
|
+
n = self.locc.get_n(reg)
|
|
1539
|
+
return self._format_dirac(sv, n)
|
|
1540
|
+
# PRINT QUBIT(n) — single-qubit Bloch info
|
|
1541
|
+
m = re.match(r'QUBIT\s*\((\d+)\)', text, re.IGNORECASE)
|
|
1542
|
+
if m and self.last_sv is not None:
|
|
1543
|
+
q = int(m.group(1))
|
|
1544
|
+
x, y, z = self._bloch_vector(self.last_sv, q)
|
|
1545
|
+
p1 = self._peek(0x0100 + q * 8)
|
|
1546
|
+
return f"q{q}: P(1)={p1:.4f} Bloch=({x:.3f},{y:.3f},{z:.3f})"
|
|
1547
|
+
# PRINT ENTANGLEMENT(a,b) — entropy between qubits
|
|
1548
|
+
m = re.match(r'ENTANGLEMENT\s*\((\d+)\s*,\s*(\d+)\)', text, re.IGNORECASE)
|
|
1549
|
+
if m and self.last_sv is not None:
|
|
1550
|
+
qa, qb = int(m.group(1)), int(m.group(2))
|
|
1551
|
+
try:
|
|
1552
|
+
from qiskit.quantum_info import Statevector, entropy, partial_trace
|
|
1553
|
+
sv_obj = Statevector(np.ascontiguousarray(self.last_sv).ravel())
|
|
1554
|
+
keep = [qa]
|
|
1555
|
+
trace_out = [q for q in range(self.num_qubits) if q not in keep]
|
|
1556
|
+
rho = partial_trace(sv_obj, trace_out)
|
|
1557
|
+
ent = entropy(rho, base=2)
|
|
1558
|
+
return f"S({qa},{qb}) = {ent:.6f} bits"
|
|
1559
|
+
except Exception:
|
|
1560
|
+
return f"S({qa},{qb}) = ?"
|
|
1561
|
+
# PRINT STATE — full statevector in Dirac notation
|
|
1562
|
+
if upper == 'STATE' and self.last_sv is not None:
|
|
1563
|
+
return self._format_dirac(self.last_sv, self.num_qubits)
|
|
1564
|
+
return None
|
|
1565
|
+
|
|
1566
|
+
def _format_dirac(self, sv, n_qubits: int) -> str:
|
|
1567
|
+
"""Format a statevector in Dirac notation."""
|
|
1568
|
+
from qbasic_core.engine import AMPLITUDE_THRESHOLD
|
|
1569
|
+
sv_flat = np.ascontiguousarray(sv).ravel()
|
|
1570
|
+
parts = []
|
|
1571
|
+
for i, amp in enumerate(sv_flat):
|
|
1572
|
+
if abs(amp) > AMPLITUDE_THRESHOLD:
|
|
1573
|
+
state = format(i, f'0{n_qubits}b')
|
|
1574
|
+
if abs(amp.imag) < AMPLITUDE_THRESHOLD:
|
|
1575
|
+
if abs(amp.real - 1.0) < 1e-6:
|
|
1576
|
+
parts.append(f"|{state}\u27E9")
|
|
1577
|
+
elif abs(amp.real + 1.0) < 1e-6:
|
|
1578
|
+
parts.append(f"-|{state}\u27E9")
|
|
1579
|
+
else:
|
|
1580
|
+
parts.append(f"{amp.real:+.4f}|{state}\u27E9")
|
|
1581
|
+
else:
|
|
1582
|
+
parts.append(f"({amp.real:.3f}{amp.imag:+.3f}j)|{state}\u27E9")
|
|
1583
|
+
if len(parts) >= 16:
|
|
1584
|
+
parts.append("...")
|
|
1585
|
+
break
|
|
1586
|
+
return " ".join(parts) if parts else "|0\u27E9"
|
|
1587
|
+
|
|
1588
|
+
# cmd_state, cmd_hist, cmd_probs, cmd_bloch, cmd_circuit provided by StateDisplayMixin.
|
|
1589
|
+
|
|
1590
|
+
# cmd_noise provided by NoiseMixin.
|
|
1591
|
+
|
|
1592
|
+
# cmd_expect, cmd_entropy, cmd_csv, cmd_density provided by AnalysisMixin / FileIOMixin.
|
|
1593
|
+
|
|
1594
|
+
def _is_clifford(self, qc: 'QuantumCircuit') -> bool:
|
|
1595
|
+
"""Check if circuit contains only Clifford gates.
|
|
1596
|
+
|
|
1597
|
+
Custom/unitary gates are conservatively treated as non-Clifford.
|
|
1598
|
+
Verifying that an arbitrary unitary normalizes the Pauli group is
|
|
1599
|
+
expensive, and the consequence of a false positive (stabilizer sim
|
|
1600
|
+
producing wrong results) is worse than a false negative (falling
|
|
1601
|
+
back to statevector sim, which always works).
|
|
1602
|
+
"""
|
|
1603
|
+
clifford_gates = {'h', 'x', 'y', 'z', 's', 'sdg', 'cx', 'cz', 'cy',
|
|
1604
|
+
'swap', 'id', 'sx', 'barrier', 'measure', 'reset'}
|
|
1605
|
+
for inst in qc.data:
|
|
1606
|
+
name = inst.operation.name.lower()
|
|
1607
|
+
if name == 'unitary':
|
|
1608
|
+
return False # custom gates: conservatively non-Clifford
|
|
1609
|
+
if name not in clifford_gates:
|
|
1610
|
+
return False
|
|
1611
|
+
return True
|
|
1612
|
+
|
|
1613
|
+
# cmd_export provided by FileIOMixin.
|
|
1614
|
+
|
|
1615
|
+
def cmd_decompose(self) -> None:
|
|
1616
|
+
"""Show gate count breakdown of last circuit."""
|
|
1617
|
+
if self.last_circuit is None:
|
|
1618
|
+
self.io.writeln("?NO CIRCUIT — RUN first")
|
|
1619
|
+
return
|
|
1620
|
+
ops = {}
|
|
1621
|
+
for inst in self.last_circuit.data:
|
|
1622
|
+
name = inst.operation.name
|
|
1623
|
+
ops[name] = ops.get(name, 0) + 1
|
|
1624
|
+
self.io.writeln(f"\n Circuit: {self.last_circuit.num_qubits} qubits, "
|
|
1625
|
+
f"depth {self.last_circuit.depth()}, {self.last_circuit.size()} gates")
|
|
1626
|
+
for name, count in sorted(ops.items(), key=lambda x: -x[1]):
|
|
1627
|
+
bar = '█' * min(count, 40)
|
|
1628
|
+
self.io.writeln(f" {name:>10} {count:>4} {bar}")
|
|
1629
|
+
self.io.writeln('')
|
|
1630
|
+
|
|
1631
|
+
# cmd_include, cmd_sweep provided by FileIOMixin / SweepMixin.
|
|
1632
|
+
|
|
1633
|
+
# LOCC commands (cmd_locc, cmd_send, cmd_share) provided by LOCCMixin.
|
|
1634
|
+
|
|
1635
|
+
# cmd_bench, cmd_ram, cmd_dir provided by AnalysisMixin / FileIOMixin.
|
|
1636
|
+
|
|
1637
|
+
# cmd_clear provided by ProgramMgmtMixin.
|
|
1638
|
+
|
|
1639
|
+
def cmd_undo(self) -> None:
|
|
1640
|
+
if not self._undo_stack:
|
|
1641
|
+
self.io.writeln("NOTHING TO UNDO")
|
|
1642
|
+
return
|
|
1643
|
+
self.program = self._undo_stack.pop()
|
|
1644
|
+
self._parsed = {num: parse_stmt(s) for num, s in self.program.items()}
|
|
1645
|
+
self.io.writeln(f"UNDO ({len(self.program)} lines)")
|
|
1646
|
+
|
|
1647
|
+
# LOCC execution (_locc_run, _locc_exec_line, etc.) provided by LOCCMixin.
|
|
1648
|
+
|
|
1649
|
+
# Control flow helpers (_cf_*, _exec_control_flow, _find_matching_wend)
|
|
1650
|
+
# provided by ControlFlowMixin.
|
|
1651
|
+
|
|
1652
|
+
# ── Help ──────────────────────────────────────────────────────────
|
|
1653
|
+
|
|
1654
|
+
def cmd_help(self) -> None:
|
|
1655
|
+
self.io.writeln(HELP_TEXT)
|
|
1656
|
+
# Auto-generated command reference from registry
|
|
1657
|
+
all_cmds = sorted(set(self._CMD_NO_ARG.keys()) | set(self._CMD_WITH_ARG.keys()))
|
|
1658
|
+
all_gates = sorted(g for g in GATE_TABLE if g not in GATE_ALIASES)
|
|
1659
|
+
self.io.writeln(f" ALL COMMANDS: {', '.join(all_cmds)}")
|
|
1660
|
+
self.io.writeln(f" ALL GATES: {', '.join(all_gates)}")
|
|
1661
|
+
|
|
1662
|
+
# ── Banner ────────────────────────────────────────────────────────
|
|
1663
|
+
|
|
1664
|
+
def print_banner(self) -> None:
|
|
1665
|
+
import platform
|
|
1666
|
+
try:
|
|
1667
|
+
import qiskit
|
|
1668
|
+
qver = qiskit.__version__
|
|
1669
|
+
except Exception:
|
|
1670
|
+
qver = '?'
|
|
1671
|
+
ram = _get_ram_gb()
|
|
1672
|
+
ram_str = f"{ram[0]:.0f} GB RAM" if ram else "RAM unknown"
|
|
1673
|
+
max_q = 32
|
|
1674
|
+
if ram:
|
|
1675
|
+
from qbasic_core.engine import _estimate_gb
|
|
1676
|
+
for n in range(32, 0, -1):
|
|
1677
|
+
if _estimate_gb(n) < ram[1]:
|
|
1678
|
+
max_q = n
|
|
1679
|
+
break
|
|
1680
|
+
if hasattr(QBasicTerminal, '_gpu_cache'):
|
|
1681
|
+
gpu_str = QBasicTerminal._gpu_cache
|
|
1682
|
+
else:
|
|
1683
|
+
try:
|
|
1684
|
+
gpu_str = ""
|
|
1685
|
+
import subprocess
|
|
1686
|
+
r = subprocess.run(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'],
|
|
1687
|
+
capture_output=True, text=True, timeout=2)
|
|
1688
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
1689
|
+
gpu_str = f" | GPU: {r.stdout.strip().split(chr(10))[0]}"
|
|
1690
|
+
QBasicTerminal._gpu_cache = gpu_str
|
|
1691
|
+
except Exception:
|
|
1692
|
+
gpu_str = ""
|
|
1693
|
+
QBasicTerminal._gpu_cache = gpu_str
|
|
1694
|
+
info_line = f"Python {platform.python_version()} | Qiskit {qver} | {ram_str}{gpu_str}"
|
|
1695
|
+
config_line = f"{self.num_qubits} qubits | {self.shots} shots | max ~{max_q} qubits"
|
|
1696
|
+
self.io.writeln(BANNER_ART.format(info_line=info_line, config_line=config_line))
|
|
1697
|
+
|