qubasic 0.6.3__tar.gz → 0.6.5__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.
Files changed (61) hide show
  1. {qubasic-0.6.3 → qubasic-0.6.5}/CHANGELOG.md +14 -0
  2. {qubasic-0.6.3/qubasic.egg-info → qubasic-0.6.5}/PKG-INFO +2 -2
  3. {qubasic-0.6.3 → qubasic-0.6.5}/README.md +1 -1
  4. {qubasic-0.6.3 → qubasic-0.6.5}/pyproject.toml +1 -1
  5. {qubasic-0.6.3 → qubasic-0.6.5/qubasic.egg-info}/PKG-INFO +2 -2
  6. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/__init__.py +1 -1
  7. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/cli.py +10 -11
  8. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/control_flow.py +49 -1
  9. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/display.py +5 -3
  10. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/executor.py +44 -1
  11. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/file_io.py +0 -2
  12. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/locc_engine.py +19 -4
  13. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/locc_execution.py +1 -2
  14. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/memory.py +6 -4
  15. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/terminal.py +12 -1
  16. {qubasic-0.6.3 → qubasic-0.6.5}/tests/test_features.py +25 -0
  17. {qubasic-0.6.3 → qubasic-0.6.5}/LICENSE +0 -0
  18. {qubasic-0.6.3 → qubasic-0.6.5}/MANIFEST.in +0 -0
  19. {qubasic-0.6.3 → qubasic-0.6.5}/examples/bell.qb +0 -0
  20. {qubasic-0.6.3 → qubasic-0.6.5}/examples/grover3.qb +0 -0
  21. {qubasic-0.6.3 → qubasic-0.6.5}/examples/locc_teleport.qb +0 -0
  22. {qubasic-0.6.3 → qubasic-0.6.5}/examples/sweep_rx.qb +0 -0
  23. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic.egg-info/SOURCES.txt +0 -0
  24. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic.egg-info/dependency_links.txt +0 -0
  25. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic.egg-info/entry_points.txt +0 -0
  26. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic.egg-info/requires.txt +0 -0
  27. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic.egg-info/top_level.txt +0 -0
  28. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/__main__.py +0 -0
  29. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/analysis.py +0 -0
  30. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/backend.py +0 -0
  31. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/classic.py +0 -0
  32. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/debug.py +0 -0
  33. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/demos.py +0 -0
  34. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/engine.py +0 -0
  35. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/engine_state.py +0 -0
  36. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/errors.py +0 -0
  37. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/exec_context.py +0 -0
  38. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/expression.py +0 -0
  39. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/gates.py +0 -0
  40. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/help_text.py +0 -0
  41. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/io_protocol.py +0 -0
  42. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/locc.py +0 -0
  43. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/locc_commands.py +0 -0
  44. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/locc_display.py +0 -0
  45. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/mock_backend.py +0 -0
  46. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/noise_mixin.py +0 -0
  47. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/parser.py +0 -0
  48. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/patterns.py +0 -0
  49. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/profiler.py +0 -0
  50. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/program_mgmt.py +0 -0
  51. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/protocol.py +0 -0
  52. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/qol.py +0 -0
  53. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/scope.py +0 -0
  54. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/screen.py +0 -0
  55. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/state_display.py +0 -0
  56. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/statements.py +0 -0
  57. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/strings.py +0 -0
  58. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/subs.py +0 -0
  59. {qubasic-0.6.3 → qubasic-0.6.5}/qubasic_core/sweep.py +0 -0
  60. {qubasic-0.6.3 → qubasic-0.6.5}/setup.cfg +0 -0
  61. {qubasic-0.6.3 → qubasic-0.6.5}/tests/test_qubasic.py +0 -0
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.5 (2026-06-19)
4
+
5
+ ### Fixed
6
+ - The Bloch sphere Y component had an inverted sign, so a qubit in |+i> was reported at -Y (and |-i> at +Y). `BLOCH`, `DRAW`, `PRINT QUBIT(n)`, and the memory-mapped Bloch-Y field (`$0100 + q*8 + 2`) were all affected, and `BLOCH` mislabeled |+i> as "|-i>". X and Z were already correct. Y now matches the standard convention (`<Y> = +2 Im(<1|rho|0>)`), verified against Qiskit expectation values.
7
+
8
+ ## 0.6.4 (2026-06-19)
9
+
10
+ ### Fixed
11
+ - Immediate-mode `PRINT` at the REPL no longer prints a spurious statevector after its output. `PRINT` is not a dispatch-table command, so it fell through to the immediate gate path, which always dumped `|psi>`. That path now skips the dump when the typed line added no circuit operations, so gate and subroutine entries still show state while `PRINT`, `MEASURE`, and other classical statements do not.
12
+ - `--quiet` now prints results with the banner suppressed, as documented. The results branch was unreachable (guarded by `elif not quiet` inside an `if quiet or json_mode` block), so `--quiet` previously produced no output at all.
13
+ - A `MEASURE` reachable only inside a `DEF`/`SUB` body, an `IF ... THEN/ELSE` clause, or a colon compound is now detected, so the program takes the shots path (and `qubasic script.qb` auto-runs) instead of the no-measure statevector path.
14
+ - `LET m(i, j) = x` writes a multi-dimensional array element using the same flat-stride convention the expression-side accessor reads, so multi-dimensional reads and writes agree.
15
+ - Immediate-mode `LET a(i) = <expr>` at the REPL assigns the array element, matching the in-program `LET` (previously only the in-program form handled array targets).
16
+
3
17
  ## 0.6.3 (2026-06-18)
