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.
- solverpilot/__init__.py +250 -0
- solverpilot/_immutability.py +27 -0
- solverpilot/_version.py +3 -0
- solverpilot/applications/__init__.py +3 -0
- solverpilot/applications/routing/__init__.py +19 -0
- solverpilot/applications/routing/compile_milp.py +321 -0
- solverpilot/applications/routing/diagnostics.py +78 -0
- solverpilot/applications/routing/errors.py +14 -0
- solverpilot/applications/routing/heuristics.py +167 -0
- solverpilot/applications/routing/model.py +259 -0
- solverpilot/applications/routing/reference.py +81 -0
- solverpilot/applications/routing/solution.py +89 -0
- solverpilot/applications/routing/timeline.py +79 -0
- solverpilot/applications/routing/validation.py +169 -0
- solverpilot/applications/tsp/__init__.py +41 -0
- solverpilot/applications/tsp/compile_cp.py +121 -0
- solverpilot/applications/tsp/compile_milp.py +139 -0
- solverpilot/applications/tsp/distance.py +60 -0
- solverpilot/applications/tsp/errors.py +17 -0
- solverpilot/applications/tsp/heuristics.py +112 -0
- solverpilot/applications/tsp/model.py +175 -0
- solverpilot/applications/tsp/reference.py +121 -0
- solverpilot/applications/tsp/solution.py +65 -0
- solverpilot/applications/tsp/validation.py +92 -0
- solverpilot/backends/__init__.py +51 -0
- solverpilot/backends/_casadi_highs_worker.py +92 -0
- solverpilot/backends/base.py +28 -0
- solverpilot/backends/bundled_capi.py +910 -0
- solverpilot/backends/casadi_conic.py +303 -0
- solverpilot/backends/health.py +240 -0
- solverpilot/backends/highspy_native.py +217 -0
- solverpilot/backends/nlopt_native.py +237 -0
- solverpilot/backends/osqp_native.py +297 -0
- solverpilot/backends/protocol_v2.py +119 -0
- solverpilot/backends/pyscipopt_native.py +200 -0
- solverpilot/backends/registry.py +38 -0
- solverpilot/backends/scipy_highs.py +138 -0
- solverpilot/backends/scipy_highs_lp.py +195 -0
- solverpilot/backends/scipy_slsqp_qp.py +133 -0
- solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
- solverpilot/benchmark/__init__.py +23 -0
- solverpilot/benchmark/acquire.py +199 -0
- solverpilot/benchmark/baselines.py +70 -0
- solverpilot/benchmark/bundle.py +37 -0
- solverpilot/benchmark/campaign.py +150 -0
- solverpilot/benchmark/environment.py +115 -0
- solverpilot/benchmark/integrity.py +46 -0
- solverpilot/benchmark/model.py +146 -0
- solverpilot/benchmark/policy.py +182 -0
- solverpilot/benchmark/registry.py +38 -0
- solverpilot/benchmark/report.py +44 -0
- solverpilot/benchmark/runner.py +516 -0
- solverpilot/benchmark/slurm.py +55 -0
- solverpilot/benchmark/specs.py +75 -0
- solverpilot/benchmark/splits.py +181 -0
- solverpilot/benchmark/summary.py +384 -0
- solverpilot/benchmark/verify.py +142 -0
- solverpilot/benchmark/worker.py +71 -0
- solverpilot/bridges/__init__.py +19 -0
- solverpilot/bridges/engine.py +44 -0
- solverpilot/bridges/indicator.py +232 -0
- solverpilot/bridges/registry.py +78 -0
- solverpilot/bridges/transformation-tape.schema.json +53 -0
- solverpilot/bridges/types.py +149 -0
- solverpilot/bridges/validation.py +110 -0
- solverpilot/capabilities/__init__.py +32 -0
- solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
- solverpilot/capabilities/conformance.py +151 -0
- solverpilot/capabilities/enums.py +27 -0
- solverpilot/capabilities/manifest.py +19 -0
- solverpilot/capabilities/ontology.py +73 -0
- solverpilot/capabilities/requirements.py +37 -0
- solverpilot/capabilities/resolve.py +30 -0
- solverpilot/capabilities/v2.py +711 -0
- solverpilot/cli/__init__.py +1 -0
- solverpilot/cli/backend_health.py +77 -0
- solverpilot/cli/benchmark.py +331 -0
- solverpilot/cli/benchmark_worker.py +33 -0
- solverpilot/cli/capabilities.py +65 -0
- solverpilot/conic/__init__.py +11 -0
- solverpilot/conic/backend.py +263 -0
- solverpilot/conic/compiler.py +310 -0
- solverpilot/conic/conformance.py +60 -0
- solverpilot/conic/conic-ir-v1.schema.json +24 -0
- solverpilot/conic/ir.py +299 -0
- solverpilot/conic/validation.py +149 -0
- solverpilot/cp/__init__.py +7 -0
- solverpilot/cp/conformance.py +32 -0
- solverpilot/cp/cp-ir-v1.schema.json +13 -0
- solverpilot/cp/ir.py +170 -0
- solverpilot/cp/model.py +143 -0
- solverpilot/cp/ortools_backend.py +423 -0
- solverpilot/cp/ortools_worker.py +75 -0
- solverpilot/cp/reference.py +51 -0
- solverpilot/cp/serialization.py +117 -0
- solverpilot/cp/validate.py +83 -0
- solverpilot/diagnose/__init__.py +33 -0
- solverpilot/diagnose/infeasibility.py +463 -0
- solverpilot/diagnose/model.py +87 -0
- solverpilot/evaluation/__init__.py +53 -0
- solverpilot/evaluation/metrics.py +150 -0
- solverpilot/evaluation/oracle.py +288 -0
- solverpilot/evaluation/proof.py +53 -0
- solverpilot/evaluation/references.py +73 -0
- solverpilot/evaluation/regret.py +281 -0
- solverpilot/exceptions.py +34 -0
- solverpilot/experimental/__init__.py +32 -0
- solverpilot/extensions/__init__.py +56 -0
- solverpilot/extensions/entrypoints.py +34 -0
- solverpilot/extensions/errors.py +35 -0
- solverpilot/extensions/manager.py +651 -0
- solverpilot/extensions/manifest.py +232 -0
- solverpilot/extensions/model.py +101 -0
- solverpilot/extensions/versioning.py +136 -0
- solverpilot/history/__init__.py +22 -0
- solverpilot/history/errors.py +15 -0
- solverpilot/history/model.py +129 -0
- solverpilot/history/sqlite.py +651 -0
- solverpilot/inspect/__init__.py +3 -0
- solverpilot/inspect/fingerprint.py +199 -0
- solverpilot/intelligence/__init__.py +36 -0
- solverpilot/intelligence/errors.py +16 -0
- solverpilot/intelligence/fingerprint.py +108 -0
- solverpilot/intelligence/leakage.py +69 -0
- solverpilot/intelligence/schema.py +314 -0
- solverpilot/intelligence/shift.py +208 -0
- solverpilot/io/__init__.py +41 -0
- solverpilot/io/binary64.py +49 -0
- solverpilot/io/csv.py +230 -0
- solverpilot/io/errors.py +25 -0
- solverpilot/io/hashing.py +111 -0
- solverpilot/io/json.py +90 -0
- solverpilot/io/mapping.py +195 -0
- solverpilot/io/model.py +407 -0
- solverpilot/io/numeric.py +108 -0
- solverpilot/io/provenance.py +114 -0
- solverpilot/io/result.py +45 -0
- solverpilot/io/source.py +104 -0
- solverpilot/io/strict_json.py +133 -0
- solverpilot/minlp/__init__.py +6 -0
- solverpilot/minlp/compiler.py +66 -0
- solverpilot/minlp/conformance.py +34 -0
- solverpilot/minlp/convexity.py +85 -0
- solverpilot/minlp/ir.py +35 -0
- solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
- solverpilot/minlp/orchestrator.py +220 -0
- solverpilot/minlp/validation.py +28 -0
- solverpilot/model/__init__.py +27 -0
- solverpilot/model/compiler.py +1263 -0
- solverpilot/model/errors.py +22 -0
- solverpilot/model/expression.py +373 -0
- solverpilot/model/model.py +716 -0
- solverpilot/model/sets.py +60 -0
- solverpilot/model/types.py +36 -0
- solverpilot/nlp/__init__.py +7 -0
- solverpilot/nlp/ad.py +166 -0
- solverpilot/nlp/backend.py +113 -0
- solverpilot/nlp/compiler.py +116 -0
- solverpilot/nlp/conformance.py +58 -0
- solverpilot/nlp/ir.py +70 -0
- solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
- solverpilot/nlp/validation.py +60 -0
- solverpilot/plan/__init__.py +33 -0
- solverpilot/plan/model.py +79 -0
- solverpilot/plan/planner.py +201 -0
- solverpilot/plan/production.py +627 -0
- solverpilot/plan/selective_lp.py +65 -0
- solverpilot/problem/__init__.py +16 -0
- solverpilot/problem/enums.py +20 -0
- solverpilot/problem/hashing.py +88 -0
- solverpilot/problem/linear.py +196 -0
- solverpilot/problem/mps.py +447 -0
- solverpilot/problem/quadratic.py +192 -0
- solverpilot/reporting/__init__.py +27 -0
- solverpilot/reporting/explain.py +327 -0
- solverpilot/reporting/model.py +264 -0
- solverpilot/reporting/render.py +55 -0
- solverpilot/runtime/__init__.py +19 -0
- solverpilot/runtime/auto.py +193 -0
- solverpilot/runtime/budgeting.py +43 -0
- solverpilot/runtime/executor.py +126 -0
- solverpilot/runtime/portfolio.py +197 -0
- solverpilot/runtime/result.py +72 -0
- solverpilot/session/__init__.py +29 -0
- solverpilot/session/conformance_v2.py +239 -0
- solverpilot/session/mutations.py +115 -0
- solverpilot/session/persistent-session-trace.schema.json +45 -0
- solverpilot/session/persistent.py +491 -0
- solverpilot/session/reuse.py +81 -0
- solverpilot/session/session.py +236 -0
- solverpilot/trace/__init__.py +3 -0
- solverpilot/trace/schema.py +47 -0
- solverpilot/validate/__init__.py +10 -0
- solverpilot/validate/core.py +179 -0
- solverpilot/validate/result.py +55 -0
- solverpilot-0.1.dist-info/METADATA +554 -0
- solverpilot-0.1.dist-info/RECORD +201 -0
- solverpilot-0.1.dist-info/WHEEL +5 -0
- solverpilot-0.1.dist-info/entry_points.txt +4 -0
- solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
- 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"}
|