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,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
|
+
}
|