4
18
 
5
19
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -661,7 +661,7 @@ QBASIC detects available RAM, estimates per-instance memory, and reports maximum
661
661
  ### Simulation method selection
662
662
  - **automatic**: stabilizer for Clifford-only circuits, MPS for >28 qubits, statevector otherwise
663
663
  - **stabilizer**: polynomial-time for Clifford circuits (H, S, CX, SWAP, etc.)
664
- - **matrix_product_state**: scales to 50+ qubits for low-entanglement circuits
664
+ - **matrix_product_state**: memory-efficient for low-entanglement circuits; handles the full 32-qubit range that would exhaust statevector
665
665
  - **extended_stabilizer**: approximate simulation for near-Clifford circuits
666
666
  - **statevector**: exact, limited by RAM (~28 qubits on 16GB)
667
667
  - **density_matrix**: includes mixed states, ~14 qubits on 16GB
@@ -628,7 +628,7 @@ QBASIC detects available RAM, estimates per-instance memory, and reports maximum
628
628
  ### Simulation method selection
629
629
  - **automatic**: stabilizer for Clifford-only circuits, MPS for >28 qubits, statevector otherwise
630
630
  - **stabilizer**: polynomial-time for Clifford circuits (H, S, CX, SWAP, etc.)
631
- - **matrix_product_state**: scales to 50+ qubits for low-entanglement circuits
631
+ - **matrix_product_state**: memory-efficient for low-entanglement circuits; handles the full 32-qubit range that would exhaust statevector
632
632
  - **extended_stabilizer**: approximate simulation for near-Clifford circuits
633
633
  - **statevector**: exact, limited by RAM (~28 qubits on 16GB)
634
634
  - **density_matrix**: includes mixed states, ~14 qubits on 16GB
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.6.3"
7
+ version = "0.6.5"
8
8
  description = "Quantum BASIC Interactive Terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -661,7 +661,7 @@ QBASIC detects available RAM, estimates per-instance memory, and reports maximum
661
661
  ### Simulation method selection
662
662
  - **automatic**: stabilizer for Clifford-only circuits, MPS for >28 qubits, statevector otherwise
663
663
  - **stabilizer**: polynomial-time for Clifford circuits (H, S, CX, SWAP, etc.)
664
- - **matrix_product_state**: scales to 50+ qubits for low-entanglement circuits
664
+ - **matrix_product_state**: memory-efficient for low-entanglement circuits; handles the full 32-qubit range that would exhaust statevector
665
665
  - **extended_stabilizer**: approximate simulation for near-Clifford circuits
666
666
  - **statevector**: exact, limited by RAM (~28 qubits on 16GB)
667
667
  - **density_matrix**: includes mixed states, ~14 qubits on 16GB
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.6.3'
31
+ __version__ = '0.6.5'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -38,12 +38,8 @@ def run_script(path: str, terminal: 'QBasicTerminal') -> None:
38
38
  ProgramMgmtMixin._load_lines_with_defs(
39
39
  lines, lambda line: terminal.process(line, track_undo=False))
40
40
 
