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,217 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import importlib.util
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ import numpy as np
8
+ from scipy import sparse
9
+
10
+ from solverpilot.capabilities import BackendManifest, Capability, SupportLevel
11
+ from solverpilot.problem import LinearProblem, ObjectiveSense, QuadraticProblem, VariableDomain
12
+ from solverpilot.problem.quadratic import require_confirmed_convexity
13
+
14
+ from .base import BackendSolveResult, BackendUnavailableError
15
+
16
+
17
+ def _highs_status_from_strings(model_status: str, primal_status: str) -> str:
18
+ model = model_status.strip().lower()
19
+ primal_feasible = "feasible" in primal_status.strip().lower() and "infeasible" not in primal_status.strip().lower()
20
+ if "optimal" in model:
21
+ return "optimal"
22
+ if "unbounded or infeasible" in model or "infeasible or unbounded" in model:
23
+ return "infeasible_or_unbounded"
24
+ if "infeasible" in model:
25
+ return "infeasible"
26
+ if "unbounded" in model:
27
+ return "unbounded"
28
+ if any(token in model for token in ("time limit", "iteration limit", "solution limit", "objective bound", "objective target", "interrupt")):
29
+ return "limit_feasible" if primal_feasible else "limit_no_solution"
30
+ if any(token in model for token in ("model error", "solve error", "memory limit")):
31
+ return "solver_error"
32
+ return "unknown"
33
+
34
+
35
+ def _linear_csc(problem: LinearProblem) -> sparse.csc_matrix:
36
+ A = problem.A.tocsc(copy=True)
37
+ A.sort_indices()
38
+ return A
39
+
40
+
41
+ def _lower_hessian_csc(problem: QuadraticProblem) -> sparse.csc_matrix:
42
+ P = sparse.tril(problem.P, format="csc")
43
+ P.sort_indices()
44
+ return P
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class HighspyNativeBackend:
49
+ """Optional native HiGHS backend.
50
+
51
+ This adapter is import-safe in environments without ``highspy``. M2 implements
52
+ native LP/MILP/QP model construction and result normalization. Stateful basis and
53
+ MIP-start reuse are intentionally not claimed until they can be integration-tested.
54
+ """
55
+
56
+ time_limit_s: float | None = None
57
+ threads: int | None = None
58
+ presolve: bool | None = None
59
+ solver: str | None = None
60
+
61
+ @property
62
+ def manifest(self) -> BackendManifest:
63
+ pkg_version = None
64
+ if self.is_available():
65
+ try:
66
+ pkg_version = version("highspy")
67
+ except PackageNotFoundError:
68
+ pass
69
+ return BackendManifest(
70
+ name="highspy-native",
71
+ version=pkg_version,
72
+ capabilities={
73
+ Capability.LP: SupportLevel.NATIVE,
74
+ Capability.CONVEX_QP: SupportLevel.NATIVE,
75
+ Capability.MILP: SupportLevel.NATIVE,
76
+ # Upstream supports these mechanisms, but M2 adapter does not yet apply them.
77
+ Capability.MIP_START: SupportLevel.UNKNOWN,
78
+ Capability.PRIMAL_START: SupportLevel.UNKNOWN,
79
+ Capability.DUAL_START: SupportLevel.UNKNOWN,
80
+ Capability.BASIS_START: SupportLevel.UNKNOWN,
81
+ Capability.SAME_SPARSITY_DATA_UPDATE: SupportLevel.UNKNOWN,
82
+ Capability.STRUCTURAL_INCREMENTAL_UPDATE: SupportLevel.UNKNOWN,
83
+ Capability.NATIVE_REOPTIMIZATION: SupportLevel.UNSUPPORTED,
84
+ Capability.CALLBACK_PROGRESS: SupportLevel.UNKNOWN,
85
+ Capability.IIS: SupportLevel.UNKNOWN,
86
+ Capability.INFEASIBILITY_CERTIFICATE: SupportLevel.UNKNOWN,
87
+ },
88
+ metadata={
89
+ "native_adapter": True,
90
+ "stateful": False,
91
+ "reuse_claim": "none in M2 adapter",
92
+ },
93
+ )
94
+
95
+ def is_available(self) -> bool:
96
+ if importlib.util.find_spec("highspy") is None:
97
+ return False
98
+ try:
99
+ # Finding package metadata is insufficient: the extension may not
100
+ # load, or another HiGHS ABI may already occupy its library name.
101
+ # Resolve the dedicated native adapter before verification bridges
102
+ # are examined by the default registry.
103
+ importlib.import_module("highspy")
104
+ except (ImportError, OSError):
105
+ return False
106
+ return True
107
+
108
+ def solve(self, problem: LinearProblem | QuadraticProblem) -> BackendSolveResult:
109
+ if not self.is_available():
110
+ raise BackendUnavailableError("highspy is not installed")
111
+ import highspy
112
+
113
+ h = highspy.Highs()
114
+ h.setOptionValue("output_flag", False)
115
+ if self.time_limit_s is not None:
116
+ if self.time_limit_s <= 0:
117
+ raise ValueError("time_limit_s must be positive")
118
+ h.setOptionValue("time_limit", float(self.time_limit_s))
119
+ if self.threads is not None:
120
+ if self.threads <= 0:
121
+ raise ValueError("threads must be positive")
122
+ h.setOptionValue("threads", int(self.threads))
123
+ if self.presolve is not None:
124
+ h.setOptionValue("presolve", "on" if self.presolve else "off")
125
+ if self.solver is not None:
126
+ h.setOptionValue("solver", str(self.solver))
127
+
128
+ if isinstance(problem, QuadraticProblem):
129
+ require_confirmed_convexity(problem)
130
+ model = self._make_qp_model(highspy, problem)
131
+ elif isinstance(problem, LinearProblem):
132
+ model = self._make_lp_model(highspy, problem)
133
+ else:
134
+ raise TypeError(f"unsupported problem type: {type(problem)!r}")
135
+
136
+ pass_status = h.passModel(model)
137
+ if "error" in str(pass_status).lower():
138
+ raise RuntimeError(f"HiGHS rejected model: {pass_status}")
139
+ h.run()
140
+ info = h.getInfo()
141
+ model_status = h.modelStatusToString(h.getModelStatus())
142
+ primal_status = h.solutionStatusToString(info.primal_solution_status)
143
+ backend_status = _highs_status_from_strings(model_status, primal_status)
144
+
145
+ x = None
146
+ if backend_status in {"optimal", "limit_feasible"}:
147
+ solution = h.getSolution()
148
+ x_candidate = np.asarray(list(solution.col_value), dtype=np.float64)
149
+ if x_candidate.shape == (problem.n_variables,) and np.all(np.isfinite(x_candidate)):
150
+ x = x_candidate
151
+
152
+ objective = None
153
+ objective_internal = None
154
+ if x is not None:
155
+ try:
156
+ candidate = float(h.getObjectiveValue())
157
+ if np.isfinite(candidate):
158
+ objective_internal = candidate
159
+ objective = candidate
160
+ except Exception:
161
+ objective_internal = None
162
+
163
+ raw = {
164
+ "model_status": str(model_status),
165
+ "primal_solution_status": str(primal_status),
166
+ "simplex_iteration_count": int(getattr(info, "simplex_iteration_count", 0)),
167
+ "ipm_iteration_count": int(getattr(info, "ipm_iteration_count", 0)),
168
+ "qp_iteration_count": int(getattr(info, "qp_iteration_count", 0)),
169
+ "mip_node_count": int(getattr(info, "mip_node_count", 0)),
170
+ "reuse_applied": False,
171
+ "reuse_mode": "cold_native_solve",
172
+ "objective_internal": objective_internal,
173
+ }
174
+ return BackendSolveResult(
175
+ backend_status=backend_status,
176
+ x=x,
177
+ objective_reported=objective,
178
+ raw_statistics=raw,
179
+ )
180
+
181
+ @staticmethod
182
+ def _make_lp_model(highspy, problem: LinearProblem):
183
+ lp = highspy.HighsLp()
184
+ lp.num_col_ = problem.n_variables
185
+ lp.num_row_ = problem.n_constraints
186
+ lp.col_cost_ = np.asarray(problem.c, dtype=np.float64)
187
+ lp.col_lower_ = np.asarray(problem.variable_lower, dtype=np.float64)
188
+ lp.col_upper_ = np.asarray(problem.variable_upper, dtype=np.float64)
189
+ lp.row_lower_ = np.asarray(problem.constraint_lower, dtype=np.float64)
190
+ lp.row_upper_ = np.asarray(problem.constraint_upper, dtype=np.float64)
191
+ lp.sense_ = highspy.ObjSense.kMinimize if problem.objective_sense is ObjectiveSense.MINIMIZE else highspy.ObjSense.kMaximize
192
+ lp.offset_ = float(problem.objective_offset)
193
+ A = _linear_csc(problem)
194
+ lp.a_matrix_.format_ = highspy.MatrixFormat.kColwise
195
+ lp.a_matrix_.start_ = np.asarray(A.indptr, dtype=np.int64)
196
+ lp.a_matrix_.index_ = np.asarray(A.indices, dtype=np.int32)
197
+ lp.a_matrix_.value_ = np.asarray(A.data, dtype=np.float64)
198
+ if problem.has_integer_variables:
199
+ lp.integrality_ = [
200
+ highspy.HighsVarType.kInteger
201
+ if domain in {VariableDomain.INTEGER.value, VariableDomain.BINARY.value}
202
+ else highspy.HighsVarType.kContinuous
203
+ for domain in problem.domains
204
+ ]
205
+ return lp
206
+
207
+ @classmethod
208
+ def _make_qp_model(cls, highspy, problem: QuadraticProblem):
209
+ model = highspy.HighsModel()
210
+ model.lp_ = cls._make_lp_model(highspy, problem.linear)
211
+ P = _lower_hessian_csc(problem)
212
+ model.hessian_.dim_ = problem.n_variables
213
+ model.hessian_.format_ = highspy.HessianFormat.kTriangular
214
+ model.hessian_.start_ = np.asarray(P.indptr, dtype=np.int64)
215
+ model.hessian_.index_ = np.asarray(P.indices, dtype=np.int32)
216
+ model.hessian_.value_ = np.asarray(P.data, dtype=np.float64)
217
+ return model
@@ -0,0 +1,237 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import importlib.util
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ import numpy as np
8
+
9
+ from solverpilot.capabilities import BackendManifest, Capability, SupportLevel
10
+ from solverpilot.problem import LinearProblem, ObjectiveSense, QuadraticProblem
11
+ from solverpilot.problem.quadratic import require_confirmed_convexity
12
+
13
+ from .base import BackendSolveResult, BackendUnavailableError
14
+
15
+
16
+ def _initial_point(lower: np.ndarray, upper: np.ndarray) -> np.ndarray:
17
+ x = np.zeros_like(lower, dtype=np.float64)
18
+ both = np.isfinite(lower) & np.isfinite(upper)
19
+ x[both] = 0.5 * (lower[both] + upper[both])
20
+ only_lower = np.isfinite(lower) & ~np.isfinite(upper)
21
+ x[only_lower] = np.maximum(0.0, lower[only_lower])
22
+ only_upper = ~np.isfinite(lower) & np.isfinite(upper)
23
+ x[only_upper] = np.minimum(0.0, upper[only_upper])
24
+ return np.minimum(np.maximum(x, lower), upper)
25
+
26
+
27
+ def _nlopt_status(code: int, *, has_candidate: bool) -> str:
28
+ # NLopt positive result codes indicate a successful stop criterion, but not a
29
+ # mathematical optimality certificate. Preserve that distinction.
30
+ if code in {1, 2, 3, 4}:
31
+ return "converged_candidate" if has_candidate else "solver_error"
32
+ if code in {5, 6}: # max evaluations / max time
33
+ return "limit_feasible" if has_candidate else "limit_no_solution"
34
+ if code in {-4, -5}: # roundoff-limited / forced stop
35
+ return "limit_feasible" if has_candidate else "limit_no_solution"
36
+ return "solver_error"
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class NLoptNativeBackend:
41
+ """Optional cross-library continuous optimizer backend using NLopt SLSQP.
42
+
43
+ This adapter is deliberately conservative: it accepts continuous LP and convex
44
+ QP instances but never claims a proof of optimality. A positive NLopt stop is
45
+ normalized as a candidate and SolverPilot's independent validator remains the
46
+ authority on feasibility.
47
+ """
48
+
49
+ time_limit_s: float | None = None
50
+ max_evals: int = 2000
51
+ ftol_rel: float = 1e-10
52
+ xtol_rel: float = 1e-10
53
+ constraint_tol: float = 1e-8
54
+
55
+ def __post_init__(self) -> None:
56
+ if self.time_limit_s is not None and self.time_limit_s <= 0:
57
+ raise ValueError("time_limit_s must be positive")
58
+ if self.max_evals <= 0:
59
+ raise ValueError("max_evals must be positive")
60
+ if self.ftol_rel <= 0 or self.xtol_rel <= 0 or self.constraint_tol < 0:
61
+ raise ValueError("NLopt tolerances must be positive (constraint_tol may be zero)")
62
+
63
+ @property
64
+ def manifest(self) -> BackendManifest:
65
+ pkg_version = None
66
+ if self.is_available():
67
+ try:
68
+ pkg_version = version("nlopt")
69
+ except PackageNotFoundError:
70
+ try:
71
+ import nlopt
72
+ pkg_version = getattr(nlopt, "__version__", None)
73
+ except Exception:
74
+ pass
75
+ return BackendManifest(
76
+ name="nlopt-slsqp-native",
77
+ version=pkg_version,
78
+ capabilities={
79
+ Capability.LP: SupportLevel.EMULATED_SAFE,
80
+ Capability.CONVEX_QP: SupportLevel.EMULATED_SAFE,
81
+ Capability.MILP: SupportLevel.UNSUPPORTED,
82
+ Capability.PRIMAL_START: SupportLevel.UNSUPPORTED,
83
+ Capability.DUAL_START: SupportLevel.UNSUPPORTED,
84
+ Capability.BASIS_START: SupportLevel.UNSUPPORTED,
85
+ Capability.SAME_SPARSITY_DATA_UPDATE: SupportLevel.UNSUPPORTED,
86
+ Capability.STRUCTURAL_INCREMENTAL_UPDATE: SupportLevel.UNSUPPORTED,
87
+ Capability.NATIVE_REOPTIMIZATION: SupportLevel.UNSUPPORTED,
88
+ Capability.CALLBACK_PROGRESS: SupportLevel.UNSUPPORTED,
89
+ Capability.IIS: SupportLevel.UNSUPPORTED,
90
+ Capability.INFEASIBILITY_CERTIFICATE: SupportLevel.UNSUPPORTED,
91
+ },
92
+ metadata={
93
+ "native_adapter": True,
94
+ "underlying_solver": "NLopt LD_SLSQP",
95
+ "continuous_only": True,
96
+ "no_optimality_certificate": True,
97
+ "cross_library_backend": True,
98
+ },
99
+ )
100
+
101
+ def is_available(self) -> bool:
102
+ return importlib.util.find_spec("nlopt") is not None
103
+
104
+ def solve(self, problem: LinearProblem | QuadraticProblem) -> BackendSolveResult:
105
+ if not self.is_available():
106
+ raise BackendUnavailableError("nlopt is not installed")
107
+ import nlopt
108
+
109
+ if isinstance(problem, QuadraticProblem):
110
+ require_confirmed_convexity(problem)
111
+ linear = problem.linear
112
+ P = ((problem.P + problem.P.T) * 0.5).tocsr()
113
+ elif isinstance(problem, LinearProblem):
114
+ if problem.has_integer_variables:
115
+ raise ValueError("NLopt backend supports continuous LP only")
116
+ linear = problem
117
+ P = None
118
+ else:
119
+ raise TypeError(f"unsupported problem type: {type(problem)!r}")
120
+
121
+ n = linear.n_variables
122
+ opt = nlopt.opt(nlopt.LD_SLSQP, n)
123
+ opt.set_lower_bounds(np.asarray(linear.variable_lower, dtype=np.float64))
124
+ opt.set_upper_bounds(np.asarray(linear.variable_upper, dtype=np.float64))
125
+ opt.set_maxeval(int(self.max_evals))
126
+ opt.set_ftol_rel(float(self.ftol_rel))
127
+ opt.set_xtol_rel(float(self.xtol_rel))
128
+ if self.time_limit_s is not None:
129
+ opt.set_maxtime(float(self.time_limit_s))
130
+
131
+ if P is None:
132
+ c = np.asarray(linear.c, dtype=np.float64)
133
+
134
+ def objective(x, grad):
135
+ if grad.size:
136
+ grad[:] = c
137
+ return float(c @ x + linear.objective_offset)
138
+ else:
139
+ q = np.asarray(linear.c, dtype=np.float64)
140
+
141
+ def objective(x, grad):
142
+ px = np.asarray(P @ x, dtype=np.float64)
143
+ if grad.size:
144
+ grad[:] = px + q
145
+ return float(0.5 * x @ px + q @ x + linear.objective_offset)
146
+
147
+ if linear.objective_sense is ObjectiveSense.MAXIMIZE:
148
+ if P is not None:
149
+ raise ValueError("convex QuadraticProblem only supports minimization")
150
+ opt.set_max_objective(objective)
151
+ else:
152
+ opt.set_min_objective(objective)
153
+
154
+ A = linear.A.tocsr()
155
+ tol = float(self.constraint_tol)
156
+ callbacks = [] # keep closures alive for the lifetime of optimize()
157
+ for i in range(linear.n_constraints):
158
+ start, end = A.indptr[i], A.indptr[i + 1]
159
+ indices = np.asarray(A.indices[start:end], dtype=np.int64).copy()
160
+ values = np.asarray(A.data[start:end], dtype=np.float64).copy()
161
+ lo = float(linear.constraint_lower[i])
162
+ hi = float(linear.constraint_upper[i])
163
+
164
+ def row_value(x, indices=indices, values=values):
165
+ return float(values @ x[indices]) if values.size else 0.0
166
+
167
+ if np.isfinite(lo) and np.isfinite(hi) and np.isclose(lo, hi, rtol=0.0, atol=0.0):
168
+ rhs = hi
169
+
170
+ def eq(x, grad, indices=indices, values=values, rhs=rhs):
171
+ if grad.size:
172
+ grad[:] = 0.0
173
+ grad[indices] = values
174
+ return float((values @ x[indices] if values.size else 0.0) - rhs)
175
+
176
+ callbacks.append(eq)
177
+ opt.add_equality_constraint(eq, tol)
178
+ continue
179
+
180
+ if np.isfinite(hi):
181
+ rhs = hi
182
+
183
+ def upper(x, grad, indices=indices, values=values, rhs=rhs):
184
+ if grad.size:
185
+ grad[:] = 0.0
186
+ grad[indices] = values
187
+ return float((values @ x[indices] if values.size else 0.0) - rhs)
188
+
189
+ callbacks.append(upper)
190
+ opt.add_inequality_constraint(upper, tol)
191
+
192
+ if np.isfinite(lo):
193
+ rhs = lo
194
+
195
+ def lower(x, grad, indices=indices, values=values, rhs=rhs):
196
+ if grad.size:
197
+ grad[:] = 0.0
198
+ grad[indices] = -values
199
+ return float(rhs - (values @ x[indices] if values.size else 0.0))
200
+
201
+ callbacks.append(lower)
202
+ opt.add_inequality_constraint(lower, tol)
203
+
204
+ x0 = _initial_point(linear.variable_lower, linear.variable_upper)
205
+ x = None
206
+ objective_reported = None
207
+ code = None
208
+ error = None
209
+ try:
210
+ candidate = np.asarray(opt.optimize(x0), dtype=np.float64)
211
+ code = int(opt.last_optimize_result())
212
+ if candidate.shape == (n,) and np.all(np.isfinite(candidate)):
213
+ x = candidate
214
+ objective_reported = float(opt.last_optimum_value())
215
+ except Exception as exc:
216
+ # NLopt exceptions may still leave a meaningful candidate unavailable to Python;
217
+ # do not manufacture one. Preserve the exception in raw statistics.
218
+ try:
219
+ code = int(opt.last_optimize_result())
220
+ except Exception:
221
+ code = None
222
+ error = f"{type(exc).__name__}: {exc}"
223
+
224
+ backend_status = _nlopt_status(-1 if code is None else code, has_candidate=x is not None)
225
+ return BackendSolveResult(
226
+ backend_status=backend_status,
227
+ x=x,
228
+ objective_reported=objective_reported,
229
+ raw_statistics={
230
+ "nlopt_result_code": code,
231
+ "algorithm": "LD_SLSQP",
232
+ "error": error,
233
+ "reuse_applied": False,
234
+ "reuse_mode": "cold_native_solve",
235
+ "no_optimality_certificate": True,
236
+ },
237
+ )