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,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from math import isfinite
|
|
5
|
+
from statistics import mean
|
|
6
|
+
from typing import Mapping, Sequence
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class PortfolioMetrics:
|
|
11
|
+
metric: str
|
|
12
|
+
lower_is_better: bool
|
|
13
|
+
solver_mean_costs: dict[str, float]
|
|
14
|
+
sbs_solver: str
|
|
15
|
+
sbs_cost: float
|
|
16
|
+
vbs_cost: float
|
|
17
|
+
policy_cost: float
|
|
18
|
+
gap_closure: float | None
|
|
19
|
+
policy_wins: int
|
|
20
|
+
policy_ties: int
|
|
21
|
+
instances: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_cost_table(costs: Mapping[str, Sequence[float]]) -> int:
|
|
25
|
+
if not costs:
|
|
26
|
+
raise ValueError("costs must contain at least one solver")
|
|
27
|
+
lengths = {len(v) for v in costs.values()}
|
|
28
|
+
if len(lengths) != 1:
|
|
29
|
+
raise ValueError("all solver cost vectors must have the same length")
|
|
30
|
+
n = lengths.pop()
|
|
31
|
+
if n == 0:
|
|
32
|
+
raise ValueError("cost vectors must not be empty")
|
|
33
|
+
for solver, values in costs.items():
|
|
34
|
+
for value in values:
|
|
35
|
+
value = float(value)
|
|
36
|
+
if not isfinite(value) or value < 0:
|
|
37
|
+
raise ValueError(f"cost for {solver!r} must be finite and non-negative")
|
|
38
|
+
return n
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def portfolio_metrics(
|
|
42
|
+
costs: Mapping[str, Sequence[float]],
|
|
43
|
+
policy_choices: Sequence[str],
|
|
44
|
+
*,
|
|
45
|
+
metric: str = "wall_time_s",
|
|
46
|
+
tie_relative_tolerance: float = 0.01,
|
|
47
|
+
policy_costs: Sequence[float] | None = None,
|
|
48
|
+
) -> PortfolioMetrics:
|
|
49
|
+
"""Evaluate a lower-is-better algorithm-selection portfolio.
|
|
50
|
+
|
|
51
|
+
Costs must include every cost paid by a deployable policy (e.g. inspection/planning
|
|
52
|
+
overhead when evaluating an automatic policy). SBS is the best single solver by mean
|
|
53
|
+
instance cost. VBS is the per-instance oracle. Gap closure measures how much of the
|
|
54
|
+
SBS-to-VBS opportunity is captured by the policy.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
n = _validate_cost_table(costs)
|
|
58
|
+
if len(policy_choices) != n:
|
|
59
|
+
raise ValueError("policy_choices length must equal the number of instances")
|
|
60
|
+
unknown = sorted(set(policy_choices) - set(costs))
|
|
61
|
+
if unknown:
|
|
62
|
+
raise ValueError(f"policy selected unknown solvers: {unknown}")
|
|
63
|
+
if tie_relative_tolerance < 0:
|
|
64
|
+
raise ValueError("tie_relative_tolerance must be non-negative")
|
|
65
|
+
|
|
66
|
+
means = {solver: float(mean(map(float, values))) for solver, values in costs.items()}
|
|
67
|
+
# Stable lexicographic tie-break prevents dict insertion order from becoming semantics.
|
|
68
|
+
sbs_solver = min(means, key=lambda s: (means[s], s))
|
|
69
|
+
sbs_cost = means[sbs_solver]
|
|
70
|
+
|
|
71
|
+
vbs_values = [min(float(costs[s][i]) for s in costs) for i in range(n)]
|
|
72
|
+
vbs_cost = float(mean(vbs_values))
|
|
73
|
+
|
|
74
|
+
chosen_solver_values = [float(costs[choice][i]) for i, choice in enumerate(policy_choices)]
|
|
75
|
+
if policy_costs is None:
|
|
76
|
+
policy_values = chosen_solver_values
|
|
77
|
+
else:
|
|
78
|
+
if len(policy_costs) != n:
|
|
79
|
+
raise ValueError("policy_costs length must equal the number of instances")
|
|
80
|
+
policy_values = [float(v) for v in policy_costs]
|
|
81
|
+
if any(not isfinite(v) or v < 0 for v in policy_values):
|
|
82
|
+
raise ValueError("policy_costs must be finite and non-negative")
|
|
83
|
+
policy_cost = float(mean(policy_values))
|
|
84
|
+
|
|
85
|
+
denominator = sbs_cost - vbs_cost
|
|
86
|
+
if denominator <= max(1e-15, 1e-12 * max(abs(sbs_cost), abs(vbs_cost), 1.0)):
|
|
87
|
+
gap_closure = None
|
|
88
|
+
else:
|
|
89
|
+
gap_closure = (sbs_cost - policy_cost) / denominator
|
|
90
|
+
|
|
91
|
+
wins = 0
|
|
92
|
+
ties = 0
|
|
93
|
+
for i, choice in enumerate(policy_choices):
|
|
94
|
+
chosen = chosen_solver_values[i]
|
|
95
|
+
best = vbs_values[i]
|
|
96
|
+
if best == 0.0:
|
|
97
|
+
is_tie = chosen == 0.0
|
|
98
|
+
else:
|
|
99
|
+
is_tie = (chosen - best) / best <= tie_relative_tolerance
|
|
100
|
+
if is_tie:
|
|
101
|
+
ties += 1
|
|
102
|
+
# Strict win means the chosen solver is uniquely faster beyond tolerance.
|
|
103
|
+
competitors = [float(costs[s][i]) for s in costs if s != choice]
|
|
104
|
+
if competitors and all(chosen < c * (1.0 - tie_relative_tolerance) for c in competitors):
|
|
105
|
+
wins += 1
|
|
106
|
+
|
|
107
|
+
return PortfolioMetrics(
|
|
108
|
+
metric=metric,
|
|
109
|
+
lower_is_better=True,
|
|
110
|
+
solver_mean_costs=means,
|
|
111
|
+
sbs_solver=sbs_solver,
|
|
112
|
+
sbs_cost=sbs_cost,
|
|
113
|
+
vbs_cost=vbs_cost,
|
|
114
|
+
policy_cost=policy_cost,
|
|
115
|
+
gap_closure=None if gap_closure is None else float(gap_closure),
|
|
116
|
+
policy_wins=wins,
|
|
117
|
+
policy_ties=ties,
|
|
118
|
+
instances=n,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def performance_profile(
|
|
123
|
+
costs: Mapping[str, Sequence[float]],
|
|
124
|
+
taus: Sequence[float],
|
|
125
|
+
) -> dict[str, list[float]]:
|
|
126
|
+
"""Compute Dolan-Moré style performance profile values for finite costs.
|
|
127
|
+
|
|
128
|
+
For each tau >= 1, returns the fraction of instances where a solver's cost is at
|
|
129
|
+
most tau times the best cost on that instance.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
n = _validate_cost_table(costs)
|
|
133
|
+
taus = [float(t) for t in taus]
|
|
134
|
+
if any(t < 1.0 or not isfinite(t) for t in taus):
|
|
135
|
+
raise ValueError("all taus must be finite and >= 1")
|
|
136
|
+
|
|
137
|
+
best = [min(float(costs[s][i]) for s in costs) for i in range(n)]
|
|
138
|
+
out: dict[str, list[float]] = {}
|
|
139
|
+
for solver, values in costs.items():
|
|
140
|
+
fractions: list[float] = []
|
|
141
|
+
for tau in taus:
|
|
142
|
+
count = 0
|
|
143
|
+
for value, b in zip(values, best):
|
|
144
|
+
value = float(value)
|
|
145
|
+
ratio = 1.0 if b == 0.0 and value == 0.0 else (float("inf") if b == 0.0 else value / b)
|
|
146
|
+
if ratio <= tau:
|
|
147
|
+
count += 1
|
|
148
|
+
fractions.append(count / n)
|
|
149
|
+
out[solver] = fractions
|
|
150
|
+
return out
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from math import isclose, isfinite
|
|
6
|
+
from typing import Iterable, Mapping, Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_REFERENCE_SUCCESS = {"objective_matches_optimum"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ObjectiveSense(str, Enum):
|
|
13
|
+
MINIMIZE = "minimize"
|
|
14
|
+
MAXIMIZE = "maximize"
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def parse(cls, value: str | "ObjectiveSense") -> "ObjectiveSense":
|
|
18
|
+
if isinstance(value, cls):
|
|
19
|
+
return value
|
|
20
|
+
normalized = str(value).strip().lower()
|
|
21
|
+
aliases = {"min": cls.MINIMIZE, "max": cls.MAXIMIZE}
|
|
22
|
+
if normalized in aliases:
|
|
23
|
+
return aliases[normalized]
|
|
24
|
+
try:
|
|
25
|
+
return cls(normalized)
|
|
26
|
+
except ValueError as exc:
|
|
27
|
+
raise ValueError("objective_sense must be 'minimize' or 'maximize'") from exc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OracleStatus(str, Enum):
|
|
31
|
+
AVAILABLE = "available"
|
|
32
|
+
NO_ELIGIBLE_CANDIDATE = "no_eligible_candidate"
|
|
33
|
+
PROTOCOL_MISMATCH = "protocol_mismatch"
|
|
34
|
+
ENVIRONMENT_MISMATCH = "environment_mismatch"
|
|
35
|
+
INSTANCE_IDENTITY_MISMATCH = "instance_identity_mismatch"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CandidateExclusionReason(str, Enum):
|
|
39
|
+
AUTO_BACKEND = "auto_backend"
|
|
40
|
+
NOT_SOLVED = "not_solved"
|
|
41
|
+
NOT_VALIDATED = "not_validated"
|
|
42
|
+
NOT_EXACT_SUCCESS = "not_exact_success"
|
|
43
|
+
MISSING_OBJECTIVE = "missing_objective"
|
|
44
|
+
NONFINITE_OBJECTIVE = "nonfinite_objective"
|
|
45
|
+
MISSING_RUNTIME = "missing_runtime"
|
|
46
|
+
NONFINITE_RUNTIME = "nonfinite_runtime"
|
|
47
|
+
NEGATIVE_RUNTIME = "negative_runtime"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class OraclePolicy:
|
|
52
|
+
objective_sense: ObjectiveSense | str = ObjectiveSense.MINIMIZE
|
|
53
|
+
objective_atol: float = 1e-7
|
|
54
|
+
objective_rtol: float = 1e-7
|
|
55
|
+
runtime_tie_breaker: bool = False
|
|
56
|
+
runtime_tolerance_s: float = 1e-9
|
|
57
|
+
require_same_protocol: bool = True
|
|
58
|
+
require_same_environment: bool = True
|
|
59
|
+
require_same_instance_sha256: bool = True
|
|
60
|
+
include_auto_backend: bool = False
|
|
61
|
+
runtime_field: str = "wall_s"
|
|
62
|
+
|
|
63
|
+
def __post_init__(self) -> None:
|
|
64
|
+
object.__setattr__(self, "objective_sense", ObjectiveSense.parse(self.objective_sense))
|
|
65
|
+
for name in ("objective_atol", "objective_rtol", "runtime_tolerance_s"):
|
|
66
|
+
raw = getattr(self, name)
|
|
67
|
+
if isinstance(raw, bool) or type(raw) not in (int, float):
|
|
68
|
+
raise ValueError(f"{name} must be numeric")
|
|
69
|
+
value = float(raw)
|
|
70
|
+
if not isfinite(value) or value < 0:
|
|
71
|
+
raise ValueError(f"{name} must be finite and non-negative")
|
|
72
|
+
object.__setattr__(self, name, value)
|
|
73
|
+
for name in (
|
|
74
|
+
"runtime_tie_breaker",
|
|
75
|
+
"require_same_protocol",
|
|
76
|
+
"require_same_environment",
|
|
77
|
+
"require_same_instance_sha256",
|
|
78
|
+
"include_auto_backend",
|
|
79
|
+
):
|
|
80
|
+
if type(getattr(self, name)) is not bool:
|
|
81
|
+
raise ValueError(f"{name} must be boolean")
|
|
82
|
+
if not isinstance(self.runtime_field, str) or not self.runtime_field.strip():
|
|
83
|
+
raise ValueError("runtime_field must be a non-empty dotted field name")
|
|
84
|
+
object.__setattr__(self, "runtime_field", self.runtime_field.strip())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class CandidateExclusion:
|
|
89
|
+
run_id: str
|
|
90
|
+
backend: str
|
|
91
|
+
reason: CandidateExclusionReason
|
|
92
|
+
detail: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True, slots=True)
|
|
96
|
+
class OracleCandidate:
|
|
97
|
+
run_id: str
|
|
98
|
+
backend: str
|
|
99
|
+
objective: float
|
|
100
|
+
runtime_s: float
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class OracleEntry:
|
|
105
|
+
instance: str
|
|
106
|
+
instance_sha256: str | None
|
|
107
|
+
protocol_id: str | None
|
|
108
|
+
environment_id: str | None
|
|
109
|
+
status: OracleStatus
|
|
110
|
+
objective_sense: ObjectiveSense
|
|
111
|
+
oracle_objective: float | None
|
|
112
|
+
oracle_run_ids: tuple[str, ...]
|
|
113
|
+
eligible_run_ids: tuple[str, ...]
|
|
114
|
+
fastest_oracle_runtime_s: float | None
|
|
115
|
+
candidates: tuple[OracleCandidate, ...]
|
|
116
|
+
exclusions: tuple[CandidateExclusion, ...]
|
|
117
|
+
reasons: tuple[str, ...] = ()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True, slots=True)
|
|
121
|
+
class OracleTable:
|
|
122
|
+
policy: OraclePolicy
|
|
123
|
+
entries: tuple[OracleEntry, ...]
|
|
124
|
+
|
|
125
|
+
def by_instance(self) -> dict[str, OracleEntry]:
|
|
126
|
+
return {entry.instance: entry for entry in self.entries}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _dotted_value(row: Mapping[str, Any], field: str) -> Any:
|
|
130
|
+
current: Any = row
|
|
131
|
+
for part in field.split("."):
|
|
132
|
+
if not isinstance(current, Mapping) or part not in current:
|
|
133
|
+
return None
|
|
134
|
+
current = current[part]
|
|
135
|
+
return current
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _exact_success(row: Mapping[str, Any]) -> bool:
|
|
139
|
+
if row.get("state") != "solved" or row.get("validated") is not True:
|
|
140
|
+
return False
|
|
141
|
+
check = row.get("reference_check")
|
|
142
|
+
if check in _REFERENCE_SUCCESS:
|
|
143
|
+
return True
|
|
144
|
+
if check == "not_checkable":
|
|
145
|
+
evidence = row.get("optimality_evidence")
|
|
146
|
+
return (
|
|
147
|
+
row.get("public_status") == "valid_optimal"
|
|
148
|
+
and isinstance(evidence, Mapping)
|
|
149
|
+
and evidence.get("independently_verified_optimal") is True
|
|
150
|
+
)
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _candidate(row: Mapping[str, Any], policy: OraclePolicy) -> tuple[OracleCandidate | None, CandidateExclusion | None]:
|
|
155
|
+
run_id = str(row.get("run_id") or "")
|
|
156
|
+
backend = str(row.get("backend") or "")
|
|
157
|
+
if not run_id:
|
|
158
|
+
raise ValueError("every oracle row must provide a non-empty run_id")
|
|
159
|
+
if not backend:
|
|
160
|
+
raise ValueError(f"run {run_id!r} has no backend")
|
|
161
|
+
if backend == "@auto" and not policy.include_auto_backend:
|
|
162
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.AUTO_BACKEND, "automatic policy rows are excluded from the direct-run oracle")
|
|
163
|
+
if row.get("state") != "solved":
|
|
164
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.NOT_SOLVED, f"state={row.get('state')!r}")
|
|
165
|
+
if row.get("validated") is not True:
|
|
166
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.NOT_VALIDATED, "independent validation did not pass")
|
|
167
|
+
if not _exact_success(row):
|
|
168
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.NOT_EXACT_SUCCESS, f"reference_check={row.get('reference_check')!r}, public_status={row.get('public_status')!r}")
|
|
169
|
+
raw_objective = row.get("objective")
|
|
170
|
+
if raw_objective is None:
|
|
171
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.MISSING_OBJECTIVE, "eligible exact run has no objective")
|
|
172
|
+
try:
|
|
173
|
+
objective = float(raw_objective)
|
|
174
|
+
except (TypeError, ValueError):
|
|
175
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.NONFINITE_OBJECTIVE, "objective is not numeric")
|
|
176
|
+
if not isfinite(objective):
|
|
177
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.NONFINITE_OBJECTIVE, "objective is not finite")
|
|
178
|
+
raw_runtime = _dotted_value(row, policy.runtime_field)
|
|
179
|
+
if raw_runtime is None:
|
|
180
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.MISSING_RUNTIME, f"runtime field {policy.runtime_field!r} is missing")
|
|
181
|
+
try:
|
|
182
|
+
runtime = float(raw_runtime)
|
|
183
|
+
except (TypeError, ValueError):
|
|
184
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.NONFINITE_RUNTIME, "runtime is not numeric")
|
|
185
|
+
if not isfinite(runtime):
|
|
186
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.NONFINITE_RUNTIME, "runtime is not finite")
|
|
187
|
+
if runtime < 0:
|
|
188
|
+
return None, CandidateExclusion(run_id, backend, CandidateExclusionReason.NEGATIVE_RUNTIME, "runtime must be non-negative")
|
|
189
|
+
return OracleCandidate(run_id, backend, objective, runtime), None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _same_objective(a: float, b: float, policy: OraclePolicy) -> bool:
|
|
193
|
+
return isclose(a, b, abs_tol=policy.objective_atol, rel_tol=policy.objective_rtol)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _identity_value(row: Mapping[str, Any], field: str) -> str | None:
|
|
197
|
+
raw = row.get(field)
|
|
198
|
+
if raw is None:
|
|
199
|
+
return None
|
|
200
|
+
text = str(raw).strip()
|
|
201
|
+
return text or None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _valid_sha256(value: str | None) -> bool:
|
|
205
|
+
return value is not None and len(value) == 64 and all(ch in "0123456789abcdefABCDEF" for ch in value)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def build_oracle_table(rows: Iterable[Mapping[str, Any]], policy: OraclePolicy | None = None) -> OracleTable:
|
|
209
|
+
policy = OraclePolicy() if policy is None else policy
|
|
210
|
+
data = tuple(rows)
|
|
211
|
+
if not data:
|
|
212
|
+
raise ValueError("oracle construction requires at least one benchmark row")
|
|
213
|
+
run_ids = [str(row.get("run_id") or "") for row in data]
|
|
214
|
+
if any(not run_id for run_id in run_ids):
|
|
215
|
+
raise ValueError("every oracle row must provide a non-empty run_id")
|
|
216
|
+
if len(set(run_ids)) != len(run_ids):
|
|
217
|
+
raise ValueError("run_id values must be unique")
|
|
218
|
+
|
|
219
|
+
grouped: dict[str, list[Mapping[str, Any]]] = {}
|
|
220
|
+
for row in data:
|
|
221
|
+
instance = str(row.get("instance") or "")
|
|
222
|
+
if not instance:
|
|
223
|
+
raise ValueError(f"run {row.get('run_id')!r} has no instance")
|
|
224
|
+
grouped.setdefault(instance, []).append(row)
|
|
225
|
+
|
|
226
|
+
entries: list[OracleEntry] = []
|
|
227
|
+
for instance in sorted(grouped):
|
|
228
|
+
group = grouped[instance]
|
|
229
|
+
protocols = {_identity_value(r, "protocol_id") for r in group}
|
|
230
|
+
environments = {_identity_value(r, "environment_id") for r in group}
|
|
231
|
+
instance_hashes = {_identity_value(r, "instance_sha256") for r in group}
|
|
232
|
+
status: OracleStatus | None = None
|
|
233
|
+
reasons: list[str] = []
|
|
234
|
+
if policy.require_same_protocol and (len(protocols) != 1 or None in protocols):
|
|
235
|
+
status = OracleStatus.PROTOCOL_MISMATCH
|
|
236
|
+
reasons.append("candidate rows must provide one identical non-empty protocol_id")
|
|
237
|
+
if status is None and policy.require_same_environment and (len(environments) != 1 or None in environments):
|
|
238
|
+
status = OracleStatus.ENVIRONMENT_MISMATCH
|
|
239
|
+
reasons.append("candidate rows must provide one identical non-empty environment_id")
|
|
240
|
+
if status is None and policy.require_same_instance_sha256:
|
|
241
|
+
if len(instance_hashes) != 1 or None in instance_hashes or not all(_valid_sha256(x) for x in instance_hashes):
|
|
242
|
+
status = OracleStatus.INSTANCE_IDENTITY_MISMATCH
|
|
243
|
+
reasons.append("candidate rows must provide one identical valid instance_sha256")
|
|
244
|
+
|
|
245
|
+
candidates: list[OracleCandidate] = []
|
|
246
|
+
exclusions: list[CandidateExclusion] = []
|
|
247
|
+
for row in sorted(group, key=lambda r: str(r.get("run_id"))):
|
|
248
|
+
candidate, exclusion = _candidate(row, policy)
|
|
249
|
+
if candidate is not None:
|
|
250
|
+
candidates.append(candidate)
|
|
251
|
+
if exclusion is not None:
|
|
252
|
+
exclusions.append(exclusion)
|
|
253
|
+
|
|
254
|
+
oracle_value: float | None = None
|
|
255
|
+
oracle_candidates: list[OracleCandidate] = []
|
|
256
|
+
fastest_runtime: float | None = None
|
|
257
|
+
if status is None:
|
|
258
|
+
if not candidates:
|
|
259
|
+
status = OracleStatus.NO_ELIGIBLE_CANDIDATE
|
|
260
|
+
reasons.append("no validated exact-success row with finite objective and runtime")
|
|
261
|
+
else:
|
|
262
|
+
values = [c.objective for c in candidates]
|
|
263
|
+
oracle_value = min(values) if policy.objective_sense is ObjectiveSense.MINIMIZE else max(values)
|
|
264
|
+
oracle_candidates = [c for c in candidates if _same_objective(c.objective, oracle_value, policy)]
|
|
265
|
+
fastest_runtime = min(c.runtime_s for c in oracle_candidates)
|
|
266
|
+
if policy.runtime_tie_breaker:
|
|
267
|
+
oracle_candidates = [
|
|
268
|
+
c for c in oracle_candidates
|
|
269
|
+
if isclose(c.runtime_s, fastest_runtime, abs_tol=policy.runtime_tolerance_s, rel_tol=0.0)
|
|
270
|
+
]
|
|
271
|
+
status = OracleStatus.AVAILABLE
|
|
272
|
+
|
|
273
|
+
entries.append(OracleEntry(
|
|
274
|
+
instance=instance,
|
|
275
|
+
instance_sha256=next(iter(instance_hashes)) if len(instance_hashes) == 1 else None,
|
|
276
|
+
protocol_id=next(iter(protocols)) if len(protocols) == 1 else None,
|
|
277
|
+
environment_id=next(iter(environments)) if len(environments) == 1 else None,
|
|
278
|
+
status=status,
|
|
279
|
+
objective_sense=policy.objective_sense,
|
|
280
|
+
oracle_objective=oracle_value,
|
|
281
|
+
oracle_run_ids=tuple(sorted(c.run_id for c in oracle_candidates)),
|
|
282
|
+
eligible_run_ids=tuple(sorted(c.run_id for c in candidates)),
|
|
283
|
+
fastest_oracle_runtime_s=fastest_runtime,
|
|
284
|
+
candidates=tuple(candidates),
|
|
285
|
+
exclusions=tuple(exclusions),
|
|
286
|
+
reasons=tuple(reasons),
|
|
287
|
+
))
|
|
288
|
+
return OracleTable(policy=policy, entries=tuple(entries))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProofLevel(IntEnum):
|
|
9
|
+
NONE = 0
|
|
10
|
+
SOLVER_ASSERTED = 1
|
|
11
|
+
PRIMAL_VALIDATED = 2
|
|
12
|
+
BOUND_VERIFIED = 3
|
|
13
|
+
INDEPENDENT_CERTIFICATE = 4
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class ProofAssessment:
|
|
18
|
+
level: ProofLevel
|
|
19
|
+
backend_reported_optimal: bool
|
|
20
|
+
primal_validated: bool
|
|
21
|
+
dual_verified: bool
|
|
22
|
+
gap_verified: bool
|
|
23
|
+
certificate_verified: bool
|
|
24
|
+
independently_verified_optimal: bool
|
|
25
|
+
reasons: tuple[str, ...] = ()
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict[str, Any]:
|
|
28
|
+
payload=asdict(self); payload["level"]=int(self.level); payload["level_name"]=self.level.name.lower(); return payload
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _from_flags(*, backend_reported_optimal: bool, primal_validated: bool, dual_verified: bool, gap_verified: bool, certificate_verified: bool) -> ProofAssessment:
|
|
32
|
+
independently=bool(primal_validated and dual_verified and (gap_verified or certificate_verified)); reasons=[]
|
|
33
|
+
if certificate_verified and independently: level=ProofLevel.INDEPENDENT_CERTIFICATE
|
|
34
|
+
elif primal_validated and dual_verified and gap_verified: level=ProofLevel.BOUND_VERIFIED
|
|
35
|
+
elif primal_validated: level=ProofLevel.PRIMAL_VALIDATED
|
|
36
|
+
elif backend_reported_optimal: level=ProofLevel.SOLVER_ASSERTED
|
|
37
|
+
else: level=ProofLevel.NONE
|
|
38
|
+
if backend_reported_optimal and not primal_validated: reasons.append("backend optimality claim lacks an independently validated primal candidate")
|
|
39
|
+
if primal_validated and not independently: reasons.append("primal feasibility is validated, but complete optimality proof is not independently reconstructed")
|
|
40
|
+
return ProofAssessment(level,backend_reported_optimal,primal_validated,dual_verified,gap_verified,certificate_verified,independently,tuple(reasons))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def assess_solve_result(result: object) -> ProofAssessment:
|
|
44
|
+
evidence=getattr(result,"optimality_evidence",None)
|
|
45
|
+
if evidence is None: return _from_flags(backend_reported_optimal=False,primal_validated=False,dual_verified=False,gap_verified=False,certificate_verified=False)
|
|
46
|
+
return _from_flags(backend_reported_optimal=bool(getattr(evidence,"backend_reported_optimal",False)),primal_validated=bool(getattr(evidence,"primal_validated",False)),dual_verified=bool(getattr(evidence,"dual_verified",False)),gap_verified=bool(getattr(evidence,"gap_verified",False)),certificate_verified=bool(getattr(evidence,"certificate_verified",False)))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def assess_benchmark_row(row: Mapping[str, Any]) -> ProofAssessment:
|
|
50
|
+
evidence=row.get("optimality_evidence")
|
|
51
|
+
if isinstance(evidence,Mapping):
|
|
52
|
+
return _from_flags(backend_reported_optimal=bool(evidence.get("backend_reported_optimal",row.get("public_status")=="valid_optimal")),primal_validated=bool(evidence.get("primal_validated",row.get("validated") is True)),dual_verified=bool(evidence.get("dual_verified",False)),gap_verified=bool(evidence.get("gap_verified",False)),certificate_verified=bool(evidence.get("certificate_verified",False)))
|
|
53
|
+
return _from_flags(backend_reported_optimal=row.get("public_status")=="valid_optimal",primal_validated=row.get("validated") is True,dual_verified=False,gap_verified=False,certificate_verified=False)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from math import isfinite
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class ObjectiveReference:
|
|
9
|
+
name: str
|
|
10
|
+
status: str
|
|
11
|
+
objective: float | None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_miplib_solu(text: str) -> dict[str, ObjectiveReference]:
|
|
15
|
+
"""Parse a MIPLIB-style ``.solu`` objective/status file.
|
|
16
|
+
|
|
17
|
+
Supported records are ``=opt=``, ``=best=``, ``=inf=``, and ``=unkn=``.
|
|
18
|
+
The parser is intentionally strict about duplicate names and malformed objectives so
|
|
19
|
+
benchmark validation cannot silently use ambiguous reference data.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
out: dict[str, ObjectiveReference] = {}
|
|
23
|
+
tag_map = {
|
|
24
|
+
"=opt=": "optimal",
|
|
25
|
+
"=best=": "best_known",
|
|
26
|
+
"=inf=": "infeasible",
|
|
27
|
+
"=unkn=": "unknown",
|
|
28
|
+
}
|
|
29
|
+
for line_no, raw in enumerate(text.splitlines(), start=1):
|
|
30
|
+
line = raw.strip()
|
|
31
|
+
if not line or line.startswith("#"):
|
|
32
|
+
continue
|
|
33
|
+
parts = line.split()
|
|
34
|
+
if len(parts) < 2 or parts[0] not in tag_map:
|
|
35
|
+
raise ValueError(f"line {line_no}: malformed .solu record")
|
|
36
|
+
tag, name = parts[0], parts[1]
|
|
37
|
+
if name in out:
|
|
38
|
+
raise ValueError(f"line {line_no}: duplicate .solu instance {name!r}")
|
|
39
|
+
status = tag_map[tag]
|
|
40
|
+
objective = None
|
|
41
|
+
if status in {"optimal", "best_known"}:
|
|
42
|
+
if len(parts) != 3:
|
|
43
|
+
raise ValueError(f"line {line_no}: {tag} record requires one objective")
|
|
44
|
+
try:
|
|
45
|
+
objective = float(parts[2])
|
|
46
|
+
except ValueError as exc:
|
|
47
|
+
raise ValueError(f"line {line_no}: invalid objective {parts[2]!r}") from exc
|
|
48
|
+
if not isfinite(objective):
|
|
49
|
+
raise ValueError(f"line {line_no}: objective must be finite")
|
|
50
|
+
elif len(parts) != 2:
|
|
51
|
+
raise ValueError(f"line {line_no}: {tag} record must not contain an objective")
|
|
52
|
+
out[name] = ObjectiveReference(name=name, status=status, objective=objective)
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_test_manifest(text: str) -> tuple[str, ...]:
|
|
57
|
+
"""Parse a one-instance-per-line benchmark manifest such as MIPLIB ``.test``."""
|
|
58
|
+
|
|
59
|
+
names: list[str] = []
|
|
60
|
+
seen: set[str] = set()
|
|
61
|
+
for line_no, raw in enumerate(text.splitlines(), start=1):
|
|
62
|
+
line = raw.strip()
|
|
63
|
+
if not line or line.startswith("#"):
|
|
64
|
+
continue
|
|
65
|
+
if any(ch.isspace() for ch in line):
|
|
66
|
+
raise ValueError(f"line {line_no}: manifest entry must be a single filename")
|
|
67
|
+
if line in seen:
|
|
68
|
+
raise ValueError(f"line {line_no}: duplicate manifest entry {line!r}")
|
|
69
|
+
seen.add(line)
|
|
70
|
+
names.append(line)
|
|
71
|
+
if not names:
|
|
72
|
+
raise ValueError("manifest contains no instances")
|
|
73
|
+
return tuple(names)
|