41
- # Auto-run if the program has a MEASURE statement
42
- has_measure = any(
43
- terminal.program.get(ln, '').strip().upper() == 'MEASURE'
44
- for ln in terminal.program
45
- )
46
- if terminal.program and has_measure:
41
+ # Auto-run if the program has a reachable MEASURE (incl. in subs / IF clauses)
42
+ if terminal.program and terminal._program_has_measure(sorted(terminal.program)):
47
43
  terminal.cmd_run()
48
44
 
49
45
 
@@ -125,8 +121,12 @@ def main():
125
121
  print(_json.dumps({'error': err}, indent=2))
126
122
  sys.exit(1)
127
123
  print(_json.dumps(term.result(), indent=2))
128
- elif not quiet:
129
- print(buf.getvalue())
124
+ else:
125
+ # --quiet (no --json): the banner was never emitted into the
126
+ # buffer (only the non-captured path calls print_banner), so the
127
+ # captured text is exactly the results. Print them, matching the
128
+ # documented "suppress banner, output results only" behavior.
129
+ print(buf.getvalue(), end='')
130
130
  if err is not None:
131
131
  print(f"?ERROR: {err}")
132
132
  sys.exit(1)
@@ -134,9 +134,8 @@ def main():
134
134
  term.print_banner()
135
135
  run_script(path, term)
136
136
  # Exit 0 on success; 1 if a measured program produced no counts.
137
- sys.exit(0 if term.last_counts is not None or not any(
138
- term.program.get(ln, '').strip().upper() == 'MEASURE'
139
- for ln in term.program) else 1)
137
+ expects_measure = bool(term.program) and term._program_has_measure(sorted(term.program))
138
+ sys.exit(0 if term.last_counts is not None or not expects_measure else 1)
140
139
  else:
141
140
  term.repl()
142
141
 
@@ -40,12 +40,60 @@ class ControlFlowMixin:
40
40
  # signature for compatibility with _exec_control_flow's argument
41
41
  # passing but is not used by the methods themselves.
42
42
 
43
+ @staticmethod
44
+ def _split_arg_list(s: str) -> list[str]:
45
+ """Split a comma list at top level (outside quotes and brackets)."""
46
+ parts: list[str] = []
47
+ buf = ''
48
+ depth = 0
49
+ quote: str | None = None
50
+ for ch in s:
51
+ if quote:
52
+ buf += ch
53
+ if ch == quote:
54
+ quote = None
55
+ elif ch in ('"', "'"):
56
+ quote = ch
57
+ buf += ch
58
+ elif ch in '([':
59
+ depth += 1
60
+ buf += ch
61
+ elif ch in ')]':
62
+ depth = max(0, depth - 1)
63
+ buf += ch
64
+ elif ch == ',' and depth == 0:
65
+ parts.append(buf)
66
+ buf = ''
67
+ else:
68
+ buf += ch
69
+ if buf.strip():
70
+ parts.append(buf)
71
+ return [p.strip() for p in parts if p.strip()]
72
+
43
73
  def _cf_let_array(self, stmt: str, run_vars: dict[str, Any],
44
74
  parsed: LetArrayStmt) -> tuple[bool, ExecOutcome]:
45
75
  name, idx_expr, val_expr = parsed.name, parsed.index_expr, parsed.value_expr
46
76
  base = getattr(self, '_option_base', 0)
47
- idx = int(self._eval_with_vars(idx_expr, run_vars)) - base
48
77
  val = self._eval_with_vars(val_expr, run_vars)
78
+ parts = self._split_arg_list(idx_expr)
79
+ if len(parts) > 1:
80
+ # Multi-dimensional write: flatten with the same stride convention
81
+ # the expression-side accessor uses, so reads and writes agree.
82
+ dims = getattr(self, '_array_dims', {}).get(name)
83
+ indices = [int(self._eval_with_vars(p, run_vars)) - base for p in parts]
84
+ if any(i < 0 for i in indices):
85
+ raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx_expr})")
86
+ flat, stride = 0, 1
87
+ for k in range(len(indices) - 1, -1, -1):
88
+ flat += indices[k] * stride
89
+ stride *= dims[k] if dims and k < len(dims) else 1
90
+ if name not in self.arrays:
91
+ raise RuntimeError(f"ARRAY NOT DIMENSIONED: {name} (use DIM first)")
92
+ if flat < 0 or flat >= len(self.arrays[name]):
93
+ raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx_expr})")
94
+ self.arrays[name][flat] = val
95
+ return True, ExecResult.ADVANCE
96
+ idx = int(self._eval_with_vars(idx_expr, run_vars)) - base
49
97
  if idx < 0:
