qcoder 0.1.0a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. qcoder/__init__.py +3 -0
  2. qcoder/__main__.py +6 -0
  3. qcoder/cli.py +116 -0
  4. qcoder/core/__init__.py +1 -0
  5. qcoder/core/context.py +16 -0
  6. qcoder/core/qasm2/__init__.py +1 -0
  7. qcoder/core/qasm2/adjoint_eligibility.py +128 -0
  8. qcoder/core/qasm2/mirror_build.py +234 -0
  9. qcoder/core/run_config.py +84 -0
  10. qcoder/core/schema.py +26 -0
  11. qcoder/engines/feature_extraction/adapters/__init__.py +1 -0
  12. qcoder/engines/feature_extraction/adapters/qiskit_intake.py +46 -0
  13. qcoder/engines/feature_extraction/extractor.py +43 -0
  14. qcoder/engines/feature_extraction/features/compute_v0.py +157 -0
  15. qcoder/engines/feature_extraction/features/schema_v0.py +84 -0
  16. qcoder/engines/feature_extraction/ir.py +41 -0
  17. qcoder/engines/feature_extraction/labeling.py +68 -0
  18. qcoder/engines/feature_extraction/parsers/__init__.py +21 -0
  19. qcoder/engines/feature_extraction/qasm2_regex_parser.py +184 -0
  20. qcoder/engines/feature_extraction/reps/cut_profile.py +106 -0
  21. qcoder/engines/feature_extraction/reps/depth.py +47 -0
  22. qcoder/engines/feature_extraction/reps/entangling_layers.py +57 -0
  23. qcoder/engines/feature_extraction/reps/gate_set_stats.py +82 -0
  24. qcoder/engines/feature_extraction/reps/interaction_graph.py +30 -0
  25. qcoder/engines/feature_extraction/reps/interaction_graph_metrics.py +113 -0
  26. qcoder/engines/feature_extraction/reps/spans.py +89 -0
  27. qcoder/engines/prediction_model/__init__.py +16 -0
  28. qcoder/engines/prediction_model/artifact.py +85 -0
  29. qcoder/engines/prediction_model/engine.py +209 -0
  30. qcoder/engines/prediction_model/models.py +62 -0
  31. qcoder/engines/prediction_model/policy.py +45 -0
  32. qcoder/engines/prediction_model/schema_alignment.py +41 -0
  33. qcoder/engines/quantumness/__init__.py +8 -0
  34. qcoder/engines/quantumness/scorer.py +254 -0
  35. qcoder/pipelines/analyze.py +131 -0
  36. qcoder/pipelines/batch.py +56 -0
  37. qcoder/tools/analyze.py +88 -0
  38. qcoder/tools/analyze_shot_scaling.py +239 -0
  39. qcoder/tools/batch.py +39 -0
  40. qcoder/tools/generate_corpus.py +491 -0
  41. qcoder/tools/harness.py +15 -0
  42. qcoder/tools/inspect_corpus_features.py +273 -0
  43. qcoder/tools/join_runs_features.py +252 -0
  44. qcoder/tools/mirror.py +15 -0
  45. qcoder/tools/predict_baseline.py +347 -0
  46. qcoder/tools/qr_dll_bootstrap.py +31 -0
  47. qcoder/tools/runner.py +15 -0
  48. qcoder/tools/runners/__init__.py +1 -0
  49. qcoder/tools/runners/quantum_rings/__init__.py +1 -0
  50. qcoder/tools/runners/quantum_rings/v12/__init__.py +1 -0
  51. qcoder/tools/runners/quantum_rings/v12/harness.py +1350 -0
  52. qcoder/tools/runners/quantum_rings/v12/mirror.py +459 -0
  53. qcoder/tools/runners/quantum_rings/v12/runner.py +549 -0
  54. qcoder/tools/train_baseline_models.py +619 -0
  55. qcoder/tools/validate_baseline.py +307 -0
  56. qcoder-0.1.0a0.dist-info/METADATA +86 -0
  57. qcoder-0.1.0a0.dist-info/RECORD +62 -0
  58. qcoder-0.1.0a0.dist-info/WHEEL +5 -0
  59. qcoder-0.1.0a0.dist-info/entry_points.txt +2 -0
  60. qcoder-0.1.0a0.dist-info/licenses/LICENSE +201 -0
  61. qcoder-0.1.0a0.dist-info/licenses/NOTICE +11 -0
  62. qcoder-0.1.0a0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,549 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ qr12_runner.py
