solverpilot 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solverpilot/__init__.py +250 -0
- solverpilot/_immutability.py +27 -0
- solverpilot/_version.py +3 -0
- solverpilot/applications/__init__.py +3 -0
- solverpilot/applications/routing/__init__.py +19 -0
- solverpilot/applications/routing/compile_milp.py +321 -0
- solverpilot/applications/routing/diagnostics.py +78 -0
- solverpilot/applications/routing/errors.py +14 -0
- solverpilot/applications/routing/heuristics.py +167 -0
- solverpilot/applications/routing/model.py +259 -0
- solverpilot/applications/routing/reference.py +81 -0
- solverpilot/applications/routing/solution.py +89 -0
- solverpilot/applications/routing/timeline.py +79 -0
- solverpilot/applications/routing/validation.py +169 -0
- solverpilot/applications/tsp/__init__.py +41 -0
- solverpilot/applications/tsp/compile_cp.py +121 -0
- solverpilot/applications/tsp/compile_milp.py +139 -0
- solverpilot/applications/tsp/distance.py +60 -0
- solverpilot/applications/tsp/errors.py +17 -0
- solverpilot/applications/tsp/heuristics.py +112 -0
- solverpilot/applications/tsp/model.py +175 -0
- solverpilot/applications/tsp/reference.py +121 -0
- solverpilot/applications/tsp/solution.py +65 -0
- solverpilot/applications/tsp/validation.py +92 -0
- solverpilot/backends/__init__.py +51 -0
- solverpilot/backends/_casadi_highs_worker.py +92 -0
- solverpilot/backends/base.py +28 -0
- solverpilot/backends/bundled_capi.py +910 -0
- solverpilot/backends/casadi_conic.py +303 -0
- solverpilot/backends/health.py +240 -0
- solverpilot/backends/highspy_native.py +217 -0
- solverpilot/backends/nlopt_native.py +237 -0
- solverpilot/backends/osqp_native.py +297 -0
- solverpilot/backends/protocol_v2.py +119 -0
- solverpilot/backends/pyscipopt_native.py +200 -0
- solverpilot/backends/registry.py +38 -0
- solverpilot/backends/scipy_highs.py +138 -0
- solverpilot/backends/scipy_highs_lp.py +195 -0
- solverpilot/backends/scipy_slsqp_qp.py +133 -0
- solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
- solverpilot/benchmark/__init__.py +23 -0
- solverpilot/benchmark/acquire.py +199 -0
- solverpilot/benchmark/baselines.py +70 -0
- solverpilot/benchmark/bundle.py +37 -0
- solverpilot/benchmark/campaign.py +150 -0
- solverpilot/benchmark/environment.py +115 -0
- solverpilot/benchmark/integrity.py +46 -0
- solverpilot/benchmark/model.py +146 -0
- solverpilot/benchmark/policy.py +182 -0
- solverpilot/benchmark/registry.py +38 -0
- solverpilot/benchmark/report.py +44 -0
- solverpilot/benchmark/runner.py +516 -0
- solverpilot/benchmark/slurm.py +55 -0
- solverpilot/benchmark/specs.py +75 -0
- solverpilot/benchmark/splits.py +181 -0
- solverpilot/benchmark/summary.py +384 -0
- solverpilot/benchmark/verify.py +142 -0
- solverpilot/benchmark/worker.py +71 -0
- solverpilot/bridges/__init__.py +19 -0
- solverpilot/bridges/engine.py +44 -0
- solverpilot/bridges/indicator.py +232 -0
- solverpilot/bridges/registry.py +78 -0
- solverpilot/bridges/transformation-tape.schema.json +53 -0
- solverpilot/bridges/types.py +149 -0
- solverpilot/bridges/validation.py +110 -0
- solverpilot/capabilities/__init__.py +32 -0
- solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
- solverpilot/capabilities/conformance.py +151 -0
- solverpilot/capabilities/enums.py +27 -0
- solverpilot/capabilities/manifest.py +19 -0
- solverpilot/capabilities/ontology.py +73 -0
- solverpilot/capabilities/requirements.py +37 -0
- solverpilot/capabilities/resolve.py +30 -0
- solverpilot/capabilities/v2.py +711 -0
- solverpilot/cli/__init__.py +1 -0
- solverpilot/cli/backend_health.py +77 -0
- solverpilot/cli/benchmark.py +331 -0
- solverpilot/cli/benchmark_worker.py +33 -0
- solverpilot/cli/capabilities.py +65 -0
- solverpilot/conic/__init__.py +11 -0
- solverpilot/conic/backend.py +263 -0
- solverpilot/conic/compiler.py +310 -0
- solverpilot/conic/conformance.py +60 -0
- solverpilot/conic/conic-ir-v1.schema.json +24 -0
- solverpilot/conic/ir.py +299 -0
- solverpilot/conic/validation.py +149 -0
- solverpilot/cp/__init__.py +7 -0
- solverpilot/cp/conformance.py +32 -0
- solverpilot/cp/cp-ir-v1.schema.json +13 -0
- solverpilot/cp/ir.py +170 -0
- solverpilot/cp/model.py +143 -0
- solverpilot/cp/ortools_backend.py +423 -0
- solverpilot/cp/ortools_worker.py +75 -0
- solverpilot/cp/reference.py +51 -0
- solverpilot/cp/serialization.py +117 -0
- solverpilot/cp/validate.py +83 -0
- solverpilot/diagnose/__init__.py +33 -0
- solverpilot/diagnose/infeasibility.py +463 -0
- solverpilot/diagnose/model.py +87 -0
- solverpilot/evaluation/__init__.py +53 -0
- solverpilot/evaluation/metrics.py +150 -0
- solverpilot/evaluation/oracle.py +288 -0
- solverpilot/evaluation/proof.py +53 -0
- solverpilot/evaluation/references.py +73 -0
- solverpilot/evaluation/regret.py +281 -0
- solverpilot/exceptions.py +34 -0
- solverpilot/experimental/__init__.py +32 -0
- solverpilot/extensions/__init__.py +56 -0
- solverpilot/extensions/entrypoints.py +34 -0
- solverpilot/extensions/errors.py +35 -0
- solverpilot/extensions/manager.py +651 -0
- solverpilot/extensions/manifest.py +232 -0
- solverpilot/extensions/model.py +101 -0
- solverpilot/extensions/versioning.py +136 -0
- solverpilot/history/__init__.py +22 -0
- solverpilot/history/errors.py +15 -0
- solverpilot/history/model.py +129 -0
- solverpilot/history/sqlite.py +651 -0
- solverpilot/inspect/__init__.py +3 -0
- solverpilot/inspect/fingerprint.py +199 -0
- solverpilot/intelligence/__init__.py +36 -0
- solverpilot/intelligence/errors.py +16 -0
- solverpilot/intelligence/fingerprint.py +108 -0
- solverpilot/intelligence/leakage.py +69 -0
- solverpilot/intelligence/schema.py +314 -0
- solverpilot/intelligence/shift.py +208 -0
- solverpilot/io/__init__.py +41 -0
- solverpilot/io/binary64.py +49 -0
- solverpilot/io/csv.py +230 -0
- solverpilot/io/errors.py +25 -0
- solverpilot/io/hashing.py +111 -0
- solverpilot/io/json.py +90 -0
- solverpilot/io/mapping.py +195 -0
- solverpilot/io/model.py +407 -0
- solverpilot/io/numeric.py +108 -0
- solverpilot/io/provenance.py +114 -0
- solverpilot/io/result.py +45 -0
- solverpilot/io/source.py +104 -0
- solverpilot/io/strict_json.py +133 -0
- solverpilot/minlp/__init__.py +6 -0
- solverpilot/minlp/compiler.py +66 -0
- solverpilot/minlp/conformance.py +34 -0
- solverpilot/minlp/convexity.py +85 -0
- solverpilot/minlp/ir.py +35 -0
- solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
- solverpilot/minlp/orchestrator.py +220 -0
- solverpilot/minlp/validation.py +28 -0
- solverpilot/model/__init__.py +27 -0
- solverpilot/model/compiler.py +1263 -0
- solverpilot/model/errors.py +22 -0
- solverpilot/model/expression.py +373 -0
- solverpilot/model/model.py +716 -0
- solverpilot/model/sets.py +60 -0
- solverpilot/model/types.py +36 -0
- solverpilot/nlp/__init__.py +7 -0
- solverpilot/nlp/ad.py +166 -0
- solverpilot/nlp/backend.py +113 -0
- solverpilot/nlp/compiler.py +116 -0
- solverpilot/nlp/conformance.py +58 -0
- solverpilot/nlp/ir.py +70 -0
- solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
- solverpilot/nlp/validation.py +60 -0
- solverpilot/plan/__init__.py +33 -0
- solverpilot/plan/model.py +79 -0
- solverpilot/plan/planner.py +201 -0
- solverpilot/plan/production.py +627 -0
- solverpilot/plan/selective_lp.py +65 -0
- solverpilot/problem/__init__.py +16 -0
- solverpilot/problem/enums.py +20 -0
- solverpilot/problem/hashing.py +88 -0
- solverpilot/problem/linear.py +196 -0
- solverpilot/problem/mps.py +447 -0
- solverpilot/problem/quadratic.py +192 -0
- solverpilot/reporting/__init__.py +27 -0
- solverpilot/reporting/explain.py +327 -0
- solverpilot/reporting/model.py +264 -0
- solverpilot/reporting/render.py +55 -0
- solverpilot/runtime/__init__.py +19 -0
- solverpilot/runtime/auto.py +193 -0
- solverpilot/runtime/budgeting.py +43 -0
- solverpilot/runtime/executor.py +126 -0
- solverpilot/runtime/portfolio.py +197 -0
- solverpilot/runtime/result.py +72 -0
- solverpilot/session/__init__.py +29 -0
- solverpilot/session/conformance_v2.py +239 -0
- solverpilot/session/mutations.py +115 -0
- solverpilot/session/persistent-session-trace.schema.json +45 -0
- solverpilot/session/persistent.py +491 -0
- solverpilot/session/reuse.py +81 -0
- solverpilot/session/session.py +236 -0
- solverpilot/trace/__init__.py +3 -0
- solverpilot/trace/schema.py +47 -0
- solverpilot/validate/__init__.py +10 -0
- solverpilot/validate/core.py +179 -0
- solverpilot/validate/result.py +55 -0
- solverpilot-0.1.dist-info/METADATA +554 -0
- solverpilot-0.1.dist-info/RECORD +201 -0
- solverpilot-0.1.dist-info/WHEEL +5 -0
- solverpilot-0.1.dist-info/entry_points.txt +4 -0
- solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
- solverpilot-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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)
|