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