qubasic 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. qbasic.py +113 -0
  2. qbasic_core/__init__.py +80 -0
  3. qbasic_core/__main__.py +6 -0
  4. qbasic_core/analysis.py +179 -0
  5. qbasic_core/backend.py +76 -0
  6. qbasic_core/classic.py +419 -0
  7. qbasic_core/control_flow.py +576 -0
  8. qbasic_core/debug.py +327 -0
  9. qbasic_core/demos.py +356 -0
  10. qbasic_core/display.py +274 -0
  11. qbasic_core/engine.py +126 -0
  12. qbasic_core/engine_state.py +109 -0
  13. qbasic_core/errors.py +37 -0
  14. qbasic_core/exec_context.py +24 -0
  15. qbasic_core/executor.py +861 -0
  16. qbasic_core/expression.py +228 -0
  17. qbasic_core/file_io.py +457 -0
  18. qbasic_core/gates.py +284 -0
  19. qbasic_core/help_text.py +167 -0
  20. qbasic_core/io_protocol.py +33 -0
  21. qbasic_core/locc.py +10 -0
  22. qbasic_core/locc_commands.py +221 -0
  23. qbasic_core/locc_display.py +61 -0
  24. qbasic_core/locc_engine.py +195 -0
  25. qbasic_core/locc_execution.py +389 -0
  26. qbasic_core/memory.py +369 -0
  27. qbasic_core/mock_backend.py +66 -0
  28. qbasic_core/noise_mixin.py +96 -0
  29. qbasic_core/parser.py +564 -0
  30. qbasic_core/patterns.py +186 -0
  31. qbasic_core/profiler.py +156 -0
  32. qbasic_core/program_mgmt.py +369 -0
  33. qbasic_core/protocol.py +77 -0
  34. qbasic_core/py.typed +0 -0
  35. qbasic_core/scope.py +74 -0
  36. qbasic_core/screen.py +115 -0
  37. qbasic_core/state_display.py +60 -0
  38. qbasic_core/statements.py +387 -0
  39. qbasic_core/strings.py +107 -0
  40. qbasic_core/subs.py +261 -0
  41. qbasic_core/sweep.py +82 -0
  42. qbasic_core/terminal.py +1697 -0
  43. qubasic-0.1.0.dist-info/METADATA +736 -0
  44. qubasic-0.1.0.dist-info/RECORD +48 -0
  45. qubasic-0.1.0.dist-info/WHEEL +5 -0
  46. qubasic-0.1.0.dist-info/entry_points.txt +2 -0
  47. qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
  48. qubasic-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,389 @@
