qubasic 0.5.0__tar.gz → 0.6.0__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.5.0 → qubasic-0.6.0}/CHANGELOG.md +29 -0
- {qubasic-0.5.0/qubasic.egg-info → qubasic-0.6.0}/PKG-INFO +4 -10
- {qubasic-0.5.0 → qubasic-0.6.0}/README.md +3 -9
- {qubasic-0.5.0 → qubasic-0.6.0}/pyproject.toml +1 -1
- {qubasic-0.5.0 → qubasic-0.6.0/qubasic.egg-info}/PKG-INFO +4 -10
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic.py +45 -31
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/__init__.py +1 -1
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/analysis.py +52 -24
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/control_flow.py +89 -20
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/debug.py +1 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/demos.py +1 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/display.py +22 -7
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/engine_state.py +10 -1
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/executor.py +35 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/expression.py +95 -19
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/file_io.py +61 -15
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/help_text.py +3 -1
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/locc_commands.py +30 -4
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/locc_engine.py +83 -31
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/locc_execution.py +16 -5
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/memory.py +62 -6
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/noise_mixin.py +17 -5
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/parser.py +8 -4
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/patterns.py +2 -2
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/profiler.py +36 -20
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/program_mgmt.py +2 -4
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/qol.py +81 -49
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/statements.py +8 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/terminal.py +210 -48
- {qubasic-0.5.0 → qubasic-0.6.0}/LICENSE +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/MANIFEST.in +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/examples/bell.qb +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/examples/grover3.qb +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/examples/locc_teleport.qb +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/examples/sweep_rx.qb +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/__main__.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/backend.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/classic.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/engine.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/errors.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/gates.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/locc.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/mock_backend.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/protocol.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/scope.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/screen.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/state_display.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/strings.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/subs.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/qubasic_core/sweep.py +0 -0
- {qubasic-0.5.0 → qubasic-0.6.0}/setup.cfg +0 -0
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 (2026-06-18)
|
|
4
|
+
|
|
5
|
+
Correctness, scalability, and usability pass (61 fixes).
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Time-travel debugging now works**: STEP records statevector checkpoints, so REWIND/FORWARD/HISTORY operate (previously `_checkpoint_sv` was never called).
|
|
9
|
+
- **TRON, breakpoints, ON TIMER, ON MEASURE now fire**: the debug hooks are wired into the execution loop instead of being dead code.
|
|
10
|
+
- **PLOT and ANIMATE sweep correctly**: the transpiled-circuit cache key now includes variable bindings, so parameter sweeps no longer reuse the first circuit.
|
|
11
|
+
- **`$XXXX` memory addresses parse**: PEEK/POKE/DUMP/SYS/WAIT accept the canonical hex-address notation, not only `0x`/decimal/`&H`.
|
|
12
|
+
- **Multi-line IF/ELSE/END IF**: the untaken branch is properly skipped (previously both branches ran).
|
|
13
|
+
- **ON ERROR** reports the real ERR/ERL for build-path errors and runs handler lines through the program executor (no spurious state dumps).
|
|
14
|
+
- **OPTION BASE 1** is honored in array indexing; FOR with STEP 0 is rejected; bare `NEXT` and `NEXT i, j` are supported.
|
|
15
|
+
- **CSV/EXPORT** report correctly when only a statevector (no counts) is present.
|
|
16
|
+
|
|
17
|
+
### Performance / scalability
|
|
18
|
+
- Stabilizer is chosen before MPS for large Clifford circuits, and the full 2^n statevector is no longer rebuilt after every measured run (a 30-qubit GHZ went from ~16s to <1s).
|
|
19
|
+
- STEP evolves the statevector incrementally; DENSITY and EXPECT avoid building large matrices; HEATMAP and STATS reuse work.
|
|
20
|
+
|
|
21
|
+
### Quantum / LOCC
|
|
22
|
+
- LOCC depolarizing noise matches the Qiskit Aer convention; amplitude- and phase-damping channels are now simulated in the LOCC engine.
|
|
23
|
+
- EXPECT/ENTROPY/DENSITY/BLOCH accept a register argument in SPLIT mode; `LOCC SHOTCAP` replaces the magic shot-cap variable; SEND prefix/suffix optimization detects all control transfers.
|
|
24
|
+
- Per-qubit memory-mapped noise applies to multi-qubit gates; complex single-qubit amplitudes are exposed in the qubit-state block; POKE-driven state preparation is applied at build.
|
|
25
|
+
|
|
26
|
+
### Language / interface
|
|
27
|
+
- User-defined TYPE record fields can be read and assigned (`p.x`); ELSEIF chains and re-parsed clauses are memoized.
|
|
28
|
+
- SAVE/LOAD round-trip noise, seed, screen mode, DEF FN, and TYPE definitions; CHAIN/MERGE/INCLUDE/IMPORT honor a `QUBASIC_PATH` search path; IMPORT no longer pollutes SAVE output.
|
|
29
|
+
- CLI gains `--version` and `--agent`; `--json` output is enriched (statevector, variables, manifest) and emits structured errors; the agent write-sandbox engages under `--json`/`--agent`.
|
|
30
|
+
- `SET_STATE` persists into the next RUN and warns when a named state cannot fit; per-command `HELP <name>`; dependency-version warnings are suppressed before the banner.
|
|
31
|
+
|
|
3
32
|
## 0.5.0 (2026-03-30)
|
|
4
33
|
|
|
5
34
|
- **Colorized histograms**: bars colored green/yellow/dim by probability in terminal
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -468,15 +468,9 @@ BLOCH A 0 Bloch sphere for register A, qubit 0
|
|
|
468
468
|
LOCCINFO Protocol metrics
|
|
469
469
|
```
|
|
470
470
|
|
|
471
|
-
SPLIT: max capacity (
|
|
471
|
+
SPLIT: max capacity (up to 33 qubits per register), no cross-register entanglement. Use SEND/IF for classical coordination.
|
|
472
472
|
|
|
473
|
-
JOINT: shared entanglement via SHARE. Limited to
|
|
474
|
-
|
|
475
|
-
### Network LOCC
|
|
476
|
-
```
|
|
477
|
-
CONNECT "host:port" AS C Attach remote register (local simulation stand-in)
|
|
478
|
-
DISCONNECT C Detach
|
|
479
|
-
```
|
|
473
|
+
JOINT: shared entanglement via SHARE. Limited to 33 total qubits.
|
|
480
474
|
|
|
481
475
|
### LOCC optimization
|
|
482
476
|
Programs with SEND execute the deterministic prefix (before first SEND) once, snapshot the statevector, then re-execute only the suffix per shot. Complexity: O(prefix + shots * suffix) instead of O(shots * total).
|
|
@@ -737,7 +731,7 @@ qubasic_core/
|
|
|
737
731
|
mock_backend.py MockAerSimulator for fast testing
|
|
738
732
|
```
|
|
739
733
|
|
|
740
|
-
`Engine` holds all program state. `QBasicTerminal` inherits `Engine` +
|
|
734
|
+
`Engine` holds all program state. `QBasicTerminal` inherits `Engine` + 20 mixins. Execution methods live on `QBasicTerminal`, so headless/agent use should instantiate `QBasicTerminal` (the `Engine` base is a state container only).
|
|
741
735
|
|
|
742
736
|
## License
|
|
743
737
|
|
|
@@ -435,15 +435,9 @@ BLOCH A 0 Bloch sphere for register A, qubit 0
|
|
|
435
435
|
LOCCINFO Protocol metrics
|
|
436
436
|
```
|
|
437
437
|
|
|
438
|
-
SPLIT: max capacity (
|
|
438
|
+
SPLIT: max capacity (up to 33 qubits per register), no cross-register entanglement. Use SEND/IF for classical coordination.
|
|
439
439
|
|
|
440
|
-
JOINT: shared entanglement via SHARE. Limited to
|
|
441
|
-
|
|
442
|
-
### Network LOCC
|
|
443
|
-
```
|
|
444
|
-
CONNECT "host:port" AS C Attach remote register (local simulation stand-in)
|
|
445
|
-
DISCONNECT C Detach
|
|
446
|
-
```
|
|
440
|
+
JOINT: shared entanglement via SHARE. Limited to 33 total qubits.
|
|
447
441
|
|
|
448
442
|
### LOCC optimization
|
|
449
443
|
Programs with SEND execute the deterministic prefix (before first SEND) once, snapshot the statevector, then re-execute only the suffix per shot. Complexity: O(prefix + shots * suffix) instead of O(shots * total).
|
|
@@ -704,7 +698,7 @@ qubasic_core/
|
|
|
704
698
|
mock_backend.py MockAerSimulator for fast testing
|
|
705
699
|
```
|
|
706
700
|
|
|
707
|
-
`Engine` holds all program state. `QBasicTerminal` inherits `Engine` +
|
|
701
|
+
`Engine` holds all program state. `QBasicTerminal` inherits `Engine` + 20 mixins. Execution methods live on `QBasicTerminal`, so headless/agent use should instantiate `QBasicTerminal` (the `Engine` base is a state container only).
|
|
708
702
|
|
|
709
703
|
## License
|
|
710
704
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -468,15 +468,9 @@ BLOCH A 0 Bloch sphere for register A, qubit 0
|
|
|
468
468
|
LOCCINFO Protocol metrics
|
|
469
469
|
```
|
|
470
470
|
|
|
471
|
-
SPLIT: max capacity (
|
|
471
|
+
SPLIT: max capacity (up to 33 qubits per register), no cross-register entanglement. Use SEND/IF for classical coordination.
|
|
472
472
|
|
|
473
|
-
JOINT: shared entanglement via SHARE. Limited to
|
|
474
|
-
|
|
475
|
-
### Network LOCC
|
|
476
|
-
```
|
|
477
|
-
CONNECT "host:port" AS C Attach remote register (local simulation stand-in)
|
|
478
|
-
DISCONNECT C Detach
|
|
479
|
-
```
|
|
473
|
+
JOINT: shared entanglement via SHARE. Limited to 33 total qubits.
|
|
480
474
|
|
|
481
475
|
### LOCC optimization
|
|
482
476
|
Programs with SEND execute the deterministic prefix (before first SEND) once, snapshot the statevector, then re-execute only the suffix per shot. Complexity: O(prefix + shots * suffix) instead of O(shots * total).
|
|
@@ -737,7 +731,7 @@ qubasic_core/
|
|
|
737
731
|
mock_backend.py MockAerSimulator for fast testing
|
|
738
732
|
```
|
|
739
733
|
|
|
740
|
-
`Engine` holds all program state. `QBasicTerminal` inherits `Engine` +
|
|
734
|
+
`Engine` holds all program state. `QBasicTerminal` inherits `Engine` + 20 mixins. Execution methods live on `QBasicTerminal`, so headless/agent use should instantiate `QBasicTerminal` (the `Engine` base is a state container only).
|
|
741
735
|
|
|
742
736
|
## License
|
|
743
737
|
|
|
@@ -50,14 +50,15 @@ def main():
|
|
|
50
50
|
import json as _json
|
|
51
51
|
os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
|
|
52
52
|
|
|
53
|
+
from qubasic_core import __version__
|
|
54
|
+
|
|
53
55
|
args = sys.argv[1:]
|
|
54
56
|
quiet = '--quiet' in args or '-q' in args
|
|
55
57
|
json_mode = '--json' in args
|
|
58
|
+
agent_mode = '--agent' in args
|
|
56
59
|
seed_val = None
|
|
57
|
-
|
|
58
|
-
args = [a for a in args if a
|
|
59
|
-
if json_mode:
|
|
60
|
-
args = [a for a in args if a != '--json']
|
|
60
|
+
for flag in ('--quiet', '-q', '--json', '--agent'):
|
|
61
|
+
args = [a for a in args if a != flag]
|
|
61
62
|
# Parse --seed N
|
|
62
63
|
filtered = []
|
|
63
64
|
i = 0
|
|
@@ -70,8 +71,11 @@ def main():
|
|
|
70
71
|
i += 1
|
|
71
72
|
args = filtered
|
|
72
73
|
|
|
74
|
+
if any(a in ('-v', '--version') for a in args):
|
|
75
|
+
print(f"QUBASIC {__version__}")
|
|
76
|
+
sys.exit(0)
|
|
77
|
+
|
|
73
78
|
if any(a in ('-h', '--help') for a in args):
|
|
74
|
-
from qubasic_core import __version__
|
|
75
79
|
print(f"QUBASIC {__version__} — Quantum BASIC Interactive Terminal")
|
|
76
80
|
print()
|
|
77
81
|
print("Usage:")
|
|
@@ -79,13 +83,17 @@ def main():
|
|
|
79
83
|
print(" qubasic script.qb Run a script file")
|
|
80
84
|
print(" qubasic --quiet script Suppress banner and progress")
|
|
81
85
|
print(" qubasic --json script Output results as JSON")
|
|
86
|
+
print(" qubasic --agent script Confine file writes to the working dir")
|
|
82
87
|
print(" qubasic --seed N script Set random seed for reproducibility")
|
|
88
|
+
print(" qubasic --version Show version")
|
|
83
89
|
print(" qubasic --help Show this help")
|
|
84
90
|
print()
|
|
85
91
|
print("Type HELP inside the REPL for full command reference.")
|
|
86
92
|
sys.exit(0)
|
|
87
93
|
|
|
88
94
|
term = QBasicTerminal()
|
|
95
|
+
# JSON mode implies agent use, so confine file writes to the working dir.
|
|
96
|
+
term.agent_mode = agent_mode or json_mode
|
|
89
97
|
if seed_val is not None:
|
|
90
98
|
import numpy as _np
|
|
91
99
|
term._seed = seed_val
|
|
@@ -93,35 +101,41 @@ def main():
|
|
|
93
101
|
|
|
94
102
|
if args:
|
|
95
103
|
path = args[0]
|
|
96
|
-
if os.path.isfile(path):
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
buf = io.StringIO()
|
|
100
|
-
old = sys.stdout
|
|
101
|
-
sys.stdout = buf
|
|
102
|
-
try:
|
|
103
|
-
run_script(path, term)
|
|
104
|
-
finally:
|
|
105
|
-
sys.stdout = old
|
|
106
|
-
if json_mode:
|
|
107
|
-
result = {
|
|
108
|
-
'counts': term.last_counts or {},
|
|
109
|
-
'num_qubits': term.num_qubits,
|
|
110
|
-
'shots': term.shots,
|
|
111
|
-
}
|
|
112
|
-
print(_json.dumps(result, indent=2))
|
|
113
|
-
elif not quiet:
|
|
114
|
-
print(buf.getvalue())
|
|
104
|
+
if not os.path.isfile(path):
|
|
105
|
+
if json_mode:
|
|
106
|
+
print(_json.dumps({'error': f'FILE NOT FOUND: {path}'}, indent=2))
|
|
115
107
|
else:
|
|
116
|
-
|
|
108
|
+
print(f"?FILE NOT FOUND: {path}")
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
if quiet or json_mode:
|
|
111
|
+
import io
|
|
112
|
+
buf = io.StringIO()
|
|
113
|
+
old = sys.stdout
|
|
114
|
+
sys.stdout = buf
|
|
115
|
+
err = None
|
|
116
|
+
try:
|
|
117
117
|
run_script(path, term)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
except Exception as e: # surface as structured error in JSON mode
|
|
119
|
+
err = str(e)
|
|
120
|
+
finally:
|
|
121
|
+
sys.stdout = old
|
|
122
|
+
if json_mode:
|
|
123
|
+
if err is not None:
|
|
124
|
+
print(_json.dumps({'error': err}, indent=2))
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
print(_json.dumps(term.result(), indent=2))
|
|
127
|
+
elif not quiet:
|
|
128
|
+
print(buf.getvalue())
|
|
129
|
+
if err is not None:
|
|
130
|
+
print(f"?ERROR: {err}")
|
|
131
|
+
sys.exit(1)
|
|
122
132
|
else:
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
term.print_banner()
|
|
134
|
+
run_script(path, term)
|
|
135
|
+
# Exit 0 on success; 1 if a measured program produced no counts.
|
|
136
|
+
sys.exit(0 if term.last_counts is not None or not any(
|
|
137
|
+
term.program.get(ln, '').strip().upper() == 'MEASURE'
|
|
138
|
+
for ln in term.program) else 1)
|
|
125
139
|
else:
|
|
126
140
|
term.repl()
|
|
127
141
|
|
|
@@ -22,24 +22,48 @@ class AnalysisMixin:
|
|
|
22
22
|
self.locc, self.locc_mode.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
+
def _resolve_analysis_target(self, rest: str):
|
|
26
|
+
"""Return (sv, n_qubits, remaining_rest) for a state-analysis command.
|
|
27
|
+
|
|
28
|
+
In LOCC SPLIT mode a leading register letter selects that register
|
|
29
|
+
(e.g. ``EXPECT A Z 0``); otherwise the active statevector is used.
|
|
30
|
+
"""
|
|
31
|
+
parts = rest.split()
|
|
32
|
+
if self.locc_mode and self.locc and not self.locc.joint and parts:
|
|
33
|
+
cand = parts[0].upper()
|
|
34
|
+
if cand in self.locc.names:
|
|
35
|
+
sv, n = self._active_sv_for_reg(cand)
|
|
36
|
+
return sv, n, ' '.join(parts[1:])
|
|
37
|
+
return self._active_sv, self._active_nqubits, rest
|
|
38
|
+
|
|
25
39
|
def cmd_expect(self, rest: str) -> None:
|
|
26
|
-
"""EXPECT <pauli> [qubits] — compute expectation value.
|
|
27
|
-
Examples: EXPECT Z 0, EXPECT ZZ 0 1, EXPECT
|
|
28
|
-
sv = self.
|
|
40
|
+
"""EXPECT [reg] <pauli> [qubits] — compute expectation value.
|
|
41
|
+
Examples: EXPECT Z 0, EXPECT ZZ 0 1, EXPECT A Z 0 (SPLIT register A)"""
|
|
42
|
+
sv, n, rest = self._resolve_analysis_target(rest)
|
|
29
43
|
if sv is None:
|
|
30
44
|
if self.locc_mode and self.locc and not self.locc.joint:
|
|
31
|
-
self.io.writeln("?SPLIT mode:
|
|
45
|
+
self.io.writeln("?SPLIT mode: prefix a register, e.g. EXPECT A Z 0")
|
|
32
46
|
else:
|
|
33
47
|
self.io.writeln("?NO STATE — RUN first")
|
|
34
48
|
return
|
|
35
49
|
parts = rest.split()
|
|
36
50
|
if not parts:
|
|
37
|
-
self.io.writeln("?USAGE: EXPECT <Z|X|Y|ZZ|...> [qubits]")
|
|
51
|
+
self.io.writeln("?USAGE: EXPECT [reg] <Z|X|Y|ZZ|...> [qubits]")
|
|
38
52
|
return
|
|
39
53
|
pauli_str = parts[0].upper()
|
|
40
54
|
qubits = [int(q) for q in parts[1:]] if len(parts) > 1 else list(range(len(pauli_str)))
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
# Fast numpy path for diagonal (Z/I-only) observables — no qiskit copy.
|
|
56
|
+
if set(pauli_str) <= {'Z', 'I'}:
|
|
57
|
+
svf = np.ascontiguousarray(sv).ravel()
|
|
58
|
+
probs = np.abs(svf) ** 2
|
|
59
|
+
idx = np.arange(svf.size)
|
|
60
|
+
sign = np.ones(svf.size)
|
|
61
|
+
for i, p in enumerate(pauli_str):
|
|
62
|
+
if p == 'Z' and i < len(qubits):
|
|
63
|
+
sign = sign * np.where(((idx >> qubits[i]) & 1) == 1, -1.0, 1.0)
|
|
64
|
+
val = float(np.sum(probs * sign))
|
|
65
|
+
self.io.writeln(f" <{pauli_str}> on qubits {qubits} = {val:.6f}")
|
|
66
|
+
return
|
|
43
67
|
try:
|
|
44
68
|
from qiskit.quantum_info import Statevector, SparsePauliOp
|
|
45
69
|
sv_q = Statevector(np.ascontiguousarray(sv).ravel())
|
|
@@ -54,17 +78,19 @@ class AnalysisMixin:
|
|
|
54
78
|
self.io.writeln(f"?EXPECT ERROR: {e}")
|
|
55
79
|
|
|
56
80
|
def cmd_entropy(self, rest: str = '') -> None:
|
|
57
|
-
"""ENTROPY [qubits] — entanglement entropy of
|
|
58
|
-
Examples: ENTROPY 0
|
|
59
|
-
sv = self.
|
|
81
|
+
"""ENTROPY [reg] [qubits] — entanglement entropy of qubits vs rest.
|
|
82
|
+
Examples: ENTROPY 0 | ENTROPY 0 1 | ENTROPY A 0 (SPLIT register A)"""
|
|
83
|
+
sv, n, rest = self._resolve_analysis_target(rest)
|
|
60
84
|
if sv is None:
|
|
61
|
-
self.
|
|
85
|
+
if self.locc_mode and self.locc and not self.locc.joint:
|
|
86
|
+
self.io.writeln("?SPLIT mode: prefix a register, e.g. ENTROPY A 0")
|
|
87
|
+
else:
|
|
88
|
+
self.io.writeln("?NO STATE — RUN first")
|
|
62
89
|
return
|
|
63
90
|
if rest.strip():
|
|
64
91
|
partition_a = [int(q) for q in rest.replace(',', ' ').split() if q.strip()]
|
|
65
92
|
else:
|
|
66
93
|
partition_a = [0]
|
|
67
|
-
n = self._active_nqubits
|
|
68
94
|
try:
|
|
69
95
|
from qiskit.quantum_info import Statevector, entropy, partial_trace
|
|
70
96
|
sv_obj = Statevector(np.ascontiguousarray(sv).ravel())
|
|
@@ -81,25 +107,27 @@ class AnalysisMixin:
|
|
|
81
107
|
except Exception as e:
|
|
82
108
|
self.io.writeln(f"?ENTROPY ERROR: {e}")
|
|
83
109
|
|
|
84
|
-
def cmd_density(self) -> None:
|
|
85
|
-
"""Show density matrix (
|
|
86
|
-
sv = self.
|
|
110
|
+
def cmd_density(self, rest: str = '') -> None:
|
|
111
|
+
"""Show density matrix (DENSITY [reg]); summarizes for large systems."""
|
|
112
|
+
sv, n, rest = self._resolve_analysis_target(rest)
|
|
87
113
|
if sv is None:
|
|
88
|
-
self.
|
|
114
|
+
if self.locc_mode and self.locc and not self.locc.joint:
|
|
115
|
+
self.io.writeln("?SPLIT mode: prefix a register, e.g. DENSITY A")
|
|
116
|
+
else:
|
|
117
|
+
self.io.writeln("?NO STATE — RUN first")
|
|
89
118
|
return
|
|
90
119
|
sv = np.ascontiguousarray(sv).ravel()
|
|
91
|
-
rho = np.outer(sv, sv.conj())
|
|
92
|
-
n = self._active_nqubits
|
|
93
120
|
dim = 2**n
|
|
94
121
|
if dim > 16:
|
|
122
|
+
# |psi><psi| is rank-1, so purity is ||psi||^4 and the von Neumann
|
|
123
|
+
# entropy is 0; report directly instead of building a 2^n x 2^n matrix.
|
|
124
|
+
norm2 = float(np.vdot(sv, sv).real)
|
|
95
125
|
self.io.writeln(f" Density matrix: {dim}x{dim} (too large to display)")
|
|
96
|
-
self.io.writeln(f"
|
|
97
|
-
self.io.
|
|
98
|
-
|
|
99
|
-
eigvals = eigvals[eigvals > 1e-15]
|
|
100
|
-
ent = -np.sum(eigvals * np.log2(eigvals))
|
|
101
|
-
self.io.writeln(f"{ent:.6f} bits")
|
|
126
|
+
self.io.writeln(f" Pure state |psi><psi|: purity {norm2 ** 2:.6f}")
|
|
127
|
+
self.io.writeln(f" Von Neumann entropy: 0.000000 bits")
|
|
128
|
+
self.io.writeln(f" (use ENTROPY <qubits> for reduced-state entanglement)")
|
|
102
129
|
return
|
|
130
|
+
rho = np.outer(sv, sv.conj())
|
|
103
131
|
self.io.writeln(f"\n Density matrix ({dim}x{dim}):\n")
|
|
104
132
|
for i in range(dim):
|
|
105
133
|
row = []
|
|
@@ -15,7 +15,7 @@ from qubasic_core.statements import (
|
|
|
15
15
|
LetArrayStmt, LetStmt, PrintStmt, GotoStmt, GosubStmt,
|
|
16
16
|
ForStmt, NextStmt, WhileStmt, IfThenStmt,
|
|
17
17
|
DataStmt, ReadStmt, OnGotoStmt, OnGosubStmt,
|
|
18
|
-
SelectCaseStmt, CaseStmt, EndSelectStmt,
|
|
18
|
+
SelectCaseStmt, CaseStmt, EndSelectStmt, ElseStmt, EndIfStmt,
|
|
19
19
|
DoStmt, LoopStmt, ExitStmt,
|
|
20
20
|
SwapStmt, DefFnStmt, OptionBaseStmt,
|
|
21
21
|
SubStmt, EndSubStmt, FunctionStmt, EndFunctionStmt, CallStmt,
|
|
@@ -43,8 +43,11 @@ class ControlFlowMixin:
|
|
|
43
43
|
def _cf_let_array(self, stmt: str, run_vars: dict[str, Any],
|
|
44
44
|
parsed: LetArrayStmt) -> tuple[bool, ExecOutcome]:
|
|
45
45
|
name, idx_expr, val_expr = parsed.name, parsed.index_expr, parsed.value_expr
|
|
46
|
-
|
|
46
|
+
base = getattr(self, '_option_base', 0)
|
|
47
|
+
idx = int(self._eval_with_vars(idx_expr, run_vars)) - base
|
|
47
48
|
val = self._eval_with_vars(val_expr, run_vars)
|
|
49
|
+
if idx < 0:
|
|
50
|
+
raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx + base})")
|
|
48
51
|
if name not in self.arrays:
|
|
49
52
|
self.arrays[name] = [0.0] * (idx + 1)
|
|
50
53
|
while idx >= len(self.arrays[name]):
|
|
@@ -126,6 +129,8 @@ class ControlFlowMixin:
|
|
|
126
129
|
start = self._eval_with_vars(start_expr, run_vars)
|
|
127
130
|
end = self._eval_with_vars(end_expr, run_vars)
|
|
128
131
|
step = self._eval_with_vars(step_expr, run_vars) if step_expr else 1
|
|
132
|
+
if step == 0:
|
|
133
|
+
raise RuntimeError(f"FOR {var}: STEP 0 would never terminate")
|
|
129
134
|
try:
|
|
130
135
|
if start == int(start): start = int(start)
|
|
131
136
|
except (OverflowError, ValueError):
|
|
@@ -146,22 +151,25 @@ class ControlFlowMixin:
|
|
|
146
151
|
|
|
147
152
|
def _cf_next(self, stmt: str, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]],
|
|
148
153
|
parsed: NextStmt) -> tuple[bool, ExecOutcome]:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
154
|
+
# Supports bare ``NEXT`` (close the innermost FOR) and ``NEXT i, j``
|
|
155
|
+
# (close i, then j). Each named variable must match the FOR on top of
|
|
156
|
+
# the loop stack at that point.
|
|
157
|
+
names = [v.strip() for v in parsed.var.split(',')] if parsed.var.strip() else ['']
|
|
158
|
+
for name in names:
|
|
159
|
+
if not loop_stack or loop_stack[-1].get('var') is None:
|
|
160
|
+
raise RuntimeError(f"NEXT {name}".rstrip() + " without matching FOR")
|
|
161
|
+
loop = loop_stack[-1]
|
|
162
|
+
cur_var = loop['var']
|
|
163
|
+
if name and cur_var != name:
|
|
164
|
+
raise RuntimeError(f"NEXT {name} does not match current FOR {cur_var}")
|
|
165
|
+
loop['current'] += loop['step']
|
|
166
|
+
if (loop['step'] > 0 and loop['current'] <= loop['end']) or \
|
|
167
|
+
(loop['step'] < 0 and loop['current'] >= loop['end']):
|
|
168
|
+
run_vars[cur_var] = loop['current']
|
|
169
|
+
self.variables[cur_var] = loop['current']
|
|
170
|
+
return True, loop['return_ip'] + 1
|
|
163
171
|
loop_stack.pop()
|
|
164
|
-
|
|
172
|
+
return True, ExecResult.ADVANCE
|
|
165
173
|
|
|
166
174
|
def _find_matching_wend(self, sorted_lines: list[int], ip: int) -> int:
|
|
167
175
|
"""Find the ip after the WEND matching the WHILE at ip.
|
|
@@ -218,8 +226,21 @@ class ControlFlowMixin:
|
|
|
218
226
|
cond_vars = run_vars
|
|
219
227
|
if self.locc_mode and self.locc:
|
|
220
228
|
cond_vars = {**run_vars, **self.locc.classical}
|
|
221
|
-
|
|
222
|
-
|
|
229
|
+
cond_true = self._eval_condition(cond_str, cond_vars)
|
|
230
|
+
|
|
231
|
+
# Block form: "IF cond THEN" with no inline THEN/ELSE clauses spans
|
|
232
|
+
# following lines up to a matching ELSE / END IF.
|
|
233
|
+
if not then_clause and else_clause is None:
|
|
234
|
+
else_ip, endif_ip = self._find_if_block(sorted_lines, ip)
|
|
235
|
+
if cond_true:
|
|
236
|
+
return True, ExecResult.ADVANCE # fall into the THEN block
|
|
237
|
+
if else_ip is not None:
|
|
238
|
+
return True, else_ip + 1 # jump to the ELSE block
|
|
239
|
+
return True, endif_ip + 1 # no ELSE: skip past END IF
|
|
240
|
+
|
|
241
|
+
# Single-line form.
|
|
242
|
+
result: ExecOutcome = ExecResult.ADVANCE
|
|
243
|
+
if cond_true:
|
|
223
244
|
if then_clause:
|
|
224
245
|
r = exec_fn(then_clause, loop_stack, sorted_lines, ip, run_vars)
|
|
225
246
|
if r is not None and r is not ExecResult.ADVANCE:
|
|
@@ -230,6 +251,52 @@ class ControlFlowMixin:
|
|
|
230
251
|
result = r
|
|
231
252
|
return True, result
|
|
232
253
|
|
|
254
|
+
@staticmethod
|
|
255
|
+
def _is_block_if(line: str) -> bool:
|
|
256
|
+
"""True for a block-opening ``IF ... THEN`` (nothing after THEN)."""
|
|
257
|
+
return re.match(r'IF\b.*\bTHEN\s*$', line.strip(), re.IGNORECASE) is not None
|
|
258
|
+
|
|
259
|
+
def _find_if_block(self, sorted_lines: list[int], ip: int) -> tuple[int | None, int]:
|
|
260
|
+
"""From a block IF at ip, find (else_ip, endif_ip) handling nesting."""
|
|
261
|
+
depth = 1
|
|
262
|
+
else_ip: int | None = None
|
|
263
|
+
scan = ip + 1
|
|
264
|
+
while scan < len(sorted_lines):
|
|
265
|
+
s = self.program[sorted_lines[scan]].strip()
|
|
266
|
+
su = s.upper()
|
|
267
|
+
if self._is_block_if(s):
|
|
268
|
+
depth += 1
|
|
269
|
+
elif su == 'END IF':
|
|
270
|
+
depth -= 1
|
|
271
|
+
if depth == 0:
|
|
272
|
+
return else_ip, scan
|
|
273
|
+
elif su == 'ELSE' and depth == 1 and else_ip is None:
|
|
274
|
+
else_ip = scan
|
|
275
|
+
scan += 1
|
|
276
|
+
raise RuntimeError(f"IF block at line {sorted_lines[ip]} has no matching END IF")
|
|
277
|
+
|
|
278
|
+
def _cf_else(self, sorted_lines: list[int], ip: int) -> tuple[bool, ExecOutcome]:
|
|
279
|
+
"""ELSE reached while running a THEN block: skip past the END IF."""
|
|
280
|
+
depth = 1
|
|
281
|
+
scan = ip + 1
|
|
282
|
+
while scan < len(sorted_lines):
|
|
283
|
+
s = self.program[sorted_lines[scan]].strip()
|
|
284
|
+
su = s.upper()
|
|
285
|
+
if self._is_block_if(s):
|
|
286
|
+
depth += 1
|
|
287
|
+
elif su == 'END IF':
|
|
288
|
+
depth -= 1
|
|
289
|
+
if depth == 0:
|
|
290
|
+
return True, scan + 1
|
|
291
|
+
scan += 1
|
|
292
|
+
return True, ExecResult.ADVANCE
|
|
293
|
+
|
|
294
|
+
def _cf_return(self) -> tuple[bool, ExecOutcome]:
|
|
295
|
+
"""RETURN — pop the GOSUB stack, or error if there is nothing to return to."""
|
|
296
|
+
if not self._gosub_stack:
|
|
297
|
+
raise RuntimeError("RETURN WITHOUT GOSUB")
|
|
298
|
+
return True, self._gosub_stack.pop()
|
|
299
|
+
|
|
233
300
|
# ── Type-based dispatch table ────────────────────────────────────
|
|
234
301
|
# Maps parsed Stmt types to handler lambdas. Each lambda receives
|
|
235
302
|
# (self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars,
|
|
@@ -240,7 +307,7 @@ class ControlFlowMixin:
|
|
|
240
307
|
RemStmt: lambda s, st, p, ls, sl, ip, rv, ef: (True, ExecResult.ADVANCE),
|
|
241
308
|
MeasureStmt: lambda s, st, p, ls, sl, ip, rv, ef: (True, ExecResult.ADVANCE),
|
|
242
309
|
EndStmt: lambda s, st, p, ls, sl, ip, rv, ef: (True, ExecResult.END),
|
|
243
|
-
ReturnStmt: lambda s, st, p, ls, sl, ip, rv, ef:
|
|
310
|
+
ReturnStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_return(),
|
|
244
311
|
# Handlers defined in control_flow.py (parsed is positional)
|
|
245
312
|
WendStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_wend(rv, ls, sl, ip),
|
|
246
313
|
LetArrayStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_let_array(st, rv, p),
|
|
@@ -260,6 +327,8 @@ class ControlFlowMixin:
|
|
|
260
327
|
SelectCaseStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_select_case(st, rv, sl, ip, parsed=p),
|
|
261
328
|
CaseStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_case(st, sl, ip, parsed=p),
|
|
262
329
|
EndSelectStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_end_select(st, parsed=p),
|
|
330
|
+
ElseStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_else(sl, ip),
|
|
331
|
+
EndIfStmt: lambda s, st, p, ls, sl, ip, rv, ef: (True, ExecResult.ADVANCE),
|
|
263
332
|
DoStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_do(st, rv, ls, sl, ip, parsed=p),
|
|
264
333
|
LoopStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_loop(st, rv, ls, sl, ip, parsed=p),
|
|
265
334
|
ExitStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_exit(st, ls, sl, ip, parsed=p),
|
|
@@ -32,6 +32,7 @@ class DebugMixin:
|
|
|
32
32
|
self._breakpoints: set[int] = set()
|
|
33
33
|
self._watches: list[str] = []
|
|
34
34
|
self._on_measure_target: int | None = None
|
|
35
|
+
self._on_measure_fired: bool = False
|
|
35
36
|
self._on_timer_target: int | None = None
|
|
36
37
|
self._on_timer_interval: float = 0.0
|
|
37
38
|
self._on_timer_last: float = 0.0
|
|
@@ -25,6 +25,21 @@ class DisplayMixin:
|
|
|
25
25
|
Requires: TerminalProtocol — uses self.num_qubits, self.arrays.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
+
def _rich_print(self, *renderables) -> None:
|
|
29
|
+
"""Render rich content to a string and emit it through the IOPort.
|
|
30
|
+
|
|
31
|
+
Routing rich output through self.io (instead of straight to stdout)
|
|
32
|
+
means headless/agent callers with a custom IOPort capture tables and
|
|
33
|
+
statevectors too, and the IOPort's encoding fallback applies uniformly.
|
|
34
|
+
ANSI colour is kept only when stdout is a real terminal.
|
|
35
|
+
"""
|
|
36
|
+
import io as _io
|
|
37
|
+
from rich.console import Console
|
|
38
|
+
buf = _io.StringIO()
|
|
39
|
+
force = bool(sys.stdout and sys.stdout.isatty())
|
|
40
|
+
Console(file=buf, highlight=False, force_terminal=force).print(*renderables)
|
|
41
|
+
self.io.write(buf.getvalue())
|
|
42
|
+
|
|
28
43
|
def print_histogram(self, counts: dict[str, int]) -> None:
|
|
29
44
|
"""Measurement histogram with optional rich-table formatting."""
|
|
30
45
|
total = sum(counts.values())
|
|
@@ -48,11 +63,11 @@ class DisplayMixin:
|
|
|
48
63
|
|
|
49
64
|
max_count = max(c for _, c in display) if display else 1
|
|
50
65
|
if len(sorted_counts) > MAX_HISTOGRAM_STATES:
|
|
51
|
-
|
|
66
|
+
self._rich_print(
|
|
52
67
|
f"\n [dim]Showing top {MAX_HISTOGRAM_STATES} of "
|
|
53
68
|
f"{len(sorted_counts)} outcomes:[/dim]\n")
|
|
54
69
|
else:
|
|
55
|
-
|
|
70
|
+
self._rich_print()
|
|
56
71
|
|
|
57
72
|
for state, count in display:
|
|
58
73
|
pct = 100 * count / total
|
|
@@ -72,8 +87,8 @@ class DisplayMixin:
|
|
|
72
87
|
f"{100*rest_count/total:5.1f}%", "[dim](remaining)[/dim]")
|
|
73
88
|
|
|
74
89
|
try:
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
self._rich_print(table)
|
|
91
|
+
self._rich_print()
|
|
77
92
|
except UnicodeEncodeError:
|
|
78
93
|
self._print_histogram_plain(display, sorted_counts, total)
|
|
79
94
|
|
|
@@ -122,7 +137,7 @@ class DisplayMixin:
|
|
|
122
137
|
table.add_column("State", justify="right", style="bold")
|
|
123
138
|
table.add_column("Amplitude", justify="right")
|
|
124
139
|
table.add_column("P", justify="right")
|
|
125
|
-
|
|
140
|
+
self._rich_print(f"\n [bold]Statevector ({n} qubits):[/bold]")
|
|
126
141
|
count = 0
|
|
127
142
|
for i, amp in enumerate(sv):
|
|
128
143
|
if abs(amp) > AMPLITUDE_THRESHOLD:
|
|
@@ -140,8 +155,8 @@ class DisplayMixin:
|
|
|
140
155
|
table.add_row("...", "", f"+{remaining} more")
|
|
141
156
|
break
|
|
142
157
|
try:
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
self._rich_print(table)
|
|
159
|
+
self._rich_print()
|
|
145
160
|
return
|
|
146
161
|
except UnicodeEncodeError:
|
|
147
162
|
pass # fall through to plain-text path below
|