qubasic 0.5.1__tar.gz → 0.6.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 (63) hide show
  1. {qubasic-0.5.1 → qubasic-0.6.1}/CHANGELOG.md +37 -0
  2. {qubasic-0.5.1/qubasic.egg-info → qubasic-0.6.1}/PKG-INFO +15 -17
  3. {qubasic-0.5.1 → qubasic-0.6.1}/README.md +14 -16
  4. {qubasic-0.5.1 → qubasic-0.6.1}/pyproject.toml +16 -7
  5. {qubasic-0.5.1 → qubasic-0.6.1/qubasic.egg-info}/PKG-INFO +15 -17
  6. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic.egg-info/SOURCES.txt +4 -2
  7. qubasic-0.6.1/qubasic.egg-info/entry_points.txt +2 -0
  8. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic.egg-info/top_level.txt +0 -1
  9. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/__init__.py +1 -1
  10. qubasic-0.6.1/qubasic_core/__main__.py +6 -0
  11. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/analysis.py +53 -25
  12. qubasic-0.5.1/qubasic.py → qubasic-0.6.1/qubasic_core/cli.py +49 -34
  13. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/control_flow.py +89 -20
  14. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/debug.py +1 -0
  15. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/demos.py +1 -0
  16. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/display.py +22 -7
  17. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/engine.py +2 -1
  18. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/engine_state.py +11 -1
  19. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/exec_context.py +1 -1
  20. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/executor.py +35 -0
  21. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/expression.py +96 -20
  22. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/file_io.py +62 -15
  23. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/gates.py +2 -2
  24. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/help_text.py +3 -1
  25. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/locc_commands.py +30 -4
  26. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/locc_engine.py +86 -34
  27. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/locc_execution.py +21 -10
  28. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/memory.py +62 -10
  29. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/noise_mixin.py +17 -5
  30. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/parser.py +8 -4
  31. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/patterns.py +2 -2
  32. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/profiler.py +36 -20
  33. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/program_mgmt.py +2 -4
  34. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/qol.py +8 -16
  35. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/statements.py +8 -0
  36. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/terminal.py +225 -54
  37. qubasic-0.6.1/tests/test_features.py +2654 -0
  38. qubasic-0.6.1/tests/test_qubasic.py +1438 -0
  39. qubasic-0.5.1/qubasic.egg-info/entry_points.txt +0 -2
  40. qubasic-0.5.1/qubasic_core/__main__.py +0 -6
  41. {qubasic-0.5.1 → qubasic-0.6.1}/LICENSE +0 -0
  42. {qubasic-0.5.1 → qubasic-0.6.1}/MANIFEST.in +0 -0
  43. {qubasic-0.5.1 → qubasic-0.6.1}/examples/bell.qb +0 -0
  44. {qubasic-0.5.1 → qubasic-0.6.1}/examples/grover3.qb +0 -0
  45. {qubasic-0.5.1 → qubasic-0.6.1}/examples/locc_teleport.qb +0 -0
  46. {qubasic-0.5.1 → qubasic-0.6.1}/examples/sweep_rx.qb +0 -0
  47. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic.egg-info/dependency_links.txt +0 -0
  48. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic.egg-info/requires.txt +0 -0
  49. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/backend.py +0 -0
  50. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/classic.py +0 -0
  51. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/errors.py +0 -0
  52. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/io_protocol.py +0 -0
  53. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/locc.py +0 -0
  54. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/locc_display.py +0 -0
  55. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/mock_backend.py +0 -0
  56. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/protocol.py +0 -0
  57. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/scope.py +0 -0
  58. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/screen.py +0 -0
  59. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/state_display.py +0 -0
  60. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/strings.py +0 -0
  61. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/subs.py +0 -0
  62. {qubasic-0.5.1 → qubasic-0.6.1}/qubasic_core/sweep.py +0 -0
  63. {qubasic-0.5.1 → qubasic-0.6.1}/setup.cfg +0 -0
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1 (2026-06-18)
4
+
5
+ ### Fixed
6
+ - Changing noise strength between runs (for example `NOISE amplitude_damping 0.1` then `0.9`) now rebuilds the circuit instead of reusing the first noise level, so sequential noise comparisons are correct.
7
+
8
+ ### Changed
9
+ - The CLI now lives in `qubasic_core/cli.py`; the `qubasic` console script and `python -m qubasic_core` are unchanged, but the former top-level `qubasic` module is removed. Test suites moved under `tests/`.
10
+
11
+ ## 0.6.0 (2026-06-18)
12
+
13
+ Correctness, scalability, and usability improvements.
14
+
15
+ ### Fixed
16
+ - **Time-travel debugging now works**: STEP records statevector checkpoints, so REWIND/FORWARD/HISTORY operate (previously `_checkpoint_sv` was never called).
17
+ - **TRON, breakpoints, ON TIMER, ON MEASURE now fire**: the debug hooks are wired into the execution loop instead of being dead code.
18
+ - **PLOT and ANIMATE sweep correctly**: the transpiled-circuit cache key now includes variable bindings, so parameter sweeps no longer reuse the first circuit.
19
+ - **`$XXXX` memory addresses parse**: PEEK/POKE/DUMP/SYS/WAIT accept the canonical hex-address notation, not only `0x`/decimal/`&H`.
20
+ - **Multi-line IF/ELSE/END IF**: the untaken branch is properly skipped (previously both branches ran).
21
+ - **ON ERROR** reports the real ERR/ERL for build-path errors and runs handler lines through the program executor (no spurious state dumps).
22
+ - **OPTION BASE 1** is honored in array indexing; FOR with STEP 0 is rejected; bare `NEXT` and `NEXT i, j` are supported.
23
+ - **CSV/EXPORT** report correctly when only a statevector (no counts) is present.
24
+
25
+ ### Performance / scalability
26
+ - 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).
27
+ - STEP evolves the statevector incrementally; DENSITY and EXPECT avoid building large matrices; HEATMAP and STATS reuse work.
28
+
29
+ ### Quantum / LOCC
30
+ - LOCC depolarizing noise matches the Qiskit Aer convention; amplitude- and phase-damping channels are now simulated in the LOCC engine.
31
+ - 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.
32
+ - 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.
33
+
34
+ ### Language / interface
35
+ - User-defined TYPE record fields can be read and assigned (`p.x`); ELSEIF chains and re-parsed clauses are memoized.
36
+ - 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.
37
+ - CLI gains `--version` and `--agent`; `--json` output is enriched (statevector, variables, manifest) and emits structured errors; the agent write-sandbox engages under `--json`/`--agent`.
38
+ - `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.
39
+
3
40
  ## 0.5.0 (2026-03-30)
4
41
 
5
42
  - **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.5.1
3
+ Version: 0.6.1
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -73,11 +73,12 @@ Requires Python >= 3.10, Qiskit >= 1.0, qiskit-aer >= 0.13.
73
73
  ## Usage
74
74
 
75
75
  ```
