qubasic 0.3.1__tar.gz → 0.4.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.
Files changed (59) hide show
  1. qubasic-0.4.0/CHANGELOG.md +63 -0
  2. {qubasic-0.3.1/qubasic.egg-info → qubasic-0.4.0}/PKG-INFO +1 -1
  3. {qubasic-0.3.1 → qubasic-0.4.0}/pyproject.toml +4 -1
  4. {qubasic-0.3.1 → qubasic-0.4.0/qubasic.egg-info}/PKG-INFO +1 -1
  5. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/__init__.py +1 -1
  6. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/analysis.py +95 -13
  7. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/demos.py +64 -1
  8. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/engine_state.py +2 -0
  9. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/executor.py +1 -2
  10. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/locc_commands.py +49 -1
  11. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/locc_engine.py +44 -3
  12. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/locc_execution.py +45 -1
  13. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/memory.py +12 -2
  14. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/mock_backend.py +2 -0
  15. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/noise_mixin.py +24 -0
  16. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/state_display.py +16 -14
  17. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/sweep.py +1 -1
  18. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/terminal.py +553 -208
  19. qubasic-0.3.1/CHANGELOG.md +0 -34
  20. {qubasic-0.3.1 → qubasic-0.4.0}/LICENSE +0 -0
  21. {qubasic-0.3.1 → qubasic-0.4.0}/MANIFEST.in +0 -0
  22. {qubasic-0.3.1 → qubasic-0.4.0}/README.md +0 -0
  23. {qubasic-0.3.1 → qubasic-0.4.0}/examples/bell.qb +0 -0
  24. {qubasic-0.3.1 → qubasic-0.4.0}/examples/grover3.qb +0 -0
  25. {qubasic-0.3.1 → qubasic-0.4.0}/examples/locc_teleport.qb +0 -0
  26. {qubasic-0.3.1 → qubasic-0.4.0}/examples/sweep_rx.qb +0 -0
  27. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.egg-info/SOURCES.txt +0 -0
  28. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.egg-info/dependency_links.txt +0 -0
  29. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.egg-info/entry_points.txt +0 -0
  30. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.egg-info/requires.txt +0 -0
  31. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.egg-info/top_level.txt +0 -0
  32. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.py +0 -0
  33. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/__main__.py +0 -0
  34. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/backend.py +0 -0
  35. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/classic.py +0 -0
  36. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/control_flow.py +0 -0
  37. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/debug.py +0 -0
  38. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/display.py +0 -0
  39. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/engine.py +0 -0
  40. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/errors.py +0 -0
  41. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/exec_context.py +0 -0
  42. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/expression.py +0 -0
  43. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/file_io.py +0 -0
  44. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/gates.py +0 -0
  45. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/help_text.py +0 -0
  46. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/io_protocol.py +0 -0
  47. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/locc.py +0 -0
  48. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/locc_display.py +0 -0
  49. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/parser.py +0 -0
  50. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/patterns.py +0 -0
  51. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/profiler.py +0 -0
  52. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/program_mgmt.py +0 -0
  53. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/protocol.py +0 -0
  54. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/scope.py +0 -0
  55. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/screen.py +0 -0
  56. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/statements.py +0 -0
  57. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/strings.py +0 -0
  58. {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/subs.py +0 -0
  59. {qubasic-0.3.1 → qubasic-0.4.0}/setup.cfg +0 -0
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0 (2026-03-29)
4
+
5
+ - **Noise correctness**: transpile with optimization_level=0 when noisy so gates survive for noise attachment
6
+ - **Noisy statevector**: STATE/BLOCH/DENSITY now reflect the noisy executed state, not the ideal state
7
+ - **LOCC noise**: Monte Carlo depolarizing noise in the numpy LOCC engine with per-shot execution
8
+ - **GPU**: _make_backend centralizes device flag to all execution paths; graceful probe and fallback
9
+ - **cmd_run decomposition**: extracted _run_no_measure, _run_with_fallback, _extract_statevector, _finalize_run, _select_method, _build_backend_opts, _run_kwargs
10
+ - **State consistency**: _active_sv/_active_nqubits unify LOCC and standard paths for all state commands
11
+ - **SPLIT mode**: EXPECT/DENSITY correctly report that per-register commands are needed
12
+ - **Non-depolarizing noise warning**: entering LOCC mode with unsupported noise types warns explicitly
13
+ - SEED command for deterministic reproducible results
14
+ - VERSION command with build ID, simulator versions, and feature flags
15
+ - PROBE command: one-shot exercise of CPU, noise, LOCC, conditional, and combined paths
16
+ - CONSISTENCY command: cross-check SV norm, purity, Bloch vectors, EXPECT, and histogram
17
+ - METHOD capability map: real probing of each method and GPU availability
18
+ - HELP STATUS: tags all 93 commands as native/experimental/partial
19
+ - CATALOG shows backend behind each SYS routine
20
+ - RUN prints method, device, noise params in summary line
21
+ - Demo self-verification: Bell, GHZ, Grover, Deutsch, BV, Superdense auto-check with pass/fail thresholds
22
+ - Teleportation fidelity output with X-basis verification
23
+ - LOCCINFO: entanglement creation, correction log, branch statistics, noise status
24
+ - Method-device pre-check blocks incompatible combinations before execution
25
+ - Runtime errors identify failing subsystem (GPU/noise/stabilizer/MPS)
26
+ - Run manifest captures all execution parameters for replay
27
+ - Correction log in LOCC engine tracks SEND outcomes
28
+ - NOISE INFO prints exact channels, operations, qubits
29
+ - 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)
30
+ - real_sim pytest marker for tests requiring actual Qiskit Aer simulation
31
+
32
+ ## 0.3.1 (2026-03-29)
33
+
34
+ - Fix f-string backslash escapes that broke import on Python 3.10/3.11
35
+
36
+ ## 0.3.0 (2026-03-28)
37
+
38
+ - FUNCTION return value fix, APPLY_CIRCUIT in programs, stabilizer fallback
39
+ - Bump to 0.3.0
40
+
41
+ ## 0.2.0 (2026-03-28)
42
+
43
+ - Rename qbasic -> qubasic everywhere (PyPI name conflict)
44
+
45
+ ## 0.1.0 (2026-03-28)
46
+
47
+ Initial PyPI release.
48
+
49
+ - BASIC REPL with line-numbered program editing
50
+ - 30+ quantum gates (H, X, Y, Z, CX, CCX, RX, RY, RZ, CP, SWAP, etc.)
51
+ - LOCC mode: 2-26 party distributed quantum simulation (SPLIT and JOINT)
52
+ - Full BASIC language: FOR/NEXT, WHILE/WEND, DO/LOOP, SELECT CASE, SUB/FUNCTION, IF/THEN/ELSE
53
+ - AST-based expression evaluator (no eval)
54
+ - Noise models: depolarizing, amplitude damping, phase flip, thermal, readout, combined, Pauli, reset
55
+ - Debugging: STEP, TRON/TROFF, breakpoints, watch, time-travel (REWIND/FORWARD), PROFILE
56
+ - Memory map: PEEK/POKE/SYS/DUMP/MAP/MONITOR
57
+ - Analysis: EXPECT, ENTROPY, DENSITY, SWEEP, BENCH, RAM
58
+ - File I/O: SAVE/LOAD/INCLUDE/IMPORT, OPEN/CLOSE/PRINT#/INPUT#, CSV, OpenQASM 3.0 export
59
+ - 12 built-in demos: Bell, GHZ, Grover, QFT, Deutsch-Jozsa, Bernstein-Vazirani, Superdense, Teleport, LOCC
60
+ - JSON output mode for agent/pipeline integration
61
+ - String variable resolution in PRINT (LEFT$, RIGHT$, CHR$, concatenation)
62
+ - DEF FN and parameterized DEF subroutine invocation
63
+ - Trusted publisher CI/CD to PyPI
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.3.1"
7
+ version = "0.4.0"
8
8
  description = "Quantum BASIC Interactive Terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -53,6 +53,9 @@ packages = ["qubasic_core"]
