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,491 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, fields, is_dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from time import perf_counter
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
import copy
|
|
8
|
+
|
|
9
|
+
from solverpilot.backends import Backend
|
|
10
|
+
from solverpilot.capabilities import (
|
|
11
|
+
BackendCapabilityManifestV2,
|
|
12
|
+
CapabilityKey,
|
|
13
|
+
resolve_backend_capabilities,
|
|
14
|
+
)
|
|
15
|
+
from solverpilot.model import CompiledModel, Model, Parameter
|
|
16
|
+
from solverpilot.problem import LinearProblem, QuadraticProblem
|
|
17
|
+
from solverpilot.runtime import SolveResult
|
|
18
|
+
from solverpilot.runtime.executor import execute
|
|
19
|
+
|
|
20
|
+
from .mutations import MutationKind, MutationRecord, classify_mutations
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PersistentSessionError(RuntimeError):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ReuseEvidenceMismatchError(PersistentSessionError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MutationExecutionPath(str, Enum):
|
|
32
|
+
COLD_BUILD = "cold_build"
|
|
33
|
+
NATIVE_PATCH = "native_patch"
|
|
34
|
+
SAFE_REBUILD = "safe_rebuild"
|
|
35
|
+
FULL_REBUILD = "full_rebuild"
|
|
36
|
+
NO_MUTATION_REUSE = "no_mutation_reuse"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class SessionPolicy:
|
|
41
|
+
require_verified_native_patch: bool = True
|
|
42
|
+
require_backend_reuse_evidence: bool = True
|
|
43
|
+
fresh_backend_on_rebuild: bool = True
|
|
44
|
+
original_space_validation: bool = True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class MutationDecision:
|
|
49
|
+
path: MutationExecutionPath
|
|
50
|
+
reason: str
|
|
51
|
+
mutation_kinds: tuple[MutationKind, ...] = ()
|
|
52
|
+
required_capabilities: tuple[CapabilityKey, ...] = ()
|
|
53
|
+
missing_capabilities: tuple[CapabilityKey, ...] = ()
|
|
54
|
+
semantic_changed: bool = False
|
|
55
|
+
execution_structure_changed: bool = False
|
|
56
|
+
compilation_plan_changed: bool = False
|
|
57
|
+
certificate_changed: bool = False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class PersistentSessionTrace:
|
|
62
|
+
revision: int
|
|
63
|
+
path: MutationExecutionPath
|
|
64
|
+
decision: MutationDecision
|
|
65
|
+
compiler_cache_status: str
|
|
66
|
+
compile_s: float
|
|
67
|
+
dispatch_s: float
|
|
68
|
+
total_s: float
|
|
69
|
+
backend: str
|
|
70
|
+
backend_reuse_applied: bool | None
|
|
71
|
+
backend_reuse_mode: str | None
|
|
72
|
+
backend_reset: bool
|
|
73
|
+
original_validation_valid: bool | None
|
|
74
|
+
semantic_hash: str
|
|
75
|
+
data_hash: str
|
|
76
|
+
compilation_hash: str
|
|
77
|
+
transformation_count: int
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> dict[str, Any]:
|
|
80
|
+
return {
|
|
81
|
+
"revision": self.revision,
|
|
82
|
+
"path": self.path.value,
|
|
83
|
+
"decision": {
|
|
84
|
+
"path": self.decision.path.value,
|
|
85
|
+
"reason": self.decision.reason,
|
|
86
|
+
"mutation_kinds": [k.value for k in self.decision.mutation_kinds],
|
|
87
|
+
"required_capabilities": [k.value for k in self.decision.required_capabilities],
|
|
88
|
+
"missing_capabilities": [k.value for k in self.decision.missing_capabilities],
|
|
89
|
+
"semantic_changed": self.decision.semantic_changed,
|
|
90
|
+
"execution_structure_changed": self.decision.execution_structure_changed,
|
|
91
|
+
"compilation_plan_changed": self.decision.compilation_plan_changed,
|
|
92
|
+
"certificate_changed": self.decision.certificate_changed,
|
|
93
|
+
},
|
|
94
|
+
"compiler_cache_status": self.compiler_cache_status,
|
|
95
|
+
"compile_s": self.compile_s,
|
|
96
|
+
"dispatch_s": self.dispatch_s,
|
|
97
|
+
"total_s": self.total_s,
|
|
98
|
+
"backend": self.backend,
|
|
99
|
+
"backend_reuse_applied": self.backend_reuse_applied,
|
|
100
|
+
"backend_reuse_mode": self.backend_reuse_mode,
|
|
101
|
+
"backend_reset": self.backend_reset,
|
|
102
|
+
"original_validation_valid": self.original_validation_valid,
|
|
103
|
+
"semantic_hash": self.semantic_hash,
|
|
104
|
+
"data_hash": self.data_hash,
|
|
105
|
+
"compilation_hash": self.compilation_hash,
|
|
106
|
+
"transformation_count": self.transformation_count,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True, slots=True)
|
|
111
|
+
class PersistentSolveOutcome:
|
|
112
|
+
result: SolveResult
|
|
113
|
+
compiled: CompiledModel
|
|
114
|
+
trace: PersistentSessionTrace
|
|
115
|
+
original_validation: Any | None = None
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def status(self):
|
|
119
|
+
return self.result.status
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def x(self):
|
|
123
|
+
return self.result.x
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def objective(self):
|
|
127
|
+
return self.result.objective
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
BackendFactory = Callable[[], Backend]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True, slots=True)
|
|
134
|
+
class PersistentBackendBinding:
|
|
135
|
+
backend: Backend
|
|
136
|
+
capabilities: BackendCapabilityManifestV2
|
|
137
|
+
persistence_verified: bool
|
|
138
|
+
conformance: Any | None = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def bind_persistent_backend(backend: Backend, *, verify: bool = True) -> PersistentBackendBinding:
|
|
142
|
+
if verify:
|
|
143
|
+
from .conformance_v2 import conform_persistent_backend
|
|
144
|
+
report = conform_persistent_backend(backend)
|
|
145
|
+
return PersistentBackendBinding(backend, report.manifest, report.passed, report)
|
|
146
|
+
manifest = resolve_backend_capabilities(backend, verify=True)
|
|
147
|
+
return PersistentBackendBinding(backend, manifest, False, None)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _factory_from_dataclass_backend(backend: Backend) -> BackendFactory | None:
|
|
151
|
+
if not is_dataclass(backend):
|
|
152
|
+
return None
|
|
153
|
+
values: dict[str, Any] = {}
|
|
154
|
+
try:
|
|
155
|
+
for item in fields(backend):
|
|
156
|
+
if not item.init or item.name.startswith("_"):
|
|
157
|
+
continue
|
|
158
|
+
values[item.name] = copy.deepcopy(getattr(backend, item.name))
|
|
159
|
+
except Exception:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def factory():
|
|
163
|
+
return type(backend)(**copy.deepcopy(values))
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
probe = factory()
|
|
167
|
+
except Exception:
|
|
168
|
+
return None
|
|
169
|
+
close = getattr(probe, "close", None)
|
|
170
|
+
if callable(close):
|
|
171
|
+
try:
|
|
172
|
+
close()
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
return factory
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _mutation_capability(kind: MutationKind) -> CapabilityKey | None:
|
|
179
|
+
return {
|
|
180
|
+
MutationKind.OBJECTIVE_VECTOR: CapabilityKey.INCREMENTAL_OBJECTIVE,
|
|
181
|
+
MutationKind.OBJECTIVE_OFFSET: CapabilityKey.INCREMENTAL_OBJECTIVE,
|
|
182
|
+
MutationKind.OBJECTIVE_SENSE: CapabilityKey.INCREMENTAL_OBJECTIVE,
|
|
183
|
+
MutationKind.OBJECTIVE_QUADRATIC_VALUES: CapabilityKey.INCREMENTAL_OBJECTIVE,
|
|
184
|
+
MutationKind.VARIABLE_BOUNDS: CapabilityKey.INCREMENTAL_VARIABLE_BOUNDS,
|
|
185
|
+
MutationKind.CONSTRAINT_BOUNDS: CapabilityKey.INCREMENTAL_RHS,
|
|
186
|
+
MutationKind.MATRIX_VALUES_SAME_SPARSITY: CapabilityKey.INCREMENTAL_MATRIX_VALUES,
|
|
187
|
+
}.get(kind)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
_STRUCTURAL_KINDS = {
|
|
191
|
+
MutationKind.MATRIX_SPARSITY_CHANGED,
|
|
192
|
+
MutationKind.QUADRATIC_SPARSITY_CHANGED,
|
|
193
|
+
MutationKind.VARIABLES_ADDED_REMOVED,
|
|
194
|
+
MutationKind.CONSTRAINTS_ADDED_REMOVED,
|
|
195
|
+
MutationKind.INTEGRALITY_CHANGED,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def decide_mutation_path(
|
|
200
|
+
previous: CompiledModel | None,
|
|
201
|
+
current: CompiledModel,
|
|
202
|
+
capabilities: BackendCapabilityManifestV2,
|
|
203
|
+
*,
|
|
204
|
+
require_verified: bool = True,
|
|
205
|
+
) -> tuple[MutationDecision, MutationRecord | None]:
|
|
206
|
+
if previous is None:
|
|
207
|
+
return MutationDecision(MutationExecutionPath.COLD_BUILD, "first solve has no bound backend state"), None
|
|
208
|
+
|
|
209
|
+
semantic_changed = previous.semantic_hash != current.semantic_hash
|
|
210
|
+
compilation_changed = previous.compilation_hash != current.compilation_hash
|
|
211
|
+
certificate_changed = previous.precondition_certificate_hashes != current.precondition_certificate_hashes
|
|
212
|
+
|
|
213
|
+
old = previous.execution_ir
|
|
214
|
+
new = current.execution_ir
|
|
215
|
+
if type(old) is not type(new):
|
|
216
|
+
return MutationDecision(
|
|
217
|
+
MutationExecutionPath.FULL_REBUILD,
|
|
218
|
+
"execution IR class changed",
|
|
219
|
+
semantic_changed=semantic_changed,
|
|
220
|
+
execution_structure_changed=True,
|
|
221
|
+
compilation_plan_changed=compilation_changed,
|
|
222
|
+
certificate_changed=certificate_changed,
|
|
223
|
+
), None
|
|
224
|
+
|
|
225
|
+
record = classify_mutations(old, new, revision_before=0, revision_after=1)
|
|
226
|
+
kinds = record.kinds
|
|
227
|
+
if record.is_noop:
|
|
228
|
+
return MutationDecision(
|
|
229
|
+
MutationExecutionPath.NO_MUTATION_REUSE,
|
|
230
|
+
"execution data is unchanged; existing backend model may be solved again",
|
|
231
|
+
mutation_kinds=(),
|
|
232
|
+
semantic_changed=semantic_changed,
|
|
233
|
+
execution_structure_changed=False,
|
|
234
|
+
compilation_plan_changed=compilation_changed,
|
|
235
|
+
certificate_changed=certificate_changed,
|
|
236
|
+
), record
|
|
237
|
+
|
|
238
|
+
if semantic_changed or any(kind in _STRUCTURAL_KINDS for kind in kinds):
|
|
239
|
+
return MutationDecision(
|
|
240
|
+
MutationExecutionPath.FULL_REBUILD,
|
|
241
|
+
"semantic or execution structure changed; P5 does not emulate structural incremental edits",
|
|
242
|
+
mutation_kinds=kinds,
|
|
243
|
+
semantic_changed=semantic_changed,
|
|
244
|
+
execution_structure_changed=record.structural_change,
|
|
245
|
+
compilation_plan_changed=compilation_changed,
|
|
246
|
+
certificate_changed=certificate_changed,
|
|
247
|
+
), record
|
|
248
|
+
|
|
249
|
+
required_list: list[CapabilityKey] = [CapabilityKey.LIFECYCLE_PERSISTENT]
|
|
250
|
+
for kind in kinds:
|
|
251
|
+
cap = _mutation_capability(kind)
|
|
252
|
+
if cap is None:
|
|
253
|
+
return MutationDecision(
|
|
254
|
+
MutationExecutionPath.SAFE_REBUILD,
|
|
255
|
+
f"mutation {kind.value} has no verified P5 native-patch mapping",
|
|
256
|
+
mutation_kinds=kinds,
|
|
257
|
+
semantic_changed=semantic_changed,
|
|
258
|
+
execution_structure_changed=record.structural_change,
|
|
259
|
+
compilation_plan_changed=compilation_changed,
|
|
260
|
+
certificate_changed=certificate_changed,
|
|
261
|
+
), record
|
|
262
|
+
if cap not in required_list:
|
|
263
|
+
required_list.append(cap)
|
|
264
|
+
required = tuple(required_list)
|
|
265
|
+
missing = tuple(
|
|
266
|
+
key for key in required
|
|
267
|
+
if not capabilities.check(key, require_verified=require_verified).usable
|
|
268
|
+
)
|
|
269
|
+
if missing:
|
|
270
|
+
return MutationDecision(
|
|
271
|
+
MutationExecutionPath.SAFE_REBUILD,
|
|
272
|
+
"same-structure update is correct but native patch lacks verified capability evidence",
|
|
273
|
+
mutation_kinds=kinds,
|
|
274
|
+
required_capabilities=required,
|
|
275
|
+
missing_capabilities=missing,
|
|
276
|
+
semantic_changed=semantic_changed,
|
|
277
|
+
execution_structure_changed=record.structural_change,
|
|
278
|
+
compilation_plan_changed=compilation_changed,
|
|
279
|
+
certificate_changed=certificate_changed,
|
|
280
|
+
), record
|
|
281
|
+
return MutationDecision(
|
|
282
|
+
MutationExecutionPath.NATIVE_PATCH,
|
|
283
|
+
"all same-structure mutation operations and persistent lifecycle are runtime-verified",
|
|
284
|
+
mutation_kinds=kinds,
|
|
285
|
+
required_capabilities=required,
|
|
286
|
+
semantic_changed=semantic_changed,
|
|
287
|
+
execution_structure_changed=record.structural_change,
|
|
288
|
+
compilation_plan_changed=compilation_changed,
|
|
289
|
+
certificate_changed=certificate_changed,
|
|
290
|
+
), record
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class PersistentSession:
|
|
294
|
+
"""Bind a semantic model to one backend lifecycle with fail-closed mutation routing.
|
|
295
|
+
|
|
296
|
+
P5 owns the *execution decision* only. P2 remains responsible for semantic/parameter
|
|
297
|
+
recompilation, P4 remains responsible for reformulation certificates, and the backend
|
|
298
|
+
remains responsible for the native numerical update implementation.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def __init__(
|
|
302
|
+
self,
|
|
303
|
+
model: Model,
|
|
304
|
+
backend: Backend,
|
|
305
|
+
*,
|
|
306
|
+
capabilities: BackendCapabilityManifestV2 | None = None,
|
|
307
|
+
backend_factory: BackendFactory | None = None,
|
|
308
|
+
bridge_policy=None,
|
|
309
|
+
policy: SessionPolicy | None = None,
|
|
310
|
+
) -> None:
|
|
311
|
+
if not isinstance(model, Model):
|
|
312
|
+
raise TypeError("PersistentSession requires an solverpilot.model.Model")
|
|
313
|
+
self.model = model
|
|
314
|
+
self.backend = backend
|
|
315
|
+
self.capabilities = capabilities or resolve_backend_capabilities(backend, verify=True)
|
|
316
|
+
if self.capabilities.backend != backend.manifest.name:
|
|
317
|
+
raise PersistentSessionError("capability manifest backend identity does not match bound backend")
|
|
318
|
+
if (
|
|
319
|
+
self.capabilities.backend_version is not None
|
|
320
|
+
and backend.manifest.version is not None
|
|
321
|
+
and self.capabilities.backend_version != backend.manifest.version
|
|
322
|
+
):
|
|
323
|
+
raise PersistentSessionError("capability manifest backend version does not match bound backend")
|
|
324
|
+
self.backend_factory = backend_factory or _factory_from_dataclass_backend(backend)
|
|
325
|
+
self.bridge_policy = bridge_policy
|
|
326
|
+
self.policy = policy or SessionPolicy()
|
|
327
|
+
self._compiled: CompiledModel | None = None
|
|
328
|
+
self._last_outcome: PersistentSolveOutcome | None = None
|
|
329
|
+
self._revision = 0
|
|
330
|
+
self._history: list[PersistentSessionTrace] = []
|
|
331
|
+
self._closed = False
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def from_binding(cls, model: Model, binding: PersistentBackendBinding, **kwargs):
|
|
335
|
+
return cls(model, binding.backend, capabilities=binding.capabilities, **kwargs)
|
|
336
|
+
|
|
337
|
+
@classmethod
|
|
338
|
+
def bind(cls, model: Model, backend: Backend, *, verify: bool = True, **kwargs):
|
|
339
|
+
return cls.from_binding(model, bind_persistent_backend(backend, verify=verify), **kwargs)
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def revision(self) -> int:
|
|
343
|
+
return self._revision
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def history(self) -> tuple[PersistentSessionTrace, ...]:
|
|
347
|
+
return tuple(self._history)
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def last_outcome(self) -> PersistentSolveOutcome | None:
|
|
351
|
+
return self._last_outcome
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def compiled(self) -> CompiledModel | None:
|
|
355
|
+
return self._compiled
|
|
356
|
+
|
|
357
|
+
def set_parameter(self, parameter: Parameter, value: Any) -> None:
|
|
358
|
+
if not isinstance(parameter, Parameter) or parameter._model is not self.model:
|
|
359
|
+
raise TypeError("parameter must belong to this PersistentSession model")
|
|
360
|
+
parameter.value = value
|
|
361
|
+
|
|
362
|
+
def set_parameter_by_name(self, name: str, value: Any) -> None:
|
|
363
|
+
matches = [p for p in self.model.parameters if p.name == name]
|
|
364
|
+
if len(matches) != 1:
|
|
365
|
+
raise KeyError(f"parameter name must resolve uniquely: {name!r}")
|
|
366
|
+
matches[0].value = value
|
|
367
|
+
|
|
368
|
+
def _replace_backend_fresh(self) -> bool:
|
|
369
|
+
old = self.backend
|
|
370
|
+
if self.backend_factory is not None:
|
|
371
|
+
close = getattr(old, "close", None)
|
|
372
|
+
if callable(close):
|
|
373
|
+
try:
|
|
374
|
+
close()
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|
|
377
|
+
self.backend = self.backend_factory()
|
|
378
|
+
return True
|
|
379
|
+
close = getattr(old, "close", None)
|
|
380
|
+
if callable(close):
|
|
381
|
+
close()
|
|
382
|
+
return True
|
|
383
|
+
if self.policy.fresh_backend_on_rebuild:
|
|
384
|
+
raise PersistentSessionError(
|
|
385
|
+
"safe rebuild requires a backend factory or a backend exposing close(); "
|
|
386
|
+
"provide backend_factory explicitly"
|
|
387
|
+
)
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
def solve(self) -> PersistentSolveOutcome:
|
|
391
|
+
if self._closed:
|
|
392
|
+
raise PersistentSessionError("PersistentSession is closed")
|
|
393
|
+
total_t0 = perf_counter()
|
|
394
|
+
compile_t0 = perf_counter()
|
|
395
|
+
compiled = self.model.compile(
|
|
396
|
+
use_cache=True,
|
|
397
|
+
bridge_policy=self.bridge_policy,
|
|
398
|
+
capabilities=self.capabilities,
|
|
399
|
+
)
|
|
400
|
+
compile_s = perf_counter() - compile_t0
|
|
401
|
+
decision, _ = decide_mutation_path(
|
|
402
|
+
self._compiled,
|
|
403
|
+
compiled,
|
|
404
|
+
self.capabilities,
|
|
405
|
+
require_verified=self.policy.require_verified_native_patch,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
backend_reset = False
|
|
409
|
+
if decision.path in {MutationExecutionPath.SAFE_REBUILD, MutationExecutionPath.FULL_REBUILD}:
|
|
410
|
+
backend_reset = self._replace_backend_fresh()
|
|
411
|
+
# A fresh backend object may have identical runtime identity; capabilities remain
|
|
412
|
+
# valid only for the exact version signature already bound to this session.
|
|
413
|
+
fresh_caps = resolve_backend_capabilities(self.backend, verify=False)
|
|
414
|
+
if (
|
|
415
|
+
fresh_caps.backend != self.capabilities.backend
|
|
416
|
+
or fresh_caps.backend_version != self.capabilities.backend_version
|
|
417
|
+
):
|
|
418
|
+
raise PersistentSessionError("fresh backend runtime identity drifted from bound capability evidence")
|
|
419
|
+
|
|
420
|
+
dispatch_t0 = perf_counter()
|
|
421
|
+
result = execute(compiled.execution_ir, self.backend)
|
|
422
|
+
dispatch_s = perf_counter() - dispatch_t0
|
|
423
|
+
raw = result.raw_statistics or {}
|
|
424
|
+
reuse_raw = raw.get("reuse_applied")
|
|
425
|
+
reuse_applied = reuse_raw if isinstance(reuse_raw, bool) else None
|
|
426
|
+
reuse_mode = None if raw.get("reuse_mode") is None else str(raw.get("reuse_mode"))
|
|
427
|
+
|
|
428
|
+
if decision.path is MutationExecutionPath.NATIVE_PATCH and self.policy.require_backend_reuse_evidence:
|
|
429
|
+
if reuse_applied is not True:
|
|
430
|
+
raise ReuseEvidenceMismatchError(
|
|
431
|
+
"P5 selected native_patch from verified capabilities, but backend did not report applied reuse"
|
|
432
|
+
)
|
|
433
|
+
if decision.path in {MutationExecutionPath.COLD_BUILD, MutationExecutionPath.SAFE_REBUILD, MutationExecutionPath.FULL_REBUILD}:
|
|
434
|
+
if backend_reset or decision.path is MutationExecutionPath.COLD_BUILD:
|
|
435
|
+
if reuse_applied is True:
|
|
436
|
+
raise ReuseEvidenceMismatchError(
|
|
437
|
+
"fresh/cold backend unexpectedly reported reuse; lifecycle evidence is inconsistent"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
original_validation = None
|
|
441
|
+
original_valid = None
|
|
442
|
+
if self.policy.original_space_validation and result.x is not None:
|
|
443
|
+
original_validation = compiled.validate_original(self.model, result.x)
|
|
444
|
+
original_valid = bool(original_validation.valid)
|
|
445
|
+
if not original_valid:
|
|
446
|
+
raise PersistentSessionError("backend candidate failed P5 original-space semantic validation")
|
|
447
|
+
|
|
448
|
+
previous_data = None if self._compiled is None else self._compiled.data_hash
|
|
449
|
+
if previous_data is not None and previous_data != compiled.data_hash:
|
|
450
|
+
self._revision += 1
|
|
451
|
+
elif self._compiled is not None and self._compiled.semantic_hash != compiled.semantic_hash:
|
|
452
|
+
self._revision += 1
|
|
453
|
+
|
|
454
|
+
trace = PersistentSessionTrace(
|
|
455
|
+
revision=self._revision,
|
|
456
|
+
path=decision.path,
|
|
457
|
+
decision=decision,
|
|
458
|
+
compiler_cache_status=compiled.compilation_report.cache_status,
|
|
459
|
+
compile_s=float(compile_s),
|
|
460
|
+
dispatch_s=float(dispatch_s),
|
|
461
|
+
total_s=float(perf_counter() - total_t0),
|
|
462
|
+
backend=self.backend.manifest.name,
|
|
463
|
+
backend_reuse_applied=reuse_applied,
|
|
464
|
+
backend_reuse_mode=reuse_mode,
|
|
465
|
+
backend_reset=backend_reset,
|
|
466
|
+
original_validation_valid=original_valid,
|
|
467
|
+
semantic_hash=compiled.semantic_hash,
|
|
468
|
+
data_hash=compiled.data_hash,
|
|
469
|
+
compilation_hash=compiled.compilation_hash,
|
|
470
|
+
transformation_count=len(compiled.transformation_tape),
|
|
471
|
+
)
|
|
472
|
+
outcome = PersistentSolveOutcome(result=result, compiled=compiled, trace=trace, original_validation=original_validation)
|
|
473
|
+
self._compiled = compiled
|
|
474
|
+
self._last_outcome = outcome
|
|
475
|
+
self._history.append(trace)
|
|
476
|
+
return outcome
|
|
477
|
+
|
|
478
|
+
def close(self) -> None:
|
|
479
|
+
if self._closed:
|
|
480
|
+
return
|
|
481
|
+
close = getattr(self.backend, "close", None)
|
|
482
|
+
if callable(close):
|
|
483
|
+
close()
|
|
484
|
+
self._closed = True
|
|
485
|
+
|
|
486
|
+
def __enter__(self):
|
|
487
|
+
return self
|
|
488
|
+
|
|
489
|
+
def __exit__(self, exc_type, exc, tb):
|
|
490
|
+
self.close()
|
|
491
|
+
return False
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from solverpilot.capabilities import BackendManifest, Capability
|
|
6
|
+
from solverpilot.problem import LinearProblem, QuadraticProblem
|
|
7
|
+
|
|
8
|
+
from .mutations import MutationKind, MutationRecord
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class ReuseAssessment:
|
|
13
|
+
backend: str
|
|
14
|
+
considered: tuple[Capability, ...]
|
|
15
|
+
supported: tuple[Capability, ...]
|
|
16
|
+
safe_candidate: bool
|
|
17
|
+
applied: bool
|
|
18
|
+
reason: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def assess_reuse(
|
|
22
|
+
problem: LinearProblem | QuadraticProblem,
|
|
23
|
+
manifest: BackendManifest,
|
|
24
|
+
mutations: tuple[MutationRecord, ...],
|
|
25
|
+
) -> ReuseAssessment:
|
|
26
|
+
"""Assess *potential* reuse from declared capabilities.
|
|
27
|
+
|
|
28
|
+
This function never claims reuse was applied. Applying solver state requires a
|
|
29
|
+
backend-specific stateful interface, which M1 deliberately does not fake.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
kinds = {kind for record in mutations for kind in record.kinds}
|
|
33
|
+
if not mutations or not kinds:
|
|
34
|
+
return ReuseAssessment(
|
|
35
|
+
backend=manifest.name,
|
|
36
|
+
considered=(),
|
|
37
|
+
supported=(),
|
|
38
|
+
safe_candidate=False,
|
|
39
|
+
applied=False,
|
|
40
|
+
reason="no model mutation since previous solve",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
structural_kinds = {
|
|
44
|
+
MutationKind.VARIABLES_ADDED_REMOVED,
|
|
45
|
+
MutationKind.CONSTRAINTS_ADDED_REMOVED,
|
|
46
|
+
MutationKind.MATRIX_SPARSITY_CHANGED,
|
|
47
|
+
MutationKind.QUADRATIC_SPARSITY_CHANGED,
|
|
48
|
+
MutationKind.INTEGRALITY_CHANGED,
|
|
49
|
+
}
|
|
50
|
+
if kinds & structural_kinds:
|
|
51
|
+
considered = (
|
|
52
|
+
Capability.STRUCTURAL_INCREMENTAL_UPDATE,
|
|
53
|
+
Capability.NATIVE_REOPTIMIZATION,
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
candidates: list[Capability] = [Capability.SAME_SPARSITY_DATA_UPDATE]
|
|
57
|
+
if isinstance(problem, QuadraticProblem):
|
|
58
|
+
candidates.extend([Capability.PRIMAL_START, Capability.DUAL_START])
|
|
59
|
+
elif problem.has_integer_variables:
|
|
60
|
+
candidates.append(Capability.MIP_START)
|
|
61
|
+
else:
|
|
62
|
+
candidates.extend([Capability.BASIS_START, Capability.PRIMAL_START])
|
|
63
|
+
candidates.append(Capability.NATIVE_REOPTIMIZATION)
|
|
64
|
+
considered = tuple(candidates)
|
|
65
|
+
|
|
66
|
+
supported = tuple(
|
|
67
|
+
cap for cap in considered if manifest.supports_without_risky_emulation(cap)
|
|
68
|
+
)
|
|
69
|
+
return ReuseAssessment(
|
|
70
|
+
backend=manifest.name,
|
|
71
|
+
considered=considered,
|
|
72
|
+
supported=supported,
|
|
73
|
+
safe_candidate=bool(supported),
|
|
74
|
+
applied=False,
|
|
75
|
+
reason=(
|
|
76
|
+
"backend declares potentially reusable mechanisms; execution evidence is required "
|
|
77
|
+
"before reuse is marked applied"
|
|
78
|
+
if supported
|
|
79
|
+
else "backend declares no compatible reuse mechanism for these mutations"
|
|
80
|
+
),
|
|
81
|
+
)
|