76
- python qubasic.py Interactive REPL
77
- python qubasic.py script.qb Run a script file
78
- python qubasic.py --quiet script Suppress banner, output results only
79
- python qubasic.py --json script Machine-readable JSON output
80
- python qubasic.py --help Show CLI help
76
+ qubasic Interactive REPL (installed console script)
77
+ python -m qubasic_core Same, without installing
78
+ qubasic script.qb Run a script file
79
+ qubasic --quiet script Suppress banner, output results only
80
+ qubasic --json script Machine-readable JSON output
81
+ qubasic --help Show CLI help
81
82
  ```
82
83
 
83
84
  Headless (no REPL):
@@ -468,15 +469,9 @@ BLOCH A 0 Bloch sphere for register A, qubit 0
468
469
  LOCCINFO Protocol metrics
469
470
  ```
470
471
 
471
- SPLIT: max capacity (31+31+...), no cross-register entanglement. Use SEND/IF for classical coordination.
472
+ SPLIT: max capacity (up to 33 qubits per register), no cross-register entanglement. Use SEND/IF for classical coordination.
472
473
 
473
- JOINT: shared entanglement via SHARE. Limited to ~32 total qubits.
474
-
475
- ### Network LOCC
476
- ```
477
- CONNECT "host:port" AS C Attach remote register (local simulation stand-in)
478
- DISCONNECT C Detach
479
- ```
474
+ JOINT: shared entanglement via SHARE. Limited to 33 total qubits.
480
475
 
481
476
  ### LOCC optimization
482
477
  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).