1
+ """QBASIC LOCC execution mixin — program execution in LOCC mode."""
2
+
3
+ import re
4
+ import time
5
+
6
+ import numpy as np
7
+
8
+ from qbasic_core.engine import (
9
+ ExecResult,
10
+ GATE_TABLE, GATE_ALIASES,
11
+ _np_gate_matrix, _sample_one_np,
12
+ LOCC_SEND_SHOT_CAP, LOCC_SEND_QUBIT_THRESHOLD,
13
+ RE_CTRL, RE_INV,
14
+ )
15
+
16
+
17
+ class LOCCExecutionMixin:
18
+ """LOCC execution methods for QBasicTerminal.
19
+
20
+ Requires: TerminalProtocol — uses self.locc, self.program, self.shots,
21
+ self.variables, self._max_iterations, self._custom_gates,
22
+ self.eval_expr(), self._eval_with_vars(), self._expand_statement(),
23
+ self._tokenize_gate(), self._gate_info(), self._resolve_qubit(),
24
+ self._parse_syndrome(), self._split_colon_stmts(), self._exec_control_flow(),
25
+ self._get_parsed(), self._gosub_stack, self._locc_display_results(),
26
+ self.last_counts, self.io.
27
+ """
28
+
29
+ def _locc_run(self):
30
+ """Execute program in LOCC mode (N registers)."""
31
+ sorted_lines = sorted(self.program.keys())
32
+ if not sorted_lines:
33
+ self.io.writeln("NOTHING TO RUN")
34
+ return
35
+ has_measure = any(self.program[l].strip().upper() == 'MEASURE'
36
+ for l in sorted_lines)
37
+ has_send = any(re.search(r'\bSEND\b', self.program[l], re.IGNORECASE)
38
+ for l in sorted_lines)
39
+ if has_send:
40
+ self._locc_run_with_send(sorted_lines, has_measure)
41
+ else:
42
+ self._locc_run_vectorized(sorted_lines, has_measure)
43
+
44
+ def _locc_run_with_send(self, sorted_lines, has_measure):
45
+ """LOCC execution with SEND — prefix/suffix split optimization.
46
+
47
+ Executes the deterministic prefix (before first SEND) once,
48
+ snapshots the quantum state, then re-executes only the suffix
49
+ (from first SEND onward) per shot. Falls back to full re-execution
50
+ if the prefix contains jumps that could skip past the SEND.
51
+ """
52
+ from qbasic_core.scope import Scope
53
+ from qbasic_core.statements import SendStmt, GotoStmt, GosubStmt
54
+
55
+ # Check if prefix contains jumps — if so, fall back to full re-exec
56
+ first_send_ip = None
57
+ for i, ln in enumerate(sorted_lines):
58
+ p = self._get_parsed(ln)
59
+ if isinstance(p, SendStmt):
60
+ first_send_ip = i
61
+ break
62
+ has_prefix_jumps = False
63
+ if first_send_ip is not None and first_send_ip > 0:
64
+ for i in range(first_send_ip):
65
+ p = self._get_parsed(sorted_lines[i])
66
+ if isinstance(p, (GotoStmt, GosubStmt)):
67
+ has_prefix_jumps = True
68
+ break
69
+
70
+ sizes_str = '+'.join(str(s) for s in self.locc.sizes)
71
+ mode = "JOINT" if self.locc.joint else "SPLIT"
72
+ shots = self.shots
73
+ max_q = max(self.locc.sizes)
74
+ if max_q > LOCC_SEND_QUBIT_THRESHOLD and shots > LOCC_SEND_SHOT_CAP:
75
+ self.io.writeln(f" WARNING: capping at {LOCC_SEND_SHOT_CAP} shots for "
76
+ f"{max_q}-qubit LOCC w/ SEND (per-shot re-execution)")
77
+ shots = LOCC_SEND_SHOT_CAP
78
+
79
+ # Execute deterministic prefix once (skip if jumps make it unsafe)
80
+ self.locc.reset()
81
+ if has_prefix_jumps or first_send_ip == 0 or first_send_ip is None:
82
+ send_ip = 0 # no safe prefix — re-exec from start
83
+ else:
84
+ prefix_vars = dict(self.variables)
85
+ try:
86
+ send_ip = self._locc_execute_program(sorted_lines, stop_before_send=True,
87
+ run_vars=Scope(prefix_vars))
88
+ except (RuntimeError, ValueError) as e:
89
+ self.io.writeln(f"?RUNTIME ERROR: {e}")
90
+ return
91
+
92
+ # Snapshot state after prefix
93
+ snap = self.locc.snapshot()
94
+ snap_vars = dict(self.variables)
95
+
96
+ per_reg = {name: {} for name in self.locc.names}
97
+ counts_joint = {}
98
+ t0 = time.time()
99
+ progress_interval = max(1, shots // 10)
100
+
101
+ for shot in range(shots):
102
+ # Restore to post-prefix state
103
+ self.locc.restore(snap)
104
+ self.variables.clear()
105
+ self.variables.update(snap_vars)
106
+ try:
107
+ self._locc_execute_program(sorted_lines, start_ip=send_ip,
108
+ run_vars=Scope(self.variables))
109
+ except (RuntimeError, ValueError) as e:
110
+ self.io.writeln(f"?RUNTIME ERROR: {e}")
111
+ return
112
+ if has_measure:
113
+ if self.locc.joint:
114
+ out = _sample_one_np(self.locc.sv, self.locc.n_total)
115
+ parts = []
116
+ pos = len(out)
117
+ for i in range(self.locc.n_regs):
118
+ size = self.locc.sizes[i]
119
+ parts.append(out[pos - size:pos])
120
+ pos -= size
121
+ else:
122
+ parts = [_sample_one_np(self.locc.svs[name],
123
+ self.locc.get_size(name))
124
+ for name in self.locc.names]
125
+ for name, bits in zip(self.locc.names, parts):
126
+ per_reg[name][bits] = per_reg[name].get(bits, 0) + 1
127
+ jkey = '|'.join(parts)
128
+ counts_joint[jkey] = counts_joint.get(jkey, 0) + 1
129
+ if shots > 50 and (shot + 1) % progress_interval == 0:
130
+ pct = 100 * (shot + 1) // shots
131
+ self.io.write(f" {pct}% ({shot+1}/{shots} shots)...\r")
132
+
133
+ if shots > 50:
134
+ self.io.write(" " * 40 + '\r')
135
+ dt = time.time() - t0
136
+ self.io.writeln(f"\nRAN {len(self.program)} lines, LOCC {mode} "
137
+ f"{sizes_str}q, {shots} shots in {dt:.2f}s")
138
+ if has_measure:
139
+ self._locc_display_results(per_reg, counts_joint)
140
+ self.last_counts = counts_joint
141
+
142
+ def _locc_run_vectorized(self, sorted_lines, has_measure):
143
+ """LOCC execution without SEND — single execution, vectorized sampling."""
144
+ sizes_str = '+'.join(str(s) for s in self.locc.sizes)
145
+ mode = "JOINT" if self.locc.joint else "SPLIT"
146
+ t0 = time.time()
147
+
148
+ self.locc.reset()
149
+ try:
150
+ self._locc_execute_program(sorted_lines)
151
+ except (RuntimeError, ValueError) as e:
152
+ self.io.writeln(f"?RUNTIME ERROR: {e}")
153
+ return
154
+
155
+ if has_measure:
156
+ per_reg, cj = self.locc.sample_joint(self.shots)
157
+ dt = time.time() - t0
158
+ self.io.writeln(f"\nRAN {len(self.program)} lines, LOCC {mode} "
159
+ f"{sizes_str}q, {self.shots} shots in {dt:.2f}s")
160
+ self._locc_display_results(per_reg, cj)
161
+ if not cj:
162
+ self.io.writeln(f"\n (SPLIT mode, no SEND — registers are independent)")
163
+ self.last_counts = cj if cj else per_reg.get('A', {})
164
+ else:
165
+ dt = time.time() - t0
166
+ self.io.writeln(f"\nRAN {len(self.program)} lines, LOCC in {dt:.2f}s")
167
+ regs = ', '.join(self.locc.names)
168
+ self.io.writeln(f"(no MEASURE — use STATE {regs} to inspect)")
169
+
170
+ def _locc_execute_program(self, sorted_lines, *, start_ip: int = 0,
171
+ run_vars=None, stop_before_send: bool = False) -> int:
172
+ """Execute LOCC program lines.
173
+
174
+ Returns the ip where execution stopped (end of program or at first SEND
175
+ if stop_before_send is True).
176
+ """
177
+ from qbasic_core.scope import Scope
178
+ from qbasic_core.exec_context import ExecContext
179
+ from qbasic_core.statements import MeasureStmt, SendStmt
180
+ if run_vars is None:
181
+ run_vars = Scope(self.variables)
182
+ ctx = ExecContext(
183
+ sorted_lines=sorted_lines, ip=start_ip, run_vars=run_vars,
184
+ max_iterations=self._max_iterations,
185
+ )
186
+ _parsed = [self._get_parsed(ln) for ln in sorted_lines]
187
+ _stmts = [self.program[ln].strip() for ln in sorted_lines]
188
+ n_lines = len(sorted_lines)
189
+ while ctx.ip < n_lines:
190
+ ctx.iteration_count += 1
191
+ if ctx.iteration_count > ctx.max_iterations:
192
+ raise RuntimeError(f"LOOP LIMIT ({ctx.max_iterations}) — possible infinite loop")
193
+ if isinstance(_parsed[ctx.ip], MeasureStmt):
194
+ ctx.ip += 1
195
+ continue
196
+ if stop_before_send and isinstance(_parsed[ctx.ip], SendStmt):
197
+ return ctx.ip
198
+ try:
199
+ result = self._locc_exec_line(_stmts[ctx.ip], ctx.loop_stack, sorted_lines, ctx.ip, run_vars, parsed=_parsed[ctx.ip])
200
+ except Exception as e:
201
+ raise RuntimeError(f"LINE {sorted_lines[ctx.ip]}: {e}") from None
202
+ if result is ExecResult.END:
203
+ break
204
+ elif isinstance(result, int):
205
+ ctx.ip = result
206
+ else:
207
+ ctx.ip += 1
208
+ return ctx.ip
209
+
210
+ def _locc_exec_line(self, stmt, loop_stack, sorted_lines, ip, run_vars, *, parsed=None):
211
+ """Execute one line in LOCC mode."""
212
+ from qbasic_core.parser import parse_stmt
213
+ from qbasic_core.statements import (
214
+ SendStmt, ShareStmt, AtRegStmt, CompoundStmt,
215
+ RemStmt, MeasureStmt, EndStmt, BarrierStmt, ReturnStmt,
216
+ )
217
+ if parsed is None:
218
+ parsed = parse_stmt(stmt)
219
+
220
+ # Fast-path for terminals
221
+ if isinstance(parsed, (RemStmt, MeasureStmt, BarrierStmt)):
222
+ return ExecResult.ADVANCE
223
+ if isinstance(parsed, EndStmt):
224
+ return ExecResult.END
225
+ if isinstance(parsed, ReturnStmt):
226
+ if not self._gosub_stack:
227
+ raise RuntimeError("RETURN WITHOUT GOSUB")
228
+ return self._gosub_stack.pop()
229
+ if isinstance(parsed, CompoundStmt):
230
+ for sub in parsed.parts:
231
+ self._locc_exec_line(sub, loop_stack, sorted_lines, ip, run_vars)
232
+ return ExecResult.ADVANCE
233
+
234
+ # Fast-path for LOCC-specific statements
235
+ if isinstance(parsed, SendStmt):
236
+ qubit = int(self.eval_expr(parsed.qubit_expr))
237
+ outcome = self.locc.send(parsed.reg, qubit)
238
+ run_vars[parsed.var] = outcome
239
+ self.variables[parsed.var] = outcome
240
+ self.locc.classical[parsed.var] = outcome
241
+ return ExecResult.ADVANCE
242
+ if isinstance(parsed, ShareStmt):
243
+ self.locc.share(parsed.reg1, parsed.q1, parsed.reg2, parsed.q2)
244
+ return ExecResult.ADVANCE
245
+ if isinstance(parsed, AtRegStmt):
246
+ if self._locc_try_special(parsed.reg, parsed.inner, run_vars):
247
+ return ExecResult.ADVANCE
248
+ self._locc_apply_gate(parsed.reg, parsed.inner)
249
+ return ExecResult.ADVANCE
250
+
251
+ # Control flow
252
+ handled, result = self._exec_control_flow(
253
+ stmt, loop_stack, sorted_lines, ip, run_vars,
254
+ lambda s, ls, sl, i, rv: self._locc_exec_line(s, ls, sl, i, rv),
255
+ parsed=parsed)
256
+ if handled:
257
+ return result
258
+
259
+ if ':' in stmt:
260
+ for sub in self._split_colon_stmts(stmt):
261
+ self._locc_exec_line(sub, loop_stack, sorted_lines, ip, run_vars)
262
+ return ExecResult.ADVANCE
263
+
264
+ raise ValueError(f"LOCC mode requires @A/@B prefix, SEND, SHARE, or IF: {stmt}")
265
+
266
+ def _locc_try_special(self, reg, stmt, run_vars):
267
+ """Handle MEAS/RESET/MEASURE_X/Y/Z/SYNDROME in LOCC mode."""
268
+ from qbasic_core.parser import parse_stmt
269
+ from qbasic_core.statements import MeasStmt, ResetStmt, MeasureBasisStmt, SyndromeStmt
270
+ p = parse_stmt(stmt)
271
+
272
+ if isinstance(p, MeasStmt):
273
+ qubit = int(self._eval_with_vars(p.qubit_expr, run_vars))
274
+ outcome = self.locc.send(reg, qubit)
275
+ run_vars[p.var] = outcome
276
+ self.variables[p.var] = outcome
277
+ self.locc.classical[p.var] = outcome
278
+ return True
279
+
280
+ if isinstance(p, ResetStmt):
281
+ qubit = int(self._eval_with_vars(p.qubit_expr, run_vars))
282
+ outcome = self.locc.send(reg, qubit)
283
+ if outcome == 1:
284
+ self.locc.apply(reg, 'X', (), [qubit])
285
+ return True
286
+
287
+ if isinstance(p, MeasureBasisStmt):
288
+ qubit = int(self._eval_with_vars(p.qubit_expr, run_vars))
289
+ if p.basis == 'X':
290
+ self.locc.apply(reg, 'H', (), [qubit])
291
+ elif p.basis == 'Y':
292
+ self.locc.apply(reg, 'SDG', (), [qubit])
293
+ self.locc.apply(reg, 'H', (), [qubit])
294
+ outcome = self.locc.send(reg, qubit)
295
+ var = f"m{p.basis.lower()}_{qubit}"
296
+ run_vars[var] = outcome
297
+ self.variables[var] = outcome
298
+ self.locc.classical[var] = outcome
299
+ return True
300
+
301
+ parsed = self._parse_syndrome(stmt, run_vars)
302
+ if parsed is not None:
303
+ pauli_str, qubits, var = parsed
304
+ n_reg = self.locc.get_size(reg)
305
+ anc = n_reg - 1
306
+ if anc in qubits:
307
+ raise ValueError(
308
+ f"Qubit {anc} needed as ancilla but appears in stabilizer. "
309
+ f"Increase register size by 1.")
310
+ self.locc.apply(reg, 'H', (), [anc])
311
+ for p, q in zip(pauli_str, qubits):
312
+ if p != 'I':
313
+ self.locc.apply(reg, self._PAULI_TO_CONTROLLED[p], (), [anc, q])
314
+ self.locc.apply(reg, 'H', (), [anc])
315
+ outcome = self.locc.send(reg, anc)
316
+ if outcome == 1:
317
+ self.locc.apply(reg, 'X', (), [anc])
318
+ run_vars[var] = outcome
319
+ self.variables[var] = outcome
320
+ self.locc.classical[var] = outcome
321
+ return True
322
+
323
+ return False
324
+
325
+ def _locc_apply_gate(self, reg, gate_stmt):
326
+ """Parse and apply a gate to a LOCC register via numpy.
327
+
328
+ Uses LOCCRegBackend for standard gates. CTRL and INV modifiers
329
+ use apply_matrix directly since they need matrix construction.
330
+ """
331
+ from qbasic_core.backend import LOCCRegBackend
332
+ backend = LOCCRegBackend(self.locc, reg)
333
+ n_reg = self.locc.get_size(reg)
334
+ expanded = self._expand_statement(gate_stmt)
335
+ for stmt in expanded:
336
+ tokens = self._tokenize_gate(stmt)
337
+ if not tokens:
338
+ continue
339
+ gate_name = tokens[0].upper()
340
+ gate_name = GATE_ALIASES.get(gate_name, gate_name)
341
+ if gate_name == 'BARRIER':
342
+ continue
343
+
344
+ # CTRL modifier
345
+ m_ctrl = RE_CTRL.match(stmt)
346
+ if m_ctrl:
347
+ inner_name = GATE_ALIASES.get(m_ctrl.group(1).upper(), m_ctrl.group(1).upper())
348
+ args = [a.strip() for a in m_ctrl.group(2).replace(',', ' ').split()]
349
+ ctrl_qubit = self._resolve_qubit(args[0], n_qubits=n_reg)
350
+ target_qubits = [self._resolve_qubit(a, n_qubits=n_reg) for a in args[1:]]
351
+ inner_matrix = _np_gate_matrix(inner_name, ())
352
+ dim = inner_matrix.shape[0]
353
+ n_inner_qubits = int(np.log2(dim))
354
+ if len(target_qubits) != n_inner_qubits:
355
+ raise ValueError(
356
+ f"CTRL {inner_name} needs 1 control + {n_inner_qubits} "
357
+ f"target qubit(s), got {len(target_qubits)} target(s)")
358
+ controlled = np.eye(2 * dim, dtype=complex)
359
+ controlled[dim:, dim:] = inner_matrix
360
+ self.locc.apply_matrix(reg, controlled, [ctrl_qubit] + target_qubits)
361
+ continue
362
+
363
+ # INV modifier
364
+ m_inv = RE_INV.match(stmt)
365
+ if m_inv:
366
+ inner_name = GATE_ALIASES.get(m_inv.group(1).upper(), m_inv.group(1).upper())
367
+ inv_args = [a.strip() for a in m_inv.group(2).replace(',', ' ').split()]
368
+ info = self._gate_info(inner_name)
369
+ if info is None:
370
+ raise ValueError(f"UNKNOWN GATE: {inner_name}")
371
+ n_params, n_qubits_needed = info
372
+ params = [self.eval_expr(a) for a in inv_args[:n_params]]
373
+ qubits = [self._resolve_qubit(a, n_qubits=n_reg) for a in inv_args[n_params:n_params+n_qubits_needed]]
374
+ matrix = _np_gate_matrix(inner_name, tuple(params)).conj().T
375
+ self.locc.apply_matrix(reg, matrix, qubits)
376
+ continue
377
+
378
+ # Standard gate — through backend
379
+ info = self._gate_info(gate_name)
380
+ if info is None:
381
+ raise ValueError(f"UNKNOWN GATE: {gate_name}")
382
+ n_params, n_qubits_needed = info
383
+ args = tokens[1:]
384
+ if len(args) < n_params + n_qubits_needed:
385
+ raise ValueError(f"{gate_name} needs {n_params} params + "
386
+ f"{n_qubits_needed} qubits")
387
+ params = tuple(self.eval_expr(a) for a in args[:n_params])
388
+ qubits = [self._resolve_qubit(a, n_qubits=n_reg) for a in args[n_params:n_params+n_qubits_needed]]
389
+ backend.apply_gate(gate_name, params, qubits)