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,239 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import date
5
+ import importlib.metadata
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ from scipy import sparse
10
+
11
+ from solverpilot.capabilities import (
12
+ BackendCapabilityManifestV2,
13
+ CapabilityEvidence,
14
+ CapabilityKey,
15
+ CapabilityStatus,
16
+ project_legacy_manifest,
17
+ with_verified_claims,
18
+ )
19
+ from solverpilot.problem import LinearProblem, QuadraticProblem
20
+ from solverpilot.validate import CandidateSolution, validate_solution
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class PersistenceConformanceCheck:
25
+ capability: CapabilityKey
26
+ passed: bool
27
+ detail: str
28
+ evidence_id: str
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class PersistenceConformanceReport:
33
+ backend: str
34
+ available: bool
35
+ checks: tuple[PersistenceConformanceCheck, ...]
36
+ manifest: BackendCapabilityManifestV2
37
+
38
+ @property
39
+ def passed(self) -> bool:
40
+ return self.available and bool(self.checks) and all(check.passed for check in self.checks)
41
+
42
+
43
+ def _pkg(name: str) -> str | None:
44
+ try:
45
+ return importlib.metadata.version(name)
46
+ except importlib.metadata.PackageNotFoundError:
47
+ return None
48
+
49
+
50
+ def _adapter_version() -> str | None:
51
+ try:
52
+ import solverpilot
53
+ return solverpilot.__version__
54
+ except Exception:
55
+ return None
56
+
57
+
58
+ def _binding(backend) -> tuple[str, str | None]:
59
+ module = type(backend).__module__
60
+ if module.endswith("bundled_capi") or module.endswith("casadi_conic"):
61
+ return "casadi", _pkg("casadi")
62
+ if module.endswith("highspy_native"):
63
+ return "highspy", _pkg("highspy")
64
+ if module.endswith("osqp_native"):
65
+ return "osqp", _pkg("osqp")
66
+ if module.startswith("solverpilot.backends.scipy_"):
67
+ return "scipy", _pkg("scipy")
68
+ return module, None
69
+
70
+
71
+ def _ev(backend, binding_version: str | None, key: CapabilityKey) -> CapabilityEvidence:
72
+ return CapabilityEvidence(
73
+ evidence_id=f"p5.persistence:{backend.manifest.name}:{key.value}",
74
+ kind="persistent_mutation_native_validation",
75
+ backend_versions=(backend.manifest.version,) if backend.manifest.version else (),
76
+ binding_versions=(binding_version,) if binding_version else (),
77
+ adapter_versions=(_adapter_version(),) if _adapter_version() else (),
78
+ verified_on=date.today().isoformat(),
79
+ notes=("same backend instance reused; native adapter reported reuse; candidate independently validated",),
80
+ )
81
+
82
+
83
+ def _lp_base() -> LinearProblem:
84
+ return LinearProblem.from_data(
85
+ A=[[1.0, 1.0], [1.0, 0.0]], c=[1.0, 2.0],
86
+ variable_lower=[0.0, 0.0], variable_upper=[5.0, 5.0],
87
+ constraint_lower=[1.0, -np.inf], constraint_upper=[np.inf, 4.0],
88
+ )
89
+
90
+
91
+ def _qp_base() -> QuadraticProblem:
92
+ return QuadraticProblem.from_data(
93
+ P=[[2.0, 0.0], [0.0, 2.0]], A=[[1.0, 1.0]], q=[-2.0, -4.0],
94
+ variable_lower=[-5.0, -5.0], variable_upper=[5.0, 5.0],
95
+ constraint_lower=[-np.inf], constraint_upper=[10.0], verify_convexity=True,
96
+ )
97
+
98
+
99
+ def _variants(problem):
100
+ if isinstance(problem, LinearProblem):
101
+ yield CapabilityKey.INCREMENTAL_OBJECTIVE, LinearProblem.from_data(
102
+ A=problem.A, c=[2.0, 1.0], variable_lower=problem.variable_lower,
103
+ variable_upper=problem.variable_upper, constraint_lower=problem.constraint_lower,
104
+ constraint_upper=problem.constraint_upper,
105
+ )
106
+ yield CapabilityKey.INCREMENTAL_RHS, LinearProblem.from_data(
107
+ A=problem.A, c=problem.c, variable_lower=problem.variable_lower,
108
+ variable_upper=problem.variable_upper, constraint_lower=[1.5, -np.inf],
109
+ constraint_upper=problem.constraint_upper,
110
+ )
111
+ yield CapabilityKey.INCREMENTAL_VARIABLE_BOUNDS, LinearProblem.from_data(
112
+ A=problem.A, c=problem.c, variable_lower=[0.0, 0.0], variable_upper=[3.0, 5.0],
113
+ constraint_lower=problem.constraint_lower, constraint_upper=problem.constraint_upper,
114
+ )
115
+ A = problem.A.copy().tolil(); A[0, 0] = 1.5
116
+ yield CapabilityKey.INCREMENTAL_MATRIX_VALUES, LinearProblem.from_data(
117
+ A=A.tocsr(), c=problem.c, variable_lower=problem.variable_lower,
118
+ variable_upper=problem.variable_upper, constraint_lower=problem.constraint_lower,
119
+ constraint_upper=problem.constraint_upper,
120
+ )
121
+ else:
122
+ lin = problem.linear
123
+ yield CapabilityKey.INCREMENTAL_OBJECTIVE, QuadraticProblem.from_data(
124
+ P=problem.P, A=lin.A, q=[-3.0, -4.0], variable_lower=lin.variable_lower,
125
+ variable_upper=lin.variable_upper, constraint_lower=lin.constraint_lower,
126
+ constraint_upper=lin.constraint_upper, verify_convexity=True,
127
+ )
128
+ yield CapabilityKey.INCREMENTAL_RHS, QuadraticProblem.from_data(
129
+ P=problem.P, A=lin.A, q=lin.c, variable_lower=lin.variable_lower,
130
+ variable_upper=lin.variable_upper, constraint_lower=[-np.inf],
131
+ constraint_upper=[8.0], verify_convexity=True,
132
+ )
133
+ yield CapabilityKey.INCREMENTAL_VARIABLE_BOUNDS, QuadraticProblem.from_data(
134
+ P=problem.P, A=lin.A, q=lin.c, variable_lower=[-3.0, -5.0],
135
+ variable_upper=lin.variable_upper, constraint_lower=lin.constraint_lower,
136
+ constraint_upper=lin.constraint_upper, verify_convexity=True,
137
+ )
138
+ A = lin.A.copy().tolil(); A[0, 0] = 1.5
139
+ P = problem.P.copy().tolil(); P[0, 0] = 3.0
140
+ yield CapabilityKey.INCREMENTAL_MATRIX_VALUES, QuadraticProblem.from_data(
141
+ P=P.tocsr(), A=A.tocsr(), q=lin.c, variable_lower=lin.variable_lower,
142
+ variable_upper=lin.variable_upper, constraint_lower=lin.constraint_lower,
143
+ constraint_upper=lin.constraint_upper, verify_convexity=True,
144
+ )
145
+
146
+
147
+ def conform_persistent_backend(backend) -> PersistenceConformanceReport:
148
+ binding, binding_version = _binding(backend)
149
+ base_manifest = project_legacy_manifest(
150
+ backend.manifest,
151
+ binding=binding,
152
+ binding_version=binding_version,
153
+ adapter_version=_adapter_version(),
154
+ )
155
+ if not backend.is_available():
156
+ return PersistenceConformanceReport(backend.manifest.name, False, (), base_manifest)
157
+
158
+ problem = None
159
+ if backend.manifest.name == "bundled-highs-capi":
160
+ problem = _lp_base()
161
+ elif backend.manifest.name == "bundled-osqp-capi":
162
+ problem = _qp_base()
163
+ else:
164
+ return PersistenceConformanceReport(backend.manifest.name, True, (), base_manifest)
165
+
166
+ checks: list[PersistenceConformanceCheck] = []
167
+ verified: dict[CapabilityKey, CapabilityEvidence] = {}
168
+ overrides: dict[CapabilityKey, CapabilityStatus] = {}
169
+ restriction_overrides: dict[CapabilityKey, dict[str, Any]] = {}
170
+
171
+ try:
172
+ first = backend.solve(problem)
173
+ valid0 = first.x is not None and validate_solution(
174
+ problem, CandidateSolution(x=first.x, objective_reported=first.objective_reported)
175
+ ).valid
176
+ cold_ok = bool(valid0 and not bool((first.raw_statistics or {}).get("reuse_applied", False)))
177
+ if not cold_ok:
178
+ return PersistenceConformanceReport(backend.manifest.name, True, (), base_manifest)
179
+
180
+ all_updates = True
181
+ for key, variant in _variants(problem):
182
+ # Rebase before each operation so every granular claim is independently exercised.
183
+ close = getattr(backend, "close", None)
184
+ if callable(close):
185
+ close()
186
+ base_result = backend.solve(problem)
187
+ result = backend.solve(variant)
188
+ raw = result.raw_statistics or {}
189
+ valid = result.x is not None and validate_solution(
190
+ variant, CandidateSolution(x=result.x, objective_reported=result.objective_reported)
191
+ ).valid
192
+ passed = bool(valid and raw.get("reuse_applied") is True)
193
+ if key is CapabilityKey.INCREMENTAL_MATRIX_VALUES:
194
+ passed = passed and (
195
+ bool(raw.get("matrix_values_updated"))
196
+ or int(raw.get("matrix_values_changed", 0)) > 0
197
+ )
198
+ evidence_id = f"p5.persistence:{backend.manifest.name}:{key.value}"
199
+ checks.append(PersistenceConformanceCheck(key, passed, "native same-instance mutation + independent validation", evidence_id))
200
+ all_updates = all_updates and passed
201
+ if passed:
202
+ verified[key] = _ev(backend, binding_version, key)
203
+ overrides[key] = CapabilityStatus.SUPPORTED
204
+ restriction_overrides[key] = {}
205
+
206
+ persistent_ok = all_updates
207
+ key = CapabilityKey.LIFECYCLE_PERSISTENT
208
+ checks.append(PersistenceConformanceCheck(key, persistent_ok, "same backend instance preserved across all verified numeric mutations", f"p5.persistence:{backend.manifest.name}:{key.value}"))
209
+ if persistent_ok:
210
+ verified[key] = _ev(backend, binding_version, key)
211
+ overrides[key] = CapabilityStatus.SUPPORTED
212
+ restriction_overrides[key] = {}
213
+
214
+ if backend.manifest.name == "bundled-highs-capi":
215
+ close = getattr(backend, "close", None)
216
+ if callable(close): close()
217
+ backend.solve(problem)
218
+ changed = next(v for k, v in _variants(problem) if k is CapabilityKey.INCREMENTAL_OBJECTIVE)
219
+ second = backend.solve(changed)
220
+ basis_ok = bool((second.raw_statistics or {}).get("basis_applied") is True)
221
+ key = CapabilityKey.START_BASIS
222
+ checks.append(PersistenceConformanceCheck(key, basis_ok, "captured LP basis was applied on second solve", f"p5.persistence:{backend.manifest.name}:{key.value}"))
223
+ if basis_ok:
224
+ verified[key] = _ev(backend, binding_version, key)
225
+ overrides[key] = CapabilityStatus.SUPPORTED
226
+ restriction_overrides[key] = {}
227
+ finally:
228
+ close = getattr(backend, "close", None)
229
+ if callable(close):
230
+ try: close()
231
+ except Exception: pass
232
+
233
+ manifest = with_verified_claims(
234
+ base_manifest,
235
+ verified,
236
+ status_overrides=overrides,
237
+ restriction_overrides=restriction_overrides,
238
+ )
239
+ return PersistenceConformanceReport(backend.manifest.name, True, tuple(checks), manifest)
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ import numpy as np
7
+ from scipy import sparse
8
+
9
+ from solverpilot.problem import LinearProblem, QuadraticProblem
10
+
11
+
12
+ class MutationKind(str, Enum):
13
+ OBJECTIVE_VECTOR = "objective_vector"
14
+ OBJECTIVE_OFFSET = "objective_offset"
15
+ OBJECTIVE_SENSE = "objective_sense"
16
+ OBJECTIVE_QUADRATIC_VALUES = "objective_quadratic_values"
17
+ QUADRATIC_SPARSITY_CHANGED = "quadratic_sparsity_changed"
18
+ VARIABLE_BOUNDS = "variable_bounds"
19
+ CONSTRAINT_BOUNDS = "constraint_bounds"
20
+ MATRIX_VALUES_SAME_SPARSITY = "matrix_values_same_sparsity"
21
+ MATRIX_SPARSITY_CHANGED = "matrix_sparsity_changed"
22
+ VARIABLES_ADDED_REMOVED = "variables_added_removed"
23
+ CONSTRAINTS_ADDED_REMOVED = "constraints_added_removed"
24
+ INTEGRALITY_CHANGED = "integrality_changed"
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class MutationRecord:
29
+ revision_before: int
30
+ revision_after: int
31
+ kinds: tuple[MutationKind, ...]
32
+ old_structural_hash: str
33
+ new_structural_hash: str
34
+ old_data_hash: str
35
+ new_data_hash: str
36
+
37
+ @property
38
+ def is_noop(self) -> bool:
39
+ return not self.kinds
40
+
41
+ @property
42
+ def structural_change(self) -> bool:
43
+ return self.old_structural_hash != self.new_structural_hash
44
+
45
+
46
+ def _same_sparse_pattern(a: sparse.csr_matrix, b: sparse.csr_matrix) -> bool:
47
+ return (
48
+ a.shape == b.shape
49
+ and np.array_equal(a.indptr, b.indptr)
50
+ and np.array_equal(a.indices, b.indices)
51
+ )
52
+
53
+
54
+ def classify_mutations(
55
+ old: LinearProblem | QuadraticProblem,
56
+ new: LinearProblem | QuadraticProblem,
57
+ *,
58
+ revision_before: int,
59
+ revision_after: int,
60
+ ) -> MutationRecord:
61
+ if type(old) is not type(new):
62
+ raise TypeError("session update cannot change problem class")
63
+
64
+ old_linear = old.linear if isinstance(old, QuadraticProblem) else old
65
+ new_linear = new.linear if isinstance(new, QuadraticProblem) else new
66
+ kinds: list[MutationKind] = []
67
+
68
+ if old_linear.n_variables != new_linear.n_variables:
69
+ kinds.append(MutationKind.VARIABLES_ADDED_REMOVED)
70
+ if old_linear.n_constraints != new_linear.n_constraints:
71
+ kinds.append(MutationKind.CONSTRAINTS_ADDED_REMOVED)
72
+
73
+ if not np.array_equal(old_linear.c, new_linear.c):
74
+ kinds.append(MutationKind.OBJECTIVE_VECTOR)
75
+ if old_linear.objective_offset != new_linear.objective_offset:
76
+ kinds.append(MutationKind.OBJECTIVE_OFFSET)
77
+ if old_linear.objective_sense != new_linear.objective_sense:
78
+ kinds.append(MutationKind.OBJECTIVE_SENSE)
79
+ if (
80
+ not np.array_equal(old_linear.variable_lower, new_linear.variable_lower)
81
+ or not np.array_equal(old_linear.variable_upper, new_linear.variable_upper)
82
+ ):
83
+ kinds.append(MutationKind.VARIABLE_BOUNDS)
84
+ if (
85
+ not np.array_equal(old_linear.constraint_lower, new_linear.constraint_lower)
86
+ or not np.array_equal(old_linear.constraint_upper, new_linear.constraint_upper)
87
+ ):
88
+ kinds.append(MutationKind.CONSTRAINT_BOUNDS)
89
+ if not np.array_equal(old_linear.domains, new_linear.domains):
90
+ kinds.append(MutationKind.INTEGRALITY_CHANGED)
91
+
92
+ same_a_pattern = _same_sparse_pattern(old_linear.A, new_linear.A)
93
+ if not same_a_pattern:
94
+ kinds.append(MutationKind.MATRIX_SPARSITY_CHANGED)
95
+ elif not np.array_equal(old_linear.A.data, new_linear.A.data):
96
+ kinds.append(MutationKind.MATRIX_VALUES_SAME_SPARSITY)
97
+
98
+ if isinstance(old, QuadraticProblem):
99
+ same_p_pattern = _same_sparse_pattern(old.P, new.P)
100
+ if not same_p_pattern:
101
+ kinds.append(MutationKind.QUADRATIC_SPARSITY_CHANGED)
102
+ elif not np.array_equal(old.P.data, new.P.data):
103
+ kinds.append(MutationKind.OBJECTIVE_QUADRATIC_VALUES)
104
+
105
+ # Keep enum order deterministic and avoid duplicates caused by broad changes.
106
+ deduped = tuple(dict.fromkeys(kinds))
107
+ return MutationRecord(
108
+ revision_before=revision_before,
109
+ revision_after=revision_after,
110
+ kinds=deduped,
111
+ old_structural_hash=old.structural_hash,
112
+ new_structural_hash=new.structural_hash,
113
+ old_data_hash=old.data_hash,
114
+ new_data_hash=new.data_hash,
115
+ )
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://solverpilot.local/schemas/persistent-session-trace-v1.json",
4
+ "title": "SolverPilot Persistent Session Trace v1",
5
+ "type": "object",
6
+ "required": [
7
+ "revision", "path", "decision", "compiler_cache_status", "compile_s", "dispatch_s", "total_s",
8
+ "backend", "backend_reuse_applied", "backend_reset", "semantic_hash", "data_hash", "compilation_hash",
9
+ "transformation_count"
10
+ ],
11
+ "properties": {
12
+ "revision": {"type": "integer", "minimum": 0},
13
+ "path": {"enum": ["cold_build", "native_patch", "safe_rebuild", "full_rebuild", "no_mutation_reuse"]},
14
+ "decision": {
15
+ "type": "object",
16
+ "required": ["path", "reason", "mutation_kinds", "required_capabilities", "missing_capabilities"],
17
+ "properties": {
18
+ "path": {"type": "string"},
19
+ "reason": {"type": "string"},
20
+ "mutation_kinds": {"type": "array", "items": {"type": "string"}},
21
+ "required_capabilities": {"type": "array", "items": {"type": "string"}},
22
+ "missing_capabilities": {"type": "array", "items": {"type": "string"}},
23
+ "semantic_changed": {"type": "boolean"},
24
+ "execution_structure_changed": {"type": "boolean"},
25
+ "compilation_plan_changed": {"type": "boolean"},
26
+ "certificate_changed": {"type": "boolean"}
27
+ },
28
+ "additionalProperties": false
29
+ },
30
+ "compiler_cache_status": {"type": "string"},
31
+ "compile_s": {"type": "number", "minimum": 0},
32
+ "dispatch_s": {"type": "number", "minimum": 0},
33
+ "total_s": {"type": "number", "minimum": 0},
34
+ "backend": {"type": "string"},
35
+ "backend_reuse_applied": {"type": ["boolean", "null"]},
36
+ "backend_reuse_mode": {"type": ["string", "null"]},
37
+ "backend_reset": {"type": "boolean"},
38
+ "original_validation_valid": {"type": ["boolean", "null"]},
39
+ "semantic_hash": {"type": "string"},
40
+ "data_hash": {"type": "string"},
41
+ "compilation_hash": {"type": "string"},
42
+ "transformation_count": {"type": "integer", "minimum": 0}
43
+ },
44
+ "additionalProperties": false
45
+ }