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,281 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from math import isclose, isfinite
6
+ from statistics import fmean, median, pstdev
7
+ from typing import Iterable, Mapping, Any
8
+
9
+ from .oracle import ObjectiveSense, OracleEntry, OracleStatus, OracleTable
10
+
11
+
12
+ class RegretStatus(str, Enum):
13
+ COMPARED = "compared"
14
+ ORACLE_UNAVAILABLE = "oracle_unavailable"
15
+ INSTANCE_MISMATCH = "instance_mismatch"
16
+ SELECTED_RUN_MISSING = "selected_run_missing"
17
+ SELECTED_RUN_INELIGIBLE = "selected_run_ineligible"
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class SelectionDecision:
22
+ decision_id: str
23
+ instance: str
24
+ selected_run_id: str | None
25
+ selector_id: str = "policy"
26
+ selector_version: str = "unknown"
27
+ selection_overhead_s: float = 0.0
28
+
29
+ def __post_init__(self) -> None:
30
+ for name in ("decision_id", "instance", "selector_id", "selector_version"):
31
+ value = str(getattr(self, name)).strip()
32
+ if not value:
33
+ raise ValueError(f"{name} must be non-empty")
34
+ object.__setattr__(self, name, value)
35
+ if self.selected_run_id is not None:
36
+ value = str(self.selected_run_id).strip()
37
+ if not value:
38
+ raise ValueError("selected_run_id must be non-empty when provided")
39
+ object.__setattr__(self, "selected_run_id", value)
40
+ overhead = float(self.selection_overhead_s)
41
+ if not isfinite(overhead) or overhead < 0:
42
+ raise ValueError("selection_overhead_s must be finite and non-negative")
43
+ object.__setattr__(self, "selection_overhead_s", overhead)
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class RegretVector:
48
+ decision_id: str
49
+ selector_id: str
50
+ selector_version: str
51
+ instance: str
52
+ selected_run_id: str | None
53
+ oracle_run_ids: tuple[str, ...]
54
+ status: RegretStatus
55
+ comparable: bool
56
+ quality_regret: float | None
57
+ relative_quality_regret: float | None
58
+ runtime_delta_s: float | None
59
+ selection_overhead_s: float
60
+ runtime_regret_s: float | None
61
+ failure_regret: int
62
+ timeout_regret: int
63
+ limit_regret: int
64
+ infeasibility_regret: int
65
+ reasons: tuple[str, ...] = ()
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class MetricSummary:
70
+ observed_count: int
71
+ missing_count: int
72
+ minimum: float | None
73
+ maximum: float | None
74
+ mean: float | None
75
+ median: float | None
76
+ population_stddev: float | None
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class SelectorRegretSummary:
81
+ selector_id: str
82
+ selector_version: str
83
+ decision_count: int
84
+ compared_count: int
85
+ incomparable_count: int
86
+ quality_regret: MetricSummary
87
+ relative_quality_regret: MetricSummary
88
+ runtime_delta_s: MetricSummary
89
+ runtime_regret_s: MetricSummary
90
+ failure_regret_count: int
91
+ timeout_regret_count: int
92
+ limit_regret_count: int
93
+ infeasibility_regret_count: int
94
+ significance_claim_supported: bool = False
95
+
96
+
97
+ def _summary(values: list[float | None]) -> MetricSummary:
98
+ observed = [float(v) for v in values if v is not None]
99
+ missing = len(values) - len(observed)
100
+ if not observed:
101
+ return MetricSummary(0, missing, None, None, None, None, None)
102
+ return MetricSummary(
103
+ observed_count=len(observed),
104
+ missing_count=missing,
105
+ minimum=min(observed),
106
+ maximum=max(observed),
107
+ mean=fmean(observed),
108
+ median=float(median(observed)),
109
+ population_stddev=pstdev(observed) if len(observed) > 1 else 0.0,
110
+ )
111
+
112
+
113
+ def _row_runtime(row: Mapping[str, Any], field: str) -> float | None:
114
+ current: Any = row
115
+ for part in field.split("."):
116
+ if not isinstance(current, Mapping) or part not in current:
117
+ return None
118
+ current = current[part]
119
+ try:
120
+ value = float(current)
121
+ except (TypeError, ValueError):
122
+ return None
123
+ return value if isfinite(value) and value >= 0 else None
124
+
125
+
126
+ def _binary_failure_dimensions(row: Mapping[str, Any]) -> tuple[int, int, int, int]:
127
+ state = str(row.get("state") or "")
128
+ public_status = str(row.get("public_status") or "")
129
+ timeout = int(state == "hard_timeout")
130
+ limit = int(public_status == "feasible_limit")
131
+ infeasible = int(public_status == "infeasible")
132
+ failure = int(
133
+ state in {"missing_instance", "parse_error", "solve_error", "worker_error"}
134
+ or public_status in {"invalid_solution", "error", "unknown", "infeasible_or_unbounded"}
135
+ )
136
+ return failure, timeout, limit, infeasible
137
+
138
+
139
+ def _directional_regret(selected: float, oracle: float, sense: ObjectiveSense, *, atol: float, rtol: float) -> float:
140
+ if isclose(selected, oracle, abs_tol=atol, rel_tol=rtol):
141
+ return 0.0
142
+ raw = selected - oracle if sense is ObjectiveSense.MINIMIZE else oracle - selected
143
+ # An eligible oracle is extremal, but clamping tiny negative numerical drift keeps the
144
+ # metric non-negative if values sit just outside the explicit tolerance boundary.
145
+ return max(0.0, float(raw))
146
+
147
+
148
+ def evaluate_selection_decisions(
149
+ decisions: Iterable[SelectionDecision],
150
+ rows: Iterable[Mapping[str, Any]],
151
+ oracle_table: OracleTable,
152
+ ) -> tuple[RegretVector, ...]:
153
+ decision_rows = tuple(decisions)
154
+ benchmark_rows = tuple(rows)
155
+ if len({d.decision_id for d in decision_rows}) != len(decision_rows):
156
+ raise ValueError("decision_id values must be unique")
157
+ by_run: dict[str, Mapping[str, Any]] = {}
158
+ for row in benchmark_rows:
159
+ run_id = str(row.get("run_id") or "")
160
+ if not run_id:
161
+ raise ValueError("every benchmark row must provide run_id")
162
+ if run_id in by_run:
163
+ raise ValueError(f"duplicate benchmark run_id: {run_id}")
164
+ by_run[run_id] = row
165
+ entries = oracle_table.by_instance()
166
+ vectors: list[RegretVector] = []
167
+
168
+ def emit(decision: SelectionDecision, entry: OracleEntry | None, status: RegretStatus, reasons: tuple[str, ...], *, selected_row: Mapping[str, Any] | None = None) -> None:
169
+ failure = timeout = limit = infeasible = 0
170
+ if selected_row is not None:
171
+ failure, timeout, limit, infeasible = _binary_failure_dimensions(selected_row)
172
+ vectors.append(RegretVector(
173
+ decision_id=decision.decision_id,
174
+ selector_id=decision.selector_id,
175
+ selector_version=decision.selector_version,
176
+ instance=decision.instance,
177
+ selected_run_id=decision.selected_run_id,
178
+ oracle_run_ids=() if entry is None else entry.oracle_run_ids,
179
+ status=status,
180
+ comparable=False,
181
+ quality_regret=None,
182
+ relative_quality_regret=None,
183
+ runtime_delta_s=None,
184
+ selection_overhead_s=decision.selection_overhead_s,
185
+ runtime_regret_s=None,
186
+ failure_regret=failure,
187
+ timeout_regret=timeout,
188
+ limit_regret=limit,
189
+ infeasibility_regret=infeasible,
190
+ reasons=reasons,
191
+ ))
192
+
193
+ for decision in sorted(decision_rows, key=lambda d: d.decision_id):
194
+ entry = entries.get(decision.instance)
195
+ if entry is None:
196
+ emit(decision, None, RegretStatus.INSTANCE_MISMATCH, ("decision instance is absent from oracle table",))
197
+ continue
198
+ if entry.status is not OracleStatus.AVAILABLE:
199
+ emit(decision, entry, RegretStatus.ORACLE_UNAVAILABLE, entry.reasons or (f"oracle status is {entry.status.value}",))
200
+ continue
201
+ if decision.selected_run_id is None:
202
+ emit(decision, entry, RegretStatus.SELECTED_RUN_MISSING, ("selector abstained or did not provide a run_id",))
203
+ continue
204
+ selected = by_run.get(decision.selected_run_id)
205
+ if selected is None:
206
+ emit(decision, entry, RegretStatus.SELECTED_RUN_MISSING, ("selected run is absent from benchmark rows",))
207
+ continue
208
+ if str(selected.get("instance") or "") != decision.instance:
209
+ emit(decision, entry, RegretStatus.INSTANCE_MISMATCH, ("selected run belongs to a different instance",), selected_row=selected)
210
+ continue
211
+ candidate_by_id = {c.run_id: c for c in entry.candidates}
212
+ candidate = candidate_by_id.get(decision.selected_run_id)
213
+ if candidate is None:
214
+ exclusion = next((x for x in entry.exclusions if x.run_id == decision.selected_run_id), None)
215
+ reason = "selected run is not oracle-eligible" if exclusion is None else f"selected run excluded: {exclusion.reason.value}: {exclusion.detail}"
216
+ emit(decision, entry, RegretStatus.SELECTED_RUN_INELIGIBLE, (reason,), selected_row=selected)
217
+ continue
218
+ if entry.oracle_objective is None or entry.fastest_oracle_runtime_s is None:
219
+ raise RuntimeError("oracle-eligible candidate is missing objective/runtime invariants")
220
+ quality = _directional_regret(
221
+ candidate.objective,
222
+ entry.oracle_objective,
223
+ entry.objective_sense,
224
+ atol=oracle_table.policy.objective_atol,
225
+ rtol=oracle_table.policy.objective_rtol,
226
+ )
227
+ if abs(entry.oracle_objective) <= oracle_table.policy.objective_atol:
228
+ relative = None
229
+ reasons = ("relative quality regret is undefined because oracle objective is zero",)
230
+ else:
231
+ relative = quality / abs(entry.oracle_objective)
232
+ reasons = ()
233
+ runtime_delta = candidate.runtime_s - entry.fastest_oracle_runtime_s
234
+ runtime_regret = max(0.0, candidate.runtime_s + decision.selection_overhead_s - entry.fastest_oracle_runtime_s)
235
+ vectors.append(RegretVector(
236
+ decision_id=decision.decision_id,
237
+ selector_id=decision.selector_id,
238
+ selector_version=decision.selector_version,
239
+ instance=decision.instance,
240
+ selected_run_id=decision.selected_run_id,
241
+ oracle_run_ids=entry.oracle_run_ids,
242
+ status=RegretStatus.COMPARED,
243
+ comparable=True,
244
+ quality_regret=quality,
245
+ relative_quality_regret=relative,
246
+ runtime_delta_s=runtime_delta,
247
+ selection_overhead_s=decision.selection_overhead_s,
248
+ runtime_regret_s=runtime_regret,
249
+ failure_regret=0,
250
+ timeout_regret=0,
251
+ limit_regret=0,
252
+ infeasibility_regret=0,
253
+ reasons=reasons,
254
+ ))
255
+ return tuple(vectors)
256
+
257
+
258
+ def summarize_regret_vectors(vectors: Iterable[RegretVector]) -> tuple[SelectorRegretSummary, ...]:
259
+ rows = tuple(vectors)
260
+ grouped: dict[tuple[str, str], list[RegretVector]] = {}
261
+ for row in rows:
262
+ grouped.setdefault((row.selector_id, row.selector_version), []).append(row)
263
+ summaries: list[SelectorRegretSummary] = []
264
+ for (selector_id, selector_version), group in sorted(grouped.items()):
265
+ summaries.append(SelectorRegretSummary(
266
+ selector_id=selector_id,
267
+ selector_version=selector_version,
268
+ decision_count=len(group),
269
+ compared_count=sum(int(v.comparable) for v in group),
270
+ incomparable_count=sum(int(not v.comparable) for v in group),
271
+ quality_regret=_summary([v.quality_regret for v in group]),
272
+ relative_quality_regret=_summary([v.relative_quality_regret for v in group]),
273
+ runtime_delta_s=_summary([v.runtime_delta_s for v in group]),
274
+ runtime_regret_s=_summary([v.runtime_regret_s for v in group]),
275
+ failure_regret_count=sum(v.failure_regret for v in group),
276
+ timeout_regret_count=sum(v.timeout_regret for v in group),
277
+ limit_regret_count=sum(v.limit_regret for v in group),
278
+ infeasibility_regret_count=sum(v.infeasibility_regret for v in group),
279
+ significance_claim_supported=False,
280
+ ))
281
+ return tuple(summaries)
@@ -0,0 +1,34 @@
1
+ """Public exception hierarchy for SolverPilot.
2
+
3
+ M30 freezes these exception classes as the stable error surface for callers that
4
+ need to distinguish environment/setup failures from model-capability failures.
5
+ Solver termination itself remains represented by :class:`solverpilot.PublicStatus`
6
+ inside ``SolveResult`` rather than by exceptions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ class SolverPilotError(Exception):
13
+ """Base class for stable SolverPilot runtime/planning exceptions."""
14
+
15
+
16
+ class BackendUnavailableError(SolverPilotError, RuntimeError):
17
+ """Raised when an explicitly requested backend cannot be used in this environment."""
18
+
19
+
20
+ class CapabilityMismatchError(SolverPilotError, ValueError):
21
+ """Raised when an explicitly chosen backend cannot represent the problem safely."""
22
+
23
+
24
+ class UnknownBackendError(SolverPilotError, KeyError):
25
+ """Raised when a backend identifier is not registered."""
26
+
27
+
28
+ class BudgetNotSupportedError(SolverPilotError, NotImplementedError):
29
+ """Raised when a requested hard budget cannot be enforced safely by a backend."""
30
+
31
+
32
+ class NoCompatibleBackendError(SolverPilotError, RuntimeError):
33
+ """Raised when planning finds no available backend satisfying all hard requirements."""
34
+
@@ -0,0 +1,32 @@
1
+ """Experimental and research-only SolverPilot APIs.
2
+
3
+ Everything in this namespace may change or disappear without the compatibility
4
+ policy applied to the stable top-level ``solverpilot`` API. M30 deliberately moves
5
+ milestone-specific evidence translators and the rejected LP selector evaluator
6
+ here so research artifacts cannot be mistaken for supported product features.
7
+ """
8
+
9
+ from solverpilot.plan.production import (
10
+ M22_OFFICIAL_EVIDENCE,
11
+ production_evidence_from_m22_gate,
12
+ production_evidence_from_m24_public_ood,
13
+ production_evidence_from_m25_opportunity,
14
+ production_evidence_from_m26_validation,
15
+ production_evidence_from_m27_heldout,
16
+ production_evidence_from_m28_native_choose,
17
+ production_evidence_from_m29_value_audit,
18
+ )
19
+ from solverpilot.plan.selective_lp import SelectiveLPDecision, decide_selective_lp_backend
20
+
21
+ __all__ = [
22
+ "M22_OFFICIAL_EVIDENCE",
23
+ "SelectiveLPDecision",
24
+ "decide_selective_lp_backend",
25
+ "production_evidence_from_m22_gate",
26
+ "production_evidence_from_m24_public_ood",
27
+ "production_evidence_from_m25_opportunity",
28
+ "production_evidence_from_m26_validation",
29
+ "production_evidence_from_m27_heldout",
30
+ "production_evidence_from_m28_native_choose",
31
+ "production_evidence_from_m29_value_audit",
32
+ ]
@@ -0,0 +1,56 @@
1
+ """Explicit extension infrastructure for trusted in-process SolverPilot integrations.
2
+
3
+ Extensions are never auto-discovered or auto-executed merely by importing this package.
4
+ Backends and bridges continue to use SolverPilot's canonical registries.
5
+ """
6
+
7
+ from .entrypoints import resolve_entrypoint, resolve_manifest_entrypoint
8
+ from .errors import (
9
+ ExtensionActivationError,
10
+ ExtensionDependencyError,
11
+ ExtensionError,
12
+ ExtensionManifestError,
13
+ ExtensionRegistrationError,
14
+ ExtensionRegistryFrozenError,
15
+ ExtensionResolutionError,
16
+ ExtensionValidationError,
17
+ )
18
+ from .manager import ExtensionManager
19
+ from .manifest import ExtensionDependency, ExtensionKind, ExtensionLifecycle, ExtensionManifest
20
+ from .model import (
21
+ ExportKind,
22
+ Extension,
23
+ ExtensionActivationPlan,
24
+ ExtensionContributions,
25
+ ExtensionSelfCheck,
26
+ NamedExtensionExport,
27
+ StagedExtensionContribution,
28
+ )
29
+ from .versioning import SemanticVersion, version_satisfies
30
+
31
+ __all__ = [
32
+ "ExportKind",
33
+ "Extension",
34
+ "ExtensionActivationError",
35
+ "ExtensionActivationPlan",
36
+ "ExtensionContributions",
37
+ "ExtensionDependency",
38
+ "ExtensionDependencyError",
39
+ "ExtensionError",
40
+ "ExtensionKind",
41
+ "ExtensionLifecycle",
42
+ "ExtensionManager",
43
+ "ExtensionManifest",
44
+ "ExtensionManifestError",
45
+ "ExtensionRegistrationError",
46
+ "ExtensionRegistryFrozenError",
47
+ "ExtensionResolutionError",
48
+ "ExtensionSelfCheck",
49
+ "ExtensionValidationError",
50
+ "NamedExtensionExport",
51
+ "SemanticVersion",
52
+ "StagedExtensionContribution",
53
+ "resolve_entrypoint",
54
+ "resolve_manifest_entrypoint",
55
+ "version_satisfies",
56
+ ]
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ from typing import Any
5
+
6
+ from .errors import ExtensionResolutionError
7
+ from .manifest import ExtensionManifest
8
+
9
+
10
+ def resolve_entrypoint(entrypoint: str) -> Any:
11
+ """Resolve ``package.module:Attribute`` only when explicitly requested by the caller."""
12
+
13
+ raw = str(entrypoint).strip()
14
+ if ":" not in raw:
15
+ raise ExtensionResolutionError("entrypoint must use the form 'package.module:Attribute'")
16
+ module_name, attribute_path = raw.split(":", 1)
17
+ try:
18
+ value: Any = importlib.import_module(module_name)
19
+ except Exception as exc:
20
+ raise ExtensionResolutionError(f"could not import extension module {module_name!r}: {exc}") from exc
21
+ for attribute in attribute_path.split("."):
22
+ try:
23
+ value = getattr(value, attribute)
24
+ except AttributeError as exc:
25
+ raise ExtensionResolutionError(
26
+ f"entrypoint attribute {attribute_path!r} was not found in {module_name!r}"
27
+ ) from exc
28
+ return value
29
+
30
+
31
+ def resolve_manifest_entrypoint(manifest: ExtensionManifest) -> Any:
32
+ if manifest.entrypoint is None:
33
+ raise ExtensionResolutionError(f"extension {manifest.extension_id!r} declares no entrypoint")
34
+ return resolve_entrypoint(manifest.entrypoint)
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from solverpilot.exceptions import SolverPilotError
4
+
5
+
6
+ class ExtensionError(SolverPilotError):
7
+ """Base class for SolverPilot extension-system errors."""
8
+
9
+
10
+ class ExtensionManifestError(ExtensionError, ValueError):
11
+ """Raised when an extension manifest is malformed or non-canonical."""
12
+
13
+
14
+ class ExtensionRegistrationError(ExtensionError, ValueError):
15
+ """Raised when extension registration would violate registry invariants."""
16
+
17
+
18
+ class ExtensionDependencyError(ExtensionError, ValueError):
19
+ """Raised when extension dependencies are missing, incompatible, or cyclic."""
20
+
21
+
22
+ class ExtensionRegistryFrozenError(ExtensionError, RuntimeError):
23
+ """Raised when a frozen extension manager is mutated."""
24
+
25
+
26
+ class ExtensionValidationError(ExtensionError, RuntimeError):
27
+ """Raised when an extension fails a self-check or contribution validation."""
28
+
29
+
30
+ class ExtensionActivationError(ExtensionError, RuntimeError):
31
+ """Raised when a staged activation plan cannot be safely applied."""
32
+
33
+
34
+ class ExtensionResolutionError(ExtensionError, ImportError):
35
+ """Raised when an explicitly requested extension entry point cannot be resolved."""