qubasic 0.3.1__tar.gz → 0.4.1__tar.gz

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 (59) hide show
  1. qubasic-0.4.1/CHANGELOG.md +80 -0
  2. {qubasic-0.3.1 → qubasic-0.4.1}/LICENSE +1 -1
  3. {qubasic-0.3.1/qubasic.egg-info → qubasic-0.4.1}/PKG-INFO +1 -1
  4. {qubasic-0.3.1 → qubasic-0.4.1}/pyproject.toml +4 -1
  5. {qubasic-0.3.1 → qubasic-0.4.1/qubasic.egg-info}/PKG-INFO +1 -1
  6. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.py +17 -0
  7. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/__init__.py +1 -1
  8. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/analysis.py +95 -13
  9. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/classic.py +3 -0
  10. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/control_flow.py +5 -3
  11. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/demos.py +65 -2
  12. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/engine_state.py +2 -0
  13. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/executor.py +1 -2
  14. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/expression.py +9 -25
  15. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/locc_commands.py +58 -6
  16. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/locc_display.py +3 -3
  17. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/locc_engine.py +44 -3
  18. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/locc_execution.py +50 -6
  19. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/memory.py +12 -2
  20. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/mock_backend.py +2 -0
  21. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/noise_mixin.py +24 -0
  22. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/parser.py +2 -2
  23. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/profiler.py +6 -0
  24. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/scope.py +0 -1
  25. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/state_display.py +16 -14
  26. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/subs.py +25 -1
  27. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/sweep.py +1 -1
  28. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/terminal.py +553 -208
  29. qubasic-0.3.1/CHANGELOG.md +0 -34
  30. {qubasic-0.3.1 → qubasic-0.4.1}/MANIFEST.in +0 -0
  31. {qubasic-0.3.1 → qubasic-0.4.1}/README.md +0 -0
  32. {qubasic-0.3.1 → qubasic-0.4.1}/examples/bell.qb +0 -0
  33. {qubasic-0.3.1 → qubasic-0.4.1}/examples/grover3.qb +0 -0
  34. {qubasic-0.3.1 → qubasic-0.4.1}/examples/locc_teleport.qb +0 -0
  35. {qubasic-0.3.1 → qubasic-0.4.1}/examples/sweep_rx.qb +0 -0
  36. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.egg-info/SOURCES.txt +0 -0
  37. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.egg-info/dependency_links.txt +0 -0
  38. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.egg-info/entry_points.txt +0 -0
  39. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.egg-info/requires.txt +0 -0
  40. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.egg-info/top_level.txt +0 -0
  41. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/__main__.py +0 -0
  42. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/backend.py +0 -0
  43. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/debug.py +0 -0
  44. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/display.py +0 -0
  45. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/engine.py +0 -0
  46. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/errors.py +0 -0
  47. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/exec_context.py +0 -0
  48. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/file_io.py +0 -0
  49. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/gates.py +0 -0
  50. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/help_text.py +0 -0
  51. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/io_protocol.py +0 -0
  52. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/locc.py +0 -0
  53. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/patterns.py +0 -0
  54. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/program_mgmt.py +0 -0
  55. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/protocol.py +0 -0
  56. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/screen.py +0 -0
  57. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/statements.py +0 -0
  58. {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/strings.py +0 -0
  59. {qubasic-0.3.1 → qubasic-0.4.1}/setup.cfg +0 -0
@@ -0,0 +1,80 @@
1
+ # Changelog
2
+
3
+ ## 0.4.1 (2026-03-30)
4
+
5
+ - **Fix SEED dispatch**: moved from no-arg to with-arg dispatch table so `SEED 42` works from the REPL
6
+ - **Fix GPU probe**: `cmd_method` GPU probe used undefined `_pqc` variable; now uses `_pqc_m`
7
+ - **Fix LOCC non-numeric args**: `LOCC 4 banana` no longer crashes with unguarded ValueError
8
+ - **Coverage threshold**: raised CI coverage floor from 60% to 75%
9
+ - **Property-based tests**: 4 new hypothesis tests (arithmetic identity, parser fuzzing, process fuzzing, FOR loop count)
10
+ - **CLI integration tests**: 8 new tests covering dispatch, SEED, LOCC error handling, METHOD probe, --seed flag, --help
11
+ - **Parser imports**: import regexes from patterns.py directly instead of double-indirection through engine.py
12
+ - **Expression simplification**: `_replace_dollar_outside_strings` reduced from two passes to one
13
+ - **Jump table**: pre-compute WHILE/WEND and DO/LOOP ip mappings in `_scan_subs`; `_find_matching_wend` and `_find_matching_loop` use O(1) lookup when available
14
+ - **Scope cleanup**: removed dual-write hack in `Scope.__setitem__`; writes go to `_runtime` only, `_persistent` is read-through fallback
15
+ - **STATS output**: redirect stdout during stats runs to suppress rich console output in non-TTY mode
16
+ - **Copyright year**: LICENSE updated from 2025 to 2026
17
+ - **CLI --seed flag**: `qubasic --seed N script.qb` sets deterministic seed before execution
18
+ - **Type annotations**: added return type annotations to 14 unannotated mixin methods across locc_commands, locc_display, locc_execution, demos
19
+
20
+ ## 0.4.0 (2026-03-29)
21
+
22
+ - **Noise correctness**: transpile with optimization_level=0 when noisy so gates survive for noise attachment
23
+ - **Noisy statevector**: STATE/BLOCH/DENSITY now reflect the noisy executed state, not the ideal state
24
+ - **LOCC noise**: Monte Carlo depolarizing noise in the numpy LOCC engine with per-shot execution
25
+ - **GPU**: _make_backend centralizes device flag to all execution paths; graceful probe and fallback
26
+ - **cmd_run decomposition**: extracted _run_no_measure, _run_with_fallback, _extract_statevector, _finalize_run, _select_method, _build_backend_opts, _run_kwargs
27
+ - **State consistency**: _active_sv/_active_nqubits unify LOCC and standard paths for all state commands
28
+ - **SPLIT mode**: EXPECT/DENSITY correctly report that per-register commands are needed
29
+ - **Non-depolarizing noise warning**: entering LOCC mode with unsupported noise types warns explicitly
30
+ - SEED command for deterministic reproducible results
31
+ - VERSION command with build ID, simulator versions, and feature flags
32
+ - PROBE command: one-shot exercise of CPU, noise, LOCC, conditional, and combined paths
33
+ - CONSISTENCY command: cross-check SV norm, purity, Bloch vectors, EXPECT, and histogram
34
+ - METHOD capability map: real probing of each method and GPU availability
35
+ - HELP STATUS: tags all 93 commands as native/experimental/partial
36
+ - CATALOG shows backend behind each SYS routine
37
+ - RUN prints method, device, noise params in summary line
38
+ - Demo self-verification: Bell, GHZ, Grover, Deutsch, BV, Superdense auto-check with pass/fail thresholds
39
+ - Teleportation fidelity output with X-basis verification
40
+ - LOCCINFO: entanglement creation, correction log, branch statistics, noise status
41
+ - Method-device pre-check blocks incompatible combinations before execution
42
+ - Runtime errors identify failing subsystem (GPU/noise/stabilizer/MPS)
43
+ - Run manifest captures all execution parameters for replay
44
+ - Correction log in LOCC engine tracks SEND outcomes
45
+ - NOISE INFO prints exact channels, operations, qubits
46
+ - 25 new tests covering noise, LOCC noise, SEED, VERSION, PROBE, CONSISTENCY, demos, state-after-LOCC, manifest, and method-device pre-check (196 total, up from 171)
47
+ - real_sim pytest marker for tests requiring actual Qiskit Aer simulation
48
+
49
+ ## 0.3.1 (2026-03-29)
50
+
51
+ - Fix f-string backslash escapes that broke import on Python 3.10/3.11
52
+
53
+ ## 0.3.0 (2026-03-28)
54
+
55
+ - FUNCTION return value fix, APPLY_CIRCUIT in programs, stabilizer fallback
56
+ - Bump to 0.3.0
57
+
58
+ ## 0.2.0 (2026-03-28)
59
+
60
+ - Rename qbasic -> qubasic everywhere (PyPI name conflict)
61
+
62
+ ## 0.1.0 (2026-03-28)
63
+
64
+ Initial PyPI release.
65
+
66
+ - BASIC REPL with line-numbered program editing
67
+ - 30+ quantum gates (H, X, Y, Z, CX, CCX, RX, RY, RZ, CP, SWAP, etc.)
68
+ - LOCC mode: 2-26 party distributed quantum simulation (SPLIT and JOINT)
69
+ - Full BASIC language: FOR/NEXT, WHILE/WEND, DO/LOOP, SELECT CASE, SUB/FUNCTION, IF/THEN/ELSE
70
+ - AST-based expression evaluator (no eval)
71
+ - Noise models: depolarizing, amplitude damping, phase flip, thermal, readout, combined, Pauli, reset
72
+ - Debugging: STEP, TRON/TROFF, breakpoints, watch, time-travel (REWIND/FORWARD), PROFILE
73
+ - Memory map: PEEK/POKE/SYS/DUMP/MAP/MONITOR
74
+ - Analysis: EXPECT, ENTROPY, DENSITY, SWEEP, BENCH, RAM
75
+ - File I/O: SAVE/LOAD/INCLUDE/IMPORT, OPEN/CLOSE/PRINT#/INPUT#, CSV, OpenQASM 3.0 export
76
+ - 12 built-in demos: Bell, GHZ, Grover, QFT, Deutsch-Jozsa, Bernstein-Vazirani, Superdense, Teleport, LOCC
77
+ - JSON output mode for agent/pipeline integration
78
+ - String variable resolution in PRINT (LEFT$, RIGHT$, CHR$, concatenation)
79
+ - DEF FN and parameterized DEF subroutine invocation
80
+ - Trusted publisher CI/CD to PyPI
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Charles C. Norton
3
+ Copyright (c) 2026 Charles C. Norton
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.3.1"
7
+ version = "0.4.1"
8
8
  description = "Quantum BASIC Interactive Terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -53,6 +53,9 @@ packages = ["qubasic_core"]
53
53
  [tool.setuptools.data-files]
54
54
  "share/qubasic/examples" = ["examples/*.qb"]
55
55
 
56
+ [tool.pytest.ini_options]
57
+ markers = ["real_sim: test requires real Qiskit Aer simulation (no mock)"]
58
+
56
59
  [tool.coverage.run]
57
60
  source = ["qubasic_core"]
58
61
  omit = ["*/test_*"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -53,10 +53,22 @@ def main():
53
53
  args = sys.argv[1:]
54
54
  quiet = '--quiet' in args or '-q' in args
55
55
  json_mode = '--json' in args
56
+ seed_val = None
56
57
  if quiet:
57
58
  args = [a for a in args if a not in ('--quiet', '-q')]
58
59
  if json_mode:
59
60
  args = [a for a in args if a != '--json']
61
+ # Parse --seed N
62
+ filtered = []
63
+ i = 0
64
+ while i < len(args):
65
+ if args[i] == '--seed' and i + 1 < len(args):
66
+ seed_val = int(args[i + 1])
67
+ i += 2
68
+ else:
69
+ filtered.append(args[i])
70
+ i += 1
71
+ args = filtered
60
72
 
61
73
  if any(a in ('-h', '--help') for a in args):
62
74
  from qubasic_core import __version__
@@ -67,12 +79,17 @@ def main():
67
79
  print(" qubasic script.qb Run a script file")
68
80
  print(" qubasic --quiet script Suppress banner and progress")
69
81
  print(" qubasic --json script Output results as JSON")
82
+ print(" qubasic --seed N script Set random seed for reproducibility")
70
83
  print(" qubasic --help Show this help")
71
84
  print()
72
85
  print("Type HELP inside the REPL for full command reference.")
73
86
  sys.exit(0)
74
87
 
75
88
  term = QBasicTerminal()
89
+ if seed_val is not None:
90
+ import numpy as _np
91
+ term._seed = seed_val
92
+ _np.random.seed(seed_val)
76
93
 
77
94
  if args:
78
95
  path = args[0]
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.3.1'
31
+ __version__ = '0.4.1'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -25,8 +25,12 @@ class AnalysisMixin:
25
25
  def cmd_expect(self, rest: str) -> None:
26
26
  """EXPECT <pauli> [qubits] — compute expectation value.
27
27
  Examples: EXPECT Z 0, EXPECT ZZ 0 1, EXPECT X 0"""
28
- if self.last_sv is None:
29
- self.io.writeln("?NO STATE RUN first")
28
+ sv = self._active_sv
29
+ if sv is None:
30
+ if self.locc_mode and self.locc and not self.locc.joint:
31
+ self.io.writeln("?SPLIT mode: use STATE A / STATE B for per-register inspection")
32
+ else:
33
+ self.io.writeln("?NO STATE — RUN first")
30
34
  return
31
35
  parts = rest.split()
32
36
  if not parts:
@@ -35,16 +39,16 @@ class AnalysisMixin:
35
39
  pauli_str = parts[0].upper()
36
40
  qubits = [int(q) for q in parts[1:]] if len(parts) > 1 else list(range(len(pauli_str)))
37
41
 
42
+ n = self._active_nqubits
38
43
  try:
39
44
  from qiskit.quantum_info import Statevector, SparsePauliOp
40
- sv = Statevector(np.ascontiguousarray(self.last_sv).ravel())
41
- # Build Pauli string for full system
42
- full_pauli = ['I'] * self.num_qubits
45
+ sv_q = Statevector(np.ascontiguousarray(sv).ravel())
46
+ full_pauli = ['I'] * n
43
47
  for i, p in enumerate(pauli_str):
44
48
  if i < len(qubits):
45
- full_pauli[self.num_qubits - 1 - qubits[i]] = p
49
+ full_pauli[n - 1 - qubits[i]] = p
46
50
  op = SparsePauliOp(''.join(full_pauli))
47
- val = sv.expectation_value(op)
51
+ val = sv_q.expectation_value(op)
48
52
  self.io.writeln(f" <{pauli_str}> on qubits {qubits} = {val.real:.6f}")
49
53
  except Exception as e:
50
54
  self.io.writeln(f"?EXPECT ERROR: {e}")
@@ -52,17 +56,18 @@ class AnalysisMixin:
52
56
  def cmd_entropy(self, rest: str = '') -> None:
53
57
  """ENTROPY [qubits] — entanglement entropy of specified qubits vs rest.
54
58
  Examples: ENTROPY 0 | ENTROPY 0 1 | ENTROPY (defaults to qubit 0)"""
55
- if self.last_sv is None:
59
+ sv = self._active_sv
60
+ if sv is None:
56
61
  self.io.writeln("?NO STATE — RUN first")
57
62
  return
58
63
  if rest.strip():
59
64
  partition_a = [int(q) for q in rest.replace(',', ' ').split() if q.strip()]
60
65
  else:
61
66
  partition_a = [0]
62
- n = self.num_qubits
67
+ n = self._active_nqubits
63
68
  try:
64
69
  from qiskit.quantum_info import Statevector, entropy, partial_trace
65
- sv_obj = Statevector(np.ascontiguousarray(self.last_sv).ravel())
70
+ sv_obj = Statevector(np.ascontiguousarray(sv).ravel())
66
71
  keep = partition_a
67
72
  rho_a = partial_trace(sv_obj, [q for q in range(n) if q not in keep])
68
73
  ent = entropy(rho_a, base=2)
@@ -78,12 +83,13 @@ class AnalysisMixin:
78
83
 
79
84
  def cmd_density(self) -> None:
80
85
  """Show density matrix (or partial trace for small systems)."""
81
- if self.last_sv is None:
86
+ sv = self._active_sv
87
+ if sv is None:
82
88
  self.io.writeln("?NO STATE — RUN first")
83
89
  return
84
- sv = np.ascontiguousarray(self.last_sv).ravel()
90
+ sv = np.ascontiguousarray(sv).ravel()
85
91
  rho = np.outer(sv, sv.conj())
86
- n = self.num_qubits
92
+ n = self._active_nqubits
87
93
  dim = 2**n
88
94
  if dim > 16:
89
95
  self.io.writeln(f" Density matrix: {dim}x{dim} (too large to display)")
@@ -140,6 +146,82 @@ class AnalysisMixin:
140
146
  self.io.writeln(f" {n:>8} {method:>20} FAILED: {e}")
141
147
  self.io.writeln('')
142
148
 
149
+ def cmd_consistency(self, rest: str = '') -> None:
150
+ """CONSISTENCY — cross-check histogram, SV, density, Bloch, and EXPECT."""
151
+ sv = self._active_sv
152
+ if sv is None:
153
+ self.io.writeln("?NO STATE — RUN first")
154
+ return
155
+ n = self._active_nqubits
156
+ sv = np.ascontiguousarray(sv).ravel()
157
+ checks = []
158
+
159
+ # 1. SV normalization
160
+ norm = float(np.sum(np.abs(sv)**2))
161
+ ok = abs(norm - 1.0) < 1e-6
162
+ checks.append(('SV norm == 1', ok, f"{norm:.8f}"))
163
+
164
+ # 2. Density matrix purity
165
+ rho = np.outer(sv, sv.conj())
166
+ purity = float(np.real(np.trace(rho @ rho)))
167
+ ok2 = purity <= 1.0 + 1e-6
168
+ checks.append(('Purity <= 1', ok2, f"{purity:.8f}"))
169
+
170
+ # 3. Bloch vector length <= 1 for each qubit
171
+ bloch_ok = True
172
+ for q in range(min(n, 8)):
173
+ x, y, z = self._bloch_vector(sv, q, n)
174
+ r = (x**2 + y**2 + z**2) ** 0.5
175
+ if r > 1.0 + 1e-4:
176
+ bloch_ok = False
177
+ break
178
+ checks.append(('Bloch |r| <= 1', bloch_ok, ''))
179
+
180
+ # 4. EXPECT Z on qubit 0 matches P(0) - P(1)
181
+ try:
182
+ from qiskit.quantum_info import Statevector, SparsePauliOp
183
+ sv_q = Statevector(sv)
184
+ pauli_z = ['I'] * n
185
+ pauli_z[n - 1] = 'Z'
186
+ op = SparsePauliOp(''.join(pauli_z))
187
+ ez = float(sv_q.expectation_value(op).real)
188
+ # Compare with direct calculation
189
+ sv_t = sv.reshape([2] * n)
190
+ ax = n - 1
191
+ t0 = np.moveaxis(sv_t, ax, 0)[0].flatten()
192
+ t1 = np.moveaxis(sv_t, ax, 0)[1].flatten()
193
+ p0 = float(np.sum(np.abs(t0)**2))
194
+ p1 = float(np.sum(np.abs(t1)**2))
195
+ ez_direct = p0 - p1
196
+ ok4 = abs(ez - ez_direct) < 1e-6
197
+ checks.append(('<Z> consistent', ok4, f"qiskit={ez:.6f} direct={ez_direct:.6f}"))
198
+ except Exception as _e:
199
+ checks.append(('<Z> consistent', None, f"skip: {_e}"))
200
+
201
+ # 5. Histogram vs SV (if counts available)
202
+ if self.last_counts:
203
+ total = sum(self.last_counts.values())
204
+ hist_p0 = self.last_counts.get('0' * n, 0) / total
205
+ sv_p0 = float(np.abs(sv[0])**2)
206
+ # Loose check: histogram is statistical, allow 10% deviation
207
+ ok5 = abs(hist_p0 - sv_p0) < 0.15 or total < 50
208
+ checks.append(('Hist~SV P(|0>)', ok5,
209
+ f"hist={hist_p0:.3f} sv={sv_p0:.3f}"))
210
+
211
+ self.io.writeln(f"\n Consistency checks ({n} qubits):")
212
+ all_pass = True
213
+ for name, ok, detail in checks:
214
+ if ok is None:
215
+ status = 'SKIP'
216
+ elif ok:
217
+ status = 'PASS'
218
+ else:
219
+ status = 'FAIL'
220
+ all_pass = False
221
+ extra = f" {detail}" if detail else ""
222
+ self.io.writeln(f" {name:25s} {status}{extra}")
223
+ self.io.writeln(f"\n {'ALL CONSISTENT' if all_pass else 'INCONSISTENCY DETECTED'}")
224
+
143
225
  def cmd_ram(self) -> None:
144
226
  """RAM — show memory budget, per-instance cost, and parallelism estimates."""
145
227
  ram = _get_ram_gb()
@@ -217,6 +217,9 @@ class ClassicMixin:
217
217
  # ── DO / LOOP ─────────────────────────────────────────────────────
218
218
 
219
219
  def _find_matching_loop(self, sorted_lines: list[int], ip: int) -> int:
220
+ jt = getattr(self, '_jump_table', None)
221
+ if jt and ip in jt:
222
+ return jt[ip]
220
223
  depth = 1
221
224
  scan = ip + 1
222
225
  while scan < len(sorted_lines):
@@ -166,10 +166,12 @@ class ControlFlowMixin:
166
166
  def _find_matching_wend(self, sorted_lines: list[int], ip: int) -> int:
167
167
  """Find the ip after the WEND matching the WHILE at ip.
168
168
 
169
- Scans forward with proper nesting depth tracking. Returns the ip
170
- index past the matching WEND. Raises with the WHILE line number
171
- for clear diagnostics.
169
+ Uses pre-computed jump table when available (O(1)), falls back
170
+ to linear scan with nesting depth tracking.
172
171
  """
172
+ jt = getattr(self, '_jump_table', None)
173
+ if jt and ip in jt:
174
+ return jt[ip] + 1
173
175
  depth = 1
174
176
  scan = ip + 1
175
177
  while scan < len(sorted_lines):
@@ -1,4 +1,4 @@
1
- """QUBASIC built-in demo circuits."""
1
+ """QUBASIC built-in demo circuits with self-verification."""
2
2
 
3
3
 
4
4
  class DemoMixin:
@@ -8,7 +8,7 @@ class DemoMixin:
8
8
  self.shots, self.cmd_new(), self.cmd_run(), self.cmd_list(), self.cmd_locc().
9
9
  """
10
10
 
11
- def cmd_demo(self, name):
11
+ def cmd_demo(self, name: str) -> None:
12
12
  demos = {
13
13
  'BELL': self._demo_bell,
14
14
  'GHZ': self._demo_ghz,
@@ -36,6 +36,29 @@ class DemoMixin:
36
36
  return
37
37
  demos[name]()
38
38
 
39
+ def _verify_demo(self, expected_states: list[str], min_frac: float = 0.85,
40
+ label: str = '') -> bool:
41
+ """Check that expected states dominate the output.
42
+
43
+ expected_states: list of bitstrings that should capture >= min_frac of shots.
44
+ Returns True if verification passes.
45
+ """
46
+ if not self.last_counts:
47
+ self.io.writeln(f" VERIFY: no results")
48
+ return False
49
+ total = sum(self.last_counts.values())
50
+ hit = sum(self.last_counts.get(s, 0) for s in expected_states)
51
+ frac = hit / total if total > 0 else 0
52
+ tag = f" ({label})" if label else ""
53
+ if frac >= min_frac:
54
+ self.io.writeln(f" VERIFY PASS{tag}: {frac:.1%} in expected states "
55
+ f"(threshold {min_frac:.0%})")
56
+ return True
57
+ else:
58
+ self.io.writeln(f" VERIFY FAIL{tag}: {frac:.1%} in expected states "
59
+ f"(threshold {min_frac:.0%})")
60
+ return False
61
+
39
62
  def _demo_bell(self):
40
63
  self.cmd_new()
41
64
  self.num_qubits = 2
@@ -49,6 +72,7 @@ class DemoMixin:
49
72
  self.io.writeln("LOADED: Bell State (2 qubits)")
50
73
  self.cmd_list()
51
74
  self.cmd_run()
75
+ self._verify_demo(['00', '11'], 0.95, 'Bell: only |00> and |11>')
52
76
 
53
77
  def _demo_ghz(self):
54
78
  self.cmd_new()
@@ -69,6 +93,7 @@ class DemoMixin:
69
93
  self.io.writeln(f"LOADED: GHZ State ({n} qubits)")
70
94
  self.cmd_list()
71
95
  self.cmd_run()
96
+ self._verify_demo(['0' * n, '1' * n], 0.95, f'GHZ: only |{"0"*n}> and |{"1"*n}>')
72
97
 
73
98
  def _demo_teleport(self):
74
99
  self.cmd_new()
@@ -138,6 +163,7 @@ class DemoMixin:
138
163
  self.io.writeln("LOADED: Grover's Search (3 qubits, target=|101>)")
139
164
  self.cmd_list()
140
165
  self.cmd_run()
166
+ self._verify_demo(['101'], 0.85, 'Grover: target |101>')
141
167
 
142
168
  def _demo_qft(self):
143
169
  self.cmd_new()
@@ -193,6 +219,8 @@ class DemoMixin:
193
219
  self.io.writeln(" Expect: qubit 0 = 1 (balanced)")
194
220
  self.cmd_list()
195
221
  self.cmd_run()
222
+ # q0=1 means bit pattern x1 (states 01 or 11)
223
+ self._verify_demo(['01', '11'], 0.95, 'Deutsch: q0=1 (balanced)')
196
224
 
197
225
  def _demo_bernstein(self):
198
226
  self.cmd_new()
@@ -218,6 +246,8 @@ class DemoMixin:
218
246
  self.io.writeln(" Expect: measurement shows ...1011 (q3q2q1q0)")
219
247
  self.cmd_list()
220
248
  self.cmd_run()
249
+ # Ancilla (q4) is random; data qubits should read 1011
250
+ self._verify_demo(['01011', '11011'], 0.95, 'BV: secret=1011')
221
251
 
222
252
  def _demo_superdense(self):
223
253
  self.cmd_new()
@@ -243,6 +273,7 @@ class DemoMixin:
243
273
  self.io.writeln(" Expect: |11> with high probability")
244
274
  self.cmd_list()
245
275
  self.cmd_run()
276
+ self._verify_demo(['11'], 0.95, 'Superdense: message=11')
246
277
 
247
278
  def _demo_random(self):
248
279
  self.cmd_new()
@@ -322,6 +353,38 @@ class DemoMixin:
322
353
  self.io.writeln(" Alice sends |+> to Bob. Expect Bob's qubit ~ 50/50.")
323
354
  self.cmd_list()
324
355
  self.cmd_run()
356
+ # Fidelity: |+> teleported to Bob B[0]
357
+ # Z-basis: should be 50/50 (|+> has equal P(0) and P(1))
358
+ # X-basis: should be ~100% |0> (|+> is eigenstate of X with eigenvalue +1)
359
+ if self.last_counts:
360
+ total = sum(self.last_counts.values())
361
+ b0_counts = {'0': 0, '1': 0}
362
+ for state, count in self.last_counts.items():
363
+ parts = state.split('|')
364
+ if len(parts) >= 2:
365
+ bob_bit = parts[-1][-1]
366
+ b0_counts[bob_bit] = b0_counts.get(bob_bit, 0) + count
367
+ b_total = sum(b0_counts.values())
368
+ if b_total > 0:
369
+ p0 = b0_counts['0'] / b_total
370
+ z_fidelity = 1.0 - abs(p0 - 0.5) * 2
371
+ self.io.writeln(f" Z-basis fidelity: {z_fidelity:.3f} "
372
+ f"(Bob B[0]: P(0)={p0:.3f}, P(1)={1-p0:.3f})")
373
+ # X-basis verification via LOCC statevector
374
+ if self.locc and self.locc.joint:
375
+ import numpy as np
376
+ sv = np.ascontiguousarray(self.locc.sv).ravel()
377
+ n = self.locc.n_total
378
+ # Bob qubit 0 is at global index = offset_B + 0
379
+ bob_q = self.locc.offsets[1]
380
+ sv_t = sv.reshape([2] * n)
381
+ ax = n - 1 - bob_q
382
+ t0 = np.moveaxis(sv_t, ax, 0)[0].flatten()
383
+ t1 = np.moveaxis(sv_t, ax, 0)[1].flatten()
384
+ rho_01 = np.sum(np.conj(t0) * t1)
385
+ expect_x = float(2 * rho_01.real)
386
+ self.io.writeln(f" X-basis verification: <X> on Bob B[0] = {expect_x:.3f} "
387
+ f"(ideal |+> = 1.000)")
325
388
 
326
389
  def _demo_locc_coord(self):
327
390
  """Classical coordination between independent registers (SPLIT mode)."""
@@ -39,6 +39,8 @@ class Engine:
39
39
  self.sim_method: str = 'automatic'
40
40
  self.sim_device: str = 'CPU'
41
41
  self._noise_model: Any = None
42
+ self._noise_depol_p: float = 0.0
43
+ self._seed: int | None = None
42
44
  self._max_iterations: int = MAX_LOOP_ITERATIONS
43
45
  self._include_depth: int = 0
44
46
 
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING
7
7
 
8
8
  import numpy as np
9
9
  from qiskit import QuantumCircuit, transpile
10
- from qiskit_aer import AerSimulator
11
10
 
12
11
  from qubasic_core.engine import (
13
12
  GATE_TABLE, GATE_ALIASES,
@@ -624,7 +623,7 @@ class ExecutorMixin:
624
623
  run_vars=dict(self.variables), qc=qc)
625
624
  self._exec_line(line, ctx=imm_ctx)
626
625
  qc.save_statevector()
627
- backend = AerSimulator(method='statevector')
626
+ backend = self._make_backend('statevector')
628
627
  result = backend.run(transpile(qc, backend)).result()
629
628
  sv = np.array(result.get_statevector())
630
629
  self.last_sv = sv
@@ -12,44 +12,28 @@ from typing import Any
12
12
 
13
13
 
14
14
  def _replace_dollar_outside_strings(expr_str: str) -> str:
15
- """Apply $->_S_ replacement only outside quoted string literals."""
16
- result: list[str] = []
15
+ """Apply $->_S_ replacement only outside quoted string literals.
16
+
17
+ Single pass: splits on quote boundaries, substitutes only in
18
+ unquoted segments.
19
+ """
20
+ parts: list[str] = []
17
21
  i = 0
18
22
  n = len(expr_str)
19
23
  while i < n:
20
24
  ch = expr_str[i]
21
25
  if ch in ('"', "'"):
22
- # Copy the entire quoted region verbatim
23
26
  quote = ch
24
27
  j = i + 1
25
28
  while j < n and expr_str[j] != quote:
26
29
  j += 1
27
- # Include closing quote (or end of string if missing)
28
- result.append(expr_str[i:j + 1])
29
- i = j + 1
30
- else:
31
- result.append(ch)
32
- i += 1
33
- text = ''.join(result)
34
- # Now apply the substitution only to non-quoted segments
35
- parts: list[str] = []
36
- i = 0
37
- n = len(text)
38
- while i < n:
39
- ch = text[i]
40
- if ch in ('"', "'"):
41
- quote = ch
42
- j = i + 1
43
- while j < n and text[j] != quote:
44
- j += 1
45
- parts.append(text[i:j + 1])
30
+ parts.append(expr_str[i:j + 1])
46
31
  i = j + 1
47
32
  else:
48
- # Accumulate non-quoted text
49
33
  j = i
50
- while j < n and text[j] not in ('"', "'"):
34
+ while j < n and expr_str[j] not in ('"', "'"):
51
35
  j += 1
52
- segment = text[i:j]
36
+ segment = expr_str[i:j]
53
37
  parts.append(re.sub(r'(\w+)\$', r'\1_S_', segment))
54
38
  i = j
55
39
  return ''.join(parts)