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,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExactnessClass(str, Enum):
|
|
12
|
+
EXACT_EQUIVALENT = "exact_equivalent"
|
|
13
|
+
CERTIFIED_RELAXATION = "certified_relaxation"
|
|
14
|
+
HEURISTIC = "heuristic"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MappingAvailability(str, Enum):
|
|
18
|
+
IDENTITY = "identity"
|
|
19
|
+
AVAILABLE = "available"
|
|
20
|
+
UNAVAILABLE = "unavailable"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _freeze(value: Any) -> Any:
|
|
24
|
+
if isinstance(value, Mapping):
|
|
25
|
+
return MappingProxyType({str(k): _freeze(v) for k, v in value.items()})
|
|
26
|
+
if isinstance(value, list):
|
|
27
|
+
return tuple(_freeze(v) for v in value)
|
|
28
|
+
if isinstance(value, tuple):
|
|
29
|
+
return tuple(_freeze(v) for v in value)
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _jsonable(value: Any) -> Any:
|
|
34
|
+
if isinstance(value, Mapping):
|
|
35
|
+
return {str(k): _jsonable(v) for k, v in value.items()}
|
|
36
|
+
if isinstance(value, tuple):
|
|
37
|
+
return [_jsonable(v) for v in value]
|
|
38
|
+
if isinstance(value, Enum):
|
|
39
|
+
return value.value
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class PreconditionCertificate:
|
|
45
|
+
kind: str
|
|
46
|
+
payload: Mapping[str, Any] = field(default_factory=dict)
|
|
47
|
+
certificate_version: str = "solverpilot.bridge-certificate.p4.v1"
|
|
48
|
+
|
|
49
|
+
def __post_init__(self) -> None:
|
|
50
|
+
object.__setattr__(self, "payload", _freeze(self.payload))
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def sha256(self) -> str:
|
|
54
|
+
raw = json.dumps(
|
|
55
|
+
{"version": self.certificate_version, "kind": self.kind, "payload": _jsonable(self.payload)},
|
|
56
|
+
sort_keys=True,
|
|
57
|
+
separators=(",", ":"),
|
|
58
|
+
ensure_ascii=True,
|
|
59
|
+
).encode()
|
|
60
|
+
return hashlib.sha256(raw).hexdigest()
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
return {
|
|
64
|
+
"kind": self.kind,
|
|
65
|
+
"payload": _jsonable(self.payload),
|
|
66
|
+
"certificate_version": self.certificate_version,
|
|
67
|
+
"sha256": self.sha256,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class TransformationStep:
|
|
73
|
+
step_id: str
|
|
74
|
+
bridge_id: str
|
|
75
|
+
bridge_version: str
|
|
76
|
+
source_entity_ids: tuple[str, ...]
|
|
77
|
+
generated_target_ids: tuple[str, ...]
|
|
78
|
+
exactness: ExactnessClass
|
|
79
|
+
preconditions: tuple[PreconditionCertificate, ...] = ()
|
|
80
|
+
primal_mapping: MappingAvailability = MappingAvailability.IDENTITY
|
|
81
|
+
dual_mapping: MappingAvailability = MappingAvailability.UNAVAILABLE
|
|
82
|
+
certificate_mapping: MappingAvailability = MappingAvailability.UNAVAILABLE
|
|
83
|
+
mutation_compatibility: tuple[str, ...] = ()
|
|
84
|
+
size_delta: Mapping[str, int] = field(default_factory=dict)
|
|
85
|
+
numerical_risk: Mapping[str, Any] = field(default_factory=dict)
|
|
86
|
+
notes: tuple[str, ...] = ()
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
object.__setattr__(self, "size_delta", _freeze(self.size_delta))
|
|
90
|
+
object.__setattr__(self, "numerical_risk", _freeze(self.numerical_risk))
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> dict[str, Any]:
|
|
93
|
+
return {
|
|
94
|
+
"step_id": self.step_id,
|
|
95
|
+
"bridge_id": self.bridge_id,
|
|
96
|
+
"bridge_version": self.bridge_version,
|
|
97
|
+
"source_entity_ids": list(self.source_entity_ids),
|
|
98
|
+
"generated_target_ids": list(self.generated_target_ids),
|
|
99
|
+
"exactness": self.exactness.value,
|
|
100
|
+
"preconditions": [c.to_dict() for c in self.preconditions],
|
|
101
|
+
"primal_mapping": self.primal_mapping.value,
|
|
102
|
+
"dual_mapping": self.dual_mapping.value,
|
|
103
|
+
"certificate_mapping": self.certificate_mapping.value,
|
|
104
|
+
"mutation_compatibility": list(self.mutation_compatibility),
|
|
105
|
+
"size_delta": dict(self.size_delta),
|
|
106
|
+
"numerical_risk": _jsonable(self.numerical_risk),
|
|
107
|
+
"notes": list(self.notes),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class TransformationTape:
|
|
113
|
+
steps: tuple[TransformationStep, ...] = ()
|
|
114
|
+
schema_version: str = "solverpilot.transformation-tape.p4.v1"
|
|
115
|
+
|
|
116
|
+
def to_dicts(self) -> tuple[dict[str, Any], ...]:
|
|
117
|
+
return tuple(step.to_dict() for step in self.steps)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def certificate_hashes(self) -> tuple[str, ...]:
|
|
121
|
+
return tuple(cert.sha256 for step in self.steps for cert in step.preconditions)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def plan_sensitive_dependencies(self) -> tuple[str, ...]:
|
|
125
|
+
deps: list[str] = []
|
|
126
|
+
for step in self.steps:
|
|
127
|
+
for cert in step.preconditions:
|
|
128
|
+
raw = cert.payload.get("parameter_dependencies", ())
|
|
129
|
+
for dep in raw:
|
|
130
|
+
dep = str(dep)
|
|
131
|
+
if dep not in deps:
|
|
132
|
+
deps.append(dep)
|
|
133
|
+
return tuple(deps)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=True)
|
|
137
|
+
class BridgePolicy:
|
|
138
|
+
mode: str = "safe"
|
|
139
|
+
allow_certified_relaxations: bool = False
|
|
140
|
+
allow_heuristic: bool = False
|
|
141
|
+
require_finite_certified_big_m: bool = True
|
|
142
|
+
policy_version: str = "solverpilot.bridge-policy.p4.safe.v1"
|
|
143
|
+
|
|
144
|
+
def permits(self, exactness: ExactnessClass) -> bool:
|
|
145
|
+
if exactness is ExactnessClass.EXACT_EQUIVALENT:
|
|
146
|
+
return True
|
|
147
|
+
if exactness is ExactnessClass.CERTIFIED_RELAXATION:
|
|
148
|
+
return self.allow_certified_relaxations
|
|
149
|
+
return self.allow_heuristic
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from solverpilot.problem import VariableDomain
|
|
9
|
+
|
|
10
|
+
from .indicator import AffineScalar
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class SemanticPrimalValidation:
|
|
15
|
+
valid: bool
|
|
16
|
+
max_violation: float
|
|
17
|
+
violations: tuple[str, ...]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _eval_expr(model, node, xflat: np.ndarray):
|
|
21
|
+
from solverpilot.model.compiler import _Evaluator, _variable_layout
|
|
22
|
+
offsets, *_ = _variable_layout(model)
|
|
23
|
+
ev = _Evaluator(model, offsets)
|
|
24
|
+
arr = ev.evaluate(node)
|
|
25
|
+
out = np.empty(arr.shape, dtype=float)
|
|
26
|
+
if arr.shape:
|
|
27
|
+
for idx in np.ndindex(arr.shape):
|
|
28
|
+
p = arr[idx]
|
|
29
|
+
val = p.constant + sum(float(a) * float(xflat[i]) for i, a in p.linear.items())
|
|
30
|
+
if p.quadratic:
|
|
31
|
+
val += sum(float(q) * float(xflat[i]) * float(xflat[j]) for (i, j), q in p.quadratic.items())
|
|
32
|
+
out[idx] = val
|
|
33
|
+
else:
|
|
34
|
+
p = arr[()]
|
|
35
|
+
val = p.constant + sum(float(a) * float(xflat[i]) for i, a in p.linear.items())
|
|
36
|
+
if p.quadratic:
|
|
37
|
+
val += sum(float(q) * float(xflat[i]) * float(xflat[j]) for (i, j), q in p.quadratic.items())
|
|
38
|
+
out = np.asarray(val)
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_semantic_primal(model, x: Any, *, atol: float = 1e-8) -> SemanticPrimalValidation:
|
|
43
|
+
from solverpilot.model.model import Constraint, IndicatorConstraint
|
|
44
|
+
from solverpilot.model.sets import EqualTo, GreaterThan, Interval, LessThan, PositiveSemidefiniteCone, RotatedSecondOrderCone, SecondOrderCone
|
|
45
|
+
from solverpilot.model.compiler import _variable_layout
|
|
46
|
+
|
|
47
|
+
if isinstance(atol, bool) or type(atol) not in (int, float) or not np.isfinite(float(atol)) or float(atol) < 0:
|
|
48
|
+
raise ValueError("atol must be finite and non-negative")
|
|
49
|
+
atol = float(atol)
|
|
50
|
+
offsets, n, vl, vu, domains, _ = _variable_layout(model)
|
|
51
|
+
xx = np.asarray(x, dtype=float).reshape(-1)
|
|
52
|
+
if xx.shape != (n,):
|
|
53
|
+
return SemanticPrimalValidation(False, float("inf"), (f"expected primal shape ({n},), got {xx.shape}",))
|
|
54
|
+
if not np.all(np.isfinite(xx)):
|
|
55
|
+
return SemanticPrimalValidation(False, float("inf"), ("primal solution contains NaN or infinity",))
|
|
56
|
+
violations: list[str] = []
|
|
57
|
+
max_v = 0.0
|
|
58
|
+
for i in range(n):
|
|
59
|
+
v = max(float(vl[i] - xx[i]), float(xx[i] - vu[i]), 0.0)
|
|
60
|
+
if domains[i] in {VariableDomain.INTEGER.value, VariableDomain.BINARY.value}:
|
|
61
|
+
v = max(v, abs(float(xx[i] - round(xx[i]))))
|
|
62
|
+
if v > atol:
|
|
63
|
+
violations.append(f"variable[{i}] violation={v}")
|
|
64
|
+
max_v = max(max_v, v)
|
|
65
|
+
|
|
66
|
+
def check_set(value: float, set_, label: str) -> None:
|
|
67
|
+
nonlocal max_v
|
|
68
|
+
if isinstance(set_, LessThan): v = max(0.0, value - set_.upper)
|
|
69
|
+
elif isinstance(set_, GreaterThan): v = max(0.0, set_.lower - value)
|
|
70
|
+
elif isinstance(set_, EqualTo): v = abs(value - set_.value)
|
|
71
|
+
elif isinstance(set_, Interval): v = max(0.0, set_.lower - value, value - set_.upper)
|
|
72
|
+
else: v = float("inf")
|
|
73
|
+
max_v = max(max_v, v)
|
|
74
|
+
if v > atol: violations.append(f"{label} violation={v}")
|
|
75
|
+
|
|
76
|
+
for constraint in model.constraints:
|
|
77
|
+
if isinstance(constraint, Constraint):
|
|
78
|
+
arr = np.asarray(_eval_expr(model, constraint.function._node, xx), dtype=float)
|
|
79
|
+
if isinstance(constraint.set, SecondOrderCone):
|
|
80
|
+
y = arr.reshape(-1); t = float(y[0]); norm = float(np.linalg.norm(y[1:])); v = max(0.0, norm - t)
|
|
81
|
+
max_v = max(max_v, v)
|
|
82
|
+
if v > atol: violations.append(f"constraint {constraint.entity_id.value} SOC violation={v}")
|
|
83
|
+
continue
|
|
84
|
+
if isinstance(constraint.set, RotatedSecondOrderCone):
|
|
85
|
+
y = arr.reshape(-1); u = float(y[0]); vv = float(y[1]); norm2 = float(np.dot(y[2:], y[2:])); v = max(0.0, -u, -vv, norm2 - 2.0*u*vv)
|
|
86
|
+
max_v = max(max_v, v)
|
|
87
|
+
if v > atol: violations.append(f"constraint {constraint.entity_id.value} rotated-SOC violation={v}")
|
|
88
|
+
continue
|
|
89
|
+
if isinstance(constraint.set, PositiveSemidefiniteCone):
|
|
90
|
+
M = arr.reshape(constraint.set.dimension, constraint.set.dimension)
|
|
91
|
+
asym = float(np.max(np.abs(M-M.T))) if M.size else 0.0
|
|
92
|
+
eigmin = float(np.linalg.eigvalsh(0.5*(M+M.T))[0]) if M.size else 0.0
|
|
93
|
+
v = max(asym, max(0.0, -eigmin))
|
|
94
|
+
max_v = max(max_v, v)
|
|
95
|
+
if v > atol: violations.append(f"constraint {constraint.entity_id.value} PSD violation={v}")
|
|
96
|
+
continue
|
|
97
|
+
vals = arr.reshape(-1) if arr.shape else np.asarray([float(arr)])
|
|
98
|
+
for k, value in enumerate(vals):
|
|
99
|
+
check_set(float(value), constraint.set, f"constraint {constraint.entity_id.value}[{k}]")
|
|
100
|
+
elif isinstance(constraint, IndicatorConstraint):
|
|
101
|
+
start, end = offsets[constraint.indicator.id.value]
|
|
102
|
+
if end - start != 1:
|
|
103
|
+
violations.append(f"indicator {constraint.entity_id.value} is not scalar")
|
|
104
|
+
max_v = float("inf")
|
|
105
|
+
continue
|
|
106
|
+
z = int(round(float(xx[start])))
|
|
107
|
+
if z == constraint.active_value:
|
|
108
|
+
value = float(np.asarray(_eval_expr(model, constraint.function._node, xx)))
|
|
109
|
+
check_set(value, constraint.set, f"indicator {constraint.entity_id.value}")
|
|
110
|
+
return SemanticPrimalValidation(not violations, max_v, tuple(violations))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from .enums import Capability, SupportLevel
|
|
2
|
+
from .manifest import BackendManifest
|
|
3
|
+
from .requirements import CapabilityRequirements, compatible, requirements_for
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"BackendManifest",
|
|
7
|
+
"Capability",
|
|
8
|
+
"CapabilityRequirements",
|
|
9
|
+
"SupportLevel",
|
|
10
|
+
"compatible",
|
|
11
|
+
"requirements_for",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
from .v2 import (
|
|
15
|
+
CAPABILITY_SCHEMA_VERSION, BackendCapabilityManifestV2, CapabilityCheck, CapabilityClaim,
|
|
16
|
+
CapabilityEvidence, CapabilityKey, CapabilityMode, CapabilityStatus, VerificationLevel,
|
|
17
|
+
project_legacy_manifest, with_verified_claims, CapabilityRequirementsV2, requirements_v2_for, compatible_v2,
|
|
18
|
+
)
|
|
19
|
+
from .conformance import BackendConformanceReport, ConformanceCheck, conform_backend_problem_classes, conform_backends
|
|
20
|
+
|
|
21
|
+
__all__ += [
|
|
22
|
+
"CAPABILITY_SCHEMA_VERSION", "BackendCapabilityManifestV2", "CapabilityCheck", "CapabilityClaim",
|
|
23
|
+
"CapabilityEvidence", "CapabilityKey", "CapabilityMode", "CapabilityStatus", "VerificationLevel",
|
|
24
|
+
"project_legacy_manifest", "with_verified_claims", "CapabilityRequirementsV2", "requirements_v2_for", "compatible_v2", "BackendConformanceReport", "ConformanceCheck",
|
|
25
|
+
"conform_backend_problem_classes", "conform_backends",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
from .resolve import BackendCapabilityProviderV2, resolve_backend_capabilities
|
|
29
|
+
__all__ += ["BackendCapabilityProviderV2", "resolve_backend_capabilities"]
|
|
30
|
+
|
|
31
|
+
from .ontology import ConvexityClass, IntegralityClass, ProblemClass, ProblemDescriptor, ProofRequirement, capability_requirements_for_descriptor, describe_problem
|
|
32
|
+
__all__ += ["ConvexityClass","IntegralityClass","ProblemClass","ProblemDescriptor","ProofRequirement","capability_requirements_for_descriptor","describe_problem"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "solverpilot.backend-capability-v2",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": ["schema_version", "backend", "backend_version", "binding_version", "problem_classes", "incremental", "result_artifacts"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema_version": {"const": "2.0"},
|
|
9
|
+
"backend": {"type": "string", "minLength": 1},
|
|
10
|
+
"backend_version": {"type": "string"},
|
|
11
|
+
"binding_version": {"type": "string"},
|
|
12
|
+
"problem_classes": {"type": "object", "additionalProperties": {"enum": ["unsupported", "supported", "restricted"]}},
|
|
13
|
+
"native_constraints": {"type": "object", "additionalProperties": {"enum": ["unsupported", "supported", "restricted"]}},
|
|
14
|
+
"incremental": {"type": "object", "additionalProperties": {"enum": ["unsupported", "supported", "restricted"]}},
|
|
15
|
+
"starts": {"type": "object", "additionalProperties": {"enum": ["unsupported", "supported", "restricted"]}},
|
|
16
|
+
"callbacks": {"type": "object", "additionalProperties": {"enum": ["unsupported", "supported", "restricted"]}},
|
|
17
|
+
"result_artifacts": {"type": "object", "additionalProperties": {"enum": ["unsupported", "supported", "restricted"]}},
|
|
18
|
+
"restrictions": {"type": "object"},
|
|
19
|
+
"verified_on": {"type": "object"}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import date
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from solverpilot.backends.health import BackendProbeStatus, probe_backend
|
|
9
|
+
|
|
10
|
+
from .v2 import (
|
|
11
|
+
BackendCapabilityManifestV2,
|
|
12
|
+
CapabilityEvidence,
|
|
13
|
+
CapabilityKey,
|
|
14
|
+
project_legacy_manifest,
|
|
15
|
+
with_verified_claims,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class ConformanceCheck:
|
|
21
|
+
capability: CapabilityKey
|
|
22
|
+
passed: bool
|
|
23
|
+
evidence_id: str
|
|
24
|
+
detail: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class BackendConformanceReport:
|
|
29
|
+
backend: str
|
|
30
|
+
available: bool
|
|
31
|
+
checks: tuple[ConformanceCheck, ...]
|
|
32
|
+
manifest: BackendCapabilityManifestV2
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def passed(self) -> bool:
|
|
36
|
+
return self.available and all(check.passed for check in self.checks)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _package_version(name: str) -> str | None:
|
|
40
|
+
try:
|
|
41
|
+
return importlib.metadata.version(name)
|
|
42
|
+
except importlib.metadata.PackageNotFoundError:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _binding_identity(backend) -> tuple[str, str | None]:
|
|
47
|
+
module = type(backend).__module__
|
|
48
|
+
if module.startswith("solverpilot.backends.scipy_"):
|
|
49
|
+
return "scipy", _package_version("scipy")
|
|
50
|
+
if module.endswith("nlopt_native"):
|
|
51
|
+
return "nlopt", _package_version("nlopt")
|
|
52
|
+
if module.endswith("osqp_native"):
|
|
53
|
+
return "osqp", _package_version("osqp")
|
|
54
|
+
if module.endswith("highspy_native"):
|
|
55
|
+
return "highspy", _package_version("highspy")
|
|
56
|
+
if module.endswith("pyscipopt_native"):
|
|
57
|
+
return "pyscipopt", _package_version("pyscipopt")
|
|
58
|
+
if module.endswith("casadi_conic") or module.endswith("bundled_capi"):
|
|
59
|
+
return "casadi", _package_version("casadi")
|
|
60
|
+
return module, None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _adapter_version() -> str | None:
|
|
64
|
+
# Prefer the imported source version so source-tree conformance cannot silently bind
|
|
65
|
+
# evidence to an older wheel installed in the same environment.
|
|
66
|
+
try:
|
|
67
|
+
import solverpilot
|
|
68
|
+
return solverpilot.__version__
|
|
69
|
+
except Exception:
|
|
70
|
+
return _package_version("solverpilot")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def conform_backend_problem_classes(backend) -> BackendConformanceReport:
|
|
74
|
+
"""Verify currently exposed solve/result channels against native smoke problems.
|
|
75
|
+
|
|
76
|
+
P3 deliberately verifies only what the current adapter surface can exercise without
|
|
77
|
+
inventing a new stateful/callback API. Advanced legacy declarations remain projected
|
|
78
|
+
and are therefore unusable by a verified-only P4 compiler until their own conformance
|
|
79
|
+
tests are added in the milestone that exposes those adapter methods.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
binding, binding_version = _binding_identity(backend)
|
|
83
|
+
adapter_version = _adapter_version()
|
|
84
|
+
projected = project_legacy_manifest(
|
|
85
|
+
backend.manifest,
|
|
86
|
+
binding=binding,
|
|
87
|
+
binding_version=binding_version,
|
|
88
|
+
adapter_version=adapter_version,
|
|
89
|
+
)
|
|
90
|
+
if not backend.is_available():
|
|
91
|
+
return BackendConformanceReport(
|
|
92
|
+
backend=backend.manifest.name,
|
|
93
|
+
available=False,
|
|
94
|
+
checks=(),
|
|
95
|
+
manifest=projected,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
health = probe_backend(backend)
|
|
99
|
+
verified = {}
|
|
100
|
+
checks: list[ConformanceCheck] = []
|
|
101
|
+
capability_map = {
|
|
102
|
+
"lp": CapabilityKey.PROBLEM_LP,
|
|
103
|
+
"milp": CapabilityKey.PROBLEM_MILP,
|
|
104
|
+
"convex_qp": CapabilityKey.PROBLEM_CONVEX_QP,
|
|
105
|
+
}
|
|
106
|
+
today = date.today().isoformat()
|
|
107
|
+
for check in health.checks:
|
|
108
|
+
key = capability_map.get(check.capability.value)
|
|
109
|
+
if key is None:
|
|
110
|
+
continue
|
|
111
|
+
evidence_id = f"p3.native-smoke:{backend.manifest.name}:{check.capability.value}"
|
|
112
|
+
passed = bool(check.passed and check.validation_valid)
|
|
113
|
+
checks.append(
|
|
114
|
+
ConformanceCheck(
|
|
115
|
+
capability=key,
|
|
116
|
+
passed=passed,
|
|
117
|
+
evidence_id=evidence_id,
|
|
118
|
+
detail=(
|
|
119
|
+
"native smoke solved and original-space validation passed"
|
|
120
|
+
if passed
|
|
121
|
+
else f"native smoke failed: {check.error or check.backend_status}"
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
if passed:
|
|
126
|
+
ev = CapabilityEvidence(
|
|
127
|
+
evidence_id=evidence_id,
|
|
128
|
+
kind="native_solve_validation",
|
|
129
|
+
backend_versions=(backend.manifest.version,) if backend.manifest.version else (),
|
|
130
|
+
binding_versions=(binding_version,) if binding_version else (),
|
|
131
|
+
adapter_versions=(adapter_version,) if adapter_version else (),
|
|
132
|
+
verified_on=today,
|
|
133
|
+
notes=("smoke result independently validated in canonical problem space",),
|
|
134
|
+
)
|
|
135
|
+
verified[key] = ev
|
|
136
|
+
# The current BackendSolveResult surface demonstrably returns these artifacts.
|
|
137
|
+
verified.setdefault(CapabilityKey.RESULT_PRIMAL, ev)
|
|
138
|
+
verified.setdefault(CapabilityKey.RESULT_OBJECTIVE, ev)
|
|
139
|
+
verified.setdefault(CapabilityKey.CONSTRAINT_LINEAR, ev)
|
|
140
|
+
|
|
141
|
+
manifest = with_verified_claims(projected, verified)
|
|
142
|
+
return BackendConformanceReport(
|
|
143
|
+
backend=backend.manifest.name,
|
|
144
|
+
available=True,
|
|
145
|
+
checks=tuple(checks),
|
|
146
|
+
manifest=manifest,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def conform_backends(backends: Iterable[object]) -> tuple[BackendConformanceReport, ...]:
|
|
151
|
+
return tuple(conform_backend_problem_classes(backend) for backend in backends)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Capability(str, Enum):
|
|
7
|
+
LP = "lp"
|
|
8
|
+
CONVEX_QP = "convex_qp"
|
|
9
|
+
MILP = "milp"
|
|
10
|
+
MIP_START = "mip_start"
|
|
11
|
+
PRIMAL_START = "primal_start"
|
|
12
|
+
DUAL_START = "dual_start"
|
|
13
|
+
BASIS_START = "basis_start"
|
|
14
|
+
SAME_SPARSITY_DATA_UPDATE = "same_sparsity_data_update"
|
|
15
|
+
STRUCTURAL_INCREMENTAL_UPDATE = "structural_incremental_update"
|
|
16
|
+
NATIVE_REOPTIMIZATION = "native_reoptimization"
|
|
17
|
+
CALLBACK_PROGRESS = "callback_progress"
|
|
18
|
+
IIS = "iis"
|
|
19
|
+
INFEASIBILITY_CERTIFICATE = "infeasibility_certificate"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SupportLevel(str, Enum):
|
|
23
|
+
NATIVE = "native"
|
|
24
|
+
EMULATED_SAFE = "emulated_safe"
|
|
25
|
+
EMULATED_RISKY = "emulated_risky"
|
|
26
|
+
UNSUPPORTED = "unsupported"
|
|
27
|
+
UNKNOWN = "unknown"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .enums import Capability, SupportLevel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class BackendManifest:
|
|
10
|
+
name: str
|
|
11
|
+
version: str | None = None
|
|
12
|
+
capabilities: dict[Capability, SupportLevel] = field(default_factory=dict)
|
|
13
|
+
metadata: dict[str, object] = field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
def support(self, capability: Capability) -> SupportLevel:
|
|
16
|
+
return self.capabilities.get(capability, SupportLevel.UNKNOWN)
|
|
17
|
+
|
|
18
|
+
def supports_without_risky_emulation(self, capability: Capability) -> bool:
|
|
19
|
+
return self.support(capability) in {SupportLevel.NATIVE, SupportLevel.EMULATED_SAFE}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from solverpilot.problem import ConvexityStatus, LinearProblem, QuadraticProblem
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProblemClass(str, Enum):
|
|
11
|
+
LP="LP"; MILP="MILP"; CONVEX_QP="CONVEX_QP"; QP_UNVERIFIED="QP_UNVERIFIED"; CONIC="CONIC"; NLP="NLP"; MINLP="MINLP"; CP="CP"; UNKNOWN="UNKNOWN"
|
|
12
|
+
class ConvexityClass(str, Enum):
|
|
13
|
+
CONFIRMED_CONVEX="confirmed_convex"; CERTIFIED_CONVEX="certified_convex"; UNKNOWN="unknown"; NOT_APPLICABLE="not_applicable"
|
|
14
|
+
class IntegralityClass(str, Enum):
|
|
15
|
+
CONTINUOUS="continuous"; MIXED_INTEGER="mixed_integer"; DISCRETE="discrete"; UNKNOWN="unknown"
|
|
16
|
+
class ProofRequirement(str, Enum):
|
|
17
|
+
FEASIBILITY="feasibility"; OPTIMALITY="optimality"; GLOBAL_OPTIMALITY="global_optimality"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class ProblemDescriptor:
|
|
22
|
+
problem_class: ProblemClass
|
|
23
|
+
convexity: ConvexityClass
|
|
24
|
+
integrality: IntegralityClass
|
|
25
|
+
objective_family: str
|
|
26
|
+
constraint_families: tuple[str, ...]
|
|
27
|
+
proof_requirement: ProofRequirement
|
|
28
|
+
source_type: str
|
|
29
|
+
def to_dict(self) -> dict[str,Any]:
|
|
30
|
+
p=asdict(self)
|
|
31
|
+
for k in ("problem_class","convexity","integrality","proof_requirement"): p[k]=getattr(self,k).value
|
|
32
|
+
return p
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def describe_problem(problem: object) -> ProblemDescriptor:
|
|
36
|
+
if isinstance(problem,QuadraticProblem):
|
|
37
|
+
confirmed=problem.convexity_status is ConvexityStatus.CONFIRMED
|
|
38
|
+
return ProblemDescriptor(ProblemClass.CONVEX_QP if confirmed else ProblemClass.QP_UNVERIFIED,ConvexityClass.CONFIRMED_CONVEX if confirmed else ConvexityClass.UNKNOWN,IntegralityClass.CONTINUOUS,"quadratic",("linear",),ProofRequirement.OPTIMALITY,type(problem).__qualname__)
|
|
39
|
+
if isinstance(problem,LinearProblem):
|
|
40
|
+
integer=bool(problem.has_integer_variables)
|
|
41
|
+
return ProblemDescriptor(ProblemClass.MILP if integer else ProblemClass.LP,ConvexityClass.CONFIRMED_CONVEX,IntegralityClass.MIXED_INTEGER if integer else IntegralityClass.CONTINUOUS,"linear",("linear",),ProofRequirement.GLOBAL_OPTIMALITY if integer else ProofRequirement.OPTIMALITY,type(problem).__qualname__)
|
|
42
|
+
try:
|
|
43
|
+
from solverpilot.conic.ir import ConicProblem
|
|
44
|
+
if isinstance(problem,ConicProblem): return ProblemDescriptor(ProblemClass.CONIC,ConvexityClass.CONFIRMED_CONVEX,IntegralityClass.CONTINUOUS,"linear_or_conic",("affine","cone"),ProofRequirement.OPTIMALITY,type(problem).__qualname__)
|
|
45
|
+
except ImportError: pass
|
|
46
|
+
try:
|
|
47
|
+
from solverpilot.minlp.ir import MINLPProblem
|
|
48
|
+
if isinstance(problem,MINLPProblem): return ProblemDescriptor(ProblemClass.MINLP,ConvexityClass.CERTIFIED_CONVEX,IntegralityClass.MIXED_INTEGER,"nonlinear",("nonlinear","integrality"),ProofRequirement.GLOBAL_OPTIMALITY,type(problem).__qualname__)
|
|
49
|
+
except ImportError: pass
|
|
50
|
+
try:
|
|
51
|
+
from solverpilot.nlp.ir import NLPProblem
|
|
52
|
+
if isinstance(problem,NLPProblem): return ProblemDescriptor(ProblemClass.NLP,ConvexityClass.UNKNOWN,IntegralityClass.CONTINUOUS,"nonlinear",("nonlinear",),ProofRequirement.OPTIMALITY,type(problem).__qualname__)
|
|
53
|
+
except ImportError: pass
|
|
54
|
+
try:
|
|
55
|
+
from solverpilot.cp.ir import CPProblem
|
|
56
|
+
if isinstance(problem,CPProblem):
|
|
57
|
+
fam=tuple(sorted({getattr(c,"kind",type(c).__name__) for c in problem.constraints}))
|
|
58
|
+
return ProblemDescriptor(ProblemClass.CP,ConvexityClass.NOT_APPLICABLE,IntegralityClass.DISCRETE,"linear_integer_or_none",fam,ProofRequirement.GLOBAL_OPTIMALITY if problem.objective is not None else ProofRequirement.FEASIBILITY,type(problem).__qualname__)
|
|
59
|
+
except ImportError: pass
|
|
60
|
+
return ProblemDescriptor(ProblemClass.UNKNOWN,ConvexityClass.UNKNOWN,IntegralityClass.UNKNOWN,"unknown",(),ProofRequirement.FEASIBILITY,type(problem).__qualname__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def capability_requirements_for_descriptor(descriptor: ProblemDescriptor):
|
|
64
|
+
from .v2 import CapabilityKey, CapabilityRequirementsV2
|
|
65
|
+
mapping={ProblemClass.LP:CapabilityKey.PROBLEM_LP,ProblemClass.MILP:CapabilityKey.PROBLEM_MILP,ProblemClass.CONVEX_QP:CapabilityKey.PROBLEM_CONVEX_QP,ProblemClass.CONIC:CapabilityKey.PROBLEM_CONIC,ProblemClass.NLP:CapabilityKey.PROBLEM_NLP,ProblemClass.MINLP:CapabilityKey.PROBLEM_MINLP,ProblemClass.CP:CapabilityKey.PROBLEM_CP}
|
|
66
|
+
if descriptor.problem_class is ProblemClass.QP_UNVERIFIED: raise ValueError("unverified QP cannot be mapped to a verified convex-QP capability")
|
|
67
|
+
key=mapping.get(descriptor.problem_class)
|
|
68
|
+
if key is None: raise ValueError(f"no capability mapping for problem class {descriptor.problem_class.value}")
|
|
69
|
+
keys=[key]
|
|
70
|
+
family_map={"linear":CapabilityKey.CONSTRAINT_LINEAR,"nonlinear":CapabilityKey.CONSTRAINT_NONLINEAR,"all_different":CapabilityKey.CONSTRAINT_ALL_DIFFERENT,"exactly_one":CapabilityKey.CONSTRAINT_EXACTLY_ONE,"table":CapabilityKey.CONSTRAINT_TABLE,"element":CapabilityKey.CONSTRAINT_ELEMENT,"circuit":CapabilityKey.CONSTRAINT_CIRCUIT,"no_overlap":CapabilityKey.CONSTRAINT_NO_OVERLAP,"cumulative":CapabilityKey.CONSTRAINT_CUMULATIVE}
|
|
71
|
+
for family in descriptor.constraint_families:
|
|
72
|
+
if family in family_map: keys.append(family_map[family])
|
|
73
|
+
return CapabilityRequirementsV2(tuple(dict.fromkeys(keys)))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from solverpilot.problem import ConvexityStatus, LinearProblem, QuadraticProblem
|
|
6
|
+
|
|
7
|
+
from .enums import Capability, SupportLevel
|
|
8
|
+
from .manifest import BackendManifest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class CapabilityRequirements:
|
|
13
|
+
required: frozenset[Capability]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def requirements_for(problem: LinearProblem | QuadraticProblem) -> CapabilityRequirements:
|
|
17
|
+
if isinstance(problem, QuadraticProblem):
|
|
18
|
+
if problem.convexity_status is not ConvexityStatus.CONFIRMED:
|
|
19
|
+
raise ValueError("unverified quadratic problem cannot be routed as convex_qp")
|
|
20
|
+
return CapabilityRequirements(frozenset({Capability.CONVEX_QP}))
|
|
21
|
+
if isinstance(problem, LinearProblem):
|
|
22
|
+
return CapabilityRequirements(
|
|
23
|
+
frozenset({Capability.MILP if problem.has_integer_variables else Capability.LP})
|
|
24
|
+
)
|
|
25
|
+
raise TypeError(f"unsupported problem type: {type(problem)!r}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compatible(
|
|
29
|
+
manifest: BackendManifest,
|
|
30
|
+
requirements: CapabilityRequirements,
|
|
31
|
+
*,
|
|
32
|
+
allow_risky_emulation: bool = False,
|
|
33
|
+
) -> bool:
|
|
34
|
+
allowed = {SupportLevel.NATIVE, SupportLevel.EMULATED_SAFE}
|
|
35
|
+
if allow_risky_emulation:
|
|
36
|
+
allowed.add(SupportLevel.EMULATED_RISKY)
|
|
37
|
+
return all(manifest.support(cap) in allowed for cap in requirements.required)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from .conformance import conform_backend_problem_classes
|
|
6
|
+
from .v2 import BackendCapabilityManifestV2, project_legacy_manifest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@runtime_checkable
|
|
10
|
+
class BackendCapabilityProviderV2(Protocol):
|
|
11
|
+
@property
|
|
12
|
+
def capability_manifest_v2(self) -> BackendCapabilityManifestV2: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_backend_capabilities(backend, *, verify: bool = False) -> BackendCapabilityManifestV2:
|
|
16
|
+
"""Resolve a backend's v2 manifest without upgrading unverified declarations.
|
|
17
|
+
|
|
18
|
+
Future adapters may expose an explicit ``capability_manifest_v2``. Current Track-M
|
|
19
|
+
adapters are projected conservatively from v1. ``verify=True`` runs the P3 native
|
|
20
|
+
problem-class conformance kit and upgrades only claims directly exercised by it.
|
|
21
|
+
"""
|
|
22
|
+
explicit = getattr(backend, "capability_manifest_v2", None)
|
|
23
|
+
if explicit is not None:
|
|
24
|
+
manifest = explicit() if callable(explicit) else explicit
|
|
25
|
+
if not isinstance(manifest, BackendCapabilityManifestV2):
|
|
26
|
+
raise TypeError("capability_manifest_v2 must be BackendCapabilityManifestV2")
|
|
27
|
+
return manifest
|
|
28
|
+
if verify:
|
|
29
|
+
return conform_backend_problem_classes(backend).manifest
|
|
30
|
+
return project_legacy_manifest(backend.manifest)
|