qubasic 0.7.0__tar.gz → 0.8.0__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 (62) hide show
  1. {qubasic-0.7.0 → qubasic-0.8.0}/CHANGELOG.md +7 -0
  2. {qubasic-0.7.0/qubasic.egg-info → qubasic-0.8.0}/PKG-INFO +12 -3
  3. {qubasic-0.7.0 → qubasic-0.8.0}/README.md +11 -2
  4. {qubasic-0.7.0 → qubasic-0.8.0}/pyproject.toml +1 -1
  5. {qubasic-0.7.0 → qubasic-0.8.0/qubasic.egg-info}/PKG-INFO +12 -3
  6. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/__init__.py +1 -1
  7. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/analysis.py +158 -0
  8. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/executor.py +6 -0
  9. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/parser.py +5 -0
  10. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/qol.py +1 -1
  11. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/statements.py +1 -1
  12. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/terminal.py +13 -2
  13. {qubasic-0.7.0 → qubasic-0.8.0}/tests/test_qubasic.py +41 -0
  14. {qubasic-0.7.0 → qubasic-0.8.0}/LICENSE +0 -0
  15. {qubasic-0.7.0 → qubasic-0.8.0}/MANIFEST.in +0 -0
  16. {qubasic-0.7.0 → qubasic-0.8.0}/examples/bell.qb +0 -0
  17. {qubasic-0.7.0 → qubasic-0.8.0}/examples/grover3.qb +0 -0
  18. {qubasic-0.7.0 → qubasic-0.8.0}/examples/locc_teleport.qb +0 -0
  19. {qubasic-0.7.0 → qubasic-0.8.0}/examples/sweep_rx.qb +0 -0
  20. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic.egg-info/SOURCES.txt +0 -0
  21. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic.egg-info/dependency_links.txt +0 -0
  22. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic.egg-info/entry_points.txt +0 -0
  23. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic.egg-info/requires.txt +0 -0
  24. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic.egg-info/top_level.txt +0 -0
  25. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/__main__.py +0 -0
  26. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/algorithms.py +0 -0
  27. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/backend.py +0 -0
  28. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/classic.py +0 -0
  29. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/cli.py +0 -0
  30. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/control_flow.py +0 -0
  31. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/debug.py +0 -0
  32. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/demos.py +0 -0
  33. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/display.py +0 -0
  34. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/engine.py +0 -0
  35. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/engine_state.py +0 -0
  36. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/errors.py +0 -0
  37. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/exec_context.py +0 -0
  38. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/expression.py +0 -0
  39. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/file_io.py +0 -0
  40. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/gates.py +0 -0
  41. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/help_text.py +0 -0
  42. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/io_protocol.py +0 -0
  43. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/locc.py +0 -0
  44. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/locc_commands.py +0 -0
  45. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/locc_display.py +0 -0
  46. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/locc_engine.py +0 -0
  47. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/locc_execution.py +0 -0
  48. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/memory.py +0 -0
  49. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/mock_backend.py +0 -0
  50. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/noise_mixin.py +0 -0
  51. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/patterns.py +0 -0
  52. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/profiler.py +0 -0
  53. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/program_mgmt.py +0 -0
  54. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/protocol.py +0 -0
  55. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/scope.py +0 -0
  56. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/screen.py +0 -0
  57. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/state_display.py +0 -0
  58. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/strings.py +0 -0
  59. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/subs.py +0 -0
  60. {qubasic-0.7.0 → qubasic-0.8.0}/qubasic_core/sweep.py +0 -0
  61. {qubasic-0.7.0 → qubasic-0.8.0}/setup.cfg +0 -0
  62. {qubasic-0.7.0 → qubasic-0.8.0}/tests/test_features.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 (2026-06-19)
4
+
5
+ ### Added
6
+ - Partial measurement: `MEASURE 0, 2` measures a subset of qubits and reports the histogram over just those qubits (the bare `MEASURE` still measures all).
7
+ - Process tomography: `PTOMOGRAPHY` reconstructs the circuit's Pauli Transfer Matrix (the unitary, or the full noisy channel via the superoperator when a noise model is active), and reports the trace-preserving and unital flags plus the average gate fidelity to the identity. Limited to <= 2 qubits.
8
+ - Randomized benchmarking: `RB [max_length] [samples]` runs single-qubit RB over the 24-element Clifford group (each sequence ends with its recovery Clifford), fits the survival decay `p(m) = A f^m + B`, and reports the decay `f` and the error per Clifford `(1 - f)/2`. Reflects the active noise model.
9
+
3
10
  ## 0.7.0 (2026-06-19)
