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,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)