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,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from qcoder.core.run_config import RunConfig
|
|
8
|
+
from qcoder.engines.feature_extraction.extractor import CircuitExample, extract_example
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class AnalyzeReport:
|
|
13
|
+
example: CircuitExample
|
|
14
|
+
run_config: RunConfig
|
|
15
|
+
mirror_metadata: dict | None = None # when --mirror-artifacts-dir used
|
|
16
|
+
|
|
17
|
+
def to_json_dict(self) -> dict:
|
|
18
|
+
ex = self.example
|
|
19
|
+
out = {
|
|
20
|
+
"circuit_id": ex.id,
|
|
21
|
+
"circuit_name": ex.name,
|
|
22
|
+
"function_hint": ex.function_hint,
|
|
23
|
+
"function_source": ex.function_source,
|
|
24
|
+
"qasm_path": ex.qasm_path,
|
|
25
|
+
"source_format": ex.ir.source_format,
|
|
26
|
+
"n_qubits": ex.ir.n_qubits,
|
|
27
|
+
"n_cbits": ex.ir.n_cbits,
|
|
28
|
+
"n_ops": ex.ir.n_ops,
|
|
29
|
+
"run_config": self.run_config.to_dict(),
|
|
30
|
+
"features": ex.global_features.to_dict(),
|
|
31
|
+
}
|
|
32
|
+
if self.mirror_metadata is not None:
|
|
33
|
+
out["adjoint_supported"] = self.mirror_metadata["adjoint_supported"]
|
|
34
|
+
out["adjoint_reason"] = self.mirror_metadata["adjoint_reason"]
|
|
35
|
+
if self.mirror_metadata.get("mirror_qasm_ref") is not None:
|
|
36
|
+
out["mirror_qasm_ref"] = self.mirror_metadata["mirror_qasm_ref"]
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def analyze_qasm(
|
|
41
|
+
qasm_path: str,
|
|
42
|
+
*,
|
|
43
|
+
circuit_id: str | None = None,
|
|
44
|
+
circuit_name: str | None = None,
|
|
45
|
+
processor: str | None = None,
|
|
46
|
+
backend: str | None = None,
|
|
47
|
+
precision: str | None = None,
|
|
48
|
+
threshold: float | None = None,
|
|
49
|
+
prediction_artifact_path: str | None = None,
|
|
50
|
+
prediction_allow_schema_mismatch: bool | None = None,
|
|
51
|
+
prediction_fidelity_target: float | None = None,
|
|
52
|
+
prediction_fidelity_metric: str | None = None,
|
|
53
|
+
mirror_artifacts_dir: str | None = None,
|
|
54
|
+
) -> AnalyzeReport:
|
|
55
|
+
ex = extract_example(qasm_path, circuit_id=circuit_id, circuit_name=circuit_name)
|
|
56
|
+
rc = RunConfig.from_raw(
|
|
57
|
+
processor=processor,
|
|
58
|
+
backend=backend,
|
|
59
|
+
precision=precision,
|
|
60
|
+
threshold=threshold,
|
|
61
|
+
prediction_artifact_path=prediction_artifact_path,
|
|
62
|
+
prediction_allow_schema_mismatch=prediction_allow_schema_mismatch,
|
|
63
|
+
prediction_fidelity_target=prediction_fidelity_target,
|
|
64
|
+
prediction_fidelity_metric=prediction_fidelity_metric,
|
|
65
|
+
)
|
|
66
|
+
mirror_metadata = None
|
|
67
|
+
if mirror_artifacts_dir:
|
|
68
|
+
from qcoder.core.qasm2.adjoint_eligibility import check_adjoint_eligibility
|
|
69
|
+
|
|
70
|
+
qasm_text = Path(qasm_path).read_text(encoding="utf-8", errors="replace")
|
|
71
|
+
content_hash = hashlib.sha256(qasm_text.encode("utf-8")).hexdigest()
|
|
72
|
+
eligibility = check_adjoint_eligibility(qasm_text, include_mirror_qasm=True)
|
|
73
|
+
mirror_qasm_ref = None
|
|
74
|
+
if eligibility.adjoint_supported and eligibility.mirror_qasm:
|
|
75
|
+
Path(mirror_artifacts_dir).mkdir(parents=True, exist_ok=True)
|
|
76
|
+
mirror_path = Path(mirror_artifacts_dir) / f"{content_hash}__mirror.qasm"
|
|
77
|
+
mirror_path.write_text(eligibility.mirror_qasm, encoding="utf-8")
|
|
78
|
+
mirror_qasm_ref = f"{content_hash}__mirror.qasm"
|
|
79
|
+
mirror_metadata = {
|
|
80
|
+
"adjoint_supported": eligibility.adjoint_supported,
|
|
81
|
+
"adjoint_reason": eligibility.adjoint_reason,
|
|
82
|
+
"mirror_qasm_ref": mirror_qasm_ref,
|
|
83
|
+
}
|
|
84
|
+
return AnalyzeReport(example=ex, run_config=rc, mirror_metadata=mirror_metadata)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def analyze_qasm_json(
|
|
88
|
+
qasm_path: str,
|
|
89
|
+
*,
|
|
90
|
+
circuit_id: str | None = None,
|
|
91
|
+
circuit_name: str | None = None,
|
|
92
|
+
processor: str | None = None,
|
|
93
|
+
backend: str | None = None,
|
|
94
|
+
precision: str | None = None,
|
|
95
|
+
threshold: float | None = None,
|
|
96
|
+
prediction_artifact_path: str | None = None,
|
|
97
|
+
prediction_allow_schema_mismatch: bool | None = None,
|
|
98
|
+
prediction_fidelity_target: float | None = None,
|
|
99
|
+
prediction_fidelity_metric: str | None = None,
|
|
100
|
+
mirror_artifacts_dir: str | None = None,
|
|
101
|
+
) -> dict:
|
|
102
|
+
"""
|
|
103
|
+
Run analyze and return JSON-serializable dict. When prediction_artifact_path is set,
|
|
104
|
+
load artifact, run prediction engine, and attach prediction_model block next to features.
|
|
105
|
+
When mirror_artifacts_dir is set, compute adjoint eligibility and optionally write mirror QASM.
|
|
106
|
+
"""
|
|
107
|
+
report = analyze_qasm(
|
|
108
|
+
qasm_path,
|
|
109
|
+
circuit_id=circuit_id,
|
|
110
|
+
circuit_name=circuit_name,
|
|
111
|
+
processor=processor,
|
|
112
|
+
backend=backend,
|
|
113
|
+
precision=precision,
|
|
114
|
+
threshold=threshold,
|
|
115
|
+
prediction_artifact_path=prediction_artifact_path,
|
|
116
|
+
prediction_allow_schema_mismatch=prediction_allow_schema_mismatch,
|
|
117
|
+
prediction_fidelity_target=prediction_fidelity_target,
|
|
118
|
+
prediction_fidelity_metric=prediction_fidelity_metric,
|
|
119
|
+
mirror_artifacts_dir=mirror_artifacts_dir,
|
|
120
|
+
)
|
|
121
|
+
out = report.to_json_dict()
|
|
122
|
+
if report.run_config.prediction_artifact_path:
|
|
123
|
+
from qcoder.engines.prediction_model.engine import predict
|
|
124
|
+
|
|
125
|
+
pred = predict(
|
|
126
|
+
out,
|
|
127
|
+
artifact_path=report.run_config.prediction_artifact_path,
|
|
128
|
+
allow_schema_mismatch=report.run_config.prediction_allow_schema_mismatch,
|
|
129
|
+
)
|
|
130
|
+
out["prediction_model"] = pred
|
|
131
|
+
return out
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from qcoder.pipelines.analyze import analyze_qasm
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class BatchItemResult:
|
|
11
|
+
qasm_path: str
|
|
12
|
+
report: dict # same structure as AnalyzeReport.to_json_dict()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def analyze_directory(
|
|
16
|
+
circuits_dir: str,
|
|
17
|
+
*,
|
|
18
|
+
processor: str | None = None,
|
|
19
|
+
backend: str | None = None,
|
|
20
|
+
precision: str | None = None,
|
|
21
|
+
threshold: float | None = None,
|
|
22
|
+
recursive: bool = True,
|
|
23
|
+
pattern: str = "*.qasm",
|
|
24
|
+
fail_fast: bool = True,
|
|
25
|
+
mirror_artifacts_dir: str | None = None,
|
|
26
|
+
) -> list[dict]:
|
|
27
|
+
root = Path(circuits_dir)
|
|
28
|
+
if not root.is_dir():
|
|
29
|
+
raise NotADirectoryError(f"Not a directory: {circuits_dir}")
|
|
30
|
+
|
|
31
|
+
if recursive:
|
|
32
|
+
paths = sorted(root.rglob(pattern), key=str)
|
|
33
|
+
else:
|
|
34
|
+
paths = sorted(root.glob(pattern), key=str)
|
|
35
|
+
|
|
36
|
+
# only files (rglob/glob can return dirs if pattern allows)
|
|
37
|
+
paths = [p for p in paths if p.is_file()]
|
|
38
|
+
|
|
39
|
+
results: list[dict] = []
|
|
40
|
+
for p in paths:
|
|
41
|
+
path_str = str(p)
|
|
42
|
+
try:
|
|
43
|
+
report = analyze_qasm(
|
|
44
|
+
path_str,
|
|
45
|
+
processor=processor,
|
|
46
|
+
backend=backend,
|
|
47
|
+
precision=precision,
|
|
48
|
+
threshold=threshold,
|
|
49
|
+
mirror_artifacts_dir=mirror_artifacts_dir,
|
|
50
|
+
)
|
|
51
|
+
results.append(report.to_json_dict())
|
|
52
|
+
except Exception as e:
|
|
53
|
+
if fail_fast:
|
|
54
|
+
raise
|
|
55
|
+
results.append({"qasm_path": path_str, "error": str(e)})
|
|
56
|
+
return results
|
qcoder/tools/analyze.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class QasmAnalysis:
|
|
10
|
+
path: str
|
|
11
|
+
format: str # "qasm2" | "qasm3" | "unknown"
|
|
12
|
+
lines: int
|
|
13
|
+
bytes: int
|
|
14
|
+
gate_lines: int
|
|
15
|
+
measure_lines: int
|
|
16
|
+
include_lines: int
|
|
17
|
+
comment_lines: int
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> dict:
|
|
20
|
+
return {
|
|
21
|
+
"path": self.path,
|
|
22
|
+
"format": self.format,
|
|
23
|
+
"lines": self.lines,
|
|
24
|
+
"bytes": self.bytes,
|
|
25
|
+
"gate_lines": self.gate_lines,
|
|
26
|
+
"measure_lines": self.measure_lines,
|
|
27
|
+
"include_lines": self.include_lines,
|
|
28
|
+
"comment_lines": self.comment_lines,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _read_text(path: Path) -> str:
|
|
33
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _detect_format(lines: Iterable[str]) -> str:
|
|
37
|
+
for raw in lines:
|
|
38
|
+
s = raw.strip()
|
|
39
|
+
if not s or s.startswith("//"):
|
|
40
|
+
continue
|
|
41
|
+
if s.startswith("OPENQASM 3"):
|
|
42
|
+
return "qasm3"
|
|
43
|
+
if s.startswith("OPENQASM 2") or s.startswith("OPENQASM 2."):
|
|
44
|
+
return "qasm2"
|
|
45
|
+
break
|
|
46
|
+
return "unknown"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def analyze_qasm_file(path: str) -> QasmAnalysis:
|
|
50
|
+
p = Path(path)
|
|
51
|
+
text = _read_text(p)
|
|
52
|
+
lines = text.splitlines()
|
|
53
|
+
|
|
54
|
+
fmt = _detect_format(lines)
|
|
55
|
+
|
|
56
|
+
comment_lines = 0
|
|
57
|
+
include_lines = 0
|
|
58
|
+
gate_lines = 0
|
|
59
|
+
measure_lines = 0
|
|
60
|
+
|
|
61
|
+
for raw in lines:
|
|
62
|
+
s = raw.strip()
|
|
63
|
+
if not s:
|
|
64
|
+
continue
|
|
65
|
+
if s.startswith("//"):
|
|
66
|
+
comment_lines += 1
|
|
67
|
+
continue
|
|
68
|
+
if s.startswith("include "):
|
|
69
|
+
include_lines += 1
|
|
70
|
+
continue
|
|
71
|
+
if "measure" in s and s.endswith(";"):
|
|
72
|
+
measure_lines += 1
|
|
73
|
+
continue
|
|
74
|
+
if s.endswith(";"):
|
|
75
|
+
head = s.split()[0]
|
|
76
|
+
if head not in {"OPENQASM", "qreg", "creg", "qubit", "bit", "let", "def", "gate", "opaque"}:
|
|
77
|
+
gate_lines += 1
|
|
78
|
+
|
|
79
|
+
return QasmAnalysis(
|
|
80
|
+
path=str(p),
|
|
81
|
+
format=fmt,
|
|
82
|
+
lines=len(lines),
|
|
83
|
+
bytes=p.stat().st_size,
|
|
84
|
+
gate_lines=gate_lines,
|
|
85
|
+
measure_lines=measure_lines,
|
|
86
|
+
include_lines=include_lines,
|
|
87
|
+
comment_lines=comment_lines,
|
|
88
|
+
)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analyze whether forward execution time scales linearly with shots for a single circuit
|
|
3
|
+
using one or more run-record JSONL files. Stdlib only.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalize_path(p: str | None) -> str:
|
|
17
|
+
if not p or not str(p).strip():
|
|
18
|
+
return ""
|
|
19
|
+
return os.path.normpath(os.path.abspath(str(p).strip()))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _sha256_file(path: str | Path) -> str:
|
|
23
|
+
h = hashlib.sha256()
|
|
24
|
+
p = Path(path)
|
|
25
|
+
with p.open("rb") as f:
|
|
26
|
+
for chunk in iter(lambda: f.read(65536), b""):
|
|
27
|
+
h.update(chunk)
|
|
28
|
+
return h.hexdigest()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _load_run_records(paths: list[str | Path]) -> list[dict[str, Any]]:
|
|
32
|
+
rows = []
|
|
33
|
+
for path in paths:
|
|
34
|
+
p = Path(path)
|
|
35
|
+
if not p.exists():
|
|
36
|
+
continue
|
|
37
|
+
with p.open("r", encoding="utf-8") as f:
|
|
38
|
+
for line in f:
|
|
39
|
+
line = line.strip()
|
|
40
|
+
if not line:
|
|
41
|
+
continue
|
|
42
|
+
rows.append(json.loads(line))
|
|
43
|
+
return rows
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _safe_int(v: Any) -> int | None:
|
|
47
|
+
if v is None:
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
return int(v)
|
|
51
|
+
except (TypeError, ValueError):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _safe_float(v: Any) -> float | None:
|
|
56
|
+
if v is None:
|
|
57
|
+
return None
|
|
58
|
+
try:
|
|
59
|
+
return float(v)
|
|
60
|
+
except (TypeError, ValueError):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _match_circuit(rows: list[dict], qasm_path: str, content_hash: str, backend_id: str | None, precision: str | None) -> list[dict]:
|
|
65
|
+
qnorm = _normalize_path(qasm_path)
|
|
66
|
+
out = []
|
|
67
|
+
for r in rows:
|
|
68
|
+
if r.get("run_kind") != "forward_runner_execution":
|
|
69
|
+
continue
|
|
70
|
+
if (r.get("content_hash") or "").strip() != content_hash and (not qnorm or _normalize_path(r.get("qasm_path")) != qnorm):
|
|
71
|
+
continue
|
|
72
|
+
if backend_id is not None and (r.get("backend_id") or r.get("backend") or "").strip() != backend_id:
|
|
73
|
+
continue
|
|
74
|
+
if precision is not None and (r.get("precision") or "").strip() != precision:
|
|
75
|
+
continue
|
|
76
|
+
out.append(r)
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _linear_fit(x: list[float], y: list[float]) -> tuple[float, float, float]:
|
|
81
|
+
"""Return (intercept, slope, r_squared). Simple OLS."""
|
|
82
|
+
n = len(x)
|
|
83
|
+
if n < 2:
|
|
84
|
+
return (0.0, 0.0, 0.0)
|
|
85
|
+
sx = sum(x)
|
|
86
|
+
sy = sum(y)
|
|
87
|
+
sx2 = sum(xi * xi for xi in x)
|
|
88
|
+
sxy = sum(xi * yi for xi, yi in zip(x, y))
|
|
89
|
+
denom = n * sx2 - sx * sx
|
|
90
|
+
if denom == 0:
|
|
91
|
+
return (0.0, 0.0, 0.0)
|
|
92
|
+
slope = (n * sxy - sx * sy) / denom
|
|
93
|
+
intercept = (sy - slope * sx) / n
|
|
94
|
+
y_mean = sy / n
|
|
95
|
+
ss_tot = sum((yi - y_mean) ** 2 for yi in y)
|
|
96
|
+
ss_res = sum((yi - (intercept + slope * xi)) ** 2 for xi, yi in zip(x, y))
|
|
97
|
+
r2 = 1.0 - (ss_res / ss_tot) if ss_tot != 0 else 0.0
|
|
98
|
+
return (intercept, slope, r2)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def run_analysis(
|
|
102
|
+
qasm_path: str,
|
|
103
|
+
run_record_paths: list[str | Path],
|
|
104
|
+
*,
|
|
105
|
+
backend_id: str | None = None,
|
|
106
|
+
precision: str | None = None,
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
content_hash = _sha256_file(qasm_path)
|
|
109
|
+
all_rows = _load_run_records(run_record_paths)
|
|
110
|
+
rows = _match_circuit(all_rows, qasm_path, content_hash, backend_id, precision)
|
|
111
|
+
|
|
112
|
+
# Group by (threshold, shots_runner); for each threshold get build row (shots_runner==1)
|
|
113
|
+
by_threshold: dict[int, list[dict]] = {}
|
|
114
|
+
for r in rows:
|
|
115
|
+
thr = _safe_int(r.get("threshold"))
|
|
116
|
+
shots = _safe_int(r.get("shots_runner"))
|
|
117
|
+
if thr is None or shots is None:
|
|
118
|
+
continue
|
|
119
|
+
runner_wall = _safe_float(r.get("runner_wall_s"))
|
|
120
|
+
if runner_wall is None:
|
|
121
|
+
continue
|
|
122
|
+
by_threshold.setdefault(thr, []).append({"shots_runner": shots, "runner_wall_s": runner_wall})
|
|
123
|
+
|
|
124
|
+
table_rows: list[dict[str, Any]] = []
|
|
125
|
+
fits: list[dict[str, Any]] = []
|
|
126
|
+
|
|
127
|
+
for thr in sorted(by_threshold.keys()):
|
|
128
|
+
group = by_threshold[thr]
|
|
129
|
+
build_row = next((g for g in group if g["shots_runner"] == 1), None)
|
|
130
|
+
if build_row is None:
|
|
131
|
+
continue
|
|
132
|
+
build_time_s = build_row["runner_wall_s"]
|
|
133
|
+
hist_rows = [g for g in group if g["shots_runner"] >= 1]
|
|
134
|
+
for g in hist_rows:
|
|
135
|
+
total_runtime_s = g["runner_wall_s"]
|
|
136
|
+
exec_time_s = max(0.0, total_runtime_s - build_time_s)
|
|
137
|
+
shots_hist = g["shots_runner"]
|
|
138
|
+
exec_per_shot = exec_time_s / shots_hist if shots_hist else 0.0
|
|
139
|
+
table_rows.append({
|
|
140
|
+
"threshold": thr,
|
|
141
|
+
"shots_hist": shots_hist,
|
|
142
|
+
"build_time_s": build_time_s,
|
|
143
|
+
"total_runtime_s": total_runtime_s,
|
|
144
|
+
"exec_time_s": exec_time_s,
|
|
145
|
+
"exec_time_per_shot_s": exec_per_shot,
|
|
146
|
+
})
|
|
147
|
+
if len(hist_rows) >= 3:
|
|
148
|
+
x = [g["shots_runner"] for g in hist_rows]
|
|
149
|
+
y = [max(0.0, g["runner_wall_s"] - build_time_s) for g in hist_rows]
|
|
150
|
+
intercept_s, slope_s_per_shot, r2 = _linear_fit(x, y)
|
|
151
|
+
fits.append({
|
|
152
|
+
"threshold": thr,
|
|
153
|
+
"intercept_s": intercept_s,
|
|
154
|
+
"slope_s_per_shot": slope_s_per_shot,
|
|
155
|
+
"r2": r2,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"qasm_path": str(qasm_path),
|
|
160
|
+
"content_hash": content_hash,
|
|
161
|
+
"backend_id": backend_id,
|
|
162
|
+
"precision": precision,
|
|
163
|
+
"table": table_rows,
|
|
164
|
+
"fits": fits,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _print_table(result: dict[str, Any]) -> None:
|
|
169
|
+
table = result.get("table") or []
|
|
170
|
+
fits_by_thr = {f["threshold"]: f for f in result.get("fits") or []}
|
|
171
|
+
if not table:
|
|
172
|
+
print("No rows (no forward_runner_execution with build row per threshold).")
|
|
173
|
+
return
|
|
174
|
+
print(f"qasm: {result['qasm_path']}")
|
|
175
|
+
print(f"content_hash: {result.get('content_hash', '')}")
|
|
176
|
+
if result.get("backend_id") is not None:
|
|
177
|
+
print(f"backend_id: {result['backend_id']}")
|
|
178
|
+
if result.get("precision") is not None:
|
|
179
|
+
print(f"precision: {result['precision']}")
|
|
180
|
+
print("")
|
|
181
|
+
w = 14
|
|
182
|
+
print(
|
|
183
|
+
f"{'threshold':>{w}} {'shots_hist':>{w}} {'build_time_s':>{w}} {'total_runtime_s':>{w}} "
|
|
184
|
+
f"{'exec_time_s':>{w}} {'exec_time_per_shot_s':>{w}}"
|
|
185
|
+
)
|
|
186
|
+
print("-" * (6 * (w + 2)))
|
|
187
|
+
for r in table:
|
|
188
|
+
print(
|
|
189
|
+
f"{r['threshold']:>{w}} {r['shots_hist']:>{w}} {r['build_time_s']:>{w}.6g} {r['total_runtime_s']:>{w}.6g} "
|
|
190
|
+
f"{r['exec_time_s']:>{w}.6g} {r['exec_time_per_shot_s']:>{w}.6e}"
|
|
191
|
+
)
|
|
192
|
+
if fits_by_thr:
|
|
193
|
+
print("")
|
|
194
|
+
print("Per-threshold linear fit (exec_time_s ≈ intercept_s + slope_s_per_shot * shots_hist):")
|
|
195
|
+
print(f"{'threshold':>{w}} {'intercept_s':>{w}} {'slope_s_per_shot':>{w}} {'r2':>{w}}")
|
|
196
|
+
print("-" * (4 * (w + 2)))
|
|
197
|
+
for f in result.get("fits") or []:
|
|
198
|
+
print(
|
|
199
|
+
f"{f['threshold']:>{w}} {f['intercept_s']:>{w}.6g} {f['slope_s_per_shot']:>{w}.6e} {f['r2']:>{w}.4f}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main(argv: list[str] | None = None) -> int:
|
|
204
|
+
ap = argparse.ArgumentParser(
|
|
205
|
+
description="Analyze forward execution time vs shots for one circuit from run-record JSONL."
|
|
206
|
+
)
|
|
207
|
+
ap.add_argument("qasm_path", help="Path to one .qasm file")
|
|
208
|
+
ap.add_argument(
|
|
209
|
+
"--run-record",
|
|
210
|
+
action="append",
|
|
211
|
+
dest="run_records",
|
|
212
|
+
default=[],
|
|
213
|
+
metavar="PATH",
|
|
214
|
+
help="Path to run_record JSONL (repeat for multiple files)",
|
|
215
|
+
)
|
|
216
|
+
ap.add_argument("--backend-id", default=None, help="Restrict to this backend_id")
|
|
217
|
+
ap.add_argument("--precision", default=None, help="Restrict to this precision")
|
|
218
|
+
ap.add_argument("--json", action="store_true", help="Emit JSON output")
|
|
219
|
+
args = ap.parse_args(argv)
|
|
220
|
+
|
|
221
|
+
if not args.run_records:
|
|
222
|
+
print("Error: at least one --run-record is required", file=__import__("sys").stderr)
|
|
223
|
+
return 1
|
|
224
|
+
|
|
225
|
+
result = run_analysis(
|
|
226
|
+
args.qasm_path,
|
|
227
|
+
args.run_records,
|
|
228
|
+
backend_id=args.backend_id or None,
|
|
229
|
+
precision=args.precision or None,
|
|
230
|
+
)
|
|
231
|
+
if args.json:
|
|
232
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
233
|
+
else:
|
|
234
|
+
_print_table(result)
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
raise SystemExit(main())
|
qcoder/tools/batch.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from qcoder.pipelines.batch import analyze_directory
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def analyze_qasm_dir_to_jsonl(
|
|
10
|
+
circuits_dir: str,
|
|
11
|
+
out_path: str,
|
|
12
|
+
*,
|
|
13
|
+
processor: str | None = None,
|
|
14
|
+
backend: str | None = None,
|
|
15
|
+
precision: str | None = None,
|
|
16
|
+
threshold: float | None = None,
|
|
17
|
+
recursive: bool = True,
|
|
18
|
+
pattern: str = "*.qasm",
|
|
19
|
+
fail_fast: bool = True,
|
|
20
|
+
mirror_artifacts_dir: str | None = None,
|
|
21
|
+
) -> int:
|
|
22
|
+
"""Run batch extraction and write one JSON object per line to out_path. Returns record count."""
|
|
23
|
+
results = analyze_directory(
|
|
24
|
+
circuits_dir,
|
|
25
|
+
processor=processor,
|
|
26
|
+
backend=backend,
|
|
27
|
+
precision=precision,
|
|
28
|
+
threshold=threshold,
|
|
29
|
+
recursive=recursive,
|
|
30
|
+
pattern=pattern,
|
|
31
|
+
fail_fast=fail_fast,
|
|
32
|
+
mirror_artifacts_dir=mirror_artifacts_dir,
|
|
33
|
+
)
|
|
34
|
+
path = Path(out_path)
|
|
35
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
with path.open("w", encoding="utf-8") as f:
|
|
37
|
+
for rec in results:
|
|
38
|
+
f.write(json.dumps(rec, sort_keys=True) + "\n")
|
|
39
|
+
return len(results)
|