53
53
  [tool.setuptools.data-files]
54
54
  "share/qubasic/examples" = ["examples/*.qb"]
55
55
 
56
+ [tool.pytest.ini_options]
57
+ markers = ["real_sim: test requires real Qiskit Aer simulation (no mock)"]
58
+
56
59
  [tool.coverage.run]
57
60
  source = ["qubasic_core"]
58
61
  omit = ["*/test_*"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.3.1'
31
+ __version__ = '0.4.0'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -25,8 +25,12 @@ class AnalysisMixin:
25
25
  def cmd_expect(self, rest: str) -> None:
26
26
  """EXPECT <pauli> [qubits] — compute expectation value.
27
27
  Examples: EXPECT Z 0, EXPECT ZZ 0 1, EXPECT X 0"""
28
- if self.last_sv is None:
29
- self.io.writeln("?NO STATE RUN first")
28
+ sv = self._active_sv
29
+ if sv is None:
30
+ if self.locc_mode and self.locc and not self.locc.joint:
31
+ self.io.writeln("?SPLIT mode: use STATE A / STATE B for per-register inspection")
32
+ else:
33
+ self.io.writeln("?NO STATE — RUN first")
30
34
  return
31
35
  parts = rest.split()
32
36
  if not parts:
@@ -35,16 +39,16 @@ class AnalysisMixin:
35
39
  pauli_str = parts[0].upper()
36
40
  qubits = [int(q) for q in parts[1:]] if len(parts) > 1 else list(range(len(pauli_str)))
37
41
 
42
+ n = self._active_nqubits
38
43
  try:
39
44
  from qiskit.quantum_info import Statevector, SparsePauliOp
40
- sv = Statevector(np.ascontiguousarray(self.last_sv).ravel())
41
- # Build Pauli string for full system
42
- full_pauli = ['I'] * self.num_qubits
45
+ sv_q = Statevector(np.ascontiguousarray(sv).ravel())
46
+ full_pauli = ['I'] * n
43
47
  for i, p in enumerate(pauli_str):
44
48
  if i < len(qubits):
45
- full_pauli[self.num_qubits - 1 - qubits[i]] = p
49
+ full_pauli[n - 1 - qubits[i]] = p
46
50
  op = SparsePauliOp(''.join(full_pauli))
47
- val = sv.expectation_value(op)
51
+ val = sv_q.expectation_value(op)
48
52
  self.io.writeln(f" <{pauli_str}> on qubits {qubits} = {val.real:.6f}")
49
53
  except Exception as e:
50
54
  self.io.writeln(f"?EXPECT ERROR: {e}")
@@ -52,17 +56,18 @@ class AnalysisMixin:
52
56
  def cmd_entropy(self, rest: str = '') -> None:
53
57
  """ENTROPY [qubits] — entanglement entropy of specified qubits vs rest.
54
58
  Examples: ENTROPY 0 | ENTROPY 0 1 | ENTROPY (defaults to qubit 0)"""
55
- if self.last_sv is None:
59
+ sv = self._active_sv
60
+ if sv is None:
56
61
  self.io.writeln("?NO STATE — RUN first")
57
62
  return
58
63
  if rest.strip():
59
64
  partition_a = [int(q) for q in rest.replace(',', ' ').split() if q.strip()]
60
65
  else:
61
66
  partition_a = [0]
62
- n = self.num_qubits
67
+ n = self._active_nqubits
63
68
  try:
64
69
  from qiskit.quantum_info import Statevector, entropy, partial_trace
65
- sv_obj = Statevector(np.ascontiguousarray(self.last_sv).ravel())
70
+ sv_obj = Statevector(np.ascontiguousarray(sv).ravel())
66
71
  keep = partition_a
67
72
  rho_a = partial_trace(sv_obj, [q for q in range(n) if q not in keep])
68
73
  ent = entropy(rho_a, base=2)
@@ -78,12 +83,13 @@ class AnalysisMixin:
78
83
 
79
84
  def cmd_density(self) -> None:
80
85
  """Show density matrix (or partial trace for small systems)."""
81
- if self.last_sv is None:
86
+ sv = self._active_sv
87
+ if sv is None:
82
88
  self.io.writeln("?NO STATE — RUN first")
83
89
  return
84
- sv = np.ascontiguousarray(self.last_sv).ravel()
90
+ sv = np.ascontiguousarray(sv).ravel()
85
91
  rho = np.outer(sv, sv.conj())
86
- n = self.num_qubits
92
+ n = self._active_nqubits
87
93
  dim = 2**n
88
94
  if dim > 16:
89
95
  self.io.writeln(f" Density matrix: {dim}x{dim} (too large to display)")
@@ -140,6 +146,82 @@ class AnalysisMixin:
140
146
  self.io.writeln(f" {n:>8} {method:>20} FAILED: {e}")
141
147
  self.io.writeln('')
142
148
 
149
+ def cmd_consistency(self, rest: str = '') -> None:
150
+ """CONSISTENCY — cross-check histogram, SV, density, Bloch, and EXPECT."""
151
+ sv = self._active_sv
152
+ if sv is None:
153
+ self.io.writeln("?NO STATE — RUN first")
154
+ return
155
+ n = self._active_nqubits
156
+ sv = np.ascontiguousarray(sv).ravel()
157
+ checks = []
158
+
159
+ # 1. SV normalization
160
+ norm = float(np.sum(np.abs(sv)**2))
161
+ ok = abs(norm - 1.0) < 1e-6
162
+ checks.append(('SV norm == 1', ok, f"{norm:.8f}"))
163
+
164
+ # 2. Density matrix purity
165
+ rho = np.outer(sv, sv.conj())
166
+ purity = float(np.real(np.trace(rho @ rho)))
167
+ ok2 = purity <= 1.0 + 1e-6
168
+ checks.append(('Purity <= 1', ok2, f"{purity:.8f}"))
169
+
170
+ # 3. Bloch vector length <= 1 for each qubit
171
+ bloch_ok = True
172
+ for q in range(min(n, 8)):
173
+ x, y, z = self._bloch_vector(sv, q, n)
174
+ r = (x**2 + y**2 + z**2) ** 0.5
175
+ if r > 1.0 + 1e-4:
176
+ bloch_ok = False
177
+ break
178
+ checks.append(('Bloch |r| <= 1', bloch_ok, ''))
179
+
180
+ # 4. EXPECT Z on qubit 0 matches P(0) - P(1)
181
+ try:
182
+ from qiskit.quantum_info import Statevector, SparsePauliOp
183
+ sv_q = Statevector(sv)
184
+ pauli_z = ['I'] * n
185
+ pauli_z[n - 1] = 'Z'
186
+ op = SparsePauliOp(''.join(pauli_z))
187
+ ez = float(sv_q.expectation_value(op).real)
188
+ # Compare with direct calculation
189
+ sv_t = sv.reshape([2] * n)
190
+ ax = n - 1
191
+ t0 = np.moveaxis(sv_t, ax, 0)[0].flatten()
192
+ t1 = np.moveaxis(sv_t, ax, 0)[1].flatten()
193
+ p0 = float(np.sum(np.abs(t0)**2))
194
+ p1 = float(np.sum(np.abs(t1)**2))
195
+ ez_direct = p0 - p1
196
+ ok4 = abs(ez - ez_direct) < 1e-6
197
+ checks.append(('<Z> consistent', ok4, f"qiskit={ez:.6f} direct={ez_direct:.6f}"))
198
+ except Exception as _e:
199
+ checks.append(('<Z> consistent', None, f"skip: {_e}"))
200
+
201
+ # 5. Histogram vs SV (if counts available)
202
+ if self.last_counts:
203
+ total = sum(self.last_counts.values())
204
+ hist_p0 = self.last_counts.get('0' * n, 0) / total
205
+ sv_p0 = float(np.abs(sv[0])**2)
206
+ # Loose check: histogram is statistical, allow 10% deviation
207
+ ok5 = abs(hist_p0 - sv_p0) < 0.15 or total < 50
208
+ checks.append(('Hist~SV P(|0>)', ok5,
209
+ f"hist={hist_p0:.3f} sv={sv_p0:.3f}"))
210
+
211
+ self.io.writeln(f"\n Consistency checks ({n} qubits):")
212
+ all_pass = True
213
+ for name, ok, detail in checks:
214
+ if ok is None:
215
+ status = 'SKIP'
216
+ elif ok:
217
+ status = 'PASS'
218
+ else:
219
+ status = 'FAIL'
220
+ all_pass = False
221
+ extra = f" {detail}" if detail else ""
222
+ self.io.writeln(f" {name:25s} {status}{extra}")
223
+ self.io.writeln(f"\n {'ALL CONSISTENT' if all_pass else 'INCONSISTENCY DETECTED'}")
224
+
143
225
  def cmd_ram(self) -> None:
144
226
  """RAM — show memory budget, per-instance cost, and parallelism estimates."""
145
227
  ram = _get_ram_gb()
@@ -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:
@@ -36,6 +36,29 @@ class DemoMixin:
36
36
  return
37
37
  demos[name]()
38
38
 
39
+ def _verify_demo(self, expected_states: list[str], min_frac: float = 0.85,
40
+ label: str = '') -> bool:
41
+ """Check that expected states dominate the output.
42
+
43
+ expected_states: list of bitstrings that should capture >= min_frac of shots.
44
+ Returns True if verification passes.
45
+ """
46
+ if not self.last_counts:
47
+ self.io.writeln(f" VERIFY: no results")
48
+ return False
49
+ total = sum(self.last_counts.values())
50
+ hit = sum(self.last_counts.get(s, 0) for s in expected_states)
51
+ frac = hit / total if total > 0 else 0
52
+ tag = f" ({label})" if label else ""
53
+ if frac >= min_frac:
54
+ self.io.writeln(f" VERIFY PASS{tag}: {frac:.1%} in expected states "
55
+ f"(threshold {min_frac:.0%})")
56
+ return True
57
+ else:
58
+ self.io.writeln(f" VERIFY FAIL{tag}: {frac:.1%} in expected states "
59
+ f"(threshold {min_frac:.0%})")
60
+ return False
61
+
39
62
  def _demo_bell(self):
40
63
  self.cmd_new()
41
64
  self.num_qubits = 2
@@ -49,6 +72,7 @@ class DemoMixin:
49
72
  self.io.writeln("LOADED: Bell State (2 qubits)")
50
73
  self.cmd_list()
51
74
  self.cmd_run()
75
+ self._verify_demo(['00', '11'], 0.95, 'Bell: only |00> and |11>')
52
76
 
53
77
  def _demo_ghz(self):
54
78
  self.cmd_new()
@@ -69,6 +93,7 @@ class DemoMixin:
69
93
  self.io.writeln(f"LOADED: GHZ State ({n} qubits)")
70
94
  self.cmd_list()
71
95
  self.cmd_run()
96
+ self._verify_demo(['0' * n, '1' * n], 0.95, f'GHZ: only |{"0"*n}> and |{"1"*n}>')
72
97
 
73
98
  def _demo_teleport(self):
74
99
  self.cmd_new()
@@ -138,6 +163,7 @@ class DemoMixin:
138
163
  self.io.writeln("LOADED: Grover's Search (3 qubits, target=|101>)")
139
164
  self.cmd_list()
140
165
  self.cmd_run()
166
+ self._verify_demo(['101'], 0.85, 'Grover: target |101>')
141
167
 
142
168
  def _demo_qft(self):
143
169
  self.cmd_new()
@@ -193,6 +219,8 @@ class DemoMixin:
193
219
  self.io.writeln(" Expect: qubit 0 = 1 (balanced)")
194
220
  self.cmd_list()
195
221
  self.cmd_run()
222
+ # q0=1 means bit pattern x1 (states 01 or 11)
223
+ self._verify_demo(['01', '11'], 0.95, 'Deutsch: q0=1 (balanced)')
196
224
 
197
225
  def _demo_bernstein(self):
198
226
  self.cmd_new()
@@ -218,6 +246,8 @@ class DemoMixin:
218
246
  self.io.writeln(" Expect: measurement shows ...1011 (q3q2q1q0)")
219
247
  self.cmd_list()
220
248
  self.cmd_run()
249
+ # Ancilla (q4) is random; data qubits should read 1011
250
+ self._verify_demo(['01011', '11011'], 0.95, 'BV: secret=1011')
221
251
 
222
252
  def _demo_superdense(self):
223
253
  self.cmd_new()
@@ -243,6 +273,7 @@ class DemoMixin:
243
273
  self.io.writeln(" Expect: |11> with high probability")
244
274
  self.cmd_list()
245
275
  self.cmd_run()
276
+ self._verify_demo(['11'], 0.95, 'Superdense: message=11')
246
277
 
247
278
  def _demo_random(self):
248
279
  self.cmd_new()
@@ -322,6 +353,38 @@ class DemoMixin:
322
353
  self.io.writeln(" Alice sends |+> to Bob. Expect Bob's qubit ~ 50/50.")
323
354
  self.cmd_list()
324
355
  self.cmd_run()
356
+ # Fidelity: |+> teleported to Bob B[0]
357
+ # Z-basis: should be 50/50 (|+> has equal P(0) and P(1))
358
+ # X-basis: should be ~100% |0> (|+> is eigenstate of X with eigenvalue +1)
359
+ if self.last_counts:
360
+ total = sum(self.last_counts.values())
361
+ b0_counts = {'0': 0, '1': 0}
362
+ for state, count in self.last_counts.items():
363
+ parts = state.split('|')
364
+ if len(parts) >= 2:
365
+ bob_bit = parts[-1][-1]
366
+ b0_counts[bob_bit] = b0_counts.get(bob_bit, 0) + count
367
+ b_total = sum(b0_counts.values())
368
+ if b_total > 0:
369
+ p0 = b0_counts['0'] / b_total
370
+ z_fidelity = 1.0 - abs(p0 - 0.5) * 2
371
+ self.io.writeln(f" Z-basis fidelity: {z_fidelity:.3f} "
372
+ f"(Bob B[0]: P(0)={p0:.3f}, P(1)={1-p0:.3f})")
373
+ # X-basis verification via LOCC statevector
374
+ if self.locc and self.locc.joint:
375
+ import numpy as np
376
+ sv = np.ascontiguousarray(self.locc.sv).ravel()
377
+ n = self.locc.n_total
378
+ # Bob qubit 0 is at global index = offset_B + 0
379
+ bob_q = self.locc.offsets[1]
380
+ sv_t = sv.reshape([2] * n)
381
+ ax = n - 1 - bob_q
382
+ t0 = np.moveaxis(sv_t, ax, 0)[0].flatten()
383
+ t1 = np.moveaxis(sv_t, ax, 0)[1].flatten()
384
+ rho_01 = np.sum(np.conj(t0) * t1)
385
+ expect_x = float(2 * rho_01.real)
386
+ self.io.writeln(f" X-basis verification: <X> on Bob B[0] = {expect_x:.3f} "
387
+ f"(ideal |+> = 1.000)")
325
388
 
326
389
  def _demo_locc_coord(self):
327
390
  """Classical coordination between independent registers (SPLIT mode)."""
@@ -39,6 +39,8 @@ class Engine:
39
39
  self.sim_method: str = 'automatic'
40
40
  self.sim_device: str = 'CPU'
41
41
  self._noise_model: Any = None
42
+ self._noise_depol_p: float = 0.0
43
+ self._seed: int | None = None
42
44
  self._max_iterations: int = MAX_LOOP_ITERATIONS
43
45
  self._include_depth: int = 0
44
46
 
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING
7
7
 
8
8
  import numpy as np
9
9
  from qiskit import QuantumCircuit, transpile
10
- from qiskit_aer import AerSimulator
11
10
 
12
11
  from qubasic_core.engine import (
13
12
  GATE_TABLE, GATE_ALIASES,
@@ -624,7 +623,7 @@ class ExecutorMixin:
624
623
  run_vars=dict(self.variables), qc=qc)
625
624
  self._exec_line(line, ctx=imm_ctx)
626
625
  qc.save_statevector()
627
- backend = AerSimulator(method='statevector')
626
+ backend = self._make_backend('statevector')
628
627
  result = backend.run(transpile(qc, backend)).result()
629
628
  sv = np.array(result.get_statevector())
630
629
  self.last_sv = sv
@@ -17,6 +17,13 @@ class LOCCCommandsMixin:
17
17
  self.variables, self.eval_expr(), self.io.
18
18
  """
19
19
 
20
+ def _locc_noise_param(self) -> float:
21
+ """Return the scalar depolarizing probability for LOCC noise.
22
+
23
+ Stored by cmd_noise when a depolarizing model is set.
24
+ """
25
+ return getattr(self, '_noise_depol_p', 0.0)
26
+
20
27
  def cmd_locc(self, rest):
21
28
  args = rest.upper().split()
22
29
  if not args:
@@ -90,7 +97,8 @@ class LOCCCommandsMixin:
90
97
 
91
98
  # Pre-check RAM before allocating
92
99
  mode = "JOINT" if joint else "SPLIT"
93
- temp_eng = LOCCEngine(sizes, joint=joint)
100
+ noise_p = self._locc_noise_param()
101
+ temp_eng = LOCCEngine(sizes, joint=joint, noise_param=noise_p)
94
102
  tot, peak = temp_eng.mem_gb()
95
103
  ram = _get_ram_gb()
96
104
  if ram and tot > ram[1]:
@@ -118,6 +126,9 @@ class LOCCCommandsMixin:
118
126
  self.io.writeln(f" Joint statevector — use SHARE for pre-shared entanglement")
119
127
  if peak > 30:
120
128
  self.io.writeln(f" WARNING: large registers. Keep SHOTS low for SEND-based protocols.")
129
+ if self._noise_model and noise_p == 0:
130
+ self.io.writeln(f" WARNING: non-depolarizing noise model active but not supported "
131
+ f"in LOCC numpy path. Only NOISE depolarizing propagates to LOCC.")
121
132
 
122
133
  def cmd_send(self, rest):
123
134
  if not self.locc_mode:
@@ -172,15 +183,52 @@ class LOCCCommandsMixin:
172
183
  reg_desc = ' '.join(f"{n}={s}q" for n, s in
173
184
  zip(self.locc.names, self.locc.sizes))
174
185
  self.io.writeln(f" Registers: {reg_desc} ({self.locc.n_regs} parties)")
186
+
187
+ # Entanglement creation
188
+ n_shares = sum(1 for l in self.program.values()
189
+ if re.search(r'\bSHARE\b', l, re.IGNORECASE))
190
+ if n_shares:
191
+ self.io.writeln(f" Entanglement: {n_shares} SHARE operation(s)")
192
+
193
+ # Classical communication
175
194
  n_classical = len(self.locc.classical)
176
195
  self.io.writeln(f" Classical bits exchanged: {n_classical}")
177
196
  if self.locc.classical:
178
197
  for k, v in self.locc.classical.items():
179
198
  self.io.writeln(f" {k} = {v}")
199
+
200
+ # Measurement outcomes and correction paths
180
201
  n_sends = sum(1 for l in self.program.values()
181
202
  if re.search(r'\bSEND\b', l, re.IGNORECASE))
203
+ n_ifs = sum(1 for l in self.program.values()
204
+ if re.search(r'\bIF\b.*\bTHEN\b.*@', l, re.IGNORECASE))
182
205
  self.io.writeln(f" SEND operations: {n_sends}")
206
+ self.io.writeln(f" Conditional corrections: {n_ifs}")
183
207
  self.io.writeln(f" Communication rounds: ~{n_sends}")
208
+
209
+ # Branch statistics from classical bits
210
+ if n_sends > 0 and self.locc.classical:
211
+ bits = [v for v in self.locc.classical.values() if isinstance(v, (int, float))]
212
+ if bits:
213
+ n0 = sum(1 for b in bits if b == 0)
214
+ n1 = sum(1 for b in bits if b == 1)
215
+ self.io.writeln(f" Branch stats: {n0} zeros, {n1} ones "
216
+ f"(last run)")
217
+
218
+ # Correction log (from last run)
219
+ log = getattr(self.locc, 'correction_log', [])
220
+ if log:
221
+ self.io.writeln(f" Correction log ({len(log)} entries, last run):")
222
+ for entry in log[-20:]: # cap display
223
+ self.io.writeln(f" {entry}")
224
+ if len(log) > 20:
225
+ self.io.writeln(f" ... ({len(log) - 20} more)")
226
+
227
+ # Noise
228
+ noise_p = getattr(self.locc, 'noise_param', 0.0)
229
+ if noise_p > 0:
230
+ self.io.writeln(f" Noise: depolarizing p={noise_p}")
231
+
184
232
  tot, peak = self.locc.mem_gb()
185
233
  self.io.writeln(f" Memory: {tot:.1f} GB")
186
234
  self.io.writeln('')
@@ -26,12 +26,19 @@ class LOCCEngine:
26
26
  JOINT mode: one statevector, LOCC constraints enforced.
27
27
  """
28
28
 
29
- def __init__(self, sizes: list[int], joint: bool = False):
30
- """Initialize LOCC engine with given register sizes."""
29
+ def __init__(self, sizes: list[int], joint: bool = False,
30
+ noise_param: float = 0.0):
31
+ """Initialize LOCC engine with given register sizes.
32
+
33
+ noise_param: depolarizing probability per gate (0 = noiseless).
34
+ When > 0, after each gate application a random Pauli (X, Y, or Z)
35
+ is applied to each target qubit with probability noise_param/3.
36
+ """
31
37
  self.sizes = list(sizes)
32
38
  self.n_regs = len(self.sizes)
33
39
  self.names = [chr(ord('A') + i) for i in range(self.n_regs)]
34
40
  self.joint = joint
41
+ self.noise_param = noise_param
35
42
  self.classical = {}
36
43
  # Precompute offsets for JOINT mode
37
44
  self.offsets = []
@@ -58,6 +65,7 @@ class LOCCEngine:
58
65
  def reset(self) -> None:
59
66
  """Reset all registers to |0> and clear classical state."""
60
67
  self.classical.clear()
68
+ self.correction_log: list[str] = []
61
69
  if self.joint:
62
70
  self.sv = np.zeros(2**self.n_total, dtype=complex)
63
71
  self.sv[0] = 1.0
@@ -93,8 +101,36 @@ class LOCCEngine:
93
101
  f"Qubit {q} out of range for register {reg} (size {size})"
94
102
  )
