qubasic 0.6.0__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.6.0 → qubasic-0.6.1}/CHANGELOG.md +9 -1
  2. {qubasic-0.6.0/qubasic.egg-info → qubasic-0.6.1}/PKG-INFO +12 -8
  3. {qubasic-0.6.0 → qubasic-0.6.1}/README.md +11 -7
  4. {qubasic-0.6.0 → qubasic-0.6.1}/pyproject.toml +16 -7
  5. {qubasic-0.6.0 → qubasic-0.6.1/qubasic.egg-info}/PKG-INFO +12 -8
  6. {qubasic-0.6.0 → 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.6.0 → qubasic-0.6.1}/qubasic.egg-info/top_level.txt +0 -1
  9. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/__init__.py +1 -1
  10. qubasic-0.6.1/qubasic_core/__main__.py +6 -0
  11. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/analysis.py +1 -1
  12. qubasic-0.6.0/qubasic.py → qubasic-0.6.1/qubasic_core/cli.py +4 -3
  13. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/engine.py +2 -1
  14. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/engine_state.py +1 -0
  15. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/exec_context.py +1 -1
  16. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/expression.py +1 -1
  17. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/file_io.py +2 -1
  18. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/gates.py +2 -2
  19. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/locc_engine.py +3 -3
  20. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/locc_execution.py +5 -5
  21. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/memory.py +0 -4
  22. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/qol.py +7 -15
  23. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/terminal.py +15 -6
  24. qubasic-0.6.1/tests/test_features.py +2654 -0
  25. qubasic-0.6.1/tests/test_qubasic.py +1438 -0
  26. qubasic-0.6.0/qubasic.egg-info/entry_points.txt +0 -2
  27. qubasic-0.6.0/qubasic_core/__main__.py +0 -6
  28. {qubasic-0.6.0 → qubasic-0.6.1}/LICENSE +0 -0
  29. {qubasic-0.6.0 → qubasic-0.6.1}/MANIFEST.in +0 -0
  30. {qubasic-0.6.0 → qubasic-0.6.1}/examples/bell.qb +0 -0
  31. {qubasic-0.6.0 → qubasic-0.6.1}/examples/grover3.qb +0 -0
  32. {qubasic-0.6.0 → qubasic-0.6.1}/examples/locc_teleport.qb +0 -0
  33. {qubasic-0.6.0 → qubasic-0.6.1}/examples/sweep_rx.qb +0 -0
  34. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic.egg-info/dependency_links.txt +0 -0
  35. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic.egg-info/requires.txt +0 -0
  36. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/backend.py +0 -0
  37. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/classic.py +0 -0
  38. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/control_flow.py +0 -0
  39. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/debug.py +0 -0
  40. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/demos.py +0 -0
  41. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/display.py +0 -0
  42. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/errors.py +0 -0
  43. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/executor.py +0 -0
  44. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/help_text.py +0 -0
  45. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/io_protocol.py +0 -0
  46. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/locc.py +0 -0
  47. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/locc_commands.py +0 -0
  48. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/locc_display.py +0 -0
  49. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/mock_backend.py +0 -0
  50. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/noise_mixin.py +0 -0
  51. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/parser.py +0 -0
  52. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/patterns.py +0 -0
  53. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/profiler.py +0 -0
  54. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/program_mgmt.py +0 -0
  55. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/protocol.py +0 -0
  56. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/scope.py +0 -0
  57. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/screen.py +0 -0
  58. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/state_display.py +0 -0
  59. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/statements.py +0 -0
  60. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/strings.py +0 -0
  61. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/subs.py +0 -0
  62. {qubasic-0.6.0 → qubasic-0.6.1}/qubasic_core/sweep.py +0 -0
  63. {qubasic-0.6.0 → qubasic-0.6.1}/setup.cfg +0 -0
@@ -1,8 +1,16 @@
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
+
3
11
  ## 0.6.0 (2026-06-18)
4
12
 
5
- Correctness, scalability, and usability pass (61 fixes).
13
+ Correctness, scalability, and usability improvements.
6
14
 
7
15
  ### Fixed
