solverpilot 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. solverpilot/__init__.py +250 -0
  2. solverpilot/_immutability.py +27 -0
  3. solverpilot/_version.py +3 -0
  4. solverpilot/applications/__init__.py +3 -0
  5. solverpilot/applications/routing/__init__.py +19 -0
  6. solverpilot/applications/routing/compile_milp.py +321 -0
  7. solverpilot/applications/routing/diagnostics.py +78 -0
  8. solverpilot/applications/routing/errors.py +14 -0
  9. solverpilot/applications/routing/heuristics.py +167 -0
  10. solverpilot/applications/routing/model.py +259 -0
  11. solverpilot/applications/routing/reference.py +81 -0
  12. solverpilot/applications/routing/solution.py +89 -0
  13. solverpilot/applications/routing/timeline.py +79 -0
  14. solverpilot/applications/routing/validation.py +169 -0
  15. solverpilot/applications/tsp/__init__.py +41 -0
  16. solverpilot/applications/tsp/compile_cp.py +121 -0
  17. solverpilot/applications/tsp/compile_milp.py +139 -0
  18. solverpilot/applications/tsp/distance.py +60 -0
  19. solverpilot/applications/tsp/errors.py +17 -0
  20. solverpilot/applications/tsp/heuristics.py +112 -0
  21. solverpilot/applications/tsp/model.py +175 -0
  22. solverpilot/applications/tsp/reference.py +121 -0
  23. solverpilot/applications/tsp/solution.py +65 -0
  24. solverpilot/applications/tsp/validation.py +92 -0
  25. solverpilot/backends/__init__.py +51 -0
  26. solverpilot/backends/_casadi_highs_worker.py +92 -0
  27. solverpilot/backends/base.py +28 -0
  28. solverpilot/backends/bundled_capi.py +910 -0
  29. solverpilot/backends/casadi_conic.py +303 -0
  30. solverpilot/backends/health.py +240 -0
  31. solverpilot/backends/highspy_native.py +217 -0
  32. solverpilot/backends/nlopt_native.py +237 -0
  33. solverpilot/backends/osqp_native.py +297 -0
  34. solverpilot/backends/protocol_v2.py +119 -0
  35. solverpilot/backends/pyscipopt_native.py +200 -0
  36. solverpilot/backends/registry.py +38 -0
  37. solverpilot/backends/scipy_highs.py +138 -0
  38. solverpilot/backends/scipy_highs_lp.py +195 -0
  39. solverpilot/backends/scipy_slsqp_qp.py +133 -0
  40. solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
  41. solverpilot/benchmark/__init__.py +23 -0
  42. solverpilot/benchmark/acquire.py +199 -0
  43. solverpilot/benchmark/baselines.py +70 -0
  44. solverpilot/benchmark/bundle.py +37 -0
  45. solverpilot/benchmark/campaign.py +150 -0
  46. solverpilot/benchmark/environment.py +115 -0
  47. solverpilot/benchmark/integrity.py +46 -0
  48. solverpilot/benchmark/model.py +146 -0
  49. solverpilot/benchmark/policy.py +182 -0
  50. solverpilot/benchmark/registry.py +38 -0
  51. solverpilot/benchmark/report.py +44 -0
  52. solverpilot/benchmark/runner.py +516 -0
  53. solverpilot/benchmark/slurm.py +55 -0
  54. solverpilot/benchmark/specs.py +75 -0
  55. solverpilot/benchmark/splits.py +181 -0
  56. solverpilot/benchmark/summary.py +384 -0
  57. solverpilot/benchmark/verify.py +142 -0
  58. solverpilot/benchmark/worker.py +71 -0
  59. solverpilot/bridges/__init__.py +19 -0
  60. solverpilot/bridges/engine.py +44 -0
  61. solverpilot/bridges/indicator.py +232 -0
  62. solverpilot/bridges/registry.py +78 -0
  63. solverpilot/bridges/transformation-tape.schema.json +53 -0
  64. solverpilot/bridges/types.py +149 -0
  65. solverpilot/bridges/validation.py +110 -0
  66. solverpilot/capabilities/__init__.py +32 -0
  67. solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
  68. solverpilot/capabilities/conformance.py +151 -0
  69. solverpilot/capabilities/enums.py +27 -0
  70. solverpilot/capabilities/manifest.py +19 -0
  71. solverpilot/capabilities/ontology.py +73 -0
  72. solverpilot/capabilities/requirements.py +37 -0
  73. solverpilot/capabilities/resolve.py +30 -0
  74. solverpilot/capabilities/v2.py +711 -0
  75. solverpilot/cli/__init__.py +1 -0
  76. solverpilot/cli/backend_health.py +77 -0
  77. solverpilot/cli/benchmark.py +331 -0
  78. solverpilot/cli/benchmark_worker.py +33 -0
  79. solverpilot/cli/capabilities.py +65 -0
  80. solverpilot/conic/__init__.py +11 -0
  81. solverpilot/conic/backend.py +263 -0
  82. solverpilot/conic/compiler.py +310 -0
  83. solverpilot/conic/conformance.py +60 -0
  84. solverpilot/conic/conic-ir-v1.schema.json +24 -0
  85. solverpilot/conic/ir.py +299 -0
  86. solverpilot/conic/validation.py +149 -0
  87. solverpilot/cp/__init__.py +7 -0
  88. solverpilot/cp/conformance.py +32 -0
  89. solverpilot/cp/cp-ir-v1.schema.json +13 -0
  90. solverpilot/cp/ir.py +170 -0
  91. solverpilot/cp/model.py +143 -0
  92. solverpilot/cp/ortools_backend.py +423 -0
  93. solverpilot/cp/ortools_worker.py +75 -0
  94. solverpilot/cp/reference.py +51 -0
  95. solverpilot/cp/serialization.py +117 -0
  96. solverpilot/cp/validate.py +83 -0
  97. solverpilot/diagnose/__init__.py +33 -0
  98. solverpilot/diagnose/infeasibility.py +463 -0
  99. solverpilot/diagnose/model.py +87 -0
  100. solverpilot/evaluation/__init__.py +53 -0
  101. solverpilot/evaluation/metrics.py +150 -0
  102. solverpilot/evaluation/oracle.py +288 -0
  103. solverpilot/evaluation/proof.py +53 -0
  104. solverpilot/evaluation/references.py +73 -0
  105. solverpilot/evaluation/regret.py +281 -0
  106. solverpilot/exceptions.py +34 -0
  107. solverpilot/experimental/__init__.py +32 -0
  108. solverpilot/extensions/__init__.py +56 -0
  109. solverpilot/extensions/entrypoints.py +34 -0
  110. solverpilot/extensions/errors.py +35 -0
  111. solverpilot/extensions/manager.py +651 -0
  112. solverpilot/extensions/manifest.py +232 -0
  113. solverpilot/extensions/model.py +101 -0
  114. solverpilot/extensions/versioning.py +136 -0
  115. solverpilot/history/__init__.py +22 -0
  116. solverpilot/history/errors.py +15 -0
  117. solverpilot/history/model.py +129 -0
  118. solverpilot/history/sqlite.py +651 -0
  119. solverpilot/inspect/__init__.py +3 -0
  120. solverpilot/inspect/fingerprint.py +199 -0
  121. solverpilot/intelligence/__init__.py +36 -0
  122. solverpilot/intelligence/errors.py +16 -0
  123. solverpilot/intelligence/fingerprint.py +108 -0
  124. solverpilot/intelligence/leakage.py +69 -0
  125. solverpilot/intelligence/schema.py +314 -0
  126. solverpilot/intelligence/shift.py +208 -0
  127. solverpilot/io/__init__.py +41 -0
  128. solverpilot/io/binary64.py +49 -0
  129. solverpilot/io/csv.py +230 -0
  130. solverpilot/io/errors.py +25 -0
  131. solverpilot/io/hashing.py +111 -0
  132. solverpilot/io/json.py +90 -0
  133. solverpilot/io/mapping.py +195 -0
  134. solverpilot/io/model.py +407 -0
  135. solverpilot/io/numeric.py +108 -0
  136. solverpilot/io/provenance.py +114 -0
  137. solverpilot/io/result.py +45 -0
  138. solverpilot/io/source.py +104 -0
  139. solverpilot/io/strict_json.py +133 -0
  140. solverpilot/minlp/__init__.py +6 -0
  141. solverpilot/minlp/compiler.py +66 -0
  142. solverpilot/minlp/conformance.py +34 -0
  143. solverpilot/minlp/convexity.py +85 -0
  144. solverpilot/minlp/ir.py +35 -0
  145. solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
  146. solverpilot/minlp/orchestrator.py +220 -0
  147. solverpilot/minlp/validation.py +28 -0
  148. solverpilot/model/__init__.py +27 -0
  149. solverpilot/model/compiler.py +1263 -0
  150. solverpilot/model/errors.py +22 -0
  151. solverpilot/model/expression.py +373 -0
  152. solverpilot/model/model.py +716 -0
  153. solverpilot/model/sets.py +60 -0
  154. solverpilot/model/types.py +36 -0
  155. solverpilot/nlp/__init__.py +7 -0
  156. solverpilot/nlp/ad.py +166 -0
  157. solverpilot/nlp/backend.py +113 -0
  158. solverpilot/nlp/compiler.py +116 -0
  159. solverpilot/nlp/conformance.py +58 -0
  160. solverpilot/nlp/ir.py +70 -0
  161. solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
  162. solverpilot/nlp/validation.py +60 -0
  163. solverpilot/plan/__init__.py +33 -0
  164. solverpilot/plan/model.py +79 -0
  165. solverpilot/plan/planner.py +201 -0
  166. solverpilot/plan/production.py +627 -0
  167. solverpilot/plan/selective_lp.py +65 -0
  168. solverpilot/problem/__init__.py +16 -0
  169. solverpilot/problem/enums.py +20 -0
  170. solverpilot/problem/hashing.py +88 -0
  171. solverpilot/problem/linear.py +196 -0
  172. solverpilot/problem/mps.py +447 -0
  173. solverpilot/problem/quadratic.py +192 -0
  174. solverpilot/reporting/__init__.py +27 -0
  175. solverpilot/reporting/explain.py +327 -0
  176. solverpilot/reporting/model.py +264 -0
  177. solverpilot/reporting/render.py +55 -0
  178. solverpilot/runtime/__init__.py +19 -0
  179. solverpilot/runtime/auto.py +193 -0
  180. solverpilot/runtime/budgeting.py +43 -0
  181. solverpilot/runtime/executor.py +126 -0
  182. solverpilot/runtime/portfolio.py +197 -0
  183. solverpilot/runtime/result.py +72 -0
  184. solverpilot/session/__init__.py +29 -0
  185. solverpilot/session/conformance_v2.py +239 -0
  186. solverpilot/session/mutations.py +115 -0
  187. solverpilot/session/persistent-session-trace.schema.json +45 -0
  188. solverpilot/session/persistent.py +491 -0
  189. solverpilot/session/reuse.py +81 -0
  190. solverpilot/session/session.py +236 -0
  191. solverpilot/trace/__init__.py +3 -0
  192. solverpilot/trace/schema.py +47 -0
  193. solverpilot/validate/__init__.py +10 -0
  194. solverpilot/validate/core.py +179 -0
  195. solverpilot/validate/result.py +55 -0
  196. solverpilot-0.1.dist-info/METADATA +554 -0
  197. solverpilot-0.1.dist-info/RECORD +201 -0
  198. solverpilot-0.1.dist-info/WHEEL +5 -0
  199. solverpilot-0.1.dist-info/entry_points.txt +4 -0
  200. solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
  201. solverpilot-0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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