95
103
 
104
+ def _apply_depolarizing(self, reg: str, qubits: list[int]) -> None:
105
+ """Apply single-qubit depolarizing noise to each target qubit.
106
+
107
+ For depolarizing parameter p, each qubit independently gets a random
108
+ Pauli (X, Y, or Z) with probability p/3 each, or identity with
109
+ probability 1-p. This is the Monte Carlo implementation of the
110
+ depolarizing channel.
111
+ """
112
+ if self.noise_param <= 0:
113
+ return
114
+ _paulis = [
115
+ _np_gate_matrix('X', ()),
116
+ _np_gate_matrix('Y', ()),
117
+ _np_gate_matrix('Z', ()),
118
+ ]
119
+ for q in qubits:
120
+ r = np.random.random()
121
+ if r < self.noise_param:
122
+ # Pick a random Pauli
123
+ pauli = _paulis[np.random.randint(3)]
124
+ if self.joint:
125
+ idx = self._idx(reg)
126
+ actual_q = q + self.offsets[idx]
127
+ self.sv = _apply_gate_np(self.sv, pauli, [actual_q], self.n_total)
128
+ else:
129
+ size = self.sizes[self._idx(reg)]
130
+ self.svs[reg] = _apply_gate_np(self.svs[reg], pauli, [q], size)
131
+
96
132
  def apply(self, reg: str, gate_name: str, params: tuple[float, ...], qubits: list[int]) -> None:
