solverpilot 0.1__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 (201) hide show
  1. solverpilot/__init__.py +250 -0
  2. solverpilot/_immutability.py +27 -0
  3. solverpilot/_version.py +3 -0
  4. solverpilot/applications/__init__.py +3 -0
  5. solverpilot/applications/routing/__init__.py +19 -0
  6. solverpilot/applications/routing/compile_milp.py +321 -0
  7. solverpilot/applications/routing/diagnostics.py +78 -0
  8. solverpilot/applications/routing/errors.py +14 -0
  9. solverpilot/applications/routing/heuristics.py +167 -0
  10. solverpilot/applications/routing/model.py +259 -0
  11. solverpilot/applications/routing/reference.py +81 -0
  12. solverpilot/applications/routing/solution.py +89 -0
  13. solverpilot/applications/routing/timeline.py +79 -0
  14. solverpilot/applications/routing/validation.py +169 -0
  15. solverpilot/applications/tsp/__init__.py +41 -0
  16. solverpilot/applications/tsp/compile_cp.py +121 -0
  17. solverpilot/applications/tsp/compile_milp.py +139 -0
  18. solverpilot/applications/tsp/distance.py +60 -0
  19. solverpilot/applications/tsp/errors.py +17 -0
  20. solverpilot/applications/tsp/heuristics.py +112 -0
  21. solverpilot/applications/tsp/model.py +175 -0
  22. solverpilot/applications/tsp/reference.py +121 -0
  23. solverpilot/applications/tsp/solution.py +65 -0
  24. solverpilot/applications/tsp/validation.py +92 -0
  25. solverpilot/backends/__init__.py +51 -0
  26. solverpilot/backends/_casadi_highs_worker.py +92 -0
  27. solverpilot/backends/base.py +28 -0
  28. solverpilot/backends/bundled_capi.py +910 -0
  29. solverpilot/backends/casadi_conic.py +303 -0
  30. solverpilot/backends/health.py +240 -0
  31. solverpilot/backends/highspy_native.py +217 -0
  32. solverpilot/backends/nlopt_native.py +237 -0
  33. solverpilot/backends/osqp_native.py +297 -0
  34. solverpilot/backends/protocol_v2.py +119 -0
  35. solverpilot/backends/pyscipopt_native.py +200 -0
  36. solverpilot/backends/registry.py +38 -0
  37. solverpilot/backends/scipy_highs.py +138 -0
  38. solverpilot/backends/scipy_highs_lp.py +195 -0
  39. solverpilot/backends/scipy_slsqp_qp.py +133 -0
  40. solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
  41. solverpilot/benchmark/__init__.py +23 -0
  42. solverpilot/benchmark/acquire.py +199 -0
  43. solverpilot/benchmark/baselines.py +70 -0
  44. solverpilot/benchmark/bundle.py +37 -0
  45. solverpilot/benchmark/campaign.py +150 -0
  46. solverpilot/benchmark/environment.py +115 -0
  47. solverpilot/benchmark/integrity.py +46 -0
  48. solverpilot/benchmark/model.py +146 -0
  49. solverpilot/benchmark/policy.py +182 -0
  50. solverpilot/benchmark/registry.py +38 -0
  51. solverpilot/benchmark/report.py +44 -0
  52. solverpilot/benchmark/runner.py +516 -0
  53. solverpilot/benchmark/slurm.py +55 -0
  54. solverpilot/benchmark/specs.py +75 -0
  55. solverpilot/benchmark/splits.py +181 -0
  56. solverpilot/benchmark/summary.py +384 -0
  57. solverpilot/benchmark/verify.py +142 -0
  58. solverpilot/benchmark/worker.py +71 -0
  59. solverpilot/bridges/__init__.py +19 -0
  60. solverpilot/bridges/engine.py +44 -0
  61. solverpilot/bridges/indicator.py +232 -0
  62. solverpilot/bridges/registry.py +78 -0
  63. solverpilot/bridges/transformation-tape.schema.json +53 -0
  64. solverpilot/bridges/types.py +149 -0
  65. solverpilot/bridges/validation.py +110 -0
  66. solverpilot/capabilities/__init__.py +32 -0
  67. solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
  68. solverpilot/capabilities/conformance.py +151 -0
  69. solverpilot/capabilities/enums.py +27 -0
  70. solverpilot/capabilities/manifest.py +19 -0
  71. solverpilot/capabilities/ontology.py +73 -0
  72. solverpilot/capabilities/requirements.py +37 -0
  73. solverpilot/capabilities/resolve.py +30 -0
  74. solverpilot/capabilities/v2.py +711 -0
  75. solverpilot/cli/__init__.py +1 -0
  76. solverpilot/cli/backend_health.py +77 -0
  77. solverpilot/cli/benchmark.py +331 -0
  78. solverpilot/cli/benchmark_worker.py +33 -0
  79. solverpilot/cli/capabilities.py +65 -0
  80. solverpilot/conic/__init__.py +11 -0
  81. solverpilot/conic/backend.py +263 -0
  82. solverpilot/conic/compiler.py +310 -0
  83. solverpilot/conic/conformance.py +60 -0
  84. solverpilot/conic/conic-ir-v1.schema.json +24 -0
  85. solverpilot/conic/ir.py +299 -0
  86. solverpilot/conic/validation.py +149 -0
  87. solverpilot/cp/__init__.py +7 -0
  88. solverpilot/cp/conformance.py +32 -0
  89. solverpilot/cp/cp-ir-v1.schema.json +13 -0
  90. solverpilot/cp/ir.py +170 -0
  91. solverpilot/cp/model.py +143 -0
  92. solverpilot/cp/ortools_backend.py +423 -0
  93. solverpilot/cp/ortools_worker.py +75 -0
  94. solverpilot/cp/reference.py +51 -0
  95. solverpilot/cp/serialization.py +117 -0
  96. solverpilot/cp/validate.py +83 -0
  97. solverpilot/diagnose/__init__.py +33 -0
  98. solverpilot/diagnose/infeasibility.py +463 -0
  99. solverpilot/diagnose/model.py +87 -0
  100. solverpilot/evaluation/__init__.py +53 -0
  101. solverpilot/evaluation/metrics.py +150 -0
  102. solverpilot/evaluation/oracle.py +288 -0
  103. solverpilot/evaluation/proof.py +53 -0
  104. solverpilot/evaluation/references.py +73 -0
  105. solverpilot/evaluation/regret.py +281 -0
  106. solverpilot/exceptions.py +34 -0
  107. solverpilot/experimental/__init__.py +32 -0
  108. solverpilot/extensions/__init__.py +56 -0
  109. solverpilot/extensions/entrypoints.py +34 -0
  110. solverpilot/extensions/errors.py +35 -0
  111. solverpilot/extensions/manager.py +651 -0
  112. solverpilot/extensions/manifest.py +232 -0
  113. solverpilot/extensions/model.py +101 -0
  114. solverpilot/extensions/versioning.py +136 -0
  115. solverpilot/history/__init__.py +22 -0
  116. solverpilot/history/errors.py +15 -0
  117. solverpilot/history/model.py +129 -0
  118. solverpilot/history/sqlite.py +651 -0
  119. solverpilot/inspect/__init__.py +3 -0
  120. solverpilot/inspect/fingerprint.py +199 -0
  121. solverpilot/intelligence/__init__.py +36 -0
  122. solverpilot/intelligence/errors.py +16 -0
  123. solverpilot/intelligence/fingerprint.py +108 -0
  124. solverpilot/intelligence/leakage.py +69 -0
  125. solverpilot/intelligence/schema.py +314 -0
  126. solverpilot/intelligence/shift.py +208 -0
  127. solverpilot/io/__init__.py +41 -0
  128. solverpilot/io/binary64.py +49 -0
  129. solverpilot/io/csv.py +230 -0
  130. solverpilot/io/errors.py +25 -0
  131. solverpilot/io/hashing.py +111 -0
  132. solverpilot/io/json.py +90 -0
  133. solverpilot/io/mapping.py +195 -0
  134. solverpilot/io/model.py +407 -0
  135. solverpilot/io/numeric.py +108 -0
  136. solverpilot/io/provenance.py +114 -0
  137. solverpilot/io/result.py +45 -0
  138. solverpilot/io/source.py +104 -0
  139. solverpilot/io/strict_json.py +133 -0
  140. solverpilot/minlp/__init__.py +6 -0
  141. solverpilot/minlp/compiler.py +66 -0
  142. solverpilot/minlp/conformance.py +34 -0
  143. solverpilot/minlp/convexity.py +85 -0
  144. solverpilot/minlp/ir.py +35 -0
  145. solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
  146. solverpilot/minlp/orchestrator.py +220 -0
  147. solverpilot/minlp/validation.py +28 -0
  148. solverpilot/model/__init__.py +27 -0
  149. solverpilot/model/compiler.py +1263 -0
  150. solverpilot/model/errors.py +22 -0
  151. solverpilot/model/expression.py +373 -0
  152. solverpilot/model/model.py +716 -0
  153. solverpilot/model/sets.py +60 -0
  154. solverpilot/model/types.py +36 -0
  155. solverpilot/nlp/__init__.py +7 -0
  156. solverpilot/nlp/ad.py +166 -0
  157. solverpilot/nlp/backend.py +113 -0
  158. solverpilot/nlp/compiler.py +116 -0
  159. solverpilot/nlp/conformance.py +58 -0
  160. solverpilot/nlp/ir.py +70 -0
  161. solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
  162. solverpilot/nlp/validation.py +60 -0
  163. solverpilot/plan/__init__.py +33 -0
  164. solverpilot/plan/model.py +79 -0
  165. solverpilot/plan/planner.py +201 -0
  166. solverpilot/plan/production.py +627 -0
  167. solverpilot/plan/selective_lp.py +65 -0
  168. solverpilot/problem/__init__.py +16 -0
  169. solverpilot/problem/enums.py +20 -0
  170. solverpilot/problem/hashing.py +88 -0
  171. solverpilot/problem/linear.py +196 -0
  172. solverpilot/problem/mps.py +447 -0
  173. solverpilot/problem/quadratic.py +192 -0
  174. solverpilot/reporting/__init__.py +27 -0
  175. solverpilot/reporting/explain.py +327 -0
  176. solverpilot/reporting/model.py +264 -0
  177. solverpilot/reporting/render.py +55 -0
  178. solverpilot/runtime/__init__.py +19 -0
  179. solverpilot/runtime/auto.py +193 -0
  180. solverpilot/runtime/budgeting.py +43 -0
  181. solverpilot/runtime/executor.py +126 -0
  182. solverpilot/runtime/portfolio.py +197 -0
  183. solverpilot/runtime/result.py +72 -0
  184. solverpilot/session/__init__.py +29 -0
  185. solverpilot/session/conformance_v2.py +239 -0
  186. solverpilot/session/mutations.py +115 -0
  187. solverpilot/session/persistent-session-trace.schema.json +45 -0
  188. solverpilot/session/persistent.py +491 -0
  189. solverpilot/session/reuse.py +81 -0
  190. solverpilot/session/session.py +236 -0
  191. solverpilot/trace/__init__.py +3 -0
  192. solverpilot/trace/schema.py +47 -0
  193. solverpilot/validate/__init__.py +10 -0
  194. solverpilot/validate/core.py +179 -0
  195. solverpilot/validate/result.py +55 -0
  196. solverpilot-0.1.dist-info/METADATA +554 -0
  197. solverpilot-0.1.dist-info/RECORD +201 -0
  198. solverpilot-0.1.dist-info/WHEEL +5 -0
  199. solverpilot-0.1.dist-info/entry_points.txt +4 -0
  200. solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
  201. solverpilot-0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ import json