50
98
  raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx + base})")
51
99
  if name not in self.arrays:
@@ -292,10 +292,12 @@ class DisplayMixin:
292
292
 
293
293
  rho_00 = np.sum(np.abs(t0)**2)
294
294
  rho_11 = np.sum(np.abs(t1)**2)
295
- rho_01 = np.sum(np.conj(t0) * t1)
295
+ # conj(t0)*t1 sums to rho_10 = <1|rho|0>. <X> = 2 Re(rho_10) (Re is
296
+ # symmetric in rho_01/rho_10), and <Y> = -2 Im(rho_01) = +2 Im(rho_10).
297
+ rho_10 = np.sum(np.conj(t0) * t1)
296
298
 
297
- x = float(2 * rho_01.real)
298
- y = float(-2 * rho_01.imag)
299
+ x = float(2 * rho_10.real)
300
+ y = float(2 * rho_10.imag)
299
301
  z = float(rho_00 - rho_11)
300
302
 
301
303
  return x, y, z
@@ -66,6 +66,41 @@ class ExecutorMixin:
66
66
 
67
67
  # ── Circuit Building ──────────────────────────────────────────────
68
68
 
69
+ def _program_has_measure(self, sorted_lines) -> bool:
70
+ """True if any reachable statement measures.
71
+
72
+ The plain per-line scan misses a MEASURE that only appears inside a DEF
73
+ subroutine body or an IF/THEN-ELSE clause, which left the run on the
74
+ no-measure path with no counts. This recursion-guarded walk follows
75
+ colon compounds, IF clauses, and subroutine bodies so the shots path
76
+ fires whenever a measurement is actually reachable.
77
+ """
78
+ from qubasic_core.statements import MeasureStmt, CompoundStmt, IfThenStmt
79
+ from qubasic_core.parser import parse_stmt
80
+
81
+ def scan(text: str, seen: frozenset) -> bool:
82
+ p = parse_stmt(text)
83
+ if isinstance(p, MeasureStmt):
84
+ return True
85
+ if isinstance(p, CompoundStmt):
86
+ return any(scan(part, seen) for part in p.parts)
87
+ if isinstance(p, IfThenStmt):
88
+ if p.then_clause and scan(p.then_clause, seen):
89
+ return True
90
+ if p.else_clause and scan(p.else_clause, seen):
91
+ return True
92
+ return False
93
+ words = text.strip().split()
94
+ if words:
95
+ word = words[0].upper().split('(')[0]
96
+ if word in self.subroutines and word not in seen:
97
+ sub = self.subroutines[word]
98
+ body = sub['body'] if isinstance(sub, dict) else sub
99
+ return any(scan(b, seen | {word}) for b in body)
100
+ return False
101
+
102
+ return any(scan(self.program[ln], frozenset()) for ln in sorted_lines)
103
+
69
104
  def build_circuit(self) -> tuple['QuantumCircuit', bool]:
70
105
  """Compile program lines into a QuantumCircuit. Returns (circuit, has_measure)."""
71
106
  from qubasic_core.exec_context import ExecContext
@@ -92,7 +127,9 @@ class ExecutorMixin:
92
127
  qc=qc,
93
128
  backend=backend,
94
129
  )
95
- has_measure = False
130
+ # Reachability scan catches MEASURE inside subroutines and IF clauses,
131
+ # not just literal top-level MEASURE lines.
132
+ has_measure = self._program_has_measure(ctx.sorted_lines)
96
133
  self._on_measure_fired = False
97
134
 
98
135
  while ctx.ip < len(ctx.sorted_lines):
@@ -685,6 +722,12 @@ class ExecutorMixin:
685
722
  imm_ctx = ExecContext(sorted_lines=[0], ip=0,
686
723
  run_vars=dict(self.variables), qc=qc)
687
724
  self._exec_line(line, ctx=imm_ctx)
725
+ # A classical statement typed at the prompt (PRINT, MEASURE, an IF whose
726
+ # clause printed, ...) adds no circuit operations and has already emitted
727
+ # its own output via _exec_line. Simulating an empty circuit just to print
728
+ # an unchanged |psi> would be spurious, so skip the statevector dump.
729
+ if qc.size() == 0:
730
+ return
688
731
  qc.save_statevector()
689
732
  backend = self._make_backend('statevector')
690
733
  result = backend.run(transpile(qc, backend)).result()