97
- """Apply a gate to a specific register."""
133
+ """Apply a gate to a specific register, then apply noise if configured."""
98
134
  self._check_qubits(reg, qubits)
99
135
  matrix = _np_gate_matrix(gate_name, tuple(params))
100
136
  if self.joint:
@@ -104,6 +140,7 @@ class LOCCEngine:
104
140
  else:
105
141
  size = self.sizes[self._idx(reg)]
106
142
  self.svs[reg] = _apply_gate_np(self.svs[reg], matrix, qubits, size)
143
+ self._apply_depolarizing(reg, qubits)
107
144
 
108
145
  def share(self, reg1: str, q1: int, reg2: str, q2: int) -> None:
109
146
  """Create Bell pair |Phi+> between reg1[q1] and reg2[q2]. JOINT only."""
@@ -116,7 +153,10 @@ class LOCCEngine:
116
153
  actual1 = q1 + self.offsets[self._idx(reg1)]
117
154
  actual2 = q2 + self.offsets[self._idx(reg2)]
118
155
  self.sv = _apply_gate_np(self.sv, h, [actual1], self.n_total)
156
+ self._apply_depolarizing(reg1, [q1])
119
157
  self.sv = _apply_gate_np(self.sv, cx, [actual1, actual2], self.n_total)
