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,106 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+ from typing import Dict, List
6
+
7
+ from .interaction_graph import InteractionGraph
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class CutProfileStats:
12
+ # raw cut array (len = max(n_qubits-1, 0)), natural order only
13
+ cut_profile: tuple[float, ...]
14
+ # summary metrics
15
+ cut_max: float
16
+ cut_mean: float
17
+ cut_std: float
18
+ cut_entropy: float
19
+ n_active_cuts: int
20
+ max_span_in_order: int
21
+
22
+
23
+ def compute_cut_profile_stats(ig: InteractionGraph) -> CutProfileStats:
24
+ """
25
+ Natural-order cut profile.
26
+
27
+ Qubit order: [0, 1, ..., n-1]
28
+ Cut k is between qubits k and k+1, for k=0..n-2.
29
+
30
+ For each interaction edge (u, v) with u < v and weight w:
31
+ it crosses cuts k in [u, v-1]
32
+ add w to each crossed cut bucket.
33
+ """
34
+ n = int(ig.n_qubits)
35
+ m = max(n - 1, 0)
36
+ cut: List[float] = [0.0] * m
37
+
38
+ max_span = 0
39
+
40
+ if m > 0:
41
+ for (u, v), w_int in ig.edges.items():
42
+ # ig guarantees u < v, but keep deterministic safety
43
+ a, b = (u, v) if u <= v else (v, u)
44
+ if a == b:
45
+ continue
46
+ if a < 0 or b < 0 or a >= n or b >= n:
47
+ continue
48
+
49
+ w = float(w_int)
50
+ if w == 0.0:
51
+ continue
52
+
53
+ span = b - a
54
+ if span > max_span:
55
+ max_span = span
56
+
57
+ # crosses cuts a, a+1, ..., b-1
58
+ # (each cut index k corresponds to boundary between k and k+1)
59
+ for k in range(a, b):
60
+ if 0 <= k < m:
61
+ cut[k] += w
62
+
63
+ # metrics over ALL cuts (including zeros)
64
+ if not cut:
65
+ return CutProfileStats(
66
+ cut_profile=tuple(),
67
+ cut_max=0.0,
68
+ cut_mean=0.0,
69
+ cut_std=0.0,
70
+ cut_entropy=0.0,
71
+ n_active_cuts=0,
72
+ max_span_in_order=0,
73
+ )
74
+
75
+ cut_max = max(cut)
76
+ cut_mean = sum(cut) / len(cut)
77
+
78
+ # population std (matches spans.py style: divide by n)
79
+ var = sum((x - cut_mean) ** 2 for x in cut) / len(cut)
80
+ cut_std = var ** 0.5
81
+
82
+ n_active = sum(1 for x in cut if x > 0.0)
83
+
84
+ # normalized Shannon entropy over distribution p_i = cut_i / sum(cut)
85
+ total = sum(cut)
86
+ if total <= 0.0 or len(cut) <= 1:
87
+ cut_entropy = 0.0
88
+ else:
89
+ H = 0.0
90
+ for x in cut:
91
+ if x <= 0.0:
92
+ continue
93
+ p = x / total
94
+ H -= p * math.log(p)
95
+ denom = math.log(len(cut))
96
+ cut_entropy = (H / denom) if denom > 0.0 else 0.0
97
+
98
+ return CutProfileStats(
99
+ cut_profile=tuple(float(x) for x in cut),
100
+ cut_max=float(cut_max),
101
+ cut_mean=float(cut_mean),
102
+ cut_std=float(cut_std),
103
+ cut_entropy=float(cut_entropy),
104
+ n_active_cuts=int(n_active),
105
+ max_span_in_order=int(max_span),
106
+ )
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from ..ir import CircuitIR
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class DepthStats:
10
+ estimated_depth: int # gate statements count (excludes measure/barrier/reset)
11
+ real_depth: int # per-qubit timeline depth proxy
12
+ avg_parallel_gates: float
13
+ parallelism_factor: float
14
+
15
+
16
+ def compute_depth_stats(ir: CircuitIR) -> DepthStats:
17
+ t = [0] * max(ir.n_qubits, 1)
18
+
19
+ gate_ops = 0
20
+ for op in ir.operations:
21
+ if op.is_measure or op.is_barrier or op.is_reset:
22
+ continue
23
+ if not op.qubits:
24
+ continue
25
+ gate_ops += 1
26
+ mx = 0
27
+ for q in op.qubits:
28
+ if 0 <= q < len(t):
29
+ if t[q] > mx:
30
+ mx = t[q]
31
+ nxt = mx + 1
32
+ for q in op.qubits:
33
+ if 0 <= q < len(t):
34
+ t[q] = nxt
35
+
36
+ real_depth = max(t) if t else 0
37
+ estimated_depth = gate_ops
38
+
39
+ avg_parallel = (gate_ops / real_depth) if real_depth > 0 else 0.0
40
+ parallelism_factor = (gate_ops / (real_depth * ir.n_qubits)) if (real_depth > 0 and ir.n_qubits > 0) else 0.0
41
+
42
+ return DepthStats(
43
+ estimated_depth=estimated_depth,
44
+ real_depth=real_depth,
45
+ avg_parallel_gates=float(avg_parallel),
46
+ parallelism_factor=float(parallelism_factor),
47
+ )
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from dataclasses import dataclass
5
+ from typing import List
6
+
7
+ from ..ir import CircuitIR, Operation
8
+
9
+
10
+ def _is_gate_op(op: Operation) -> bool:
11
+ return not (op.is_measure or op.is_barrier or op.is_reset) and len(op.qubits) > 0
12
+
13
+
14
+ def _is_entangling_op(op: Operation) -> bool:
15
+ return _is_gate_op(op) and len(op.qubits) >= 2
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class EntanglingLayerStats:
20
+ entangling_depth: int
21
+ n_entangling_layers: int
22
+ avg_2q_per_entangling_layer: float
23
+ max_2q_per_entangling_layer: int
24
+
25
+
26
+ def compute_entangling_layer_stats(ir: CircuitIR) -> EntanglingLayerStats:
27
+ n = max(ir.n_qubits, 1)
28
+ t: List[int] = [0] * n
29
+
30
+ # layer -> count of 2Q ops in that layer
31
+ layer_2q_count: dict[int, int] = defaultdict(int)
32
+ total_2q_ops = 0
33
+
34
+ for op in ir.operations:
35
+ if not _is_entangling_op(op):
36
+ continue
37
+ qubits = [q for q in op.qubits if 0 <= q < n]
38
+ if not qubits:
39
+ continue
40
+ layer = 1 + max(t[q] for q in qubits)
41
+ for q in qubits:
42
+ t[q] = layer
43
+ if op.arity == 2:
44
+ layer_2q_count[layer] += 1
45
+ total_2q_ops += 1
46
+
47
+ entangling_depth = max(t) if t else 0
48
+ n_entangling_layers = len(layer_2q_count)
49
+ avg_2q = total_2q_ops / max(n_entangling_layers, 1)
50
+ max_2q = max(layer_2q_count.values()) if layer_2q_count else 0
51
+
52
+ return EntanglingLayerStats(
53
+ entangling_depth=entangling_depth,
54
+ n_entangling_layers=n_entangling_layers,
55
+ avg_2q_per_entangling_layer=float(avg_2q),
56
+ max_2q_per_entangling_layer=max_2q,
57
+ )
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from ..ir import CircuitIR, Operation
6
+
7
+ BASIS_CHANGE_NAMES = frozenset({"h", "rx", "ry", "u", "u1", "u2", "u3"})
8
+ DIAGONAL_NAMES = frozenset({"z", "s", "sdg", "t", "tdg", "rz", "u1", "cp", "cz", "ccz", "rzz"})
9
+ T_LIKE_NAMES = frozenset({"t", "tdg"})
10
+ CLIFFORD_LIKE_ANGLE_TOKENS = frozenset({
11
+ "pi/2", "-pi/2", "pi", "-pi", "0", "pi/4", "-pi/4", "pi/8", "-pi/8", "0.0",
12
+ })
13
+
14
+
15
+ def _is_gate_op(op: Operation) -> bool:
16
+ return not (op.is_measure or op.is_barrier or op.is_reset) and len(op.qubits) > 0
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class GateSetStats:
21
+ n_basis_change_ops: int
22
+ basis_change_qubit_coverage: float
23
+ n_diagonal_gate_ops: int
24
+ diagonal_gate_fraction: float
25
+ n_t_like_ops: int
26
+ n_distinct_angles: int
27
+ angle_genericity_ratio: float
28
+ is_certified_diagonal_only: int # 0 or 1
29
+
30
+
31
+ def compute_gate_set_stats(ir: CircuitIR) -> GateSetStats:
32
+ gate_ops = [op for op in ir.operations if _is_gate_op(op)]
33
+ n_gate_ops = len(gate_ops)
34
+
35
+ n_basis_change_ops = 0
36
+ basis_change_qubits: set[int] = set()
37
+ n_diagonal_gate_ops = 0
38
+ n_t_like_ops = 0
39
+ all_diagonal = True
40
+
41
+ for op in gate_ops:
42
+ name = op.name.lower().strip()
43
+ if name in BASIS_CHANGE_NAMES:
44
+ n_basis_change_ops += 1
45
+ basis_change_qubits.update(op.qubits)
46
+ if name in DIAGONAL_NAMES:
47
+ n_diagonal_gate_ops += 1
48
+ else:
49
+ all_diagonal = False
50
+ if name in T_LIKE_NAMES:
51
+ n_t_like_ops += 1
52
+
53
+ n_q = max(ir.n_qubits, 1)
54
+ basis_change_qubit_coverage = len(basis_change_qubits) / n_q if n_q else 0.0
55
+ diagonal_gate_fraction = n_diagonal_gate_ops / max(n_gate_ops, 1)
56
+
57
+ angle_tokens: set[str] = set()
58
+ for op in gate_ops:
59
+ for p in op.params:
60
+ t = p.strip()
61
+ if t:
62
+ angle_tokens.add(t)
63
+
64
+ n_distinct_angles = len(angle_tokens)
65
+ if n_distinct_angles == 0:
66
+ angle_genericity_ratio = 0.0
67
+ else:
68
+ non_clifford = sum(1 for t in angle_tokens if t not in CLIFFORD_LIKE_ANGLE_TOKENS)
69
+ angle_genericity_ratio = non_clifford / n_distinct_angles
70
+
71
+ is_certified_diagonal_only = 1 if (n_gate_ops > 0 and all_diagonal) else 0
72
+
73
+ return GateSetStats(
74
+ n_basis_change_ops=n_basis_change_ops,
75
+ basis_change_qubit_coverage=basis_change_qubit_coverage,
76
+ n_diagonal_gate_ops=n_diagonal_gate_ops,
77
+ diagonal_gate_fraction=diagonal_gate_fraction,
78
+ n_t_like_ops=n_t_like_ops,
79
+ n_distinct_angles=n_distinct_angles,
80
+ angle_genericity_ratio=angle_genericity_ratio,
81
+ is_certified_diagonal_only=is_certified_diagonal_only,
82
+ )
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from ..ir import CircuitIR
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class InteractionGraph:
10
+ n_qubits: int
11
+ edges: dict[tuple[int, int], int] # (u,v)->weight, u<v
12
+
13
+
14
+ def build_interaction_graph(ir: CircuitIR) -> InteractionGraph:
15
+ edges: dict[tuple[int, int], int] = {}
16
+ for op in ir.operations:
17
+ if op.is_measure or op.is_barrier or op.is_reset:
18
+ continue
19
+ if len(op.qubits) < 2:
20
+ continue
21
+ qs = op.qubits
22
+ # connect all pairs for multi-qubit ops
23
+ for i in range(len(qs)):
24
+ for j in range(i + 1, len(qs)):
25
+ u, v = qs[i], qs[j]
26
+ if u == v:
27
+ continue
28
+ a, b = (u, v) if u < v else (v, u)
29
+ edges[(a, b)] = edges.get((a, b), 0) + 1
30
+ return InteractionGraph(n_qubits=ir.n_qubits, edges=edges)
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from collections import Counter, deque
5
+ from dataclasses import dataclass
6
+ from typing import List
7
+
8
+ from .interaction_graph import InteractionGraph
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class InteractionGraphMetrics:
13
+ ig_max_degree: int
14
+ ig_avg_degree: float
15
+ ig_degree_std: float
16
+ ig_degree_entropy: float
17
+ ig_n_components: int
18
+ ig_largest_cc_frac: float
19
+ ig_is_connected: int # 0 or 1
20
+ ig_pair_reuse_hhi: float
21
+ ig_pair_reuse_top1_frac: float
22
+
23
+
24
+ def compute_interaction_graph_metrics(ig: InteractionGraph) -> InteractionGraphMetrics:
25
+ n = max(1, ig.n_qubits) # treat n_qubits <= 0 as 1 node for safety
26
+
27
+ if n == 1:
28
+ return InteractionGraphMetrics(
29
+ ig_max_degree=0,
30
+ ig_avg_degree=0.0,
31
+ ig_degree_std=0.0,
32
+ ig_degree_entropy=0.0,
33
+ ig_n_components=1,
34
+ ig_largest_cc_frac=1.0,
35
+ ig_is_connected=1,
36
+ ig_pair_reuse_hhi=0.0,
37
+ ig_pair_reuse_top1_frac=0.0,
38
+ )
39
+
40
+ # Build adjacency lists (undirected); nodes 0..n-1
41
+ adj: List[List[int]] = [[] for _ in range(n)]
42
+ for (u, v), _ in ig.edges.items():
43
+ if 0 <= u < n and 0 <= v < n and u != v:
44
+ adj[u].append(v)
45
+ adj[v].append(u)
46
+
47
+ # Unweighted degree = number of unique neighbors
48
+ degrees = [len(neighbors) for neighbors in adj]
49
+
50
+ # Components via BFS
51
+ visited = [False] * n
52
+ components: List[List[int]] = []
53
+ for start in range(n):
54
+ if visited[start]:
55
+ continue
56
+ comp: List[int] = []
57
+ q: deque[int] = deque([start])
58
+ visited[start] = True
59
+ while q:
60
+ node = q.popleft()
61
+ comp.append(node)
62
+ for nei in adj[node]:
63
+ if not visited[nei]:
64
+ visited[nei] = True
65
+ q.append(nei)
66
+ components.append(comp)
67
+
68
+ ig_n_components = len(components)
69
+ largest_size = max(len(c) for c in components) if components else 0
70
+ ig_largest_cc_frac = largest_size / n
71
+ ig_is_connected = 1 if ig_n_components == 1 else 0
72
+
73
+ # Degree stats
74
+ ig_max_degree = max(degrees)
75
+ ig_avg_degree = sum(degrees) / n
76
+ variance = sum((d - ig_avg_degree) ** 2 for d in degrees) / n
77
+ ig_degree_std = math.sqrt(variance) if variance >= 0 else 0.0
78
+
79
+ # Normalized degree entropy: distribution over degree values
80
+ # p_k = count(nodes with degree k) / n; H = -sum p_k log(p_k); normalize by log(m), m = distinct degree values
81
+ hist = Counter(degrees)
82
+ distinct_degree_values = len(hist)
83
+ if distinct_degree_values <= 1:
84
+ ig_degree_entropy = 0.0
85
+ else:
86
+ h = 0.0
87
+ for k, count in hist.items():
88
+ p_k = count / n
89
+ if p_k > 0:
90
+ h -= p_k * math.log(p_k)
91
+ ig_degree_entropy = h / math.log(distinct_degree_values)
92
+
93
+ # Weighted interaction-pair reuse concentration from pair counts.
94
+ total_pair_count = sum(int(w) for w in ig.edges.values() if int(w) > 0)
95
+ if total_pair_count > 0:
96
+ probs = [int(w) / total_pair_count for w in ig.edges.values() if int(w) > 0]
97
+ ig_pair_reuse_hhi = sum(p * p for p in probs)
98
+ ig_pair_reuse_top1_frac = max(int(w) for w in ig.edges.values() if int(w) > 0) / total_pair_count
99
+ else:
100
+ ig_pair_reuse_hhi = 0.0
101
+ ig_pair_reuse_top1_frac = 0.0
102
+
103
+ return InteractionGraphMetrics(
104
+ ig_max_degree=ig_max_degree,
105
+ ig_avg_degree=ig_avg_degree,
106
+ ig_degree_std=ig_degree_std,
107
+ ig_degree_entropy=ig_degree_entropy,
108
+ ig_n_components=ig_n_components,
109
+ ig_largest_cc_frac=ig_largest_cc_frac,
110
+ ig_is_connected=ig_is_connected,
111
+ ig_pair_reuse_hhi=ig_pair_reuse_hhi,
112
+ ig_pair_reuse_top1_frac=ig_pair_reuse_top1_frac,
113
+ )
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from ..ir import CircuitIR
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class SpanStats:
10
+ avg_span: float
11
+ max_span: int
12
+ span_std: float
13
+ nearest_neighbor_ratio: float
14
+ long_range_ratio: float # span > 1
15
+ long_range_ratio_early: float
16
+ long_range_ratio_late: float
17
+ avg_span_early: float
18
+ avg_span_late: float
19
+
20
+
21
+ def compute_span_stats(ir: CircuitIR) -> SpanStats:
22
+ gate_ops = [
23
+ op
24
+ for op in ir.operations
25
+ if (not op.is_measure and not op.is_barrier and not op.is_reset and bool(op.qubits))
26
+ ]
27
+ split = len(gate_ops) // 2
28
+ early_gate_ops = gate_ops[:split]
29
+ late_gate_ops = gate_ops[split:]
30
+
31
+ def _half_stats(ops: list) -> tuple[float, float]:
32
+ half_spans: list[int] = []
33
+ for op in ops:
34
+ if len(op.qubits) != 2:
35
+ continue
36
+ a, b = op.qubits
37
+ half_spans.append(abs(a - b))
38
+ if not half_spans:
39
+ return 0.0, 0.0
40
+ n_half = len(half_spans)
41
+ avg_half = sum(half_spans) / n_half
42
+ lr_half = sum(1 for x in half_spans if x > 1) / n_half
43
+ return float(lr_half), float(avg_half)
44
+
45
+ long_range_ratio_early, avg_span_early = _half_stats(early_gate_ops)
46
+ long_range_ratio_late, avg_span_late = _half_stats(late_gate_ops)
47
+
48
+ spans: list[int] = []
49
+ for op in ir.operations:
50
+ if op.is_measure or op.is_barrier or op.is_reset:
51
+ continue
52
+ if len(op.qubits) != 2:
53
+ continue
54
+ a, b = op.qubits
55
+ spans.append(abs(a - b))
56
+
57
+ if not spans:
58
+ return SpanStats(
59
+ 0.0,
60
+ 0,
61
+ 0.0,
62
+ 0.0,
63
+ 0.0,
64
+ long_range_ratio_early,
65
+ long_range_ratio_late,
66
+ avg_span_early,
67
+ avg_span_late,
68
+ )
69
+
70
+ n = len(spans)
71
+ avg = sum(spans) / n
72
+ mx = max(spans)
73
+ var = sum((x - avg) ** 2 for x in spans) / n
74
+ std = var ** 0.5
75
+
76
+ nn = sum(1 for x in spans if x == 1) / n
77
+ lr = sum(1 for x in spans if x > 1) / n
78
+
79
+ return SpanStats(
80
+ float(avg),
81
+ int(mx),
82
+ float(std),
83
+ float(nn),
84
+ float(lr),
85
+ long_range_ratio_early,
86
+ long_range_ratio_late,
87
+ avg_span_early,
88
+ avg_span_late,
89
+ )
@@ -0,0 +1,16 @@
1
+ # Path: src/qcoder/engines/prediction_model/__init__.py
2
+ from __future__ import annotations
3
+
4
+ from .artifact import load_artifact, save_artifact
5
+ from .engine import predict
6
+ from .policy import expected_score_for_rung, score_matrix_value
7
+ from .schema_alignment import align_features_to_artifact
8
+
9
+ __all__ = [
10
+ "align_features_to_artifact",
11
+ "expected_score_for_rung",
12
+ "load_artifact",
13
+ "predict",
14
+ "save_artifact",
15
+ "score_matrix_value",
16
+ ]
@@ -0,0 +1,85 @@
1
+ # Path: src/qcoder/engines/prediction_model/artifact.py
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ ARTIFACT_VERSION = "0.1.0"
10
+
11
+
12
+ def load_artifact(path: str | Path) -> dict[str, Any]:
13
+ """Load artifact from JSON file."""
14
+ p = Path(path)
15
+ text = p.read_text(encoding="utf-8")
16
+ return json.loads(text)
17
+
18
+
19
+ def save_artifact(artifact: dict[str, Any], path: str | Path) -> None:
20
+ """Write artifact to JSON file."""
21
+ p = Path(path)
22
+ p.parent.mkdir(parents=True, exist_ok=True)
23
+ p.write_text(json.dumps(artifact, indent=2, sort_keys=True), encoding="utf-8")
24
+
25
+
26
+ def artifact_structure(
27
+ *,
28
+ qcoder_schema_version: str,
29
+ feature_names: list[str],
30
+ threshold_rungs: list[float],
31
+ threshold_policy: str = "expected_score",
32
+ runtime_feature_names: list[str],
33
+ runtime_intercept: float,
34
+ runtime_coef: list[float],
35
+ # fidelity_curve mode (optional); threshold_grid must be integers
36
+ threshold_stage_mode: str = "ladder",
37
+ threshold_grid: list[int] | list[float] | None = None,
38
+ fidelity_target: float = 0.7,
39
+ fidelity_metric: str = "mirror_fidelity",
40
+ fidelity_curve_value: float = 0.0,
41
+ selection_policy: dict[str, Any] | None = None,
42
+ ) -> dict[str, Any]:
43
+ """
44
+ Build artifact dict with required keys:
45
+ artifact_version, qcoder_schema_version, feature_names,
46
+ threshold_stage (rungs + policy config; mode ladder | fidelity_curve),
47
+ runtime_stage (threshold-derived feature list + model params).
48
+ If mode is missing when loading, treat as "ladder" (backward compatible).
49
+ threshold_grid elements must be integers (validated for fidelity_curve).
50
+ """
51
+ thr_stage: dict[str, Any] = {
52
+ "rungs": list(threshold_rungs),
53
+ "policy": threshold_policy,
54
+ }
55
+ if threshold_stage_mode == "fidelity_curve":
56
+ raw_grid = list(threshold_grid or threshold_rungs)
57
+ int_grid: list[int] = []
58
+ for v in raw_grid:
59
+ i = int(v)
60
+ if i != v:
61
+ raise ValueError(f"threshold_grid must contain integers only, got {v!r}")
62
+ int_grid.append(i)
63
+ thr_stage["mode"] = "fidelity_curve"
64
+ thr_stage["threshold_grid"] = int_grid
65
+ thr_stage["fidelity_target"] = float(fidelity_target)
66
+ thr_stage["fidelity_metric"] = str(fidelity_metric)
67
+ thr_stage["model"] = "ConstantFidelityCurveModel"
68
+ thr_stage["fidelity_curve_value"] = float(fidelity_curve_value)
69
+ thr_stage["selection_policy"] = selection_policy or {
70
+ "type": "min_runtime_subject_to_fidelity",
71
+ "min_fidelity": float(fidelity_target),
72
+ }
73
+ # else: no "mode" key → backward compat as "ladder"
74
+
75
+ return {
76
+ "artifact_version": ARTIFACT_VERSION,
77
+ "qcoder_schema_version": qcoder_schema_version,
78
+ "feature_names": list(feature_names),
79
+ "threshold_stage": thr_stage,
80
+ "runtime_stage": {
81
+ "threshold_derived_feature_names": list(runtime_feature_names),
82
+ "intercept": float(runtime_intercept),
83
+ "coef": list(runtime_coef),
84
+ },
85
+ }