qubasic 0.4.1__tar.gz → 0.5.1__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.1 → qubasic-0.5.1}/CHANGELOG.md +27 -0
- {qubasic-0.4.1/qubasic.egg-info → qubasic-0.5.1}/PKG-INFO +1 -1
- {qubasic-0.4.1 → qubasic-0.5.1}/pyproject.toml +1 -1
- {qubasic-0.4.1 → qubasic-0.5.1/qubasic.egg-info}/PKG-INFO +1 -1
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic.egg-info/SOURCES.txt +1 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/__init__.py +1 -1
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/display.py +10 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/locc_execution.py +3 -1
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/profiler.py +3 -1
- qubasic-0.5.1/qubasic_core/qol.py +703 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/terminal.py +69 -22
- {qubasic-0.4.1 → qubasic-0.5.1}/LICENSE +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/MANIFEST.in +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/README.md +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/examples/bell.qb +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/examples/grover3.qb +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/examples/locc_teleport.qb +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/examples/sweep_rx.qb +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/__main__.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/analysis.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/backend.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/classic.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/control_flow.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/debug.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/demos.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/engine.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/engine_state.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/errors.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/executor.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/expression.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/file_io.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/gates.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/help_text.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/locc.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/locc_commands.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/locc_engine.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/memory.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/mock_backend.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/noise_mixin.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/parser.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/patterns.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/protocol.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/scope.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/screen.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/state_display.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/statements.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/strings.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/subs.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/qubasic_core/sweep.py +0 -0
- {qubasic-0.4.1 → qubasic-0.5.1}/setup.cfg +0 -0
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 (2026-03-30)
|
|
4
|
+
|
|
5
|
+
- **Colorized histograms**: bars colored green/yellow/dim by probability in terminal
|
|
6
|
+
- **Animated STEP mode**: press A during STEP for auto-advance with configurable delay
|
|
7
|
+
- **"Did you mean?"**: typo suggestions for misspelled commands (BLASCH -> BLOCH)
|
|
8
|
+
- **Status bar prompt**: REPL prompt shows qubit count, LOCC, noise status
|
|
9
|
+
- **DRAW command**: braille-character Bloch sphere rendering
|
|
10
|
+
- **Color-coded LIST**: gates in cyan, flow in yellow, comments dim (THEME controls)
|
|
11
|
+
- **Gate throughput**: RUN summary shows gates/s and circuit complexity (T-count, CNOT count)
|
|
12
|
+
- **COMPARE command**: run circuit with two methods and diff output distributions
|
|
13
|
+
- **HEATMAP command**: qubit-qubit entanglement entropy grid
|
|
14
|
+
- **Startup tip of the day**: random tip shown at REPL launch
|
|
15
|
+
- **ANIMATE command**: animated parameter sweep with in-place terminal updates
|
|
16
|
+
- **QUIZ mode**: interactive quantum computing quiz with multiple choice
|
|
17
|
+
- **Tab completion for file paths**: SAVE/LOAD/INCLUDE complete .qb filenames
|
|
18
|
+
- **DIFF command**: diff current program against another BANK slot
|
|
19
|
+
- **PLOT command**: ASCII scatter plot of P(top state) vs swept variable
|
|
20
|
+
- **UNDO preview**: shows what lines will change before applying
|
|
21
|
+
- **THEME command**: switch color schemes (default, retro, none)
|
|
22
|
+
- **F1/F2/F3 demo shortcuts**: load Bell, GHZ, Grover demos via function keys
|
|
23
|
+
- **Quantum spinner**: |0>, |+>, |1>, |-> cycling during STATS and LOCC progress
|
|
24
|
+
- **EXPLAIN command**: describe each program line in plain English
|
|
25
|
+
- **LOCC progress spinner**: quantum-themed progress during SEND-based LOCC runs
|
|
26
|
+
- **CLIP command**: copy last results to system clipboard
|
|
27
|
+
- **Sound on completion**: terminal bell for runs exceeding 2 seconds
|
|
28
|
+
- New QoLMixin with 24 quality-of-life features
|
|
29
|
+
|
|
3
30
|
## 0.4.1 (2026-03-30)
|
|
4
31
|
|
|
5
32
|
- **Fix SEED dispatch**: moved from no-arg to with-arg dispatch table so `SEED 42` works from the REPL
|
|
@@ -88,11 +88,21 @@ class DisplayMixin:
|
|
|
88
88
|
max_count = max(c for _, c in display) if display else 1
|
|
89
89
|
max_label = max(len(k) for k, _ in display) if display else 1
|
|
90
90
|
|
|
91
|
+
_theme = getattr(self, '_theme', {})
|
|
91
92
|
for state, count in display:
|
|
92
93
|
pct = 100 * count / total
|
|
93
94
|
bar_len = int(HISTOGRAM_BAR_WIDTH * count / max_count)
|
|
94
95
|
bar = '\u2588' * bar_len
|
|
95
96
|
ket = f"|{state}\u27E9"
|
|
97
|
+
# Colorize bar by probability
|
|
98
|
+
if _theme and sys.stdout.isatty():
|
|
99
|
+
rst = _theme.get('reset', '')
|
|
100
|
+
if pct > 40:
|
|
101
|
+
bar = f"{_theme.get('bar_hi', '')}{bar}{rst}"
|
|
102
|
+
elif pct > 10:
|
|
103
|
+
bar = f"{_theme.get('bar_mid', '')}{bar}{rst}"
|
|
104
|
+
else:
|
|
105
|
+
bar = f"{_theme.get('bar_lo', '')}{bar}{rst}"
|
|
96
106
|
self.io.writeln(f" {ket:>{max_label+3}} {count:>6} ({pct:5.1f}%) {bar}")
|
|
97
107
|
|
|
98
108
|
if len(sorted_counts) > MAX_HISTOGRAM_STATES:
|
|
@@ -134,7 +134,9 @@ class LOCCExecutionMixin:
|
|
|
134
134
|
counts_joint[jkey] = counts_joint.get(jkey, 0) + 1
|
|
135
135
|
if shots > 50 and (shot + 1) % progress_interval == 0:
|
|
136
136
|
pct = 100 * (shot + 1) // shots
|
|
137
|
-
|
|
137
|
+
from qubasic_core.qol import quantum_spin
|
|
138
|
+
spin = quantum_spin(shot)
|
|
139
|
+
_prog = f" {spin} {pct}% ({shot+1}/{shots} shots)..."
|
|
138
140
|
if hasattr(self.io, 'write') and sys.stdout.isatty():
|
|
139
141
|
self.io.write(_prog + '\r')
|
|
140
142
|
# Non-terminal: skip progress to avoid \r noise
|
|
@@ -128,7 +128,9 @@ class ProfilerMixin:
|
|
|
128
128
|
break
|
|
129
129
|
self._stats_runs.append(dict(self.last_counts))
|
|
130
130
|
if n > 10 and (trial + 1) % (n // 10) == 0:
|
|
131
|
-
|
|
131
|
+
from qubasic_core.qol import quantum_spin
|
|
132
|
+
spin = quantum_spin(trial)
|
|
133
|
+
self.io.write(f" {spin} {100 * (trial + 1) // n}%..." + '\r')
|
|
132
134
|
if n > 10:
|
|
133
135
|
self.io.write(" " * 30 + '\r')
|
|
134
136
|
self.io.writeln(f"Collected {len(self._stats_runs)} runs ({n} trials)")
|
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
"""QUBASIC quality-of-life features — visual, fun, and convenience commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import random
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
17
|
+
# Quantum spinner (#21)
|
|
18
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
_QUANTUM_SPINNER = [
|
|
21
|
+
'|0\u27E9', '|+\u27E9', '|1\u27E9', '|-\u27E9',
|
|
22
|
+
'|i\u27E9', '|\u03C8\u27E9', '|0\u27E9', '|\u03D5\u27E9',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def quantum_spin(step: int) -> str:
|
|
27
|
+
"""Return the spinner frame for the given step."""
|
|
28
|
+
return _QUANTUM_SPINNER[step % len(_QUANTUM_SPINNER)]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
32
|
+
# Tips of the day (#11)
|
|
33
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
34
|
+
|
|
35
|
+
_TIPS = [
|
|
36
|
+
"Use CTRL H 0, 1 to make any gate controlled.",
|
|
37
|
+
"STEP mode lets you watch the statevector evolve line by line.",
|
|
38
|
+
"REWIND goes back in time during STEP mode.",
|
|
39
|
+
"LOCC JOINT 3 3 enables quantum teleportation with classical correction.",
|
|
40
|
+
"SWEEP var 0 PI 10 shows how probability changes with a parameter.",
|
|
41
|
+
"NOISE depolarizing 0.05 adds realistic noise to your circuit.",
|
|
42
|
+
"SEED 42 makes your results reproducible.",
|
|
43
|
+
"DEMO LIST shows all 12 built-in quantum algorithms.",
|
|
44
|
+
"BLOCH draws an ASCII Bloch sphere for each qubit.",
|
|
45
|
+
"INV RX 0.5, 0 applies the inverse (dagger) of any gate.",
|
|
46
|
+
"UNITARY MYGATE = [[0,1],[1,0]] defines a custom gate from a matrix.",
|
|
47
|
+
"DEF BELL = H 0 : CX 0,1 creates a reusable gate sequence.",
|
|
48
|
+
"POKE $D000, 8 sets the qubit count via the memory map.",
|
|
49
|
+
"SYS $E000 runs a built-in algorithm by address.",
|
|
50
|
+
"STATS 100 runs 100 trials and shows mean/stddev per state.",
|
|
51
|
+
"PROFILE ON then RUN shows per-line execution time.",
|
|
52
|
+
"ENTROPY 0 measures entanglement of qubit 0 with the rest.",
|
|
53
|
+
"EXPECT ZZ 0 1 computes the two-qubit ZZ correlation.",
|
|
54
|
+
"BANK 1 switches to a second program slot (auto-saves current).",
|
|
55
|
+
"SCREEN 3 auto-displays Bloch spheres after every RUN.",
|
|
56
|
+
"CONSISTENCY cross-checks statevector, density, Bloch, and histogram.",
|
|
57
|
+
"FOR I = 0 TO 7 / H I / NEXT I applies H to 8 qubits in a loop.",
|
|
58
|
+
"FIND \"CX\" searches your program for a string.",
|
|
59
|
+
"CHECKSUM gives an MD5 hash for program verification.",
|
|
60
|
+
"10 H 0 : CX 0,1 : RZ PI/4, 0 puts three gates on one line.",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def tip_of_the_day() -> str:
|
|
65
|
+
"""Return a random tip."""
|
|
66
|
+
return random.choice(_TIPS)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
70
|
+
# "Did you mean?" (#4)
|
|
71
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
72
|
+
|
|
73
|
+
_ALL_COMMANDS = [
|
|
74
|
+
'RUN', 'LIST', 'NEW', 'SAVE', 'LOAD', 'QUBITS', 'SHOTS', 'METHOD',
|
|
75
|
+
'DEF', 'REG', 'LET', 'STEP', 'STATE', 'HIST', 'BLOCH', 'PROBS',
|
|
76
|
+
'DEMO', 'DELETE', 'RENUM', 'DEFS', 'REGS', 'VARS', 'HELP',
|
|
77
|
+
'CIRCUIT', 'LOCC', 'SEND', 'SHARE', 'SWEEP', 'INCLUDE', 'EXPORT',
|
|
78
|
+
'DECOMPOSE', 'NOISE', 'EXPECT', 'DENSITY', 'ENTROPY', 'CSV',
|
|
79
|
+
'MEASURE', 'BARRIER', 'PRINT', 'INPUT', 'DIM', 'UNITARY',
|
|
80
|
+
'LOCCINFO', 'RAM', 'UNDO', 'CLEAR', 'PEEK', 'POKE', 'SYS',
|
|
81
|
+
'DUMP', 'MAP', 'CATALOG', 'MONITOR', 'SCREEN', 'COLOR', 'CLS',
|
|
82
|
+
'LOCATE', 'PLAY', 'PROMPT', 'BREAK', 'WATCH', 'PROFILE', 'STATS',
|
|
83
|
+
'REWIND', 'FORWARD', 'HISTORY', 'TRON', 'TROFF', 'CONT',
|
|
84
|
+
'AUTO', 'EDIT', 'COPY', 'MOVE', 'FIND', 'REPLACE', 'BANK',
|
|
85
|
+
'CHAIN', 'MERGE', 'CHECKSUM', 'VERSION', 'SEED', 'PROBE',
|
|
86
|
+
'RESTORE', 'OPEN', 'CLOSE', 'IMPORT', 'SAMPLE', 'ESTIMATE',
|
|
87
|
+
'BENCH', 'SET_STATE', 'CIRCUIT_DEF', 'APPLY_CIRCUIT',
|
|
88
|
+
'CONSISTENCY', 'COMPARE', 'HEATMAP', 'ANIMATE', 'QUIZ',
|
|
89
|
+
'DIFF', 'PLOT', 'THEME', 'CLIP', 'EXPLAIN',
|
|
90
|
+
# Gates
|
|
91
|
+
'H', 'X', 'Y', 'Z', 'S', 'T', 'SDG', 'TDG', 'SX', 'ID',
|
|
92
|
+
'RX', 'RY', 'RZ', 'P', 'U', 'CX', 'CZ', 'CY', 'CH',
|
|
93
|
+
'SWAP', 'DCX', 'ISWAP', 'CRX', 'CRY', 'CRZ', 'CP',
|
|
94
|
+
'RXX', 'RYY', 'RZZ', 'CCX', 'CSWAP',
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def did_you_mean(word: str) -> str | None:
|
|
99
|
+
"""Return a suggestion for a misspelled command, or None."""
|
|
100
|
+
w = word.upper()
|
|
101
|
+
if w in _ALL_COMMANDS:
|
|
102
|
+
return None
|
|
103
|
+
matches = difflib.get_close_matches(w, _ALL_COMMANDS, n=1, cutoff=0.6)
|
|
104
|
+
return matches[0] if matches else None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
108
|
+
# Color themes (#19)
|
|
109
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
110
|
+
|
|
111
|
+
THEMES = {
|
|
112
|
+
'default': {'reset': '\033[0m', 'gate': '\033[36m', 'flow': '\033[33m',
|
|
113
|
+
'comment': '\033[2m', 'string': '\033[32m', 'number': '\033[35m',
|
|
114
|
+
'error': '\033[31m', 'bar_hi': '\033[32m', 'bar_mid': '\033[33m',
|
|
115
|
+
'bar_lo': '\033[2m', 'prompt': ''},
|
|
116
|
+
'retro': {'reset': '\033[0m', 'gate': '\033[32m', 'flow': '\033[32m',
|
|
117
|
+
'comment': '\033[2;32m', 'string': '\033[32m', 'number': '\033[32m',
|
|
118
|
+
'error': '\033[1;32m', 'bar_hi': '\033[1;32m', 'bar_mid': '\033[32m',
|
|
119
|
+
'bar_lo': '\033[2;32m', 'prompt': '\033[32m'},
|
|
120
|
+
'none': {'reset': '', 'gate': '', 'flow': '', 'comment': '', 'string': '',
|
|
121
|
+
'number': '', 'error': '', 'bar_hi': '', 'bar_mid': '', 'bar_lo': '',
|
|
122
|
+
'prompt': ''},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
127
|
+
# Braille Bloch sphere (#6)
|
|
128
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
129
|
+
|
|
130
|
+
_BRAILLE_BASE = 0x2800
|
|
131
|
+
# Braille dot positions: col0=(0,1,2,6), col1=(3,4,5,7)
|
|
132
|
+
_BRAILLE_DOTS = [
|
|
133
|
+
(0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (0, 3, 0x40),
|
|
134
|
+
(1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (1, 3, 0x80),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def braille_bloch(x: float, y: float, z: float, radius: int = 10) -> list[str]:
|
|
139
|
+
"""Render a Bloch sphere in braille characters (XZ plane projection).
|
|
140
|
+
|
|
141
|
+
Returns a list of strings (lines) forming the braille image.
|
|
142
|
+
"""
|
|
143
|
+
W = radius * 2 + 1 # pixels
|
|
144
|
+
# Braille cell = 2 wide x 4 tall pixels
|
|
145
|
+
cols = (W + 1) // 2
|
|
146
|
+
rows = (W + 3) // 4
|
|
147
|
+
|
|
148
|
+
grid = [[0] * cols for _ in range(rows)]
|
|
149
|
+
|
|
150
|
+
def _set_pixel(px: int, py: int) -> None:
|
|
151
|
+
if 0 <= px < W and 0 <= py < W:
|
|
152
|
+
bc = px // 2
|
|
153
|
+
br = py // 4
|
|
154
|
+
dx = px % 2
|
|
155
|
+
dy = py % 4
|
|
156
|
+
for dc, dr, bit in _BRAILLE_DOTS:
|
|
157
|
+
if dc == dx and dr == dy:
|
|
158
|
+
grid[br][bc] |= bit
|
|
159
|
+
|
|
160
|
+
cx, cy = radius, radius
|
|
161
|
+
|
|
162
|
+
# Draw circle
|
|
163
|
+
for angle in range(0, 360, 3):
|
|
164
|
+
rad = math.radians(angle)
|
|
165
|
+
px = round(cx + radius * math.cos(rad))
|
|
166
|
+
py = round(cy + radius * math.sin(rad))
|
|
167
|
+
_set_pixel(px, py)
|
|
168
|
+
|
|
169
|
+
# Axes (horizontal and vertical through center)
|
|
170
|
+
for i in range(W):
|
|
171
|
+
_set_pixel(i, cy)
|
|
172
|
+
_set_pixel(cx, i)
|
|
173
|
+
|
|
174
|
+
# State point
|
|
175
|
+
px = round(cx + x * (radius - 1))
|
|
176
|
+
pz = round(cy - z * (radius - 1))
|
|
177
|
+
# Draw a 2x2 block for visibility
|
|
178
|
+
for dx in range(-1, 2):
|
|
179
|
+
for dy in range(-1, 2):
|
|
180
|
+
_set_pixel(px + dx, pz + dy)
|
|
181
|
+
|
|
182
|
+
lines = []
|
|
183
|
+
for row in grid:
|
|
184
|
+
line = ''.join(chr(_BRAILLE_BASE + cell) for cell in row)
|
|
185
|
+
lines.append(line)
|
|
186
|
+
return lines
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
190
|
+
# QoL Mixin
|
|
191
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
192
|
+
|
|
193
|
+
class QoLMixin:
|
|
194
|
+
"""Quality-of-life commands for QBasicTerminal.
|
|
195
|
+
|
|
196
|
+
Provides: COMPARE, HEATMAP, ANIMATE, QUIZ, DIFF, PLOT, THEME,
|
|
197
|
+
CLIP, EXPLAIN, and enhanced display features.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def _init_qol(self) -> None:
|
|
201
|
+
self._theme: dict[str, str] = THEMES['default']
|
|
202
|
+
self._theme_name: str = 'default'
|
|
203
|
+
|
|
204
|
+
# ── #4: "Did you mean?" in dispatch ──────────────────────────────
|
|
205
|
+
|
|
206
|
+
def _suggest_command(self, word: str) -> None:
|
|
207
|
+
"""Print a suggestion if the word is close to a known command."""
|
|
208
|
+
suggestion = did_you_mean(word)
|
|
209
|
+
if suggestion:
|
|
210
|
+
self.io.writeln(f" Did you mean: {suggestion}?")
|
|
211
|
+
|
|
212
|
+
# ── #5: Status bar prompt ─────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
def _status_prompt(self) -> str:
|
|
215
|
+
"""Build a prompt showing current config."""
|
|
216
|
+
parts = [f"{self.num_qubits}q"]
|
|
217
|
+
if self.locc_mode:
|
|
218
|
+
parts.append("LOCC")
|
|
219
|
+
if self._noise_model:
|
|
220
|
+
parts.append("noisy")
|
|
221
|
+
return ' '.join(parts) + ' ] '
|
|
222
|
+
|
|
223
|
+
# ── #6: Braille Bloch sphere ──────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
def cmd_draw(self, rest: str) -> None:
|
|
226
|
+
"""DRAW [qubit] — braille Bloch sphere."""
|
|
227
|
+
sv = self._active_sv
|
|
228
|
+
if sv is None:
|
|
229
|
+
self.io.writeln("?NO STATE -- RUN first")
|
|
230
|
+
return
|
|
231
|
+
n = self._active_nqubits
|
|
232
|
+
if rest.strip():
|
|
233
|
+
q = int(rest.strip())
|
|
234
|
+
x, y, z = self._bloch_vector(sv, q, n)
|
|
235
|
+
self._draw_braille_bloch(x, y, z, q)
|
|
236
|
+
else:
|
|
237
|
+
for q in range(min(n, 4)):
|
|
238
|
+
x, y, z = self._bloch_vector(sv, q, n)
|
|
239
|
+
self._draw_braille_bloch(x, y, z, q)
|
|
240
|
+
|
|
241
|
+
def _draw_braille_bloch(self, x: float, y: float, z: float, qubit: int) -> None:
|
|
242
|
+
label = f"q{qubit} ({x:.2f},{y:.2f},{z:.2f})"
|
|
243
|
+
self.io.writeln(f" {label}")
|
|
244
|
+
for line in braille_bloch(x, y, z):
|
|
245
|
+
self.io.writeln(f" {line}")
|
|
246
|
+
|
|
247
|
+
# ── #7: Color-coded LIST ──────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
def cmd_list_colored(self) -> None:
|
|
250
|
+
"""LIST with syntax highlighting."""
|
|
251
|
+
if not self.program:
|
|
252
|
+
self.io.writeln("EMPTY PROGRAM")
|
|
253
|
+
return
|
|
254
|
+
t = self._theme
|
|
255
|
+
from qubasic_core.gates import GATE_TABLE, GATE_ALIASES
|
|
256
|
+
gate_names = set(GATE_TABLE.keys()) | set(GATE_ALIASES.keys())
|
|
257
|
+
flow_kw = {'FOR', 'NEXT', 'WHILE', 'WEND', 'IF', 'THEN', 'ELSE',
|
|
258
|
+
'GOTO', 'GOSUB', 'RETURN', 'END', 'DO', 'LOOP', 'EXIT',
|
|
259
|
+
'SUB', 'FUNCTION', 'CALL', 'SELECT', 'CASE'}
|
|
260
|
+
for num in sorted(self.program.keys()):
|
|
261
|
+
raw = self.program[num]
|
|
262
|
+
upper = raw.strip().upper()
|
|
263
|
+
if upper.startswith('REM') or upper.startswith("'"):
|
|
264
|
+
colored = f"{t['comment']}{raw}{t['reset']}"
|
|
265
|
+
else:
|
|
266
|
+
words = raw.split()
|
|
267
|
+
parts = []
|
|
268
|
+
for w in words:
|
|
269
|
+
wu = w.upper().rstrip(',').rstrip(':')
|
|
270
|
+
if wu in gate_names:
|
|
271
|
+
parts.append(f"{t['gate']}{w}{t['reset']}")
|
|
272
|
+
elif wu in flow_kw:
|
|
273
|
+
parts.append(f"{t['flow']}{w}{t['reset']}")
|
|
274
|
+
elif w.startswith('"') or w.startswith("'"):
|
|
275
|
+
parts.append(f"{t['string']}{w}{t['reset']}")
|
|
276
|
+
else:
|
|
277
|
+
parts.append(w)
|
|
278
|
+
colored = ' '.join(parts)
|
|
279
|
+
self.io.writeln(f" {num:5d} {colored}")
|
|
280
|
+
|
|
281
|
+
# ── #8: Gate throughput in RUN summary ────────────────────────────
|
|
282
|
+
# (Injected into cmd_run output — see terminal.py modification)
|
|
283
|
+
|
|
284
|
+
# ── #9: COMPARE ──────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
def cmd_compare(self, rest: str) -> None:
|
|
287
|
+
"""COMPARE method1 method2 — run circuit with two methods and diff results."""
|
|
288
|
+
parts = rest.split()
|
|
289
|
+
if len(parts) < 2:
|
|
290
|
+
self.io.writeln("?USAGE: COMPARE <method1> <method2>")
|
|
291
|
+
return
|
|
292
|
+
m1, m2 = parts[0].lower(), parts[1].lower()
|
|
293
|
+
old_method = self.sim_method
|
|
294
|
+
results = {}
|
|
295
|
+
for m in [m1, m2]:
|
|
296
|
+
self.sim_method = m
|
|
297
|
+
old_io = self.io
|
|
298
|
+
|
|
299
|
+
class _NullIO:
|
|
300
|
+
def write(self, t: str) -> None: pass
|
|
301
|
+
def writeln(self, t: str) -> None: pass
|
|
302
|
+
def read_line(self, p: str) -> str: return ''
|
|
303
|
+
self.io = _NullIO()
|
|
304
|
+
try:
|
|
305
|
+
self.cmd_run()
|
|
306
|
+
finally:
|
|
307
|
+
self.io = old_io
|
|
308
|
+
results[m] = dict(self.last_counts) if self.last_counts else {}
|
|
309
|
+
self.sim_method = old_method
|
|
310
|
+
# Display comparison
|
|
311
|
+
all_states = sorted(set(list(results[m1].keys()) + list(results[m2].keys())))
|
|
312
|
+
total1 = sum(results[m1].values()) or 1
|
|
313
|
+
total2 = sum(results[m2].values()) or 1
|
|
314
|
+
self.io.writeln(f"\n {'State':>12} {m1:>12} {m2:>12} {'diff':>8}")
|
|
315
|
+
for s in all_states[:32]:
|
|
316
|
+
c1 = results[m1].get(s, 0)
|
|
317
|
+
c2 = results[m2].get(s, 0)
|
|
318
|
+
p1, p2 = c1 / total1, c2 / total2
|
|
319
|
+
diff = p2 - p1
|
|
320
|
+
self.io.writeln(f" |{s}\u27E9 {p1:>11.3%} {p2:>11.3%} {diff:>+7.3%}")
|
|
321
|
+
self.io.writeln('')
|
|
322
|
+
|
|
323
|
+
# ── #10: HEATMAP ─────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
def cmd_heatmap(self, rest: str = '') -> None:
|
|
326
|
+
"""HEATMAP — qubit-qubit entanglement entropy grid."""
|
|
327
|
+
sv = self._active_sv
|
|
328
|
+
if sv is None:
|
|
329
|
+
self.io.writeln("?NO STATE -- RUN first")
|
|
330
|
+
return
|
|
331
|
+
n = self._active_nqubits
|
|
332
|
+
n_show = min(n, 12)
|
|
333
|
+
self.io.writeln(f"\n Entanglement heatmap ({n_show} qubits):")
|
|
334
|
+
_shades = ' \u2591\u2592\u2593\u2588'
|
|
335
|
+
header = ' ' + ''.join(f'{q:>4}' for q in range(n_show))
|
|
336
|
+
self.io.writeln(header)
|
|
337
|
+
sv_flat = np.ascontiguousarray(sv).ravel()
|
|
338
|
+
for qi in range(n_show):
|
|
339
|
+
row = f' {qi:>2} '
|
|
340
|
+
for qj in range(n_show):
|
|
341
|
+
if qi == qj:
|
|
342
|
+
row += ' - '
|
|
343
|
+
continue
|
|
344
|
+
keep = sorted({qi, qj})
|
|
345
|
+
trace_out = [q for q in range(n) if q not in keep]
|
|
346
|
+
psi_t = sv_flat.reshape([2] * n)
|
|
347
|
+
if trace_out:
|
|
348
|
+
rho = np.tensordot(psi_t, psi_t.conj(), axes=(trace_out, trace_out))
|
|
349
|
+
else:
|
|
350
|
+
rho = np.outer(sv_flat, sv_flat.conj())
|
|
351
|
+
rho = rho.reshape(4, 4)
|
|
352
|
+
ev = np.linalg.eigvalsh(rho)
|
|
353
|
+
ev = ev[ev > 1e-14]
|
|
354
|
+
S = -np.sum(ev * np.log2(ev)) if len(ev) > 0 else 0.0
|
|
355
|
+
idx = min(len(_shades) - 1, int(S * 2))
|
|
356
|
+
row += f' {_shades[idx]}{S:.0f} ' if S > 0.05 else ' . '
|
|
357
|
+
self.io.writeln(row)
|
|
358
|
+
self.io.writeln('')
|
|
359
|
+
|
|
360
|
+
# ── #12: ANIMATE ──────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
def cmd_animate(self, rest: str) -> None:
|
|
363
|
+
"""ANIMATE var start end [steps] [delay] — animated parameter sweep."""
|
|
364
|
+
parts = rest.split()
|
|
365
|
+
if len(parts) < 3:
|
|
366
|
+
self.io.writeln("?USAGE: ANIMATE <var> <start> <end> [steps] [delay_ms]")
|
|
367
|
+
return
|
|
368
|
+
var = parts[0]
|
|
369
|
+
start = self.eval_expr(parts[1])
|
|
370
|
+
end = self.eval_expr(parts[2])
|
|
371
|
+
steps = int(parts[3]) if len(parts) > 3 else 10
|
|
372
|
+
delay = float(parts[4]) / 1000 if len(parts) > 4 else 0.3
|
|
373
|
+
values = [start + (end - start) * i / max(1, steps - 1) for i in range(steps)]
|
|
374
|
+
|
|
375
|
+
class _NullIO:
|
|
376
|
+
def write(self, t: str) -> None: pass
|
|
377
|
+
def writeln(self, t: str) -> None: pass
|
|
378
|
+
def read_line(self, p: str) -> str: return ''
|
|
379
|
+
|
|
380
|
+
for i, val in enumerate(values):
|
|
381
|
+
self.variables[var] = val
|
|
382
|
+
old_io = self.io
|
|
383
|
+
self.io = _NullIO()
|
|
384
|
+
try:
|
|
385
|
+
self.cmd_run()
|
|
386
|
+
finally:
|
|
387
|
+
self.io = old_io
|
|
388
|
+
if self.last_counts:
|
|
389
|
+
ranked = sorted(self.last_counts.items(), key=lambda x: -x[1])
|
|
390
|
+
total = sum(self.last_counts.values())
|
|
391
|
+
top = ranked[0]
|
|
392
|
+
bar = '\u2588' * int(30 * top[1] / total)
|
|
393
|
+
frame = f"\r {var}={val:>8.3f} |{top[0]}\u27E9 {top[1]/total:>5.1%} {bar:<30}"
|
|
394
|
+
old_io.write(frame)
|
|
395
|
+
time.sleep(delay)
|
|
396
|
+
self.io.writeln('')
|
|
397
|
+
|
|
398
|
+
# ── #13: QUIZ ─────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
def cmd_quiz(self, rest: str = '') -> None:
|
|
401
|
+
"""QUIZ — quantum computing quiz."""
|
|
402
|
+
quizzes = [
|
|
403
|
+
("H|0> = ?", ["|+>", "|1>", "|->", "|0>"], 0),
|
|
404
|
+
("CX|10> = ?", ["|11>", "|10>", "|00>", "|01>"], 0),
|
|
405
|
+
("X|0> = ?", ["|1>", "|0>", "|+>", "|->"], 0),
|
|
406
|
+
("Z|+> = ?", ["|->", "|+>", "|0>", "|1>"], 0),
|
|
407
|
+
("H|1> = ?", ["|->", "|+>", "|0>", "|1>"], 0),
|
|
408
|
+
("What gate creates superposition?", ["H", "X", "Z", "CX"], 0),
|
|
409
|
+
("Bell state is:", ["|00>+|11>", "|00>+|01>", "|01>+|10>", "|00>-|11>"], 0),
|
|
410
|
+
("CNOT flips target when control is:", ["|1>", "|0>", "|+>", "|->"], 0),
|
|
411
|
+
]
|
|
412
|
+
q = random.choice(quizzes)
|
|
413
|
+
prompt, options, correct = q
|
|
414
|
+
self.io.writeln(f"\n QUIZ: {prompt}")
|
|
415
|
+
indices = list(range(len(options)))
|
|
416
|
+
random.shuffle(indices)
|
|
417
|
+
correct_label = None
|
|
418
|
+
for label_idx, orig_idx in enumerate(indices):
|
|
419
|
+
letter = chr(65 + label_idx)
|
|
420
|
+
marker = ""
|
|
421
|
+
if orig_idx == correct:
|
|
422
|
+
correct_label = letter
|
|
423
|
+
self.io.writeln(f" {letter}) {options[orig_idx]}")
|
|
424
|
+
try:
|
|
425
|
+
answer = self.io.read_line(" Your answer: ").strip().upper()
|
|
426
|
+
except (EOFError, KeyboardInterrupt):
|
|
427
|
+
self.io.writeln("")
|
|
428
|
+
return
|
|
429
|
+
if answer == correct_label:
|
|
430
|
+
self.io.writeln(" Correct!")
|
|
431
|
+
else:
|
|
432
|
+
self.io.writeln(f" Wrong -- the answer is {correct_label}) {options[correct]}")
|
|
433
|
+
self.io.writeln('')
|
|
434
|
+
|
|
435
|
+
# ── #15: DIFF ─────────────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
def cmd_diff(self, rest: str) -> None:
|
|
438
|
+
"""DIFF <slot> — diff current program against another bank slot."""
|
|
439
|
+
if not rest.strip():
|
|
440
|
+
self.io.writeln("?USAGE: DIFF <slot>")
|
|
441
|
+
return
|
|
442
|
+
slot = int(rest.strip())
|
|
443
|
+
other = self._program_slots.get(slot, {})
|
|
444
|
+
if not other:
|
|
445
|
+
self.io.writeln(f"?BANK {slot} is empty")
|
|
446
|
+
return
|
|
447
|
+
current_lines = [f"{n} {self.program[n]}" for n in sorted(self.program.keys())]
|
|
448
|
+
other_lines = [f"{n} {other[n]}" for n in sorted(other.keys())]
|
|
449
|
+
diff = list(difflib.unified_diff(other_lines, current_lines,
|
|
450
|
+
fromfile=f'bank {slot}', tofile='current',
|
|
451
|
+
lineterm=''))
|
|
452
|
+
if diff:
|
|
453
|
+
for line in diff:
|
|
454
|
+
self.io.writeln(f" {line}")
|
|
455
|
+
else:
|
|
456
|
+
self.io.writeln(f" No differences with bank {slot}")
|
|
457
|
+
|
|
458
|
+
# ── #16: Circuit complexity readout ────────────────────────────────
|
|
459
|
+
|
|
460
|
+
def _circuit_complexity(self) -> str:
|
|
461
|
+
"""Return a complexity summary string for the last circuit."""
|
|
462
|
+
if not self.last_circuit:
|
|
463
|
+
return ''
|
|
464
|
+
qc = self.last_circuit
|
|
465
|
+
ops = qc.count_ops()
|
|
466
|
+
t_count = ops.get('t', 0) + ops.get('tdg', 0)
|
|
467
|
+
cx_count = ops.get('cx', 0) + ops.get('cnot', 0)
|
|
468
|
+
parts = []
|
|
469
|
+
if t_count:
|
|
470
|
+
parts.append(f"T-count={t_count}")
|
|
471
|
+
if cx_count:
|
|
472
|
+
parts.append(f"CNOT={cx_count}")
|
|
473
|
+
return ', '.join(parts)
|
|
474
|
+
|
|
475
|
+
# ── #17: PLOT ─────────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
def cmd_plot(self, rest: str) -> None:
|
|
478
|
+
"""PLOT var start end [steps] — braille line plot of P(|0...0>) vs variable."""
|
|
479
|
+
parts = rest.split()
|
|
480
|
+
if len(parts) < 3:
|
|
481
|
+
self.io.writeln("?USAGE: PLOT <var> <start> <end> [steps]")
|
|
482
|
+
return
|
|
483
|
+
var = parts[0]
|
|
484
|
+
start = self.eval_expr(parts[1])
|
|
485
|
+
end = self.eval_expr(parts[2])
|
|
486
|
+
steps = int(parts[3]) if len(parts) > 3 else 20
|
|
487
|
+
values = [start + (end - start) * i / max(1, steps - 1) for i in range(steps)]
|
|
488
|
+
|
|
489
|
+
class _NullIO:
|
|
490
|
+
def write(self, t: str) -> None: pass
|
|
491
|
+
def writeln(self, t: str) -> None: pass
|
|
492
|
+
def read_line(self, p: str) -> str: return ''
|
|
493
|
+
|
|
494
|
+
xs, ys = [], []
|
|
495
|
+
for val in values:
|
|
496
|
+
self.variables[var] = val
|
|
497
|
+
old_io = self.io
|
|
498
|
+
self.io = _NullIO()
|
|
499
|
+
try:
|
|
500
|
+
self.cmd_run()
|
|
501
|
+
finally:
|
|
502
|
+
self.io = old_io
|
|
503
|
+
if self.last_counts:
|
|
504
|
+
total = sum(self.last_counts.values())
|
|
505
|
+
top_state = max(self.last_counts, key=self.last_counts.get)
|
|
506
|
+
xs.append(val)
|
|
507
|
+
ys.append(self.last_counts[top_state] / total)
|
|
508
|
+
|
|
509
|
+
if not xs:
|
|
510
|
+
self.io.writeln("?No data to plot")
|
|
511
|
+
return
|
|
512
|
+
# ASCII plot
|
|
513
|
+
W, H = 60, 12
|
|
514
|
+
y_min, y_max = min(ys), max(ys)
|
|
515
|
+
if y_max - y_min < 0.001:
|
|
516
|
+
y_max = y_min + 0.1
|
|
517
|
+
self.io.writeln(f"\n P(top) vs {var}: {start} to {end}")
|
|
518
|
+
canvas = [[' '] * W for _ in range(H)]
|
|
519
|
+
for i, (xv, yv) in enumerate(zip(xs, ys)):
|
|
520
|
+
col = int((i / max(1, len(xs) - 1)) * (W - 1))
|
|
521
|
+
row = H - 1 - int(((yv - y_min) / (y_max - y_min)) * (H - 1))
|
|
522
|
+
row = max(0, min(H - 1, row))
|
|
523
|
+
col = max(0, min(W - 1, col))
|
|
524
|
+
canvas[row][col] = '\u2022'
|
|
525
|
+
for r, line in enumerate(canvas):
|
|
526
|
+
label = f"{y_max - (y_max - y_min) * r / (H - 1):.2f}" if r in (0, H - 1) else ' '
|
|
527
|
+
self.io.writeln(f" {label:>5} |{''.join(line)}|")
|
|
528
|
+
self.io.writeln(f" {start:<10.3f}{' ' * (W - 20)}{end:>10.3f}")
|
|
529
|
+
self.io.writeln('')
|
|
530
|
+
|
|
531
|
+
# ── #18: UNDO with preview ────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
def cmd_undo_preview(self) -> None:
|
|
534
|
+
"""UNDO — show what will change and restore."""
|
|
535
|
+
if not self._undo_stack:
|
|
536
|
+
self.io.writeln("?NOTHING TO UNDO")
|
|
537
|
+
return
|
|
538
|
+
prev = self._undo_stack[-1]
|
|
539
|
+
# Show diff
|
|
540
|
+
added = set(self.program.keys()) - set(prev.keys())
|
|
541
|
+
removed = set(prev.keys()) - set(self.program.keys())
|
|
542
|
+
changed = {k for k in set(self.program.keys()) & set(prev.keys())
|
|
543
|
+
if self.program[k] != prev[k]}
|
|
544
|
+
if added:
|
|
545
|
+
self.io.writeln(f" Undo will remove: {sorted(added)}")
|
|
546
|
+
if removed:
|
|
547
|
+
self.io.writeln(f" Undo will restore: {sorted(removed)}")
|
|
548
|
+
if changed:
|
|
549
|
+
for k in sorted(changed):
|
|
550
|
+
self.io.writeln(f" Undo will revert {k}: {self.program[k]} -> {prev[k]}")
|
|
551
|
+
# Apply
|
|
552
|
+
self.program = self._undo_stack.pop()
|
|
553
|
+
self._parsed.clear()
|
|
554
|
+
self.io.writeln(f"UNDO ({len(self.program)} lines)")
|
|
555
|
+
|
|
556
|
+
# ── #19: THEME ────────────────────────────────────────────────────
|
|
557
|
+
|
|
558
|
+
def cmd_theme(self, rest: str) -> None:
|
|
559
|
+
"""THEME [name] — switch color theme (default, retro, none)."""
|
|
560
|
+
name = rest.strip().lower() if rest.strip() else ''
|
|
561
|
+
if not name:
|
|
562
|
+
self.io.writeln(f" Current theme: {self._theme_name}")
|
|
563
|
+
self.io.writeln(f" Available: {', '.join(THEMES.keys())}")
|
|
564
|
+
return
|
|
565
|
+
if name not in THEMES:
|
|
566
|
+
self.io.writeln(f"?Unknown theme: {name}. Available: {', '.join(THEMES.keys())}")
|
|
567
|
+
return
|
|
568
|
+
self._theme = THEMES[name]
|
|
569
|
+
self._theme_name = name
|
|
570
|
+
self.io.writeln(f"THEME = {name}")
|
|
571
|
+
|
|
572
|
+
# ── #22: EXPLAIN ──────────────────────────────────────────────────
|
|
573
|
+
|
|
574
|
+
def cmd_explain(self, rest: str = '') -> None:
|
|
575
|
+
"""EXPLAIN — describe each program line in plain English."""
|
|
576
|
+
if not self.program:
|
|
577
|
+
self.io.writeln("EMPTY PROGRAM")
|
|
578
|
+
return
|
|
579
|
+
from qubasic_core.gates import GATE_TABLE, GATE_ALIASES
|
|
580
|
+
gate_desc = {
|
|
581
|
+
'H': 'put qubit {q} in superposition',
|
|
582
|
+
'X': 'flip qubit {q} (NOT)',
|
|
583
|
+
'Y': 'Pauli-Y on qubit {q}',
|
|
584
|
+
'Z': 'flip phase of qubit {q}',
|
|
585
|
+
'S': 'S-phase on qubit {q}',
|
|
586
|
+
'T': 'T-phase on qubit {q}',
|
|
587
|
+
'SDG': 'S-dagger on qubit {q}',
|
|
588
|
+
'TDG': 'T-dagger on qubit {q}',
|
|
589
|
+
'SX': 'sqrt(X) on qubit {q}',
|
|
590
|
+
'ID': 'identity on qubit {q}',
|
|
591
|
+
'CX': 'CNOT: flip {q1} if {q0}=|1>',
|
|
592
|
+
'CZ': 'CZ: phase flip if both |1>',
|
|
593
|
+
'CY': 'controlled-Y',
|
|
594
|
+
'CH': 'controlled-H',
|
|
595
|
+
'CCX': 'Toffoli: flip {q2} if {q0},{q1}=|1>',
|
|
596
|
+
'CSWAP': 'Fredkin: swap if control=|1>',
|
|
597
|
+
'SWAP': 'swap qubits {q0} and {q1}',
|
|
598
|
+
'RX': 'rotate {q} around X by {p}',
|
|
599
|
+
'RY': 'rotate {q} around Y by {p}',
|
|
600
|
+
'RZ': 'rotate {q} around Z by {p}',
|
|
601
|
+
'P': 'phase({p}) on qubit {q}',
|
|
602
|
+
'CP': 'controlled-phase({p})',
|
|
603
|
+
'CRX': 'controlled-RX({p})',
|
|
604
|
+
'CRY': 'controlled-RY({p})',
|
|
605
|
+
'CRZ': 'controlled-RZ({p})',
|
|
606
|
+
'RXX': 'XX interaction({p})',
|
|
607
|
+
'RYY': 'YY interaction({p})',
|
|
608
|
+
'RZZ': 'ZZ interaction({p})',
|
|
609
|
+
'U': 'general unitary U({p})',
|
|
610
|
+
'DCX': 'double-CNOT',
|
|
611
|
+
'ISWAP': 'iSWAP',
|
|
612
|
+
}
|
|
613
|
+
flow_desc = {
|
|
614
|
+
'FOR': 'begin loop', 'NEXT': 'end of loop',
|
|
615
|
+
'WHILE': 'loop while condition holds', 'WEND': 'end of WHILE loop',
|
|
616
|
+
'IF': 'conditional branch', 'GOTO': 'jump to line',
|
|
617
|
+
'GOSUB': 'call subroutine', 'RETURN': 'return from subroutine',
|
|
618
|
+
'END': 'stop execution', 'MEASURE': 'measure all qubits',
|
|
619
|
+
'BARRIER': 'optimization barrier', 'PRINT': 'output text or value',
|
|
620
|
+
'LET': 'set variable', 'DIM': 'create array',
|
|
621
|
+
'DO': 'begin DO loop', 'LOOP': 'end of DO loop',
|
|
622
|
+
'EXIT': 'exit loop early', 'SELECT': 'begin SELECT CASE',
|
|
623
|
+
'CASE': 'match case', 'CALL': 'call subroutine',
|
|
624
|
+
'SUB': 'define subroutine', 'FUNCTION': 'define function',
|
|
625
|
+
'DATA': 'inline data', 'READ': 'read from DATA',
|
|
626
|
+
'RESTORE': 'reset DATA pointer',
|
|
627
|
+
}
|
|
628
|
+
for num in sorted(self.program.keys()):
|
|
629
|
+
raw = self.program[num].strip()
|
|
630
|
+
upper = raw.upper()
|
|
631
|
+
if upper.startswith('REM') or upper.startswith("'"):
|
|
632
|
+
self.io.writeln(f" {num:5d} (comment)")
|
|
633
|
+
continue
|
|
634
|
+
# Split on colons and explain each part
|
|
635
|
+
parts = [s.strip() for s in raw.split(':') if s.strip()]
|
|
636
|
+
descs = []
|
|
637
|
+
for part in parts:
|
|
638
|
+
word = part.split()[0].upper() if part.split() else ''
|
|
639
|
+
canonical = GATE_ALIASES.get(word, word)
|
|
640
|
+
if canonical in gate_desc:
|
|
641
|
+
info = GATE_TABLE.get(canonical)
|
|
642
|
+
args = part.split(None, 1)[1].strip() if ' ' in part else ''
|
|
643
|
+
arg_list = [a.strip() for a in args.split(',')]
|
|
644
|
+
n_params = info[0] if info else 0
|
|
645
|
+
params = arg_list[:n_params]
|
|
646
|
+
qubits = arg_list[n_params:]
|
|
647
|
+
fmt = gate_desc[canonical]
|
|
648
|
+
fmt = fmt.replace('{q}', ', '.join(qubits) if qubits else '?')
|
|
649
|
+
fmt = fmt.replace('{p}', ', '.join(params) if params else '?')
|
|
650
|
+
for i, q in enumerate(qubits):
|
|
651
|
+
fmt = fmt.replace(f'{{q{i}}}', q)
|
|
652
|
+
descs.append(fmt)
|
|
653
|
+
elif word in flow_desc:
|
|
654
|
+
descs.append(flow_desc[word])
|
|
655
|
+
elif word.startswith('@'):
|
|
656
|
+
reg = word[1:]
|
|
657
|
+
inner = part.split(None, 1)[1].strip() if ' ' in part else ''
|
|
658
|
+
descs.append(f"@{reg}: {inner}")
|
|
659
|
+
elif word == 'SEND':
|
|
660
|
+
descs.append("measure and send classical bit")
|
|
661
|
+
elif word == 'SHARE':
|
|
662
|
+
descs.append("create shared entanglement")
|
|
663
|
+
else:
|
|
664
|
+
descs.append(part)
|
|
665
|
+
self.io.writeln(f" {num:5d} {'; '.join(descs)}")
|
|
666
|
+
|
|
667
|
+
# ── #24: CLIP ─────────────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
def cmd_clip(self, rest: str = '') -> None:
|
|
670
|
+
"""CLIP — copy last results to clipboard (if available)."""
|
|
671
|
+
lines = []
|
|
672
|
+
if self.last_counts:
|
|
673
|
+
total = sum(self.last_counts.values())
|
|
674
|
+
for s, c in sorted(self.last_counts.items(), key=lambda x: -x[1]):
|
|
675
|
+
lines.append(f"|{s}> {c}/{total} ({100*c/total:.1f}%)")
|
|
676
|
+
elif self.last_sv is not None:
|
|
677
|
+
n = self._active_nqubits
|
|
678
|
+
sv = np.ascontiguousarray(self.last_sv).ravel()
|
|
679
|
+
for i, a in enumerate(sv):
|
|
680
|
+
if abs(a) > 1e-8:
|
|
681
|
+
lines.append(f"|{format(i, f'0{n}b')}> {a.real:+.4f}{a.imag:+.4f}j")
|
|
682
|
+
else:
|
|
683
|
+
self.io.writeln("?NO RESULTS TO COPY")
|
|
684
|
+
return
|
|
685
|
+
text = '\n'.join(lines)
|
|
686
|
+
try:
|
|
687
|
+
if sys.platform == 'win32':
|
|
688
|
+
import subprocess
|
|
689
|
+
subprocess.run(['clip'], input=text.encode(), check=True)
|
|
690
|
+
elif sys.platform == 'darwin':
|
|
691
|
+
import subprocess
|
|
692
|
+
subprocess.run(['pbcopy'], input=text.encode(), check=True)
|
|
693
|
+
else:
|
|
694
|
+
import subprocess
|
|
695
|
+
subprocess.run(['xclip', '-selection', 'clipboard'],
|
|
696
|
+
input=text.encode(), check=True, timeout=2)
|
|
697
|
+
self.io.writeln(f"COPIED {len(lines)} lines to clipboard")
|
|
698
|
+
except Exception:
|
|
699
|
+
self.io.writeln("?Clipboard not available. Output:")
|
|
700
|
+
for line in lines[:20]:
|
|
701
|
+
self.io.writeln(f" {line}")
|
|
702
|
+
if len(lines) > 20:
|
|
703
|
+
self.io.writeln(f" ... ({len(lines) - 20} more)")
|
|
@@ -57,6 +57,7 @@ from qubasic_core.program_mgmt import ProgramMgmtMixin
|
|
|
57
57
|
from qubasic_core.profiler import ProfilerMixin
|
|
58
58
|
from qubasic_core.noise_mixin import NoiseMixin
|
|
59
59
|
from qubasic_core.state_display import StateDisplayMixin
|
|
60
|
+
from qubasic_core.qol import QoLMixin, did_you_mean, tip_of_the_day, quantum_spin
|
|
60
61
|
from qubasic_core.errors import QBasicError, QBasicBuildError, QBasicRangeError
|
|
61
62
|
from qubasic_core.io_protocol import StdIOPort
|
|
62
63
|
from qubasic_core.parser import parse_stmt
|
|
@@ -126,7 +127,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
126
127
|
LOCCMixin, ControlFlowMixin, FileIOMixin, AnalysisMixin,
|
|
127
128
|
SweepMixin, MemoryMixin, StringMixin, ScreenMixin, ClassicMixin,
|
|
128
129
|
SubroutineMixin, DebugMixin, ProgramMgmtMixin, ProfilerMixin,
|
|
129
|
-
NoiseMixin, StateDisplayMixin):
|
|
130
|
+
NoiseMixin, StateDisplayMixin, QoLMixin):
|
|
130
131
|
# Architecture: QBasicTerminal composes Engine (state) + 16 mixins (behavior).
|
|
131
132
|
#
|
|
132
133
|
# Mixin map (each provides specific methods; see TerminalProtocol for contract):
|
|
@@ -198,6 +199,10 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
198
199
|
# Reject absolute paths (Unix / or Windows drive letters)
|
|
199
200
|
if os.path.isabs(path):
|
|
200
201
|
raise ValueError("Absolute paths are not allowed")
|
|
202
|
+
# Catch Windows drive letters on non-Windows hosts (e.g. CI on Linux)
|
|
203
|
+
import re as _re
|
|
204
|
+
if _re.match(r'^[A-Za-z]:[/\\]', path):
|
|
205
|
+
raise ValueError("Absolute paths are not allowed")
|
|
201
206
|
# Reject UNC paths (\\server\share) and extended-length paths (\\?\)
|
|
202
207
|
if path.startswith('\\\\'):
|
|
203
208
|
raise ValueError("UNC/extended paths are not allowed")
|
|
@@ -215,6 +220,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
215
220
|
self._init_program_mgmt()
|
|
216
221
|
self._init_profiler()
|
|
217
222
|
self._init_file_handles()
|
|
223
|
+
self._init_qol()
|
|
218
224
|
|
|
219
225
|
# ── Backend factory ─────────────────────────────────────────────
|
|
220
226
|
|
|
@@ -340,10 +346,27 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
340
346
|
matches += [s for s in self.subroutines if s.startswith(t)]
|
|
341
347
|
matches += [v for v in self.variables if v.upper().startswith(t)]
|
|
342
348
|
matches += [r for r in self.registers if r.upper().startswith(t)]
|
|
349
|
+
# File path completion for SAVE/LOAD/INCLUDE
|
|
350
|
+
try:
|
|
351
|
+
line_buf = readline.get_line_buffer()
|
|
352
|
+
first = line_buf.split()[0].upper() if line_buf.split() else ''
|
|
353
|
+
if first in ('SAVE', 'LOAD', 'INCLUDE', 'IMPORT', 'CHAIN', 'MERGE'):
|
|
354
|
+
import glob as _glob
|
|
355
|
+
pattern = text + '*.qb' if not text.endswith('.qb') else text + '*'
|
|
356
|
+
matches = _glob.glob(pattern)
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
343
359
|
return matches[state] + ' ' if state < len(matches) else None
|
|
344
360
|
readline.set_completer(completer)
|
|
345
361
|
readline.parse_and_bind('tab: complete')
|
|
346
362
|
readline.set_completer_delims(' \t\n')
|
|
363
|
+
# Bind F1-F3 to load demos (terminal permitting)
|
|
364
|
+
try:
|
|
365
|
+
readline.parse_and_bind('"\\eOP": "DEMO BELL\\n"')
|
|
366
|
+
readline.parse_and_bind('"\\eOQ": "DEMO GHZ\\n"')
|
|
367
|
+
readline.parse_and_bind('"\\eOR": "DEMO GROVER\\n"')
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
347
370
|
except ImportError:
|
|
348
371
|
# On Windows, readline is not bundled; try pyreadline3 as fallback
|
|
349
372
|
if sys.platform == 'win32':
|
|
@@ -364,10 +387,12 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
364
387
|
except Exception:
|
|
365
388
|
pass
|
|
366
389
|
self.print_banner()
|
|
390
|
+
self.io.writeln(f" Tip: {tip_of_the_day()}")
|
|
367
391
|
self._setup_readline()
|
|
368
392
|
while True:
|
|
369
393
|
try:
|
|
370
|
-
|
|
394
|
+
prompt = self._status_prompt() if self._prompt == '] ' else self._prompt
|
|
395
|
+
line = self.io.read_line(prompt).strip()
|
|
371
396
|
if line:
|
|
372
397
|
self.process(line)
|
|
373
398
|
except KeyboardInterrupt:
|
|
@@ -461,6 +486,11 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
461
486
|
'CIRCUIT_DEF': 'cmd_circuit_def', 'APPLY_CIRCUIT': 'cmd_apply_circuit',
|
|
462
487
|
'HELP': 'cmd_help', 'CONSISTENCY': 'cmd_consistency',
|
|
463
488
|
'SEED': 'cmd_seed',
|
|
489
|
+
# QoL features
|
|
490
|
+
'COMPARE': 'cmd_compare', 'HEATMAP': 'cmd_heatmap',
|
|
491
|
+
'ANIMATE': 'cmd_animate', 'QUIZ': 'cmd_quiz',
|
|
492
|
+
'DIFF': 'cmd_diff', 'PLOT': 'cmd_plot', 'THEME': 'cmd_theme',
|
|
493
|
+
'CLIP': 'cmd_clip', 'EXPLAIN': 'cmd_explain', 'DRAW': 'cmd_draw',
|
|
464
494
|
}
|
|
465
495
|
|
|
466
496
|
def dispatch(self, line: str) -> None:
|
|
@@ -496,6 +526,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
496
526
|
self.run_immediate(line)
|
|
497
527
|
except Exception as e:
|
|
498
528
|
self.io.writeln(f"?SYNTAX ERROR: {e}")
|
|
529
|
+
self._suggest_command(cmd)
|
|
499
530
|
|
|
500
531
|
def _quit(self) -> None:
|
|
501
532
|
"""Exit the REPL by raising EOFError."""
|
|
@@ -607,8 +638,9 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
607
638
|
if not self.program:
|
|
608
639
|
self.io.writeln("EMPTY PROGRAM")
|
|
609
640
|
return
|
|
610
|
-
|
|
611
|
-
|
|
641
|
+
if self._theme_name != 'none' and sys.stdout.isatty():
|
|
642
|
+
return self.cmd_list_colored()
|
|
643
|
+
for num in sorted(self.program.keys()):
|
|
612
644
|
self.io.writeln(f" {num:5d} {self.program[num]}")
|
|
613
645
|
|
|
614
646
|
def cmd_new(self, *, silent: bool = False) -> None:
|
|
@@ -1170,16 +1202,22 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1170
1202
|
_noise_tag = f"noise=depol({m['noise_depol_p']})" if m['noise_depol_p'] > 0 else "noise=on"
|
|
1171
1203
|
_meta_parts.append(_noise_tag)
|
|
1172
1204
|
_meta = ', '.join(_meta_parts)
|
|
1205
|
+
_throughput = f", {m['gates']/m['time_s']:.0f} gates/s" if m['time_s'] > 0.001 and m['gates'] > 0 else ""
|
|
1206
|
+
_complexity = self._circuit_complexity()
|
|
1207
|
+
_cx_str = f", {_complexity}" if _complexity else ""
|
|
1173
1208
|
self.io.writeln(f"\nRAN {len(self.program)} lines, {self.num_qubits} qubits, "
|
|
1174
1209
|
f"{self.shots} shots in {m['time_s']:.2f}s "
|
|
1175
|
-
f"[depth={m['depth']}, gates={m['gates']}, {_meta}]")
|
|
1210
|
+
f"[depth={m['depth']}, gates={m['gates']}{_throughput}, {_meta}{_cx_str}]")
|
|
1176
1211
|
if method in ('unitary', 'superop'):
|
|
1177
1212
|
pass # matrix already displayed above
|
|
1178
1213
|
elif has_measure and self.last_counts:
|
|
1179
1214
|
self.print_histogram(self.last_counts)
|
|
1180
1215
|
self._auto_display()
|
|
1181
1216
|
else:
|
|
1182
|
-
self.io.writeln("(no MEASURE in program
|
|
1217
|
+
self.io.writeln("(no MEASURE in program \u2014 use STATE or PROBS to inspect)")
|
|
1218
|
+
# Sound on completion for long runs (#25)
|
|
1219
|
+
if m['time_s'] > 2.0 and sys.stdout.isatty():
|
|
1220
|
+
self.io.write('\a')
|
|
1183
1221
|
|
|
1184
1222
|
def cmd_sample(self, rest: str = '') -> None:
|
|
1185
1223
|
"""SAMPLE [shots] — sample the current circuit using SamplerV2 primitive."""
|
|
@@ -1267,8 +1305,8 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1267
1305
|
max_iterations=self._max_iterations, qc=qc,
|
|
1268
1306
|
)
|
|
1269
1307
|
|
|
1270
|
-
self.io.writeln(f"STEP MODE
|
|
1271
|
-
self.io.writeln("Press ENTER to advance, Q to quit\n")
|
|
1308
|
+
self.io.writeln(f"STEP MODE \u2014 {len(sorted_lines)} lines, {self.num_qubits} qubits")
|
|
1309
|
+
self.io.writeln("Press ENTER to advance, A for auto-play, Q to quit\n")
|
|
1272
1310
|
|
|
1273
1311
|
while ctx.ip < len(sorted_lines):
|
|
1274
1312
|
ctx.iteration_count += 1
|
|
@@ -1295,21 +1333,35 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1295
1333
|
except Exception:
|
|
1296
1334
|
self.io.writeln(" (state unavailable)")
|
|
1297
1335
|
|
|
1298
|
-
# Wait for input
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1336
|
+
# Wait for input (or auto-advance)
|
|
1337
|
+
if not getattr(self, '_step_auto', False):
|
|
1338
|
+
try:
|
|
1339
|
+
user = self.io.read_line(" [ENTER/A/Q] ").strip().upper()
|
|
1340
|
+
if user == 'Q':
|
|
1341
|
+
self.io.writeln("STOPPED")
|
|
1342
|
+
return
|
|
1343
|
+
if user == 'A' or user.startswith('A'):
|
|
1344
|
+
self._step_auto = True
|
|
1345
|
+
delay = 0.5
|
|
1346
|
+
if len(user) > 1:
|
|
1347
|
+
try:
|
|
1348
|
+
delay = float(user[1:]) / 1000
|
|
1349
|
+
except ValueError:
|
|
1350
|
+
pass
|
|
1351
|
+
self._step_delay = delay
|
|
1352
|
+
except (KeyboardInterrupt, EOFError):
|
|
1353
|
+
self.io.writeln("\nSTOPPED")
|
|
1303
1354
|
return
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1355
|
+
if getattr(self, '_step_auto', False):
|
|
1356
|
+
import time as _time
|
|
1357
|
+
_time.sleep(getattr(self, '_step_delay', 0.5))
|
|
1307
1358
|
|
|
1308
1359
|
if isinstance(result, int):
|
|
1309
1360
|
ctx.ip = result
|
|
1310
1361
|
else:
|
|
1311
1362
|
ctx.ip += 1
|
|
1312
1363
|
|
|
1364
|
+
self._step_auto = False
|
|
1313
1365
|
self.io.writeln("DONE")
|
|
1314
1366
|
|
|
1315
1367
|
# run_immediate provided by ExecutorMixin.
|
|
@@ -1996,12 +2048,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1996
2048
|
# cmd_clear provided by ProgramMgmtMixin.
|
|
1997
2049
|
|
|
1998
2050
|
def cmd_undo(self) -> None:
|
|
1999
|
-
|
|
2000
|
-
self.io.writeln("NOTHING TO UNDO")
|
|
2001
|
-
return
|
|
2002
|
-
self.program = self._undo_stack.pop()
|
|
2003
|
-
self._parsed = {num: parse_stmt(s) for num, s in self.program.items()}
|
|
2004
|
-
self.io.writeln(f"UNDO ({len(self.program)} lines)")
|
|
2051
|
+
return self.cmd_undo_preview()
|
|
2005
2052
|
|
|
2006
2053
|
# LOCC execution (_locc_run, _locc_exec_line, etc.) provided by LOCCMixin.
|
|
2007
2054
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|