158
+ self._apply_depolarizing(reg1, [q1])
159
+ self._apply_depolarizing(reg2, [q2])
120
160
 
121
161
  def send(self, reg: str, qubit: int) -> int:
122
162
  """Measure a qubit (Born rule) and return the classical outcome."""
@@ -184,6 +224,7 @@ class LOCCEngine:
184
224
  else:
185
225
  size = self.sizes[self._idx(reg)]
186
226
  self.svs[reg] = _apply_gate_np(self.svs[reg], matrix, qubits, size)
227
+ self._apply_depolarizing(reg, qubits)
187
228
 
188
229
  def mem_gb(self) -> tuple[float, float]: # (total_gb, peak_gb)
189
230
  """Return (total_gb, peak_gb) realistic memory estimates including overhead."""
@@ -41,6 +41,8 @@ class LOCCExecutionMixin:
41
41
  self._locc_run_with_send(sorted_lines, has_measure)
42
42
  else:
43
43
  self._locc_run_vectorized(sorted_lines, has_measure)
44
+ # Sync last_sv from LOCC engine so EXPECT/DENSITY/BLOCH work
45
+ self.last_sv = self._active_sv
44
46
 
45
47
  def _locc_run_with_send(self, sorted_lines, has_measure):
46
48
  """LOCC execution with SEND — prefix/suffix split optimization.
@@ -147,11 +149,51 @@ class LOCCExecutionMixin:
147
149
  self.last_counts = counts_joint
148
150
 
149
151
  def _locc_run_vectorized(self, sorted_lines, has_measure):
150
- """LOCC execution without SEND — single execution, vectorized sampling."""
152
+ """LOCC execution without SEND — single execution, vectorized sampling.
153
+
154
+ When noise is active, re-executes per shot so that Monte Carlo noise
155
+ fires independently each time (matching the SEND path behavior).
156
+ """
151
157
  sizes_str = '+'.join(str(s) for s in self.locc.sizes)
152
158
  mode = "JOINT" if self.locc.joint else "SPLIT"
153
159
  t0 = time.time()
154
160
 
161
+ noisy = getattr(self.locc, 'noise_param', 0.0) > 0
162
+
163
+ if noisy and has_measure:
164
+ # Per-shot execution so noise fires independently each time
165
+ per_reg = {name: {} for name in self.locc.names}
166
+ counts_joint = {}
167
+ for _shot in range(self.shots):
168
+ self.locc.reset()
169
+ try:
170
+ self._locc_execute_program(sorted_lines)
171
+ except (RuntimeError, ValueError) as e:
172
+ self.io.writeln(f"?RUNTIME ERROR: {e}")
173
+ return
174
+ if self.locc.joint:
175
+ out = _sample_one_np(self.locc.sv, self.locc.n_total)
176
+ parts = []
177
+ pos = len(out)
178
+ for i in range(self.locc.n_regs):
179
+ size = self.locc.sizes[i]
180
+ parts.append(out[pos - size:pos])
181
+ pos -= size
182
+ else:
183
+ parts = [_sample_one_np(self.locc.svs[name],
184
+ self.locc.get_size(name))
185
+ for name in self.locc.names]
186
+ for name, bits in zip(self.locc.names, parts):
187
+ per_reg[name][bits] = per_reg[name].get(bits, 0) + 1
188
+ jkey = '|'.join(parts)
189
+ counts_joint[jkey] = counts_joint.get(jkey, 0) + 1
190
+ dt = time.time() - t0
191
+ self.io.writeln(f"\nRAN {len(self.program)} lines, LOCC {mode} "
192
+ f"{sizes_str}q, {self.shots} shots in {dt:.2f}s")
193
+ self._locc_display_results(per_reg, counts_joint)
194
+ self.last_counts = counts_joint if counts_joint else per_reg.get('A', {})
195
+ return
196
+
155
197
  self.locc.reset()
156
198
  try:
157
199
  self._locc_execute_program(sorted_lines)
@@ -245,6 +287,8 @@ class LOCCExecutionMixin:
245
287
  run_vars[parsed.var] = outcome
246
288
  self.variables[parsed.var] = outcome
247
289
  self.locc.classical[parsed.var] = outcome
290
+ self.locc.correction_log.append(
291
+ f"SEND {parsed.reg}[{qubit}] -> {parsed.var}={outcome}")
248
292
  return ExecResult.ADVANCE
249
293
  if isinstance(parsed, ShareStmt):
250
294
  self.locc.share(parsed.reg1, parsed.q1, parsed.reg2, parsed.q2)