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.
- qcoder/__init__.py +3 -0
- qcoder/__main__.py +6 -0
- qcoder/cli.py +116 -0
- qcoder/core/__init__.py +1 -0
- qcoder/core/context.py +16 -0
- qcoder/core/qasm2/__init__.py +1 -0
- qcoder/core/qasm2/adjoint_eligibility.py +128 -0
- qcoder/core/qasm2/mirror_build.py +234 -0
- qcoder/core/run_config.py +84 -0
- qcoder/core/schema.py +26 -0
- qcoder/engines/feature_extraction/adapters/__init__.py +1 -0
- qcoder/engines/feature_extraction/adapters/qiskit_intake.py +46 -0
- qcoder/engines/feature_extraction/extractor.py +43 -0
- qcoder/engines/feature_extraction/features/compute_v0.py +157 -0
- qcoder/engines/feature_extraction/features/schema_v0.py +84 -0
- qcoder/engines/feature_extraction/ir.py +41 -0
- qcoder/engines/feature_extraction/labeling.py +68 -0
- qcoder/engines/feature_extraction/parsers/__init__.py +21 -0
- qcoder/engines/feature_extraction/qasm2_regex_parser.py +184 -0
- qcoder/engines/feature_extraction/reps/cut_profile.py +106 -0
- qcoder/engines/feature_extraction/reps/depth.py +47 -0
- qcoder/engines/feature_extraction/reps/entangling_layers.py +57 -0
- qcoder/engines/feature_extraction/reps/gate_set_stats.py +82 -0
- qcoder/engines/feature_extraction/reps/interaction_graph.py +30 -0
- qcoder/engines/feature_extraction/reps/interaction_graph_metrics.py +113 -0
- qcoder/engines/feature_extraction/reps/spans.py +89 -0
- qcoder/engines/prediction_model/__init__.py +16 -0
- qcoder/engines/prediction_model/artifact.py +85 -0
- qcoder/engines/prediction_model/engine.py +209 -0
- qcoder/engines/prediction_model/models.py +62 -0
- qcoder/engines/prediction_model/policy.py +45 -0
- qcoder/engines/prediction_model/schema_alignment.py +41 -0
- qcoder/engines/quantumness/__init__.py +8 -0
- qcoder/engines/quantumness/scorer.py +254 -0
- qcoder/pipelines/analyze.py +131 -0
- qcoder/pipelines/batch.py +56 -0
- qcoder/tools/analyze.py +88 -0
- qcoder/tools/analyze_shot_scaling.py +239 -0
- qcoder/tools/batch.py +39 -0
- qcoder/tools/generate_corpus.py +491 -0
- qcoder/tools/harness.py +15 -0
- qcoder/tools/inspect_corpus_features.py +273 -0
- qcoder/tools/join_runs_features.py +252 -0
- qcoder/tools/mirror.py +15 -0
- qcoder/tools/predict_baseline.py +347 -0
- qcoder/tools/qr_dll_bootstrap.py +31 -0
- qcoder/tools/runner.py +15 -0
- qcoder/tools/runners/__init__.py +1 -0
- qcoder/tools/runners/quantum_rings/__init__.py +1 -0
- qcoder/tools/runners/quantum_rings/v12/__init__.py +1 -0
- qcoder/tools/runners/quantum_rings/v12/harness.py +1350 -0
- qcoder/tools/runners/quantum_rings/v12/mirror.py +459 -0
- qcoder/tools/runners/quantum_rings/v12/runner.py +549 -0
- qcoder/tools/train_baseline_models.py +619 -0
- qcoder/tools/validate_baseline.py +307 -0
- qcoder-0.1.0a0.dist-info/METADATA +86 -0
- qcoder-0.1.0a0.dist-info/RECORD +62 -0
- qcoder-0.1.0a0.dist-info/WHEEL +5 -0
- qcoder-0.1.0a0.dist-info/entry_points.txt +2 -0
- qcoder-0.1.0a0.dist-info/licenses/LICENSE +201 -0
- qcoder-0.1.0a0.dist-info/licenses/NOTICE +11 -0
- 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
|
+
|