5
+ import math
6
+ from pathlib import Path
7
+ from statistics import median
8
+ from typing import Mapping
9
+
10
+ from solverpilot.evaluation import portfolio_metrics
11
+
12
+ from .summary import (
13
+ _get_cost,
14
+ _paired_bootstrap_policy_ratio,
15
+ _require_comparable_environment,
16
+ _require_experiment_identity,
17
+ _require_instance_identity,
18
+ _require_protocol_identity,
19
+ _row_is_benchmark_success,
20
+ load_rows,
21
+ )
22
+
23
+
24
+ def _finite_nonnegative(value: object, *, label: str) -> float:
25
+ try:
26
+ out = float(value)
27
+ except (TypeError, ValueError) as exc:
28
+ raise ValueError(f"{label} must be numeric") from exc
29
+ if not math.isfinite(out) or out < 0:
30
+ raise ValueError(f"{label} must be finite and non-negative")
31
+ return out
32
+
33
+
34
+ def load_policy_map(path: str | Path) -> dict[str, dict]:
35
+ """Load a frozen external selector policy without inventing missing overhead."""
36
+ payload = json.loads(Path(path).read_text(encoding="utf-8"))
37
+ if not isinstance(payload, dict):
38
+ raise ValueError("policy map must be a JSON object keyed by instance")
39
+ out: dict[str, dict] = {}
40
+ for instance, raw in payload.items():
41
+ if isinstance(raw, str):
42
+ out[str(instance)] = {
43
+ "backend": raw,
44
+ "overhead_s": None,
45
+ "overhead_measured": False,
46
+ "overhead_components_s": {},
47
+ }
48
+ continue
49
+ if not isinstance(raw, dict) or not isinstance(raw.get("backend"), str):
50
+ raise ValueError(f"policy entry for {instance!r} must provide backend")
51
+ components_raw = raw.get("overhead_components_s", {})
52
+ if components_raw is None:
53
+ components_raw = {}
54
+ if not isinstance(components_raw, Mapping):
55
+ raise ValueError(f"policy overhead_components_s for {instance!r} must be an object")
56
+ components = {
57
+ str(name): _finite_nonnegative(value, label=f"policy overhead component {name!r} for {instance!r}")
58
+ for name, value in components_raw.items()
59
+ }
60
+ explicit_total = "overhead_s" in raw and raw.get("overhead_s") is not None
61
+ if explicit_total:
62
+ overhead = _finite_nonnegative(raw["overhead_s"], label=f"policy overhead for {instance!r}")
63
+ if components:
64
+ component_sum = sum(components.values())
65
+ if not math.isclose(overhead, component_sum, rel_tol=1e-9, abs_tol=1e-12):
66
+ raise ValueError(f"policy overhead for {instance!r} disagrees with overhead_components_s: total={overhead}, components={component_sum}")
67
+ measured = True
68
+ elif components:
69
+ overhead = float(sum(components.values())); measured = True
70
+ else:
71
+ overhead = None; measured = False
72
+ out[str(instance)] = {
73
+ "backend": raw["backend"],
74
+ "overhead_s": overhead,
75
+ "overhead_measured": measured,
76
+ "overhead_components_s": components,
77
+ }
78
+ return out
79
+
80
+
81
+ def evaluate_policy_map(
82
+ rows: list[dict],
83
+ policy: dict[str, dict],
84
+ *,
85
+ cutoff_s: float,
86
+ par_penalty: float = 10.0,
87
+ cost_field: str = "wall_s",
88
+ bootstrap_draws: int = 10000,
89
+ bootstrap_seed: int = 0,
90
+ allow_mixed_environments: bool = False,
91
+ allow_unmeasured_overhead: bool = False,
92
+ ) -> dict:
93
+ if cutoff_s <= 0 or par_penalty < 1:
94
+ raise ValueError("invalid cutoff/par_penalty")
95
+ environment_ids = _require_comparable_environment(rows, allow_mixed_environments=allow_mixed_environments)
96
+ experiment_ids = _require_experiment_identity(rows)
97
+ protocol_id = _require_protocol_identity(rows)
98
+ _require_instance_identity(rows)
99
+ direct_rows = [r for r in rows if r.get("backend") != "@auto"]
100
+ backends = sorted({str(r["backend"]) for r in direct_rows})
101
+ instances = sorted({str(r["instance"]) for r in direct_rows})
102
+ if set(policy) != set(instances):
103
+ missing = sorted(set(instances) - set(policy)); extra = sorted(set(policy) - set(instances))
104
+ raise ValueError(f"policy map must exactly cover benchmark instances; missing={missing[:10]}, extra={extra[:10]}")
105
+
106
+ unmeasured = sorted(instance for instance in instances if not bool(policy[instance].get("overhead_measured", policy[instance].get("overhead_s") is not None)))
107
+ if unmeasured and not allow_unmeasured_overhead:
108
+ raise ValueError(
109
+ "policy decision overhead is unmeasured for instances "
110
+ f"{unmeasured[:10]}; provide overhead_s/overhead_components_s or pass "
111
+ "allow_unmeasured_overhead=True explicitly for non-deployable analysis"
112
+ )
113
+
114
+ grouped: dict[tuple[str, str], list[dict]] = defaultdict(list)
115
+ for row in direct_rows:
116
+ grouped[(str(row["instance"]), str(row["backend"]))].append(row)
117
+ costs = {b: [] for b in backends}
118
+ for instance in instances:
119
+ for backend in backends:
120
+ good = []
121
+ for r in grouped.get((instance, backend), []):
122
+ if _row_is_benchmark_success(r):
123
+ v = _get_cost(r, cost_field)
124
+ if v is not None: good.append(min(v, cutoff_s))
125
+ costs[backend].append(float(median(good)) if good else cutoff_s * par_penalty)
126
+
127
+ choices = []; policy_costs = []; overhead_values: list[float] = []
128
+ for idx, instance in enumerate(instances):
129
+ entry = policy[instance]; backend = entry["backend"]
130
+ if backend not in costs:
131
+ raise ValueError(f"policy selected backend {backend!r} not present in benchmark")
132
+ choices.append(backend)
133
+ raw_overhead = entry.get("overhead_s")
134
+ overhead = 0.0 if raw_overhead is None else _finite_nonnegative(raw_overhead, label=f"policy overhead for {instance!r}")
135
+ overhead_values.append(overhead); policy_costs.append(float(costs[backend][idx] + overhead))
136
+
137
+ metrics = portfolio_metrics(costs, choices, metric=f"PAR{par_penalty:g}@{cutoff_s:g}s", policy_costs=policy_costs)
138
+ ratio = _paired_bootstrap_policy_ratio(policy_costs, costs[metrics.sbs_solver], draws=bootstrap_draws, seed=bootstrap_seed)
139
+ overhead_complete = not unmeasured
140
+ deployable_cost_scope = cost_field in {"wall_s", "worker_solve_wall_s"}
141
+ return {
142
+ "schema_version": "1.1",
143
+ "protocol_id": protocol_id,
144
+ "environment_ids": sorted(environment_ids),
145
+ "experiment_identities": sorted(experiment_ids),
146
+ "mixed_environments_allowed": bool(allow_mixed_environments),
147
+ "instances": len(instances),
148
+ "cost_field": cost_field,
149
+ "cutoff_s": cutoff_s,
150
+ "par_penalty": par_penalty,
151
+ "overhead_accounting": {
152
+ "complete": overhead_complete,
153
+ "unmeasured_instances": unmeasured,
154
+ "allow_unmeasured_overhead": bool(allow_unmeasured_overhead),
155
+ "assumed_zero_for_unmeasured": bool(unmeasured and allow_unmeasured_overhead),
156
+ "mean_measured_or_assumed_overhead_s": sum(overhead_values) / len(overhead_values),
157
+ },
158
+ "cost_accounting": {
159
+ "solver_cost_field": cost_field,
160
+ "external_policy_overhead_added": True,
161
+ "deployable_cost_scope": deployable_cost_scope,
162
+ "deployable_evidence": bool(overhead_complete and deployable_cost_scope and not allow_mixed_environments),
163
+ },
164
+ "vbs": {"cost": metrics.vbs_cost, "is_oracle": True, "deployable": False},
165
+ "policy_metrics": {
166
+ "metric": metrics.metric,
167
+ "solver_mean_costs": metrics.solver_mean_costs,
168
+ "sbs_solver": metrics.sbs_solver,
169
+ "sbs_cost": metrics.sbs_cost,
170
+ "vbs_cost": metrics.vbs_cost,
171
+ "policy_cost": metrics.policy_cost,
172
+ "gap_closure": metrics.gap_closure,
173
+ "policy_wins": metrics.policy_wins,
174
+ "policy_ties": metrics.policy_ties,
175
+ "policy_to_sbs_ratio_bootstrap_95pct": ratio,
176
+ },
177
+ "choices": {instance: policy[instance] for instance in instances},
178
+ }
179
+
180
+
181
+ def evaluate_policy_jsonl(result_paths: list[str | Path], policy_path: str | Path, **kwargs) -> dict:
182
+ return evaluate_policy_map(load_rows(result_paths), load_policy_map(policy_path), **kwargs)
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from solverpilot.backends import BackendRegistry, BundledOSQPCAPIBackend, BundledHighsCAPIBackend
4
+ from solverpilot.runtime.auto import builtin_backend_candidates
5
+
6
+
7
+ def benchmark_registry(backends: tuple[str, ...]) -> BackendRegistry:
8
+ backends = tuple(b for b in backends if b != "@auto")
9
+ if not backends:
10
+ raise ValueError("benchmark registry needs at least one explicit backend; @auto is evaluated over that portfolio")
11
+ candidates = (*builtin_backend_candidates(), BundledOSQPCAPIBackend(), BundledHighsCAPIBackend())
12
+ by_name = {b.manifest.name: b for b in candidates}
13
+ unknown = sorted(set(backends) - set(by_name))
14
+ if unknown:
15
+ raise KeyError(f"unknown benchmark backends: {unknown}")
16
+ registry = BackendRegistry()
17
+ unavailable = []
18
+ unverified = []
19
+ for name in backends:
20
+ backend = by_name[name]
21
+ if not backend.is_available():
22
+ unavailable.append(name)
23
+ continue
24
+ verified_bridge_versions = backend.manifest.metadata.get("verified_bridge_package_versions")
25
+ bridge_version = backend.manifest.metadata.get("bridge_package_version")
26
+ if verified_bridge_versions is not None and bridge_version not in set(verified_bridge_versions):
27
+ unverified.append((name, f"bridge={bridge_version}", tuple(verified_bridge_versions)))
28
+ continue
29
+ verified_solver_versions = backend.manifest.metadata.get("verified_solver_versions")
30
+ if verified_solver_versions is not None and backend.manifest.version not in set(verified_solver_versions):
31
+ unverified.append((name, f"solver={backend.manifest.version}", tuple(verified_solver_versions)))
32
+ continue
33
+ registry.register(backend)
34
+ if unavailable:
35
+ raise RuntimeError(f"requested backends are unavailable: {unavailable}")
36
+ if unverified:
37
+ raise RuntimeError(f"requested verification bridges use unverified package versions: {unverified}")
38
+ return registry
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Mapping
7
+
8
+ from .campaign import CampaignSpec
9
+
10
+
11
+ def scientific_report_payload(campaign: CampaignSpec, summary: Mapping[str,Any], *, campaign_run: Mapping[str,Any]|None=None, limitations: tuple[str,...]=()) -> dict[str,Any]:
12
+ vbs=summary.get("vbs") or {"cost":summary.get("vbs_cost"),"is_oracle":True,"deployable":False}
13
+ if vbs.get("deployable") is not False or vbs.get("is_oracle") is not True:
14
+ raise ValueError("scientific report requires VBS to be explicitly oracle/non-deployable")
15
+ provenance_ok=None if campaign_run is None else bool(campaign_run.get("scientific_provenance_verified"))
16
+ deployable=bool(provenance_ok and not summary.get("mixed_environments_allowed",False) and summary.get("instance_identity_verified") is True)
17
+ return {"schema_version":"solverpilot.scientific-report.v1","generated_at_utc":datetime.now(timezone.utc).isoformat(),"campaign":campaign.to_dict(),"campaign_sha256":campaign.sha256,"scientific_provenance_verified":provenance_ok,"deployable_scientific_evidence":deployable,"instances":summary.get("instances"),"backends":summary.get("backends"),"cost_field":summary.get("cost_field"),"sbs":{"backend":summary.get("sbs_solver"),"cost":summary.get("sbs_cost")},"vbs":vbs,"sbs_vbs_gap":summary.get("sbs_vbs_gap"),"relative_sbs_vbs_gap":summary.get("relative_sbs_vbs_gap"),"solver_stats":summary.get("solver_stats"),"auto_policy_metrics":summary.get("auto_policy_metrics"),"limitations":list(limitations),"claim_boundary":"performance claims may be made only when deployable_scientific_evidence=true; VBS is an oracle baseline and is never a deployable selector"}
18
+
19
+
20
+ def render_scientific_report_markdown(payload: Mapping[str,Any])->str:
21
+ sbs=payload.get("sbs") or {}; vbs=payload.get("vbs") or {}
22
+ lines=["# SolverPilot Scientific Benchmark Report","",f"- Campaign: `{payload.get('campaign',{}).get('campaign_id')}`",f"- Campaign SHA-256: `{payload.get('campaign_sha256')}`",f"- Scientific provenance verified: `{payload.get('scientific_provenance_verified')}`",f"- Deployable scientific evidence: `{payload.get('deployable_scientific_evidence')}`",f"- Instances: `{payload.get('instances')}`",f"- Cost field: `{payload.get('cost_field')}`","","## Baselines","",f"- SBS: `{sbs.get('backend')}` — cost `{sbs.get('cost')}`",f"- VBS oracle cost: `{vbs.get('cost')}` (oracle=`{vbs.get('is_oracle')}`, deployable=`{vbs.get('deployable')}`)","","## Claim boundary","",str(payload.get("claim_boundary"))]
23
+ if payload.get("limitations"): lines += ["","## Limitations",""]+[f"- {x}" for x in payload["limitations"]]
24
+ return "\n".join(lines)+"\n"
25
+
26
+
27
+ def write_scientific_report(payload:Mapping[str,Any], *, json_path:str|Path, markdown_path:str|Path)->None:
28
+ Path(json_path).write_text(json.dumps(payload,indent=2,sort_keys=True,default=str),encoding="utf-8")
29
+ Path(markdown_path).write_text(render_scientific_report_markdown(payload),encoding="utf-8")
30
+
31
+
32
+ def compute_scientific_metrics(rows:list[dict[str,Any]], *, cutoff_s:float, cost_field:str="wall_s")->dict[str,Any]:
33
+ from .summary import summarize_rows
34
+ par2=summarize_rows(rows,cutoff_s=cutoff_s,par_penalty=2.0,cost_field=cost_field); par10=summarize_rows(rows,cutoff_s=cutoff_s,par_penalty=10.0,cost_field=cost_field)
35
+ direct=[r for r in rows if r.get("backend") != "@auto"]; backends=sorted({str(r.get("backend")) for r in direct})
36
+ timeout={b:sum(1 for r in direct if str(r.get("backend"))==b and (r.get("state")=="hard_timeout" or r.get("public_status")=="feasible_limit")) for b in backends}
37
+ gaps={b:[] for b in backends}
38
+ for row in direct:
39
+ raw=row.get("relative_gap",row.get("optimality_gap"))
40
+ if raw is None: continue
41
+ try: value=float(raw)
42
+ except (TypeError,ValueError): continue
43
+ if value>=0: gaps[str(row.get("backend"))].append(value)
44
+ return {"schema_version":"solverpilot.scientific-metrics.v1","cost_field":cost_field,"cutoff_s":float(cutoff_s),"par2":{"sbs_solver":par2["sbs_solver"],"sbs_cost":par2["sbs_cost"],"vbs_cost":par2["vbs_cost"],"solver_stats":par2["solver_stats"]},"par10":{"sbs_solver":par10["sbs_solver"],"sbs_cost":par10["sbs_cost"],"vbs_cost":par10["vbs_cost"],"solver_stats":par10["solver_stats"]},"timeout_count":timeout,"mean_reported_relative_gap":{b:(None if not v else sum(v)/len(v)) for b,v in gaps.items()},"optimality_gap_availability":"explicit_row_field_only; null means the backend/run did not expose a normalized gap","regret":"evaluate through solverpilot.evaluation oracle/regret APIs on a frozen held-out split","selection_overhead":"included only through deployable policy evaluation with measured overhead"}