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,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
@@ -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)