@@ -195,8 +195,6 @@ class FileIOMixin:
195
195
  DEFs in the imported file are prefixed with the module name.
196
196
  E.g., IMPORT "math.qb" makes DEF ROT available as MATH.ROT.
197
197
  """
198
- from qubasic_core.engine import RE_IMPORT
199
- m = RE_IMPORT.match(f"IMPORT {rest}") if not rest.startswith('IMPORT') else RE_IMPORT.match(rest)
200
198
  path = rest.strip().strip('"').strip("'")
201
199
  if not path:
202
200
  self.io.writeln('?USAGE: IMPORT "filename"')
@@ -180,7 +180,13 @@ class LOCCEngine:
180
180
  self._renormalize(reg)
181
181
 
182
182
  def apply(self, reg: str, gate_name: str, params: tuple[float, ...], qubits: list[int]) -> None:
183
- """Apply a gate to a specific register, then apply noise if configured."""
183
+ """Apply a gate to a register, then apply noise if configured.
184
+
185
+ Qubit indices are register-LOCAL (0..size-1); the register offset into
186
+ the joint statevector is added internally. Callers that index the raw
187
+ statevector (e.g. for fidelity/inspection) must convert with
188
+ ``offsets[reg] + local`` themselves.
189
+ """
184
190
  self._check_qubits(reg, qubits)
185
191
  matrix = _np_gate_matrix(gate_name, tuple(params))
186
192
  if self.joint:
@@ -194,7 +200,10 @@ class LOCCEngine:
194
200
  self._contiguate(reg)
195
201
 
196
202
  def share(self, reg1: str, q1: int, reg2: str, q2: int) -> None:
197
- """Create Bell pair |Phi+> between reg1[q1] and reg2[q2]. JOINT only."""
203
+ """Create Bell pair |Phi+> between reg1[q1] and reg2[q2]. JOINT only.
204
+
205
+ ``q1``/``q2`` are register-LOCAL indices; offsets are added internally.
206
+ """
198
207
  if not self.joint:
199
208
  raise RuntimeError("SHARE requires LOCC JOINT mode")
200
209
  self._check_qubits(reg1, [q1])
@@ -210,7 +219,10 @@ class LOCCEngine:
210
219
  self._apply_noise(reg2, [q2])
211
220
 
212
221
  def send(self, reg: str, qubit: int) -> int:
213
- """Measure a qubit (Born rule) and return the classical outcome."""
222
+ """Measure a qubit (Born rule) and return the classical outcome.
223
+
224
+ ``qubit`` is register-LOCAL (0..size-1); the offset is added internally.
225
+ """
214
226
  self._check_qubits(reg, [qubit])
215
227
  if self.joint:
216
228
  actual = qubit + self.offsets[self._idx(reg)]
@@ -266,7 +278,10 @@ class LOCCEngine:
266
278
  return per_reg, {}
267
279
 
268
280
  def apply_matrix(self, reg: str, matrix: np.ndarray, qubits: list[int]) -> None:
269
- """Apply a raw unitary matrix to qubits in a register."""
281
+ """Apply a raw unitary matrix to qubits in a register.
282
+
283
+ ``qubits`` are register-LOCAL indices; offsets are added internally.
284
+ """
270
285
  self._check_qubits(reg, qubits)
271
286
  if self.joint:
272
287
  idx = self._idx(reg)
@@ -33,8 +33,7 @@ class LOCCExecutionMixin:
33
33
  if not sorted_lines:
34
34
  self.io.writeln("NOTHING TO RUN")
35
35
  return
36
- has_measure = any(self.program[l].strip().upper() == 'MEASURE'
37
- for l in sorted_lines)
36
+ has_measure = self._program_has_measure(sorted_lines)
38
37
  has_send = any(re.search(r'\bSEND\b', self.program[l], re.IGNORECASE)
39
38
  for l in sorted_lines)
40
39
  if has_send:
@@ -112,17 +112,19 @@ class MemoryMixin:
112
112
  t0, t1 = t[0].flatten(), t[1].flatten()
113
113
  rho_00 = float(np.sum(np.abs(t0) ** 2))
114
114
  rho_11 = float(np.sum(np.abs(t1) ** 2))
115
- rho_01 = complex(np.sum(np.conj(t0) * t1))
115
+ # conj(t0)*t1 sums to rho_10 = <1|rho|0>. Bloch X = 2 Re(rho_10) and
116
+ # Bloch Y = -2 Im(rho_01) = +2 Im(rho_10).
117
+ rho_10 = complex(np.sum(np.conj(t0) * t1))
116
118
  # Recover amplitudes with alpha as the real phase reference; beta then
117
119
  # carries the relative phase (real and imaginary parts both exposed).
118
120
  alpha = np.sqrt(max(0.0, rho_00))
119
121
  if alpha > 1e-12:
120
- beta = rho_01 / alpha
122
+ beta = rho_10 / alpha
121
123
  else:
122
124
  beta = complex(np.sqrt(max(0.0, rho_11)))
123
125
  return [rho_11,
124
- float(2 * rho_01.real),
125
- float(-2 * rho_01.imag),
126
+ float(2 * rho_10.real),
127
+ float(2 * rho_10.imag),
126
128
  float(rho_00 - rho_11),
127
129
  float(alpha),
128
130
  0.0,
@@ -928,7 +928,18 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
928
928
 
929
929
  def cmd_let(self, rest: str) -> None:
930
930
  """LET <var> = <expr> — assign a computed value to a variable.