4
11
 
5
12
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -417,12 +417,21 @@ STATS SHOW Show mean/stddev/min/max per state
417
417
  STATS CLEAR Reset accumulator
418
418
  ```
419
419
 
420
- ### State comparison
420
+ ### Characterization
421
421
  ```
422
422
  FIDELITY |BELL> State fidelity |<target|psi>|^2 vs a named state
423
423
  FIDELITY [0.707,0,0,0.707] ...or an explicit amplitude list
424
- TOMOGRAPHY Reconstruct the density matrix from Pauli expectations
424
+ TOMOGRAPHY State tomography: reconstruct rho from Pauli expectations
425
425
  TOMOGRAPHY 2000 Statistical tomography (2000 shots per Pauli basis)
426
+ PTOMOGRAPHY Process tomography: reconstruct the Pauli Transfer Matrix (<=2 qubits)
427
+ RB Single-qubit randomized benchmarking (fits decay -> error/Clifford)
428
+ RB 64 12 Sequence lengths up to 64, 12 random sequences each
429
+ ```
430
+
431
+ ### Partial measurement
432
+ ```
433
+ MEASURE Measure all qubits (the default)
434
+ MEASURE 0, 2 Measure only qubits 0 and 2; histogram is over that subset
426
435
  ```
427
436
 
428
437
  ## Algorithm primitives
@@ -384,12 +384,21 @@ STATS SHOW Show mean/stddev/min/max per state
384
384
  STATS CLEAR Reset accumulator
385
385
  ```
386
386
 
387
- ### State comparison
387
+ ### Characterization
388
388
  ```
389
389
  FIDELITY |BELL> State fidelity |<target|psi>|^2 vs a named state
390
390
  FIDELITY [0.707,0,0,0.707] ...or an explicit amplitude list
391
- TOMOGRAPHY Reconstruct the density matrix from Pauli expectations
391
+ TOMOGRAPHY State tomography: reconstruct rho from Pauli expectations
392
392
  TOMOGRAPHY 2000 Statistical tomography (2000 shots per Pauli basis)
393
+ PTOMOGRAPHY Process tomography: reconstruct the Pauli Transfer Matrix (<=2 qubits)
394
+ RB Single-qubit randomized benchmarking (fits decay -> error/Clifford)
395
+ RB 64 12 Sequence lengths up to 64, 12 random sequences each
396
+ ```
397
+
398
+ ### Partial measurement
399
+ ```
400
+ MEASURE Measure all qubits (the default)
401
+ MEASURE 0, 2 Measure only qubits 0 and 2; histogram is over that subset
393
402
  ```
394
403
 
395
404
  ## Algorithm primitives
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.7.0"
7
+ version = "0.8.0"
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.7.0
3
+ Version: 0.8.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -417,12 +417,21 @@ STATS SHOW Show mean/stddev/min/max per state
417
417
  STATS CLEAR Reset accumulator
418
418
  ```
419
419
 
420
- ### State comparison
420
+ ### Characterization
421
421
  ```
422
422
  FIDELITY |BELL> State fidelity |<target|psi>|^2 vs a named state
423
423
  FIDELITY [0.707,0,0,0.707] ...or an explicit amplitude list
424
- TOMOGRAPHY Reconstruct the density matrix from Pauli expectations
424
+ TOMOGRAPHY State tomography: reconstruct rho from Pauli expectations
425
425
  TOMOGRAPHY 2000 Statistical tomography (2000 shots per Pauli basis)
