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
qbasic.py ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ QBASIC — Quantum BASIC Interactive Terminal
4
+
5
+ Usage:
6
+ python qbasic.py Interactive REPL
7
+ python qbasic.py script.qb Run a script file
8
+ """
9
+
10
+ import sys
11
+ import os
12
+
13
+ # Force UTF-8 output on Windows
14
+ if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
15
+ try:
16
+ sys.stdout.reconfigure(encoding='utf-8')
17
+ except Exception:
18
+ pass
19
+ if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
20
+ try:
21
+ sys.stderr.reconfigure(encoding='utf-8')
22
+ except Exception:
23
+ pass
24
+
25
+ from qbasic_core.terminal import QBasicTerminal
26
+ from qbasic_core.program_mgmt import ProgramMgmtMixin
27
+
28
+
29
+ def run_script(path: str, terminal: 'QBasicTerminal') -> None:
30
+ """Run a .qb script file. Supports multi-line DEF blocks.
31
+
32
+ After loading all lines, auto-runs the program if it contains
33
+ numbered lines with a MEASURE statement.
34
+ """
35
+ with open(path, 'r') as f:
36
+ lines = [l.rstrip('\n\r') for l in f.readlines()]
37
+ ProgramMgmtMixin._load_lines_with_defs(
38
+ lines, lambda line: terminal.process(line, track_undo=False))
39
+
40
+ # Auto-run if the program has a MEASURE statement
41
+ has_measure = any(
42
+ terminal.program.get(ln, '').strip().upper() == 'MEASURE'
43
+ for ln in terminal.program
44
+ )
45
+ if terminal.program and has_measure:
46
+ terminal.cmd_run()
47
+
48
+
49
+ def main():
50
+ import json as _json
51
+ os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
52
+
53
+ args = sys.argv[1:]
54
+ quiet = '--quiet' in args or '-q' in args
55
+ json_mode = '--json' in args
56
+ if quiet:
57
+ args = [a for a in args if a not in ('--quiet', '-q')]
58
+ if json_mode:
59
+ args = [a for a in args if a != '--json']
60
+
61
+ if any(a in ('-h', '--help') for a in args):
62
+ from qbasic_core import __version__
63
+ print(f"QBASIC {__version__} — Quantum BASIC Interactive Terminal")
64
+ print()
65
+ print("Usage:")
66
+ print(" qbasic Interactive REPL")
67
+ print(" qbasic script.qb Run a script file")
68
+ print(" qbasic --quiet script Suppress banner and progress")
69
+ print(" qbasic --json script Output results as JSON")
70
+ print(" qbasic --help Show this help")
71
+ print()
72
+ print("Type HELP inside the REPL for full command reference.")
73
+ sys.exit(0)
74
+
75
+ term = QBasicTerminal()
76
+
77
+ if args:
78
+ path = args[0]
79
+ if os.path.isfile(path):
80
+ if quiet or json_mode:
81
+ import io
82
+ buf = io.StringIO()
83
+ old = sys.stdout
84
+ sys.stdout = buf
85
+ try:
86
+ run_script(path, term)
87
+ finally:
88
+ sys.stdout = old
89
+ if json_mode:
90
+ result = {
91
+ 'counts': term.last_counts or {},
92
+ 'num_qubits': term.num_qubits,
93
+ 'shots': term.shots,
94
+ }
95
+ print(_json.dumps(result, indent=2))
96
+ elif not quiet:
97
+ print(buf.getvalue())
98
+ else:
99
+ term.print_banner()
100
+ run_script(path, term)
101
+ # Exit code: 0 if results exist, 1 if error
102
+ sys.exit(0 if term.last_counts is not None or not any(
103
+ term.program.get(ln, '').strip().upper() == 'MEASURE'
104
+ for ln in term.program) else 1)
105
+ else:
106
+ print(f"?FILE NOT FOUND: {path}")
107
+ sys.exit(1)
108
+ else:
109
+ term.repl()
110
+
111
+
112
+ if __name__ == '__main__':
113
+ main()
@@ -0,0 +1,80 @@
1
+ """QBASIC — Quantum BASIC Interactive Terminal (package)."""
2
+
3
+ # Lightweight imports that don't pull in Qiskit/numpy
4
+ from qbasic_core.statements import Stmt, GateStmt, RawStmt
5
+ from qbasic_core.errors import (
6
+ QBasicError, QBasicSyntaxError, QBasicRuntimeError,
7
+ QBasicBuildError, QBasicRangeError, QBasicIOError, QBasicUndefinedError,
8
+ )
9
+ from qbasic_core.io_protocol import IOPort, StdIOPort
10
+ from qbasic_core.exec_context import ExecContext
11
+
12
+ __all__ = [
13
+ 'QBasicTerminal', 'LOCCEngine', 'ExecResult', 'ExecOutcome',
14
+ 'ExpressionMixin', 'DisplayMixin', 'DemoMixin', 'ControlFlowMixin',
15
+ 'LOCCMixin', 'LOCCCommandsMixin', 'LOCCDisplayMixin', 'LOCCExecutionMixin',
16
+ 'NoiseMixin', 'StateDisplayMixin', 'HELP_TEXT', 'BANNER_ART',
17
+ 'FileIOMixin', 'AnalysisMixin', 'SweepMixin',
18
+ 'MemoryMixin', 'StringMixin', 'ScreenMixin', 'ClassicMixin',
19
+ 'SubroutineMixin', 'DebugMixin', 'ProgramMgmtMixin', 'ProfilerMixin',
20
+ 'TerminalProtocol',
21
+ 'Engine', 'ExecutorMixin',
22
+ 'QBasicError', 'QBasicSyntaxError', 'QBasicRuntimeError',
23
+ 'QBasicBuildError', 'QBasicRangeError', 'QBasicIOError', 'QBasicUndefinedError',
24
+ 'IOPort', 'StdIOPort',
25
+ 'Stmt', 'GateStmt', 'RawStmt', 'parse_stmt',
26
+ 'ExecContext', 'Scope',
27
+ 'QuantumBackend', 'QiskitBackend', 'LOCCRegBackend',
28
+ 'GATE_TABLE', 'GATE_ALIASES',
29
+ ]
30
+
31
+ __version__ = '0.1.0'
32
+
33
+ def __getattr__(name):
34
+ """Lazy import heavy modules on first access."""
35
+ _lazy_imports = {
36
+ 'GATE_TABLE': 'qbasic_core.engine',
37
+ 'GATE_ALIASES': 'qbasic_core.engine',
38
+ 'LOCCEngine': 'qbasic_core.engine',
39
+ 'ExecResult': 'qbasic_core.engine',
40
+ 'ExecOutcome': 'qbasic_core.engine',
41
+ 'QBasicTerminal': 'qbasic_core.terminal',
42
+ 'Engine': 'qbasic_core.engine_state',
43
+ 'ExecutorMixin': 'qbasic_core.executor',
44
+ 'ExpressionMixin': 'qbasic_core.expression',
45
+ 'DisplayMixin': 'qbasic_core.display',
46
+ 'LOCCMixin': 'qbasic_core.locc',
47
+ 'LOCCCommandsMixin': 'qbasic_core.locc_commands',
48
+ 'LOCCDisplayMixin': 'qbasic_core.locc_display',
49
+ 'LOCCExecutionMixin': 'qbasic_core.locc_execution',
50
+ 'ControlFlowMixin': 'qbasic_core.control_flow',
51
+ 'DemoMixin': 'qbasic_core.demos',
52
+ 'FileIOMixin': 'qbasic_core.file_io',
53
+ 'AnalysisMixin': 'qbasic_core.analysis',
54
+ 'SweepMixin': 'qbasic_core.sweep',
55
+ 'MemoryMixin': 'qbasic_core.memory',
56
+ 'StringMixin': 'qbasic_core.strings',
57
+ 'ScreenMixin': 'qbasic_core.screen',
58
+ 'ClassicMixin': 'qbasic_core.classic',
59
+ 'SubroutineMixin': 'qbasic_core.subs',
60
+ 'DebugMixin': 'qbasic_core.debug',
61
+ 'ProgramMgmtMixin': 'qbasic_core.program_mgmt',
62
+ 'ProfilerMixin': 'qbasic_core.profiler',
63
+ 'NoiseMixin': 'qbasic_core.noise_mixin',
64
+ 'StateDisplayMixin': 'qbasic_core.state_display',
65
+ 'TerminalProtocol': 'qbasic_core.protocol',
66
+ 'HELP_TEXT': 'qbasic_core.help_text',
67
+ 'BANNER_ART': 'qbasic_core.help_text',
68
+ 'QuantumBackend': 'qbasic_core.backend',
69
+ 'QiskitBackend': 'qbasic_core.backend',
70
+ 'LOCCRegBackend': 'qbasic_core.backend',
71
+ 'Scope': 'qbasic_core.scope',
72
+ 'parse_stmt': 'qbasic_core.parser',
73
+ }
74
+ if name in _lazy_imports:
75
+ import importlib
76
+ module = importlib.import_module(_lazy_imports[name])
77
+ val = getattr(module, name)
78
+ globals()[name] = val # cache for next access
79
+ return val
80
+ raise AttributeError(f"module 'qbasic_core' has no attribute {name}")
@@ -0,0 +1,6 @@
1
+ """Allow running qbasic_core as a module: python -m qbasic_core."""
2
+
3
+ from qbasic_core.terminal import QBasicTerminal
4
+
5
+ if __name__ == '__main__':
6
+ QBasicTerminal().repl()
@@ -0,0 +1,179 @@
1
+ """QBASIC analysis mixin — EXPECT, ENTROPY, DENSITY, BENCH, RAM."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ from qiskit import QuantumCircuit, transpile
8
+ from qiskit_aer import AerSimulator
9
+ import numpy as np
10
+
11
+ from qbasic_core.engine import (
12
+ _estimate_gb, _get_ram_gb,
13
+ OVERHEAD_FACTOR, RAM_BUDGET_FRACTION,
14
+ )
15
+
16
+
17
+ class AnalysisMixin:
18
+ """Analysis and introspection commands for QBasicTerminal.
19
+
20
+ Requires: TerminalProtocol — uses self.last_sv, self.last_counts,
21
+ self.num_qubits, self.shots, self.sim_method, self.sim_device,
22
+ self.locc, self.locc_mode.
23
+ """
24
+
25
+ def cmd_expect(self, rest: str) -> None:
26
+ """EXPECT <pauli> [qubits] — compute expectation value.
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")
30
+ return
31
+ parts = rest.split()
32
+ if not parts:
33
+ self.io.writeln("?USAGE: EXPECT <Z|X|Y|ZZ|...> [qubits]")
34
+ return
35
+ pauli_str = parts[0].upper()
36
+ qubits = [int(q) for q in parts[1:]] if len(parts) > 1 else list(range(len(pauli_str)))
37
+
38
+ try:
39
+ 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
43
+ for i, p in enumerate(pauli_str):
44
+ if i < len(qubits):
45
+ full_pauli[self.num_qubits - 1 - qubits[i]] = p
46
+ op = SparsePauliOp(''.join(full_pauli))
47
+ val = sv.expectation_value(op)
48
+ self.io.writeln(f" <{pauli_str}> on qubits {qubits} = {val.real:.6f}")
49
+ except Exception as e:
50
+ self.io.writeln(f"?EXPECT ERROR: {e}")
51
+
52
+ def cmd_entropy(self, rest: str = '') -> None:
53
+ """ENTROPY [qubits] — entanglement entropy of specified qubits vs rest.
54
+ Examples: ENTROPY 0 | ENTROPY 0 1 | ENTROPY (defaults to qubit 0)"""
55
+ if self.last_sv is None:
56
+ self.io.writeln("?NO STATE — RUN first")
57
+ return
58
+ if rest.strip():
59
+ partition_a = [int(q) for q in rest.replace(',', ' ').split() if q.strip()]
60
+ else:
61
+ partition_a = [0]
62
+ n = self.num_qubits
63
+ try:
64
+ from qiskit.quantum_info import Statevector, entropy, partial_trace
65
+ sv_obj = Statevector(np.ascontiguousarray(self.last_sv).ravel())
66
+ keep = partition_a
67
+ rho_a = partial_trace(sv_obj, [q for q in range(n) if q not in keep])
68
+ ent = entropy(rho_a, base=2)
69
+ self.io.writeln(f" Entanglement entropy S(A) = {ent:.6f} bits")
70
+ self.io.writeln(f" Partition A: qubits {partition_a}")
71
+ self.io.writeln(f" Partition B: qubits {[q for q in range(n) if q not in partition_a]}")
72
+ if ent < 0.01:
73
+ self.io.writeln(f" -> Qubits are separable (product state)")
74
+ elif abs(ent - len(partition_a)) < 0.01:
75
+ self.io.writeln(f" -> Maximally entangled")
76
+ except Exception as e:
77
+ self.io.writeln(f"?ENTROPY ERROR: {e}")
78
+
79
+ def cmd_density(self) -> None:
80
+ """Show density matrix (or partial trace for small systems)."""
81
+ if self.last_sv is None:
82
+ self.io.writeln("?NO STATE — RUN first")
83
+ return
84
+ sv = np.ascontiguousarray(self.last_sv).ravel()
85
+ rho = np.outer(sv, sv.conj())
86
+ n = self.num_qubits
87
+ dim = 2**n
88
+ if dim > 16:
89
+ self.io.writeln(f" Density matrix: {dim}x{dim} (too large to display)")
90
+ self.io.writeln(f" Purity: {np.real(np.trace(rho @ rho)):.6f}")
91
+ self.io.write(f" Von Neumann entropy: ")
92
+ eigvals = np.linalg.eigvalsh(rho)
93
+ eigvals = eigvals[eigvals > 1e-15]
94
+ ent = -np.sum(eigvals * np.log2(eigvals))
95
+ self.io.writeln(f"{ent:.6f} bits")
96
+ return
97
+ self.io.writeln(f"\n Density matrix ({dim}x{dim}):\n")
98
+ for i in range(dim):
99
+ row = []
100
+ for j in range(dim):
101
+ v = rho[i, j]
102
+ if abs(v.imag) < 1e-6:
103
+ row.append(f"{v.real:7.3f}")
104
+ else:
105
+ row.append(f"{v.real:+.2f}{v.imag:+.2f}j")
106
+ self.io.writeln(f" {' '.join(row)}")
107
+ self.io.writeln(f"\n Purity: {np.real(np.trace(rho @ rho)):.6f}")
108
+ self.io.writeln('')
109
+
110
+ def cmd_bench(self) -> None:
111
+ """BENCH — benchmark simulation at various qubit counts."""
112
+ self.io.writeln("\n Benchmark (H + CX chain + measure):")
113
+ try:
114
+ import psutil
115
+ has_psutil = True
116
+ except ImportError:
117
+ has_psutil = False
118
+ header_mem = ' mem_gb' if has_psutil else ''
119
+ self.io.writeln(f" {'qubits':>8} {'method':>20} {'time':>8}{header_mem}")
120
+ for n in [4, 8, 12, 16, 20, 24, 28]:
121
+ qc = QuantumCircuit(n)
122
+ for i in range(n):
123
+ qc.h(i)
124
+ for i in range(n - 1):
125
+ qc.cx(i, i + 1)
126
+ qc.measure_all()
127
+ method = 'stabilizer' if n > 28 else 'automatic'
128
+ backend = AerSimulator(method=method)
129
+ t0 = time.time()
130
+ try:
131
+ result = backend.run(transpile(qc, backend), shots=100).result()
132
+ dt = time.time() - t0
133
+ mem_str = f" {psutil.virtual_memory().used / 1e9:>7.1f}" if has_psutil else ""
134
+ self.io.writeln(f" {n:>8} {method:>20} {dt:>7.2f}s{mem_str}")
135
+ except Exception as e:
136
+ self.io.writeln(f" {n:>8} {method:>20} FAILED: {e}")
137
+ self.io.writeln('')
138
+
139
+ def cmd_ram(self) -> None:
140
+ """RAM — show memory budget, per-instance cost, and parallelism estimates."""
141
+ ram = _get_ram_gb()
142
+ if not ram:
143
+ self.io.writeln("?psutil not installed — RAM detection unavailable")
144
+ self.io.writeln(" Install: pip install psutil")
145
+ return
146
+ total, avail = ram
147
+ budget = total * RAM_BUDGET_FRACTION
148
+
149
+ self.io.writeln(f"\n System RAM: {total:.1f} GB total, {avail:.1f} GB available")
150
+ self.io.writeln(f" 80% budget: {budget:.1f} GB\n")
151
+
152
+ if self.locc_mode and self.locc:
153
+ mode = "JOINT" if self.locc.joint else "SPLIT"
154
+ sizes_str = '+'.join(str(s) for s in self.locc.sizes)
155
+ est, _ = self.locc.mem_gb()
156
+ max_par = int(budget / est) if est > 0 else 0
157
+ self.io.writeln(f" Current: LOCC {mode} {sizes_str}q")
158
+ self.io.writeln(f" Per instance: {est:.2f} GB")
159
+ if max_par > 0:
160
+ self.io.writeln(f" Parallel: ~{max_par} instances in budget")
161
+ else:
162
+ n = self.num_qubits
163
+ est = _estimate_gb(n)
164
+ max_par = int(budget / est) if est > 0 else 0
165
+ self.io.writeln(f" Current: {n} qubits")
166
+ self.io.writeln(f" Per instance: {est:.2f} GB")
167
+ if max_par > 0:
168
+ self.io.writeln(f" Parallel: ~{max_par} instances in budget")
169
+
170
+ self.io.writeln(f"\n Qubit scaling (per instance, with {OVERHEAD_FACTOR:.0f}x overhead):")
171
+ for nq in [16, 20, 24, 28, 30, 32]:
172
+ e = _estimate_gb(nq)
173
+ mp = int(budget / e) if e > 0 and budget >= e else 0
174
+ marker = " <--" if nq == self.num_qubits else ""
175
+ if mp > 0:
176
+ self.io.writeln(f" {nq:>2} qubits: {e:>8.2f} GB -> ~{mp:>6,} parallel{marker}")
177
+ else:
178
+ self.io.writeln(f" {nq:>2} qubits: {e:>8.2f} GB -> exceeds budget{marker}")
179
+ self.io.writeln('')
qbasic_core/backend.py ADDED
@@ -0,0 +1,76 @@
1
+ """QBASIC backend abstraction — Qiskit and numpy behind a common interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Protocol
6
+
7
+ import numpy as np
8
+
9
+
10
+ class QuantumBackend(Protocol):
11
+ """Abstract quantum backend for gate application."""
12
+
13
+ def apply_gate(self, gate_name: str, params: tuple[float, ...],
14
+ qubits: list[int]) -> None: ...
15
+ def barrier(self) -> None: ...
16
+ def reset(self, qubit: int) -> None: ...
17
+
18
+
19
+ class QiskitBackend:
20
+ """Wraps a QuantumCircuit for the circuit-build path."""
21
+
22
+ def __init__(self, qc: Any, apply_gate_fn: Any):
23
+ self._qc = qc
24
+ self._apply_gate = apply_gate_fn
25
+
26
+ def apply_gate(self, gate_name: str, params: tuple[float, ...],
27
+ qubits: list[int]) -> None:
28
+ self._apply_gate(self._qc, gate_name, list(params), qubits)
29
+
30
+ def barrier(self) -> None:
31
+ self._qc.barrier()
32
+
33
+ def reset(self, qubit: int) -> None:
34
+ self._qc.reset(qubit)
35
+
36
+ def measure(self, qubit: int, classical_bit: Any) -> None:
37
+ self._qc.measure(qubit, classical_bit)
38
+
39
+ def add_classical_register(self, name: str, size: int = 1) -> Any:
40
+ from qiskit.circuit import ClassicalRegister
41
+ cr = ClassicalRegister(size, name)
42
+ self._qc.add_register(cr)
43
+ return cr
44
+
45
+ def h(self, qubit: int) -> None:
46
+ self._qc.h(qubit)
47
+
48
+ def append_inverse(self, sub_qc: Any, qubits: list[int]) -> None:
49
+ self._qc.append(sub_qc.inverse(), qubits)
50
+
51
+ def append_controlled(self, gate: Any, qubits: list[int]) -> None:
52
+ self._qc.append(gate, qubits)
53
+
54
+ @property
55
+ def qc(self) -> Any:
56
+ return self._qc
57
+
58
+
59
+ class LOCCRegBackend:
60
+ """Wraps a LOCCEngine register for the numpy path."""
61
+
62
+ def __init__(self, engine: Any, reg: str):
63
+ self._engine = engine
64
+ self._reg = reg
65
+
66
+ def apply_gate(self, gate_name: str, params: tuple[float, ...],
67
+ qubits: list[int]) -> None:
68
+ self._engine.apply(self._reg, gate_name, params, qubits)
69
+
70
+ def barrier(self) -> None:
71
+ pass
72
+
73
+ def reset(self, qubit: int) -> None:
74
+ outcome = self._engine.send(self._reg, qubit)
75
+ if outcome == 1:
76
+ self._engine.apply(self._reg, 'X', (), [qubit])