@@ -691,7 +686,7 @@ Programs with SEND use prefix/suffix splitting: the deterministic prefix (before
691
686
  ## JSON output
692
687
 
693
688
  ```
694
- python qubasic.py --json examples/bell.qb
689
+ qubasic --json examples/bell.qb
695
690
  ```
696
691
 
697
692
  ```json
@@ -705,8 +700,9 @@ python qubasic.py --json examples/bell.qb
705
700
  ## Architecture
706
701
 
707
702
  ```
708
- qubasic.py CLI entry point
709
703
  qubasic_core/
704
+ cli.py CLI entry point (qubasic / python -m qubasic_core)
705
+ __main__.py Module entry point
710
706
  engine_state.py Engine: standalone state container
711
707
  terminal.py QBasicTerminal: REPL + command dispatch
712
708
  engine.py Constants, gate tables, numpy simulation, LOCCEngine
@@ -735,9 +731,11 @@ qubasic_core/
735
731
  demos.py Built-in demo circuits
736
732
  protocol.py TerminalProtocol (mixin contract)
737
733
  mock_backend.py MockAerSimulator for fast testing
734
+ tests/ Test suites (test_qubasic.py, test_features.py)
735
+ examples/ Sample .qb programs
738
736
  ```
739
737
 
740
- `Engine` holds all program state. `QBasicTerminal` inherits `Engine` + 16 mixins. `Engine` can be used independently for headless/agent execution.
738
+ `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
739
 
742
740
  ## License
743
741
 
@@ -40,11 +40,12 @@ Requires Python >= 3.10, Qiskit >= 1.0, qiskit-aer >= 0.13.
40
40
  ## Usage
41
41
 
42
42
  ```
43
- python qubasic.py Interactive REPL
44
- python qubasic.py script.qb Run a script file
45
- python qubasic.py --quiet script Suppress banner, output results only
46
- python qubasic.py --json script Machine-readable JSON output
47
- python qubasic.py --help Show CLI help
43
+ qubasic Interactive REPL (installed console script)
44
+ python -m qubasic_core Same, without installing
45
+ qubasic script.qb Run a script file
46
+ qubasic --quiet script Suppress banner, output results only
47
+ qubasic --json script Machine-readable JSON output
48
+ qubasic --help Show CLI help
48
49
  ```
49
50
 
50
51
  Headless (no REPL):
@@ -435,15 +436,9 @@ BLOCH A 0 Bloch sphere for register A, qubit 0
435
436
  LOCCINFO Protocol metrics
436
437
  ```
437
438
 
438
- SPLIT: max capacity (31+31+...), no cross-register entanglement. Use SEND/IF for classical coordination.
439
+ SPLIT: max capacity (up to 33 qubits per register), no cross-register entanglement. Use SEND/IF for classical coordination.
439
440
 
440
- JOINT: shared entanglement via SHARE. Limited to ~32 total qubits.
441
-
442
- ### Network LOCC
443
- ```
444
- CONNECT "host:port" AS C Attach remote register (local simulation stand-in)
445
- DISCONNECT C Detach
446
- ```
441
+ JOINT: shared entanglement via SHARE. Limited to 33 total qubits.
447
442
 
448
443
  ### LOCC optimization
449
444
  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).
@@ -658,7 +653,7 @@ Programs with SEND use prefix/suffix splitting: the deterministic prefix (before
658
653
  ## JSON output
659
654
 
660
655
  ```
661
- python qubasic.py --json examples/bell.qb
656
+ qubasic --json examples/bell.qb
662
657
  ```
663
658
 
664
659
  ```json
@@ -672,8 +667,9 @@ python qubasic.py --json examples/bell.qb
672
667
  ## Architecture
673
668
 
674
669
  ```
675
- qubasic.py CLI entry point
676
670
  qubasic_core/
671
+ cli.py CLI entry point (qubasic / python -m qubasic_core)
672
+ __main__.py Module entry point
677
673
  engine_state.py Engine: standalone state container
678
674
  terminal.py QBasicTerminal: REPL + command dispatch
679
675
  engine.py Constants, gate tables, numpy simulation, LOCCEngine
@@ -702,9 +698,11 @@ qubasic_core/
702
698
  demos.py Built-in demo circuits
703
699
  protocol.py TerminalProtocol (mixin contract)
704
700
  mock_backend.py MockAerSimulator for fast testing
701
+ tests/ Test suites (test_qubasic.py, test_features.py)
702
+ examples/ Sample .qb programs
705
703
  ```
706
704
 
707
- `Engine` holds all program state. `QBasicTerminal` inherits `Engine` + 16 mixins. `Engine` can be used independently for headless/agent execution.
705
+ `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
706
 
709
707
  ## License
710
708
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.5.1"
7
+ version = "0.6.1"
8
8
  description = "Quantum BASIC Interactive Terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -42,10 +42,9 @@ charts = ["plotille>=5.0"]
42
42
  readline = ["pyreadline3>=3.4; sys_platform == 'win32'"]
43
43
 
44
44
  [project.scripts]
45
- qubasic = "qubasic:main"
45
+ qubasic = "qubasic_core.cli:main"
46
46
 
47
47
  [tool.setuptools]
48
- py-modules = ["qubasic"]
49
48
  packages = ["qubasic_core"]
50
49
 
51
50
  [tool.setuptools.package-data]
@@ -66,9 +65,19 @@ skip_empty = true
66
65
 
67
66
  [tool.mypy]
68
67
  python_version = "3.10"
69
- warn_return_any = true
68
+ ignore_missing_imports = true
70
69
  warn_unused_configs = true
71
- disallow_untyped_defs = true
72
- check_untyped_defs = true
73
70
  warn_redundant_casts = true
74
- warn_unused_ignores = true
71
+ # QBasicTerminal composes 20 mixins; each mixin accesses attributes the host
72
+ # class provides, which mypy cannot resolve in isolation. The codebase is
73
+ # intentionally gradually typed, so the strict per-method settings and the
74
+ # cross-mixin attribute check are disabled while keeping mypy useful for real
75
+ # type mismatches, undefined names, and signature checks.
76
+ disallow_untyped_defs = false
77
+ check_untyped_defs = false
78
+ warn_return_any = false
79
+ warn_unused_ignores = false
80
+ # attr-defined: cross-mixin attributes the host class provides.
81
+ # has-type: types behind the lazy __getattr__ re-exports cannot be inferred.
82
+ # misc/union-attr: mixin base-class attribute overlap and optional access.
83
+ disable_error_code = ["attr-defined", "has-type", "misc", "union-attr"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.5.1
3
+ Version: 0.6.1
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -73,11 +73,12 @@ Requires Python >= 3.10, Qiskit >= 1.0, qiskit-aer >= 0.13.
73
73
  ## Usage
74
74
 
75
75
  ```
76
- python qubasic.py Interactive REPL
77
- python qubasic.py script.qb Run a script file
78
- python qubasic.py --quiet script Suppress banner, output results only
79
- python qubasic.py --json script Machine-readable JSON output
80
- python qubasic.py --help Show CLI help
76
+ qubasic Interactive REPL (installed console script)
77
+ python -m qubasic_core Same, without installing
78
+ qubasic script.qb Run a script file
79
+ qubasic --quiet script Suppress banner, output results only
80
+ qubasic --json script Machine-readable JSON output
81
+ qubasic --help Show CLI help
81
82
  ```
82
83
 
83
84
  Headless (no REPL):
@@ -468,15 +469,9 @@ BLOCH A 0 Bloch sphere for register A, qubit 0
468
469
  LOCCINFO Protocol metrics
469
470
  ```
470
471
 
471
- SPLIT: max capacity (31+31+...), no cross-register entanglement. Use SEND/IF for classical coordination.
472
+ SPLIT: max capacity (up to 33 qubits per register), no cross-register entanglement. Use SEND/IF for classical coordination.
472
473
 
473
- JOINT: shared entanglement via SHARE. Limited to ~32 total qubits.
474
-
475
- ### Network LOCC
476
- ```
477
- CONNECT "host:port" AS C Attach remote register (local simulation stand-in)
478
- DISCONNECT C Detach
479
- ```
474
+ JOINT: shared entanglement via SHARE. Limited to 33 total qubits.
480
475
 
481
476
  ### LOCC optimization
482
477
  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).