4
+
5
+ QR12 single-purpose runner -> one JSON per run (top-K only) plus SDK-native metrics (best-effort).
6
+
7
+ Why this exists:
8
+ - In QR11 we assumed result.get_fidelity / result.get_peakmemorysize_0 were missing or unreliable.
9
+ - In QR12, they appear to exist, so we:
10
+ - keep our external timing + peak RSS sampler
11
+ - ALSO record built-in SDK metrics if callable
12
+
13
+ Outputs:
14
+ - JSON per run with:
15
+ - timing_s.run_wall_s
16
+ - memory.peak_rss_bytes / peak_rss_mb (external sampler)
17
+ - sdk_metrics.get_fidelity / get_peakmemorysize_0 / get_memory (best-effort)
18
+ - hist.top (top-K counts), tail_mass, unique_outcomes
19
+
20
+ NOTE:
21
+ - We keep your CUDA DLL search-path bootstrap (critical for your unusual CUDA DLL folder layout).
22
+ """
23
+
24
+ from qcoder.tools import qr_dll_bootstrap
25
+
26
+ qr_dll_bootstrap.bootstrap()
27
+
28
+ import argparse
29
+ import datetime as _dt
30
+ import hashlib
31
+ import importlib.metadata as _imd
32
+ import json
33
+ import os
34
+ import platform
35
+ import re
36
+ import subprocess
37
+ import sys
38
+ import time
39
+ from typing import Any, Dict, List, Optional, Tuple
40
+
41
+ import psutil
42
+
43
+ from QuantumRingsLib import QuantumRingsProvider, QuantumCircuit as QR_Circuit, job_monitor
44
+
45
+
46
+ # -----------------------------
47
+ # Helpers
48
+ # -----------------------------
49
+
50
+ def die(msg: str, code: int = 1) -> None:
51
+ print(f"[fatal] {msg}", file=sys.stderr, flush=True)
52
+ sys.exit(code)
53
+
54
+ def read_file(path: str) -> str:
55
+ if not path:
56
+ die("No QASM path provided.")
57
+ if not os.path.exists(path):
58
+ die(f"File not found: {path}")
59
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
60
+ return f.read()
61
+
62
+ def sha256_file(path: str, chunk_size: int = 1 << 20) -> str:
63
+ h = hashlib.sha256()
64
+ with open(path, "rb") as f:
65
+ while True:
66
+ b = f.read(chunk_size)
67
+ if not b:
68
+ break
69
+ h.update(b)
70
+ return h.hexdigest()
71
+
72
+ def first_noncomment_nonblank_line(text: str) -> Optional[str]:
73
+ for line in text.splitlines():
74
+ s = line.strip()
75
+ if not s:
76
+ continue
77
+ if s.startswith("//"):
78
+ continue
79
+ return s
80
+ return None
81
+
82
+ def sanitize_qasm2_identifiers(text: str) -> Tuple[str, Dict[str, str]]:
83
+ """
84
+ Rename qreg/creg identifiers that can conflict with gate names in some parsers.
85
+ Preserve your existing behavior.
86
+ """
87
+ target = {"x": "xr", "y": "yr", "t": "anc"}
88
+ decls = re.findall(r'^\s*(qreg|creg)\s+([A-Za-z_]\w*)\s*\[\d+\]\s*;', text, flags=re.M)
89
+ need = {name for _, name in decls if name in target}
90
+ if not need:
91
+ return text, {}
92
+ renames = {old: target[old] for old in need}
93
+
94
+ for old, new in renames.items():
95
+ text = re.sub(rf'(^\s*qreg\s+){old}(\s*\[\d+\]\s*;)', rf'\1{new}\2', text, flags=re.M)
96
+ text = re.sub(rf'(^\s*creg\s+){old}(\s*\[\d+\]\s*;)', rf'\1{new}\2', text, flags=re.M)
97
+
98
+ for old, new in renames.items():
99
+ text = re.sub(rf'(?<![A-Za-z0-9_]){old}\[', f'{new}[', text)
100
+
101
+ return text, renames
102
+
103
+ def has_measures(qc: QR_Circuit) -> bool:
104
+ try:
105
+ if getattr(qc, "num_clbits", 0) > 0:
106
+ return True
107
+ except Exception:
108
+ pass
109
+ try:
110
+ ops = qc.count_ops()
111
+ return bool(ops.get("measure", 0))
112
+ except Exception:
113
+ return False
114
+
115
+ def try_nvidia_smi() -> Optional[Dict[str, Any]]:
116
+ """
117
+ Best-effort GPU info. If nvidia-smi isn't available, return None.
118
+ """
119
+ try:
120
+ cmd = [
121
+ "nvidia-smi",
122
+ "--query-gpu=name,driver_version,memory.total",
123
+ "--format=csv,noheader,nounits",
124
+ ]
125
+ p = subprocess.run(cmd, capture_output=True, text=True, check=False)
126
+ if p.returncode != 0:
127
+ return None
128
+ line = (p.stdout.strip().splitlines() or [""])[0].strip()
129
+ if not line:
130
+ return None
131
+ parts = [x.strip() for x in line.split(",")]
132
+ if len(parts) >= 3:
133
+ return {
134
+ "gpu_name": parts[0],
135
+ "driver_version": parts[1],
136
+ "gpu_memory_total_mb": float(parts[2]) if parts[2].replace(".", "", 1).isdigit() else parts[2],
137
+ }
138
+ return {"raw": line}
139
+ except Exception:
140
+ return None
141
+
142
+ def resolve_backend_id(provider: QuantumRingsProvider, backend_arg: str, precision: str) -> str:
143
+ """
144
+ Accept friendly names (Scarlet/Amber/Serin or CPU/GPU/Both) OR raw backend ids.
145
+ """
146
+ if not backend_arg:
147
+ return "scarlet_quantum_rings"
148
+
149
+ b = backend_arg.strip()
150
+ if "_" in b or b.endswith("_quantum_rings"):
151
+ return b
152
+
153
+ key = b.lower()
154
+ if key in ("scarlet", "cpu"):
155
+ candidates = ["scarlet_quantum_rings", "scarlet"]
156
+ elif key in ("amber", "gpu"):
157
+ candidates = ["amber_quantum_rings", "amber"]
158
+ elif key in ("serin", "both", "hybrid"):
159
+ candidates = ["serin_quantum_rings", "serin"]
160
+ else:
161
+ candidates = [b]
162
+
163
+ last_err = None
164
+ for cid in candidates:
165
+ try:
166
+ _ = provider.get_backend(cid, precision=precision)
167
+ return cid
168
+ except Exception as e:
169
+ last_err = e
170
+ die(f"Could not resolve backend '{backend_arg}'. Tried: {candidates}. Last error: {last_err}")
171
+ return b # unreachable
172
+
173
+ def default_json_out(qasm_path: str, backend_id: str, precision: str, threshold: int, shots: int, out_dir: str) -> str:
174
+ base = os.path.splitext(os.path.basename(qasm_path))[0]
175
+ fname = f"{base}__{backend_id}__{precision}__thr{threshold}__shots{shots}.json"
176
+ return os.path.join(out_dir, fname)
177
+
178
+ def start_peak_rss_sampler(interval_s: float = 0.05):
179
+ import threading
180
+ import time as _time
181
+ import os as _os
182
+
183
+ proc = psutil.Process(_os.getpid())
184
+ peak = {"rss": 0}
185
+ running = {"on": True}
186
+
187
+ def _loop():
188
+ while running["on"]:
189
+ try:
190
+ rss = proc.memory_info().rss
191
+ if rss > peak["rss"]:
192
+ peak["rss"] = rss
193
+ except Exception:
194
+ pass
195
+ _time.sleep(interval_s)
196
+
197
+ th = threading.Thread(target=_loop, daemon=True)
198
+ th.start()
199
+ return running, th, peak
200
+
201
+ def stop_peak_rss_sampler(running, th):
202
+ running["on"] = False
203
+ th.join(timeout=1.0)
204
+
205
+ _QREG_DECL_LINE = re.compile(r'^\s*qreg\s+([A-Za-z_]\w*)\s*\[(\d+)\]\s*;\s*$', re.M)
206
+
207
+ def normalize_terminal_measurements_qasm2(text: str) -> str:
208
+ """
209
+ If SDK complains about classical bits vs qubits mismatch, we:
210
+ - drop existing creg + measure lines
211
+ - create a new creg meas[total_qubits]
212
+ - append explicit terminal measurement of every qubit -> meas[i]
213
+ """
214
+ regs = [(m.group(1), int(m.group(2))) for m in _QREG_DECL_LINE.finditer(text)]
215
+ if not regs:
216
+ return text
217
+
218
+ total = sum(n for _, n in regs)
219
+
220
+ lines = text.splitlines()
221
+ kept = []
222
+ last_qreg_idx = -1
223
+ for i, line in enumerate(lines):
224
+ s = line.strip()
225
+ if s.startswith("qreg "):
226
+ last_qreg_idx = i
227
+ kept.append(line)
228
+ continue
229
+ if s.startswith("creg "):
230
+ continue
231
+ if s.startswith("measure "):
232
+ continue
233
+ kept.append(line)
234
+
235
+ insert_at = max(0, last_qreg_idx + 1)
236
+ kept.insert(insert_at, f"creg meas[{total}];")
237
+
238
+ out = kept[:]
239
+ out.append("")
240
+ k = 0
241
+ for name, n in regs:
242
+ for qi in range(n):
243
+ out.append(f"measure {name}[{qi}] -> meas[{k}];")
244
+ k += 1
245
+
246
+ return "\n".join(out) + "\n"
247
+
248
+ def get_dist_version(names: List[str]) -> Optional[str]:
249
+ for n in names:
250
+ try:
251
+ return _imd.version(n)
252
+ except Exception:
253
+ pass
254
+ return None
255
+
256
+ def safe_call_result_getter(result: Any, name: str):
257
+ """
258
+ Best-effort: call result.<name>() if it exists and is no-arg.
259
+ Returns: (value, error_string_or_None)
260
+ """
261
+ if not hasattr(result, name):
262
+ return None, "missing"
263
+ try:
264
+ return getattr(result, name)(), None
265
+ except Exception as e:
266
+ return None, repr(e)
267
+
268
+ def jsonable_value(x: Any) -> Any:
269
+ if x is None:
270
+ return None
271
+ if isinstance(x, (str, int, float, bool)):
272
+ return x
273
+ # Some SDK objects won't serialize; store repr
274
+ return repr(x)
275
+
276
+
277
+ # -----------------------------
278
+ # Main
279
+ # -----------------------------
280
+
281
+ def main() -> None:
282
+ ap = argparse.ArgumentParser(
283
+ description="QR12 single run: run one QASM once and emit one JSON (top-K only) + SDK native metrics."
284
+ )
285
+ ap.add_argument("qasm", help="Path to .qasm (intended QASM2; header comments OK)")
286
+ ap.add_argument("--backend", default="Scarlet",
287
+ help="Backend: Scarlet|Amber|Serin (or CPU/GPU/Both) OR backend id like scarlet_quantum_rings")
288
+ ap.add_argument("--precision", choices=["single", "double"], default="single",
289
+ help="Numeric precision passed to get_backend")
290
+ ap.add_argument("--threshold", type=int, default=128,
291
+ help="Threshold knob (performance='custom')")
292
+ ap.add_argument("--shots", type=int, default=10000,
293
+ help="Number of shots")
294
+ ap.add_argument("--topk", type=int, default=50,
295
+ help="Emit top-K bitstrings only")
296
+ ap.add_argument("--json-out", default=None,
297
+ help="Output JSON path (default: runs/<auto>.json)")
298
+ ap.add_argument("--out-dir", default="runs",
299
+ help="Directory for default JSON output name when --json-out not provided")
300
+ ap.add_argument("--save-qasm-post", default=None,
301
+ help="Optional: save sanitized QASM that is executed")
302
+ ap.add_argument("--print-ops", action="store_true",
303
+ help="Print operation counts before running")
304
+
305
+ args = ap.parse_args()
306
+
307
+ print("===== QR12 Single-Run Runner =====", flush=True)
308
+
309
+ os.makedirs(args.out_dir, exist_ok=True)
310
+
311
+ # Identify installed distribution (helps debugging env mismatch)
312
+ sdk_pkg = "quantumrings-cuda13x"
313
+ sdk_ver = get_dist_version([sdk_pkg, "quantumrings_cuda13x", "QuantumRingsLib"])
314
+ print(f"[env] sdk_pkg={sdk_pkg} sdk_version={sdk_ver} python={sys.version.split()[0]}", flush=True)
315
+
316
+ # Provider first (SDK may validate entitlements during circuit load)
317
+ provider = QuantumRingsProvider()
318
+
319
+ # Backend resolution + get_backend with precision
320
+ backend_id = resolve_backend_id(provider, args.backend, args.precision)
321
+ backend = provider.get_backend(backend_id, precision=args.precision)
322
+ print(f"[backend] {backend}", flush=True)
323
+
324
+ # Read + basic header check (non-fatal unless empty)
325
+ text = read_file(args.qasm)
326
+ first_stmt = first_noncomment_nonblank_line(text)
327
+ if first_stmt is None:
328
+ die("QASM file is empty.")
329
+ if first_stmt.upper().startswith("OPENQASM") and not first_stmt.upper().startswith("OPENQASM 2.0"):
330
+ print(f"[warn] header is not 'OPENQASM 2.0': {first_stmt}", flush=True)
331
+
332
+ # Sanitize identifiers
333
+ text_fixed, renames = sanitize_qasm2_identifiers(text)
334
+ if renames:
335
+ print(f"[sanitize] renamed identifiers: {renames}", flush=True)
336
+
337
+ # Optionally save post-QASM
338
+ if args.save_qasm_post:
339
+ os.makedirs(os.path.dirname(args.save_qasm_post) or ".", exist_ok=True)
340
+ with open(args.save_qasm_post, "w", encoding="utf-8") as f:
341
+ f.write(text_fixed)
342
+ print(f"[ok] wrote post.qasm: {args.save_qasm_post}", flush=True)
343
+
344
+ # Load circuit (with normalization fallback)
345
+ try:
346
+ qc = QR_Circuit.from_qasm_str(text_fixed)
347
+ text_executed = text_fixed
348
+ except Exception as e:
349
+ msg = str(e).lower()
350
+ if "classical bits" in msg and "qubits" in msg:
351
+ print("[warn] QASM load failed due to classical/qubit mismatch; normalizing terminal measurements and retrying.", flush=True)
352
+ text_norm = normalize_terminal_measurements_qasm2(text_fixed)
353
+ if args.save_qasm_post:
354
+ os.makedirs(os.path.dirname(args.save_qasm_post) or ".", exist_ok=True)
355
+ with open(args.save_qasm_post, "w", encoding="utf-8") as f:
356
+ f.write(text_norm)
357
+ print(f"[ok] wrote normalized post.qasm: {args.save_qasm_post}", flush=True)
358
+ qc = QR_Circuit.from_qasm_str(text_norm)
359
+ text_executed = text_norm
360
+ else:
361
+ die(f"QASM load failed: {e}")
362
+
363
+ # Basic info
364
+ try:
365
+ n_qubits = int(qc.num_qubits)
366
+ except Exception:
367
+ n_qubits = None
368
+ print(f"[circuit] qubits={n_qubits if n_qubits is not None else '?'}", flush=True)
369
+
370
+ if args.print_ops:
371
+ try:
372
+ ops = qc.count_ops()
373
+ total_ops = sum(int(v) for v in ops.values())
374
+ print(f"[ops] total={total_ops}", flush=True)
375
+ for k, v in sorted(ops.items(), key=lambda kv: kv[1], reverse=True):
376
+ print(f" {k:12s}: {int(v)}", flush=True)
377
+ except Exception as e:
378
+ print(f"[ops] count_ops unavailable: {e}", flush=True)
379
+
380
+ if not has_measures(qc):
381
+ die("Circuit has no measurements / classical bits; cannot produce counts. Fix the QASM or add measurements.")
382
+
383
+ # Run settings (consistent with your harness assumptions)
384
+ run_kwargs = dict(
385
+ shots=int(args.shots),
386
+ mode="sync",
387
+ generate_amplitude=False,
388
+ quiet=True,
389
+ performance="custom",
390
+ threshold=int(args.threshold),
391
+ transfer_to_cpu=True,
392
+ max_threads=5,
393
+ )
394
+
395
+ print(f"[run] backend_id={backend_id} precision={args.precision} threshold={args.threshold} shots={args.shots}", flush=True)
396
+
397
+ # Timing + peak RSS sampler around run
398
+ running, th, peak = start_peak_rss_sampler(0.05)
399
+ t_submit = time.perf_counter()
400
+
401
+ job = backend.run(qc, **run_kwargs)
402
+ job_monitor(job, quiet=True)
403
+ result = job.result()
404
+
405
+ t_done = time.perf_counter()
406
+ stop_peak_rss_sampler(running, th)
407
+
408
+ run_wall_s = t_done - t_submit
409
+ peak_rss_bytes = int(peak["rss"])
410
+
411
+ # Counts top-K
412
+ counts = result.get_counts()
413
+ if not isinstance(counts, dict):
414
+ die(f"result.get_counts() returned unexpected type: {type(counts)}")
415
+
416
+ # normalize keys (strip spaces just in case)
417
+ counts_norm = {str(k).replace(" ", ""): int(v) for k, v in counts.items()}
418
+ total_shots_from_counts = sum(int(v) for v in counts_norm.values())
419
+
420
+ items = [(str(k), int(v)) for k, v in counts_norm.items()]
421
+ items.sort(key=lambda kv: (-kv[1], kv[0]))
422
+
423
+ topk = max(1, int(args.topk))
424
+ top_items = items[:topk]
425
+ top_sum = sum(c for _, c in top_items)
426
+ tail_mass = (total_shots_from_counts - top_sum) / total_shots_from_counts if total_shots_from_counts else 0.0
427
+
428
+ # QR12 SDK-native metrics (best-effort)
429
+ sdk_metrics: Dict[str, Any] = {"errors": {}}
430
+
431
+ bif, bif_err = safe_call_result_getter(result, "get_fidelity")
432
+ if bif_err:
433
+ sdk_metrics["errors"]["get_fidelity"] = bif_err
434
+ sdk_metrics["get_fidelity"] = jsonable_value(bif)
435
+
436
+ # get_peakmemorysize appears to require an experiment index in QR12 (arg0:int)
437
+ bipm = None
438
+ bipm_err = None
439
+ if hasattr(result, "get_peakmemorysize"):
440
+ try:
441
+ bipm = result.get_peakmemorysize(0)
442
+ except Exception as e0:
443
+ # fallback: some builds *might* accept no-arg
444
+ try:
445
+ bipm = result.get_peakmemorysize()
446
+ except Exception as e1:
447
+ bipm_err = repr(e1)
448
+ # optional: keep original error too
449
+ sdk_metrics["errors"]["get_peakmemorysize(0)"] = repr(e0)
450
+ else:
451
+ bipm_err = "missing"
452
+
453
+ if bipm_err:
454
+ sdk_metrics["errors"]["get_peakmemorysize_0"] = bipm_err
455
+
456
+ # store under an explicit name so we remember index=0 was used
457
+ sdk_metrics["get_peakmemorysize_0"] = jsonable_value(bipm)
458
+
459
+
460
+ memx, memx_err = safe_call_result_getter(result, "get_memory")
461
+ if memx_err:
462
+ sdk_metrics["errors"]["get_memory"] = memx_err
463
+ sdk_metrics["get_memory"] = jsonable_value(memx)
464
+
465
+ # If peakmemorysize looks numeric, also store a convenience conversion guess
466
+ try:
467
+ if isinstance(bipm, (int, float)) and float(bipm) > 0:
468
+ bipm_f = float(bipm)
469
+ # heuristic: if it looks like bytes, offer MB estimate too
470
+ sdk_metrics["get_peakmemorysize_mb_guess"] = bipm_f / (1024.0 * 1024.0) if bipm_f > 1024.0 * 1024.0 else bipm_f
471
+ except Exception:
472
+ pass
473
+
474
+ # Output path
475
+ if args.json_out:
476
+ out_path = args.json_out
477
+ else:
478
+ out_path = default_json_out(args.qasm, backend_id, args.precision, int(args.threshold), int(args.shots), args.out_dir)
479
+ os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
480
+
481
+ # System info (best-effort)
482
+ gpu_info = try_nvidia_smi()
483
+
484
+ payload: Dict[str, Any] = {
485
+ "meta": {
486
+ "created_utc": _dt.datetime.now(_dt.timezone.utc).replace(microsecond=0).isoformat(),
487
+ "qasm_file": os.path.basename(args.qasm),
488
+ "qasm_path": os.path.abspath(args.qasm),
489
+ "qasm_sha256": sha256_file(args.qasm),
490
+ "n_qubits": n_qubits,
491
+ "shots": int(args.shots),
492
+ "topk": topk,
493
+ "backend": str(args.backend),
494
+ "backend_id": backend_id,
495
+ "backend_repr": str(backend),
496
+ "precision": args.precision,
497
+ "threshold": int(args.threshold),
498
+ "sdk_pkg": sdk_pkg,
499
+ "sdk_version": sdk_ver,
500
+ "python_full": sys.version,
501
+ },
502
+ "sdk": {
503
+ **run_kwargs
504
+ },
505
+ "system": {
506
+ "hostname": platform.node(),
507
+ "platform": platform.platform(),
508
+ "python": sys.version.split()[0],
509
+ "cpu_count": os.cpu_count(),
510
+ "gpu": gpu_info,
511
+ },
512
+ "timing_s": {
513
+ "run_wall_s": float(run_wall_s),
514
+ },
515
+ "memory": {
516
+ "peak_rss_bytes": int(peak_rss_bytes),
517
+ "peak_rss_mb": float(peak_rss_bytes) / (1024.0 * 1024.0),
518
+ },
519
+ "sdk_metrics": sdk_metrics,
520
+ "hist": {
521
+ "unique_outcomes": len(items),
522
+ "total_shots": int(total_shots_from_counts),
523
+ "tail_mass": float(tail_mass),
524
+ "top": [{"bitstring": b, "count": c} for b, c in top_items],
525
+ },
526
+ }
527
+
528
+ with open(out_path, "w", encoding="utf-8") as f:
529
+ json.dump(payload, f, indent=2, sort_keys=True)
530
+
531
+ print(f"[ok] wrote JSON: {out_path}", flush=True)
532
+ print(f"[result] unique_outcomes={len(items)} total_shots={total_shots_from_counts} tail_mass={tail_mass:.6f}", flush=True)
533
+ print(f"[timing] run_wall_s={run_wall_s:.3f} [mem] peak_rss_mb={payload['memory']['peak_rss_mb']:.1f}", flush=True)
534
+ print(f"[sdk] get_fidelity={sdk_metrics.get('get_fidelity')} get_peakmemorysize_0={sdk_metrics.get('get_peakmemorysize_0')}", flush=True)
535
+ if sdk_metrics.get("errors"):
536
+ print(f"[sdk] errors={sdk_metrics['errors']}", flush=True)
537
+ print("===== done =====", flush=True)
538
+
539
+
540
+ if __name__ == "__main__":
541
+ try:
542
+ main()
543
+ except SystemExit:
544
+ raise
545
+ except Exception as e:
546
+ import traceback
547
+ traceback.print_exc()
548
+ die(f"Unhandled exception: {e}")
549
+