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,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from .features.compute_v0 import FeatureVector, compute_features_v0
7
+ from .ir import CircuitIR
8
+ from .labeling import infer_function
9
+ from .parsers import parse_circuit_file
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class CircuitExample:
14
+ id: str | None
15
+ name: str | None
16
+ function_hint: str # small normalized label
17
+ function_source: str # "name" | "qasm" | "unknown"
18
+ qasm_path: str
19
+ ir: CircuitIR
20
+ global_features: FeatureVector
21
+
22
+
23
+ def extract_example(
24
+ qasm_path: str,
25
+ *,
26
+ circuit_id: str | None = None,
27
+ circuit_name: str | None = None,
28
+ ) -> CircuitExample:
29
+ p = Path(qasm_path)
30
+ ir = parse_circuit_file(str(p))
31
+ fv = compute_features_v0(ir)
32
+
33
+ function_hint, function_source = infer_function(circuit_name, ir)
34
+
35
+ return CircuitExample(
36
+ id=circuit_id,
37
+ name=circuit_name,
38
+ function_hint=function_hint,
39
+ function_source=function_source,
40
+ qasm_path=str(p),
41
+ ir=ir,
42
+ global_features=fv,
43
+ )
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from ..ir import CircuitIR
6
+ from ..reps.cut_profile import compute_cut_profile_stats
7
+ from ..reps.depth import compute_depth_stats
8
+ from ..reps.entangling_layers import compute_entangling_layer_stats
9
+ from ..reps.gate_set_stats import compute_gate_set_stats
10
+ from ..reps.interaction_graph import build_interaction_graph
11
+ from ..reps.interaction_graph_metrics import compute_interaction_graph_metrics
12
+ from ..reps.spans import compute_span_stats
13
+ from .schema_v0 import SCHEMA_V0
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class FeatureVector:
18
+ schema_version: str
19
+ feature_names: tuple[str, ...]
20
+ features: tuple[float, ...]
21
+
22
+ def to_dict(self) -> dict:
23
+ return {
24
+ "schema_version": self.schema_version,
25
+ "feature_names": list(self.feature_names),
26
+ "features": list(self.features),
27
+ }
28
+
29
+
30
+ def compute_features_v0(ir: CircuitIR) -> FeatureVector:
31
+ n_measure = 0
32
+ n_barrier = 0
33
+ n_reset = 0
34
+ n_gate = 0
35
+ n_1q = 0
36
+ n_2q = 0
37
+ n_3p = 0
38
+ n_param = 0
39
+ n_custom = 0
40
+
41
+ for op in ir.operations:
42
+ if op.is_measure:
43
+ n_measure += 1
44
+ continue
45
+ if op.is_barrier:
46
+ n_barrier += 1
47
+ continue
48
+ if op.is_reset:
49
+ n_reset += 1
50
+ continue
51
+
52
+ if not op.qubits:
53
+ continue
54
+
55
+ n_gate += 1
56
+ if op.is_custom:
57
+ n_custom += 1
58
+ if op.params:
59
+ n_param += 1
60
+
61
+ ar = len(op.qubits)
62
+ if ar == 1:
63
+ n_1q += 1
64
+ elif ar == 2:
65
+ n_2q += 1
66
+ else:
67
+ n_3p += 1
68
+
69
+ depth = compute_depth_stats(ir)
70
+ ig = build_interaction_graph(ir)
71
+ spans = compute_span_stats(ir)
72
+ cuts = compute_cut_profile_stats(ig)
73
+ gate_stats = compute_gate_set_stats(ir)
74
+ ig_metrics = compute_interaction_graph_metrics(ig)
75
+ ent_layers = compute_entangling_layer_stats(ir)
76
+
77
+ n_edges = float(len(ig.edges))
78
+ denom = (ir.n_qubits * (ir.n_qubits - 1) / 2.0) if ir.n_qubits >= 2 else 1.0
79
+ edge_density = float(n_edges / denom) if denom > 0 else 0.0
80
+
81
+ values = {
82
+ "n_qubits": float(ir.n_qubits),
83
+ "n_cbits": float(ir.n_cbits),
84
+ "n_ops": float(ir.n_ops),
85
+
86
+ "n_gate_ops": float(n_gate),
87
+ "n_1q_gate_ops": float(n_1q),
88
+ "n_2q_gate_ops": float(n_2q),
89
+ "n_3p_gate_ops": float(n_3p),
90
+ "n_param_ops": float(n_param),
91
+ "n_custom_ops": float(n_custom),
92
+
93
+ "n_measure_ops": float(n_measure),
94
+ "n_barrier_ops": float(n_barrier),
95
+ "n_reset_ops": float(n_reset),
96
+
97
+ "estimated_depth": float(depth.estimated_depth),
98
+ "real_depth": float(depth.real_depth),
99
+ "avg_parallel_gates": float(depth.avg_parallel_gates),
100
+ "parallelism_factor": float(depth.parallelism_factor),
101
+
102
+ "ig_n_edges": float(n_edges),
103
+ "ig_edge_density": float(edge_density),
104
+
105
+ "span_avg": float(spans.avg_span),
106
+ "span_max": float(spans.max_span),
107
+ "span_std": float(spans.span_std),
108
+ "span_nearest_neighbor_ratio": float(spans.nearest_neighbor_ratio),
109
+ "span_long_range_ratio": float(spans.long_range_ratio),
110
+ "span_long_range_ratio_early": float(spans.long_range_ratio_early),
111
+ "span_long_range_ratio_late": float(spans.long_range_ratio_late),
112
+ "span_avg_early": float(spans.avg_span_early),
113
+ "span_avg_late": float(spans.avg_span_late),
114
+
115
+ # Phase 2: cut profile (natural qubit order only)
116
+ "cut_max": float(cuts.cut_max),
117
+ "cut_mean": float(cuts.cut_mean),
118
+ "cut_std": float(cuts.cut_std),
119
+ "cut_entropy": float(cuts.cut_entropy),
120
+ "n_active_cuts": float(cuts.n_active_cuts),
121
+ "max_span_in_order": float(cuts.max_span_in_order),
122
+
123
+ # Quantumness v1: gate set + angles + basis/diagonal
124
+ "n_basis_change_ops": float(gate_stats.n_basis_change_ops),
125
+ "basis_change_qubit_coverage": float(gate_stats.basis_change_qubit_coverage),
126
+ "n_diagonal_gate_ops": float(gate_stats.n_diagonal_gate_ops),
127
+ "diagonal_gate_fraction": float(gate_stats.diagonal_gate_fraction),
128
+ "n_t_like_ops": float(gate_stats.n_t_like_ops),
129
+ "n_distinct_angles": float(gate_stats.n_distinct_angles),
130
+ "angle_genericity_ratio": float(gate_stats.angle_genericity_ratio),
131
+ "is_certified_diagonal_only": float(gate_stats.is_certified_diagonal_only),
132
+
133
+ # Quantumness v1: IG degree/connectivity metrics (unweighted)
134
+ "ig_max_degree": float(ig_metrics.ig_max_degree),
135
+ "ig_avg_degree": float(ig_metrics.ig_avg_degree),
136
+ "ig_degree_std": float(ig_metrics.ig_degree_std),
137
+ "ig_degree_entropy": float(ig_metrics.ig_degree_entropy),
138
+ "ig_n_components": float(ig_metrics.ig_n_components),
139
+ "ig_largest_cc_frac": float(ig_metrics.ig_largest_cc_frac),
140
+ "ig_is_connected": float(ig_metrics.ig_is_connected),
141
+ "ig_pair_reuse_hhi": float(ig_metrics.ig_pair_reuse_hhi),
142
+ "ig_pair_reuse_top1_frac": float(ig_metrics.ig_pair_reuse_top1_frac),
143
+
144
+ # Quantumness v1: entangling depth/layers
145
+ "entangling_depth": float(ent_layers.entangling_depth),
146
+ "n_entangling_layers": float(ent_layers.n_entangling_layers),
147
+ "avg_2q_per_entangling_layer": float(ent_layers.avg_2q_per_entangling_layer),
148
+ "max_2q_per_entangling_layer": float(ent_layers.max_2q_per_entangling_layer),
149
+ }
150
+
151
+ feats = tuple(float(values.get(name, 0.0)) for name in SCHEMA_V0.feature_names)
152
+
153
+ return FeatureVector(
154
+ schema_version=SCHEMA_V0.version,
155
+ feature_names=SCHEMA_V0.feature_names,
156
+ features=feats,
157
+ )
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from qcoder.core.schema import make_schema
4
+
5
+ # Schema 0.4.0: PURE circuit-derived features (no backend/precision/threshold).
6
+ FEATURE_NAMES_V0 = [
7
+ # basic sizes
8
+ "n_qubits",
9
+ "n_cbits",
10
+ "n_ops",
11
+
12
+ # gate arity counts (exclude measure/barrier/reset)
13
+ "n_gate_ops",
14
+ "n_1q_gate_ops",
15
+ "n_2q_gate_ops",
16
+ "n_3p_gate_ops", # 3+ qubit gate ops
17
+ "n_param_ops",
18
+ "n_custom_ops",
19
+
20
+ # control ops
21
+ "n_measure_ops",
22
+ "n_barrier_ops",
23
+ "n_reset_ops",
24
+
25
+ # depth proxies
26
+ "estimated_depth",
27
+ "real_depth",
28
+ "avg_parallel_gates",
29
+ "parallelism_factor",
30
+
31
+ # interaction graph
32
+ "ig_n_edges",
33
+ "ig_edge_density",
34
+
35
+ # span stats (2Q only)
36
+ "span_avg",
37
+ "span_max",
38
+ "span_std",
39
+ "span_nearest_neighbor_ratio",
40
+ "span_long_range_ratio",
41
+
42
+ # Phase 2: cut profile (natural qubit order only)
43
+ "cut_max",
44
+ "cut_mean",
45
+ "cut_std",
46
+ "cut_entropy",
47
+ "n_active_cuts",
48
+ "max_span_in_order",
49
+
50
+ # Quantumness v1: gate set + angles + basis/diagonal
51
+ "n_basis_change_ops",
52
+ "basis_change_qubit_coverage",
53
+ "n_diagonal_gate_ops",
54
+ "diagonal_gate_fraction",
55
+ "n_t_like_ops",
56
+ "n_distinct_angles",
57
+ "angle_genericity_ratio",
58
+ "is_certified_diagonal_only",
59
+
60
+ # Quantumness v1: IG degree/connectivity metrics (unweighted)
61
+ "ig_max_degree",
62
+ "ig_avg_degree",
63
+ "ig_degree_std",
64
+ "ig_degree_entropy",
65
+ "ig_n_components",
66
+ "ig_largest_cc_frac",
67
+ "ig_is_connected",
68
+
69
+ # Quantumness v1: entangling depth/layers
70
+ "entangling_depth",
71
+ "n_entangling_layers",
72
+ "avg_2q_per_entangling_layer",
73
+ "max_2q_per_entangling_layer",
74
+
75
+ # Phase 3: temporal span + weighted pair-reuse features (append-only)
76
+ "span_long_range_ratio_early",
77
+ "span_long_range_ratio_late",
78
+ "span_avg_early",
79
+ "span_avg_late",
80
+ "ig_pair_reuse_hhi",
81
+ "ig_pair_reuse_top1_frac",
82
+ ]
83
+
84
+ SCHEMA_V0 = make_schema(FEATURE_NAMES_V0)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Sequence
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Operation:
9
+ name: str
10
+ qubits: tuple[int, ...]
11
+ params: tuple[str, ...]
12
+ line_index: int
13
+ op_index: int
14
+ is_measure: bool = False
15
+ is_barrier: bool = False
16
+ is_reset: bool = False
17
+ is_custom: bool = False
18
+
19
+ @property
20
+ def arity(self) -> int:
21
+ return len(self.qubits)
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class CircuitIR:
26
+ n_qubits: int
27
+ n_cbits: int
28
+ operations: tuple[Operation, ...]
29
+ qasm_format: str # "qasm2" | "qasm3" | "unknown"
30
+
31
+ @property
32
+ def source_format(self) -> str:
33
+ # Future-facing alias (QIR, QASM3, etc.)
34
+ return self.qasm_format
35
+
36
+ @property
37
+ def n_ops(self) -> int:
38
+ return len(self.operations)
39
+
40
+ def iter_ops(self) -> Sequence[Operation]:
41
+ return self.operations
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from .ir import CircuitIR
4
+
5
+
6
+ def _name_hint(name: str) -> str | None:
7
+ s = name.strip().lower()
8
+ if not s:
9
+ return None
10
+
11
+ # Small, deterministic keyword map (expand later as needed)
12
+ if "qft" in s:
13
+ return "qft"
14
+ if "qaoa" in s:
15
+ return "qaoa"
16
+ if "grover" in s:
17
+ return "grover"
18
+ if "ucc" in s or "vqe" in s:
19
+ return "vqe_ucc"
20
+ if "adder" in s or "arith" in s or "mult" in s:
21
+ return "arithmetic"
22
+ if "qnn" in s or "ansatz" in s or "vqc" in s:
23
+ return "variational"
24
+ return None
25
+
26
+
27
+ def infer_function(circuit_name: str | None, ir: CircuitIR) -> tuple[str, str]:
28
+ """
29
+ Returns: (function_hint, source) where source ∈ {"name", "qasm", "unknown"}.
30
+
31
+ Keep categories small/stable. This is metadata for models to optionally consume.
32
+ """
33
+ if circuit_name:
34
+ h = _name_hint(circuit_name)
35
+ if h is not None:
36
+ return (h, "name")
37
+
38
+ # QASM-derived minimal heuristics (cheap + robust)
39
+ counts: dict[str, int] = {}
40
+ n_gate = 0
41
+ n_param = 0
42
+
43
+ for op in ir.operations:
44
+ if op.is_measure or op.is_barrier or op.is_reset:
45
+ continue
46
+ if not op.qubits:
47
+ continue
48
+ n_gate += 1
49
+ counts[op.name] = counts.get(op.name, 0) + 1
50
+ if op.params:
51
+ n_param += 1
52
+
53
+ ccx = counts.get("ccx", 0)
54
+ cp_like = counts.get("cp", 0) + counts.get("cu1", 0)
55
+ h = counts.get("h", 0)
56
+
57
+ if ccx > 0:
58
+ return ("arithmetic", "qasm")
59
+
60
+ # Very rough QFT signal: many controlled-phase-like ops + some H
61
+ if cp_like >= 3 and h >= 1 and cp_like >= int(0.25 * max(1, n_gate)):
62
+ return ("qft", "qasm")
63
+
64
+ # Variational-ish: lots of parameterized rotations
65
+ if n_gate >= 10 and n_param >= int(0.30 * n_gate):
66
+ return ("variational", "qasm")
67
+
68
+ return ("unknown", "unknown")
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from ..ir import CircuitIR
4
+ from ..qasm2_regex_parser import parse_qasm2_file, parse_qasm2_text
5
+
6
+ __all__ = ["parse_circuit_file", "parse_qasm2_file", "parse_qasm2_text"]
7
+
8
+
9
+ def parse_circuit_file(path: str) -> CircuitIR:
10
+ """
11
+ Parser routing point.
12
+ Today: OpenQASM2 (regex parser).
13
+ Future: OpenQASM3, QIR, other IR adapters.
14
+ """
15
+ ir = parse_qasm2_file(path)
16
+
17
+ # For now: we accept qasm2 and unknown headers (still parseable),
18
+ # but explicitly reject qasm3 until implemented.
19
+ if ir.qasm_format == "qasm3":
20
+ raise NotImplementedError("OpenQASM 3 parsing is not implemented yet.")
21
+ return ir
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from .ir import CircuitIR, Operation
8
+
9
+
10
+ _RE_QREG = re.compile(r"^\s*qreg\s+([A-Za-z_]\w*)\s*\[\s*(\d+)\s*\]\s*;\s*$")
11
+ _RE_CREG = re.compile(r"^\s*creg\s+([A-Za-z_]\w*)\s*\[\s*(\d+)\s*\]\s*;\s*$")
12
+ _RE_OPENQASM2 = re.compile(r"^\s*OPENQASM\s+2(\.\d+)?\s*;\s*$", re.IGNORECASE)
13
+ _RE_OPENQASM3 = re.compile(r"^\s*OPENQASM\s+3(\.\d+)?\s*;\s*$", re.IGNORECASE)
14
+ _RE_INCLUDE = re.compile(r'^\s*include\s+"[^"]+"\s*;\s*$', re.IGNORECASE)
15
+
16
+ # op forms: name(params?) q[i],q[j];
17
+ _RE_OP = re.compile(
18
+ r"^\s*([A-Za-z_]\w*)\s*(\([^)]*\))?\s+(.+?)\s*;\s*$"
19
+ )
20
+
21
+ # qubit ref: q[12]
22
+ _RE_QREF = re.compile(r"([A-Za-z_]\w*)\s*\[\s*(\d+)\s*\]")
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class _Regs:
27
+ qreg_base: dict[str, int]
28
+ qreg_size: dict[str, int]
29
+ n_qubits: int
30
+ n_cbits: int
31
+
32
+
33
+ def _strip_inline_comment(line: str) -> str:
34
+ # QASM2 line comments start with //
35
+ idx = line.find("//")
36
+ return line[:idx] if idx >= 0 else line
37
+
38
+
39
+ def _detect_format(lines: list[str]) -> str:
40
+ for raw in lines:
41
+ s = raw.strip()
42
+ if not s or s.startswith("//"):
43
+ continue
44
+ if _RE_OPENQASM3.match(s):
45
+ return "qasm3"
46
+ if _RE_OPENQASM2.match(s):
47
+ return "qasm2"
48
+ break
49
+ return "unknown"
50
+
51
+
52
+ def _build_regs(lines: list[str]) -> _Regs:
53
+ qreg_base: dict[str, int] = {}
54
+ qreg_size: dict[str, int] = {}
55
+ n_qubits = 0
56
+ n_cbits = 0
57
+
58
+ for raw in lines:
59
+ s = raw.strip()
60
+ if not s:
61
+ continue
62
+
63
+ m = _RE_QREG.match(s)
64
+ if m:
65
+ name = m.group(1)
66
+ size = int(m.group(2))
67
+ if name not in qreg_base:
68
+ qreg_base[name] = n_qubits
69
+ qreg_size[name] = size
70
+ n_qubits += size
71
+ continue
72
+
73
+ m = _RE_CREG.match(s)
74
+ if m:
75
+ size = int(m.group(2))
76
+ n_cbits += size
77
+ continue
78
+
79
+ return _Regs(qreg_base=qreg_base, qreg_size=qreg_size, n_qubits=n_qubits, n_cbits=n_cbits)
80
+
81
+
82
+ def _flatten_qubits(arg_str: str, regs: _Regs) -> tuple[int, ...]:
83
+ # find all qref occurrences in the operand string
84
+ out: list[int] = []
85
+ for reg_name, idx_str in _RE_QREF.findall(arg_str):
86
+ if reg_name not in regs.qreg_base:
87
+ continue
88
+ idx = int(idx_str)
89
+ size = regs.qreg_size[reg_name]
90
+ if 0 <= idx < size:
91
+ out.append(regs.qreg_base[reg_name] + idx)
92
+ return tuple(out)
93
+
94
+
95
+ def _parse_qasm2_lines(lines: list[str]) -> CircuitIR:
96
+ fmt = _detect_format(lines)
97
+ regs = _build_regs(lines)
98
+
99
+ ops: list[Operation] = []
100
+ op_index = 0
101
+
102
+ for line_index, s in enumerate(lines):
103
+ if not s:
104
+ continue
105
+ if _RE_OPENQASM2.match(s) or _RE_OPENQASM3.match(s) or _RE_INCLUDE.match(s):
106
+ continue
107
+ if _RE_QREG.match(s) or _RE_CREG.match(s):
108
+ continue
109
+
110
+ # recognize common non-gate statements
111
+ low = s.lower()
112
+ if low.startswith("barrier"):
113
+ qubits = _flatten_qubits(s, regs)
114
+ ops.append(Operation("barrier", qubits, (), line_index, op_index, is_barrier=True))
115
+ op_index += 1
116
+ continue
117
+ if low.startswith("reset"):
118
+ qubits = _flatten_qubits(s, regs)
119
+ ops.append(Operation("reset", qubits, (), line_index, op_index, is_reset=True))
120
+ op_index += 1
121
+ continue
122
+ if low.startswith("measure"):
123
+ qubits = _flatten_qubits(s, regs)
124
+ ops.append(Operation("measure", qubits, (), line_index, op_index, is_measure=True))
125
+ op_index += 1
126
+ continue
127
+
128
+ m = _RE_OP.match(s)
129
+ if not m:
130
+ # unknown construct; count as a custom op with no qubits
131
+ ops.append(Operation("custom", (), (), line_index, op_index, is_custom=True))
132
+ op_index += 1
133
+ continue
134
+
135
+ name = m.group(1)
136
+ params_raw = (m.group(2) or "").strip()
137
+ operands = (m.group(3) or "").strip()
138
+
139
+ params: tuple[str, ...] = ()
140
+ if params_raw.startswith("(") and params_raw.endswith(")"):
141
+ inside = params_raw[1:-1].strip()
142
+ if inside:
143
+ params = tuple(x.strip() for x in inside.split(",") if x.strip())
144
+
145
+ qubits = _flatten_qubits(operands, regs)
146
+
147
+ is_custom = False
148
+ if not name or (not qubits and name not in {"id"}):
149
+ is_custom = True
150
+ name = "custom"
151
+
152
+ ops.append(Operation(name=name.lower(), qubits=qubits, params=params, line_index=line_index, op_index=op_index, is_custom=is_custom))
153
+ op_index += 1
154
+
155
+ return CircuitIR(
156
+ n_qubits=regs.n_qubits,
157
+ n_cbits=regs.n_cbits,
158
+ operations=tuple(ops),
159
+ qasm_format=fmt,
160
+ )
161
+
162
+
163
+ def parse_qasm2_text(qasm_text: str, *, source_label: str | None = None) -> CircuitIR:
164
+ """
165
+ Parse OpenQASM 2 source from a string. Same IR as parse_qasm2_file for identical content.
166
+
167
+ Does not raise for OpenQASM 3 headers; consumers that need rejection should use
168
+ parse_circuit_file() or check ir.qasm_format.
169
+
170
+ source_label: optional provenance string for future diagnostics (unused today).
171
+ """
172
+ _ = source_label # reserved for error-context diagnostics
173
+ raw_lines = qasm_text.splitlines()
174
+ lines: list[str] = []
175
+ for raw in raw_lines:
176
+ stripped = _strip_inline_comment(raw).strip()
177
+ lines.append(stripped)
178
+ return _parse_qasm2_lines(lines)
179
+
180
+
181
+ def parse_qasm2_file(path: str) -> CircuitIR:
182
+ p = Path(path)
183
+ text = p.read_text(encoding="utf-8", errors="replace")
184
+ return parse_qasm2_text(text)