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.
- qubasic-0.4.1/CHANGELOG.md +80 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/LICENSE +1 -1
- {qubasic-0.3.1/qubasic.egg-info → qubasic-0.4.1}/PKG-INFO +1 -1
- {qubasic-0.3.1 → qubasic-0.4.1}/pyproject.toml +4 -1
- {qubasic-0.3.1 → qubasic-0.4.1/qubasic.egg-info}/PKG-INFO +1 -1
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.py +17 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/__init__.py +1 -1
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/analysis.py +95 -13
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/classic.py +3 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/control_flow.py +5 -3
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/demos.py +65 -2
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/engine_state.py +2 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/executor.py +1 -2
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/expression.py +9 -25
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/locc_commands.py +58 -6
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/locc_display.py +3 -3
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/locc_engine.py +44 -3
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/locc_execution.py +50 -6
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/memory.py +12 -2
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/mock_backend.py +2 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/noise_mixin.py +24 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/parser.py +2 -2
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/profiler.py +6 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/scope.py +0 -1
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/state_display.py +16 -14
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/subs.py +25 -1
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/sweep.py +1 -1
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/terminal.py +553 -208
- qubasic-0.3.1/CHANGELOG.md +0 -34
- {qubasic-0.3.1 → qubasic-0.4.1}/MANIFEST.in +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/README.md +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/examples/bell.qb +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/examples/grover3.qb +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/examples/locc_teleport.qb +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/examples/sweep_rx.qb +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/__main__.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/backend.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/debug.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/display.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/engine.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/errors.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/file_io.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/gates.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/help_text.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/locc.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/patterns.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/protocol.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/screen.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/statements.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.1}/qubasic_core/strings.py +0 -0
- {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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qubasic"
|
|
7
|
-
version = "0.
|
|
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_*"]
|
|
@@ -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]
|
|
@@ -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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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[
|
|
49
|
+
full_pauli[n - 1 - qubits[i]] = p
|
|
46
50
|
op = SparsePauliOp(''.join(full_pauli))
|
|
47
|
-
val =
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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(
|
|
90
|
+
sv = np.ascontiguousarray(sv).ravel()
|
|
85
91
|
rho = np.outer(sv, sv.conj())
|
|
86
|
-
n = self.
|
|
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
|
-
|
|
170
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
34
|
+
while j < n and expr_str[j] not in ('"', "'"):
|
|
51
35
|
j += 1
|
|
52
|
-
segment =
|
|
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)
|