@@ -691,7 +686,7 @@ Programs with SEND use prefix/suffix splitting: the deterministic prefix (before
691
686
  ## JSON output
692
687
 
693
688
  ```
694
- python qubasic.py --json examples/bell.qb
689
+ qubasic --json examples/bell.qb
695
690
  ```
696
691
 
697
692
  ```json
@@ -705,8 +700,9 @@ python qubasic.py --json examples/bell.qb
705
700
  ## Architecture
706
701
 
707
702
  ```
708
- qubasic.py CLI entry point
709
703
  qubasic_core/
704
+ cli.py CLI entry point (qubasic / python -m qubasic_core)
705
+ __main__.py Module entry point
710
706
  engine_state.py Engine: standalone state container
711
707
  terminal.py QBasicTerminal: REPL + command dispatch
712
708
  engine.py Constants, gate tables, numpy simulation, LOCCEngine
@@ -735,9 +731,11 @@ qubasic_core/
735
731
  demos.py Built-in demo circuits
736
732
  protocol.py TerminalProtocol (mixin contract)
737
733
  mock_backend.py MockAerSimulator for fast testing
734
+ tests/ Test suites (test_qubasic.py, test_features.py)
735
+ examples/ Sample .qb programs
738
736
  ```
739
737
 
740
- `Engine` holds all program state. `QBasicTerminal` inherits `Engine` + 16 mixins. `Engine` can be used independently for headless/agent execution.
738
+ `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
739
 
742
740
  ## License
743
741
 
@@ -3,7 +3,6 @@ LICENSE
3
3
  MANIFEST.in
4
4
  README.md
5
5
  pyproject.toml
6
- qubasic.py
7
6
  examples/bell.qb
8
7
  examples/grover3.qb
9
8
  examples/locc_teleport.qb
@@ -19,6 +18,7 @@ qubasic_core/__main__.py
19
18
  qubasic_core/analysis.py
20
19
  qubasic_core/backend.py
21
20
  qubasic_core/classic.py
21
+ qubasic_core/cli.py
22
22
  qubasic_core/control_flow.py
23
23
  qubasic_core/debug.py
24
24
  qubasic_core/demos.py
@@ -54,4 +54,6 @@ qubasic_core/statements.py
54
54
  qubasic_core/strings.py
55
55
  qubasic_core/subs.py
56
56
  qubasic_core/sweep.py
57
- qubasic_core/terminal.py
57
+ qubasic_core/terminal.py
58
+ tests/test_features.py
59
+ tests/test_qubasic.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qubasic = qubasic_core.cli:main
@@ -1,2 +1 @@
1
- qubasic
2
1
  qubasic_core
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.5.1'
31
+ __version__ = '0.6.1'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -0,0 +1,6 @@
1
+ """Run the CLI as a module: python -m qubasic_core [args]."""
2
+
3
+ from qubasic_core.cli import main
4
+
5
+ if __name__ == '__main__':
6
+ main()
@@ -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 X 0"""
28
- sv = self._active_sv
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: use STATE A / STATE B for per-register inspection")
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
- n = self._active_nqubits
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 specified qubits vs rest.
58
- Examples: ENTROPY 0 | ENTROPY 0 1 | ENTROPY (defaults to qubit 0)"""
59
- sv = self._active_sv
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.io.writeln("?NO STATE RUN first")
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 (or partial trace for small systems)."""
86
- sv = self._active_sv
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.io.writeln("?NO STATE RUN first")
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" Purity: {np.real(np.trace(rho @ rho)):.6f}")
97
- self.io.write(f" Von Neumann entropy: ")
98
- eigvals = np.linalg.eigvalsh(rho)
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 = []
@@ -154,7 +182,7 @@ class AnalysisMixin:
154
182
  return
155
183
  n = self._active_nqubits
156
184
  sv = np.ascontiguousarray(sv).ravel()
157
- checks = []
185
+ checks: list = []
158
186
 
159
187
  # 1. SV normalization
160
188
  norm = float(np.sum(np.abs(sv)**2))
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- QUBASIC — Quantum BASIC Interactive Terminal
3
+ QUBASIC — command-line entry point.
4
4
 
5
5
  Usage:
6
- python qubasic.py Interactive REPL
7
- python qubasic.py script.qb Run a script file
6
+ qubasic Interactive REPL (installed console script)
7
+ python -m qubasic_core Same, run as a module
8
+ qubasic script.qb Run a script file
8
9
  """
9
10
 
10
11
  import sys
@@ -50,14 +51,15 @@ def main():
50
51
  import json as _json
51
52
  os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
52
53
 
54
+ from qubasic_core import __version__
55
+
53
56
  args = sys.argv[1:]
54
57
  quiet = '--quiet' in args or '-q' in args
55
58
  json_mode = '--json' in args
59
+ agent_mode = '--agent' in args
56
60
  seed_val = None
57
- if quiet:
58
- args = [a for a in args if a not in ('--quiet', '-q')]
59
- if json_mode:
60
- args = [a for a in args if a != '--json']
61
+ for flag in ('--quiet', '-q', '--json', '--agent'):
62
+ args = [a for a in args if a != flag]
61
63
  # Parse --seed N
62
64
  filtered = []
63
65
  i = 0
@@ -70,8 +72,11 @@ def main():
70
72
  i += 1
71
73
  args = filtered
72
74
 
75
+ if any(a in ('-v', '--version') for a in args):
76
+ print(f"QUBASIC {__version__}")
77
+ sys.exit(0)
78
+
73
79
  if any(a in ('-h', '--help') for a in args):
74
- from qubasic_core import __version__
75
80
  print(f"QUBASIC {__version__} — Quantum BASIC Interactive Terminal")
76
81
  print()
77
82
  print("Usage:")
@@ -79,13 +84,17 @@ def main():
79
84
  print(" qubasic script.qb Run a script file")
80
85
  print(" qubasic --quiet script Suppress banner and progress")
81
86
  print(" qubasic --json script Output results as JSON")
87
+ print(" qubasic --agent script Confine file writes to the working dir")
82
88
  print(" qubasic --seed N script Set random seed for reproducibility")
89
+ print(" qubasic --version Show version")
83
90
  print(" qubasic --help Show this help")
84
91
  print()
85
92
  print("Type HELP inside the REPL for full command reference.")
86
93
  sys.exit(0)
87
94
 
88
95
  term = QBasicTerminal()
96
+ # JSON mode implies agent use, so confine file writes to the working dir.
97
+ term.agent_mode = agent_mode or json_mode
89
98
  if seed_val is not None:
90
99
  import numpy as _np
91
100
  term._seed = seed_val
@@ -93,35 +102,41 @@ def main():
93
102
 
94
103
  if args:
95
104
  path = args[0]
96
- if os.path.isfile(path):
97
- if quiet or json_mode:
98
- import io
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())
105
+ if not os.path.isfile(path):
106
+ if json_mode:
107
+ print(_json.dumps({'error': f'FILE NOT FOUND: {path}'}, indent=2))
115
108
  else:
116
- term.print_banner()
109
+ print(f"?FILE NOT FOUND: {path}")
110
+ sys.exit(1)
111
+ if quiet or json_mode:
112
+ import io
113
+ buf = io.StringIO()
114
+ old = sys.stdout
115
+ sys.stdout = buf
116
+ err = None
117
+ try:
117
118
  run_script(path, term)
118
- # Exit code: 0 if results exist, 1 if error
119
- sys.exit(0 if term.last_counts is not None or not any(
120
- term.program.get(ln, '').strip().upper() == 'MEASURE'
121
- for ln in term.program) else 1)
119
+ except Exception as e: # surface as structured error in JSON mode
120
+ err = str(e)
121
+ finally:
122
+ sys.stdout = old
123
+ if json_mode:
124
+ if err is not None:
125
+ print(_json.dumps({'error': err}, indent=2))
126
+ sys.exit(1)
127
+ print(_json.dumps(term.result(), indent=2))
128
+ elif not quiet:
129
+ print(buf.getvalue())
130
+ if err is not None:
131
+ print(f"?ERROR: {err}")
132
+ sys.exit(1)
122
133
  else:
123
- print(f"?FILE NOT FOUND: {path}")
124
- sys.exit(1)
134
+ term.print_banner()
135
+ run_script(path, term)
136
+ # Exit 0 on success; 1 if a measured program produced no counts.
137
+ sys.exit(0 if term.last_counts is not None or not any(
138
+ term.program.get(ln, '').strip().upper() == 'MEASURE'
139
+ for ln in term.program) else 1)
125
140
  else:
126
141
  term.repl()
127
142