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,711 @@
|
|
|
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 typing import Any, Iterable, Mapping
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
|
|
10
|
+
from .enums import Capability, SupportLevel
|
|
11
|
+
from .manifest import BackendManifest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CAPABILITY_SCHEMA_VERSION = "2.0"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CapabilityStatus(str, Enum):
|
|
18
|
+
UNSUPPORTED = "unsupported"
|
|
19
|
+
SUPPORTED = "supported"
|
|
20
|
+
RESTRICTED = "restricted"
|
|
21
|
+
UNVERIFIED = "unverified"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CapabilityMode(str, Enum):
|
|
25
|
+
NATIVE = "native"
|
|
26
|
+
EMULATED_SAFE = "emulated_safe"
|
|
27
|
+
EMULATED_RISKY = "emulated_risky"
|
|
28
|
+
NONE = "none"
|
|
29
|
+
UNKNOWN = "unknown"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class VerificationLevel(str, Enum):
|
|
33
|
+
VERIFIED = "verified"
|
|
34
|
+
PROJECTED_LEGACY = "projected_legacy"
|
|
35
|
+
UNVERIFIED = "unverified"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CapabilityKey(str, Enum):
|
|
39
|
+
# Problem classes
|
|
40
|
+
PROBLEM_LP = "problem.lp"
|
|
41
|
+
PROBLEM_MILP = "problem.milp"
|
|
42
|
+
PROBLEM_CONVEX_QP = "problem.convex_qp"
|
|
43
|
+
PROBLEM_CONIC = "problem.conic"
|
|
44
|
+
PROBLEM_CONIC_QUADRATIC = "problem.conic_quadratic"
|
|
45
|
+
PROBLEM_NLP = "problem.nlp"
|
|
46
|
+
PROBLEM_MINLP = "problem.minlp"
|
|
47
|
+
PROBLEM_CP = "problem.cp"
|
|
48
|
+
|
|
49
|
+
# Native semantic/constraint families that P4 may query.
|
|
50
|
+
CONSTRAINT_LINEAR = "constraint.linear"
|
|
51
|
+
CONSTRAINT_INDICATOR = "constraint.indicator"
|
|
52
|
+
CONSTRAINT_SOS1 = "constraint.sos1"
|
|
53
|
+
CONSTRAINT_SOS2 = "constraint.sos2"
|
|
54
|
+
CONSTRAINT_SOC = "constraint.soc"
|
|
55
|
+
CONSTRAINT_ROTATED_SOC = "constraint.rotated_soc"
|
|
56
|
+
CONSTRAINT_PSD = "constraint.psd"
|
|
57
|
+
CONSTRAINT_NONLINEAR = "constraint.nonlinear"
|
|
58
|
+
CONSTRAINT_ALL_DIFFERENT = "constraint.all_different"
|
|
59
|
+
CONSTRAINT_EXACTLY_ONE = "constraint.exactly_one"
|
|
60
|
+
CONSTRAINT_TABLE = "constraint.table"
|
|
61
|
+
CONSTRAINT_ELEMENT = "constraint.element"
|
|
62
|
+
CONSTRAINT_CIRCUIT = "constraint.circuit"
|
|
63
|
+
CONSTRAINT_NO_OVERLAP = "constraint.no_overlap"
|
|
64
|
+
CONSTRAINT_CUMULATIVE = "constraint.cumulative"
|
|
65
|
+
|
|
66
|
+
# Derivative surface.
|
|
67
|
+
DERIVATIVE_GRADIENT = "derivative.gradient"
|
|
68
|
+
DERIVATIVE_JACOBIAN = "derivative.jacobian"
|
|
69
|
+
DERIVATIVE_HESSIAN = "derivative.hessian"
|
|
70
|
+
|
|
71
|
+
# Incremental modification surface.
|
|
72
|
+
INCREMENTAL_VARIABLE_BOUNDS = "incremental.variable_bounds"
|
|
73
|
+
INCREMENTAL_RHS = "incremental.rhs"
|
|
74
|
+
INCREMENTAL_OBJECTIVE = "incremental.objective"
|
|
75
|
+
INCREMENTAL_MATRIX_VALUES = "incremental.matrix_values"
|
|
76
|
+
INCREMENTAL_MATRIX_STRUCTURE = "incremental.matrix_structure"
|
|
77
|
+
INCREMENTAL_ADD_VARIABLE = "incremental.add_variable"
|
|
78
|
+
INCREMENTAL_DELETE_VARIABLE = "incremental.delete_variable"
|
|
79
|
+
INCREMENTAL_ADD_CONSTRAINT = "incremental.add_constraint"
|
|
80
|
+
INCREMENTAL_DELETE_CONSTRAINT = "incremental.delete_constraint"
|
|
81
|
+
|
|
82
|
+
# Starts / reuse artifacts.
|
|
83
|
+
START_PRIMAL = "start.primal"
|
|
84
|
+
START_DUAL = "start.dual"
|
|
85
|
+
START_BASIS = "start.basis"
|
|
86
|
+
START_MIP = "start.mip"
|
|
87
|
+
|
|
88
|
+
# Callback surface.
|
|
89
|
+
CALLBACK_PROGRESS = "callback.progress"
|
|
90
|
+
CALLBACK_INCUMBENT = "callback.incumbent"
|
|
91
|
+
CALLBACK_NODE = "callback.node"
|
|
92
|
+
CALLBACK_LAZY_CONSTRAINT = "callback.lazy_constraint"
|
|
93
|
+
CALLBACK_USER_CUT = "callback.user_cut"
|
|
94
|
+
CALLBACK_HEURISTIC = "callback.heuristic"
|
|
95
|
+
|
|
96
|
+
# Result artifacts.
|
|
97
|
+
RESULT_PRIMAL = "result.primal"
|
|
98
|
+
RESULT_OBJECTIVE = "result.objective"
|
|
99
|
+
RESULT_DUAL = "result.dual"
|
|
100
|
+
RESULT_SLACK = "result.slack"
|
|
101
|
+
RESULT_REDUCED_COST = "result.reduced_cost"
|
|
102
|
+
RESULT_PRIMAL_RAY = "result.primal_ray"
|
|
103
|
+
RESULT_DUAL_RAY = "result.dual_ray"
|
|
104
|
+
RESULT_BASIS = "result.basis"
|
|
105
|
+
RESULT_IIS = "result.iis"
|
|
106
|
+
RESULT_SOLUTION_POOL = "result.solution_pool"
|
|
107
|
+
RESULT_INFEASIBILITY_CERTIFICATE = "result.infeasibility_certificate"
|
|
108
|
+
|
|
109
|
+
# Lifecycle / execution surface.
|
|
110
|
+
LIFECYCLE_PERSISTENT = "lifecycle.persistent"
|
|
111
|
+
LIFECYCLE_INTERRUPT = "lifecycle.interrupt"
|
|
112
|
+
LIFECYCLE_CLONE = "lifecycle.clone"
|
|
113
|
+
LIFECYCLE_THREAD_SAFE = "lifecycle.thread_safe"
|
|
114
|
+
LIFECYCLE_THREAD_CONTROL = "lifecycle.thread_control"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _freeze_value(value: Any) -> Any:
|
|
118
|
+
if isinstance(value, Mapping):
|
|
119
|
+
return MappingProxyType({k: _freeze_value(v) for k, v in value.items()})
|
|
120
|
+
if isinstance(value, list):
|
|
121
|
+
return tuple(_freeze_value(v) for v in value)
|
|
122
|
+
if isinstance(value, tuple):
|
|
123
|
+
return tuple(_freeze_value(v) for v in value)
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True, slots=True)
|
|
128
|
+
class CapabilityEvidence:
|
|
129
|
+
evidence_id: str
|
|
130
|
+
kind: str
|
|
131
|
+
backend_versions: tuple[str, ...] = ()
|
|
132
|
+
binding_versions: tuple[str, ...] = ()
|
|
133
|
+
adapter_versions: tuple[str, ...] = ()
|
|
134
|
+
verified_on: str | None = None
|
|
135
|
+
notes: tuple[str, ...] = ()
|
|
136
|
+
|
|
137
|
+
def matches_runtime(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
backend_version: str | None,
|
|
141
|
+
binding_version: str | None,
|
|
142
|
+
adapter_version: str | None,
|
|
143
|
+
) -> bool:
|
|
144
|
+
def exact_or_unconstrained(allowed: tuple[str, ...], actual: str | None) -> bool:
|
|
145
|
+
if not allowed:
|
|
146
|
+
return True
|
|
147
|
+
return actual is not None and actual in allowed
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
exact_or_unconstrained(self.backend_versions, backend_version)
|
|
151
|
+
and exact_or_unconstrained(self.binding_versions, binding_version)
|
|
152
|
+
and exact_or_unconstrained(self.adapter_versions, adapter_version)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True, slots=True)
|
|
157
|
+
class CapabilityClaim:
|
|
158
|
+
status: CapabilityStatus
|
|
159
|
+
mode: CapabilityMode = CapabilityMode.UNKNOWN
|
|
160
|
+
verification: VerificationLevel = VerificationLevel.UNVERIFIED
|
|
161
|
+
restrictions: Mapping[str, Any] = field(default_factory=dict)
|
|
162
|
+
evidence: tuple[CapabilityEvidence, ...] = ()
|
|
163
|
+
notes: tuple[str, ...] = ()
|
|
164
|
+
|
|
165
|
+
def __post_init__(self) -> None:
|
|
166
|
+
object.__setattr__(self, "restrictions", _freeze_value(self.restrictions))
|
|
167
|
+
if self.status is CapabilityStatus.UNSUPPORTED and self.mode not in {
|
|
168
|
+
CapabilityMode.NONE,
|
|
169
|
+
CapabilityMode.UNKNOWN,
|
|
170
|
+
}:
|
|
171
|
+
raise ValueError("unsupported capability cannot declare an active implementation mode")
|
|
172
|
+
if self.status is CapabilityStatus.UNVERIFIED and self.verification is VerificationLevel.VERIFIED:
|
|
173
|
+
raise ValueError("unverified capability cannot have verified evidence state")
|
|
174
|
+
if self.status is CapabilityStatus.RESTRICTED and not self.restrictions:
|
|
175
|
+
raise ValueError("restricted capability requires machine-readable restrictions")
|
|
176
|
+
|
|
177
|
+
def runtime_verified(
|
|
178
|
+
self,
|
|
179
|
+
*,
|
|
180
|
+
backend_version: str | None,
|
|
181
|
+
binding_version: str | None,
|
|
182
|
+
adapter_version: str | None,
|
|
183
|
+
) -> bool:
|
|
184
|
+
if self.verification is not VerificationLevel.VERIFIED:
|
|
185
|
+
return False
|
|
186
|
+
if not self.evidence:
|
|
187
|
+
return False
|
|
188
|
+
return any(
|
|
189
|
+
item.matches_runtime(
|
|
190
|
+
backend_version=backend_version,
|
|
191
|
+
binding_version=binding_version,
|
|
192
|
+
adapter_version=adapter_version,
|
|
193
|
+
)
|
|
194
|
+
for item in self.evidence
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def usable(
|
|
198
|
+
self,
|
|
199
|
+
*,
|
|
200
|
+
backend_version: str | None,
|
|
201
|
+
binding_version: str | None,
|
|
202
|
+
adapter_version: str | None,
|
|
203
|
+
allow_restricted: bool = True,
|
|
204
|
+
allow_safe_emulation: bool = False,
|
|
205
|
+
allow_risky_emulation: bool = False,
|
|
206
|
+
require_verified: bool = True,
|
|
207
|
+
) -> bool:
|
|
208
|
+
if self.status is CapabilityStatus.UNSUPPORTED or self.status is CapabilityStatus.UNVERIFIED:
|
|
209
|
+
return False
|
|
210
|
+
if self.status is CapabilityStatus.RESTRICTED and not allow_restricted:
|
|
211
|
+
return False
|
|
212
|
+
if self.mode is CapabilityMode.EMULATED_SAFE and not allow_safe_emulation:
|
|
213
|
+
return False
|
|
214
|
+
if self.mode is CapabilityMode.EMULATED_RISKY and not allow_risky_emulation:
|
|
215
|
+
return False
|
|
216
|
+
if self.mode in {CapabilityMode.NONE, CapabilityMode.UNKNOWN}:
|
|
217
|
+
return False
|
|
218
|
+
if require_verified and not self.runtime_verified(
|
|
219
|
+
backend_version=backend_version,
|
|
220
|
+
binding_version=binding_version,
|
|
221
|
+
adapter_version=adapter_version,
|
|
222
|
+
):
|
|
223
|
+
return False
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True, slots=True)
|
|
228
|
+
class CapabilityCheck:
|
|
229
|
+
key: CapabilityKey
|
|
230
|
+
usable: bool
|
|
231
|
+
status: CapabilityStatus
|
|
232
|
+
mode: CapabilityMode
|
|
233
|
+
verification: VerificationLevel
|
|
234
|
+
reason: str
|
|
235
|
+
restrictions: Mapping[str, Any] = field(default_factory=dict)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@dataclass(frozen=True, slots=True)
|
|
239
|
+
class BackendCapabilityManifestV2:
|
|
240
|
+
backend: str
|
|
241
|
+
backend_version: str | None
|
|
242
|
+
binding: str
|
|
243
|
+
binding_version: str | None
|
|
244
|
+
adapter_version: str | None
|
|
245
|
+
claims: Mapping[CapabilityKey, CapabilityClaim] = field(default_factory=dict)
|
|
246
|
+
numeric_limits: Mapping[str, Any] = field(default_factory=dict)
|
|
247
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
248
|
+
schema_version: str = CAPABILITY_SCHEMA_VERSION
|
|
249
|
+
|
|
250
|
+
def __post_init__(self) -> None:
|
|
251
|
+
object.__setattr__(self, "claims", MappingProxyType(dict(self.claims)))
|
|
252
|
+
object.__setattr__(self, "numeric_limits", _freeze_value(self.numeric_limits))
|
|
253
|
+
object.__setattr__(self, "metadata", _freeze_value(self.metadata))
|
|
254
|
+
if not self.backend:
|
|
255
|
+
raise ValueError("backend must be non-empty")
|
|
256
|
+
if not self.binding:
|
|
257
|
+
raise ValueError("binding must be non-empty")
|
|
258
|
+
if self.schema_version != CAPABILITY_SCHEMA_VERSION:
|
|
259
|
+
raise ValueError(f"unsupported capability schema version {self.schema_version!r}")
|
|
260
|
+
|
|
261
|
+
def claim(self, key: CapabilityKey) -> CapabilityClaim:
|
|
262
|
+
return self.claims.get(
|
|
263
|
+
key,
|
|
264
|
+
CapabilityClaim(
|
|
265
|
+
status=CapabilityStatus.UNVERIFIED,
|
|
266
|
+
mode=CapabilityMode.UNKNOWN,
|
|
267
|
+
verification=VerificationLevel.UNVERIFIED,
|
|
268
|
+
notes=("capability absent from manifest",),
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def check(
|
|
273
|
+
self,
|
|
274
|
+
key: CapabilityKey,
|
|
275
|
+
*,
|
|
276
|
+
allow_restricted: bool = True,
|
|
277
|
+
allow_safe_emulation: bool = False,
|
|
278
|
+
allow_risky_emulation: bool = False,
|
|
279
|
+
require_verified: bool = True,
|
|
280
|
+
) -> CapabilityCheck:
|
|
281
|
+
claim = self.claim(key)
|
|
282
|
+
usable = claim.usable(
|
|
283
|
+
backend_version=self.backend_version,
|
|
284
|
+
binding_version=self.binding_version,
|
|
285
|
+
adapter_version=self.adapter_version,
|
|
286
|
+
allow_restricted=allow_restricted,
|
|
287
|
+
allow_safe_emulation=allow_safe_emulation,
|
|
288
|
+
allow_risky_emulation=allow_risky_emulation,
|
|
289
|
+
require_verified=require_verified,
|
|
290
|
+
)
|
|
291
|
+
if usable:
|
|
292
|
+
reason = "verified capability is usable under the requested policy"
|
|
293
|
+
elif claim.status is CapabilityStatus.UNSUPPORTED:
|
|
294
|
+
reason = "capability is explicitly unsupported"
|
|
295
|
+
elif claim.status is CapabilityStatus.UNVERIFIED:
|
|
296
|
+
reason = "capability is unverified"
|
|
297
|
+
elif require_verified and not claim.runtime_verified(
|
|
298
|
+
backend_version=self.backend_version,
|
|
299
|
+
binding_version=self.binding_version,
|
|
300
|
+
adapter_version=self.adapter_version,
|
|
301
|
+
):
|
|
302
|
+
reason = "capability evidence does not verify this runtime version"
|
|
303
|
+
elif claim.mode is CapabilityMode.EMULATED_SAFE and not allow_safe_emulation:
|
|
304
|
+
reason = "safe emulation is disabled by policy"
|
|
305
|
+
elif claim.mode is CapabilityMode.EMULATED_RISKY and not allow_risky_emulation:
|
|
306
|
+
reason = "risky emulation is disabled by policy"
|
|
307
|
+
elif claim.status is CapabilityStatus.RESTRICTED and not allow_restricted:
|
|
308
|
+
reason = "restricted support is disabled by policy"
|
|
309
|
+
else:
|
|
310
|
+
reason = "capability is not usable under the requested policy"
|
|
311
|
+
return CapabilityCheck(
|
|
312
|
+
key=key,
|
|
313
|
+
usable=usable,
|
|
314
|
+
status=claim.status,
|
|
315
|
+
mode=claim.mode,
|
|
316
|
+
verification=claim.verification,
|
|
317
|
+
reason=reason,
|
|
318
|
+
restrictions=claim.restrictions,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def supports_verified(self, key: CapabilityKey, **policy: Any) -> bool:
|
|
322
|
+
return self.check(key, require_verified=True, **policy).usable
|
|
323
|
+
|
|
324
|
+
def require_verified(self, keys: Iterable[CapabilityKey], **policy: Any) -> tuple[CapabilityCheck, ...]:
|
|
325
|
+
checks = tuple(self.check(key, require_verified=True, **policy) for key in keys)
|
|
326
|
+
return checks
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def signature(self) -> str:
|
|
330
|
+
raw = json.dumps(self.to_canonical_dict(), sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
|
|
331
|
+
return hashlib.sha256(raw).hexdigest()
|
|
332
|
+
|
|
333
|
+
def to_canonical_dict(self) -> dict[str, Any]:
|
|
334
|
+
claims: dict[str, Any] = {}
|
|
335
|
+
for key in sorted(self.claims, key=lambda item: item.value):
|
|
336
|
+
claim = self.claims[key]
|
|
337
|
+
claims[key.value] = {
|
|
338
|
+
"status": claim.status.value,
|
|
339
|
+
"mode": claim.mode.value,
|
|
340
|
+
"verification": claim.verification.value,
|
|
341
|
+
"restrictions": _jsonable(claim.restrictions),
|
|
342
|
+
"evidence": [
|
|
343
|
+
{
|
|
344
|
+
"evidence_id": item.evidence_id,
|
|
345
|
+
"kind": item.kind,
|
|
346
|
+
"backend_versions": list(item.backend_versions),
|
|
347
|
+
"binding_versions": list(item.binding_versions),
|
|
348
|
+
"adapter_versions": list(item.adapter_versions),
|
|
349
|
+
"verified_on": item.verified_on,
|
|
350
|
+
"notes": list(item.notes),
|
|
351
|
+
}
|
|
352
|
+
for item in claim.evidence
|
|
353
|
+
],
|
|
354
|
+
"notes": list(claim.notes),
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
"schema_version": self.schema_version,
|
|
358
|
+
"backend": self.backend,
|
|
359
|
+
"backend_version": self.backend_version,
|
|
360
|
+
"binding": self.binding,
|
|
361
|
+
"binding_version": self.binding_version,
|
|
362
|
+
"adapter_version": self.adapter_version,
|
|
363
|
+
"claims": claims,
|
|
364
|
+
"numeric_limits": _jsonable(self.numeric_limits),
|
|
365
|
+
"metadata": _jsonable(self.metadata),
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
def to_p0_schema_dict(self) -> dict[str, Any]:
|
|
369
|
+
groups: dict[str, dict[str, str]] = {
|
|
370
|
+
"problem_classes": {},
|
|
371
|
+
"native_constraints": {},
|
|
372
|
+
"incremental": {},
|
|
373
|
+
"starts": {},
|
|
374
|
+
"callbacks": {},
|
|
375
|
+
"result_artifacts": {},
|
|
376
|
+
}
|
|
377
|
+
restrictions: dict[str, Any] = {}
|
|
378
|
+
for key, claim in self.claims.items():
|
|
379
|
+
group, leaf = _p0_group_leaf(key)
|
|
380
|
+
if group is None:
|
|
381
|
+
continue
|
|
382
|
+
p0_status = (
|
|
383
|
+
"unsupported"
|
|
384
|
+
if claim.status is CapabilityStatus.UNSUPPORTED
|
|
385
|
+
else "restricted"
|
|
386
|
+
if claim.status in {CapabilityStatus.RESTRICTED, CapabilityStatus.UNVERIFIED}
|
|
387
|
+
or claim.mode is not CapabilityMode.NATIVE
|
|
388
|
+
or claim.verification is not VerificationLevel.VERIFIED
|
|
389
|
+
else "supported"
|
|
390
|
+
)
|
|
391
|
+
groups[group][leaf] = p0_status
|
|
392
|
+
if p0_status == "restricted":
|
|
393
|
+
restrictions[key.value] = {
|
|
394
|
+
**_jsonable(claim.restrictions),
|
|
395
|
+
"mode": claim.mode.value,
|
|
396
|
+
"verification": claim.verification.value,
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
"schema_version": self.schema_version,
|
|
400
|
+
"backend": self.backend,
|
|
401
|
+
"backend_version": self.backend_version or "unknown",
|
|
402
|
+
"binding_version": self.binding_version or "unknown",
|
|
403
|
+
**groups,
|
|
404
|
+
"restrictions": restrictions,
|
|
405
|
+
"verified_on": {
|
|
406
|
+
"adapter_version": self.adapter_version,
|
|
407
|
+
"manifest_signature": self.signature,
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _jsonable(value: Any) -> Any:
|
|
413
|
+
if isinstance(value, Enum):
|
|
414
|
+
return value.value
|
|
415
|
+
if isinstance(value, Mapping):
|
|
416
|
+
return {str(k): _jsonable(v) for k, v in sorted(value.items(), key=lambda item: str(item[0]))}
|
|
417
|
+
if isinstance(value, (tuple, list)):
|
|
418
|
+
return [_jsonable(v) for v in value]
|
|
419
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
420
|
+
return value
|
|
421
|
+
return repr(value)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _p0_group_leaf(key: CapabilityKey) -> tuple[str | None, str]:
|
|
425
|
+
prefix, leaf = key.value.split(".", 1)
|
|
426
|
+
group = {
|
|
427
|
+
"problem": "problem_classes",
|
|
428
|
+
"constraint": "native_constraints",
|
|
429
|
+
"incremental": "incremental",
|
|
430
|
+
"start": "starts",
|
|
431
|
+
"callback": "callbacks",
|
|
432
|
+
"result": "result_artifacts",
|
|
433
|
+
}.get(prefix)
|
|
434
|
+
return group, leaf
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _mode_from_legacy(level: SupportLevel) -> CapabilityMode:
|
|
438
|
+
return {
|
|
439
|
+
SupportLevel.NATIVE: CapabilityMode.NATIVE,
|
|
440
|
+
SupportLevel.EMULATED_SAFE: CapabilityMode.EMULATED_SAFE,
|
|
441
|
+
SupportLevel.EMULATED_RISKY: CapabilityMode.EMULATED_RISKY,
|
|
442
|
+
SupportLevel.UNSUPPORTED: CapabilityMode.NONE,
|
|
443
|
+
SupportLevel.UNKNOWN: CapabilityMode.UNKNOWN,
|
|
444
|
+
}[level]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _status_from_legacy(level: SupportLevel, *, coarse: bool = False) -> CapabilityStatus:
|
|
448
|
+
if level is SupportLevel.UNSUPPORTED:
|
|
449
|
+
return CapabilityStatus.UNSUPPORTED
|
|
450
|
+
if level is SupportLevel.UNKNOWN:
|
|
451
|
+
return CapabilityStatus.UNVERIFIED
|
|
452
|
+
if coarse or level is not SupportLevel.NATIVE:
|
|
453
|
+
return CapabilityStatus.RESTRICTED
|
|
454
|
+
return CapabilityStatus.SUPPORTED
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
_LEGACY_DIRECT: dict[Capability, CapabilityKey] = {
|
|
458
|
+
Capability.LP: CapabilityKey.PROBLEM_LP,
|
|
459
|
+
Capability.MILP: CapabilityKey.PROBLEM_MILP,
|
|
460
|
+
Capability.CONVEX_QP: CapabilityKey.PROBLEM_CONVEX_QP,
|
|
461
|
+
Capability.MIP_START: CapabilityKey.START_MIP,
|
|
462
|
+
Capability.PRIMAL_START: CapabilityKey.START_PRIMAL,
|
|
463
|
+
Capability.DUAL_START: CapabilityKey.START_DUAL,
|
|
464
|
+
Capability.BASIS_START: CapabilityKey.START_BASIS,
|
|
465
|
+
Capability.CALLBACK_PROGRESS: CapabilityKey.CALLBACK_PROGRESS,
|
|
466
|
+
Capability.IIS: CapabilityKey.RESULT_IIS,
|
|
467
|
+
Capability.INFEASIBILITY_CERTIFICATE: CapabilityKey.RESULT_INFEASIBILITY_CERTIFICATE,
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def project_legacy_manifest(
|
|
472
|
+
manifest: BackendManifest,
|
|
473
|
+
*,
|
|
474
|
+
binding: str = "legacy-adapter",
|
|
475
|
+
binding_version: str | None = None,
|
|
476
|
+
adapter_version: str | None = None,
|
|
477
|
+
) -> BackendCapabilityManifestV2:
|
|
478
|
+
"""Project the coarse M-track manifest into v2 without upgrading evidence.
|
|
479
|
+
|
|
480
|
+
The projection is intentionally conservative. It preserves legacy declarations for
|
|
481
|
+
observability, but projected claims are *not* considered verified v2 capabilities.
|
|
482
|
+
Coarse same-sparsity/structural-update flags become restricted granular claims.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
claims: dict[CapabilityKey, CapabilityClaim] = {}
|
|
486
|
+
for legacy_key, v2_key in _LEGACY_DIRECT.items():
|
|
487
|
+
level = manifest.support(legacy_key)
|
|
488
|
+
claims[v2_key] = CapabilityClaim(
|
|
489
|
+
status=_status_from_legacy(level),
|
|
490
|
+
mode=_mode_from_legacy(level),
|
|
491
|
+
verification=(
|
|
492
|
+
VerificationLevel.UNVERIFIED
|
|
493
|
+
if level is SupportLevel.UNKNOWN
|
|
494
|
+
else VerificationLevel.PROJECTED_LEGACY
|
|
495
|
+
),
|
|
496
|
+
restrictions=(
|
|
497
|
+
{"legacy_emulation": level.value}
|
|
498
|
+
if level in {SupportLevel.EMULATED_SAFE, SupportLevel.EMULATED_RISKY}
|
|
499
|
+
else {}
|
|
500
|
+
),
|
|
501
|
+
notes=("projected from BackendManifest v1; native v2 conformance not implied",),
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
same = manifest.support(Capability.SAME_SPARSITY_DATA_UPDATE)
|
|
505
|
+
for key in (
|
|
506
|
+
CapabilityKey.INCREMENTAL_VARIABLE_BOUNDS,
|
|
507
|
+
CapabilityKey.INCREMENTAL_RHS,
|
|
508
|
+
CapabilityKey.INCREMENTAL_OBJECTIVE,
|
|
509
|
+
CapabilityKey.INCREMENTAL_MATRIX_VALUES,
|
|
510
|
+
):
|
|
511
|
+
claims[key] = CapabilityClaim(
|
|
512
|
+
status=_status_from_legacy(same, coarse=same not in {SupportLevel.UNSUPPORTED, SupportLevel.UNKNOWN}),
|
|
513
|
+
mode=_mode_from_legacy(same),
|
|
514
|
+
verification=(
|
|
515
|
+
VerificationLevel.UNVERIFIED
|
|
516
|
+
if same is SupportLevel.UNKNOWN
|
|
517
|
+
else VerificationLevel.PROJECTED_LEGACY
|
|
518
|
+
),
|
|
519
|
+
restrictions=(
|
|
520
|
+
{"legacy_scope": "same_sparsity_data_update", "granular_operation_not_individually_verified": True}
|
|
521
|
+
if same not in {SupportLevel.UNSUPPORTED, SupportLevel.UNKNOWN}
|
|
522
|
+
else {}
|
|
523
|
+
),
|
|
524
|
+
notes=("coarse M-track capability projected conservatively",),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
structural = manifest.support(Capability.STRUCTURAL_INCREMENTAL_UPDATE)
|
|
528
|
+
for key in (
|
|
529
|
+
CapabilityKey.INCREMENTAL_MATRIX_STRUCTURE,
|
|
530
|
+
CapabilityKey.INCREMENTAL_ADD_VARIABLE,
|
|
531
|
+
CapabilityKey.INCREMENTAL_DELETE_VARIABLE,
|
|
532
|
+
CapabilityKey.INCREMENTAL_ADD_CONSTRAINT,
|
|
533
|
+
CapabilityKey.INCREMENTAL_DELETE_CONSTRAINT,
|
|
534
|
+
):
|
|
535
|
+
claims[key] = CapabilityClaim(
|
|
536
|
+
status=_status_from_legacy(structural, coarse=structural not in {SupportLevel.UNSUPPORTED, SupportLevel.UNKNOWN}),
|
|
537
|
+
mode=_mode_from_legacy(structural),
|
|
538
|
+
verification=(
|
|
539
|
+
VerificationLevel.UNVERIFIED
|
|
540
|
+
if structural is SupportLevel.UNKNOWN
|
|
541
|
+
else VerificationLevel.PROJECTED_LEGACY
|
|
542
|
+
),
|
|
543
|
+
restrictions=(
|
|
544
|
+
{"legacy_scope": "structural_incremental_update", "granular_operation_not_individually_verified": True}
|
|
545
|
+
if structural not in {SupportLevel.UNSUPPORTED, SupportLevel.UNKNOWN}
|
|
546
|
+
else {}
|
|
547
|
+
),
|
|
548
|
+
notes=("coarse M-track structural capability projected conservatively",),
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
reopt = manifest.support(Capability.NATIVE_REOPTIMIZATION)
|
|
552
|
+
claims[CapabilityKey.LIFECYCLE_PERSISTENT] = CapabilityClaim(
|
|
553
|
+
status=_status_from_legacy(reopt, coarse=reopt not in {SupportLevel.UNSUPPORTED, SupportLevel.UNKNOWN}),
|
|
554
|
+
mode=_mode_from_legacy(reopt),
|
|
555
|
+
verification=(VerificationLevel.UNVERIFIED if reopt is SupportLevel.UNKNOWN else VerificationLevel.PROJECTED_LEGACY),
|
|
556
|
+
restrictions=(
|
|
557
|
+
{"legacy_scope": "native_reoptimization", "persistent_lifecycle_not_individually_verified": True}
|
|
558
|
+
if reopt not in {SupportLevel.UNSUPPORTED, SupportLevel.UNKNOWN}
|
|
559
|
+
else {}
|
|
560
|
+
),
|
|
561
|
+
notes=("native_reoptimization does not automatically prove the full v2 persistent lifecycle",),
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Current BackendSolveResult always has a primal/objective channel, but whether a
|
|
565
|
+
# backend produces a valid candidate is verified separately by the conformance kit.
|
|
566
|
+
for key in (CapabilityKey.RESULT_PRIMAL, CapabilityKey.RESULT_OBJECTIVE):
|
|
567
|
+
claims.setdefault(
|
|
568
|
+
key,
|
|
569
|
+
CapabilityClaim(
|
|
570
|
+
status=CapabilityStatus.UNVERIFIED,
|
|
571
|
+
mode=CapabilityMode.NATIVE,
|
|
572
|
+
verification=VerificationLevel.UNVERIFIED,
|
|
573
|
+
notes=("adapter result channel exists; successful native production requires conformance",),
|
|
574
|
+
),
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
return BackendCapabilityManifestV2(
|
|
578
|
+
backend=manifest.name,
|
|
579
|
+
backend_version=manifest.version,
|
|
580
|
+
binding=binding,
|
|
581
|
+
binding_version=binding_version,
|
|
582
|
+
adapter_version=adapter_version,
|
|
583
|
+
claims=claims,
|
|
584
|
+
metadata={"legacy_manifest_metadata": dict(manifest.metadata)},
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def with_verified_claims(
|
|
589
|
+
manifest: BackendCapabilityManifestV2,
|
|
590
|
+
verified: Mapping[CapabilityKey, CapabilityEvidence],
|
|
591
|
+
*,
|
|
592
|
+
status_overrides: Mapping[CapabilityKey, CapabilityStatus] | None = None,
|
|
593
|
+
restriction_overrides: Mapping[CapabilityKey, Mapping[str, Any]] | None = None,
|
|
594
|
+
) -> BackendCapabilityManifestV2:
|
|
595
|
+
claims = dict(manifest.claims)
|
|
596
|
+
status_overrides = status_overrides or {}
|
|
597
|
+
restriction_overrides = restriction_overrides or {}
|
|
598
|
+
for key, evidence in verified.items():
|
|
599
|
+
prior = claims.get(key)
|
|
600
|
+
status = status_overrides.get(
|
|
601
|
+
key,
|
|
602
|
+
prior.status if prior and prior.status not in {CapabilityStatus.UNVERIFIED, CapabilityStatus.UNSUPPORTED} else CapabilityStatus.SUPPORTED,
|
|
603
|
+
)
|
|
604
|
+
restrictions = restriction_overrides.get(key, prior.restrictions if prior else {})
|
|
605
|
+
if status is CapabilityStatus.RESTRICTED and not restrictions:
|
|
606
|
+
restrictions = {"verified_restriction": "see evidence"}
|
|
607
|
+
mode = prior.mode if prior and prior.mode not in {CapabilityMode.UNKNOWN, CapabilityMode.NONE} else CapabilityMode.NATIVE
|
|
608
|
+
claims[key] = CapabilityClaim(
|
|
609
|
+
status=status,
|
|
610
|
+
mode=mode,
|
|
611
|
+
verification=VerificationLevel.VERIFIED,
|
|
612
|
+
restrictions=restrictions,
|
|
613
|
+
evidence=(evidence,),
|
|
614
|
+
notes=prior.notes if prior else (),
|
|
615
|
+
)
|
|
616
|
+
return BackendCapabilityManifestV2(
|
|
617
|
+
backend=manifest.backend,
|
|
618
|
+
backend_version=manifest.backend_version,
|
|
619
|
+
binding=manifest.binding,
|
|
620
|
+
binding_version=manifest.binding_version,
|
|
621
|
+
adapter_version=manifest.adapter_version,
|
|
622
|
+
claims=claims,
|
|
623
|
+
numeric_limits=manifest.numeric_limits,
|
|
624
|
+
metadata=manifest.metadata,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
@dataclass(frozen=True, slots=True)
|
|
628
|
+
class CapabilityRequirementsV2:
|
|
629
|
+
required: tuple[CapabilityKey, ...]
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def requirements_v2_for(problem) -> CapabilityRequirementsV2:
|
|
633
|
+
# Import lazily so capability protocol remains usable during package bootstrap.
|
|
634
|
+
from solverpilot.problem import LinearProblem, QuadraticProblem
|
|
635
|
+
try:
|
|
636
|
+
from solverpilot.conic import ConeKind, ConicProblem
|
|
637
|
+
except Exception: # pragma: no cover - package bootstrap
|
|
638
|
+
ConeKind = ConicProblem = None
|
|
639
|
+
try:
|
|
640
|
+
from solverpilot.nlp import NLPProblem
|
|
641
|
+
except Exception: # pragma: no cover
|
|
642
|
+
NLPProblem = None
|
|
643
|
+
try:
|
|
644
|
+
from solverpilot.minlp import MINLPProblem
|
|
645
|
+
except Exception: # pragma: no cover
|
|
646
|
+
MINLPProblem = None
|
|
647
|
+
try:
|
|
648
|
+
from solverpilot.cp import CPProblem, CPAllDifferentIR, CPExactlyOneIR, CPTableIR, CPElementIR, CPCircuitIR, CPNoOverlapIR, CPCumulativeIR
|
|
649
|
+
except Exception: # pragma: no cover
|
|
650
|
+
CPProblem = CPAllDifferentIR = CPExactlyOneIR = CPTableIR = CPElementIR = CPCircuitIR = CPNoOverlapIR = CPCumulativeIR = None
|
|
651
|
+
|
|
652
|
+
if CPProblem is not None and isinstance(problem, CPProblem):
|
|
653
|
+
keys = [CapabilityKey.PROBLEM_CP]
|
|
654
|
+
for constraint in problem.constraints:
|
|
655
|
+
if CPAllDifferentIR is not None and isinstance(constraint, CPAllDifferentIR): keys.append(CapabilityKey.CONSTRAINT_ALL_DIFFERENT)
|
|
656
|
+
elif CPExactlyOneIR is not None and isinstance(constraint, CPExactlyOneIR): keys.append(CapabilityKey.CONSTRAINT_EXACTLY_ONE)
|
|
657
|
+
elif CPTableIR is not None and isinstance(constraint, CPTableIR): keys.append(CapabilityKey.CONSTRAINT_TABLE)
|
|
658
|
+
elif CPElementIR is not None and isinstance(constraint, CPElementIR): keys.append(CapabilityKey.CONSTRAINT_ELEMENT)
|
|
659
|
+
elif CPCircuitIR is not None and isinstance(constraint, CPCircuitIR): keys.append(CapabilityKey.CONSTRAINT_CIRCUIT)
|
|
660
|
+
elif CPNoOverlapIR is not None and isinstance(constraint, CPNoOverlapIR): keys.append(CapabilityKey.CONSTRAINT_NO_OVERLAP)
|
|
661
|
+
elif CPCumulativeIR is not None and isinstance(constraint, CPCumulativeIR): keys.append(CapabilityKey.CONSTRAINT_CUMULATIVE)
|
|
662
|
+
else: keys.append(CapabilityKey.CONSTRAINT_LINEAR)
|
|
663
|
+
return CapabilityRequirementsV2(tuple(dict.fromkeys(keys)))
|
|
664
|
+
|
|
665
|
+
if ConicProblem is not None and isinstance(problem, ConicProblem):
|
|
666
|
+
keys = [CapabilityKey.PROBLEM_CONIC, CapabilityKey.CONSTRAINT_LINEAR]
|
|
667
|
+
if problem.P.nnz:
|
|
668
|
+
keys.append(CapabilityKey.PROBLEM_CONIC_QUADRATIC)
|
|
669
|
+
kinds = {c.kind for c in problem.cones}
|
|
670
|
+
if ConeKind.SECOND_ORDER in kinds:
|
|
671
|
+
keys.append(CapabilityKey.CONSTRAINT_SOC)
|
|
672
|
+
if ConeKind.ROTATED_SECOND_ORDER in kinds:
|
|
673
|
+
keys.append(CapabilityKey.CONSTRAINT_ROTATED_SOC)
|
|
674
|
+
if ConeKind.POSITIVE_SEMIDEFINITE in kinds:
|
|
675
|
+
keys.append(CapabilityKey.CONSTRAINT_PSD)
|
|
676
|
+
return CapabilityRequirementsV2(tuple(keys))
|
|
677
|
+
|
|
678
|
+
if MINLPProblem is not None and isinstance(problem, MINLPProblem):
|
|
679
|
+
return CapabilityRequirementsV2((CapabilityKey.PROBLEM_MINLP, CapabilityKey.CONSTRAINT_NONLINEAR))
|
|
680
|
+
|
|
681
|
+
if NLPProblem is not None and isinstance(problem, NLPProblem):
|
|
682
|
+
return CapabilityRequirementsV2((CapabilityKey.PROBLEM_NLP, CapabilityKey.CONSTRAINT_NONLINEAR))
|
|
683
|
+
|
|
684
|
+
if isinstance(problem, QuadraticProblem):
|
|
685
|
+
return CapabilityRequirementsV2((CapabilityKey.PROBLEM_CONVEX_QP, CapabilityKey.CONSTRAINT_LINEAR))
|
|
686
|
+
if isinstance(problem, LinearProblem):
|
|
687
|
+
problem_key = CapabilityKey.PROBLEM_MILP if problem.has_integer_variables else CapabilityKey.PROBLEM_LP
|
|
688
|
+
return CapabilityRequirementsV2((problem_key, CapabilityKey.CONSTRAINT_LINEAR))
|
|
689
|
+
raise TypeError(f"unsupported problem type: {type(problem)!r}")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def compatible_v2(
|
|
693
|
+
manifest: BackendCapabilityManifestV2,
|
|
694
|
+
requirements: CapabilityRequirementsV2,
|
|
695
|
+
*,
|
|
696
|
+
allow_restricted: bool = True,
|
|
697
|
+
allow_safe_emulation: bool = False,
|
|
698
|
+
allow_risky_emulation: bool = False,
|
|
699
|
+
require_verified: bool = True,
|
|
700
|
+
) -> tuple[bool, tuple[CapabilityCheck, ...]]:
|
|
701
|
+
checks = tuple(
|
|
702
|
+
manifest.check(
|
|
703
|
+
key,
|
|
704
|
+
allow_restricted=allow_restricted,
|
|
705
|
+
allow_safe_emulation=allow_safe_emulation,
|
|
706
|
+
allow_risky_emulation=allow_risky_emulation,
|
|
707
|
+
require_verified=require_verified,
|
|
708
|
+
)
|
|
709
|
+
for key in requirements.required
|
|
710
|
+
)
|
|
711
|
+
return all(check.usable for check in checks), checks
|