8
16
  - **Time-travel debugging now works**: STEP records statevector checkpoints, so REWIND/FORWARD/HISTORY operate (previously `_checkpoint_sv` was never called).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.6.0
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):
@@ -685,7 +686,7 @@ Programs with SEND use prefix/suffix splitting: the deterministic prefix (before
685
686
  ## JSON output
686
687
 
687
688
  ```
688
- python qubasic.py --json examples/bell.qb
689
+ qubasic --json examples/bell.qb
689
690
  ```
690
691
 
691
692
  ```json
@@ -699,8 +700,9 @@ python qubasic.py --json examples/bell.qb
699
700
  ## Architecture
700
701
 
701
702
  ```
702
- qubasic.py CLI entry point
703
703
  qubasic_core/
704
+ cli.py CLI entry point (qubasic / python -m qubasic_core)
705
+ __main__.py Module entry point
704
706
  engine_state.py Engine: standalone state container
705
707
  terminal.py QBasicTerminal: REPL + command dispatch
706
708
  engine.py Constants, gate tables, numpy simulation, LOCCEngine
@@ -729,6 +731,8 @@ qubasic_core/
729
731
  demos.py Built-in demo circuits
730
732
  protocol.py TerminalProtocol (mixin contract)
731
733
  mock_backend.py MockAerSimulator for fast testing
734
+ tests/ Test suites (test_qubasic.py, test_features.py)
735
+ examples/ Sample .qb programs
732
736
  ```
733
737
 
734
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).
@@ -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):
@@ -652,7 +653,7 @@ Programs with SEND use prefix/suffix splitting: the deterministic prefix (before
652
653
  ## JSON output
653
654
 
654
655
  ```
655
- python qubasic.py --json examples/bell.qb
656
+ qubasic --json examples/bell.qb
656
657
  ```
657
658
 
658
659
  ```json
@@ -666,8 +667,9 @@ python qubasic.py --json examples/bell.qb
666
667
  ## Architecture
667
668
 
668
669
  ```
669
- qubasic.py CLI entry point
670
670
  qubasic_core/
671
+ cli.py CLI entry point (qubasic / python -m qubasic_core)
672
+ __main__.py Module entry point
671
673
  engine_state.py Engine: standalone state container
672
674
  terminal.py QBasicTerminal: REPL + command dispatch
673
675
  engine.py Constants, gate tables, numpy simulation, LOCCEngine
@@ -696,6 +698,8 @@ qubasic_core/
696
698
  demos.py Built-in demo circuits
697
699
  protocol.py TerminalProtocol (mixin contract)
698
700
  mock_backend.py MockAerSimulator for fast testing
701
+ tests/ Test suites (test_qubasic.py, test_features.py)
702
+ examples/ Sample .qb programs
699
703
  ```
700
704
 
701
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).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.6.0"
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.6.0
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):
@@ -685,7 +686,7 @@ Programs with SEND use prefix/suffix splitting: the deterministic prefix (before
685
686
  ## JSON output
686
687
 
687
688
  ```
688
- python qubasic.py --json examples/bell.qb
689
+ qubasic --json examples/bell.qb
689
690
  ```
690
691
 
691
692
  ```json
@@ -699,8 +700,9 @@ python qubasic.py --json examples/bell.qb
699
700
  ## Architecture
700
701
 
701
702
  ```
702
- qubasic.py CLI entry point
703
703
  qubasic_core/
704
+ cli.py CLI entry point (qubasic / python -m qubasic_core)
705
+ __main__.py Module entry point
704
706
  engine_state.py Engine: standalone state container
705
707
  terminal.py QBasicTerminal: REPL + command dispatch
706
708
  engine.py Constants, gate tables, numpy simulation, LOCCEngine
@@ -729,6 +731,8 @@ qubasic_core/
729
731
  demos.py Built-in demo circuits
730
732
  protocol.py TerminalProtocol (mixin contract)
731
733
  mock_backend.py MockAerSimulator for fast testing
734
+ tests/ Test suites (test_qubasic.py, test_features.py)
735
+ examples/ Sample .qb programs
732
736
  ```
733
737
 
734
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).
@@ -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.6.0'
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()
@@ -182,7 +182,7 @@ class AnalysisMixin:
182
182
  return
183
183
  n = self._active_nqubits
184
184
  sv = np.ascontiguousarray(sv).ravel()
185
- checks = []
185
+ checks: list = []
186
186
 
187
187
  # 1. SV normalization
188
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
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from enum import Enum, auto
6
+ from typing import Any
6
7
  import numpy as np
7
8
  from qubasic_core.patterns import (
8
9
  RE_LINE_NUM, RE_DEF_SINGLE, RE_DEF_BEGIN, RE_REG_INDEX,
@@ -79,7 +80,7 @@ try:
79
80
  from rich.panel import Panel as _RichPanel
80
81
  from rich.text import Text as _RichText
81
82
  _RICH = True
82
- _console = _RichConsole(highlight=False)
83
+ _console: Any = _RichConsole(highlight=False)
83
84
  except ImportError:
84
85
  _RICH = False
85
86
  _console = None
@@ -71,6 +71,7 @@ class Engine:
71
71
  self._circuit_cache_key: Any = None
72
72
  self._circuit_cache: Any = None
73
73
  self._pending_set_state: Any = None # immediate SET_STATE for next RUN
74
+ self._poke_state_prep: dict[Any, Any] = {} # qubit prep from POKE $0100
74
75
 
75
76
  # LOCC
76
77
  self.locc: Any = None
@@ -12,7 +12,7 @@ class ExecContext:
12
12
 
13
13
  sorted_lines: list[int]
14
14
  ip: int
15
- run_vars: dict[str, Any]
15
+ run_vars: Any # dict[str, Any] or a Scope wrapper
16
16
  loop_stack: list[dict[str, Any]] = field(default_factory=list)
17
17
  iteration_count: int = 0
18
18
  max_iterations: int = 100_000
@@ -116,7 +116,7 @@ class ExpressionMixin:
116
116
  'SQRT2': math.sqrt(2), 'sqrt2': math.sqrt(2),
117
117
  'True': True, 'False': False,
118
118
  }
119
- _AST_OPS = {
119
+ _AST_OPS: dict[type, Any] = {
120
120
  ast.Add: operator.add, ast.Sub: operator.sub,
121
121
  ast.Mult: operator.mul, ast.Div: operator.truediv,
122
122
  ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod,
@@ -6,6 +6,7 @@ import os
6
6
  import re
7
7
  import sys
8
8
  from pathlib import Path
9
+ from typing import Any
9
10
 
10
11
  import numpy as np
11
12
 
@@ -99,7 +100,7 @@ class FileIOMixin:
99
100
  else:
100
101
  f.write(f"LET {name} = {val}\n")
101
102
  # Save program (exclude lines injected by IMPORT)
102
- imported = getattr(self, '_imported_lines', set())
103
+ imported: set[int] = getattr(self, '_imported_lines', set())
103
104
  saved = [n for n in sorted(self.program.keys()) if n not in imported]
104
105
  for num in saved:
105
106
  f.write(f"{num} {self.program[num]}\n")
@@ -242,8 +242,8 @@ def _measure_np(
242
242
  """
243
243
  sv = np.ascontiguousarray(sv).reshape([2] * n_qubits)
244
244
  ax = n_qubits - 1 - qubit
245
- idx_0 = [slice(None)] * n_qubits
246
- idx_1 = [slice(None)] * n_qubits
245
+ idx_0: list = [slice(None)] * n_qubits
246
+ idx_1: list = [slice(None)] * n_qubits
247
247
  idx_0[ax] = 0
248
248
  idx_1[ax] = 1
249
249
  p0 = float(np.sum(np.abs(sv[tuple(idx_0)])**2))
@@ -42,7 +42,7 @@ class LOCCEngine:
42
42
  self.joint = joint
43
43
  self.noise_param = noise_param
44
44
  self.noise_type = noise_type
45
- self.classical = {}
45
+ self.classical: dict[str, int] = {}
46
46
  # Precompute offsets for JOINT mode
47
47
  self.offsets = []
48
48
  off = 0
@@ -237,8 +237,8 @@ class LOCCEngine:
237
237
  """Sample and return (per_reg_counts, joint_counts)."""
238
238
  if self.joint:
239
239
  raw = _sample_np(self.sv, self.n_total, shots)
240
- per_reg = {name: {} for name in self.names}
241
- joint = {}
240
+ per_reg: dict[str, dict[str, int]] = {name: {} for name in self.names}
241
+ joint: dict[str, int] = {}
242
242
  for state, count in raw.items():
243
243
  # Split the joint bitstring into per-register segments.
244
244
  # The joint statevector is ordered A[0..nA-1] B[0..nB-1] ...
@@ -110,8 +110,8 @@ class LOCCExecutionMixin:
110
110
  snap = self.locc.snapshot()
111
111
  snap_vars = dict(self.variables)
112
112
 
113
- per_reg = {name: {} for name in self.locc.names}
114
- counts_joint = {}
113
+ per_reg: dict[str, dict[str, int]] = {name: {} for name in self.locc.names}
114
+ counts_joint: dict[str, int] = {}
115
115
  t0 = time.time()
116
116
  progress_interval = max(1, shots // 10)
117
117
 
@@ -175,8 +175,8 @@ class LOCCExecutionMixin:
175
175
 
176
176
  if noisy and has_measure:
177
177
  # Per-shot execution so noise fires independently each time
178
- per_reg = {name: {} for name in self.locc.names}
179
- counts_joint = {}
178
+ per_reg: dict[str, dict[str, int]] = {name: {} for name in self.locc.names}
179
+ counts_joint: dict[str, int] = {}
180
180
  for _shot in range(self.shots):
181
181
  self.locc.reset()
182
182
  try:
@@ -433,7 +433,7 @@ class LOCCExecutionMixin:
433
433
  if info is None:
434
434
  raise ValueError(f"UNKNOWN GATE: {inner_name}")
435
435
  n_params, n_qubits_needed = info
436
- params = [self.eval_expr(a) for a in inv_args[:n_params]]
436
+ params = tuple(self.eval_expr(a) for a in inv_args[:n_params])
437
437
  qubits = [self._resolve_qubit(a, n_qubits=n_reg) for a in inv_args[n_params:n_params+n_qubits_needed]]
438
438
  matrix = _np_gate_matrix(inner_name, tuple(params)).conj().T
439
439
  self.locc.apply_matrix(reg, matrix, qubits)
@@ -197,14 +197,10 @@ class MemoryMixin:
197
197
  p1 = max(0.0, min(1.0, v))
198
198
  theta = 2 * math.asin(math.sqrt(p1))
199
199
  # This requires a circuit context — store for next RUN
200
- if not hasattr(self, '_poke_state_prep'):
201
- self._poke_state_prep = {}
202
200
  self._poke_state_prep[qubit] = ('RY', theta)
203
201
  self.io.writeln(f"POKE q{qubit}: P(1)={p1:.4f} -> RY({theta:.4f})")
204
202
  elif field in (1, 2, 3):
205
203
  # Bloch x, y, z — store target vector
206
- if not hasattr(self, '_poke_state_prep'):
207
- self._poke_state_prep = {}
208
204
  labels = {1: 'Bx', 2: 'By', 3: 'Bz'}
209
205
  self._poke_state_prep[(qubit, labels[field])] = v
210
206
  self.io.writeln(f"POKE q{qubit}.{labels[field]} = {v:.4f}")
@@ -13,6 +13,13 @@ from typing import Any
13
13
  import numpy as np
14
14
 
15
15
 
16
+ class _NullIO:
17
+ """IOPort that swallows output, used to silence inner runs (COMPARE/PLOT/ANIMATE)."""
18
+ def write(self, text: str) -> None: pass
19
+ def writeln(self, text: str) -> None: pass
20
+ def read_line(self, prompt: str) -> str: return ''
21
+
22
+
16
23
  # ═══════════════════════════════════════════════════════════════════════
17
24
  # Quantum spinner (#21)
18
25
  # ═══════════════════════════════════════════════════════════════════════
@@ -295,11 +302,6 @@ class QoLMixin:
295
302
  for m in [m1, m2]:
296
303
  self.sim_method = m
297
304
  old_io = self.io
298
-
299
- class _NullIO:
300
- def write(self, t: str) -> None: pass
301
- def writeln(self, t: str) -> None: pass
302
- def read_line(self, p: str) -> str: return ''
303
305
  self.io = _NullIO()
304
306
  try:
305
307
  self.cmd_run()
@@ -372,11 +374,6 @@ class QoLMixin:
372
374
  delay = float(parts[4]) / 1000 if len(parts) > 4 else 0.3
373
375
  values = [start + (end - start) * i / max(1, steps - 1) for i in range(steps)]
374
376
 
375
- class _NullIO:
376
- def write(self, t: str) -> None: pass
377
- def writeln(self, t: str) -> None: pass
378
- def read_line(self, p: str) -> str: return ''
379
-
380
377
  for i, val in enumerate(values):
381
378
  self.variables[var] = val
382
379
  old_io = self.io
@@ -486,11 +483,6 @@ class QoLMixin:
486
483
  steps = int(parts[3]) if len(parts) > 3 else 20
487
484
  values = [start + (end - start) * i / max(1, steps - 1) for i in range(steps)]
488
485
 
489
- class _NullIO:
490
- def write(self, t: str) -> None: pass
491
- def writeln(self, t: str) -> None: pass
492
- def read_line(self, p: str) -> str: return ''
493
-
494
486
  xs, ys = [], []
495
487
  for val in values:
496
488
  self.variables[var] = val
@@ -6,6 +6,7 @@ import re
6
6
  import time
7
7
  import warnings as _warnings
8
8
  from collections import OrderedDict
9
+ from typing import Any
9
10
 
10
11
  # Quiet the third-party dependency-version notice (urllib3/chardet) that
11
12
  # qiskit's transitive imports emit, so it doesn't precede the banner.
@@ -681,13 +682,13 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
681
682
  self.io.writeln("?USAGE: DELETE <line> or DELETE <start>-<end>")
682
683
  return
683
684
  if '-' in rest:
684
- a, b = rest.split('-')
685
- a, b = int(a.strip()), int(b.strip())
685
+ lo_s, hi_s = rest.split('-')
686
+ lo, hi = int(lo_s.strip()), int(hi_s.strip())
686
687
  for k in list(self.program.keys()):
687
- if a <= k <= b:
688
+ if lo <= k <= hi:
688
689
  del self.program[k]
689
690
  self._parsed.pop(k, None)
690
- self.io.writeln(f"DELETED {a}-{b}")
691
+ self.io.writeln(f"DELETED {lo}-{hi}")
691
692
  else:
692
693
  n = int(rest)
693
694
  if n in self.program:
@@ -857,6 +858,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
857
858
  def _accumulate_type_field(self, line: str) -> None:
858
859
  """Accumulate a field for a pending TYPE definition, or finalize on END TYPE."""
859
860
  from qubasic_core.engine import RE_TYPE_FIELD, RE_END_TYPE
861
+ assert self._pending_type is not None # only called while a TYPE is open
860
862
  stripped = line.strip()
861
863
  if RE_END_TYPE.match(stripped):
862
864
  # Finalize the type definition
@@ -1081,7 +1083,14 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1081
1083
 
1082
1084
  def _run_with_fallback(self, qc, backend_opts: dict, method: str) -> 'Any':
1083
1085
  """Run the circuit with shots, handling GPU and stabilizer fallbacks."""
1084
- _noise_key = str(self._noise_model) if self._noise_model else None
1086
+ # str(NoiseModel) does not vary with channel parameters (amplitude_damping
1087
+ # 0.1 and 0.9 stringify identically), so fingerprint noise by the exact
1088
+ # NOISE command and the per-qubit memory-mapped noise instead — otherwise
1089
+ # changing noise strength between runs reuses the first circuit.
1090
+ _noise_key = (
1091
+ getattr(self, '_noise_spec', None),
1092
+ tuple(sorted(getattr(self, '_qubit_noise', {}).items())),
1093
+ )
1085
1094
  # Numeric variable bindings affect gate parameters baked into the
1086
1095
  # transpiled circuit, so they must be part of the cache key. Without
1087
1096
  # this, parameter sweeps that re-run via cmd_run (PLOT, ANIMATE) reuse
@@ -2173,7 +2182,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
2173
2182
  if self.last_circuit is None:
2174
2183
  self.io.writeln("?NO CIRCUIT — RUN first")
2175
2184
  return
2176
- ops = {}
2185
+ ops: dict[str, int] = {}
2177
2186
  for inst in self.last_circuit.data:
2178
2187
  name = inst.operation.name
2179
2188
  ops[name] = ops.get(name, 0) + 1