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,138 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import importlib.util
5
+
6
+ import numpy as np
7
+
8
+ from solverpilot.capabilities import BackendManifest, Capability, SupportLevel
9
+ from solverpilot.problem import LinearProblem, ObjectiveSense, QuadraticProblem, VariableDomain
10
+
11
+ from .base import BackendSolveResult, BackendUnavailableError
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class ScipyHighsBackend:
16
+ """Development bridge to the HiGHS implementation bundled by SciPy.
17
+
18
+ This is intentionally not the final native highspy adapter. It exists to exercise
19
+ the backend protocol, status normalization, and end-to-end validation in environments
20
+ where highspy itself is unavailable.
21
+ """
22
+
23
+ time_limit_s: float | None = None
24
+ mip_rel_gap: float | None = None
25
+ presolve: bool | None = None
26
+
27
+ @property
28
+ def manifest(self) -> BackendManifest:
29
+ version = None
30
+ if self.is_available():
31
+ import scipy
32
+ version = scipy.__version__
33
+ return BackendManifest(
34
+ name="scipy-highs-bridge",
35
+ version=version,
36
+ capabilities={
37
+ Capability.LP: SupportLevel.NATIVE,
38
+ Capability.MILP: SupportLevel.NATIVE,
39
+ Capability.CONVEX_QP: SupportLevel.UNSUPPORTED,
40
+ Capability.MIP_START: SupportLevel.UNSUPPORTED,
41
+ Capability.BASIS_START: SupportLevel.UNSUPPORTED,
42
+ Capability.SAME_SPARSITY_DATA_UPDATE: SupportLevel.UNSUPPORTED,
43
+ Capability.STRUCTURAL_INCREMENTAL_UPDATE: SupportLevel.UNSUPPORTED,
44
+ Capability.NATIVE_REOPTIMIZATION: SupportLevel.UNSUPPORTED,
45
+ Capability.CALLBACK_PROGRESS: SupportLevel.UNSUPPORTED,
46
+ Capability.IIS: SupportLevel.UNSUPPORTED,
47
+ },
48
+ metadata={
49
+ "development_bridge": True,
50
+ "underlying_solver": "HiGHS via scipy.optimize.milp",
51
+ },
52
+ )
53
+
54
+ def is_available(self) -> bool:
55
+ return importlib.util.find_spec("scipy.optimize") is not None
56
+
57
+ def solve(self, problem: LinearProblem | QuadraticProblem) -> BackendSolveResult:
58
+ if not self.is_available():
59
+ raise BackendUnavailableError("SciPy optimize is not available")
60
+ if isinstance(problem, QuadraticProblem):
61
+ raise ValueError("scipy-highs-bridge does not support QuadraticProblem")
62
+ if not isinstance(problem, LinearProblem):
63
+ raise TypeError(f"unsupported problem type: {type(problem)!r}")
64
+
65
+ from scipy.optimize import Bounds, LinearConstraint, milp
66
+
67
+ c = np.array(problem.c, copy=True)
68
+ if problem.objective_sense is ObjectiveSense.MAXIMIZE:
69
+ c = -c
70
+
71
+ integrality = np.zeros(problem.n_variables, dtype=np.int32)
72
+ integer_mask = np.isin(
73
+ problem.domains,
74
+ [VariableDomain.INTEGER.value, VariableDomain.BINARY.value],
75
+ )
76
+ integrality[integer_mask] = 1
77
+
78
+ options: dict[str, object] = {"disp": False}
79
+ if self.time_limit_s is not None:
80
+ if self.time_limit_s <= 0:
81
+ raise ValueError("time_limit_s must be positive")
82
+ options["time_limit"] = float(self.time_limit_s)
83
+ if self.mip_rel_gap is not None:
84
+ if self.mip_rel_gap < 0:
85
+ raise ValueError("mip_rel_gap must be non-negative")
86
+ options["mip_rel_gap"] = float(self.mip_rel_gap)
87
+ if self.presolve is not None:
88
+ options["presolve"] = bool(self.presolve)
89
+
90
+ result = milp(
91
+ c=c,
92
+ integrality=integrality,
93
+ bounds=Bounds(problem.variable_lower, problem.variable_upper),
94
+ constraints=LinearConstraint(
95
+ problem.A,
96
+ problem.constraint_lower,
97
+ problem.constraint_upper,
98
+ ),
99
+ options=options,
100
+ )
101
+
102
+ status = self._status_name(int(result.status), result.x is not None)
103
+ x = None if result.x is None else np.asarray(result.x, dtype=np.float64)
104
+ objective = None
105
+ if x is not None:
106
+ objective = float(problem.c @ x + problem.objective_offset)
107
+
108
+ raw = {
109
+ "scipy_status": int(result.status),
110
+ "message": str(result.message),
111
+ "success": bool(result.success),
112
+ }
113
+ for attr in ("mip_node_count", "mip_dual_bound", "mip_gap"):
114
+ if hasattr(result, attr):
115
+ value = getattr(result, attr)
116
+ if value is not None:
117
+ raw[attr] = float(value)
118
+
119
+ return BackendSolveResult(
120
+ backend_status=status,
121
+ x=x,
122
+ objective_reported=objective,
123
+ raw_statistics=raw,
124
+ )
125
+
126
+ @staticmethod
127
+ def _status_name(status: int, has_solution: bool) -> str:
128
+ if status == 0:
129
+ return "optimal"
130
+ if status == 1:
131
+ return "limit_feasible" if has_solution else "limit_no_solution"
132
+ if status == 2:
133
+ return "infeasible"
134
+ if status == 3:
135
+ return "unbounded"
136
+ if status == 4:
137
+ return "solver_error"
138
+ return "unknown"
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import importlib.util
5
+
6
+ import numpy as np
7
+ from scipy import sparse
8
+
9
+ from solverpilot.capabilities import BackendManifest, Capability, SupportLevel
10
+ from solverpilot.problem import LinearProblem, ObjectiveSense, QuadraticProblem
11
+
12
+ from .base import BackendSolveResult, BackendUnavailableError
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class ScipyHighsLPBackend:
17
+ """Development LP backend using ``scipy.optimize.linprog`` + HiGHS.
18
+
19
+ Parameters
20
+ ----------
21
+ method:
22
+ ``"highs-ds"`` for HiGHS dual simplex or ``"highs-ipm"`` for the
23
+ HiGHS interior-point method. This backend intentionally supports only
24
+ continuous LPs; integer and quadratic models must be routed elsewhere.
25
+
26
+ Notes
27
+ -----
28
+ This is a development bridge, not the eventual native ``highspy`` adapter.
29
+ It exists so the adaptive runtime can exercise real algorithm alternatives
30
+ without pretending SciPy exposes native HiGHS state reuse.
31
+ """
32
+
33
+ method: str = "highs-ds"
34
+ time_limit_s: float | None = None
35
+ presolve: bool | None = None
36
+
37
+ def __post_init__(self) -> None:
38
+ if self.method not in {"highs-ds", "highs-ipm"}:
39
+ raise ValueError("method must be 'highs-ds' or 'highs-ipm'")
40
+ if self.time_limit_s is not None and self.time_limit_s <= 0:
41
+ raise ValueError("time_limit_s must be positive")
42
+
43
+ @property
44
+ def manifest(self) -> BackendManifest:
45
+ version = None
46
+ if self.is_available():
47
+ import scipy
48
+
49
+ version = scipy.__version__
50
+ return BackendManifest(
51
+ name=f"scipy-{self.method}",
52
+ version=version,
53
+ capabilities={
54
+ Capability.LP: SupportLevel.NATIVE,
55
+ Capability.MILP: SupportLevel.UNSUPPORTED,
56
+ Capability.CONVEX_QP: SupportLevel.UNSUPPORTED,
57
+ Capability.MIP_START: SupportLevel.UNSUPPORTED,
58
+ Capability.PRIMAL_START: SupportLevel.UNSUPPORTED,
59
+ Capability.DUAL_START: SupportLevel.UNSUPPORTED,
60
+ Capability.BASIS_START: SupportLevel.UNSUPPORTED,
61
+ Capability.SAME_SPARSITY_DATA_UPDATE: SupportLevel.UNSUPPORTED,
62
+ Capability.STRUCTURAL_INCREMENTAL_UPDATE: SupportLevel.UNSUPPORTED,
63
+ Capability.NATIVE_REOPTIMIZATION: SupportLevel.UNSUPPORTED,
64
+ Capability.CALLBACK_PROGRESS: SupportLevel.UNSUPPORTED,
65
+ Capability.IIS: SupportLevel.UNSUPPORTED,
66
+ Capability.INFEASIBILITY_CERTIFICATE: SupportLevel.UNSUPPORTED,
67
+ },
68
+ metadata={
69
+ "development_bridge": True,
70
+ "underlying_solver": f"HiGHS {self.method} via scipy.optimize.linprog",
71
+ "algorithm": "dual_simplex" if self.method == "highs-ds" else "interior_point",
72
+ },
73
+ )
74
+
75
+ def is_available(self) -> bool:
76
+ return importlib.util.find_spec("scipy.optimize") is not None
77
+
78
+ def solve(self, problem: LinearProblem | QuadraticProblem) -> BackendSolveResult:
79
+ if not self.is_available():
80
+ raise BackendUnavailableError("SciPy optimize is not available")
81
+ if isinstance(problem, QuadraticProblem):
82
+ raise ValueError(f"{self.manifest.name} does not support QuadraticProblem")
83
+ if not isinstance(problem, LinearProblem):
84
+ raise TypeError(f"unsupported problem type: {type(problem)!r}")
85
+ if problem.has_integer_variables:
86
+ raise ValueError(f"{self.manifest.name} supports continuous LPs only")
87
+
88
+ from scipy.optimize import linprog
89
+
90
+ c = np.array(problem.c, copy=True)
91
+ if problem.objective_sense is ObjectiveSense.MAXIMIZE:
92
+ c = -c
93
+
94
+ A_ub, b_ub, A_eq, b_eq = _to_linprog_constraints(problem)
95
+ bounds = list(zip(problem.variable_lower.tolist(), problem.variable_upper.tolist()))
96
+
97
+ options: dict[str, object] = {"disp": False}
98
+ if self.time_limit_s is not None:
99
+ options["time_limit"] = float(self.time_limit_s)
100
+ if self.presolve is not None:
101
+ options["presolve"] = bool(self.presolve)
102
+
103
+ result = linprog(
104
+ c,
105
+ A_ub=A_ub,
106
+ b_ub=b_ub,
107
+ A_eq=A_eq,
108
+ b_eq=b_eq,
109
+ bounds=bounds,
110
+ method=self.method,
111
+ options=options,
112
+ )
113
+
114
+ status = self._status_name(int(result.status), result.x is not None)
115
+ x = None if result.x is None else np.asarray(result.x, dtype=np.float64)
116
+ objective = None
117
+ if x is not None:
118
+ objective = float(problem.c @ x + problem.objective_offset)
119
+
120
+ raw: dict[str, object] = {
121
+ "scipy_status": int(result.status),
122
+ "message": str(result.message),
123
+ "success": bool(result.success),
124
+ "nit": int(getattr(result, "nit", 0) or 0),
125
+ "method": self.method,
126
+ }
127
+ if hasattr(result, "crossover_nit") and result.crossover_nit is not None:
128
+ raw["crossover_nit"] = int(result.crossover_nit)
129
+
130
+ return BackendSolveResult(
131
+ backend_status=status,
132
+ x=x,
133
+ objective_reported=objective,
134
+ raw_statistics=raw,
135
+ )
136
+
137
+ @staticmethod
138
+ def _status_name(status: int, has_solution: bool) -> str:
139
+ if status == 0:
140
+ return "optimal"
141
+ if status == 1:
142
+ return "limit_feasible" if has_solution else "limit_no_solution"
143
+ if status == 2:
144
+ return "infeasible"
145
+ if status == 3:
146
+ return "unbounded"
147
+ if status == 4:
148
+ return "solver_error"
149
+ return "unknown"
150
+
151
+
152
+ def _to_linprog_constraints(
153
+ problem: LinearProblem,
154
+ ) -> tuple[
155
+ sparse.csr_matrix | None,
156
+ np.ndarray | None,
157
+ sparse.csr_matrix | None,
158
+ np.ndarray | None,
159
+ ]:
160
+ """Translate ranged rows into linprog equality/upper-bound form.
161
+
162
+ A row ``l <= a @ x <= u`` becomes:
163
+ - equality if finite ``l == u``;
164
+ - ``a @ x <= u`` when ``u`` is finite;
165
+ - ``-a @ x <= -l`` when ``l`` is finite.
166
+
167
+ The canonical problem remains the source of truth for final validation.
168
+ """
169
+
170
+ ub_rows: list[sparse.csr_matrix] = []
171
+ ub_rhs: list[float] = []
172
+ eq_rows: list[sparse.csr_matrix] = []
173
+ eq_rhs: list[float] = []
174
+
175
+ for i in range(problem.n_constraints):
176
+ lo = float(problem.constraint_lower[i])
177
+ hi = float(problem.constraint_upper[i])
178
+ row = problem.A.getrow(i)
179
+
180
+ if np.isfinite(lo) and np.isfinite(hi) and lo == hi:
181
+ eq_rows.append(row)
182
+ eq_rhs.append(lo)
183
+ continue
184
+ if np.isfinite(hi):
185
+ ub_rows.append(row)
186
+ ub_rhs.append(hi)
187
+ if np.isfinite(lo):
188
+ ub_rows.append(-row)
189
+ ub_rhs.append(-lo)
190
+
191
+ A_ub = sparse.vstack(ub_rows, format="csr") if ub_rows else None
192
+ b_ub = np.asarray(ub_rhs, dtype=np.float64) if ub_rows else None
193
+ A_eq = sparse.vstack(eq_rows, format="csr") if eq_rows else None
194
+ b_eq = np.asarray(eq_rhs, dtype=np.float64) if eq_rows else None
195
+ return A_ub, b_ub, A_eq, b_eq
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import importlib.util
5
+
6
+ import numpy as np
7
+
8
+ from solverpilot.capabilities import BackendManifest, Capability, SupportLevel
9
+ from solverpilot.problem import LinearProblem, QuadraticProblem
10
+ from solverpilot.problem.quadratic import require_confirmed_convexity
11
+
12
+ from .base import BackendSolveResult, BackendUnavailableError
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class ScipySLSQPQPBackend:
17
+ """Small-problem development bridge for convex QP via SciPy SLSQP.
18
+
19
+ This backend exists only to exercise the full QP runtime path when OSQP is not
20
+ installed. It deliberately advertises ``EMULATED_SAFE`` rather than native QP
21
+ support and does not claim an optimality certificate. A successful SLSQP exit
22
+ is normalized as a validated feasible candidate by the runtime.
23
+ """
24
+
25
+ maxiter: int = 1000
26
+ ftol: float = 1e-10
27
+
28
+ def __post_init__(self) -> None:
29
+ if self.maxiter <= 0:
30
+ raise ValueError("maxiter must be positive")
31
+ if self.ftol <= 0:
32
+ raise ValueError("ftol must be positive")
33
+
34
+ @property
35
+ def manifest(self) -> BackendManifest:
36
+ version = None
37
+ if self.is_available():
38
+ import scipy
39
+
40
+ version = scipy.__version__
41
+ return BackendManifest(
42
+ name="scipy-slsqp-qp-bridge",
43
+ version=version,
44
+ capabilities={
45
+ Capability.LP: SupportLevel.UNSUPPORTED,
46
+ Capability.MILP: SupportLevel.UNSUPPORTED,
47
+ Capability.CONVEX_QP: SupportLevel.EMULATED_SAFE,
48
+ Capability.PRIMAL_START: SupportLevel.UNSUPPORTED,
49
+ Capability.DUAL_START: SupportLevel.UNSUPPORTED,
50
+ Capability.SAME_SPARSITY_DATA_UPDATE: SupportLevel.UNSUPPORTED,
51
+ Capability.NATIVE_REOPTIMIZATION: SupportLevel.UNSUPPORTED,
52
+ Capability.INFEASIBILITY_CERTIFICATE: SupportLevel.UNSUPPORTED,
53
+ },
54
+ metadata={
55
+ "development_bridge": True,
56
+ "underlying_solver": "scipy.optimize.minimize(method='SLSQP')",
57
+ "no_optimality_certificate": True,
58
+ },
59
+ )
60
+
61
+ def is_available(self) -> bool:
62
+ return importlib.util.find_spec("scipy.optimize") is not None
63
+
64
+ def solve(self, problem: LinearProblem | QuadraticProblem) -> BackendSolveResult:
65
+ if not self.is_available():
66
+ raise BackendUnavailableError("SciPy optimize is not available")
67
+ if not isinstance(problem, QuadraticProblem):
68
+ raise ValueError("scipy-slsqp-qp-bridge supports QuadraticProblem only")
69
+
70
+ from scipy.optimize import Bounds, LinearConstraint, minimize
71
+
72
+ linear = problem.linear
73
+ P = ((problem.P + problem.P.T) * 0.5).tocsr()
74
+ q = linear.c
75
+
76
+ def fun(x: np.ndarray) -> float:
77
+ return float(0.5 * x @ (P @ x) + q @ x + linear.objective_offset)
78
+
79
+ def jac(x: np.ndarray) -> np.ndarray:
80
+ return np.asarray(P @ x + q, dtype=np.float64)
81
+
82
+ x0 = _initial_point(linear.variable_lower, linear.variable_upper)
83
+ constraints = []
84
+ if linear.n_constraints:
85
+ constraints = [
86
+ LinearConstraint(
87
+ linear.A,
88
+ linear.constraint_lower,
89
+ linear.constraint_upper,
90
+ )
91
+ ]
92
+ result = minimize(
93
+ fun,
94
+ x0,
95
+ jac=jac,
96
+ method="SLSQP",
97
+ bounds=Bounds(linear.variable_lower, linear.variable_upper),
98
+ constraints=constraints,
99
+ options={
100
+ "disp": False,
101
+ "maxiter": int(self.maxiter),
102
+ "ftol": float(self.ftol),
103
+ },
104
+ )
105
+
106
+ x = None if result.x is None else np.asarray(result.x, dtype=np.float64)
107
+ status = "converged_candidate" if bool(result.success) else (
108
+ "limit_feasible" if x is not None else "solver_error"
109
+ )
110
+ objective = None if x is None else fun(x)
111
+ return BackendSolveResult(
112
+ backend_status=status,
113
+ x=x,
114
+ objective_reported=objective,
115
+ raw_statistics={
116
+ "scipy_status": int(result.status),
117
+ "message": str(result.message),
118
+ "success": bool(result.success),
119
+ "nit": int(getattr(result, "nit", 0) or 0),
120
+ "development_only": True,
121
+ },
122
+ )
123
+
124
+
125
+ def _initial_point(lower: np.ndarray, upper: np.ndarray) -> np.ndarray:
126
+ x = np.zeros_like(lower, dtype=np.float64)
127
+ both = np.isfinite(lower) & np.isfinite(upper)
128
+ x[both] = 0.5 * (lower[both] + upper[both])
129
+ only_lower = np.isfinite(lower) & ~np.isfinite(upper)
130
+ x[only_lower] = np.maximum(0.0, lower[only_lower])
131
+ only_upper = ~np.isfinite(lower) & np.isfinite(upper)
132
+ x[only_upper] = np.minimum(0.0, upper[only_upper])
133
+ return x