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,209 @@
1
+ # Path: src/qcoder/engines/prediction_model/engine.py
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ from typing import Any
6
+
7
+ from .artifact import load_artifact
8
+ from .models import (
9
+ ConstantFidelityCurveModel,
10
+ ConstantProbaModel,
11
+ LinearRuntimeModel,
12
+ expm1,
13
+ log1p,
14
+ )
15
+ from .policy import choose_rung_expected_score
16
+ from .schema_alignment import align_features_to_artifact, parse_feature_bundle
17
+
18
+
19
+ def predict(
20
+ report_json_dict: dict[str, Any],
21
+ artifact: dict[str, Any] | None = None,
22
+ artifact_path: str | None = None,
23
+ *,
24
+ allow_schema_mismatch: bool = False,
25
+ ) -> dict[str, Any]:
26
+ """
27
+ Predict from analyze report (or any dict with "features" bundle).
28
+ Returns prediction_model block: predicted_threshold_min, predicted_forward_wall_s, thr_pred, thr_conf, etc.
29
+ Enforces no monotone assumptions. Strict schema_version match unless allow_schema_mismatch.
30
+ For mode "fidelity_curve", uses run_config.prediction_fidelity_target and prediction_fidelity_metric
31
+ when present; otherwise artifact defaults. If both missing in fidelity_curve mode, raises.
32
+ """
33
+ if artifact is None and artifact_path:
34
+ artifact = load_artifact(artifact_path)
35
+ if artifact is None:
36
+ raise ValueError("either artifact or artifact_path must be provided")
37
+
38
+ features_bundle = report_json_dict.get("features")
39
+ if not isinstance(features_bundle, dict):
40
+ raise TypeError("report must contain 'features' dict (schema_version, feature_names, features)")
41
+
42
+ schema_version, _, _ = parse_feature_bundle(features_bundle)
43
+ art_schema = artifact.get("qcoder_schema_version") or ""
44
+ if not allow_schema_mismatch and schema_version != art_schema:
45
+ raise ValueError(
46
+ f"schema_version mismatch: report has {schema_version!r}, artifact expects {art_schema!r}. "
47
+ "Set allow_schema_mismatch=True to override."
48
+ )
49
+
50
+ art_features = artifact.get("feature_names") or []
51
+ x = align_features_to_artifact(features_bundle, art_features)
52
+ run_config = report_json_dict.get("run_config") or {}
53
+
54
+ thr_stage = artifact.get("threshold_stage") or {}
55
+ mode = thr_stage.get("mode", "ladder") # backward compat: missing -> ladder
56
+
57
+ rt_stage = artifact.get("runtime_stage") or {}
58
+ rt_intercept = rt_stage.get("intercept", 0.0)
59
+ rt_coef = rt_stage.get("coef", [0.0])
60
+ rt_model = LinearRuntimeModel(intercept=rt_intercept, coef=rt_coef)
61
+
62
+ def _runtime_for_threshold(thr: int | float) -> float:
63
+ thr_val = int(thr) if isinstance(thr, (int, float)) else 0
64
+ log2_thr = math.log2(thr_val) if thr_val > 0 else 0.0
65
+ rt_x = [log2_thr]
66
+ for _ in range(len(rt_coef) - 1):
67
+ rt_x.append(0.0)
68
+ rt_x = rt_x[: len(rt_coef)]
69
+ y_log = rt_model.predict_log1p_runtime(rt_x)
70
+ return max(0.0, expm1(y_log) if y_log >= 0 else 0.0)
71
+
72
+ if mode == "fidelity_curve":
73
+ return _predict_fidelity_curve(
74
+ report_json_dict,
75
+ artifact,
76
+ run_config,
77
+ art_features,
78
+ x,
79
+ rt_model,
80
+ rt_coef,
81
+ _runtime_for_threshold,
82
+ )
83
+
84
+ # Stage 1 — ladder (no monotone constraint; use expected-score policy)
85
+ rungs = thr_stage.get("rungs") or [0.0]
86
+ n_rungs = len(rungs)
87
+ thr_model = ConstantProbaModel(n_rungs=n_rungs)
88
+ probs = thr_model.predict_proba()
89
+ chosen_idx = choose_rung_expected_score(probs)
90
+ thr_pred = rungs[chosen_idx] if chosen_idx < len(rungs) else rungs[-1]
91
+ thr_conf = probs[chosen_idx] if probs else 0.0
92
+ thr_entropy = _entropy(probs)
93
+ thr_expected_rung = sum(i * p for i, p in enumerate(probs))
94
+ predicted_forward_wall_s = _runtime_for_threshold(thr_pred)
95
+
96
+ return {
97
+ "predicted_threshold_min": thr_pred,
98
+ "predicted_forward_wall_s": predicted_forward_wall_s,
99
+ "thr_pred": thr_pred,
100
+ "thr_conf": thr_conf,
101
+ "thr_entropy": thr_entropy,
102
+ "thr_expected_rung": thr_expected_rung,
103
+ }
104
+
105
+
106
+ def _predict_fidelity_curve(
107
+ report_json_dict: dict[str, Any],
108
+ artifact: dict[str, Any],
109
+ run_config: dict[str, Any],
110
+ art_features: list[str],
111
+ x: list[float],
112
+ rt_model: LinearRuntimeModel,
113
+ rt_coef: list[float],
114
+ runtime_for_threshold: Any,
115
+ ) -> dict[str, Any]:
116
+ thr_stage = artifact.get("threshold_stage") or {}
117
+ raw_grid = thr_stage.get("threshold_grid") or thr_stage.get("rungs") or [0]
118
+ threshold_grid = [int(v) for v in raw_grid]
119
+
120
+ fidelity_target = run_config.get("prediction_fidelity_target")
121
+ if fidelity_target is None:
122
+ fidelity_target = thr_stage.get("fidelity_target")
123
+ run_config_metric = run_config.get("prediction_fidelity_metric")
124
+ artifact_metric = thr_stage.get("fidelity_metric")
125
+ if run_config_metric is not None and run_config_metric != "":
126
+ fidelity_metric = str(run_config_metric)
127
+ fidelity_metric_explicit = True
128
+ elif artifact_metric is not None and artifact_metric != "":
129
+ fidelity_metric = str(artifact_metric)
130
+ fidelity_metric_explicit = "fidelity_metric" in thr_stage
131
+ else:
132
+ fidelity_metric = "mirror_fidelity"
133
+ fidelity_metric_explicit = False
134
+ if fidelity_target is None:
135
+ raise ValueError(
136
+ "fidelity_curve mode requires prediction_fidelity_target (RunConfig or artifact threshold_stage.fidelity_target)"
137
+ )
138
+
139
+ selection_policy = thr_stage.get("selection_policy") or {
140
+ "type": "min_runtime_subject_to_fidelity",
141
+ "min_fidelity": float(fidelity_target),
142
+ }
143
+ min_fidelity = selection_policy.get("min_fidelity", fidelity_target)
144
+ no_feasible_policy = str(selection_policy.get("type", "min_runtime_subject_to_fidelity"))
145
+
146
+ # Fidelity curve model (stdlib placeholder)
147
+ fid_model = ConstantFidelityCurveModel(n_points=len(threshold_grid), value=thr_stage.get("fidelity_curve_value", 0.0))
148
+ predicted_fidelity = fid_model.predict_curve()
149
+ if len(predicted_fidelity) < len(threshold_grid):
150
+ predicted_fidelity = predicted_fidelity + [0.0] * (len(threshold_grid) - len(predicted_fidelity))
151
+ predicted_fidelity = predicted_fidelity[: len(threshold_grid)]
152
+
153
+ best_fidelity = max(predicted_fidelity) if predicted_fidelity else 0.0
154
+ candidate_indices = [k for k in range(len(threshold_grid)) if predicted_fidelity[k] >= min_fidelity]
155
+ feasible = len(candidate_indices) > 0
156
+ warnings: list[str] = []
157
+ if not fidelity_metric_explicit and fidelity_metric == "mirror_fidelity":
158
+ warnings.append("fidelity_metric_defaulted_to_mirror_fidelity")
159
+
160
+ if not candidate_indices:
161
+ chosen_idx = max(range(len(threshold_grid)), key=lambda k: predicted_fidelity[k] if k < len(predicted_fidelity) else 0.0)
162
+ chosen_threshold = threshold_grid[chosen_idx]
163
+ warnings.append("no_threshold_meets_fidelity_target")
164
+ else:
165
+ best_idx = candidate_indices[0]
166
+ best_runtime = runtime_for_threshold(threshold_grid[best_idx])
167
+ for k in candidate_indices[1:]:
168
+ r = runtime_for_threshold(threshold_grid[k])
169
+ if r < best_runtime:
170
+ best_runtime = r
171
+ best_idx = k
172
+ chosen_idx = best_idx
173
+ chosen_threshold = threshold_grid[chosen_idx]
174
+
175
+ best_runtime_s = runtime_for_threshold(chosen_threshold)
176
+
177
+ fidelity_curve_block = {
178
+ "threshold_grid": list(threshold_grid),
179
+ "predicted_fidelity": list(predicted_fidelity),
180
+ "fidelity_target": float(fidelity_target),
181
+ "fidelity_metric": str(fidelity_metric),
182
+ "candidate_thresholds": [threshold_grid[k] for k in candidate_indices],
183
+ "chosen_threshold": chosen_threshold,
184
+ "selection_policy": selection_policy,
185
+ "feasible": feasible,
186
+ "no_feasible_policy": no_feasible_policy,
187
+ "best_fidelity": float(best_fidelity),
188
+ "best_runtime_s": float(best_runtime_s),
189
+ }
190
+
191
+ return {
192
+ "predicted_threshold_min": chosen_threshold,
193
+ "predicted_forward_wall_s": best_runtime_s,
194
+ "thr_pred": chosen_threshold,
195
+ "thr_conf": 0.0,
196
+ "thr_entropy": 0.0,
197
+ "thr_expected_rung": 0.0,
198
+ "fidelity_curve": fidelity_curve_block,
199
+ "warnings": warnings,
200
+ }
201
+
202
+
203
+ def _entropy(probs: list[float]) -> float:
204
+ """Shannon entropy of discrete distribution."""
205
+ h = 0.0
206
+ for p in probs:
207
+ if p > 0:
208
+ h -= p * math.log2(p)
209
+ return h
@@ -0,0 +1,62 @@
1
+ # Path: src/qcoder/engines/prediction_model/models.py
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class ConstantProbaModel:
11
+ """Constant (uniform) probability over rungs. Stdlib-only."""
12
+
13
+ n_rungs: int
14
+
15
+ def predict_proba(self) -> list[float]:
16
+ if self.n_rungs <= 0:
17
+ return []
18
+ p = 1.0 / self.n_rungs
19
+ return [p] * self.n_rungs
20
+
21
+
22
+ @dataclass
23
+ class LinearRuntimeModel:
24
+ """
25
+ Tiny stdlib linear regression: y = intercept + dot(coef, x).
26
+ No monotone constraints (runtime vs threshold may be non-monotonic).
27
+ """
28
+
29
+ intercept: float
30
+ coef: list[float]
31
+
32
+ def predict(self, x: list[float]) -> float:
33
+ if len(x) != len(self.coef):
34
+ return self.intercept
35
+ return self.intercept + sum(c * v for c, v in zip(self.coef, x))
36
+
37
+ def predict_log1p_runtime(self, x: list[float]) -> float:
38
+ """Target is log1p(runtime_seconds). No monotone constraint."""
39
+ return self.predict(x)
40
+
41
+
42
+ def expm1(x: float) -> float:
43
+ """exp(x) - 1, stdlib-only."""
44
+ return math.exp(x) - 1.0
45
+
46
+
47
+ def log1p(x: float) -> float:
48
+ """log(1 + x), stdlib."""
49
+ return math.log1p(x)
50
+
51
+
52
+ @dataclass
53
+ class ConstantFidelityCurveModel:
54
+ """Stdlib placeholder: returns a flat fidelity curve [value] * n_points."""
55
+
56
+ n_points: int
57
+ value: float = 0.0
58
+
59
+ def predict_curve(self) -> list[float]:
60
+ if self.n_points <= 0:
61
+ return []
62
+ return [self.value] * self.n_points
@@ -0,0 +1,45 @@
1
+ # Path: src/qcoder/engines/prediction_model/policy.py
2
+ from __future__ import annotations
3
+
4
+ import math
5
+
6
+
7
+ def score_matrix_value(pred_rung: int, true_rung: int) -> float:
8
+ """
9
+ Decision matrix: if pred < true → 0; if pred >= true → 2^-(pred - true).
10
+ Rungs are non-negative integer indices.
11
+ """
12
+ if pred_rung < true_rung:
13
+ return 0.0
14
+ return math.pow(2.0, -(pred_rung - true_rung))
15
+
16
+
17
+ def expected_score_for_rung(probs: list[float], chosen_rung: int) -> float:
18
+ """
19
+ Expected score when we choose chosen_rung and true rung is distributed per probs.
20
+ E[score] = sum_t probs[t] * score_matrix_value(chosen_rung, t).
21
+ """
22
+ n = len(probs)
23
+ total = 0.0
24
+ for t in range(n):
25
+ if t < len(probs) and probs[t] > 0:
26
+ total += probs[t] * score_matrix_value(chosen_rung, t)
27
+ return total
28
+
29
+
30
+ def choose_rung_expected_score(probs: list[float]) -> int:
31
+ """
32
+ Expected-score decision policy (do NOT use argmax of probs).
33
+ Choose rung r that maximizes expected score sum_t probs[t] * score_matrix_value(r, t).
34
+ """
35
+ n = len(probs)
36
+ if n == 0:
37
+ return 0
38
+ best_r = 0
39
+ best_score = expected_score_for_rung(probs, 0)
40
+ for r in range(1, n):
41
+ s = expected_score_for_rung(probs, r)
42
+ if s > best_score:
43
+ best_score = s
44
+ best_r = r
45
+ return best_r
@@ -0,0 +1,41 @@
1
+ # Path: src/qcoder/engines/prediction_model/schema_alignment.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+
7
+ def parse_feature_bundle(bundle: dict[str, Any]) -> tuple[str, list[str], list[float]]:
8
+ """
9
+ Parse the actual feature bundle shape: schema_version, feature_names, features.
10
+ Returns (schema_version, feature_names, features).
11
+ Raises if bundle is invalid.
12
+ """
13
+ if not isinstance(bundle, dict):
14
+ raise TypeError("feature bundle must be a dict")
15
+ schema_version = bundle.get("schema_version")
16
+ if not isinstance(schema_version, str):
17
+ schema_version = "unknown"
18
+ names = bundle.get("feature_names")
19
+ vals = bundle.get("features")
20
+ if not isinstance(names, list) or not isinstance(vals, list):
21
+ raise TypeError("feature bundle must contain feature_names and features lists")
22
+ if len(names) != len(vals):
23
+ raise ValueError("feature_names and features length mismatch")
24
+ name_list = [str(n) for n in names]
25
+ val_list = [float(v) for v in vals]
26
+ return schema_version, name_list, val_list
27
+
28
+
29
+ def align_features_to_artifact(
30
+ bundle: dict[str, Any],
31
+ artifact_feature_names: list[str],
32
+ *,
33
+ default_value: float = 0.0,
34
+ ) -> list[float]:
35
+ """
36
+ Align by name to artifact feature list.
37
+ Returns a list of floats in artifact_feature_names order; uses default_value for missing names.
38
+ """
39
+ _, names, values = parse_feature_bundle(bundle)
40
+ name_to_val = dict(zip(names, values))
41
+ return [name_to_val.get(name, default_value) for name in artifact_feature_names]
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from .scorer import QuantumnessReport, score_from_feature_vector
4
+
5
+ __all__ = [
6
+ "QuantumnessReport",
7
+ "score_from_feature_vector",
8
+ ]
@@ -0,0 +1,254 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ import math
5
+ from typing import Any, Mapping
6
+
7
+
8
+ def _clamp01(x: float) -> float:
9
+ if x < 0.0:
10
+ return 0.0
11
+ if x > 1.0:
12
+ return 1.0
13
+ return float(x)
14
+
15
+
16
+ def _safe_float(x: Any, default: float = 0.0) -> float:
17
+ try:
18
+ if x is None:
19
+ return default
20
+ return float(x)
21
+ except Exception:
22
+ return default
23
+
24
+
25
+ def _safe_int(x: Any, default: int = 0) -> int:
26
+ try:
27
+ if x is None:
28
+ return default
29
+ return int(x)
30
+ except Exception:
31
+ return default
32
+
33
+
34
+ def _to_feature_dict(feature_vector: Any) -> dict[str, Any]:
35
+ """
36
+ Accept either:
37
+ A) {"schema_version": str, "feature_names": [...], "features": [...]}
38
+ B) {"n_qubits": 2, ...} (already name->value)
39
+
40
+ Return name->value dict.
41
+
42
+ Deterministic, dependency-free, and tolerant of extra keys.
43
+ """
44
+ if not isinstance(feature_vector, Mapping):
45
+ raise TypeError("feature_vector must be a mapping (dict-like)")
46
+
47
+ # Case A: bundle form
48
+ if "feature_names" in feature_vector and "features" in feature_vector:
49
+ names = feature_vector.get("feature_names")
50
+ vals = feature_vector.get("features")
51
+ if not isinstance(names, list) or not isinstance(vals, list):
52
+ raise TypeError("feature_vector bundle must contain lists: feature_names, features")
53
+ if len(names) != len(vals):
54
+ raise ValueError("feature_vector bundle feature_names/features length mismatch")
55
+ out: dict[str, Any] = {}
56
+ for k, v in zip(names, vals):
57
+ # Keep last occurrence if duplicates exist (should not happen by schema tests)
58
+ out[str(k)] = v
59
+ return out
60
+
61
+ # Case B: assume name->value dict, but strip known non-feature keys if present
62
+ out = dict(feature_vector)
63
+ out.pop("schema_version", None)
64
+ return out
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class QuantumnessReport:
69
+ """
70
+ Minimal, explainable quantumness scoring output.
71
+
72
+ This is NOT part of the feature schema; it is a separate engine output.
73
+ """
74
+
75
+ # The feature schema version this scorer consumed (e.g., "0.3.0").
76
+ feature_schema_version: str
77
+
78
+ # Final score in [0, 1].
79
+ score: float
80
+
81
+ # Stage 1 hard reasons (conservative no-chance filters).
82
+ triggered_reasons: list[str] = field(default_factory=list)
83
+
84
+ # Stage 2 subscores in [0, 1].
85
+ subscores: dict[str, float] = field(default_factory=dict)
86
+
87
+ # Small dict of feature values / derived metrics supporting explainability.
88
+ supporting_metrics: dict[str, Any] = field(default_factory=dict)
89
+
90
+ def to_json_dict(self) -> dict[str, Any]:
91
+ # Deterministic output keys; dict key ordering in Python is insertion-ordered.
92
+ return {
93
+ "feature_schema_version": self.feature_schema_version,
94
+ "score": float(self.score),
95
+ "triggered_reasons": list(self.triggered_reasons),
96
+ "subscores": dict(self.subscores),
97
+ "supporting_metrics": dict(self.supporting_metrics),
98
+ }
99
+
100
+
101
+ def score_from_feature_vector(feature_vector: Mapping[str, Any]) -> QuantumnessReport:
102
+ """
103
+ Score quantumness from an extracted feature vector.
104
+
105
+ - Deterministic
106
+ - Dependency-free
107
+ - Independent of RunConfig
108
+ - Model-agnostic
109
+ - Consumes a stable subset of existing extracted features
110
+ """
111
+ # Determine feature schema version if provided (bundle-form).
112
+ feature_schema_version = "unknown"
113
+ if isinstance(feature_vector, Mapping):
114
+ v = feature_vector.get("schema_version")
115
+ if isinstance(v, str) and v:
116
+ feature_schema_version = v
117
+
118
+ fv = _to_feature_dict(feature_vector)
119
+
120
+ # --- Pull the (documented) subset we use ---
121
+ n_qubits = _safe_int(fv.get("n_qubits"))
122
+ n_gate_ops = _safe_int(fv.get("n_gate_ops"))
123
+ n_2q = _safe_int(fv.get("n_2q_gate_ops"))
124
+ n_3p = _safe_int(fv.get("n_3p_gate_ops"))
125
+
126
+ ent_depth = _safe_int(fv.get("entangling_depth"))
127
+ n_ent_layers = _safe_int(fv.get("n_entangling_layers"))
128
+ avg_2q_per_layer = _safe_float(fv.get("avg_2q_per_entangling_layer"))
129
+
130
+ is_diag_only = _safe_int(fv.get("is_certified_diagonal_only"))
131
+ n_basis_change = _safe_int(fv.get("n_basis_change_ops"))
132
+ diag_frac = _safe_float(fv.get("diagonal_gate_fraction"))
133
+
134
+ n_t_like = _safe_int(fv.get("n_t_like_ops"))
135
+ n_distinct_angles = _safe_int(fv.get("n_distinct_angles"))
136
+ angle_genericity_ratio = _safe_float(fv.get("angle_genericity_ratio"))
137
+
138
+ basis_change_qubit_coverage = _safe_float(fv.get("basis_change_qubit_coverage"))
139
+
140
+ ig_n_components = _safe_int(fv.get("ig_n_components"))
141
+ ig_largest_cc_frac = _safe_float(fv.get("ig_largest_cc_frac"))
142
+ ig_degree_entropy = _safe_float(fv.get("ig_degree_entropy"))
143
+
144
+ # These are used only for normalization; safe defaults if absent.
145
+ real_depth = _safe_int(fv.get("real_depth"), default=0)
146
+
147
+ supporting: dict[str, Any] = {
148
+ "n_qubits": n_qubits,
149
+ "n_gate_ops": n_gate_ops,
150
+ "n_2q_gate_ops": n_2q,
151
+ "n_3p_gate_ops": n_3p,
152
+ "entangling_depth": ent_depth,
153
+ "n_entangling_layers": n_ent_layers,
154
+ "avg_2q_per_entangling_layer": float(avg_2q_per_layer),
155
+ "is_certified_diagonal_only": is_diag_only,
156
+ "n_basis_change_ops": n_basis_change,
157
+ "diagonal_gate_fraction": float(diag_frac),
158
+ "n_t_like_ops": n_t_like,
159
+ "n_distinct_angles": n_distinct_angles,
160
+ "angle_genericity_ratio": float(angle_genericity_ratio),
161
+ "basis_change_qubit_coverage": float(basis_change_qubit_coverage),
162
+ "ig_n_components": ig_n_components,
163
+ "ig_largest_cc_frac": float(ig_largest_cc_frac),
164
+ "ig_degree_entropy": float(ig_degree_entropy),
165
+ "real_depth": real_depth,
166
+ }
167
+
168
+ # -------------------------------
169
+ # Stage 1: conservative no-chance filters
170
+ # -------------------------------
171
+ reasons: list[str] = []
172
+
173
+ # No entanglement proxy: no 2Q/3+Q gates OR entangling depth/layers both zero.
174
+ no_entanglement = ((n_2q + n_3p) == 0) or (ent_depth == 0 and n_ent_layers == 0)
175
+ if no_entanglement:
176
+ reasons.append("no_entanglement")
177
+
178
+ # Certified diagonal-only: current schema already supports a certified flag.
179
+ # We only trigger when certified, and basis change is absent (strong evidence phases won't matter for Z-basis outcomes).
180
+ diagonal_only = (is_diag_only == 1) and (n_basis_change == 0) and (diag_frac >= 0.999)
181
+ if diagonal_only:
182
+ reasons.append("diagonal_only")
183
+
184
+ if reasons:
185
+ return QuantumnessReport(
186
+ feature_schema_version=feature_schema_version,
187
+ score=0.0,
188
+ triggered_reasons=reasons,
189
+ subscores={},
190
+ supporting_metrics=supporting,
191
+ )
192
+
193
+ # -------------------------------
194
+ # Stage 2: ingredient subscores (interpretable)
195
+ # -------------------------------
196
+ # MagicScore: non-Clifford proxies (simple conservative normalization).
197
+ denom_gate = max(1, n_gate_ops)
198
+ t_frac = min(1.0, n_t_like / float(denom_gate))
199
+ angle_score = _clamp01(angle_genericity_ratio) # already ratio-like
200
+ # Distinct angles: normalize by a mild scale to avoid overweighting.
201
+ distinct_scale = max(1, 2 * max(1, n_qubits))
202
+ distinct_score = min(1.0, n_distinct_angles / float(distinct_scale))
203
+ magic = _clamp01(0.5 * t_frac + 0.3 * angle_score + 0.2 * distinct_score)
204
+
205
+ # EntangleScore: depth + layer density + connectivity.
206
+ denom_depth = max(1, real_depth) if real_depth > 0 else max(1, ent_depth)
207
+ depth_ratio = min(1.0, ent_depth / float(denom_depth))
208
+ # Layer density: how many entangling ops per entangling layer relative to ~n_qubits/2.
209
+ target_2q_per_layer = max(1.0, n_qubits / 2.0) if n_qubits > 0 else 1.0
210
+ layer_density = min(1.0, avg_2q_per_layer / target_2q_per_layer)
211
+ connectivity = _clamp01(ig_largest_cc_frac) if n_qubits > 0 else 0.0
212
+ entangle = _clamp01(0.4 * depth_ratio + 0.3 * layer_density + 0.3 * connectivity)
213
+
214
+ # CoherenceScore: basis change presence + coverage.
215
+ basis_frac = min(1.0, n_basis_change / float(denom_gate))
216
+ coverage = _clamp01(basis_change_qubit_coverage)
217
+ coherence = _clamp01(0.6 * coverage + 0.4 * basis_frac)
218
+
219
+ # Optional mild structure hint via degree entropy (normalized by log2(n_qubits)).
220
+ # This is a supporting subscore, not a hard signal.
221
+ if n_qubits >= 2:
222
+ norm = math.log2(float(n_qubits))
223
+ deg_entropy_norm = _clamp01(ig_degree_entropy / norm) if norm > 0 else 0.0
224
+ else:
225
+ deg_entropy_norm = 0.0
226
+
227
+ subscores = {
228
+ "MagicScore": float(magic),
229
+ "EntangleScore": float(entangle),
230
+ "CoherenceScore": float(coherence),
231
+ "DegreeEntropyNorm": float(deg_entropy_norm),
232
+ }
233
+
234
+ # Weighted combination (documented and deterministic).
235
+ # Keep conservative weights and avoid overfitting.
236
+ score = _clamp01(0.45 * magic + 0.40 * entangle + 0.15 * coherence)
237
+
238
+ # Add a couple derived metrics for explainability, deterministic and small.
239
+ supporting.update(
240
+ {
241
+ "derived_t_frac": float(t_frac),
242
+ "derived_depth_ratio": float(depth_ratio),
243
+ "derived_layer_density": float(layer_density),
244
+ "derived_basis_frac": float(basis_frac),
245
+ }
246
+ )
247
+
248
+ return QuantumnessReport(
249
+ feature_schema_version=feature_schema_version,
250
+ score=float(score),
251
+ triggered_reasons=[],
252
+ subscores=subscores,
253
+ supporting_metrics=supporting,
254
+ )