931
- Supports record fields, e.g. LET p.x = 3.14 (see TYPE)."""
931
+ Supports record fields (LET p.x = 3.14) and array elements
932
+ (LET a(0) = PI, LET m(i, j) = x), matching the in-program LET."""
933
+ from qubasic_core.patterns import RE_LET_ARRAY
934
+ am = RE_LET_ARRAY.match(f"LET {rest}")
935
+ if am:
936
+ from qubasic_core.statements import LetArrayStmt
937
+ name, idx_expr, val_expr = am.group(1), am.group(2), am.group(3)
938
+ parsed = LetArrayStmt(raw=f"LET {rest}", name=name,
939
+ index_expr=idx_expr, value_expr=val_expr)
940
+ self._cf_let_array(f"LET {rest}", self.variables, parsed)
941
+ self.io.writeln(f"{name}({idx_expr.strip()}) = {self.eval_expr(val_expr)}")
942
+ return
932
943
  m = re.match(r'(\w+(?:\.\w+)?)\s*=\s*(.*)', rest)
933
944
  if not m:
934
945
  self.io.writeln("?USAGE: LET <var> = <expr>")
@@ -2595,6 +2595,31 @@ class TestCommandSurfaceCoverage(unittest.TestCase):
2595
2595
  self.assertIsInstance(t._status_prompt(), str)
2596
2596
  capture(t._suggest_command, 'XYZ')
2597
2597
 
2598
+ def test_bloch_vector_axes(self):
2599
+ """_bloch_vector and the $0100 By field carry the right sign on every
2600
+ axis. Regression: the Y component was inverted (|+i> read as -Y)."""
2601
+ import numpy as np
2602
+ from qubasic_core.terminal import QBasicTerminal
2603
+ t = QBasicTerminal()
2604
+ r2 = 1.0 / np.sqrt(2)
2605
+ cases = [
2606
+ (np.array([1, 0], dtype=complex), (0.0, 0.0, +1.0)), # |0> +Z
2607
+ (np.array([0, 1], dtype=complex), (0.0, 0.0, -1.0)), # |1> -Z
2608
+ (np.array([r2, r2], dtype=complex), (+1.0, 0.0, 0.0)), # |+> +X
2609
+ (np.array([r2, -r2], dtype=complex), (-1.0, 0.0, 0.0)), # |-> -X
2610
+ (np.array([r2, 1j * r2], dtype=complex), (0.0, +1.0, 0.0)), # |+i> +Y
2611
+ (np.array([r2, -1j * r2], dtype=complex), (0.0, -1.0, 0.0)), # |-i> -Y
2612
+ ]
2613
+ for sv, (ex, ey, ez) in cases:
2614
+ x, y, z = t._bloch_vector(sv, 0, 1)
2615
+ self.assertAlmostEqual(x, ex, places=6)
2616
+ self.assertAlmostEqual(y, ey, places=6)
2617
+ self.assertAlmostEqual(z, ez, places=6)
2618
+ # Memory-mapped Bloch Y for q0 ($0102) must agree for |+i>.
2619
+ t.num_qubits = 1
2620
+ t.last_sv = np.array([r2, 1j * r2], dtype=complex)
2621
+ self.assertAlmostEqual(t._peek(0x0102), +1.0, places=6)
2622
+
2598
2623
  def test_program_and_io_surface(self):
2599
2624
  import builtins
2600
2625
  t = self._ready()
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