qubasic 0.1.0__py3-none-any.whl
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.
- qbasic.py +113 -0
- qbasic_core/__init__.py +80 -0
- qbasic_core/__main__.py +6 -0
- qbasic_core/analysis.py +179 -0
- qbasic_core/backend.py +76 -0
- qbasic_core/classic.py +419 -0
- qbasic_core/control_flow.py +576 -0
- qbasic_core/debug.py +327 -0
- qbasic_core/demos.py +356 -0
- qbasic_core/display.py +274 -0
- qbasic_core/engine.py +126 -0
- qbasic_core/engine_state.py +109 -0
- qbasic_core/errors.py +37 -0
- qbasic_core/exec_context.py +24 -0
- qbasic_core/executor.py +861 -0
- qbasic_core/expression.py +228 -0
- qbasic_core/file_io.py +457 -0
- qbasic_core/gates.py +284 -0
- qbasic_core/help_text.py +167 -0
- qbasic_core/io_protocol.py +33 -0
- qbasic_core/locc.py +10 -0
- qbasic_core/locc_commands.py +221 -0
- qbasic_core/locc_display.py +61 -0
- qbasic_core/locc_engine.py +195 -0
- qbasic_core/locc_execution.py +389 -0
- qbasic_core/memory.py +369 -0
- qbasic_core/mock_backend.py +66 -0
- qbasic_core/noise_mixin.py +96 -0
- qbasic_core/parser.py +564 -0
- qbasic_core/patterns.py +186 -0
- qbasic_core/profiler.py +156 -0
- qbasic_core/program_mgmt.py +369 -0
- qbasic_core/protocol.py +77 -0
- qbasic_core/py.typed +0 -0
- qbasic_core/scope.py +74 -0
- qbasic_core/screen.py +115 -0
- qbasic_core/state_display.py +60 -0
- qbasic_core/statements.py +387 -0
- qbasic_core/strings.py +107 -0
- qbasic_core/subs.py +261 -0
- qbasic_core/sweep.py +82 -0
- qbasic_core/terminal.py +1697 -0
- qubasic-0.1.0.dist-info/METADATA +736 -0
- qubasic-0.1.0.dist-info/RECORD +48 -0
- qubasic-0.1.0.dist-info/WHEEL +5 -0
- qubasic-0.1.0.dist-info/entry_points.txt +2 -0
- qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
- qubasic-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""QBASIC LOCC commands mixin — cmd_locc, cmd_send, cmd_share, cmd_connect, cmd_disconnect, cmd_loccinfo."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from qbasic_core.engine import (
|
|
6
|
+
LOCCEngine,
|
|
7
|
+
_get_ram_gb,
|
|
8
|
+
LOCC_MAX_JOINT_QUBITS, LOCC_MAX_SPLIT_QUBITS, LOCC_MAX_REGISTERS,
|
|
9
|
+
RAM_BUDGET_FRACTION,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LOCCCommandsMixin:
|
|
14
|
+
"""LOCC user-facing commands for QBasicTerminal.
|
|
15
|
+
|
|
16
|
+
Requires: TerminalProtocol — uses self.locc, self.locc_mode, self.program,
|
|
17
|
+
self.variables, self.eval_expr(), self.io.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def cmd_locc(self, rest):
|
|
21
|
+
args = rest.upper().split()
|
|
22
|
+
if not args:
|
|
23
|
+
if self.locc_mode:
|
|
24
|
+
mode = "JOINT" if self.locc.joint else "SPLIT"
|
|
25
|
+
reg_desc = ' '.join(f"{n}={s}q" for n, s in
|
|
26
|
+
zip(self.locc.names, self.locc.sizes))
|
|
27
|
+
self.io.writeln(f"LOCC {mode}: {reg_desc}")
|
|
28
|
+
tot, peak = self.locc.mem_gb()
|
|
29
|
+
self.io.writeln(f" RAM per instance: {tot:.1f} GB (with overhead)")
|
|
30
|
+
if self.locc.classical:
|
|
31
|
+
self.io.writeln(f" Classical: {self.locc.classical}")
|
|
32
|
+
else:
|
|
33
|
+
self.io.writeln("LOCC OFF. Usage: LOCC [JOINT] <n1> <n2> [n3 ...]")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
if args[0] == 'OFF':
|
|
37
|
+
self.locc = None
|
|
38
|
+
self.locc_mode = False
|
|
39
|
+
self.io.writeln("LOCC OFF — back to normal Aer mode")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if args[0] == 'STATUS':
|
|
43
|
+
if not self.locc_mode:
|
|
44
|
+
self.io.writeln("NOT IN LOCC MODE")
|
|
45
|
+
return
|
|
46
|
+
mode = "JOINT" if self.locc.joint else "SPLIT"
|
|
47
|
+
reg_desc = ' '.join(f"{n}={s}q" for n, s in
|
|
48
|
+
zip(self.locc.names, self.locc.sizes))
|
|
49
|
+
self.io.writeln(f"LOCC {mode}: {reg_desc}")
|
|
50
|
+
tot, peak = self.locc.mem_gb()
|
|
51
|
+
self.io.writeln(f" RAM per instance: {tot:.1f} GB (with overhead)")
|
|
52
|
+
ram = _get_ram_gb()
|
|
53
|
+
if ram:
|
|
54
|
+
budget = ram[0] * RAM_BUDGET_FRACTION
|
|
55
|
+
max_par = int(budget / tot) if tot > 0 else 0
|
|
56
|
+
if max_par > 0:
|
|
57
|
+
self.io.writeln(f" Max parallel in 80% budget: ~{max_par}")
|
|
58
|
+
self.io.writeln(f" Classical vars: {self.locc.classical if self.locc.classical else '(none)'}")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
joint = False
|
|
62
|
+
nums = args
|
|
63
|
+
if args[0] == 'JOINT':
|
|
64
|
+
joint = True
|
|
65
|
+
nums = args[1:]
|
|
66
|
+
elif args[0] == 'SPLIT':
|
|
67
|
+
nums = args[1:]
|
|
68
|
+
|
|
69
|
+
# Support "2+2+2" notation: split on + if present
|
|
70
|
+
expanded = []
|
|
71
|
+
for n in nums:
|
|
72
|
+
expanded.extend(n.split('+'))
|
|
73
|
+
nums = [n for n in expanded if n]
|
|
74
|
+
|
|
75
|
+
if len(nums) < 2:
|
|
76
|
+
self.io.writeln("?USAGE: LOCC [JOINT|SPLIT] <n1> <n2> [n3 ...]")
|
|
77
|
+
return
|
|
78
|
+
if len(nums) > LOCC_MAX_REGISTERS:
|
|
79
|
+
self.io.writeln(f"?MAX {LOCC_MAX_REGISTERS} registers (A-Z)")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
sizes = [int(n) for n in nums]
|
|
83
|
+
total = sum(sizes)
|
|
84
|
+
if joint and total > LOCC_MAX_JOINT_QUBITS:
|
|
85
|
+
self.io.writeln(f"?JOINT mode limited to {LOCC_MAX_JOINT_QUBITS} total qubits (requested {total})")
|
|
86
|
+
return
|
|
87
|
+
if not joint and max(sizes) > LOCC_MAX_SPLIT_QUBITS:
|
|
88
|
+
self.io.writeln(f"?Each register limited to {LOCC_MAX_SPLIT_QUBITS} qubits")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Pre-check RAM before allocating
|
|
92
|
+
mode = "JOINT" if joint else "SPLIT"
|
|
93
|
+
temp_eng = LOCCEngine(sizes, joint=joint)
|
|
94
|
+
tot, peak = temp_eng.mem_gb()
|
|
95
|
+
ram = _get_ram_gb()
|
|
96
|
+
if ram and tot > ram[1]:
|
|
97
|
+
self.io.writeln(f"?BLOCKED: LOCC {mode} needs ~{tot:.1f} GB but only "
|
|
98
|
+
f"{ram[1]:.1f} GB available. Reduce register sizes.")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
self.locc = temp_eng
|
|
102
|
+
self.locc_mode = True
|
|
103
|
+
reg_desc = ' '.join(f"{n}={s}q" for n, s in
|
|
104
|
+
zip(self.locc.names, sizes))
|
|
105
|
+
self.io.writeln(f"LOCC {mode}: {reg_desc} ({total} total)")
|
|
106
|
+
self.io.writeln(f" RAM per instance: {tot:.1f} GB (with overhead)")
|
|
107
|
+
if ram:
|
|
108
|
+
sys_total, avail = ram
|
|
109
|
+
budget = sys_total * RAM_BUDGET_FRACTION
|
|
110
|
+
if tot > 0:
|
|
111
|
+
max_par = int(budget / tot)
|
|
112
|
+
if max_par > 0:
|
|
113
|
+
self.io.writeln(f" Max parallel instances in 80% budget: ~{max_par}")
|
|
114
|
+
if not joint:
|
|
115
|
+
self.io.writeln(f" Registers are INDEPENDENT — no cross-register entanglement")
|
|
116
|
+
self.io.writeln(f" Use SEND/IF for classical coordination")
|
|
117
|
+
else:
|
|
118
|
+
self.io.writeln(f" Joint statevector — use SHARE for pre-shared entanglement")
|
|
119
|
+
if peak > 30:
|
|
120
|
+
self.io.writeln(f" WARNING: large registers. Keep SHOTS low for SEND-based protocols.")
|
|
121
|
+
|
|
122
|
+
def cmd_send(self, rest):
|
|
123
|
+
if not self.locc_mode:
|
|
124
|
+
self.io.writeln("?SEND requires LOCC mode")
|
|
125
|
+
return
|
|
126
|
+
m = re.match(r'([A-Z])\s+(\S+)\s*->\s*(\w+)', rest, re.IGNORECASE)
|
|
127
|
+
if not m:
|
|
128
|
+
self.io.writeln("?USAGE: SEND <reg> <qubit> -> <var>")
|
|
129
|
+
return
|
|
130
|
+
reg = m.group(1).upper()
|
|
131
|
+
if reg not in self.locc.names:
|
|
132
|
+
self.io.writeln(f"?UNKNOWN REGISTER: {reg} (have {', '.join(self.locc.names)})")
|
|
133
|
+
return
|
|
134
|
+
qubit = int(self.eval_expr(m.group(2)))
|
|
135
|
+
var = m.group(3)
|
|
136
|
+
n_reg = self.locc.get_size(reg)
|
|
137
|
+
if qubit < 0 or qubit >= n_reg:
|
|
138
|
+
self.io.writeln(f"?QUBIT {qubit} OUT OF RANGE for register {reg} (0-{n_reg-1})")
|
|
139
|
+
return
|
|
140
|
+
outcome = self.locc.send(reg, qubit)
|
|
141
|
+
self.variables[var] = outcome
|
|
142
|
+
self.locc.classical[var] = outcome
|
|
143
|
+
self.io.writeln(f" {reg}[{qubit}] -> {var} = {outcome}")
|
|
144
|
+
|
|
145
|
+
def cmd_share(self, rest):
|
|
146
|
+
if not self.locc_mode:
|
|
147
|
+
self.io.writeln("?SHARE requires LOCC mode")
|
|
148
|
+
return
|
|
149
|
+
m = re.match(r'([A-Z])\s+(\d+)\s*,?\s*([A-Z])\s+(\d+)', rest, re.IGNORECASE)
|
|
150
|
+
if not m:
|
|
151
|
+
self.io.writeln("?USAGE: SHARE <reg1> <qubit> <reg2> <qubit>")
|
|
152
|
+
return
|
|
153
|
+
reg1, q1 = m.group(1).upper(), int(m.group(2))
|
|
154
|
+
reg2, q2 = m.group(3).upper(), int(m.group(4))
|
|
155
|
+
for r in (reg1, reg2):
|
|
156
|
+
if r not in self.locc.names:
|
|
157
|
+
self.io.writeln(f"?UNKNOWN REGISTER: {r}")
|
|
158
|
+
return
|
|
159
|
+
try:
|
|
160
|
+
self.locc.share(reg1, q1, reg2, q2)
|
|
161
|
+
self.io.writeln(f" Bell pair |Phi+> created: {reg1}[{q1}] <-> {reg2}[{q2}]")
|
|
162
|
+
except RuntimeError as e:
|
|
163
|
+
self.io.writeln(f"?{e}")
|
|
164
|
+
|
|
165
|
+
def cmd_connect(self, rest: str) -> None:
|
|
166
|
+
"""CONNECT "host:port" AS <reg> — attach a remote quantum register.
|
|
167
|
+
|
|
168
|
+
Stub: uses local simulation. No network transport implemented.
|
|
169
|
+
|
|
170
|
+
Creates a local register that can be used with LOCC commands.
|
|
171
|
+
The connection info is stored for future network-backed execution.
|
|
172
|
+
"""
|
|
173
|
+
m = re.match(r'"?([^"]+)"?\s+AS\s+([A-Z])', rest, re.IGNORECASE)
|
|
174
|
+
if not m:
|
|
175
|
+
self.io.writeln('?USAGE: CONNECT "host:port" AS <reg>')
|
|
176
|
+
return
|
|
177
|
+
endpoint = m.group(1).strip()
|
|
178
|
+
reg = m.group(2).upper()
|
|
179
|
+
if not hasattr(self, '_connections'):
|
|
180
|
+
self._connections = {}
|
|
181
|
+
self._connections[reg] = endpoint
|
|
182
|
+
# If not in LOCC mode, auto-enter with a default local register + the remote
|
|
183
|
+
if not self.locc_mode:
|
|
184
|
+
self.cmd_locc(f'3 3')
|
|
185
|
+
self.io.writeln(f"CONNECTED {reg} -> {endpoint} (experimental stub — no real network transport)")
|
|
186
|
+
self.io.writeln(" (local simulation only)")
|
|
187
|
+
|
|
188
|
+
def cmd_disconnect(self, rest: str) -> None:
|
|
189
|
+
"""DISCONNECT <reg> — detach a remote register.
|
|
190
|
+
|
|
191
|
+
Stub: uses local simulation. No network transport implemented.
|
|
192
|
+
"""
|
|
193
|
+
reg = rest.strip().upper()
|
|
194
|
+
if hasattr(self, '_connections') and reg in self._connections:
|
|
195
|
+
del self._connections[reg]
|
|
196
|
+
self.io.writeln(f"DISCONNECTED {reg}")
|
|
197
|
+
else:
|
|
198
|
+
self.io.writeln(f"?{reg} NOT CONNECTED")
|
|
199
|
+
|
|
200
|
+
def cmd_loccinfo(self):
|
|
201
|
+
"""Show LOCC protocol metrics after a run."""
|
|
202
|
+
if not self.locc_mode:
|
|
203
|
+
self.io.writeln("?NOT IN LOCC MODE")
|
|
204
|
+
return
|
|
205
|
+
mode = "JOINT" if self.locc.joint else "SPLIT"
|
|
206
|
+
self.io.writeln(f"\n LOCC Protocol Metrics ({mode})")
|
|
207
|
+
reg_desc = ' '.join(f"{n}={s}q" for n, s in
|
|
208
|
+
zip(self.locc.names, self.locc.sizes))
|
|
209
|
+
self.io.writeln(f" Registers: {reg_desc} ({self.locc.n_regs} parties)")
|
|
210
|
+
n_classical = len(self.locc.classical)
|
|
211
|
+
self.io.writeln(f" Classical bits exchanged: {n_classical}")
|
|
212
|
+
if self.locc.classical:
|
|
213
|
+
for k, v in self.locc.classical.items():
|
|
214
|
+
self.io.writeln(f" {k} = {v}")
|
|
215
|
+
n_sends = sum(1 for l in self.program.values()
|
|
216
|
+
if re.search(r'\bSEND\b', l, re.IGNORECASE))
|
|
217
|
+
self.io.writeln(f" SEND operations: {n_sends}")
|
|
218
|
+
self.io.writeln(f" Communication rounds: ~{n_sends}")
|
|
219
|
+
tot, peak = self.locc.mem_gb()
|
|
220
|
+
self.io.writeln(f" Memory: {tot:.1f} GB")
|
|
221
|
+
self.io.writeln('')
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""QBASIC LOCC display mixin — state inspection and Bloch sphere for LOCC registers."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LOCCDisplayMixin:
|
|
7
|
+
"""LOCC display methods for QBasicTerminal.
|
|
8
|
+
|
|
9
|
+
Requires: TerminalProtocol — uses self.locc, self._print_statevector(),
|
|
10
|
+
self._print_bloch_single(), self.print_histogram(), self.io.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def _locc_state(self, rest=''):
|
|
14
|
+
reg = rest.strip().upper() if rest else ''
|
|
15
|
+
if self.locc.joint:
|
|
16
|
+
if not reg or reg in self.locc.names:
|
|
17
|
+
sizes = '+'.join(str(s) for s in self.locc.sizes)
|
|
18
|
+
self.io.writeln(f"\n Joint statevector ({sizes} qubits):")
|
|
19
|
+
self._print_statevector(self.locc.sv, self.locc.n_total)
|
|
20
|
+
else:
|
|
21
|
+
show = [reg] if reg and reg in self.locc.names else self.locc.names
|
|
22
|
+
for name in show:
|
|
23
|
+
size = self.locc.get_size(name)
|
|
24
|
+
self.io.writeln(f"\n Register {name} ({size} qubits):")
|
|
25
|
+
self._print_statevector(self.locc.svs[name], size)
|
|
26
|
+
|
|
27
|
+
def _locc_bloch(self, rest):
|
|
28
|
+
m = re.match(r'([A-Z])\s*(\d*)', rest.strip(), re.IGNORECASE) if rest.strip() else None
|
|
29
|
+
if m and m.group(1):
|
|
30
|
+
reg = m.group(1).upper()
|
|
31
|
+
if reg not in self.locc.names:
|
|
32
|
+
self.io.writeln(f"?UNKNOWN REGISTER: {reg}")
|
|
33
|
+
return
|
|
34
|
+
sv = self.locc.get_sv(reg)
|
|
35
|
+
n = self.locc.get_n(reg)
|
|
36
|
+
idx = self.locc._idx(reg)
|
|
37
|
+
if m.group(2):
|
|
38
|
+
q = int(m.group(2))
|
|
39
|
+
actual_q = q if not self.locc.joint else q + self.locc.offsets[idx]
|
|
40
|
+
self.io.writeln(f" [Register {reg}, qubit {q}]")
|
|
41
|
+
self._print_bloch_single(sv, actual_q, n)
|
|
42
|
+
else:
|
|
43
|
+
n_show = self.locc.get_size(reg)
|
|
44
|
+
for q in range(min(n_show, 4)):
|
|
45
|
+
actual_q = q if not self.locc.joint else q + self.locc.offsets[idx]
|
|
46
|
+
self.io.writeln(f" [Register {reg}, qubit {q}]")
|
|
47
|
+
self._print_bloch_single(sv, actual_q, n)
|
|
48
|
+
self.io.writeln('')
|
|
49
|
+
else:
|
|
50
|
+
self.io.writeln(f"?USAGE: BLOCH <reg> [qubit] (registers: {', '.join(self.locc.names)})")
|
|
51
|
+
|
|
52
|
+
def _locc_display_results(self, per_reg, counts_joint):
|
|
53
|
+
"""Display per-register and joint histograms."""
|
|
54
|
+
for name in self.locc.names:
|
|
55
|
+
size = self.locc.get_size(name)
|
|
56
|
+
self.io.writeln(f"\n Register {name} ({size}q):")
|
|
57
|
+
self.print_histogram(per_reg[name])
|
|
58
|
+
if counts_joint and self.locc.n_regs <= 4:
|
|
59
|
+
jlabel = '|'.join(self.locc.names)
|
|
60
|
+
self.io.writeln(f"\n Joint ({jlabel}):")
|
|
61
|
+
self.print_histogram(counts_joint)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""LOCC quantum engine — multi-register simulation with classical channels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from qbasic_core.gates import (
|
|
7
|
+
_np_gate_matrix, _apply_gate_np, _measure_np, _sample_np,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
11
|
+
# LOCC-specific constants
|
|
12
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
LOCC_MAX_JOINT_QUBITS = 33
|
|
15
|
+
LOCC_MAX_SPLIT_QUBITS = 33
|
|
16
|
+
LOCC_MAX_REGISTERS = 26
|
|
17
|
+
LOCC_SEND_SHOT_CAP = 100
|
|
18
|
+
LOCC_SEND_QUBIT_THRESHOLD = 20
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LOCCEngine:
|
|
22
|
+
"""N-register quantum simulation with classical channels.
|
|
23
|
+
|
|
24
|
+
Supports 2-26 independent quantum registers (A through Z).
|
|
25
|
+
SPLIT mode: independent statevectors per register. Max capacity.
|
|
26
|
+
JOINT mode: one statevector, LOCC constraints enforced.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, sizes: list[int], joint: bool = False):
|
|
30
|
+
"""Initialize LOCC engine with given register sizes."""
|
|
31
|
+
self.sizes = list(sizes)
|
|
32
|
+
self.n_regs = len(self.sizes)
|
|
33
|
+
self.names = [chr(ord('A') + i) for i in range(self.n_regs)]
|
|
34
|
+
self.joint = joint
|
|
35
|
+
self.classical = {}
|
|
36
|
+
# Precompute offsets for JOINT mode
|
|
37
|
+
self.offsets = []
|
|
38
|
+
off = 0
|
|
39
|
+
for s in self.sizes:
|
|
40
|
+
self.offsets.append(off)
|
|
41
|
+
off += s
|
|
42
|
+
self.n_total = sum(self.sizes)
|
|
43
|
+
self.reset()
|
|
44
|
+
|
|
45
|
+
# Backward-compatible properties
|
|
46
|
+
@property
|
|
47
|
+
def n_a(self):
|
|
48
|
+
return self.sizes[0] if self.sizes else 0
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def n_b(self):
|
|
52
|
+
return self.sizes[1] if len(self.sizes) > 1 else 0
|
|
53
|
+
|
|
54
|
+
def _idx(self, reg):
|
|
55
|
+
"""Register name -> index."""
|
|
56
|
+
return ord(reg) - ord('A')
|
|
57
|
+
|
|
58
|
+
def reset(self) -> None:
|
|
59
|
+
"""Reset all registers to |0> and clear classical state."""
|
|
60
|
+
self.classical.clear()
|
|
61
|
+
if self.joint:
|
|
62
|
+
self.sv = np.zeros(2**self.n_total, dtype=complex)
|
|
63
|
+
self.sv[0] = 1.0
|
|
64
|
+
else:
|
|
65
|
+
self.svs = {}
|
|
66
|
+
for name, size in zip(self.names, self.sizes):
|
|
67
|
+
sv = np.zeros(2**size, dtype=complex)
|
|
68
|
+
sv[0] = 1.0
|
|
69
|
+
self.svs[name] = sv
|
|
70
|
+
|
|
71
|
+
def snapshot(self) -> dict:
|
|
72
|
+
"""Capture current quantum state for later restore."""
|
|
73
|
+
if self.joint:
|
|
74
|
+
return {'joint': True, 'sv': self.sv.copy(), 'classical': dict(self.classical)}
|
|
75
|
+
return {'joint': False,
|
|
76
|
+
'svs': {n: sv.copy() for n, sv in self.svs.items()},
|
|
77
|
+
'classical': dict(self.classical)}
|
|
78
|
+
|
|
79
|
+
def restore(self, snap: dict) -> None:
|
|
80
|
+
"""Restore quantum state from a snapshot."""
|
|
81
|
+
self.classical = dict(snap['classical'])
|
|
82
|
+
if snap['joint']:
|
|
83
|
+
self.sv = snap['sv'].copy()
|
|
84
|
+
else:
|
|
85
|
+
self.svs = {n: sv.copy() for n, sv in snap['svs'].items()}
|
|
86
|
+
|
|
87
|
+
def _check_qubits(self, reg: str, qubits: list[int]) -> None:
|
|
88
|
+
"""Validate that all qubit indices are in range for the given register."""
|
|
89
|
+
size = self.sizes[self._idx(reg)]
|
|
90
|
+
for q in qubits:
|
|
91
|
+
if q < 0 or q >= size:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Qubit {q} out of range for register {reg} (size {size})"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def apply(self, reg: str, gate_name: str, params: tuple[float, ...], qubits: list[int]) -> None:
|
|
97
|
+
"""Apply a gate to a specific register."""
|
|
98
|
+
self._check_qubits(reg, qubits)
|
|
99
|
+
matrix = _np_gate_matrix(gate_name, tuple(params))
|
|
100
|
+
if self.joint:
|
|
101
|
+
idx = self._idx(reg)
|
|
102
|
+
actual = [q + self.offsets[idx] for q in qubits]
|
|
103
|
+
self.sv = _apply_gate_np(self.sv, matrix, actual, self.n_total)
|
|
104
|
+
else:
|
|
105
|
+
size = self.sizes[self._idx(reg)]
|
|
106
|
+
self.svs[reg] = _apply_gate_np(self.svs[reg], matrix, qubits, size)
|
|
107
|
+
|
|
108
|
+
def share(self, reg1: str, q1: int, reg2: str, q2: int) -> None:
|
|
109
|
+
"""Create Bell pair |Phi+> between reg1[q1] and reg2[q2]. JOINT only."""
|
|
110
|
+
if not self.joint:
|
|
111
|
+
raise RuntimeError("SHARE requires LOCC JOINT mode")
|
|
112
|
+
self._check_qubits(reg1, [q1])
|
|
113
|
+
self._check_qubits(reg2, [q2])
|
|
114
|
+
h = _np_gate_matrix('H', ())
|
|
115
|
+
cx = _np_gate_matrix('CX', ())
|
|
116
|
+
actual1 = q1 + self.offsets[self._idx(reg1)]
|
|
117
|
+
actual2 = q2 + self.offsets[self._idx(reg2)]
|
|
118
|
+
self.sv = _apply_gate_np(self.sv, h, [actual1], self.n_total)
|
|
119
|
+
self.sv = _apply_gate_np(self.sv, cx, [actual1, actual2], self.n_total)
|
|
120
|
+
|
|
121
|
+
def send(self, reg: str, qubit: int) -> int:
|
|
122
|
+
"""Measure a qubit (Born rule) and return the classical outcome."""
|
|
123
|
+
self._check_qubits(reg, [qubit])
|
|
124
|
+
if self.joint:
|
|
125
|
+
actual = qubit + self.offsets[self._idx(reg)]
|
|
126
|
+
outcome, self.sv = _measure_np(self.sv, actual, self.n_total)
|
|
127
|
+
else:
|
|
128
|
+
size = self.sizes[self._idx(reg)]
|
|
129
|
+
outcome, self.svs[reg] = _measure_np(self.svs[reg], qubit, size)
|
|
130
|
+
return outcome
|
|
131
|
+
|
|
132
|
+
def get_sv(self, reg: str) -> np.ndarray:
|
|
133
|
+
if self.joint:
|
|
134
|
+
return self.sv
|
|
135
|
+
return self.svs[reg]
|
|
136
|
+
|
|
137
|
+
def get_n(self, reg: str) -> int:
|
|
138
|
+
if self.joint:
|
|
139
|
+
return self.n_total
|
|
140
|
+
return self.sizes[self._idx(reg)]
|
|
141
|
+
|
|
142
|
+
def get_size(self, reg: str) -> int:
|
|
143
|
+
return self.sizes[self._idx(reg)]
|
|
144
|
+
|
|
145
|
+
def sample_joint(self, shots: int) -> tuple[dict[str, dict[str, int]], dict[str, int]]:
|
|
146
|
+
"""Sample and return (per_reg_counts, joint_counts)."""
|
|
147
|
+
if self.joint:
|
|
148
|
+
raw = _sample_np(self.sv, self.n_total, shots)
|
|
149
|
+
per_reg = {name: {} for name in self.names}
|
|
150
|
+
joint = {}
|
|
151
|
+
for state, count in raw.items():
|
|
152
|
+
# Split the joint bitstring into per-register segments.
|
|
153
|
+
# The joint statevector is ordered A[0..nA-1] B[0..nB-1] ...
|
|
154
|
+
# with register A occupying the most-significant qubits.
|
|
155
|
+
# The bitstring is MSB-first, so register A's bits are at
|
|
156
|
+
# the left end. We walk backward from the right to peel
|
|
157
|
+
# off each register's segment from least-significant first.
|
|
158
|
+
parts = []
|
|
159
|
+
pos = len(state)
|
|
160
|
+
for i in range(self.n_regs):
|
|
161
|
+
size = self.sizes[i]
|
|
162
|
+
parts.append(state[pos - size:pos])
|
|
163
|
+
pos -= size
|
|
164
|
+
for name, part in zip(self.names, parts):
|
|
165
|
+
per_reg[name][part] = per_reg[name].get(part, 0) + count
|
|
166
|
+
jkey = '|'.join(parts)
|
|
167
|
+
joint[jkey] = joint.get(jkey, 0) + count
|
|
168
|
+
return per_reg, joint
|
|
169
|
+
else:
|
|
170
|
+
per_reg = {}
|
|
171
|
+
for name in self.names:
|
|
172
|
+
size = self.sizes[self._idx(name)]
|
|
173
|
+
per_reg[name] = _sample_np(self.svs[name], size, shots)
|
|
174
|
+
return per_reg, {}
|
|
175
|
+
|
|
176
|
+
def apply_matrix(self, reg: str, matrix: np.ndarray, qubits: list[int]) -> None:
|
|
177
|
+
"""Apply a raw unitary matrix to qubits in a register."""
|
|
178
|
+
self._check_qubits(reg, qubits)
|
|
179
|
+
if self.joint:
|
|
180
|
+
idx = self._idx(reg)
|
|
181
|
+
actual = [q + self.offsets[idx] for q in qubits]
|
|
182
|
+
self.sv = _apply_gate_np(self.sv, matrix, actual, self.n_total)
|
|
183
|
+
else:
|
|
184
|
+
size = self.sizes[self._idx(reg)]
|
|
185
|
+
self.svs[reg] = _apply_gate_np(self.svs[reg], matrix, qubits, size)
|
|
186
|
+
|
|
187
|
+
def mem_gb(self) -> tuple[float, float]: # (total_gb, peak_gb)
|
|
188
|
+
"""Return (total_gb, peak_gb) realistic memory estimates including overhead."""
|
|
189
|
+
from qbasic_core.engine import OVERHEAD_FACTOR
|
|
190
|
+
if self.joint:
|
|
191
|
+
total = (2**self.n_total) * 16 * OVERHEAD_FACTOR / 1e9
|
|
192
|
+
return total, total
|
|
193
|
+
total = sum((2**s) * 16 * OVERHEAD_FACTOR / 1e9 for s in self.sizes)
|
|
194
|
+
peak = max((2**s) * 16 * OVERHEAD_FACTOR / 1e9 for s in self.sizes)
|
|
195
|
+
return total, peak
|