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,263 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import importlib.util
5
+ from importlib.metadata import PackageNotFoundError, version
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+
10
+ from .ir import ConeKind, ConicProblem
11
+ from .validation import ConicValidationReport, validate_conic_solution
12
+
13
+
14
+ def _casadi_version() -> str | None:
15
+ try:
16
+ return version("casadi")
17
+ except PackageNotFoundError:
18
+ try:
19
+ import casadi as ca
20
+ return getattr(ca, "__version__", None)
21
+ except Exception:
22
+ return None
23
+
24
+
25
+ def _superscs_available() -> bool:
26
+ if importlib.util.find_spec("casadi") is None:
27
+ return False
28
+ try:
29
+ import casadi as ca
30
+ return bool(ca.has_conic("superscs"))
31
+ except Exception:
32
+ return False
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class ConicSolveResult:
37
+ backend: str
38
+ backend_status: str
39
+ x: np.ndarray | None
40
+ objective_reported: float | None
41
+ validation: ConicValidationReport
42
+ raw_statistics: dict[str, Any]
43
+
44
+ @property
45
+ def validated(self) -> bool:
46
+ return bool(self.validation.valid)
47
+
48
+
49
+ @dataclass(slots=True)
50
+ class CasadiSuperSCSBackend:
51
+ """Verified P6 bridge to the SuperSCS plugin bundled by CasADi 3.7.2.
52
+
53
+ This adapter is intentionally labeled a bridge, not a public SuperSCS Python
54
+ binding. It is used because dedicated Clarabel/SCS packages are unavailable in
55
+ the current release environment. Verified P6 solve scope is linear-objective SOC
56
+ and rotated SOC (the latter via an exact linear map to SOC). PSD is represented
57
+ and independently validated by P6 but intentionally solve-disabled for this adapter
58
+ after negative objective-accuracy conformance.
59
+ """
60
+
61
+ max_iter: int = 20_000
62
+ eps: float = 1e-8
63
+ verbose: bool = False
64
+
65
+ @property
66
+ def name(self) -> str:
67
+ return "casadi-superscs-conic-bridge"
68
+
69
+ @property
70
+ def capability_manifest_v2(self):
71
+ from solverpilot import __version__
72
+ from solverpilot.capabilities.v2 import (
73
+ BackendCapabilityManifestV2, CapabilityClaim, CapabilityEvidence, CapabilityKey,
74
+ CapabilityMode, CapabilityStatus, VerificationLevel,
75
+ )
76
+ binding_version = self.binding_version
77
+ backend_version = f"bundled-with-casadi-{binding_version or 'unknown'}"
78
+ evidence = CapabilityEvidence(
79
+ evidence_id="p6-casadi-superscs-conformance-v1",
80
+ kind="native-runtime-conformance",
81
+ backend_versions=(backend_version,),
82
+ binding_versions=(() if binding_version is None else (binding_version,)),
83
+ adapter_versions=(__version__,),
84
+ verified_on="2026-09-06",
85
+ notes=(
86
+ "CasADi SuperSCS plugin exercised on SOC/rotated-SOC models; PSD negative conformance retained",
87
+ "rotated SOC is exact adapter transformation to SOC",
88
+ ),
89
+ )
90
+ def verified(mode=CapabilityMode.NATIVE, restrictions=None):
91
+ restrictions = {} if restrictions is None else restrictions
92
+ status = CapabilityStatus.SUPPORTED if not restrictions else CapabilityStatus.RESTRICTED
93
+ return CapabilityClaim(status=status, mode=mode, verification=VerificationLevel.VERIFIED, restrictions=restrictions, evidence=(evidence,))
94
+ claims = {
95
+ CapabilityKey.PROBLEM_CONIC: verified(CapabilityMode.EMULATED_SAFE, {"binding": "CasADi SuperSCS bridge", "continuous_only": True, "verified_objective": "linear"}),
96
+ CapabilityKey.PROBLEM_CONIC_QUADRATIC: CapabilityClaim(status=CapabilityStatus.UNVERIFIED, mode=CapabilityMode.UNKNOWN, verification=VerificationLevel.UNVERIFIED, notes=("P6 generic quadratic-SOCP conformance failed on feasible analytic projection cases",)),
97
+ CapabilityKey.CONSTRAINT_LINEAR: verified(CapabilityMode.NATIVE),
98
+ CapabilityKey.CONSTRAINT_SOC: verified(CapabilityMode.EMULATED_SAFE, {"transport": "PSD Schur-complement representation via casadi.soc"}),
99
+ CapabilityKey.CONSTRAINT_ROTATED_SOC: verified(CapabilityMode.EMULATED_SAFE, {"transport": "exact linear map to SOC then PSD representation"}),
100
+ CapabilityKey.CONSTRAINT_PSD: CapabilityClaim(status=CapabilityStatus.UNVERIFIED, mode=CapabilityMode.UNKNOWN, verification=VerificationLevel.UNVERIFIED, notes=("P6 PSD semantic IR is implemented, but bundled SuperSCS failed objective-accuracy conformance for generic PSD programs",)),
101
+ CapabilityKey.RESULT_PRIMAL: verified(CapabilityMode.NATIVE),
102
+ CapabilityKey.RESULT_OBJECTIVE: verified(CapabilityMode.NATIVE),
103
+ }
104
+ return BackendCapabilityManifestV2(
105
+ backend=self.name,
106
+ backend_version=backend_version,
107
+ binding="casadi",
108
+ binding_version=binding_version,
109
+ adapter_version=__version__,
110
+ claims=claims,
111
+ numeric_limits={"p6_tolerance": self.eps},
112
+ metadata={"verification_only_bridge": True, "plugin": "superscs"},
113
+ )
114
+
115
+ @property
116
+ def binding_version(self) -> str | None:
117
+ return _casadi_version()
118
+
119
+ def is_available(self) -> bool:
120
+ return _superscs_available()
121
+
122
+ def _build_solver(self, problem: ConicProblem):
123
+ import casadi as ca
124
+
125
+ n = problem.n_variables
126
+ x = ca.MX.sym("x", n)
127
+ P = ca.DM(problem.P.toarray())
128
+ q = ca.DM(problem.q)
129
+ f = 0.5 * ca.mtimes([x.T, P, x]) + ca.dot(q, x) + float(problem.objective_offset)
130
+
131
+ g_exprs = []
132
+ lbg: list[float] = []
133
+ ubg: list[float] = []
134
+ if problem.n_linear_constraints:
135
+ Ad = ca.DM(problem.A.toarray())
136
+ lin = ca.mtimes(Ad, x)
137
+ g_exprs.append(lin)
138
+ lbg.extend(problem.constraint_lower.tolist())
139
+ ubg.extend(problem.constraint_upper.tolist())
140
+
141
+ psd_blocks = []
142
+ for block in problem.cones:
143
+ F = ca.DM(block.F.toarray())
144
+ g = ca.DM(block.g)
145
+ y = ca.mtimes(F, x) + g
146
+ if block.kind is ConeKind.SECOND_ORDER:
147
+ psd_blocks.append(ca.soc(y[1:], y[0]))
148
+ elif block.kind is ConeKind.ROTATED_SECOND_ORDER:
149
+ # 2*u*v >= ||z||^2, u,v>=0 <=>
150
+ # ||[sqrt(2)z, u-v]|| <= u+v.
151
+ u = y[0]; v = y[1]; z = y[2:]
152
+ soc_vec = ca.vertcat(np.sqrt(2.0) * z, u - v)
153
+ psd_blocks.append(ca.soc(soc_vec, u + v))
154
+ # Explicit nonnegativity keeps the semantic boundary obvious and
155
+ # protects against numerical interpretations of the transformed SOC.
156
+ g_exprs.append(ca.vertcat(u, v))
157
+ lbg.extend([0.0, 0.0]); ubg.extend([np.inf, np.inf])
158
+ elif block.kind is ConeKind.POSITIVE_SEMIDEFINITE:
159
+ rows, cols = block.output_shape
160
+ M = ca.reshape(y, cols, rows).T # row-major semantic flattening
161
+ psd_blocks.append(M)
162
+ else: # pragma: no cover
163
+ raise ValueError(block.kind)
164
+
165
+ h = None
166
+ if psd_blocks:
167
+ h = psd_blocks[0]
168
+ for blk in psd_blocks[1:]:
169
+ r1, c1 = h.shape; r2, c2 = blk.shape
170
+ h = ca.blockcat([[h, ca.MX.zeros(r1, c2)], [ca.MX.zeros(r2, c1), blk]])
171
+
172
+ qp: dict[str, Any] = {"x": x, "f": f}
173
+ if g_exprs:
174
+ qp["g"] = ca.vertcat(*g_exprs)
175
+ if h is not None:
176
+ qp["h"] = h
177
+
178
+ opts: dict[str, Any] = {"error_on_fail": False}
179
+ # SuperSCS option names follow its native interface. Keep the set minimal;
180
+ # unsupported verbosity options have differed across CasADi builds.
181
+ opts["superscs"] = {
182
+ "max_iters": int(self.max_iter),
183
+ "eps": float(self.eps),
184
+ "verbose": 1 if self.verbose else 0,
185
+ }
186
+ try:
187
+ solver = ca.qpsol("solverpilot_p6_superscs", "superscs", qp, opts)
188
+ except Exception:
189
+ # Some bundled SuperSCS builds reject the verbose key. Retry with only
190
+ # convergence controls rather than silently abandoning the backend.
191
+ opts["superscs"] = {"max_iters": int(self.max_iter), "eps": float(self.eps)}
192
+ solver = ca.qpsol("solverpilot_p6_superscs", "superscs", qp, opts)
193
+ return solver, np.asarray(lbg, dtype=float), np.asarray(ubg, dtype=float)
194
+
195
+ def solve(self, problem: ConicProblem) -> ConicSolveResult:
196
+ if not self.is_available():
197
+ raise RuntimeError("CasADi SuperSCS conic plugin is unavailable")
198
+ if problem.P.nnz:
199
+ raise RuntimeError("P6 fail-closed: quadratic conic objective conformance did not pass for the bundled CasADi SuperSCS plugin")
200
+ if any(block.kind is ConeKind.POSITIVE_SEMIDEFINITE for block in problem.cones):
201
+ raise RuntimeError("P6 fail-closed: generic PSD solve conformance did not pass for the bundled CasADi SuperSCS plugin; install a future verified PSD backend instead")
202
+ solver, lbg, ubg = self._build_solver(problem)
203
+ kwargs: dict[str, Any] = {
204
+ "lbx": problem.variable_lower,
205
+ "ubx": problem.variable_upper,
206
+ }
207
+ if lbg.size:
208
+ kwargs["lbg"] = lbg
209
+ kwargs["ubg"] = ubg
210
+ error = None
211
+ result = None
212
+ try:
213
+ result = solver(**kwargs)
214
+ except Exception as exc:
215
+ error = f"{type(exc).__name__}: {exc}"
216
+ try:
217
+ stats = dict(solver.stats())
218
+ except Exception:
219
+ stats = {}
220
+ x = None
221
+ objective = None
222
+ if result is not None and "x" in result:
223
+ arr = np.asarray(result["x"], dtype=float).reshape(-1)
224
+ if arr.shape == (problem.n_variables,) and np.all(np.isfinite(arr)):
225
+ x = arr
226
+ objective = problem.objective_value(arr)
227
+ raw = " ".join(str(stats.get(k, "")) for k in ("return_status", "unified_return_status")).lower()
228
+ if bool(stats.get("success")) and x is not None:
229
+ status = "optimal_candidate"
230
+ elif "infeasible" in raw:
231
+ status = "infeasible"
232
+ elif "unbounded" in raw:
233
+ status = "unbounded"
234
+ elif x is not None:
235
+ status = "candidate"
236
+ else:
237
+ status = "solver_error"
238
+ validation_tol = max(self.eps * 25.0, 5e-7)
239
+ validation = validate_conic_solution(problem, x, atol=validation_tol, rtol=validation_tol)
240
+ stats["independent_validation_tolerance"] = validation_tol
241
+ stats.update({
242
+ "casadi_version": _casadi_version(),
243
+ "conic_plugin": "superscs",
244
+ "error": error,
245
+ "verification_only_bridge": True,
246
+ "cone_families": sorted({c.kind.value for c in problem.cones}),
247
+ "adapter_transformations": (["rotated_second_order->second_order_exact_linear_map"] if any(c.kind is ConeKind.ROTATED_SECOND_ORDER for c in problem.cones) else []),
248
+ })
249
+ return ConicSolveResult(self.name, status, x, objective, validation, stats)
250
+
251
+
252
+ def solve_conic(problem: ConicProblem, *, backend: CasadiSuperSCSBackend | None = None) -> ConicSolveResult:
253
+ backend = CasadiSuperSCSBackend() if backend is None else backend
254
+ from solverpilot.capabilities.v2 import compatible_v2, requirements_v2_for
255
+ manifest = backend.capability_manifest_v2
256
+ ok, checks = compatible_v2(
257
+ manifest, requirements_v2_for(problem),
258
+ allow_safe_emulation=True, allow_risky_emulation=False, require_verified=True,
259
+ )
260
+ if not ok:
261
+ reasons = "; ".join(f"{c.key.value}: {c.reason}" for c in checks if not c.usable)
262
+ raise RuntimeError(f"P6 conic backend capability gate rejected solve: {reasons}")
263
+ return backend.solve(problem)
@@ -0,0 +1,310 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import OrderedDict
4
+ from dataclasses import dataclass, replace
5
+ import hashlib
6
+ import json
7
+ from time import perf_counter
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ from scipy import sparse
12
+
13
+ from solverpilot.problem import ObjectiveSense, VariableDomain
14
+ from solverpilot.model.errors import CompileError
15
+ from solverpilot.model.sets import (
16
+ EqualTo, GreaterThan, Interval, LessThan,
17
+ PositiveSemidefiniteCone, RotatedSecondOrderCone, SecondOrderCone,
18
+ )
19
+
20
+ from .ir import ConeAffineBlock, ConeKind, ConicProblem
21
+
22
+
23
+ _P6_CACHE_KEY = "conic-ir:p6:v1"
24
+ _P6_CAPABILITY_SIGNATURE = "conic-ir:p6:v1:casadi-superscs-bridge-verified-soc-rsoc-psd-unverified"
25
+ _P6_POLICY = "p0-rfc-012:p6-convex-conic-v1"
26
+ _MAX_SNAPSHOTS = 8
27
+
28
+
29
+ @dataclass(slots=True)
30
+ class _ConicCacheEntry:
31
+ semantic_hash: str
32
+ current: Any
33
+ parameter_versions: dict[str, int]
34
+ snapshots: OrderedDict[str, Any]
35
+ full_compiles: int = 1
36
+ snapshot_hits: int = 0
37
+
38
+
39
+ def _compilation_hash(model) -> str:
40
+ payload = {
41
+ "semantic_hash": model.semantic_hash,
42
+ "target": "conic-ir-v1",
43
+ "capability_signature": _P6_CAPABILITY_SIGNATURE,
44
+ "numerical_policy": _P6_POLICY,
45
+ "cone_semantics": ["soc", "rotated_soc", "psd"],
46
+ }
47
+ return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
48
+
49
+
50
+ def _poly_to_affine(poly, n: int, *, context: str) -> tuple[dict[int, float], float]:
51
+ if poly.degree > 1:
52
+ raise CompileError(f"{context} must be affine for P6 conic lowering")
53
+ return dict(poly.linear), float(poly.constant)
54
+
55
+
56
+ def _affine_map(evaluator, node, n: int, *, context: str) -> tuple[sparse.csr_matrix, np.ndarray, tuple[int, ...]]:
57
+ arr = np.asarray(evaluator.evaluate(node), dtype=object)
58
+ shape = tuple(arr.shape)
59
+ flat = arr.reshape(-1)
60
+ rr: list[int] = []
61
+ cc: list[int] = []
62
+ vv: list[float] = []
63
+ g = np.zeros(flat.size, dtype=float)
64
+ for r, poly in enumerate(flat):
65
+ coeffs, const = _poly_to_affine(poly, n, context=context)
66
+ g[r] = const
67
+ for c, value in coeffs.items():
68
+ if value != 0.0:
69
+ rr.append(r); cc.append(int(c)); vv.append(float(value))
70
+ F = sparse.csr_matrix((vv, (rr, cc)), shape=(flat.size, n), dtype=float)
71
+ return F, g, shape
72
+
73
+
74
+ def _assert_psd_affine_symmetry(evaluator, node, *, context: str) -> None:
75
+ arr = np.asarray(evaluator.evaluate(node), dtype=object)
76
+ if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
77
+ raise CompileError(f"{context} must be a square matrix")
78
+ for i in range(arr.shape[0]):
79
+ for j in range(i + 1, arr.shape[1]):
80
+ if arr[i, j] != arr[j, i]:
81
+ raise CompileError(
82
+ f"{context} is not structurally symmetric; P6 refuses to silently symmetrize PSD semantics"
83
+ )
84
+
85
+
86
+ def _build_conic(model, *, cache_status: str, changed_parameters: tuple[str, ...]):
87
+ # Import P2/P4 compiler internals only at execution time to avoid module init cycles.
88
+ from solverpilot.model.compiler import (
89
+ CompiledModel, CompilationReport, _Evaluator, _constraint_block,
90
+ _objective_data, _rows_to_csr, _variable_layout,
91
+ )
92
+
93
+ total_t0 = perf_counter()
94
+ if model.objective is None:
95
+ raise CompileError("model has no objective")
96
+ if model.objective.sense is not ObjectiveSense.MINIMIZE:
97
+ raise CompileError("P6 conic compilation supports convex minimization only")
98
+ if any(data.domain is not VariableDomain.CONTINUOUS for data in model._variables.values()):
99
+ raise CompileError("P6 conic execution currently supports continuous variables only")
100
+ if model.objective.expression.polynomial_degree is None or model.objective.expression.polynomial_degree > 2:
101
+ raise CompileError("P6 objective must be affine or convex quadratic")
102
+
103
+ analysis_t0 = perf_counter()
104
+ offsets, n, vl, vu, domains, source_vars = _variable_layout(model)
105
+ semantic_analysis_s = perf_counter() - analysis_t0
106
+ evaluator = _Evaluator(model, offsets)
107
+
108
+ lower_t0 = perf_counter()
109
+ rows: list[dict[int, float]] = []
110
+ cl: list[float] = []
111
+ cu: list[float] = []
112
+ cones: list[ConeAffineBlock] = []
113
+ source_constraints: dict[str, Any] = {}
114
+ cone_dependencies: set[str] = set()
115
+
116
+ for constraint in model.constraints:
117
+ cid = constraint.entity_id.value
118
+ set_ = constraint.set
119
+ if isinstance(set_, (LessThan, GreaterThan, EqualTo, Interval)):
120
+ block_rows, block_cl, block_cu = _constraint_block(evaluator, constraint)
121
+ start = len(rows)
122
+ rows.extend(block_rows); cl.extend(block_cl); cu.extend(block_cu)
123
+ source_constraints[cid] = {
124
+ "semantic_kind": "function_in_scalar_set",
125
+ "execution_rows": list(range(start, len(rows))),
126
+ "shape": list(constraint.function.shape),
127
+ }
128
+ continue
129
+
130
+ F, g, shape = _affine_map(evaluator, constraint.function._node, n, context=f"cone constraint {cid}")
131
+ deps = tuple(sorted(constraint.function.parameter_dependencies))
132
+ cone_dependencies.update(deps)
133
+ if isinstance(set_, SecondOrderCone):
134
+ kind = ConeKind.SECOND_ORDER
135
+ expected = (set_.dimension,)
136
+ elif isinstance(set_, RotatedSecondOrderCone):
137
+ kind = ConeKind.ROTATED_SECOND_ORDER
138
+ expected = (set_.dimension,)
139
+ elif isinstance(set_, PositiveSemidefiniteCone):
140
+ _assert_psd_affine_symmetry(evaluator, constraint.function._node, context=f"PSD constraint {cid}")
141
+ kind = ConeKind.POSITIVE_SEMIDEFINITE
142
+ expected = (set_.dimension, set_.dimension)
143
+ else:
144
+ raise CompileError(f"unsupported P6 constraint set {type(set_).__name__}")
145
+ if shape != expected:
146
+ raise CompileError(f"cone shape mismatch for {cid}: expected {expected}, got {shape}")
147
+ index = len(cones)
148
+ cones.append(ConeAffineBlock(kind, F, g, expected, source_id=cid, metadata={"parameter_dependencies": deps}))
149
+ source_constraints[cid] = {
150
+ "semantic_kind": kind.value,
151
+ "cone_block": index,
152
+ "shape": list(shape),
153
+ "parameter_dependencies": list(deps),
154
+ }
155
+
156
+ if not cones:
157
+ raise CompileError("P6 conic compiler received a model with no cone constraints")
158
+ A = _rows_to_csr(rows, n)
159
+ # Reuse the already verified P2 quadratic convention 0.5*x'P*x + q'x + offset.
160
+ target = "legacy-qp-v1" if model.objective.expression.polynomial_degree == 2 else "legacy-linear-v1"
161
+ q, offset, P = _objective_data(evaluator, model, n, target)
162
+ if P is None:
163
+ P = sparse.csr_matrix((n, n), dtype=float)
164
+ try:
165
+ problem = ConicProblem.from_data(
166
+ P=P,
167
+ q=q,
168
+ A=A,
169
+ variable_lower=vl,
170
+ variable_upper=vu,
171
+ constraint_lower=np.asarray(cl, dtype=float),
172
+ constraint_upper=np.asarray(cu, dtype=float),
173
+ cones=cones,
174
+ objective_offset=offset,
175
+ name=model.name,
176
+ metadata={
177
+ "semantic_hash": model.semantic_hash,
178
+ "compiled_by": "solverpilot-p6",
179
+ "compiler_policy": _P6_POLICY,
180
+ "cone_families": sorted({c.kind.value for c in cones}),
181
+ },
182
+ verify_convexity=True,
183
+ )
184
+ except ValueError as exc:
185
+ raise CompileError(str(exc)) from exc
186
+
187
+ pmap: dict[str, Any] = {}
188
+ for pid, pdata in model._parameters.items():
189
+ regions: list[str] = []
190
+ for cid, info in source_constraints.items():
191
+ if pid in info.get("parameter_dependencies", []):
192
+ regions.append(f"cone:{cid}")
193
+ if model.objective is not None and pid in model.objective.expression.parameter_dependencies:
194
+ regions.append("objective")
195
+ for c in model.constraints:
196
+ if pid in c.function.parameter_dependencies and f"cone:{c.entity_id.value}" not in regions:
197
+ regions.append(f"constraint:{c.entity_id.value}")
198
+ pmap[pid] = {
199
+ "shape": list(pdata.shape),
200
+ "patchable": False,
201
+ "parameter_compile_class": "p6_full_conic_relower",
202
+ "plan_sensitive": True if regions else False,
203
+ "execution_regions": regions,
204
+ }
205
+ comp_hash = _compilation_hash(model)
206
+ source_map = {
207
+ "variables": source_vars,
208
+ "constraints": source_constraints,
209
+ "objective": {model.objective.entity_id.value: {"execution_objective": True}},
210
+ }
211
+ lowering_s = perf_counter() - lower_t0
212
+ report = CompilationReport(
213
+ target="conic-ir-v1",
214
+ semantic_analysis_s=semantic_analysis_s,
215
+ lowering_s=lowering_s,
216
+ total_s=perf_counter() - total_t0,
217
+ n_semantic_variables=len(model._variables),
218
+ n_execution_variables=n,
219
+ n_semantic_constraints=len(model._constraints),
220
+ n_execution_constraints=len(rows) + len(cones),
221
+ cache_status=cache_status,
222
+ changed_parameters=changed_parameters,
223
+ reused_execution_rows=0,
224
+ recompiled_execution_rows=len(rows) + len(cones),
225
+ objective_recompiled=True,
226
+ execution_mutations=(),
227
+ n_transformations=0,
228
+ )
229
+ return CompiledModel(
230
+ execution_ir=problem,
231
+ semantic_hash=model.semantic_hash,
232
+ data_hash=model.data_hash,
233
+ compilation_hash=comp_hash,
234
+ parameter_map=pmap,
235
+ source_map=source_map,
236
+ transformation_tape=(),
237
+ capability_signature=_P6_CAPABILITY_SIGNATURE,
238
+ mutation_plan={
239
+ "mode": "p6_full_conic_relower",
240
+ "incremental_parameter_patching": False,
241
+ "backend_session_patch": False,
242
+ "changed_parameters": list(changed_parameters),
243
+ "affected_regions": [r for pid in changed_parameters for r in pmap.get(pid, {}).get("execution_regions", [])],
244
+ "execution_mutations": [],
245
+ "reason": "P6 cone maps are rebuilt from semantic affine expressions; persistent conic mutation is outside P6 scope",
246
+ },
247
+ plan_sensitive_dependencies=tuple(sorted(cone_dependencies)),
248
+ precondition_certificate_hashes=(),
249
+ compilation_report=report,
250
+ reconstruction_contract={
251
+ "primal": "identity_by_source_map",
252
+ "dual": "backend_specific_not_standardized_p6",
253
+ "certificate": "none_p6",
254
+ "original_space_validation": "solverpilot.conic.validate_conic_solution + semantic validator",
255
+ },
256
+ schema_version="solverpilot.compiled-model.p6.v1",
257
+ )
258
+
259
+
260
+ def compile_conic_model(model, *, use_cache: bool = True):
261
+ entry = model._compiler_cache.get(_P6_CACHE_KEY)
262
+ changed: tuple[str, ...] = ()
263
+ if isinstance(entry, _ConicCacheEntry):
264
+ changed = tuple(
265
+ pid for pid in model._parameters
266
+ if model._parameter_versions.get(pid, 0) != entry.parameter_versions.get(pid, 0)
267
+ )
268
+ if not use_cache:
269
+ return _build_conic(model, cache_status="disabled", changed_parameters=changed)
270
+ if not isinstance(entry, _ConicCacheEntry) or entry.semantic_hash != model.semantic_hash:
271
+ compiled = _build_conic(model, cache_status="miss", changed_parameters=())
272
+ snapshots = OrderedDict([(compiled.data_hash, compiled)])
273
+ model._compiler_cache[_P6_CACHE_KEY] = _ConicCacheEntry(
274
+ semantic_hash=model.semantic_hash,
275
+ current=compiled,
276
+ parameter_versions=dict(model._parameter_versions),
277
+ snapshots=snapshots,
278
+ )
279
+ return compiled
280
+ if model.data_hash in entry.snapshots:
281
+ entry.snapshot_hits += 1
282
+ snap = entry.snapshots[model.data_hash]
283
+ hit = replace(
284
+ snap,
285
+ compilation_report=replace(
286
+ snap.compilation_report,
287
+ semantic_analysis_s=0.0,
288
+ lowering_s=0.0,
289
+ total_s=0.0,
290
+ cache_status="snapshot_hit",
291
+ changed_parameters=changed,
292
+ reused_execution_rows=snap.compilation_report.n_execution_constraints,
293
+ recompiled_execution_rows=0,
294
+ objective_recompiled=False,
295
+ ),
296
+ )
297
+ entry.current = hit
298
+ entry.parameter_versions = dict(model._parameter_versions)
299
+ entry.snapshots[model.data_hash] = hit
300
+ entry.snapshots.move_to_end(model.data_hash)
301
+ return hit
302
+ compiled = _build_conic(model, cache_status="full_conic_relower", changed_parameters=changed)
303
+ entry.full_compiles += 1
304
+ entry.current = compiled
305
+ entry.parameter_versions = dict(model._parameter_versions)
306
+ entry.snapshots[compiled.data_hash] = compiled
307
+ entry.snapshots.move_to_end(compiled.data_hash)
308
+ while len(entry.snapshots) > _MAX_SNAPSHOTS:
309
+ entry.snapshots.popitem(last=False)
310
+ return compiled
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import numpy as np
5
+
6
+ from .backend import CasadiSuperSCSBackend
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class ConicConformanceCheck:
11
+ name: str
12
+ passed: bool
13
+ detail: str
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ConicConformanceReport:
18
+ backend: str
19
+ binding_version: str | None
20
+ checks: tuple[ConicConformanceCheck, ...]
21
+
22
+ @property
23
+ def passed(self) -> bool:
24
+ return all(c.passed for c in self.checks)
25
+
26
+
27
+ def conform_casadi_superscs_backend(backend: CasadiSuperSCSBackend | None = None) -> ConicConformanceReport:
28
+ import solverpilot as om
29
+ backend = CasadiSuperSCSBackend(eps=1e-9, max_iter=100000) if backend is None else backend
30
+ checks: list[ConicConformanceCheck] = []
31
+ if not backend.is_available():
32
+ return ConicConformanceReport(backend.name, backend.binding_version, (ConicConformanceCheck("availability", False, "plugin unavailable"),))
33
+
34
+ m=om.Model(); x=m.variable(2,lower=[3,4],upper=[3,4]); t=m.variable(lower=0,upper=10); m.soc(t,x); m.minimize(t)
35
+ r=backend.solve(m.compile(use_cache=False).execution_ir)
36
+ ok=r.validation.valid and r.objective_reported is not None and abs(r.objective_reported-5.0) <= 2e-6
37
+ checks.append(ConicConformanceCheck("soc-3-4-5", ok, f"objective={r.objective_reported}, valid={r.validation.valid}"))
38
+
39
+ m=om.Model(); z=m.variable(1,lower=[1],upper=[1]); u=m.variable(lower=0,upper=5); v=m.variable(lower=0,upper=5); m.rotated_soc(u,v,z); m.minimize(u+v)
40
+ r=backend.solve(m.compile(use_cache=False).execution_ir)
41
+ expected=float(np.sqrt(2.0)); ok=r.validation.valid and r.objective_reported is not None and abs(r.objective_reported-expected) <= 5e-6
42
+ checks.append(ConicConformanceCheck("rotated-soc-analytic", ok, f"objective={r.objective_reported}, expected={expected}, valid={r.validation.valid}"))
43
+
44
+ # Negative evidence is part of conformance: generic PSD must not be silently accepted.
45
+ m=om.Model(); tt=m.variable(lower=0,upper=5); m.psd(tt*m.constant(np.eye(2))+m.constant([[0.,1.],[1.,0.]])); m.minimize(tt)
46
+ try:
47
+ backend.solve(m.compile(use_cache=False).execution_ir)
48
+ refused=False; detail="PSD unexpectedly solved"
49
+ except RuntimeError as exc:
50
+ refused="PSD solve conformance did not pass" in str(exc); detail=str(exc)
51
+ checks.append(ConicConformanceCheck("psd-fail-closed", refused, detail))
52
+
53
+ m=om.Model(); xx=m.variable(3,lower=-10,upper=10); a=np.array([0.4322322767575165,-0.861140634080589,0.06823807914880933]); m.soc(2.848110356815995,xx); m.minimize(0.5*((xx-m.constant(a))**2).sum())
54
+ try:
55
+ backend.solve(m.compile(use_cache=False).execution_ir)
56
+ qrefused=False; qdetail="quadratic conic objective unexpectedly solved"
57
+ except RuntimeError as exc:
58
+ qrefused="quadratic conic objective conformance did not pass" in str(exc); qdetail=str(exc)
59
+ checks.append(ConicConformanceCheck("quadratic-conic-fail-closed", qrefused, qdetail))
60
+ return ConicConformanceReport(backend.name, backend.binding_version, tuple(checks))
@@ -0,0 +1,24 @@
1
+ {
2
+ "$id": "solverpilot.conic-ir.v1",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "type": "object",
5
+ "required": ["schema", "P", "q", "A", "variable_lower", "variable_upper", "constraint_lower", "constraint_upper", "cones", "objective_sense", "objective_offset", "structural_hash", "data_hash"],
6
+ "properties": {
7
+ "schema": {"const": "solverpilot.conic-ir.v1"},
8
+ "P": {"type": "object"},
9
+ "q": {"type": "array", "items": {"type": "number"}},
10
+ "A": {"type": "object"},
11
+ "variable_lower": {"type": "array", "items": {"anyOf": [{"type": "number"}, {"enum": ["+inf", "-inf"]}]}},
12
+ "variable_upper": {"type": "array", "items": {"anyOf": [{"type": "number"}, {"enum": ["+inf", "-inf"]}]}},
13
+ "constraint_lower": {"type": "array", "items": {"anyOf": [{"type": "number"}, {"enum": ["+inf", "-inf"]}]}},
14
+ "constraint_upper": {"type": "array", "items": {"anyOf": [{"type": "number"}, {"enum": ["+inf", "-inf"]}]}},
15
+ "cones": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["kind", "F", "g", "output_shape", "structural_hash", "data_hash"], "properties": {"kind": {"enum": ["second_order", "rotated_second_order", "positive_semidefinite"]}}}},
16
+ "objective_sense": {"const": "minimize"},
17
+ "objective_offset": {"type": "number"},
18
+ "name": {"type": ["string", "null"]},
19
+ "metadata": {"type": "object"},
20
+ "convexity_status": {"type": "string"},
21
+ "structural_hash": {"type": "string"},
22
+ "data_hash": {"type": "string"}
23
+ }
24
+ }