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