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.
Files changed (48) hide show
  1. qbasic.py +113 -0
  2. qbasic_core/__init__.py +80 -0
  3. qbasic_core/__main__.py +6 -0
  4. qbasic_core/analysis.py +179 -0
  5. qbasic_core/backend.py +76 -0
  6. qbasic_core/classic.py +419 -0
  7. qbasic_core/control_flow.py +576 -0
  8. qbasic_core/debug.py +327 -0
  9. qbasic_core/demos.py +356 -0
  10. qbasic_core/display.py +274 -0
  11. qbasic_core/engine.py +126 -0
  12. qbasic_core/engine_state.py +109 -0
  13. qbasic_core/errors.py +37 -0
  14. qbasic_core/exec_context.py +24 -0
  15. qbasic_core/executor.py +861 -0
  16. qbasic_core/expression.py +228 -0
  17. qbasic_core/file_io.py +457 -0
  18. qbasic_core/gates.py +284 -0
  19. qbasic_core/help_text.py +167 -0
  20. qbasic_core/io_protocol.py +33 -0
  21. qbasic_core/locc.py +10 -0
  22. qbasic_core/locc_commands.py +221 -0
  23. qbasic_core/locc_display.py +61 -0
  24. qbasic_core/locc_engine.py +195 -0
  25. qbasic_core/locc_execution.py +389 -0
  26. qbasic_core/memory.py +369 -0
  27. qbasic_core/mock_backend.py +66 -0
  28. qbasic_core/noise_mixin.py +96 -0
  29. qbasic_core/parser.py +564 -0
  30. qbasic_core/patterns.py +186 -0
  31. qbasic_core/profiler.py +156 -0
  32. qbasic_core/program_mgmt.py +369 -0
  33. qbasic_core/protocol.py +77 -0
  34. qbasic_core/py.typed +0 -0
  35. qbasic_core/scope.py +74 -0
  36. qbasic_core/screen.py +115 -0
  37. qbasic_core/state_display.py +60 -0
  38. qbasic_core/statements.py +387 -0
  39. qbasic_core/strings.py +107 -0
  40. qbasic_core/subs.py +261 -0
  41. qbasic_core/sweep.py +82 -0
  42. qbasic_core/terminal.py +1697 -0
  43. qubasic-0.1.0.dist-info/METADATA +736 -0
  44. qubasic-0.1.0.dist-info/RECORD +48 -0
  45. qubasic-0.1.0.dist-info/WHEEL +5 -0
  46. qubasic-0.1.0.dist-info/entry_points.txt +2 -0
  47. qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
  48. 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