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,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
|
+
}
|