426
+ PTOMOGRAPHY Process tomography: reconstruct the Pauli Transfer Matrix (<=2 qubits)
427
+ RB Single-qubit randomized benchmarking (fits decay -> error/Clifford)
428
+ RB 64 12 Sequence lengths up to 64, 12 random sequences each
429
+ ```
430
+
431
+ ### Partial measurement
432
+ ```
433
+ MEASURE Measure all qubits (the default)
434
+ MEASURE 0, 2 Measure only qubits 0 and 2; histogram is over that subset
426
435
  ```
427
436
 
428
437
  ## Algorithm primitives
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.7.0'
31
+ __version__ = '0.8.0'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -344,6 +344,164 @@ class AnalysisMixin:
344
344
  self.io.writeln(f" Purity Tr(rho^2) = {purity:.6f}")
345
345
  self.io.writeln(f" Fidelity to the simulated pure state = {fid:.6f}")
346
346
 
347
+ def cmd_ptomography(self, rest: str = '') -> None:
348
+ """PTOMOGRAPHY — reconstruct the circuit's process as a Pauli Transfer Matrix.
349
+
350
+ Builds the channel of the current program (the unitary, or the full
351
+ noisy channel when a noise model is active) and reports its PTM, where
352
+ R[i,j] = (1/d) Tr(P_i E(P_j)) over Pauli operators. Trace-preserving and
353
+ unital flags and the average gate fidelity to the identity are shown.
354
+ Limited to <= 2 qubits (the process matrix is 4^n x 4^n)."""
355
+ if not self.program:
356
+ self.io.writeln("?NOTHING TO CHARACTERIZE — enter a program first")
357
+ return
358
+ n = self.num_qubits
359
+ if n > 2:
360
+ self.io.writeln(f"?PTOMOGRAPHY limited to 2 qubits (4^n process matrix); have {n}")
361
+ return
362
+ import itertools
363
+ try:
364
+ qc, _ = self.build_circuit()
365
+ from qiskit.quantum_info import Operator, SuperOp, PTM
366
+ if self._noise_model is not None:
367
+ from qiskit_aer import AerSimulator
368
+ from qiskit import transpile
369
+ qc2 = qc.copy()
370
+ qc2.save_superop()
371
+ b = AerSimulator(method='superop', noise_model=self._noise_model)
372
+ res = b.run(transpile(qc2, b, optimization_level=self._transpile_opt_level)).result()
373
+ ptm = PTM(SuperOp(np.asarray(res.data()['superop'])))
374
+ src = 'noisy channel'
375
+ else:
376
+ ptm = PTM(Operator(qc))
377
+ src = 'unitary'
378
+ R = np.real(np.asarray(ptm.data))
379
+ labels = [''.join(p) for p in itertools.product('IXYZ', repeat=n)]
380
+ self.io.writeln(f"\n Pauli Transfer Matrix ({len(R)}x{len(R)}, {src}):")
381
+ self.io.writeln(' ' + ''.join(f'{lb:>7}' for lb in labels))
382
+ for i, row in enumerate(R):
383
+ cells = ''.join(f'{v:+7.3f}' for v in row)
384
+ self.io.writeln(f" {labels[i]:>4} {cells}")
385
+ tp = bool(np.allclose(R[0], [1.0] + [0.0] * (len(R) - 1), atol=1e-6))
386
+ unital = bool(np.allclose(R[:, 0], [1.0] + [0.0] * (len(R) - 1), atol=1e-6))
387
+ d = 2 ** n
388
+ f_avg = (float(np.trace(R)) + d) / (d * d + d) # avg gate fidelity to identity
389
+ self.io.writeln(f" Trace-preserving: {tp} Unital: {unital}")
390
+ self.io.writeln(f" Avg gate fidelity to identity: {f_avg:.6f}")
391
+ except Exception as e:
392
+ self.io.writeln(f"?PTOMOGRAPHY ERROR: {e}")
393
+
394
+ def _single_qubit_cliffords(self):
395
+ """Return the 24 single-qubit Cliffords as (gate_word, matrix) pairs.
396
+
397
+ Generated by BFS over words in {H, S} (which generate the group up to
398
+ global phase), cached on the instance. The word, applied in order, gives
399
+ the stored matrix, so RB sequences run as native h/s gates that the
400
+ active noise model decorates."""
401
+ cached = getattr(self, '_rb_cliffords', None)
402
+ if cached is not None:
403
+ return cached
404
+ from collections import deque
405
+ H = np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2)
406
+ S = np.array([[1, 0], [0, 1j]], dtype=complex)
407
+ gens = (('H', H), ('S', S))
408
+
409
+ def canon(M):
410
+ for v in M.ravel():
411
+ if abs(v) > 1e-9:
412
+ M = M / (v / abs(v)) # fix global phase: first nonzero entry real+
413
+ break
414
+ return tuple(np.round(M, 6).ravel())
415
+
416
+ ident = np.eye(2, dtype=complex)
417
+ table = {canon(ident): ([], ident)}
418
+ queue = deque(table)
419
+ while queue:
420
+ word, M = table[queue.popleft()]
421
+ for gname, G in gens:
422
+ M2 = G @ M
423
+ k2 = canon(M2)
424
+ if k2 not in table:
425
+ table[k2] = (word + [gname], M2)
426
+ queue.append(k2)
427
+ self._rb_cliffords = list(table.values())
428
+ return self._rb_cliffords
429
+
430
+ def cmd_rb(self, rest: str = '') -> None:
431
+ """RB [max_length] [samples] — single-qubit randomized benchmarking.
432
+
433
+ Runs random Clifford sequences of growing length, each ending with the
434
+ recovery Clifford that inverts the sequence (ideal output |0>), measures
435
+ the survival probability, and fits p(m) = A f^m + B to extract the decay
436
+ f and the error per Clifford (1 - f)/2. Reflects the active noise model
437
+ (with no noise, f ~ 1). Defaults: max_length 16, 10 sequences per length."""
438
+ parts = rest.split()
439
+ max_len = int(parts[0]) if len(parts) > 0 else 16
440
+ samples = int(parts[1]) if len(parts) > 1 else 10
441
+ cliffords = self._single_qubit_cliffords()
442
+ gate_method = {'H': 'h', 'S': 's'}
443
+ from qiskit import QuantumCircuit, transpile
444
+ from qiskit_aer import AerSimulator
445
+ backend = AerSimulator(noise_model=self._noise_model) if self._noise_model else AerSimulator()
446
+ shots = max(200, self.shots)
447
+ if self._seed is not None:
448
+ np.random.seed(self._seed)
449
+ lengths = []
450
+ m = 1
451
+ while m <= max_len:
452
+ lengths.append(m)
453
+ m *= 2
454
+ self.io.writeln(f"\n Randomized benchmarking (1 qubit, {samples} sequences/length):")
455
+ self.io.writeln(f" {'length':>8} {'survival':>9}")
456
+ survival = []
457
+ for m in lengths:
458
+ ps = []
459
+ for _ in range(samples):
460
+ qc = QuantumCircuit(1, 1)
461
+ net = np.eye(2, dtype=complex)
462
+ for idx in np.random.randint(len(cliffords), size=m):
463
+ word, mat = cliffords[idx]
464
+ for g in word:
465
+ getattr(qc, gate_method[g])(0)
466
+ net = mat @ net
467
+ inv = net.conj().T
468
+ ridx = max(range(len(cliffords)),
469
+ key=lambda i: abs(np.trace(cliffords[i][1].conj().T @ inv)))
470
+ for g in cliffords[ridx][0]:
471
+ getattr(qc, gate_method[g])(0)
472
+ qc.measure(0, 0)
473
+ kw = {'shots': shots}
474
+ if self._seed is not None:
475
+ kw['seed_simulator'] = self._seed
476
+ # optimization_level=0: the sequence composes to ~identity, so any
477
+ # gate cancellation would strip the gates (and their noise) and
478
+ # flatten the decay. RB needs every gate physically executed.
479
+ counts = backend.run(
480
+ transpile(qc, backend, optimization_level=0), **kw).result().get_counts()
481
+ ps.append(counts.get('0', 0) / sum(counts.values()))
482
+ mean = float(np.mean(ps))
483
+ survival.append(mean)
484
+ self.io.writeln(f" {m:>8} {mean:>9.4f}")
485
+
486
+ if max(survival) - min(survival) < 0.02:
487
+ # Flat survival: no measurable decay, so f is underdetermined. The
488
+ # physical reading is no error (f = 1), not whatever the fit drifts to.
489
+ A, f, B = 0.0, 1.0, float(np.mean(survival))
490
+ else:
491
+ def obj(v):
492
+ A, f, B = v
493
+ f = min(max(f, 0.0), 1.2)
494
+ return sum((p - (A * f ** mm + B)) ** 2 for mm, p in zip(lengths, survival))
495
+
496
+ best, _ = self._nelder_mead(obj, [0.5, 0.99, 0.5], 400, 0.2)
497
+ A, f, B = best
498
+ f = min(max(f, 0.0), 1.0)
499
+ epc = (1.0 - f) / 2.0
500
+ self.io.writeln(f" fit p(m) = {A:.3f} * {f:.5f}^m + {B:.3f}")
501
+ self.io.writeln(f" Decay f = {f:.5f} error per Clifford = {epc:.5f}")
502
+ self.variables['_RB_F'] = f
503
+ self.variables['_RB_EPC'] = epc
504
+
347
505
  def cmd_ram(self) -> None:
348
506
  """RAM — show memory budget, per-instance cost, and parallelism estimates."""
349
507
  ram = _get_ram_gb()
@@ -111,6 +111,8 @@ class ExecutorMixin:
111
111
  qc = QuantumCircuit(self.num_qubits)
112
112
  # Fresh mid-circuit measurement registry for this build (dynamic IF).
113
113
  self._classical_bits = {}
114
+ # Partial-measurement subset (None = measure all at the end).
115
+ self._measure_subset = None
114
116
  # Apply any qubit state preparation requested via POKE to $0100.
115
117
  if getattr(self, '_poke_state_prep', None):
116
118
  self._emit_poke_state_prep(qc)
@@ -161,6 +163,10 @@ class ExecutorMixin:
161
163
 
162
164
  if isinstance(parsed, MeasureStmt):
163
165
  has_measure = True
166
+ if parsed.qubits:
167
+ self._measure_subset = [
168
+ self._resolve_qubit(q)
169
+ for q in parsed.qubits.replace(',', ' ').split()]
164
170
  if self._on_measure_target is not None and not self._on_measure_fired:
165
171
  self._on_measure_fired = True
166
172
  self._gosub_stack.append(ctx.ip + 1)
@@ -517,6 +517,11 @@ def parse_stmt(raw: str) -> Stmt:
517
517
  return RemStmt(raw=raw)
518
518
  if upper == 'MEASURE':
519
519
  return MeasureStmt(raw=raw)
520
+ # MEASURE <qubit list> — partial measurement of a subset (digits/commas only,
521
+ # so MEASURE_X and similar are not captured here).
522
+ m_sub = re.match(r'MEASURE\s+([\d,\s]+)$', text, re.IGNORECASE)
523
+ if m_sub:
524
+ return MeasureStmt(raw=raw, qubits=m_sub.group(1).strip())
520
525
  if upper == 'END':
521
526
  return EndStmt(raw=raw)
522
527
  if upper == 'END IF':
@@ -96,7 +96,7 @@ _ALL_COMMANDS = [
96
96
  'DIFF', 'PLOT', 'THEME', 'CLIP', 'EXPLAIN',
97
97
  'QFT', 'IQFT', 'DIFFUSE', 'MCX', 'MCZ', 'MCP', 'QADD', 'QADDC', 'QPE',
98
98
  'MINIMIZE', 'GRADIENT', 'FIDELITY', 'TOMOGRAPHY', 'COUPLING', 'BASIS',
99
- 'LOADQASM', 'SET_DENSITY', 'SAVEPNG',
99
+ 'LOADQASM', 'SET_DENSITY', 'SAVEPNG', 'PTOMOGRAPHY', 'RB', 'MEASURE',
100
100
  # Gates
101
101
  'H', 'X', 'Y', 'Z', 'S', 'T', 'SDG', 'TDG', 'SX', 'ID',
102
102
  'RX', 'RY', 'RZ', 'P', 'U', 'CX', 'CZ', 'CY', 'CH',
@@ -19,7 +19,7 @@ class RemStmt(Stmt):
19
19
 
20
20
  @dataclass(frozen=True, slots=True)
21
21
  class MeasureStmt(Stmt):
22
- pass
22
+ qubits: str = '' # '' = measure all; else a comma/space list of qubit indices
23
23
 
24
24
  @dataclass(frozen=True, slots=True)
25
25
  class EndStmt(Stmt):
@@ -487,6 +487,8 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
487
487
  'PROBE': 'cmd_probe',
488
488
  # Classic
489
489
  'RESTORE': 'cmd_restore',
490
+ # Characterization
491
+ 'PTOMOGRAPHY': 'cmd_ptomography',
490
492
  }
491
493
  _CMD_WITH_ARG = {
492
494
  'LIST': 'cmd_list', 'QUBITS': 'cmd_qubits', 'SHOTS': 'cmd_shots',
@@ -519,7 +521,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
519
521
  'IMPORT': 'cmd_import', 'TYPE': 'cmd_type',
520
522
  'SAMPLE': 'cmd_sample', 'ESTIMATE': 'cmd_estimate', 'BENCH': 'cmd_bench',
521
523
  'MINIMIZE': 'cmd_minimize', 'GRADIENT': 'cmd_gradient',
522
- 'FIDELITY': 'cmd_fidelity', 'TOMOGRAPHY': 'cmd_tomography',
524
+ 'FIDELITY': 'cmd_fidelity', 'TOMOGRAPHY': 'cmd_tomography', 'RB': 'cmd_rb',
523
525
  'COUPLING': 'cmd_coupling', 'BASIS': 'cmd_basis',
524
526
  'LOADQASM': 'cmd_loadqasm', 'SAVEPNG': 'cmd_savepng',
525
527
  'SET_STATE': 'cmd_set_state', 'SET_DENSITY': 'cmd_set_density',
@@ -1325,7 +1327,16 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
1325
1327
  if self.sim_method in ('unitary', 'superop'):
1326
1328
  qc.save_unitary(label='unitary') if self.sim_method == 'unitary' else qc.save_superop(label='superop')
1327
1329
  elif has_measure:
1328
- qc.measure_all()
1330
+ subset = getattr(self, '_measure_subset', None)
1331
+ if subset:
1332
+ # Partial measurement: report counts over the chosen qubits only.
1333
+ from qiskit.circuit import ClassicalRegister
1334
+ cr = ClassicalRegister(len(subset), 'sub')
1335
+ qc.add_register(cr)
1336
+ for i, q in enumerate(subset):
1337
+ qc.measure(q, cr[i])
1338
+ else:
1339
+ qc.measure_all()
1329
1340
 
1330
1341
  self.last_circuit = qc
1331
1342
  self._last_transpiled = None
@@ -1648,6 +1648,47 @@ class TestNewCommands(unittest.TestCase):
1648
1648
  self.assertIsNotNone(t._last_transpiled)
1649
1649
 
1650
1650
 
1651
+ class TestCharacterization(unittest.TestCase):
1652
+ """Partial measurement, process tomography, randomized benchmarking."""
1653
+
1654
+ def test_partial_measurement(self):
1655
+ # GHZ measured on a subset gives correlated subset counts.
1656
+ t = QBasicTerminal(); t.num_qubits = 3; t.shots = 1000; t._seed = 1
1657
+ for ln in ['10 H 0', '20 CX 0,1', '30 CX 0,2', '40 MEASURE 0,2']:
1658
+ t.process(ln, track_undo=False)
1659
+ capture(t.cmd_run)
1660
+ self.assertTrue(all(len(k) == 2 for k in t.last_counts)) # two-bit keys
1661
+ self.assertEqual(set(t.last_counts), {'00', '11'}) # correlated
1662
+ # single-qubit subset gives one-bit keys
1663
+ t2 = QBasicTerminal(); t2.num_qubits = 3; t2.shots = 1000; t2._seed = 1
1664
+ for ln in ['10 H 0', '20 CX 0,1', '30 MEASURE 1']:
1665
+ t2.process(ln, track_undo=False)
1666
+ capture(t2.cmd_run)
1667
+ self.assertEqual(set(t2.last_counts), {'0', '1'})
1668
+
1669
+ def test_process_tomography(self):
1670
+ # X gate: PTM = diag(1, 1, -1, -1).
1671
+ t = QBasicTerminal(); t.num_qubits = 1
1672
+ t.process('10 X 0', track_undo=False)
1673
+ _, out = capture(t.cmd_ptomography)
1674
+ self.assertIn('Trace-preserving: True', out)
1675
+ self.assertIn('-1.000', out) # Y and Z rows flip sign
1676
+ # H gate maps Z -> X, so the X column has a +1 off the diagonal.
1677
+ t2 = QBasicTerminal(); t2.num_qubits = 1
1678
+ t2.process('10 H 0', track_undo=False)
1679
+ _, out2 = capture(t2.cmd_ptomography)
1680
+ self.assertIn('Trace-preserving: True', out2)
1681
+
1682
+ def test_randomized_benchmarking(self):
1683
+ # Noiseless single-qubit RB: perfect recovery, f = 1, error per Clifford = 0.
1684
+ t = QBasicTerminal(); t.num_qubits = 1; t.shots = 400; t._seed = 3
1685
+ capture(t.cmd_rb, '4 3')
1686
+ self.assertAlmostEqual(t.variables['_RB_F'], 1.0, places=6)
1687
+ self.assertAlmostEqual(t.variables['_RB_EPC'], 0.0, places=6)
1688
+ # 24 single-qubit Cliffords are generated.
1689
+ self.assertEqual(len(t._single_qubit_cliffords()), 24)
1690
+
1691
+
1651
1692
  if __name__ == '__main__':
1652
1693
  if hasattr(sys.stdout, 'reconfigure'):
1653
1694
  sys.stdout.reconfigure(encoding='utf-8')
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