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.
- qubasic-0.4.0/CHANGELOG.md +63 -0
- {qubasic-0.3.1/qubasic.egg-info → qubasic-0.4.0}/PKG-INFO +1 -1
- {qubasic-0.3.1 → qubasic-0.4.0}/pyproject.toml +4 -1
- {qubasic-0.3.1 → qubasic-0.4.0/qubasic.egg-info}/PKG-INFO +1 -1
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/__init__.py +1 -1
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/analysis.py +95 -13
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/demos.py +64 -1
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/engine_state.py +2 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/executor.py +1 -2
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/locc_commands.py +49 -1
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/locc_engine.py +44 -3
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/locc_execution.py +45 -1
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/memory.py +12 -2
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/mock_backend.py +2 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/noise_mixin.py +24 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/state_display.py +16 -14
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/sweep.py +1 -1
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/terminal.py +553 -208
- qubasic-0.3.1/CHANGELOG.md +0 -34
- {qubasic-0.3.1 → qubasic-0.4.0}/LICENSE +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/MANIFEST.in +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/README.md +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/examples/bell.qb +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/examples/grover3.qb +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/examples/locc_teleport.qb +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/examples/sweep_rx.qb +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/__main__.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/backend.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/classic.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/control_flow.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/debug.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/display.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/engine.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/errors.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/expression.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/file_io.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/gates.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/help_text.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/locc.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/parser.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/patterns.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/profiler.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/protocol.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/scope.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/screen.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/statements.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/strings.py +0 -0
- {qubasic-0.3.1 → qubasic-0.4.0}/qubasic_core/subs.py +0 -0
- {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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qubasic"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.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_*"]
|
|
@@ -25,8 +25,12 @@ class AnalysisMixin:
|
|
|
25
25
|
def cmd_expect(self, rest: str) -> None:
|
|
26
26
|
"""EXPECT <pauli> [qubits] — compute expectation value.
|
|
27
27
|
Examples: EXPECT Z 0, EXPECT ZZ 0 1, EXPECT X 0"""
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
sv = self._active_sv
|
|
29
|
+
if sv is None:
|
|
30
|
+
if self.locc_mode and self.locc and not self.locc.joint:
|
|
31
|
+
self.io.writeln("?SPLIT mode: use STATE A / STATE B for per-register inspection")
|
|
32
|
+
else:
|
|
33
|
+
self.io.writeln("?NO STATE — RUN first")
|
|
30
34
|
return
|
|
31
35
|
parts = rest.split()
|
|
32
36
|
if not parts:
|
|
@@ -35,16 +39,16 @@ class AnalysisMixin:
|
|
|
35
39
|
pauli_str = parts[0].upper()
|
|
36
40
|
qubits = [int(q) for q in parts[1:]] if len(parts) > 1 else list(range(len(pauli_str)))
|
|
37
41
|
|
|
42
|
+
n = self._active_nqubits
|
|
38
43
|
try:
|
|
39
44
|
from qiskit.quantum_info import Statevector, SparsePauliOp
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
full_pauli = ['I'] * self.num_qubits
|
|
45
|
+
sv_q = Statevector(np.ascontiguousarray(sv).ravel())
|
|
46
|
+
full_pauli = ['I'] * n
|
|
43
47
|
for i, p in enumerate(pauli_str):
|
|
44
48
|
if i < len(qubits):
|
|
45
|
-
full_pauli[
|
|
49
|
+
full_pauli[n - 1 - qubits[i]] = p
|
|
46
50
|
op = SparsePauliOp(''.join(full_pauli))
|
|
47
|
-
val =
|
|
51
|
+
val = sv_q.expectation_value(op)
|
|
48
52
|
self.io.writeln(f" <{pauli_str}> on qubits {qubits} = {val.real:.6f}")
|
|
49
53
|
except Exception as e:
|
|
50
54
|
self.io.writeln(f"?EXPECT ERROR: {e}")
|
|
@@ -52,17 +56,18 @@ class AnalysisMixin:
|
|
|
52
56
|
def cmd_entropy(self, rest: str = '') -> None:
|
|
53
57
|
"""ENTROPY [qubits] — entanglement entropy of specified qubits vs rest.
|
|
54
58
|
Examples: ENTROPY 0 | ENTROPY 0 1 | ENTROPY (defaults to qubit 0)"""
|
|
55
|
-
|
|
59
|
+
sv = self._active_sv
|
|
60
|
+
if sv is None:
|
|
56
61
|
self.io.writeln("?NO STATE — RUN first")
|
|
57
62
|
return
|
|
58
63
|
if rest.strip():
|
|
59
64
|
partition_a = [int(q) for q in rest.replace(',', ' ').split() if q.strip()]
|
|
60
65
|
else:
|
|
61
66
|
partition_a = [0]
|
|
62
|
-
n = self.
|
|
67
|
+
n = self._active_nqubits
|
|
63
68
|
try:
|
|
64
69
|
from qiskit.quantum_info import Statevector, entropy, partial_trace
|
|
65
|
-
sv_obj = Statevector(np.ascontiguousarray(
|
|
70
|
+
sv_obj = Statevector(np.ascontiguousarray(sv).ravel())
|
|
66
71
|
keep = partition_a
|
|
67
72
|
rho_a = partial_trace(sv_obj, [q for q in range(n) if q not in keep])
|
|
68
73
|
ent = entropy(rho_a, base=2)
|
|
@@ -78,12 +83,13 @@ class AnalysisMixin:
|
|
|
78
83
|
|
|
79
84
|
def cmd_density(self) -> None:
|
|
80
85
|
"""Show density matrix (or partial trace for small systems)."""
|
|
81
|
-
|
|
86
|
+
sv = self._active_sv
|
|
87
|
+
if sv is None:
|
|
82
88
|
self.io.writeln("?NO STATE — RUN first")
|
|
83
89
|
return
|
|
84
|
-
sv = np.ascontiguousarray(
|
|
90
|
+
sv = np.ascontiguousarray(sv).ravel()
|
|
85
91
|
rho = np.outer(sv, sv.conj())
|
|
86
|
-
n = self.
|
|
92
|
+
n = self._active_nqubits
|
|
87
93
|
dim = 2**n
|
|
88
94
|
if dim > 16:
|
|
89
95
|
self.io.writeln(f" Density matrix: {dim}x{dim} (too large to display)")
|
|
@@ -140,6 +146,82 @@ class AnalysisMixin:
|
|
|
140
146
|
self.io.writeln(f" {n:>8} {method:>20} FAILED: {e}")
|
|
141
147
|
self.io.writeln('')
|
|
142
148
|
|
|
149
|
+
def cmd_consistency(self, rest: str = '') -> None:
|
|
150
|
+
"""CONSISTENCY — cross-check histogram, SV, density, Bloch, and EXPECT."""
|
|
151
|
+
sv = self._active_sv
|
|
152
|
+
if sv is None:
|
|
153
|
+
self.io.writeln("?NO STATE — RUN first")
|
|
154
|
+
return
|
|
155
|
+
n = self._active_nqubits
|
|
156
|
+
sv = np.ascontiguousarray(sv).ravel()
|
|
157
|
+
checks = []
|
|
158
|
+
|
|
159
|
+
# 1. SV normalization
|
|
160
|
+
norm = float(np.sum(np.abs(sv)**2))
|
|
161
|
+
ok = abs(norm - 1.0) < 1e-6
|
|
162
|
+
checks.append(('SV norm == 1', ok, f"{norm:.8f}"))
|
|
163
|
+
|
|
164
|
+
# 2. Density matrix purity
|
|
165
|
+
rho = np.outer(sv, sv.conj())
|
|
166
|
+
purity = float(np.real(np.trace(rho @ rho)))
|
|
167
|
+
ok2 = purity <= 1.0 + 1e-6
|
|
168
|
+
checks.append(('Purity <= 1', ok2, f"{purity:.8f}"))
|
|
169
|
+
|
|
170
|
+
# 3. Bloch vector length <= 1 for each qubit
|
|
171
|
+
bloch_ok = True
|
|
172
|
+
for q in range(min(n, 8)):
|
|
173
|
+
x, y, z = self._bloch_vector(sv, q, n)
|
|
174
|
+
r = (x**2 + y**2 + z**2) ** 0.5
|
|
175
|
+
if r > 1.0 + 1e-4:
|
|
176
|
+
bloch_ok = False
|
|
177
|
+
break
|
|
178
|
+
checks.append(('Bloch |r| <= 1', bloch_ok, ''))
|
|
179
|
+
|
|
180
|
+
# 4. EXPECT Z on qubit 0 matches P(0) - P(1)
|
|
181
|
+
try:
|
|
182
|
+
from qiskit.quantum_info import Statevector, SparsePauliOp
|
|
183
|
+
sv_q = Statevector(sv)
|
|
184
|
+
pauli_z = ['I'] * n
|
|
185
|
+
pauli_z[n - 1] = 'Z'
|
|
186
|
+
op = SparsePauliOp(''.join(pauli_z))
|
|
187
|
+
ez = float(sv_q.expectation_value(op).real)
|
|
188
|
+
# Compare with direct calculation
|
|
189
|
+
sv_t = sv.reshape([2] * n)
|
|
190
|
+
ax = n - 1
|
|
191
|
+
t0 = np.moveaxis(sv_t, ax, 0)[0].flatten()
|
|
192
|
+
t1 = np.moveaxis(sv_t, ax, 0)[1].flatten()
|
|
193
|
+
p0 = float(np.sum(np.abs(t0)**2))
|
|
194
|
+
p1 = float(np.sum(np.abs(t1)**2))
|
|
195
|
+
ez_direct = p0 - p1
|
|
196
|
+
ok4 = abs(ez - ez_direct) < 1e-6
|
|
197
|
+
checks.append(('<Z> consistent', ok4, f"qiskit={ez:.6f} direct={ez_direct:.6f}"))
|
|
198
|
+
except Exception as _e:
|
|
199
|
+
checks.append(('<Z> consistent', None, f"skip: {_e}"))
|
|
200
|
+
|
|
201
|
+
# 5. Histogram vs SV (if counts available)
|
|
202
|
+
if self.last_counts:
|
|
203
|
+
total = sum(self.last_counts.values())
|
|
204
|
+
hist_p0 = self.last_counts.get('0' * n, 0) / total
|
|
205
|
+
sv_p0 = float(np.abs(sv[0])**2)
|
|
206
|
+
# Loose check: histogram is statistical, allow 10% deviation
|
|
207
|
+
ok5 = abs(hist_p0 - sv_p0) < 0.15 or total < 50
|
|
208
|
+
checks.append(('Hist~SV P(|0>)', ok5,
|
|
209
|
+
f"hist={hist_p0:.3f} sv={sv_p0:.3f}"))
|
|
210
|
+
|
|
211
|
+
self.io.writeln(f"\n Consistency checks ({n} qubits):")
|
|
212
|
+
all_pass = True
|
|
213
|
+
for name, ok, detail in checks:
|
|
214
|
+
if ok is None:
|
|
215
|
+
status = 'SKIP'
|
|
216
|
+
elif ok:
|
|
217
|
+
status = 'PASS'
|
|
218
|
+
else:
|
|
219
|
+
status = 'FAIL'
|
|
220
|
+
all_pass = False
|
|
221
|
+
extra = f" {detail}" if detail else ""
|
|
222
|
+
self.io.writeln(f" {name:25s} {status}{extra}")
|
|
223
|
+
self.io.writeln(f"\n {'ALL CONSISTENT' if all_pass else 'INCONSISTENCY DETECTED'}")
|
|
224
|
+
|
|
143
225
|
def cmd_ram(self) -> None:
|
|
144
226
|
"""RAM — show memory budget, per-instance cost, and parallelism estimates."""
|
|
145
227
|
ram = _get_ram_gb()
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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)
|