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,60 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ import numpy as np
4
+ from .ad import NLPDerivativeEngine, evaluate_node
5
+ from .ir import NLPProblem
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class NLPValidationReport:
9
+ valid: bool
10
+ max_bound_violation: float
11
+ max_constraint_violation: float
12
+ objective_recomputed: float | None
13
+ objective_reported: float | None
14
+ objective_difference: float | None
15
+ objective_consistent: bool | None
16
+ finite: bool
17
+ warnings: tuple[str,...]=()
18
+
19
+
20
+ def _violation(v, lo, hi):
21
+ return np.maximum(np.maximum(lo-v, v-hi), 0.0)
22
+
23
+ def validate_nlp_solution(problem:NLPProblem, x, *, objective_reported=None, atol=1e-7, rtol=1e-7):
24
+ for name, raw in (("atol", atol), ("rtol", rtol)):
25
+ if isinstance(raw, bool) or type(raw) not in (int, float) or not np.isfinite(float(raw)) or float(raw) < 0:
26
+ raise ValueError(f'{name} must be finite and non-negative')
27
+ atol=float(atol); rtol=float(rtol)
28
+ if x is None:
29
+ return NLPValidationReport(False, np.inf, np.inf, None, objective_reported, None, None, False, ('missing primal solution',))
30
+ x=np.asarray(x,dtype=float).reshape(-1)
31
+ if x.shape != (problem.n_variables,) or not np.all(np.isfinite(x)):
32
+ return NLPValidationReport(False, np.inf, np.inf, None, objective_reported, None, None, False, ('non-finite or wrong-shaped primal solution',))
33
+ bv=float(np.max(_violation(x,problem.variable_lower,problem.variable_upper))) if x.size else 0.0
34
+ vals=[]
35
+ finite=True
36
+ for c in problem.constraints:
37
+ val=np.asarray(evaluate_node(problem,c.node,x),dtype=float).reshape(-1)
38
+ if not np.all(np.isfinite(val)): finite=False
39
+ vals.append((val,c.lower,c.upper))
40
+ cv=max((float(np.max(_violation(v,l,u))) for v,l,u in vals), default=0.0) if finite else np.inf
41
+ obj_arr=np.asarray(evaluate_node(problem,problem.objective_node,x),dtype=float).reshape(-1)
42
+ obj=float(obj_arr[0]) if obj_arr.size==1 and np.isfinite(obj_arr[0]) else None
43
+ if obj is None:
44
+ finite=False
45
+ diff=None; consistent=None
46
+ reported_finite=True
47
+ if objective_reported is not None:
48
+ try:
49
+ reported_finite=np.isfinite(float(objective_reported))
50
+ except (TypeError, ValueError):
51
+ reported_finite=False
52
+ if not reported_finite:
53
+ finite=False
54
+ diff=np.inf
55
+ consistent=False
56
+ if objective_reported is not None and obj is not None and reported_finite:
57
+ diff=abs(float(objective_reported)-obj)
58
+ consistent=diff <= atol + rtol*max(1.0,abs(obj),abs(float(objective_reported)))
59
+ valid=finite and bv<=atol and cv<=atol and (consistent is not False)
60
+ return NLPValidationReport(valid,bv,cv,obj,objective_reported,diff,consistent,finite,())
@@ -0,0 +1,33 @@
1
+ """Planning primitives.
2
+
3
+ Only the names listed in ``__all__`` are part of the M30 stable planning surface.
4
+ Milestone-specific evidence translators and rejected research selectors live under
5
+ ``solverpilot.experimental``.
6
+ """
7
+
8
+ from .model import CandidatePlan, HealthPolicy, PlannerContext, SolveBudget, SolveIntent, SolvePlan
9
+ from .planner import NoCompatibleBackendError, plan_solve
10
+ from .production import (
11
+ EvidenceClass,
12
+ M22_OFFICIAL_EVIDENCE,
13
+ PerformancePolicy,
14
+ ProductionDecision,
15
+ ProductionEvidence,
16
+ plan_production_solve,
17
+ )
18
+
19
+ __all__ = [
20
+ "CandidatePlan",
21
+ "EvidenceClass",
22
+ "HealthPolicy",
23
+ "NoCompatibleBackendError",
24
+ "PerformancePolicy",
25
+ "PlannerContext",
26
+ "ProductionDecision",
27
+ "ProductionEvidence",
28
+ "SolveBudget",
29
+ "SolveIntent",
30
+ "SolvePlan",
31
+ "plan_production_solve",
32
+ "plan_solve",
33
+ ]
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ import math
6
+
7
+ from solverpilot.inspect import ProblemFingerprint
8
+
9
+
10
+ class SolveIntent(str, Enum):
11
+ PROVE_OPTIMAL = "prove_optimal"
12
+ BEST_FEASIBLE_UNDER_BUDGET = "best_feasible_under_budget"
13
+ FAST_FEASIBLE = "fast_feasible"
14
+ BALANCED = "balanced"
15
+ HIGH_ACCURACY = "high_accuracy"
16
+ LOW_MEMORY = "low_memory"
17
+
18
+
19
+ class HealthPolicy(str, Enum):
20
+ """How planner evidence from explicit backend probes is used.
21
+
22
+ ``IGNORE`` preserves the capability-only planner. ``PREFER_HEALTHY`` excludes
23
+ backends that were explicitly observed unhealthy and gives a small tie-break to
24
+ healthy backends while leaving unprobed backends eligible. ``REQUIRE_HEALTHY``
25
+ admits only backends with a successful active smoke probe.
26
+ """
27
+
28
+ IGNORE = "ignore"
29
+ PREFER_HEALTHY = "prefer_healthy"
30
+ REQUIRE_HEALTHY = "require_healthy"
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class SolveBudget:
35
+ wall_time_s: float | None = None
36
+ memory_mb: int | None = None
37
+ threads: int | None = None
38
+
39
+ def __post_init__(self) -> None:
40
+ if self.wall_time_s is not None:
41
+ if isinstance(self.wall_time_s, bool) or type(self.wall_time_s) not in (int, float):
42
+ raise ValueError("wall_time_s must be a finite positive number")
43
+ value = float(self.wall_time_s)
44
+ if not math.isfinite(value) or value <= 0:
45
+ raise ValueError("wall_time_s must be a finite positive number")
46
+ object.__setattr__(self, "wall_time_s", value)
47
+ for name in ("memory_mb", "threads"):
48
+ raw = getattr(self, name)
49
+ if raw is None:
50
+ continue
51
+ if isinstance(raw, bool) or type(raw) is not int or raw <= 0:
52
+ raise ValueError(f"{name} must be a positive integer")
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class PlannerContext:
57
+ previous_backend: str | None = None
58
+ session_revision: int | None = None
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class CandidatePlan:
63
+ backend: str
64
+ score: float
65
+ rationale: tuple[str, ...]
66
+ health_status: str | None = None
67
+
68
+
69
+ @dataclass(frozen=True, slots=True)
70
+ class SolvePlan:
71
+ selected_backend: str
72
+ candidates: tuple[CandidatePlan, ...]
73
+ intent: SolveIntent
74
+ budget: SolveBudget
75
+ fingerprint: ProblemFingerprint
76
+ strategy: str
77
+ rationale: tuple[str, ...]
78
+ evidence_level: str = "capability_only"
79
+ health_policy: HealthPolicy = HealthPolicy.IGNORE
@@ -0,0 +1,201 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from solverpilot.backends import Backend, BackendHealthReport, BackendProbeStatus, BackendRegistry
6
+ from solverpilot.capabilities import SupportLevel, compatible, requirements_for
7
+ from solverpilot.inspect import ProblemFingerprint, inspect_problem
8
+ from solverpilot.exceptions import NoCompatibleBackendError
9
+ from solverpilot.problem import LinearProblem, QuadraticProblem
10
+
11
+ from .model import CandidatePlan, HealthPolicy, PlannerContext, SolveBudget, SolveIntent, SolvePlan
12
+
13
+
14
+ def _honors_intent(backend: Backend, intent: SolveIntent) -> tuple[bool, str | None]:
15
+ """Return whether a backend can honor hard semantic parts of an intent.
16
+
17
+ Most intents are performance preferences and remain ranking concerns until we have
18
+ empirical models. ``PROVE_OPTIMAL`` requests a backend capable of a solver-level
19
+ optimal termination/certificate. It does not promise independent proof reconstruction;
20
+ callers must inspect ``SolveResult.optimality_evidence`` for the actual trust level.
21
+ """
22
+ if intent is SolveIntent.PROVE_OPTIMAL and backend.manifest.metadata.get(
23
+ "no_optimality_certificate"
24
+ ) is True:
25
+ return False, "backend explicitly lacks an optimality certificate"
26
+ return True, None
27
+
28
+
29
+ def _health_map(reports: Iterable[BackendHealthReport] | None) -> dict[str, BackendHealthReport]:
30
+ if reports is None:
31
+ return {}
32
+ result: dict[str, BackendHealthReport] = {}
33
+ for report in reports:
34
+ if report.backend in result:
35
+ raise ValueError(f"duplicate backend health report: {report.backend}")
36
+ result[report.backend] = report
37
+ return result
38
+
39
+
40
+ def _candidate(
41
+ backend: Backend,
42
+ problem: LinearProblem | QuadraticProblem,
43
+ *,
44
+ context: PlannerContext,
45
+ health_report: BackendHealthReport | None,
46
+ health_policy: HealthPolicy,
47
+ ) -> CandidatePlan:
48
+ requirements = requirements_for(problem)
49
+ rationale: list[str] = []
50
+ score = 0.0
51
+
52
+ for capability in sorted(requirements.required, key=lambda c: c.value):
53
+ level = backend.manifest.support(capability)
54
+ if level is SupportLevel.NATIVE:
55
+ score += 100.0
56
+ rationale.append(f"native support: {capability.value}")
57
+ elif level is SupportLevel.EMULATED_SAFE:
58
+ score += 80.0
59
+ rationale.append(f"safe emulation: {capability.value}")
60
+
61
+ health_status = None if health_report is None else health_report.status.value
62
+ if health_policy is not HealthPolicy.IGNORE:
63
+ if health_report is None:
64
+ rationale.append("no active backend health probe supplied")
65
+ elif health_report.status is BackendProbeStatus.HEALTHY:
66
+ # Deliberately smaller than a capability support step: health is a tie-break,
67
+ # not an empirical performance claim.
68
+ score += 1.0
69
+ rationale.append("active backend smoke probe passed")
70
+ else:
71
+ rationale.append(f"backend health probe status: {health_report.status.value}")
72
+
73
+ # Stability is a deliberately tiny tie-breaker, not a performance claim.
74
+ if context.previous_backend == backend.manifest.name:
75
+ score += 0.1
76
+ rationale.append("small continuity tie-break: previous backend")
77
+
78
+ if backend.manifest.metadata.get("development_bridge"):
79
+ rationale.append("development bridge; no native state-reuse claim")
80
+
81
+ return CandidatePlan(
82
+ backend=backend.manifest.name,
83
+ score=score,
84
+ rationale=tuple(rationale),
85
+ health_status=health_status,
86
+ )
87
+
88
+
89
+ def plan_solve(
90
+ problem: LinearProblem | QuadraticProblem,
91
+ registry: BackendRegistry,
92
+ *,
93
+ intent: SolveIntent | str = SolveIntent.BALANCED,
94
+ budget: SolveBudget | None = None,
95
+ fingerprint: ProblemFingerprint | None = None,
96
+ context: PlannerContext | None = None,
97
+ health_reports: Iterable[BackendHealthReport] | None = None,
98
+ health_policy: HealthPolicy | str = HealthPolicy.IGNORE,
99
+ ) -> SolvePlan:
100
+ intent = SolveIntent(intent)
101
+ health_policy = HealthPolicy(health_policy)
102
+ budget = SolveBudget() if budget is None else budget
103
+ fingerprint = inspect_problem(problem) if fingerprint is None else fingerprint
104
+ context = PlannerContext() if context is None else context
105
+ requirements = requirements_for(problem)
106
+ health = _health_map(health_reports)
107
+
108
+ eligible: list[tuple[int, CandidatePlan]] = []
109
+ unavailable_compatible: list[str] = []
110
+ rejected_by_health: list[str] = []
111
+ rejected_by_intent: list[str] = []
112
+
113
+ for registration_index, backend in enumerate(registry.all()):
114
+ if not compatible(backend.manifest, requirements):
115
+ continue
116
+ if not backend.is_available():
117
+ unavailable_compatible.append(backend.manifest.name)
118
+ continue
119
+
120
+ honors, intent_reason = _honors_intent(backend, intent)
121
+ if not honors:
122
+ rejected_by_intent.append(
123
+ f"{backend.manifest.name} ({intent_reason})"
124
+ )
125
+ continue
126
+
127
+ report = health.get(backend.manifest.name)
128
+ if report is not None and report.version != backend.manifest.version:
129
+ # Health evidence is version-specific. A report from another solver/package
130
+ # version is stale and must not authorize execution.
131
+ report = None
132
+ if health_policy is HealthPolicy.REQUIRE_HEALTHY:
133
+ if report is None or report.status is not BackendProbeStatus.HEALTHY:
134
+ rejected_by_health.append(backend.manifest.name)
135
+ continue
136
+ elif health_policy is HealthPolicy.PREFER_HEALTHY:
137
+ if report is not None and report.status in {
138
+ BackendProbeStatus.UNHEALTHY,
139
+ BackendProbeStatus.UNAVAILABLE,
140
+ }:
141
+ rejected_by_health.append(backend.manifest.name)
142
+ continue
143
+
144
+ eligible.append(
145
+ (
146
+ registration_index,
147
+ _candidate(
148
+ backend,
149
+ problem,
150
+ context=context,
151
+ health_report=report,
152
+ health_policy=health_policy,
153
+ ),
154
+ )
155
+ )
156
+
157
+ if not eligible:
158
+ required = ", ".join(sorted(c.value for c in requirements.required))
159
+ extras: list[str] = []
160
+ if unavailable_compatible:
161
+ extras.append(f"compatible but unavailable: {', '.join(unavailable_compatible)}")
162
+ if rejected_by_health:
163
+ extras.append(f"rejected by health policy: {', '.join(rejected_by_health)}")
164
+ if rejected_by_intent:
165
+ extras.append(f"rejected by solve intent: {', '.join(rejected_by_intent)}")
166
+ suffix = "" if not extras else " " + "; ".join(extras) + "."
167
+ raise NoCompatibleBackendError(
168
+ f"no available backend supports required capabilities: {required}.{suffix}"
169
+ )
170
+
171
+ # Score first. Registration order is the explicit deterministic tie-breaker.
172
+ ranked = sorted(eligible, key=lambda item: (-item[1].score, item[0]))
173
+ candidates = tuple(item[1] for item in ranked)
174
+ selected = candidates[0]
175
+
176
+ rationale = [
177
+ f"selected {selected.backend} after hard capability and availability filtering",
178
+ "no empirical performance model is active",
179
+ ]
180
+ if health_policy is not HealthPolicy.IGNORE:
181
+ rationale.append(f"backend health policy: {health_policy.value}")
182
+ if intent is SolveIntent.PROVE_OPTIMAL:
183
+ rationale.append("hard intent gate excluded backends that explicitly lack optimality certificates")
184
+ tied = [c for c in candidates if c.score == selected.score]
185
+ if len(tied) > 1:
186
+ rationale.append(
187
+ "top candidates tied on evidence-backed score; registration order used as tie-break"
188
+ )
189
+
190
+ evidence_level = "capability_only" if health_policy is HealthPolicy.IGNORE else "capability+active_health"
191
+ return SolvePlan(
192
+ selected_backend=selected.backend,
193
+ candidates=candidates,
194
+ intent=intent,
195
+ budget=budget,
196
+ fingerprint=fingerprint,
197
+ strategy="single_backend",
198
+ rationale=tuple(rationale),
199
+ evidence_level=evidence_level,
200
+ health_policy=health_policy,
201
+ )