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,910 @@
1
+ from __future__ import annotations
2
+
3
+ """Direct C-API verification bridges for solver libraries bundled by CasADi.
4
+
5
+ These backends intentionally do *not* pretend to be the dedicated public Python
6
+ packages. They exist to exercise the underlying solver C APIs directly in the
7
+ network-restricted verification environment used by this project. Availability
8
+ is deliberately strict: only the CasADi build/version and solver ABI that were
9
+ verified by M13 are accepted.
10
+ """
11
+
12
+ from dataclasses import dataclass, field
13
+ import ctypes
14
+ import hashlib
15
+ import importlib.util
16
+ from importlib.metadata import PackageNotFoundError, version
17
+ from pathlib import Path
18
+ from time import perf_counter
19
+
20
+ import numpy as np
21
+ from scipy import sparse
22
+
23
+ from solverpilot.capabilities import BackendManifest, Capability, SupportLevel
24
+ from solverpilot.problem import LinearProblem, ObjectiveSense, QuadraticProblem, VariableDomain
25
+ from solverpilot.problem.quadratic import require_confirmed_convexity
26
+
27
+ from .base import BackendSolveResult, BackendUnavailableError
28
+ from .osqp_native import _dual_certificate_check, _osqp_data, _osqp_status, _primal_certificate_check
29
+
30
+
31
+ _VERIFIED_CASADI_VERSION = "3.7.2"
32
+ _VERIFIED_OSQP_VERSION = "0.6.3"
33
+ _VERIFIED_HIGHS_VERSION = "1.10.0"
34
+
35
+
36
+ def _sha256(path: Path) -> str:
37
+ h = hashlib.sha256()
38
+ with path.open("rb") as f:
39
+ for block in iter(lambda: f.read(1024 * 1024), b""):
40
+ h.update(block)
41
+ return h.hexdigest()
42
+
43
+
44
+ def _casadi_version() -> str | None:
45
+ try:
46
+ return version("casadi")
47
+ except PackageNotFoundError:
48
+ return None
49
+
50
+
51
+ def _casadi_dir() -> Path | None:
52
+ spec = importlib.util.find_spec("casadi")
53
+ if spec is None or spec.origin is None:
54
+ return None
55
+ return Path(spec.origin).resolve().parent
56
+
57
+
58
+ def _bundled_paths() -> dict[str, Path] | None:
59
+ root = _casadi_dir()
60
+ if root is None:
61
+ return None
62
+ return {
63
+ "root": root,
64
+ "osqp_lib": root / "libosqp.so",
65
+ "osqp_header": root / "include" / "osqp" / "osqp.h",
66
+ "osqp_types": root / "include" / "osqp" / "types.h",
67
+ "osqp_config": root / "include" / "osqp" / "osqp_configure.h",
68
+ "highs_lib": root / "libhighs.so",
69
+ "highs_header": root / "include" / "highs" / "interfaces" / "highs_c_api.h",
70
+ }
71
+
72
+
73
+ # ---- OSQP 0.6.3 ABI verified against the headers bundled by CasADi 3.7.2 ----
74
+ _CInt = ctypes.c_longlong # DLONG
75
+ _CFloat = ctypes.c_double # !DFLOAT
76
+
77
+
78
+ class _CSC(ctypes.Structure):
79
+ _fields_ = [
80
+ ("nzmax", _CInt),
81
+ ("m", _CInt),
82
+ ("n", _CInt),
83
+ ("p", ctypes.POINTER(_CInt)),
84
+ ("i", ctypes.POINTER(_CInt)),
85
+ ("x", ctypes.POINTER(_CFloat)),
86
+ ("nz", _CInt),
87
+ ]
88
+
89
+
90
+ class _OSQPData(ctypes.Structure):
91
+ _fields_ = [
92
+ ("n", _CInt),
93
+ ("m", _CInt),
94
+ ("P", ctypes.POINTER(_CSC)),
95
+ ("A", ctypes.POINTER(_CSC)),
96
+ ("q", ctypes.POINTER(_CFloat)),
97
+ ("l", ctypes.POINTER(_CFloat)),
98
+ ("u", ctypes.POINTER(_CFloat)),
99
+ ]
100
+
101
+
102
+ class _OSQPSettings(ctypes.Structure):
103
+ _fields_ = [
104
+ ("rho", _CFloat),
105
+ ("sigma", _CFloat),
106
+ ("scaling", _CInt),
107
+ ("adaptive_rho", _CInt),
108
+ ("adaptive_rho_interval", _CInt),
109
+ ("adaptive_rho_tolerance", _CFloat),
110
+ ("max_iter", _CInt),
111
+ ("eps_abs", _CFloat),
112
+ ("eps_rel", _CFloat),
113
+ ("eps_prim_inf", _CFloat),
114
+ ("eps_dual_inf", _CFloat),
115
+ ("alpha", _CFloat),
116
+ ("linsys_solver", ctypes.c_int),
117
+ ("delta", _CFloat),
118
+ ("polish", _CInt),
119
+ ("polish_refine_iter", _CInt),
120
+ ("verbose", _CInt),
121
+ ("scaled_termination", _CInt),
122
+ ("check_termination", _CInt),
123
+ ("warm_start", _CInt),
124
+ ]
125
+
126
+
127
+ class _OSQPSolution(ctypes.Structure):
128
+ _fields_ = [("x", ctypes.POINTER(_CFloat)), ("y", ctypes.POINTER(_CFloat))]
129
+
130
+
131
+ class _OSQPInfo(ctypes.Structure):
132
+ _fields_ = [
133
+ ("iter", _CInt),
134
+ ("status", ctypes.c_char * 32),
135
+ ("status_val", _CInt),
136
+ ("status_polish", _CInt),
137
+ ("obj_val", _CFloat),
138
+ ("pri_res", _CFloat),
139
+ ("dua_res", _CFloat),
140
+ ("rho_updates", _CInt),
141
+ ("rho_estimate", _CFloat),
142
+ ]
143
+
144
+
145
+ class _OSQPWorkspace(ctypes.Structure):
146
+ # PROFILING is disabled in the verified bundled OSQP build. Only fields up
147
+ # through `info` are needed, but preceding fields must preserve ABI offsets.
148
+ _fields_ = [
149
+ ("data", ctypes.POINTER(_OSQPData)),
150
+ ("linsys_solver", ctypes.c_void_p),
151
+ ("pol", ctypes.c_void_p),
152
+ ("rho_vec", ctypes.POINTER(_CFloat)),
153
+ ("rho_inv_vec", ctypes.POINTER(_CFloat)),
154
+ ("constr_type", ctypes.POINTER(_CInt)),
155
+ ("x", ctypes.POINTER(_CFloat)),
156
+ ("y", ctypes.POINTER(_CFloat)),
157
+ ("z", ctypes.POINTER(_CFloat)),
158
+ ("xz_tilde", ctypes.POINTER(_CFloat)),
159
+ ("x_prev", ctypes.POINTER(_CFloat)),
160
+ ("z_prev", ctypes.POINTER(_CFloat)),
161
+ ("Ax", ctypes.POINTER(_CFloat)),
162
+ ("Px", ctypes.POINTER(_CFloat)),
163
+ ("Aty", ctypes.POINTER(_CFloat)),
164
+ ("delta_y", ctypes.POINTER(_CFloat)),
165
+ ("Atdelta_y", ctypes.POINTER(_CFloat)),
166
+ ("delta_x", ctypes.POINTER(_CFloat)),
167
+ ("Pdelta_x", ctypes.POINTER(_CFloat)),
168
+ ("Adelta_x", ctypes.POINTER(_CFloat)),
169
+ ("D_temp", ctypes.POINTER(_CFloat)),
170
+ ("D_temp_A", ctypes.POINTER(_CFloat)),
171
+ ("E_temp", ctypes.POINTER(_CFloat)),
172
+ ("settings", ctypes.POINTER(_OSQPSettings)),
173
+ ("scaling", ctypes.c_void_p),
174
+ ("solution", ctypes.POINTER(_OSQPSolution)),
175
+ ("info", ctypes.POINTER(_OSQPInfo)),
176
+ ("summary_printed", _CInt),
177
+ ]
178
+
179
+
180
+ _OSQP_ABI_EXPECTED = {
181
+ "sizeof_c_int": 8,
182
+ "sizeof_c_float": 8,
183
+ "sizeof_csc": 56,
184
+ "sizeof_OSQPData": 56,
185
+ "sizeof_OSQPSettings": 160,
186
+ "sizeof_OSQPSolution": 16,
187
+ "sizeof_OSQPInfo": 96,
188
+ "sizeof_OSQPWorkspace": 224,
189
+ "offset_ws_solution": 200,
190
+ "offset_ws_info": 208,
191
+ "offset_info_status": 8,
192
+ "offset_info_obj": 56,
193
+ }
194
+
195
+
196
+ def _osqp_ctypes_layout() -> dict[str, int]:
197
+ return {
198
+ "sizeof_c_int": ctypes.sizeof(_CInt),
199
+ "sizeof_c_float": ctypes.sizeof(_CFloat),
200
+ "sizeof_csc": ctypes.sizeof(_CSC),
201
+ "sizeof_OSQPData": ctypes.sizeof(_OSQPData),
202
+ "sizeof_OSQPSettings": ctypes.sizeof(_OSQPSettings),
203
+ "sizeof_OSQPSolution": ctypes.sizeof(_OSQPSolution),
204
+ "sizeof_OSQPInfo": ctypes.sizeof(_OSQPInfo),
205
+ "sizeof_OSQPWorkspace": ctypes.sizeof(_OSQPWorkspace),
206
+ "offset_ws_solution": _OSQPWorkspace.solution.offset,
207
+ "offset_ws_info": _OSQPWorkspace.info.offset,
208
+ "offset_info_status": _OSQPInfo.status.offset,
209
+ "offset_info_obj": _OSQPInfo.obj_val.offset,
210
+ }
211
+
212
+
213
+ def _verified_osqp_config(paths: dict[str, Path]) -> bool:
214
+ try:
215
+ config = paths["osqp_config"].read_text(encoding="utf-8")
216
+ except OSError:
217
+ return False
218
+ return (
219
+ "#define DLONG" in config
220
+ and "/* #undef DFLOAT */" in config
221
+ and "/* #undef PROFILING */" in config
222
+ and "/* #undef EMBEDDED */" in config
223
+ and _osqp_ctypes_layout() == _OSQP_ABI_EXPECTED
224
+ )
225
+
226
+
227
+ def _load_osqp_capi(paths: dict[str, Path]):
228
+ lib = ctypes.CDLL(str(paths["osqp_lib"]))
229
+ lib.osqp_version.restype = ctypes.c_char_p
230
+ lib.osqp_set_default_settings.argtypes = [ctypes.POINTER(_OSQPSettings)]
231
+ lib.osqp_setup.argtypes = [
232
+ ctypes.POINTER(ctypes.POINTER(_OSQPWorkspace)),
233
+ ctypes.POINTER(_OSQPData),
234
+ ctypes.POINTER(_OSQPSettings),
235
+ ]
236
+ lib.osqp_setup.restype = _CInt
237
+ lib.osqp_solve.argtypes = [ctypes.POINTER(_OSQPWorkspace)]
238
+ lib.osqp_solve.restype = _CInt
239
+ lib.osqp_cleanup.argtypes = [ctypes.POINTER(_OSQPWorkspace)]
240
+ lib.osqp_cleanup.restype = _CInt
241
+ lib.osqp_update_lin_cost.argtypes = [ctypes.POINTER(_OSQPWorkspace), ctypes.POINTER(_CFloat)]
242
+ lib.osqp_update_lin_cost.restype = _CInt
243
+ lib.osqp_update_bounds.argtypes = [
244
+ ctypes.POINTER(_OSQPWorkspace),
245
+ ctypes.POINTER(_CFloat),
246
+ ctypes.POINTER(_CFloat),
247
+ ]
248
+ lib.osqp_update_bounds.restype = _CInt
249
+ lib.osqp_update_P_A.argtypes = [
250
+ ctypes.POINTER(_OSQPWorkspace),
251
+ ctypes.POINTER(_CFloat),
252
+ ctypes.POINTER(_CInt),
253
+ _CInt,
254
+ ctypes.POINTER(_CFloat),
255
+ ctypes.POINTER(_CInt),
256
+ _CInt,
257
+ ]
258
+ lib.osqp_update_P_A.restype = _CInt
259
+ return lib
260
+
261
+
262
+ def _status_text(info: _OSQPInfo) -> str:
263
+ return bytes(info.status).split(b"\0", 1)[0].decode("ascii", errors="replace")
264
+
265
+
266
+ def _c_int_array(values: np.ndarray):
267
+ arr = np.asarray(values, dtype=np.int64)
268
+ return (_CInt * len(arr))(*arr.tolist())
269
+
270
+
271
+ def _c_float_array(values: np.ndarray):
272
+ arr = np.asarray(values, dtype=np.float64)
273
+ return (_CFloat * len(arr))(*arr.tolist())
274
+
275
+
276
+ @dataclass(slots=True)
277
+ class _OSQPState:
278
+ work: ctypes.POINTER(_OSQPWorkspace)
279
+ keepalive: tuple[object, ...]
280
+ structural_hash: str
281
+ p_values: np.ndarray
282
+ a_values: np.ndarray
283
+
284
+
285
+ @dataclass(slots=True)
286
+ class BundledOSQPCAPIBackend:
287
+ """Direct OSQP C-API verification backend for CasADi's bundled library.
288
+
289
+ Reuse evidence is stronger than the CasADi bridge: the OSQP workspace is
290
+ preserved and the documented update functions are called directly. We do
291
+ *not* claim factorization reuse because matrix-value updates may refactorize.
292
+ """
293
+
294
+ max_iter: int = 10000
295
+ eps_abs: float = 1e-7
296
+ eps_rel: float = 1e-7
297
+ eps_prim_inf: float = 1e-4
298
+ eps_dual_inf: float = 1e-4
299
+ polish: bool = True
300
+ _state: _OSQPState | None = field(default=None, init=False, repr=False)
301
+
302
+ @property
303
+ def manifest(self) -> BackendManifest:
304
+ paths = _bundled_paths()
305
+ metadata: dict[str, object] = {
306
+ "native_adapter": False,
307
+ "verification_only": True,
308
+ "direct_c_api": True,
309
+ "public_python_package": False,
310
+ "bundled_by": "casadi",
311
+ "bridge_package_version": _casadi_version(),
312
+ "verified_bridge_package_versions": (_VERIFIED_CASADI_VERSION,),
313
+ "expected_solver_version": _VERIFIED_OSQP_VERSION,
314
+ "verified_solver_versions": (_VERIFIED_OSQP_VERSION,),
315
+ "supports_wall_time_budget": False,
316
+ "stateful": True,
317
+ "reuse_semantics": "persistent OSQP workspace + direct same-sparsity update API + warm_start=1",
318
+ "factorization_reuse_claimed": False,
319
+ "verified_abi_layout": dict(_OSQP_ABI_EXPECTED),
320
+ }
321
+ solver_version = None
322
+ if paths and paths["osqp_lib"].is_file():
323
+ try:
324
+ lib = _load_osqp_capi(paths)
325
+ solver_version = (lib.osqp_version() or b"").decode("ascii", errors="replace") or None
326
+ metadata.update(
327
+ {
328
+ "library_sha256": _sha256(paths["osqp_lib"]),
329
+ "header_sha256": _sha256(paths["osqp_header"]),
330
+ "types_header_sha256": _sha256(paths["osqp_types"]),
331
+ "configure_header_sha256": _sha256(paths["osqp_config"]),
332
+ }
333
+ )
334
+ except Exception:
335
+ pass
336
+ return BackendManifest(
337
+ name="bundled-osqp-capi",
338
+ version=solver_version,
339
+ capabilities={
340
+ Capability.LP: SupportLevel.UNSUPPORTED,
341
+ Capability.CONVEX_QP: SupportLevel.NATIVE,
342
+ Capability.MILP: SupportLevel.UNSUPPORTED,
343
+ Capability.MIP_START: SupportLevel.UNSUPPORTED,
344
+ Capability.PRIMAL_START: SupportLevel.NATIVE,
345
+ Capability.DUAL_START: SupportLevel.NATIVE,
346
+ Capability.BASIS_START: SupportLevel.UNSUPPORTED,
347
+ Capability.SAME_SPARSITY_DATA_UPDATE: SupportLevel.NATIVE,
348
+ Capability.STRUCTURAL_INCREMENTAL_UPDATE: SupportLevel.UNSUPPORTED,
349
+ Capability.NATIVE_REOPTIMIZATION: SupportLevel.UNSUPPORTED,
350
+ Capability.CALLBACK_PROGRESS: SupportLevel.UNSUPPORTED,
351
+ Capability.IIS: SupportLevel.UNSUPPORTED,
352
+ Capability.INFEASIBILITY_CERTIFICATE: SupportLevel.NATIVE,
353
+ },
354
+ metadata=metadata,
355
+ )
356
+
357
+ def is_available(self) -> bool:
358
+ if _casadi_version() != _VERIFIED_CASADI_VERSION:
359
+ return False
360
+ paths = _bundled_paths()
361
+ if paths is None or not all(paths[k].is_file() for k in ("osqp_lib", "osqp_header", "osqp_types", "osqp_config")):
362
+ return False
363
+ if not _verified_osqp_config(paths):
364
+ return False
365
+ try:
366
+ lib = _load_osqp_capi(paths)
367
+ return (lib.osqp_version() or b"").decode("ascii") == _VERIFIED_OSQP_VERSION
368
+ except Exception:
369
+ return False
370
+
371
+ def close(self) -> None:
372
+ if self._state is None:
373
+ return
374
+ try:
375
+ paths = _bundled_paths()
376
+ if paths is not None:
377
+ _load_osqp_capi(paths).osqp_cleanup(self._state.work)
378
+ finally:
379
+ self._state = None
380
+
381
+ def __del__(self): # pragma: no cover - defensive process-exit cleanup
382
+ try:
383
+ self.close()
384
+ except Exception:
385
+ pass
386
+
387
+ def _setup(self, problem: QuadraticProblem, data) -> tuple[object, float]:
388
+ self.close()
389
+ paths = _bundled_paths()
390
+ if paths is None:
391
+ raise RuntimeError("bundled OSQP C API paths became unavailable during setup")
392
+ lib = _load_osqp_capi(paths)
393
+
394
+ P = sparse.csc_matrix(data.P, dtype=np.float64, copy=True)
395
+ A = sparse.csc_matrix(data.A, dtype=np.float64, copy=True)
396
+ P.sort_indices(); A.sort_indices()
397
+ Pp, Pi, Px = _c_int_array(P.indptr), _c_int_array(P.indices), _c_float_array(P.data)
398
+ Ap, Ai, Ax = _c_int_array(A.indptr), _c_int_array(A.indices), _c_float_array(A.data)
399
+ q, l, u = _c_float_array(data.q), _c_float_array(data.l), _c_float_array(data.u)
400
+ P_csc = _CSC(P.nnz, P.shape[0], P.shape[1], Pp, Pi, Px, -1)
401
+ A_csc = _CSC(A.nnz, A.shape[0], A.shape[1], Ap, Ai, Ax, -1)
402
+ cdata = _OSQPData(problem.n_variables, A.shape[0], ctypes.pointer(P_csc), ctypes.pointer(A_csc), q, l, u)
403
+ settings = _OSQPSettings()
404
+ lib.osqp_set_default_settings(ctypes.byref(settings))
405
+ settings.verbose = 0
406
+ settings.warm_start = 1
407
+ settings.polish = int(bool(self.polish))
408
+ settings.max_iter = int(self.max_iter)
409
+ settings.eps_abs = float(self.eps_abs)
410
+ settings.eps_rel = float(self.eps_rel)
411
+ settings.eps_prim_inf = float(self.eps_prim_inf)
412
+ settings.eps_dual_inf = float(self.eps_dual_inf)
413
+ work = ctypes.POINTER(_OSQPWorkspace)()
414
+ t0 = perf_counter()
415
+ rc = int(lib.osqp_setup(ctypes.byref(work), ctypes.byref(cdata), ctypes.byref(settings)))
416
+ setup_wall = perf_counter() - t0
417
+ if rc != 0 or not bool(work):
418
+ raise RuntimeError(f"OSQP C API setup failed with code {rc}")
419
+ keepalive = (Pp, Pi, Px, Ap, Ai, Ax, q, l, u, P_csc, A_csc, cdata, settings)
420
+ self._state = _OSQPState(
421
+ work=work,
422
+ keepalive=keepalive,
423
+ structural_hash=problem.structural_hash,
424
+ p_values=np.asarray(P.data, dtype=np.float64).copy(),
425
+ a_values=np.asarray(A.data, dtype=np.float64).copy(),
426
+ )
427
+ return lib, setup_wall
428
+
429
+ def solve(self, problem: LinearProblem | QuadraticProblem) -> BackendSolveResult:
430
+ if not self.is_available():
431
+ raise BackendUnavailableError("verified bundled OSQP C API is unavailable")
432
+ if not isinstance(problem, QuadraticProblem):
433
+ raise ValueError("bundled-osqp-capi supports convex continuous QP only")
434
+ if self.max_iter <= 0 or min(self.eps_abs, self.eps_rel, self.eps_prim_inf, self.eps_dual_inf) <= 0:
435
+ raise ValueError("OSQP iteration/tolerance settings must be positive")
436
+
437
+ data = _osqp_data(problem)
438
+ reused = self._state is not None and self._state.structural_hash == problem.structural_hash
439
+ setup_wall = 0.0
440
+ update_wall = 0.0
441
+ matrix_values_updated = False
442
+ if not reused:
443
+ lib, setup_wall = self._setup(problem, data)
444
+ reuse_mode = "cold_capi_setup"
445
+ else:
446
+ paths = _bundled_paths()
447
+ if paths is None:
448
+ raise RuntimeError("bundled OSQP C API paths became unavailable after availability check")
449
+ lib = _load_osqp_capi(paths)
450
+ if self._state is None:
451
+ raise RuntimeError("OSQP persistent state unexpectedly missing during reuse")
452
+ q = _c_float_array(data.q); l = _c_float_array(data.l); u = _c_float_array(data.u)
453
+ t0 = perf_counter()
454
+ rc_q = int(lib.osqp_update_lin_cost(self._state.work, q))
455
+ rc_b = int(lib.osqp_update_bounds(self._state.work, l, u))
456
+ if rc_q != 0 or rc_b != 0:
457
+ raise RuntimeError(f"OSQP C API vector update failed: q={rc_q}, bounds={rc_b}")
458
+ p_new = np.asarray(data.P.data, dtype=np.float64)
459
+ a_new = np.asarray(data.A.data, dtype=np.float64)
460
+ if not np.array_equal(p_new, self._state.p_values) or not np.array_equal(a_new, self._state.a_values):
461
+ Px = _c_float_array(p_new); Ax = _c_float_array(a_new)
462
+ rc_m = int(
463
+ lib.osqp_update_P_A(
464
+ self._state.work,
465
+ Px,
466
+ None,
467
+ len(p_new),
468
+ Ax,
469
+ None,
470
+ len(a_new),
471
+ )
472
+ )
473
+ if rc_m != 0:
474
+ raise RuntimeError(f"OSQP C API matrix-value update failed: {rc_m}")
475
+ self._state.p_values = p_new.copy()
476
+ self._state.a_values = a_new.copy()
477
+ matrix_values_updated = True
478
+ update_wall = perf_counter() - t0
479
+ reuse_mode = (
480
+ "persistent_workspace+same_sparsity_matrix_value_update+automatic_warm_start"
481
+ if matrix_values_updated
482
+ else "persistent_workspace+vector_update+automatic_warm_start"
483
+ )
484
+
485
+ if self._state is None:
486
+ raise RuntimeError("OSQP persistent state unexpectedly missing before solve")
487
+ t0 = perf_counter()
488
+ solve_rc = int(lib.osqp_solve(self._state.work))
489
+ solve_wall = perf_counter() - t0
490
+ if solve_rc != 0:
491
+ raise RuntimeError(f"OSQP C API solve failed with code {solve_rc}")
492
+ ws = self._state.work.contents
493
+ info = ws.info.contents
494
+ status_text = _status_text(info)
495
+ candidate = None
496
+ if bool(ws.solution) and bool(ws.solution.contents.x):
497
+ arr = np.ctypeslib.as_array(ws.solution.contents.x, shape=(problem.n_variables,)).copy()
498
+ if np.all(np.isfinite(arr)):
499
+ candidate = arr
500
+ has_candidate = candidate is not None
501
+ backend_status = _osqp_status(status_text, has_candidate)
502
+
503
+ certificate_kind = None
504
+ certificate_check = None
505
+ status_lower = status_text.strip().lower()
506
+ if status_lower.startswith("primal infeasible"):
507
+ certificate_kind = "primal_infeasibility"
508
+ cert = None
509
+ if bool(ws.delta_y):
510
+ cert = np.ctypeslib.as_array(ws.delta_y, shape=(data.A.shape[0],)).copy()
511
+ multiplier = 10.0 if status_lower.endswith("inaccurate") else 1.0
512
+ certificate_check = _primal_certificate_check(data, cert, eps=self.eps_prim_inf * multiplier)
513
+ if status_lower == "primal infeasible" and not certificate_check.get("valid"):
514
+ backend_status = "infeasible_candidate"
515
+ elif status_lower.startswith("dual infeasible"):
516
+ certificate_kind = "dual_infeasibility"
517
+ cert = None
518
+ if bool(ws.delta_x):
519
+ cert = np.ctypeslib.as_array(ws.delta_x, shape=(problem.n_variables,)).copy()
520
+ multiplier = 10.0 if status_lower.endswith("inaccurate") else 1.0
521
+ certificate_check = _dual_certificate_check(problem, data, cert, eps=self.eps_dual_inf * multiplier)
522
+ if status_lower == "dual infeasible" and not certificate_check.get("valid"):
523
+ backend_status = "unbounded_candidate"
524
+
525
+ x = candidate if has_candidate and backend_status in {"optimal", "converged_candidate", "limit_feasible"} else None
526
+ objective = None
527
+ if x is not None and np.isfinite(float(info.obj_val)):
528
+ objective = float(info.obj_val) + float(problem.linear.objective_offset)
529
+
530
+ raw = {
531
+ "osqp_status": status_text,
532
+ "status_val": int(info.status_val),
533
+ "iter": int(info.iter),
534
+ "objective_internal": float(info.obj_val),
535
+ "pri_res": float(info.pri_res),
536
+ "dua_res": float(info.dua_res),
537
+ "rho_updates": int(info.rho_updates),
538
+ "rho_estimate": float(info.rho_estimate),
539
+ "setup_wall_s": float(setup_wall),
540
+ "update_wall_s": float(update_wall),
541
+ "solve_wall_s": float(solve_wall),
542
+ "reuse_applied": bool(reused),
543
+ "reuse_mode": reuse_mode,
544
+ "matrix_values_updated": bool(matrix_values_updated),
545
+ "certificate_kind": certificate_kind,
546
+ "certificate_check": certificate_check,
547
+ "solver_core_direct_c_api": True,
548
+ "factorization_reuse_claimed": False,
549
+ }
550
+ return BackendSolveResult(backend_status=backend_status, x=x, objective_reported=objective, raw_statistics=raw)
551
+
552
+
553
+ # ---- HiGHS C API ----
554
+ _HInt = ctypes.c_int32
555
+ _HIGHS_STATUS_OK = 0
556
+ _HIGHS_OBJ_MIN = 1
557
+ _HIGHS_OBJ_MAX = -1
558
+ _HIGHS_MATRIX_COLWISE = 1
559
+ _HIGHS_VAR_CONTINUOUS = 0
560
+ _HIGHS_VAR_INTEGER = 1
561
+ _HIGHS_MODEL_OPTIMAL = 7
562
+ _HIGHS_MODEL_INFEASIBLE = 8
563
+ _HIGHS_MODEL_UNBOUNDED_OR_INFEASIBLE = 9
564
+ _HIGHS_MODEL_UNBOUNDED = 10
565
+ _HIGHS_MODEL_OBJECTIVE_BOUND = 11
566
+ _HIGHS_MODEL_OBJECTIVE_TARGET = 12
567
+ _HIGHS_MODEL_TIME_LIMIT = 13
568
+ _HIGHS_MODEL_ITERATION_LIMIT = 14
569
+ _HIGHS_MODEL_SOLUTION_LIMIT = 16
570
+ _HIGHS_MODEL_INTERRUPT = 17
571
+
572
+
573
+ def _load_highs_capi(paths: dict[str, Path]):
574
+ lib = ctypes.CDLL(str(paths["highs_lib"]))
575
+ lib.Highs_version.restype = ctypes.c_char_p
576
+ lib.Highs_create.restype = ctypes.c_void_p
577
+ lib.Highs_destroy.argtypes = [ctypes.c_void_p]
578
+ lib.Highs_resetGlobalScheduler.argtypes = [_HInt]
579
+ lib.Highs_resetGlobalScheduler.restype = None
580
+ lib.Highs_getInfinity.restype = ctypes.c_double
581
+ lib.Highs_setBoolOptionValue.argtypes = [ctypes.c_void_p, ctypes.c_char_p, _HInt]
582
+ lib.Highs_setBoolOptionValue.restype = _HInt
583
+ lib.Highs_setIntOptionValue.argtypes = [ctypes.c_void_p, ctypes.c_char_p, _HInt]
584
+ lib.Highs_setIntOptionValue.restype = _HInt
585
+ lib.Highs_setDoubleOptionValue.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_double]
586
+ lib.Highs_setDoubleOptionValue.restype = _HInt
587
+ lib.Highs_setStringOptionValue.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
588
+ lib.Highs_setStringOptionValue.restype = _HInt
589
+ dbl_p = ctypes.POINTER(ctypes.c_double); int_p = ctypes.POINTER(_HInt)
590
+ lib.Highs_passLp.argtypes = [ctypes.c_void_p, _HInt, _HInt, _HInt, _HInt, _HInt, ctypes.c_double, dbl_p, dbl_p, dbl_p, dbl_p, dbl_p, int_p, int_p, dbl_p]
591
+ lib.Highs_passLp.restype = _HInt
592
+ lib.Highs_passMip.argtypes = [ctypes.c_void_p, _HInt, _HInt, _HInt, _HInt, _HInt, ctypes.c_double, dbl_p, dbl_p, dbl_p, dbl_p, dbl_p, int_p, int_p, dbl_p, int_p]
593
+ lib.Highs_passMip.restype = _HInt
594
+ lib.Highs_run.argtypes = [ctypes.c_void_p]; lib.Highs_run.restype = _HInt
595
+ lib.Highs_zeroAllClocks.argtypes = [ctypes.c_void_p]; lib.Highs_zeroAllClocks.restype = _HInt
596
+ lib.Highs_getRunTime.argtypes = [ctypes.c_void_p]; lib.Highs_getRunTime.restype = ctypes.c_double
597
+ lib.Highs_getModelStatus.argtypes = [ctypes.c_void_p]; lib.Highs_getModelStatus.restype = _HInt
598
+ lib.Highs_getObjectiveValue.argtypes = [ctypes.c_void_p]; lib.Highs_getObjectiveValue.restype = ctypes.c_double
599
+ lib.Highs_getSolution.argtypes = [ctypes.c_void_p, dbl_p, dbl_p, dbl_p, dbl_p]; lib.Highs_getSolution.restype = _HInt
600
+ lib.Highs_getBasis.argtypes = [ctypes.c_void_p, int_p, int_p]; lib.Highs_getBasis.restype = _HInt
601
+ lib.Highs_setBasis.argtypes = [ctypes.c_void_p, int_p, int_p]; lib.Highs_setBasis.restype = _HInt
602
+ lib.Highs_changeObjectiveSense.argtypes = [ctypes.c_void_p, _HInt]; lib.Highs_changeObjectiveSense.restype = _HInt
603
+ lib.Highs_changeObjectiveOffset.argtypes = [ctypes.c_void_p, ctypes.c_double]; lib.Highs_changeObjectiveOffset.restype = _HInt
604
+ lib.Highs_changeColsCostByRange.argtypes = [ctypes.c_void_p, _HInt, _HInt, dbl_p]; lib.Highs_changeColsCostByRange.restype = _HInt
605
+ lib.Highs_changeColsBoundsByRange.argtypes = [ctypes.c_void_p, _HInt, _HInt, dbl_p, dbl_p]; lib.Highs_changeColsBoundsByRange.restype = _HInt
606
+ lib.Highs_changeRowsBoundsBySet.argtypes = [ctypes.c_void_p, _HInt, int_p, dbl_p, dbl_p]; lib.Highs_changeRowsBoundsBySet.restype = _HInt
607
+ lib.Highs_changeCoeff.argtypes = [ctypes.c_void_p, _HInt, _HInt, ctypes.c_double]; lib.Highs_changeCoeff.restype = _HInt
608
+ lib.Highs_getIntInfoValue.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(_HInt)]; lib.Highs_getIntInfoValue.restype = _HInt
609
+ return lib
610
+
611
+
612
+ def _highs_status(model_status: int, has_candidate: bool) -> str:
613
+ if model_status == _HIGHS_MODEL_OPTIMAL:
614
+ return "optimal"
615
+ if model_status == _HIGHS_MODEL_INFEASIBLE:
616
+ return "infeasible"
617
+ if model_status == _HIGHS_MODEL_UNBOUNDED:
618
+ return "unbounded"
619
+ if model_status == _HIGHS_MODEL_UNBOUNDED_OR_INFEASIBLE:
620
+ return "infeasible_or_unbounded"
621
+ if model_status in {_HIGHS_MODEL_TIME_LIMIT, _HIGHS_MODEL_ITERATION_LIMIT, _HIGHS_MODEL_SOLUTION_LIMIT, _HIGHS_MODEL_INTERRUPT, _HIGHS_MODEL_OBJECTIVE_BOUND, _HIGHS_MODEL_OBJECTIVE_TARGET}:
622
+ return "limit_feasible" if has_candidate else "limit_no_solution"
623
+ return "solver_error"
624
+
625
+
626
+ def _h_double(values: np.ndarray):
627
+ arr = np.asarray(values, dtype=np.float64)
628
+ return (ctypes.c_double * len(arr))(*arr.tolist())
629
+
630
+
631
+ def _h_int(values: np.ndarray):
632
+ arr = np.asarray(values, dtype=np.int32)
633
+ return (_HInt * len(arr))(*arr.tolist())
634
+
635
+
636
+ @dataclass(slots=True)
637
+ class _HighsState:
638
+ handle: int
639
+ structural_hash: str
640
+ matrix_csc: sparse.csc_matrix
641
+ effective_solver: str
642
+ basis_col: np.ndarray | None = None
643
+ basis_row: np.ndarray | None = None
644
+
645
+
646
+ @dataclass(slots=True)
647
+ class BundledHighsCAPIBackend:
648
+ """Direct HiGHS C-API verification backend for CasADi's bundled library."""
649
+
650
+ time_limit_s: float | None = None
651
+ threads: int | None = 1
652
+ solver: str = "simplex"
653
+ presolve: bool = True
654
+ enable_basis_hot_start: bool = True
655
+ _state: _HighsState | None = field(default=None, init=False, repr=False)
656
+
657
+ @property
658
+ def manifest(self) -> BackendManifest:
659
+ paths = _bundled_paths()
660
+ solver_version = None
661
+ metadata: dict[str, object] = {
662
+ "native_adapter": False,
663
+ "verification_only": True,
664
+ "direct_c_api": True,
665
+ "public_python_package": False,
666
+ "bundled_by": "casadi",
667
+ "bridge_package_version": _casadi_version(),
668
+ "verified_bridge_package_versions": (_VERIFIED_CASADI_VERSION,),
669
+ "expected_solver_version": _VERIFIED_HIGHS_VERSION,
670
+ "verified_solver_versions": (_VERIFIED_HIGHS_VERSION,),
671
+ "supports_wall_time_budget": True,
672
+ "stateful": True,
673
+ "reuse_semantics": "persistent HiGHS instance + numerical edits + explicit LP basis hot start",
674
+ }
675
+ if paths and paths["highs_lib"].is_file():
676
+ try:
677
+ lib = _load_highs_capi(paths)
678
+ solver_version = (lib.Highs_version() or b"").decode("ascii", errors="replace") or None
679
+ metadata.update({"library_sha256": _sha256(paths["highs_lib"]), "header_sha256": _sha256(paths["highs_header"]), "highs_int_bytes": ctypes.sizeof(_HInt)})
680
+ except Exception:
681
+ pass
682
+ return BackendManifest(
683
+ name="bundled-highs-capi",
684
+ version=solver_version,
685
+ capabilities={
686
+ Capability.LP: SupportLevel.NATIVE,
687
+ Capability.CONVEX_QP: SupportLevel.UNSUPPORTED,
688
+ Capability.MILP: SupportLevel.NATIVE,
689
+ Capability.MIP_START: SupportLevel.UNKNOWN,
690
+ Capability.PRIMAL_START: SupportLevel.UNKNOWN,
691
+ Capability.DUAL_START: SupportLevel.UNKNOWN,
692
+ Capability.BASIS_START: SupportLevel.NATIVE,
693
+ Capability.SAME_SPARSITY_DATA_UPDATE: SupportLevel.NATIVE,
694
+ Capability.STRUCTURAL_INCREMENTAL_UPDATE: SupportLevel.UNSUPPORTED,
695
+ Capability.NATIVE_REOPTIMIZATION: SupportLevel.UNSUPPORTED,
696
+ Capability.CALLBACK_PROGRESS: SupportLevel.UNKNOWN,
697
+ Capability.IIS: SupportLevel.UNKNOWN,
698
+ Capability.INFEASIBILITY_CERTIFICATE: SupportLevel.UNKNOWN,
699
+ },
700
+ metadata=metadata,
701
+ )
702
+
703
+ def is_available(self) -> bool:
704
+ if _casadi_version() != _VERIFIED_CASADI_VERSION:
705
+ return False
706
+ paths = _bundled_paths()
707
+ if paths is None or not paths["highs_lib"].is_file() or not paths["highs_header"].is_file():
708
+ return False
709
+ try:
710
+ lib = _load_highs_capi(paths)
711
+ return ctypes.sizeof(_HInt) == 4 and (lib.Highs_version() or b"").decode("ascii") == _VERIFIED_HIGHS_VERSION
712
+ except Exception:
713
+ return False
714
+
715
+ def close(self) -> None:
716
+ if self._state is None:
717
+ return
718
+ try:
719
+ paths = _bundled_paths()
720
+ if paths is not None:
721
+ _load_highs_capi(paths).Highs_destroy(ctypes.c_void_p(self._state.handle))
722
+ finally:
723
+ self._state = None
724
+
725
+ def reset_global_scheduler(self, *, blocking: bool = True) -> None:
726
+ """Release HiGHS' process-global scheduler after sequential verification.
727
+
728
+ HiGHS documents that scheduler resources survive ``Highs_destroy`` and are
729
+ shared across instances. This method is intentionally separate from
730
+ :meth:`close` because resetting the global scheduler while another HiGHS
731
+ instance is in use is undefined behavior. Health probes call it only after
732
+ the backend instance has been closed and probes are executed sequentially.
733
+ """
734
+ paths = _bundled_paths()
735
+ if paths is None:
736
+ return
737
+ _load_highs_capi(paths).Highs_resetGlobalScheduler(1 if blocking else 0)
738
+
739
+ def __del__(self): # pragma: no cover
740
+ try:
741
+ self.close()
742
+ except Exception:
743
+ pass
744
+
745
+ def _set_options(self, lib, h, *, is_mip: bool) -> str:
746
+ if int(lib.Highs_setBoolOptionValue(h, b"output_flag", 0)) < 0:
747
+ raise RuntimeError("HiGHS failed to disable output")
748
+ if self.threads is not None:
749
+ if self.threads <= 0:
750
+ raise ValueError("threads must be positive")
751
+ if int(lib.Highs_setIntOptionValue(h, b"threads", int(self.threads))) < 0:
752
+ raise RuntimeError("HiGHS failed to set thread count")
753
+ if self.time_limit_s is not None:
754
+ if self.time_limit_s <= 0:
755
+ raise ValueError("time_limit_s must be positive")
756
+ if int(lib.Highs_setDoubleOptionValue(h, b"time_limit", float(self.time_limit_s))) < 0:
757
+ raise RuntimeError("HiGHS failed to set time limit")
758
+ # HiGHS' ``solver`` option selects LP algorithms. For a MIP, forcing
759
+ # ``simplex`` or ``ipm`` solves only the LP relaxation. Preserve integer
760
+ # semantics by always using the MIP-capable automatic path.
761
+ effective_solver = "choose" if is_mip else str(self.solver)
762
+ if int(lib.Highs_setStringOptionValue(h, b"solver", effective_solver.encode())) < 0:
763
+ raise RuntimeError("HiGHS failed to set solver")
764
+ if int(lib.Highs_setStringOptionValue(h, b"presolve", b"on" if self.presolve else b"off")) < 0:
765
+ raise RuntimeError("HiGHS failed to set presolve")
766
+ return effective_solver
767
+
768
+ def _build(self, lib, problem: LinearProblem) -> float:
769
+ self.close()
770
+ # HiGHS uses a process-global scheduler. A previously executed HiGHS
771
+ # binding (for example the CasADi verification bridge) may have created it
772
+ # with a different thread count. HiGHS documents that the scheduler must
773
+ # be reset before changing the thread option. This verification backend
774
+ # is executed sequentially by the benchmark/health harness, so reset it
775
+ # before constructing an independent C-API instance.
776
+ if self.threads is not None:
777
+ lib.Highs_resetGlobalScheduler(1)
778
+ h = lib.Highs_create()
779
+ if not h:
780
+ raise RuntimeError("Highs_create returned null")
781
+ effective_solver = self._set_options(lib, h, is_mip=problem.has_integer_variables)
782
+ A = problem.A.tocsc(copy=True); A.sort_indices()
783
+ cost = _h_double(problem.c); vl = _h_double(problem.variable_lower); vu = _h_double(problem.variable_upper)
784
+ rl = _h_double(problem.constraint_lower); ru = _h_double(problem.constraint_upper)
785
+ starts = _h_int(A.indptr); indices = _h_int(A.indices); values = _h_double(A.data)
786
+ sense = _HIGHS_OBJ_MIN if problem.objective_sense is ObjectiveSense.MINIMIZE else _HIGHS_OBJ_MAX
787
+ t0 = perf_counter()
788
+ if problem.has_integer_variables:
789
+ integrality = _h_int(np.array([_HIGHS_VAR_CONTINUOUS if d == VariableDomain.CONTINUOUS.value else _HIGHS_VAR_INTEGER for d in problem.domains], dtype=np.int32))
790
+ rc = int(lib.Highs_passMip(h, problem.n_variables, problem.n_constraints, problem.nnz, _HIGHS_MATRIX_COLWISE, sense, float(problem.objective_offset), cost, vl, vu, rl, ru, starts, indices, values, integrality))
791
+ else:
792
+ rc = int(lib.Highs_passLp(h, problem.n_variables, problem.n_constraints, problem.nnz, _HIGHS_MATRIX_COLWISE, sense, float(problem.objective_offset), cost, vl, vu, rl, ru, starts, indices, values))
793
+ build_wall = perf_counter() - t0
794
+ if rc != _HIGHS_STATUS_OK:
795
+ lib.Highs_destroy(h)
796
+ raise RuntimeError(f"HiGHS C API model load failed with code {rc}")
797
+ self._state = _HighsState(handle=int(h), structural_hash=problem.structural_hash, matrix_csc=A.copy(), effective_solver=effective_solver)
798
+ return build_wall
799
+
800
+ def _update(self, lib, problem: LinearProblem) -> tuple[float, bool, int]:
801
+ if self._state is None:
802
+ raise RuntimeError("HiGHS persistent state unexpectedly missing during update")
803
+ h = ctypes.c_void_p(self._state.handle)
804
+ t0 = perf_counter()
805
+ n, m = problem.n_variables, problem.n_constraints
806
+ sense = _HIGHS_OBJ_MIN if problem.objective_sense is ObjectiveSense.MINIMIZE else _HIGHS_OBJ_MAX
807
+ calls = [
808
+ int(lib.Highs_changeObjectiveSense(h, sense)),
809
+ int(lib.Highs_changeObjectiveOffset(h, float(problem.objective_offset))),
810
+ ]
811
+ if n:
812
+ calls.append(int(lib.Highs_changeColsCostByRange(h, 0, n - 1, _h_double(problem.c))))
813
+ calls.append(int(lib.Highs_changeColsBoundsByRange(h, 0, n - 1, _h_double(problem.variable_lower), _h_double(problem.variable_upper))))
814
+ if m:
815
+ row_idx = _h_int(np.arange(m, dtype=np.int32))
816
+ calls.append(int(lib.Highs_changeRowsBoundsBySet(h, m, row_idx, _h_double(problem.constraint_lower), _h_double(problem.constraint_upper))))
817
+ A_new = problem.A.tocsc(copy=True); A_new.sort_indices()
818
+ changed = 0
819
+ if A_new.shape != self._state.matrix_csc.shape or not np.array_equal(A_new.indptr, self._state.matrix_csc.indptr) or not np.array_equal(A_new.indices, self._state.matrix_csc.indices):
820
+ raise RuntimeError("same structural hash produced different sparse structure")
821
+ diff = np.flatnonzero(A_new.data != self._state.matrix_csc.data)
822
+ if len(diff):
823
+ # Convert CSC positions to (row,col) only for values that changed.
824
+ for col in range(A_new.shape[1]):
825
+ lo, hi = A_new.indptr[col], A_new.indptr[col + 1]
826
+ for pos in diff[(diff >= lo) & (diff < hi)]:
827
+ rc = int(lib.Highs_changeCoeff(h, int(A_new.indices[pos]), col, float(A_new.data[pos])))
828
+ calls.append(rc); changed += 1
829
+ self._state.matrix_csc = A_new.copy()
830
+ if any(rc < 0 for rc in calls):
831
+ raise RuntimeError(f"HiGHS C API numerical update failed: {calls}")
832
+ basis_applied = False
833
+ if self.enable_basis_hot_start and not problem.has_integer_variables and self._state.basis_col is not None and self._state.basis_row is not None:
834
+ rc = int(lib.Highs_setBasis(h, _h_int(self._state.basis_col), _h_int(self._state.basis_row)))
835
+ if rc == _HIGHS_STATUS_OK:
836
+ basis_applied = True
837
+ return perf_counter() - t0, basis_applied, changed
838
+
839
+ def solve(self, problem: LinearProblem | QuadraticProblem) -> BackendSolveResult:
840
+ if isinstance(problem, QuadraticProblem):
841
+ require_confirmed_convexity(problem)
842
+ if not self.is_available():
843
+ raise BackendUnavailableError("verified bundled HiGHS C API is unavailable")
844
+ if not isinstance(problem, LinearProblem):
845
+ raise ValueError("bundled-highs-capi M13 supports LP/MILP only; QP remains verified through other paths")
846
+ paths = _bundled_paths()
847
+ if paths is None:
848
+ raise RuntimeError("bundled HiGHS C API paths became unavailable after availability check")
849
+ lib = _load_highs_capi(paths)
850
+ reused = self._state is not None and self._state.structural_hash == problem.structural_hash
851
+ build_wall = 0.0; update_wall = 0.0; basis_applied = False; matrix_values_changed = 0
852
+ if not reused:
853
+ build_wall = self._build(lib, problem)
854
+ reuse_mode = "cold_capi_model_build"
855
+ else:
856
+ update_wall, basis_applied, matrix_values_changed = self._update(lib, problem)
857
+ reuse_mode = "persistent_highs_model+numeric_update"
858
+ if basis_applied:
859
+ reuse_mode += "+explicit_basis_hot_start"
860
+ if self._state is None:
861
+ raise RuntimeError("HiGHS persistent state unexpectedly missing before solve")
862
+ h = ctypes.c_void_p(self._state.handle)
863
+ lib.Highs_zeroAllClocks(h)
864
+ t0 = perf_counter(); run_rc = int(lib.Highs_run(h)); solve_wall = perf_counter() - t0
865
+ if run_rc < 0:
866
+ raise RuntimeError(f"HiGHS C API solve failed with code {run_rc}")
867
+ model_status = int(lib.Highs_getModelStatus(h))
868
+ x_arr = (ctypes.c_double * problem.n_variables)()
869
+ col_dual = (ctypes.c_double * problem.n_variables)()
870
+ row_val = (ctypes.c_double * problem.n_constraints)()
871
+ row_dual = (ctypes.c_double * problem.n_constraints)()
872
+ sol_rc = int(lib.Highs_getSolution(h, x_arr, col_dual, row_val, row_dual))
873
+ candidate = None
874
+ if sol_rc == _HIGHS_STATUS_OK:
875
+ arr = np.ctypeslib.as_array(x_arr).copy()
876
+ if arr.shape == (problem.n_variables,) and np.all(np.isfinite(arr)):
877
+ candidate = arr
878
+ backend_status = _highs_status(model_status, candidate is not None)
879
+ x = candidate if candidate is not None and backend_status in {"optimal", "limit_feasible"} else None
880
+ objective = float(problem.c @ x + problem.objective_offset) if x is not None else None
881
+
882
+ simplex_iters = _HInt(0); ipm_iters = _HInt(0)
883
+ lib.Highs_getIntInfoValue(h, b"simplex_iteration_count", ctypes.byref(simplex_iters))
884
+ lib.Highs_getIntInfoValue(h, b"ipm_iteration_count", ctypes.byref(ipm_iters))
885
+ basis_captured = False
886
+ if self.enable_basis_hot_start and not problem.has_integer_variables and backend_status == "optimal":
887
+ col_status = (_HInt * problem.n_variables)(); row_status = (_HInt * problem.n_constraints)()
888
+ if int(lib.Highs_getBasis(h, col_status, row_status)) == _HIGHS_STATUS_OK:
889
+ self._state.basis_col = np.ctypeslib.as_array(col_status).copy().astype(np.int32)
890
+ self._state.basis_row = np.ctypeslib.as_array(row_status).copy().astype(np.int32)
891
+ basis_captured = True
892
+ raw = {
893
+ "model_status": model_status,
894
+ "objective_internal": float(lib.Highs_getObjectiveValue(h)),
895
+ "simplex_iteration_count": int(simplex_iters.value),
896
+ "ipm_iteration_count": int(ipm_iters.value),
897
+ "highs_run_time_s": float(lib.Highs_getRunTime(h)),
898
+ "build_wall_s": float(build_wall),
899
+ "update_wall_s": float(update_wall),
900
+ "solve_wall_s": float(solve_wall),
901
+ "reuse_applied": bool(reused),
902
+ "reuse_mode": reuse_mode,
903
+ "basis_applied": bool(basis_applied),
904
+ "basis_captured": bool(basis_captured),
905
+ "matrix_values_changed": int(matrix_values_changed),
906
+ "solver_core_direct_c_api": True,
907
+ "requested_solver": str(self.solver),
908
+ "effective_solver": self._state.effective_solver,
909
+ }
910
+ return BackendSolveResult(backend_status=backend_status, x=x, objective_reported=objective, raw_statistics=raw)