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,1263 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from time import perf_counter
|
|
8
|
+
from typing import Any, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from scipy import sparse
|
|
12
|
+
|
|
13
|
+
from solverpilot.problem import LinearProblem, ObjectiveSense, QuadraticProblem, VariableDomain
|
|
14
|
+
from solverpilot.session.mutations import classify_mutations
|
|
15
|
+
from solverpilot.bridges import (
|
|
16
|
+
AffineScalar, BridgePolicy, BridgeUnavailable, MappingAvailability, TransformationTape,
|
|
17
|
+
choose_indicator_path, lower_indicator_big_m, validate_semantic_primal,
|
|
18
|
+
)
|
|
19
|
+
from solverpilot.capabilities.v2 import BackendCapabilityManifestV2
|
|
20
|
+
|
|
21
|
+
from .errors import CompileError, ShapeError
|
|
22
|
+
from .expression import ExprNode
|
|
23
|
+
from .model import Constraint, IndicatorConstraint, Model
|
|
24
|
+
from .sets import EqualTo, GreaterThan, Interval, LessThan
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from solverpilot.conic.ir import ConicProblem
|
|
28
|
+
from solverpilot.nlp.ir import NLPProblem
|
|
29
|
+
from solverpilot.minlp.ir import MINLPProblem
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_CAPABILITY_SIGNATURE = "legacy-ir:p2:v1"
|
|
33
|
+
_NUMERICAL_POLICY = "p0-rfc-012:p2-default-v1"
|
|
34
|
+
_CACHE_KEY = f"{_CAPABILITY_SIGNATURE}|{_NUMERICAL_POLICY}"
|
|
35
|
+
_P4_NUMERICAL_POLICY = "p0-rfc-012:p4-safe-bridge-v1"
|
|
36
|
+
_P4_CAPABILITY_SIGNATURE = "legacy-ir:p4:v1"
|
|
37
|
+
_MAX_SNAPSHOTS = 8
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(slots=True)
|
|
41
|
+
class _Polynomial:
|
|
42
|
+
constant: float
|
|
43
|
+
linear: dict[int, float]
|
|
44
|
+
quadratic: dict[tuple[int, int], float]
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def scalar(cls, value: float) -> "_Polynomial":
|
|
48
|
+
return cls(float(value), {}, {})
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def variable(cls, index: int) -> "_Polynomial":
|
|
52
|
+
return cls(0.0, {index: 1.0}, {})
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def degree(self) -> int:
|
|
56
|
+
if self.quadratic:
|
|
57
|
+
return 2
|
|
58
|
+
if self.linear:
|
|
59
|
+
return 1
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
def __add__(self, other: "_Polynomial") -> "_Polynomial":
|
|
63
|
+
lin = dict(self.linear)
|
|
64
|
+
for i, v in other.linear.items():
|
|
65
|
+
lin[i] = lin.get(i, 0.0) + v
|
|
66
|
+
if lin[i] == 0.0:
|
|
67
|
+
del lin[i]
|
|
68
|
+
quad = dict(self.quadratic)
|
|
69
|
+
for k, v in other.quadratic.items():
|
|
70
|
+
quad[k] = quad.get(k, 0.0) + v
|
|
71
|
+
if quad[k] == 0.0:
|
|
72
|
+
del quad[k]
|
|
73
|
+
return _Polynomial(self.constant + other.constant, lin, quad)
|
|
74
|
+
|
|
75
|
+
def __neg__(self) -> "_Polynomial":
|
|
76
|
+
return _Polynomial(
|
|
77
|
+
-self.constant,
|
|
78
|
+
{i: -v for i, v in self.linear.items()},
|
|
79
|
+
{k: -v for k, v in self.quadratic.items()},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def __sub__(self, other: "_Polynomial") -> "_Polynomial":
|
|
83
|
+
return self + (-other)
|
|
84
|
+
|
|
85
|
+
def __mul__(self, other: "_Polynomial") -> "_Polynomial":
|
|
86
|
+
if self.degree + other.degree > 2:
|
|
87
|
+
raise CompileError("expression degree exceeds quadratic P2 scope")
|
|
88
|
+
constant = self.constant * other.constant
|
|
89
|
+
lin: dict[int, float] = {}
|
|
90
|
+
for i, v in self.linear.items():
|
|
91
|
+
lin[i] = lin.get(i, 0.0) + v * other.constant
|
|
92
|
+
for i, v in other.linear.items():
|
|
93
|
+
lin[i] = lin.get(i, 0.0) + v * self.constant
|
|
94
|
+
quad: dict[tuple[int, int], float] = {}
|
|
95
|
+
for k, v in self.quadratic.items():
|
|
96
|
+
quad[k] = quad.get(k, 0.0) + v * other.constant
|
|
97
|
+
for k, v in other.quadratic.items():
|
|
98
|
+
quad[k] = quad.get(k, 0.0) + v * self.constant
|
|
99
|
+
for i, a in self.linear.items():
|
|
100
|
+
for j, b in other.linear.items():
|
|
101
|
+
key = (i, j) if i <= j else (j, i)
|
|
102
|
+
quad[key] = quad.get(key, 0.0) + a * b
|
|
103
|
+
lin = {k: v for k, v in lin.items() if v != 0.0}
|
|
104
|
+
quad = {k: v for k, v in quad.items() if v != 0.0}
|
|
105
|
+
return _Polynomial(constant, lin, quad)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class CompilationReport:
|
|
110
|
+
target: str
|
|
111
|
+
semantic_analysis_s: float
|
|
112
|
+
lowering_s: float
|
|
113
|
+
total_s: float
|
|
114
|
+
n_semantic_variables: int
|
|
115
|
+
n_execution_variables: int
|
|
116
|
+
n_semantic_constraints: int
|
|
117
|
+
n_execution_constraints: int
|
|
118
|
+
warnings: tuple[str, ...] = ()
|
|
119
|
+
cache_status: str = "miss"
|
|
120
|
+
changed_parameters: tuple[str, ...] = ()
|
|
121
|
+
reused_execution_rows: int = 0
|
|
122
|
+
recompiled_execution_rows: int = 0
|
|
123
|
+
objective_recompiled: bool = True
|
|
124
|
+
execution_mutations: tuple[str, ...] = ()
|
|
125
|
+
bridge_search_s: float = 0.0
|
|
126
|
+
n_transformations: int = 0
|
|
127
|
+
rejected_bridge_paths: tuple[str, ...] = ()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True, slots=True)
|
|
131
|
+
class CompilerCacheInfo:
|
|
132
|
+
semantic_hash: str | None
|
|
133
|
+
snapshots: int
|
|
134
|
+
full_compiles: int
|
|
135
|
+
partial_compiles: int
|
|
136
|
+
snapshot_hits: int
|
|
137
|
+
semantic_invalidations: int
|
|
138
|
+
last_data_hash: str | None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True, slots=True)
|
|
142
|
+
class CompiledModel:
|
|
143
|
+
execution_ir: LinearProblem | QuadraticProblem | "ConicProblem" | "NLPProblem" | "MINLPProblem"
|
|
144
|
+
semantic_hash: str
|
|
145
|
+
data_hash: str
|
|
146
|
+
compilation_hash: str
|
|
147
|
+
parameter_map: dict[str, Any]
|
|
148
|
+
source_map: dict[str, Any]
|
|
149
|
+
transformation_tape: tuple[dict[str, Any], ...]
|
|
150
|
+
capability_signature: str
|
|
151
|
+
mutation_plan: dict[str, Any]
|
|
152
|
+
plan_sensitive_dependencies: tuple[str, ...]
|
|
153
|
+
precondition_certificate_hashes: tuple[str, ...]
|
|
154
|
+
compilation_report: CompilationReport
|
|
155
|
+
reconstruction_contract: dict[str, Any]
|
|
156
|
+
schema_version: str = "solverpilot.compiled-model.p2.v1"
|
|
157
|
+
|
|
158
|
+
def solve(self, **kwargs):
|
|
159
|
+
from solverpilot.conic import ConicProblem, solve_conic
|
|
160
|
+
if isinstance(self.execution_ir, ConicProblem):
|
|
161
|
+
return solve_conic(self.execution_ir, **kwargs)
|
|
162
|
+
try:
|
|
163
|
+
from solverpilot.minlp import MINLPProblem, solve_outer_approximation
|
|
164
|
+
if isinstance(self.execution_ir, MINLPProblem):
|
|
165
|
+
return solve_outer_approximation(self.execution_ir, **kwargs)
|
|
166
|
+
except ImportError:
|
|
167
|
+
pass
|
|
168
|
+
try:
|
|
169
|
+
from solverpilot.nlp import NLPProblem, CasadiIpoptBackend
|
|
170
|
+
if isinstance(self.execution_ir, NLPProblem):
|
|
171
|
+
backend = kwargs.pop("backend", None) or CasadiIpoptBackend()
|
|
172
|
+
return backend.solve(self.execution_ir, **kwargs)
|
|
173
|
+
except ImportError:
|
|
174
|
+
pass
|
|
175
|
+
from solverpilot.runtime import solve
|
|
176
|
+
return solve(self.execution_ir, **kwargs)
|
|
177
|
+
|
|
178
|
+
def refresh(self, model: Model, *, use_cache: bool = True, bridge_policy=None, capabilities=None) -> "CompiledModel":
|
|
179
|
+
return compile_model(model, use_cache=use_cache, bridge_policy=bridge_policy, capabilities=capabilities)
|
|
180
|
+
|
|
181
|
+
def reconstruct_primal(self, x):
|
|
182
|
+
import numpy as _np
|
|
183
|
+
# P4 bridges currently add no execution variables, so primal reconstruction
|
|
184
|
+
# is an identity map. The tape still records this explicitly for auditability.
|
|
185
|
+
for step in self.transformation_tape:
|
|
186
|
+
if step.get("primal_mapping") not in {"identity", "available"}:
|
|
187
|
+
raise CompileError("primal reconstruction is unavailable for a transformation step")
|
|
188
|
+
return _np.asarray(x, dtype=float).copy()
|
|
189
|
+
|
|
190
|
+
def validate_original(self, model: Model, x, *, atol: float = 1e-8):
|
|
191
|
+
try:
|
|
192
|
+
from solverpilot.minlp import MINLPProblem, validate_minlp_solution
|
|
193
|
+
if isinstance(self.execution_ir, MINLPProblem):
|
|
194
|
+
return validate_minlp_solution(self.execution_ir, self.reconstruct_primal(x), atol=atol, rtol=atol)
|
|
195
|
+
except ImportError:
|
|
196
|
+
pass
|
|
197
|
+
try:
|
|
198
|
+
from solverpilot.nlp import NLPProblem, validate_nlp_solution
|
|
199
|
+
if isinstance(self.execution_ir, NLPProblem):
|
|
200
|
+
return validate_nlp_solution(self.execution_ir, self.reconstruct_primal(x), atol=atol, rtol=atol)
|
|
201
|
+
except ImportError:
|
|
202
|
+
pass
|
|
203
|
+
return validate_semantic_primal(model, self.reconstruct_primal(x), atol=atol)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True, slots=True)
|
|
207
|
+
class _Template:
|
|
208
|
+
semantic_hash: str
|
|
209
|
+
target: str
|
|
210
|
+
offsets: dict[str, tuple[int, int]]
|
|
211
|
+
n_variables: int
|
|
212
|
+
variable_lower: np.ndarray
|
|
213
|
+
variable_upper: np.ndarray
|
|
214
|
+
domains: tuple[str, ...]
|
|
215
|
+
source_map: dict[str, Any]
|
|
216
|
+
constraint_by_id: dict[str, Constraint]
|
|
217
|
+
constraint_dependencies: dict[str, frozenset[str]]
|
|
218
|
+
objective_dependencies: frozenset[str]
|
|
219
|
+
parameter_to_constraints: dict[str, tuple[str, ...]]
|
|
220
|
+
parameter_affects_objective: dict[str, bool]
|
|
221
|
+
compilation_hash: str
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(slots=True)
|
|
225
|
+
class _CacheEntry:
|
|
226
|
+
semantic_hash: str
|
|
227
|
+
template: _Template
|
|
228
|
+
current: CompiledModel
|
|
229
|
+
parameter_versions: dict[str, int]
|
|
230
|
+
snapshots: OrderedDict[str, CompiledModel]
|
|
231
|
+
full_compiles: int = 1
|
|
232
|
+
partial_compiles: int = 0
|
|
233
|
+
snapshot_hits: int = 0
|
|
234
|
+
semantic_invalidations: int = 0
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class _Evaluator:
|
|
238
|
+
def __init__(self, model: Model, offsets: dict[str, tuple[int, int]]) -> None:
|
|
239
|
+
self.model = model
|
|
240
|
+
self.offsets = offsets
|
|
241
|
+
self._cache: dict[int, np.ndarray] = {}
|
|
242
|
+
|
|
243
|
+
def evaluate(self, node: ExprNode) -> np.ndarray:
|
|
244
|
+
cache_key = id(node)
|
|
245
|
+
if cache_key in self._cache:
|
|
246
|
+
return self._cache[cache_key]
|
|
247
|
+
if node.kind == "constant":
|
|
248
|
+
raw = np.asarray(node.payload, dtype=np.float64)
|
|
249
|
+
out = np.empty(raw.shape if raw.shape else (), dtype=object)
|
|
250
|
+
if raw.shape:
|
|
251
|
+
for idx in np.ndindex(raw.shape):
|
|
252
|
+
out[idx] = _Polynomial.scalar(float(raw[idx]))
|
|
253
|
+
else:
|
|
254
|
+
out[()] = _Polynomial.scalar(float(raw))
|
|
255
|
+
elif node.kind == "parameter":
|
|
256
|
+
raw = np.asarray(self.model._parameters[node.payload].value, dtype=np.float64)
|
|
257
|
+
out = np.empty(raw.shape if raw.shape else (), dtype=object)
|
|
258
|
+
if raw.shape:
|
|
259
|
+
for idx in np.ndindex(raw.shape):
|
|
260
|
+
out[idx] = _Polynomial.scalar(float(raw[idx]))
|
|
261
|
+
else:
|
|
262
|
+
out[()] = _Polynomial.scalar(float(raw))
|
|
263
|
+
elif node.kind == "variable":
|
|
264
|
+
data = self.model._variables[node.payload]
|
|
265
|
+
start, _ = self.offsets[node.payload]
|
|
266
|
+
out = np.empty(data.shape if data.shape else (), dtype=object)
|
|
267
|
+
if data.shape:
|
|
268
|
+
for local, idx in enumerate(np.ndindex(data.shape)):
|
|
269
|
+
out[idx] = _Polynomial.variable(start + local)
|
|
270
|
+
else:
|
|
271
|
+
out[()] = _Polynomial.variable(start)
|
|
272
|
+
elif node.kind in {"add", "mul"}:
|
|
273
|
+
a = self.evaluate(node.args[0])
|
|
274
|
+
b = self.evaluate(node.args[1])
|
|
275
|
+
aa, bb = np.broadcast_arrays(a, b)
|
|
276
|
+
out = np.empty(aa.shape, dtype=object)
|
|
277
|
+
for idx in np.ndindex(aa.shape):
|
|
278
|
+
out[idx] = aa[idx] + bb[idx] if node.kind == "add" else aa[idx] * bb[idx]
|
|
279
|
+
elif node.kind == "neg":
|
|
280
|
+
a = self.evaluate(node.args[0])
|
|
281
|
+
out = np.empty(a.shape, dtype=object)
|
|
282
|
+
if a.shape:
|
|
283
|
+
for idx in np.ndindex(a.shape):
|
|
284
|
+
out[idx] = -a[idx]
|
|
285
|
+
else:
|
|
286
|
+
out[()] = -a[()]
|
|
287
|
+
elif node.kind == "matmul":
|
|
288
|
+
a = self.evaluate(node.args[0])
|
|
289
|
+
b = self.evaluate(node.args[1])
|
|
290
|
+
try:
|
|
291
|
+
out = np.matmul(a, b)
|
|
292
|
+
except ValueError as exc:
|
|
293
|
+
raise ShapeError("matmul failed during scalarization") from exc
|
|
294
|
+
out = np.asarray(out, dtype=object)
|
|
295
|
+
elif node.kind == "index":
|
|
296
|
+
out = np.asarray(self.evaluate(node.args[0])[node.payload], dtype=object)
|
|
297
|
+
elif node.kind == "transpose":
|
|
298
|
+
out = np.asarray(self.evaluate(node.args[0]).T, dtype=object)
|
|
299
|
+
elif node.kind == "sum":
|
|
300
|
+
a = self.evaluate(node.args[0])
|
|
301
|
+
out = np.asarray(np.sum(a, axis=node.payload), dtype=object)
|
|
302
|
+
elif node.kind == "concat":
|
|
303
|
+
pieces = [np.asarray(self.evaluate(arg), dtype=object).reshape(-1) for arg in node.args]
|
|
304
|
+
out = np.concatenate(pieces) if pieces else np.asarray([], dtype=object)
|
|
305
|
+
else:
|
|
306
|
+
raise CompileError(f"unsupported symbolic expression node {node.kind!r}")
|
|
307
|
+
self._cache[cache_key] = out
|
|
308
|
+
return out
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _scalar_polynomial(arr: np.ndarray, *, context: str) -> _Polynomial:
|
|
312
|
+
arr = np.asarray(arr, dtype=object)
|
|
313
|
+
if arr.shape != ():
|
|
314
|
+
raise CompileError(f"{context} must be scalar, got shape {arr.shape}")
|
|
315
|
+
value = arr[()]
|
|
316
|
+
if not isinstance(value, _Polynomial):
|
|
317
|
+
raise CompileError(f"{context} did not scalarize to a polynomial")
|
|
318
|
+
return value
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _structural_target(model: Model) -> str:
|
|
322
|
+
if model.objective is None:
|
|
323
|
+
raise CompileError("model has no objective")
|
|
324
|
+
has_indicator = False
|
|
325
|
+
for constraint in model.constraints:
|
|
326
|
+
degree = constraint.function.polynomial_degree
|
|
327
|
+
if degree is None or degree > 1:
|
|
328
|
+
raise CompileError(f"quadratic constraint {constraint.entity_id} is outside P4 scope")
|
|
329
|
+
has_indicator = has_indicator or isinstance(constraint, IndicatorConstraint)
|
|
330
|
+
degree = model.objective.expression.polynomial_degree
|
|
331
|
+
if degree is None or degree > 2:
|
|
332
|
+
raise CompileError("objective is outside quadratic P4 scope")
|
|
333
|
+
if has_indicator:
|
|
334
|
+
if degree > 1:
|
|
335
|
+
raise CompileError("P4 indicator lowering currently targets legacy MILP and does not support quadratic objectives")
|
|
336
|
+
return "legacy-linear-v1"
|
|
337
|
+
if degree <= 1:
|
|
338
|
+
return "legacy-linear-v1"
|
|
339
|
+
if model.objective.sense is ObjectiveSense.MAXIMIZE:
|
|
340
|
+
raise CompileError("P4 quadratic compilation supports convex minimization only")
|
|
341
|
+
if any(data.domain is not VariableDomain.CONTINUOUS for data in model._variables.values()):
|
|
342
|
+
raise CompileError("P4 quadratic compilation supports continuous variables only")
|
|
343
|
+
return "legacy-qp-v1"
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _compilation_hash(model: Model, target: str) -> str:
|
|
347
|
+
# P0 RFC-004: CompilationHash is plan identity, not execution-data identity.
|
|
348
|
+
payload = {
|
|
349
|
+
"semantic_hash": model.semantic_hash,
|
|
350
|
+
"target": target,
|
|
351
|
+
"capability_signature": _CAPABILITY_SIGNATURE,
|
|
352
|
+
"numerical_policy": _NUMERICAL_POLICY,
|
|
353
|
+
"transformation_tape": [],
|
|
354
|
+
"precondition_certificate_hashes": [],
|
|
355
|
+
}
|
|
356
|
+
raw = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
|
357
|
+
return hashlib.sha256(raw).hexdigest()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _variable_layout(model: Model) -> tuple[dict[str, tuple[int, int]], int, np.ndarray, np.ndarray, tuple[str, ...], dict[str, Any]]:
|
|
361
|
+
offsets: dict[str, tuple[int, int]] = {}
|
|
362
|
+
n = 0
|
|
363
|
+
var_lower: list[float] = []
|
|
364
|
+
var_upper: list[float] = []
|
|
365
|
+
domains: list[str] = []
|
|
366
|
+
source_vars: dict[str, Any] = {}
|
|
367
|
+
for key, data in model._variables.items():
|
|
368
|
+
size = int(np.prod(data.shape, dtype=np.int64)) if data.shape else 1
|
|
369
|
+
offsets[key] = (n, n + size)
|
|
370
|
+
source_vars[key] = {"execution_slice": [n, n + size], "shape": list(data.shape)}
|
|
371
|
+
var_lower.extend(np.asarray(data.lower).reshape(-1).tolist())
|
|
372
|
+
var_upper.extend(np.asarray(data.upper).reshape(-1).tolist())
|
|
373
|
+
domains.extend([data.domain.value] * size)
|
|
374
|
+
n += size
|
|
375
|
+
return (
|
|
376
|
+
offsets,
|
|
377
|
+
n,
|
|
378
|
+
np.asarray(var_lower, dtype=np.float64),
|
|
379
|
+
np.asarray(var_upper, dtype=np.float64),
|
|
380
|
+
tuple(domains),
|
|
381
|
+
source_vars,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _constraint_block(evaluator: _Evaluator, constraint: Constraint) -> tuple[list[dict[int, float]], list[float], list[float]]:
|
|
386
|
+
arr = np.asarray(evaluator.evaluate(constraint.function._node), dtype=object)
|
|
387
|
+
rows: list[dict[int, float]] = []
|
|
388
|
+
cl: list[float] = []
|
|
389
|
+
cu: list[float] = []
|
|
390
|
+
indices = list(np.ndindex(arr.shape)) if arr.shape else [()]
|
|
391
|
+
for idx in indices:
|
|
392
|
+
poly = arr[idx]
|
|
393
|
+
if not isinstance(poly, _Polynomial) or poly.degree > 1:
|
|
394
|
+
raise CompileError(f"quadratic constraint {constraint.entity_id} is outside P2 scope")
|
|
395
|
+
rows.append(dict(poly.linear))
|
|
396
|
+
if isinstance(constraint.set, LessThan):
|
|
397
|
+
cl.append(-np.inf)
|
|
398
|
+
cu.append(float(constraint.set.upper) - poly.constant)
|
|
399
|
+
elif isinstance(constraint.set, GreaterThan):
|
|
400
|
+
cl.append(float(constraint.set.lower) - poly.constant)
|
|
401
|
+
cu.append(np.inf)
|
|
402
|
+
elif isinstance(constraint.set, EqualTo):
|
|
403
|
+
rhs = float(constraint.set.value) - poly.constant
|
|
404
|
+
cl.append(rhs)
|
|
405
|
+
cu.append(rhs)
|
|
406
|
+
elif isinstance(constraint.set, Interval):
|
|
407
|
+
cl.append(float(constraint.set.lower) - poly.constant)
|
|
408
|
+
cu.append(float(constraint.set.upper) - poly.constant)
|
|
409
|
+
else:
|
|
410
|
+
raise CompileError(f"unsupported constraint set {type(constraint.set).__name__}")
|
|
411
|
+
return rows, cl, cu
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _rows_to_csr(rows: list[dict[int, float]], n: int) -> sparse.csr_matrix:
|
|
415
|
+
if not rows:
|
|
416
|
+
return sparse.csr_matrix((0, n), dtype=np.float64)
|
|
417
|
+
rr: list[int] = []
|
|
418
|
+
cc: list[int] = []
|
|
419
|
+
vv: list[float] = []
|
|
420
|
+
for r, coeffs in enumerate(rows):
|
|
421
|
+
for c, v in coeffs.items():
|
|
422
|
+
if v != 0.0:
|
|
423
|
+
rr.append(r)
|
|
424
|
+
cc.append(c)
|
|
425
|
+
vv.append(float(v))
|
|
426
|
+
return sparse.csr_matrix((vv, (rr, cc)), shape=(len(rows), n), dtype=np.float64)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _objective_data(evaluator: _Evaluator, model: Model, n: int, target: str) -> tuple[np.ndarray, float, sparse.csr_matrix | None]:
|
|
430
|
+
if model.objective is None:
|
|
431
|
+
raise CompileError("model objective is required before objective compilation")
|
|
432
|
+
obj = _scalar_polynomial(evaluator.evaluate(model.objective.expression._node), context="objective")
|
|
433
|
+
c = np.zeros(n, dtype=np.float64)
|
|
434
|
+
for i, v in obj.linear.items():
|
|
435
|
+
c[i] = v
|
|
436
|
+
if target == "legacy-linear-v1":
|
|
437
|
+
# Structural target is authoritative. A structural affine objective cannot
|
|
438
|
+
# become quadratic solely because parameter values change.
|
|
439
|
+
if obj.quadratic:
|
|
440
|
+
raise CompileError("affine semantic objective evaluated to quadratic data")
|
|
441
|
+
return c, obj.constant, None
|
|
442
|
+
|
|
443
|
+
pr: list[int] = []
|
|
444
|
+
pc: list[int] = []
|
|
445
|
+
pv: list[float] = []
|
|
446
|
+
for (i, j), q in obj.quadratic.items():
|
|
447
|
+
if i == j:
|
|
448
|
+
val = 2.0 * q
|
|
449
|
+
if val != 0.0:
|
|
450
|
+
pr.append(i)
|
|
451
|
+
pc.append(i)
|
|
452
|
+
pv.append(val)
|
|
453
|
+
elif q != 0.0:
|
|
454
|
+
pr.extend([i, j])
|
|
455
|
+
pc.extend([j, i])
|
|
456
|
+
pv.extend([q, q])
|
|
457
|
+
P = sparse.csr_matrix((pv, (pr, pc)), shape=(n, n), dtype=np.float64)
|
|
458
|
+
return c, obj.constant, P
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _make_metadata(model: Model, *, compiled_by: str = "solverpilot-p2", compiler_policy: str = _NUMERICAL_POLICY) -> dict[str, Any]:
|
|
462
|
+
return {
|
|
463
|
+
"semantic_hash": model.semantic_hash,
|
|
464
|
+
"semantic_model_schema": "solverpilot.semantic-model.p1.v1",
|
|
465
|
+
"compiled_by": compiled_by,
|
|
466
|
+
"compiler_policy": compiler_policy,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _build_execution_ir(
|
|
471
|
+
*,
|
|
472
|
+
model: Model,
|
|
473
|
+
target: str,
|
|
474
|
+
A: sparse.csr_matrix,
|
|
475
|
+
c: np.ndarray,
|
|
476
|
+
P: sparse.csr_matrix | None,
|
|
477
|
+
variable_lower: np.ndarray,
|
|
478
|
+
variable_upper: np.ndarray,
|
|
479
|
+
constraint_lower: np.ndarray,
|
|
480
|
+
constraint_upper: np.ndarray,
|
|
481
|
+
domains: tuple[str, ...],
|
|
482
|
+
objective_offset: float,
|
|
483
|
+
previous: LinearProblem | QuadraticProblem | None = None,
|
|
484
|
+
objective_recompiled: bool = True,
|
|
485
|
+
metadata: dict[str, Any] | None = None,
|
|
486
|
+
) -> LinearProblem | QuadraticProblem:
|
|
487
|
+
if model.objective is None:
|
|
488
|
+
raise CompileError("model objective is required before execution IR construction")
|
|
489
|
+
metadata = _make_metadata(model) if metadata is None else metadata
|
|
490
|
+
if target == "legacy-linear-v1":
|
|
491
|
+
return LinearProblem.from_data(
|
|
492
|
+
A=A,
|
|
493
|
+
c=c,
|
|
494
|
+
variable_lower=variable_lower,
|
|
495
|
+
variable_upper=variable_upper,
|
|
496
|
+
constraint_lower=constraint_lower,
|
|
497
|
+
constraint_upper=constraint_upper,
|
|
498
|
+
domains=domains,
|
|
499
|
+
objective_sense=model.objective.sense,
|
|
500
|
+
objective_offset=objective_offset,
|
|
501
|
+
name=model.name,
|
|
502
|
+
metadata=metadata,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
if P is None:
|
|
506
|
+
raise CompileError("P2 QP target requires a quadratic matrix")
|
|
507
|
+
# If the quadratic objective was not touched, preserve the already verified P
|
|
508
|
+
# and convexity status to avoid a redundant eigenvalue check.
|
|
509
|
+
if isinstance(previous, QuadraticProblem) and not objective_recompiled and P is previous.P:
|
|
510
|
+
linear = LinearProblem.from_data(
|
|
511
|
+
A=A,
|
|
512
|
+
c=c,
|
|
513
|
+
variable_lower=variable_lower,
|
|
514
|
+
variable_upper=variable_upper,
|
|
515
|
+
constraint_lower=constraint_lower,
|
|
516
|
+
constraint_upper=constraint_upper,
|
|
517
|
+
objective_sense=model.objective.sense,
|
|
518
|
+
objective_offset=objective_offset,
|
|
519
|
+
name=model.name,
|
|
520
|
+
metadata=metadata,
|
|
521
|
+
)
|
|
522
|
+
return QuadraticProblem(
|
|
523
|
+
linear=linear,
|
|
524
|
+
P=previous.P,
|
|
525
|
+
convexity_status=previous.convexity_status,
|
|
526
|
+
metadata=metadata,
|
|
527
|
+
)
|
|
528
|
+
return QuadraticProblem.from_data(
|
|
529
|
+
P=P,
|
|
530
|
+
A=A,
|
|
531
|
+
q=c,
|
|
532
|
+
variable_lower=variable_lower,
|
|
533
|
+
variable_upper=variable_upper,
|
|
534
|
+
constraint_lower=constraint_lower,
|
|
535
|
+
constraint_upper=constraint_upper,
|
|
536
|
+
objective_sense=model.objective.sense,
|
|
537
|
+
objective_offset=objective_offset,
|
|
538
|
+
verify_convexity=True,
|
|
539
|
+
metadata=metadata,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _build_template(model: Model, target: str, offsets: dict[str, tuple[int, int]], n: int, vl: np.ndarray, vu: np.ndarray, domains: tuple[str, ...], source_vars: dict[str, Any], source_constraints: dict[str, Any]) -> _Template:
|
|
544
|
+
constraint_by_id = {c.entity_id.value: c for c in model.constraints}
|
|
545
|
+
constraint_dependencies = {
|
|
546
|
+
cid: frozenset(c.function.parameter_dependencies)
|
|
547
|
+
for cid, c in constraint_by_id.items()
|
|
548
|
+
}
|
|
549
|
+
objective_dependencies = frozenset(model.objective.expression.parameter_dependencies if model.objective is not None else ())
|
|
550
|
+
parameter_to_constraints: dict[str, tuple[str, ...]] = {}
|
|
551
|
+
parameter_affects_objective: dict[str, bool] = {}
|
|
552
|
+
for pid in model._parameters:
|
|
553
|
+
parameter_to_constraints[pid] = tuple(
|
|
554
|
+
cid for cid, deps in constraint_dependencies.items() if pid in deps
|
|
555
|
+
)
|
|
556
|
+
parameter_affects_objective[pid] = pid in objective_dependencies
|
|
557
|
+
source_map = {
|
|
558
|
+
"variables": source_vars,
|
|
559
|
+
"constraints": source_constraints,
|
|
560
|
+
"objective": {} if model.objective is None else {model.objective.entity_id.value: {"execution_objective": True}},
|
|
561
|
+
}
|
|
562
|
+
return _Template(
|
|
563
|
+
semantic_hash=model.semantic_hash,
|
|
564
|
+
target=target,
|
|
565
|
+
offsets=offsets,
|
|
566
|
+
n_variables=n,
|
|
567
|
+
variable_lower=vl,
|
|
568
|
+
variable_upper=vu,
|
|
569
|
+
domains=domains,
|
|
570
|
+
source_map=source_map,
|
|
571
|
+
constraint_by_id=constraint_by_id,
|
|
572
|
+
constraint_dependencies=constraint_dependencies,
|
|
573
|
+
objective_dependencies=objective_dependencies,
|
|
574
|
+
parameter_to_constraints=parameter_to_constraints,
|
|
575
|
+
parameter_affects_objective=parameter_affects_objective,
|
|
576
|
+
compilation_hash=_compilation_hash(model, target),
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _parameter_map(model: Model, template: _Template) -> dict[str, Any]:
|
|
581
|
+
out: dict[str, Any] = {}
|
|
582
|
+
for pid, data in model._parameters.items():
|
|
583
|
+
constraints = list(template.parameter_to_constraints.get(pid, ()))
|
|
584
|
+
objective = bool(template.parameter_affects_objective.get(pid, False))
|
|
585
|
+
regions = [f"constraint:{cid}" for cid in constraints]
|
|
586
|
+
if objective:
|
|
587
|
+
regions.append("objective")
|
|
588
|
+
out[pid] = {
|
|
589
|
+
"shape": list(data.shape),
|
|
590
|
+
"patchable": bool(regions),
|
|
591
|
+
"parameter_compile_class": "partial_recompile_p2" if regions else "unused_parameter_p2",
|
|
592
|
+
"plan_sensitive": False,
|
|
593
|
+
"constraint_ids": constraints,
|
|
594
|
+
"objective": objective,
|
|
595
|
+
"execution_regions": regions,
|
|
596
|
+
}
|
|
597
|
+
return out
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _mutation_plan(changed_parameters: tuple[str, ...], parameter_map: dict[str, Any], mutations: tuple[str, ...]) -> dict[str, Any]:
|
|
601
|
+
affected_regions: list[str] = []
|
|
602
|
+
for pid in changed_parameters:
|
|
603
|
+
for region in parameter_map.get(pid, {}).get("execution_regions", []):
|
|
604
|
+
if region not in affected_regions:
|
|
605
|
+
affected_regions.append(region)
|
|
606
|
+
return {
|
|
607
|
+
"mode": "partial_recompile_p2",
|
|
608
|
+
"incremental_parameter_patching": True,
|
|
609
|
+
"backend_session_patch": False,
|
|
610
|
+
"changed_parameters": list(changed_parameters),
|
|
611
|
+
"affected_regions": affected_regions,
|
|
612
|
+
"execution_mutations": list(mutations),
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _full_compile(model: Model, *, cache_status: str) -> tuple[CompiledModel, _Template]:
|
|
617
|
+
total_t0 = perf_counter()
|
|
618
|
+
target = _structural_target(model)
|
|
619
|
+
analysis_t0 = perf_counter()
|
|
620
|
+
offsets, n, vl, vu, domains, source_vars = _variable_layout(model)
|
|
621
|
+
semantic_analysis_s = perf_counter() - analysis_t0
|
|
622
|
+
|
|
623
|
+
lower_t0 = perf_counter()
|
|
624
|
+
evaluator = _Evaluator(model, offsets)
|
|
625
|
+
rows: list[dict[int, float]] = []
|
|
626
|
+
cl: list[float] = []
|
|
627
|
+
cu: list[float] = []
|
|
628
|
+
source_constraints: dict[str, Any] = {}
|
|
629
|
+
for constraint in model.constraints:
|
|
630
|
+
block_rows, block_cl, block_cu = _constraint_block(evaluator, constraint)
|
|
631
|
+
start = len(rows)
|
|
632
|
+
rows.extend(block_rows)
|
|
633
|
+
cl.extend(block_cl)
|
|
634
|
+
cu.extend(block_cu)
|
|
635
|
+
source_constraints[constraint.entity_id.value] = {
|
|
636
|
+
"execution_rows": list(range(start, len(rows))),
|
|
637
|
+
"shape": list(constraint.function.shape),
|
|
638
|
+
}
|
|
639
|
+
A = _rows_to_csr(rows, n)
|
|
640
|
+
c, offset, P = _objective_data(evaluator, model, n, target)
|
|
641
|
+
ir = _build_execution_ir(
|
|
642
|
+
model=model,
|
|
643
|
+
target=target,
|
|
644
|
+
A=A,
|
|
645
|
+
c=c,
|
|
646
|
+
P=P,
|
|
647
|
+
variable_lower=vl,
|
|
648
|
+
variable_upper=vu,
|
|
649
|
+
constraint_lower=np.asarray(cl, dtype=np.float64),
|
|
650
|
+
constraint_upper=np.asarray(cu, dtype=np.float64),
|
|
651
|
+
domains=domains,
|
|
652
|
+
objective_offset=offset,
|
|
653
|
+
previous=None,
|
|
654
|
+
objective_recompiled=True,
|
|
655
|
+
)
|
|
656
|
+
lowering_s = perf_counter() - lower_t0
|
|
657
|
+
template = _build_template(model, target, offsets, n, vl, vu, domains, source_vars, source_constraints)
|
|
658
|
+
pmap = _parameter_map(model, template)
|
|
659
|
+
report = CompilationReport(
|
|
660
|
+
target=target,
|
|
661
|
+
semantic_analysis_s=semantic_analysis_s,
|
|
662
|
+
lowering_s=lowering_s,
|
|
663
|
+
total_s=perf_counter() - total_t0,
|
|
664
|
+
n_semantic_variables=len(model._variables),
|
|
665
|
+
n_execution_variables=n,
|
|
666
|
+
n_semantic_constraints=len(model._constraints),
|
|
667
|
+
n_execution_constraints=len(rows),
|
|
668
|
+
cache_status=cache_status,
|
|
669
|
+
reused_execution_rows=0,
|
|
670
|
+
recompiled_execution_rows=len(rows),
|
|
671
|
+
objective_recompiled=True,
|
|
672
|
+
)
|
|
673
|
+
compiled = CompiledModel(
|
|
674
|
+
execution_ir=ir,
|
|
675
|
+
semantic_hash=model.semantic_hash,
|
|
676
|
+
data_hash=model.data_hash,
|
|
677
|
+
compilation_hash=template.compilation_hash,
|
|
678
|
+
parameter_map=pmap,
|
|
679
|
+
source_map=template.source_map,
|
|
680
|
+
transformation_tape=(),
|
|
681
|
+
capability_signature=_CAPABILITY_SIGNATURE,
|
|
682
|
+
mutation_plan=_mutation_plan((), pmap, ()),
|
|
683
|
+
plan_sensitive_dependencies=(),
|
|
684
|
+
precondition_certificate_hashes=(),
|
|
685
|
+
compilation_report=report,
|
|
686
|
+
reconstruction_contract={"primal": "identity_by_source_map", "dual": "legacy_row_mapping", "certificate": "none_p2"},
|
|
687
|
+
)
|
|
688
|
+
return compiled, template
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _changed_parameters(model: Model, entry: _CacheEntry) -> tuple[str, ...]:
|
|
692
|
+
return tuple(
|
|
693
|
+
pid for pid in model._parameters
|
|
694
|
+
if model._parameter_versions.get(pid, 0) != entry.parameter_versions.get(pid, 0)
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _snapshot_hit(snapshot: CompiledModel, changed: tuple[str, ...]) -> CompiledModel:
|
|
699
|
+
report = replace(
|
|
700
|
+
snapshot.compilation_report,
|
|
701
|
+
semantic_analysis_s=0.0,
|
|
702
|
+
lowering_s=0.0,
|
|
703
|
+
total_s=0.0,
|
|
704
|
+
cache_status="snapshot_hit",
|
|
705
|
+
changed_parameters=changed,
|
|
706
|
+
reused_execution_rows=snapshot.compilation_report.n_execution_constraints,
|
|
707
|
+
recompiled_execution_rows=0,
|
|
708
|
+
objective_recompiled=False,
|
|
709
|
+
execution_mutations=(),
|
|
710
|
+
)
|
|
711
|
+
return replace(snapshot, compilation_report=report)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _incremental_compile(model: Model, entry: _CacheEntry, changed: tuple[str, ...]) -> CompiledModel:
|
|
715
|
+
total_t0 = perf_counter()
|
|
716
|
+
template = entry.template
|
|
717
|
+
old = entry.current.execution_ir
|
|
718
|
+
old_linear = old.linear if isinstance(old, QuadraticProblem) else old
|
|
719
|
+
affected_constraints: set[str] = set()
|
|
720
|
+
objective_recompiled = False
|
|
721
|
+
for pid in changed:
|
|
722
|
+
affected_constraints.update(template.parameter_to_constraints.get(pid, ()))
|
|
723
|
+
objective_recompiled = objective_recompiled or template.parameter_affects_objective.get(pid, False)
|
|
724
|
+
|
|
725
|
+
lower_t0 = perf_counter()
|
|
726
|
+
A_lil = old_linear.A.tolil(copy=True)
|
|
727
|
+
cl = np.array(old_linear.constraint_lower, copy=True)
|
|
728
|
+
cu = np.array(old_linear.constraint_upper, copy=True)
|
|
729
|
+
evaluator = _Evaluator(model, template.offsets)
|
|
730
|
+
recompiled_rows = 0
|
|
731
|
+
for cid in sorted(affected_constraints):
|
|
732
|
+
constraint = template.constraint_by_id[cid]
|
|
733
|
+
block_rows, block_cl, block_cu = _constraint_block(evaluator, constraint)
|
|
734
|
+
execution_rows = template.source_map["constraints"][cid]["execution_rows"]
|
|
735
|
+
if len(block_rows) != len(execution_rows):
|
|
736
|
+
raise CompileError("parameter update changed execution row cardinality; full semantic recompile required")
|
|
737
|
+
for local, row_index in enumerate(execution_rows):
|
|
738
|
+
coeffs = block_rows[local]
|
|
739
|
+
cols = sorted(k for k, v in coeffs.items() if v != 0.0)
|
|
740
|
+
A_lil.rows[row_index] = cols
|
|
741
|
+
A_lil.data[row_index] = [float(coeffs[k]) for k in cols]
|
|
742
|
+
cl[row_index] = block_cl[local]
|
|
743
|
+
cu[row_index] = block_cu[local]
|
|
744
|
+
recompiled_rows += 1
|
|
745
|
+
A = A_lil.tocsr()
|
|
746
|
+
|
|
747
|
+
if isinstance(old, QuadraticProblem):
|
|
748
|
+
old_c = old.linear.c
|
|
749
|
+
old_offset = old.linear.objective_offset
|
|
750
|
+
old_P: sparse.csr_matrix | None = old.P
|
|
751
|
+
else:
|
|
752
|
+
old_c = old.c
|
|
753
|
+
old_offset = old.objective_offset
|
|
754
|
+
old_P = None
|
|
755
|
+
|
|
756
|
+
if objective_recompiled:
|
|
757
|
+
c, offset, P = _objective_data(evaluator, model, template.n_variables, template.target)
|
|
758
|
+
else:
|
|
759
|
+
c = old_c
|
|
760
|
+
offset = old_offset
|
|
761
|
+
P = old_P
|
|
762
|
+
|
|
763
|
+
new_ir = _build_execution_ir(
|
|
764
|
+
model=model,
|
|
765
|
+
target=template.target,
|
|
766
|
+
A=A,
|
|
767
|
+
c=c,
|
|
768
|
+
P=P,
|
|
769
|
+
variable_lower=template.variable_lower,
|
|
770
|
+
variable_upper=template.variable_upper,
|
|
771
|
+
constraint_lower=cl,
|
|
772
|
+
constraint_upper=cu,
|
|
773
|
+
domains=template.domains,
|
|
774
|
+
objective_offset=offset,
|
|
775
|
+
previous=old,
|
|
776
|
+
objective_recompiled=objective_recompiled,
|
|
777
|
+
)
|
|
778
|
+
lowering_s = perf_counter() - lower_t0
|
|
779
|
+
record = classify_mutations(old, new_ir, revision_before=0, revision_after=1)
|
|
780
|
+
mutations = tuple(kind.value for kind in record.kinds)
|
|
781
|
+
pmap = _parameter_map(model, template)
|
|
782
|
+
report = CompilationReport(
|
|
783
|
+
target=template.target,
|
|
784
|
+
semantic_analysis_s=0.0,
|
|
785
|
+
lowering_s=lowering_s,
|
|
786
|
+
total_s=perf_counter() - total_t0,
|
|
787
|
+
n_semantic_variables=len(model._variables),
|
|
788
|
+
n_execution_variables=template.n_variables,
|
|
789
|
+
n_semantic_constraints=len(model._constraints),
|
|
790
|
+
n_execution_constraints=old_linear.n_constraints,
|
|
791
|
+
cache_status="partial_recompile",
|
|
792
|
+
changed_parameters=changed,
|
|
793
|
+
reused_execution_rows=old_linear.n_constraints - recompiled_rows,
|
|
794
|
+
recompiled_execution_rows=recompiled_rows,
|
|
795
|
+
objective_recompiled=objective_recompiled,
|
|
796
|
+
execution_mutations=mutations,
|
|
797
|
+
)
|
|
798
|
+
return CompiledModel(
|
|
799
|
+
execution_ir=new_ir,
|
|
800
|
+
semantic_hash=model.semantic_hash,
|
|
801
|
+
data_hash=model.data_hash,
|
|
802
|
+
compilation_hash=template.compilation_hash,
|
|
803
|
+
parameter_map=pmap,
|
|
804
|
+
source_map=template.source_map,
|
|
805
|
+
transformation_tape=(),
|
|
806
|
+
capability_signature=_CAPABILITY_SIGNATURE,
|
|
807
|
+
mutation_plan=_mutation_plan(changed, pmap, mutations),
|
|
808
|
+
plan_sensitive_dependencies=(),
|
|
809
|
+
precondition_certificate_hashes=(),
|
|
810
|
+
compilation_report=report,
|
|
811
|
+
reconstruction_contract={"primal": "identity_by_source_map", "dual": "legacy_row_mapping", "certificate": "none_p2"},
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _remember_snapshot(entry: _CacheEntry, compiled: CompiledModel) -> None:
|
|
816
|
+
entry.snapshots[compiled.data_hash] = compiled
|
|
817
|
+
entry.snapshots.move_to_end(compiled.data_hash)
|
|
818
|
+
while len(entry.snapshots) > _MAX_SNAPSHOTS:
|
|
819
|
+
entry.snapshots.popitem(last=False)
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
@dataclass(slots=True)
|
|
824
|
+
class _BridgeCacheEntry:
|
|
825
|
+
semantic_hash: str
|
|
826
|
+
current: CompiledModel
|
|
827
|
+
parameter_versions: dict[str, int]
|
|
828
|
+
snapshots: OrderedDict[str, CompiledModel]
|
|
829
|
+
full_compiles: int = 1
|
|
830
|
+
snapshot_hits: int = 0
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _p4_capability_signature(capabilities: BackendCapabilityManifestV2 | None) -> str:
|
|
834
|
+
if capabilities is None:
|
|
835
|
+
return _P4_CAPABILITY_SIGNATURE
|
|
836
|
+
return f"backend-capability-v2:{capabilities.signature}:legacy-indicator-transport=0"
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _p4_compilation_hash(
|
|
840
|
+
model: Model,
|
|
841
|
+
target: str,
|
|
842
|
+
*,
|
|
843
|
+
capability_signature: str,
|
|
844
|
+
bridge_policy: BridgePolicy,
|
|
845
|
+
tape: TransformationTape,
|
|
846
|
+
) -> str:
|
|
847
|
+
payload = {
|
|
848
|
+
"semantic_hash": model.semantic_hash,
|
|
849
|
+
"target": target,
|
|
850
|
+
"capability_signature": capability_signature,
|
|
851
|
+
"numerical_policy": _P4_NUMERICAL_POLICY,
|
|
852
|
+
"bridge_policy": bridge_policy.policy_version,
|
|
853
|
+
"transformation_tape": tape.to_dicts(),
|
|
854
|
+
"precondition_certificate_hashes": list(tape.certificate_hashes),
|
|
855
|
+
}
|
|
856
|
+
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()
|
|
857
|
+
return hashlib.sha256(raw).hexdigest()
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def _p4_parameter_map(model: Model, tape: TransformationTape) -> dict[str, Any]:
|
|
861
|
+
plan_sensitive = set(tape.plan_sensitive_dependencies)
|
|
862
|
+
out: dict[str, Any] = {}
|
|
863
|
+
for pid, data in model._parameters.items():
|
|
864
|
+
constraint_ids = [
|
|
865
|
+
c.entity_id.value for c in model.constraints
|
|
866
|
+
if pid in c.function.parameter_dependencies
|
|
867
|
+
]
|
|
868
|
+
objective = bool(model.objective is not None and pid in model.objective.expression.parameter_dependencies)
|
|
869
|
+
regions = [f"constraint:{cid}" for cid in constraint_ids]
|
|
870
|
+
if objective:
|
|
871
|
+
regions.append("objective")
|
|
872
|
+
sensitive = pid in plan_sensitive
|
|
873
|
+
out[pid] = {
|
|
874
|
+
"shape": list(data.shape),
|
|
875
|
+
"patchable": bool(regions) and not sensitive,
|
|
876
|
+
"parameter_compile_class": "plan_sensitive_full_recompile_p4" if sensitive else "partial_recompile_p2",
|
|
877
|
+
"plan_sensitive": sensitive,
|
|
878
|
+
"constraint_ids": constraint_ids,
|
|
879
|
+
"objective": objective,
|
|
880
|
+
"execution_regions": regions,
|
|
881
|
+
}
|
|
882
|
+
return out
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _indicator_block_p4(
|
|
886
|
+
evaluator: _Evaluator,
|
|
887
|
+
constraint: IndicatorConstraint,
|
|
888
|
+
*,
|
|
889
|
+
offsets: dict[str, tuple[int, int]],
|
|
890
|
+
variable_lower: np.ndarray,
|
|
891
|
+
variable_upper: np.ndarray,
|
|
892
|
+
domains: tuple[str, ...],
|
|
893
|
+
capabilities: BackendCapabilityManifestV2 | None,
|
|
894
|
+
bridge_policy: BridgePolicy,
|
|
895
|
+
):
|
|
896
|
+
arr = np.asarray(evaluator.evaluate(constraint.function._node), dtype=object)
|
|
897
|
+
poly = _scalar_polynomial(arr, context=f"indicator body {constraint.entity_id.value}")
|
|
898
|
+
if poly.degree > 1:
|
|
899
|
+
raise CompileError("P4 indicator body must be affine")
|
|
900
|
+
start, end = offsets[constraint.indicator.id.value]
|
|
901
|
+
if end - start != 1:
|
|
902
|
+
raise CompileError("P4 indicator variable must lower to exactly one execution variable")
|
|
903
|
+
z_index = start
|
|
904
|
+
decision = choose_indicator_path(
|
|
905
|
+
capabilities,
|
|
906
|
+
native_transport_available=False,
|
|
907
|
+
policy=bridge_policy,
|
|
908
|
+
)
|
|
909
|
+
try:
|
|
910
|
+
lowered = lower_indicator_big_m(
|
|
911
|
+
source_id=constraint.entity_id.value,
|
|
912
|
+
indicator_index=z_index,
|
|
913
|
+
active_value=constraint.active_value,
|
|
914
|
+
indicator_domain=VariableDomain(domains[z_index]),
|
|
915
|
+
indicator_lower=float(variable_lower[z_index]),
|
|
916
|
+
indicator_upper=float(variable_upper[z_index]),
|
|
917
|
+
body=AffineScalar(poly.constant, dict(poly.linear)),
|
|
918
|
+
body_set=constraint.set,
|
|
919
|
+
variable_lower=variable_lower,
|
|
920
|
+
variable_upper=variable_upper,
|
|
921
|
+
parameter_dependencies=tuple(sorted(constraint.function.parameter_dependencies)),
|
|
922
|
+
policy=bridge_policy,
|
|
923
|
+
)
|
|
924
|
+
except BridgeUnavailable as exc:
|
|
925
|
+
raise CompileError(f"indicator bridge unavailable for {constraint.entity_id.value}: {exc}") from exc
|
|
926
|
+
return lowered, decision
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def _full_compile_p4(
|
|
930
|
+
model: Model,
|
|
931
|
+
*,
|
|
932
|
+
cache_status: str,
|
|
933
|
+
changed_parameters: tuple[str, ...],
|
|
934
|
+
bridge_policy: BridgePolicy,
|
|
935
|
+
capabilities: BackendCapabilityManifestV2 | None,
|
|
936
|
+
) -> CompiledModel:
|
|
937
|
+
total_t0 = perf_counter()
|
|
938
|
+
target = _structural_target(model)
|
|
939
|
+
if target != "legacy-linear-v1":
|
|
940
|
+
raise CompileError("P4 bridge compiler currently targets legacy linear/MILP execution IR")
|
|
941
|
+
analysis_t0 = perf_counter()
|
|
942
|
+
offsets, n, vl, vu, domains, source_vars = _variable_layout(model)
|
|
943
|
+
semantic_analysis_s = perf_counter() - analysis_t0
|
|
944
|
+
|
|
945
|
+
lower_t0 = perf_counter()
|
|
946
|
+
evaluator = _Evaluator(model, offsets)
|
|
947
|
+
rows: list[dict[int, float]] = []
|
|
948
|
+
cl: list[float] = []
|
|
949
|
+
cu: list[float] = []
|
|
950
|
+
source_constraints: dict[str, Any] = {}
|
|
951
|
+
tape_steps = []
|
|
952
|
+
rejected_paths: list[str] = []
|
|
953
|
+
bridge_search_s = 0.0
|
|
954
|
+
for constraint in model.constraints:
|
|
955
|
+
start_row = len(rows)
|
|
956
|
+
if isinstance(constraint, IndicatorConstraint):
|
|
957
|
+
bt0 = perf_counter()
|
|
958
|
+
lowered, decision = _indicator_block_p4(
|
|
959
|
+
evaluator,
|
|
960
|
+
constraint,
|
|
961
|
+
offsets=offsets,
|
|
962
|
+
variable_lower=vl,
|
|
963
|
+
variable_upper=vu,
|
|
964
|
+
domains=domains,
|
|
965
|
+
capabilities=capabilities,
|
|
966
|
+
bridge_policy=bridge_policy,
|
|
967
|
+
)
|
|
968
|
+
bridge_search_s += perf_counter() - bt0
|
|
969
|
+
rows.extend(dict(r) for r in lowered.rows)
|
|
970
|
+
cl.extend(lowered.lower)
|
|
971
|
+
cu.extend(lowered.upper)
|
|
972
|
+
tape_steps.append(lowered.step)
|
|
973
|
+
rejected_paths.extend(decision.rejected_paths)
|
|
974
|
+
source_constraints[constraint.entity_id.value] = {
|
|
975
|
+
"execution_rows": list(range(start_row, len(rows))),
|
|
976
|
+
"shape": [],
|
|
977
|
+
"semantic_kind": "indicator",
|
|
978
|
+
"bridge_step_id": lowered.step.step_id,
|
|
979
|
+
"generated_target_ids": list(lowered.step.generated_target_ids),
|
|
980
|
+
}
|
|
981
|
+
else:
|
|
982
|
+
block_rows, block_cl, block_cu = _constraint_block(evaluator, constraint)
|
|
983
|
+
rows.extend(block_rows)
|
|
984
|
+
cl.extend(block_cl)
|
|
985
|
+
cu.extend(block_cu)
|
|
986
|
+
source_constraints[constraint.entity_id.value] = {
|
|
987
|
+
"execution_rows": list(range(start_row, len(rows))),
|
|
988
|
+
"shape": list(constraint.function.shape),
|
|
989
|
+
"semantic_kind": "function_in_set",
|
|
990
|
+
}
|
|
991
|
+
A = _rows_to_csr(rows, n)
|
|
992
|
+
c, offset, P = _objective_data(evaluator, model, n, target)
|
|
993
|
+
if P is not None:
|
|
994
|
+
raise CompileError("P4 indicator bridge unexpectedly produced a quadratic target")
|
|
995
|
+
tape = TransformationTape(tuple(tape_steps))
|
|
996
|
+
capability_signature = _p4_capability_signature(capabilities)
|
|
997
|
+
compilation_hash = _p4_compilation_hash(
|
|
998
|
+
model,
|
|
999
|
+
target,
|
|
1000
|
+
capability_signature=capability_signature,
|
|
1001
|
+
bridge_policy=bridge_policy,
|
|
1002
|
+
tape=tape,
|
|
1003
|
+
)
|
|
1004
|
+
metadata = _make_metadata(model, compiled_by="solverpilot-p4", compiler_policy=_P4_NUMERICAL_POLICY)
|
|
1005
|
+
metadata.update({
|
|
1006
|
+
"compilation_hash": compilation_hash,
|
|
1007
|
+
"transformation_count": len(tape.steps),
|
|
1008
|
+
"bridge_policy": bridge_policy.policy_version,
|
|
1009
|
+
})
|
|
1010
|
+
ir = _build_execution_ir(
|
|
1011
|
+
model=model,
|
|
1012
|
+
target=target,
|
|
1013
|
+
A=A,
|
|
1014
|
+
c=c,
|
|
1015
|
+
P=None,
|
|
1016
|
+
variable_lower=vl,
|
|
1017
|
+
variable_upper=vu,
|
|
1018
|
+
constraint_lower=np.asarray(cl, dtype=np.float64),
|
|
1019
|
+
constraint_upper=np.asarray(cu, dtype=np.float64),
|
|
1020
|
+
domains=domains,
|
|
1021
|
+
objective_offset=offset,
|
|
1022
|
+
previous=None,
|
|
1023
|
+
objective_recompiled=True,
|
|
1024
|
+
metadata=metadata,
|
|
1025
|
+
)
|
|
1026
|
+
lowering_s = perf_counter() - lower_t0
|
|
1027
|
+
pmap = _p4_parameter_map(model, tape)
|
|
1028
|
+
source_map = {"variables": source_vars, "constraints": source_constraints}
|
|
1029
|
+
report = CompilationReport(
|
|
1030
|
+
target=target,
|
|
1031
|
+
semantic_analysis_s=semantic_analysis_s,
|
|
1032
|
+
lowering_s=lowering_s,
|
|
1033
|
+
total_s=perf_counter() - total_t0,
|
|
1034
|
+
n_semantic_variables=len(model._variables),
|
|
1035
|
+
n_execution_variables=n,
|
|
1036
|
+
n_semantic_constraints=len(model._constraints),
|
|
1037
|
+
n_execution_constraints=len(rows),
|
|
1038
|
+
cache_status=cache_status,
|
|
1039
|
+
changed_parameters=changed_parameters,
|
|
1040
|
+
reused_execution_rows=0,
|
|
1041
|
+
recompiled_execution_rows=len(rows),
|
|
1042
|
+
objective_recompiled=True,
|
|
1043
|
+
execution_mutations=(),
|
|
1044
|
+
bridge_search_s=bridge_search_s,
|
|
1045
|
+
n_transformations=len(tape.steps),
|
|
1046
|
+
rejected_bridge_paths=tuple(rejected_paths),
|
|
1047
|
+
)
|
|
1048
|
+
return CompiledModel(
|
|
1049
|
+
execution_ir=ir,
|
|
1050
|
+
semantic_hash=model.semantic_hash,
|
|
1051
|
+
data_hash=model.data_hash,
|
|
1052
|
+
compilation_hash=compilation_hash,
|
|
1053
|
+
parameter_map=pmap,
|
|
1054
|
+
source_map=source_map,
|
|
1055
|
+
transformation_tape=tape.to_dicts(),
|
|
1056
|
+
capability_signature=capability_signature,
|
|
1057
|
+
mutation_plan={
|
|
1058
|
+
"mode": "bridge_full_recompile_p4",
|
|
1059
|
+
"incremental_parameter_patching": False,
|
|
1060
|
+
"backend_session_patch": False,
|
|
1061
|
+
"changed_parameters": list(changed_parameters),
|
|
1062
|
+
"affected_regions": [
|
|
1063
|
+
region for pid in changed_parameters for region in pmap.get(pid, {}).get("execution_regions", [])
|
|
1064
|
+
],
|
|
1065
|
+
"execution_mutations": [],
|
|
1066
|
+
"reason": "P4 plan-sensitive certificates are revalidated by full semantic lowering",
|
|
1067
|
+
},
|
|
1068
|
+
plan_sensitive_dependencies=tape.plan_sensitive_dependencies,
|
|
1069
|
+
precondition_certificate_hashes=tape.certificate_hashes,
|
|
1070
|
+
compilation_report=report,
|
|
1071
|
+
reconstruction_contract={
|
|
1072
|
+
"primal": "identity_by_source_map",
|
|
1073
|
+
"dual": "unavailable_for_indicator_bridges",
|
|
1074
|
+
"certificate": "unavailable_for_indicator_bridges",
|
|
1075
|
+
"original_space_validation": "solverpilot.bridges.validate_semantic_primal",
|
|
1076
|
+
},
|
|
1077
|
+
schema_version="solverpilot.compiled-model.p4.v1",
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def _p4_cache_key(capabilities: BackendCapabilityManifestV2 | None, bridge_policy: BridgePolicy) -> str:
|
|
1082
|
+
return f"p4|{_p4_capability_signature(capabilities)}|{bridge_policy.policy_version}"
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def _compile_model_p4(
|
|
1086
|
+
model: Model,
|
|
1087
|
+
*,
|
|
1088
|
+
use_cache: bool,
|
|
1089
|
+
bridge_policy: BridgePolicy,
|
|
1090
|
+
capabilities: BackendCapabilityManifestV2 | None,
|
|
1091
|
+
) -> CompiledModel:
|
|
1092
|
+
key = _p4_cache_key(capabilities, bridge_policy)
|
|
1093
|
+
entry = model._compiler_cache.get(key)
|
|
1094
|
+
changed: tuple[str, ...] = ()
|
|
1095
|
+
if isinstance(entry, _BridgeCacheEntry):
|
|
1096
|
+
changed = tuple(
|
|
1097
|
+
pid for pid in model._parameters
|
|
1098
|
+
if model._parameter_versions.get(pid, 0) != entry.parameter_versions.get(pid, 0)
|
|
1099
|
+
)
|
|
1100
|
+
if not use_cache:
|
|
1101
|
+
return _full_compile_p4(
|
|
1102
|
+
model,
|
|
1103
|
+
cache_status="disabled",
|
|
1104
|
+
changed_parameters=changed,
|
|
1105
|
+
bridge_policy=bridge_policy,
|
|
1106
|
+
capabilities=capabilities,
|
|
1107
|
+
)
|
|
1108
|
+
semantic_hash = model.semantic_hash
|
|
1109
|
+
if entry is None or not isinstance(entry, _BridgeCacheEntry) or entry.semantic_hash != semantic_hash:
|
|
1110
|
+
compiled = _full_compile_p4(
|
|
1111
|
+
model,
|
|
1112
|
+
cache_status="miss",
|
|
1113
|
+
changed_parameters=(),
|
|
1114
|
+
bridge_policy=bridge_policy,
|
|
1115
|
+
capabilities=capabilities,
|
|
1116
|
+
)
|
|
1117
|
+
snapshots: OrderedDict[str, CompiledModel] = OrderedDict([(compiled.data_hash, compiled)])
|
|
1118
|
+
model._compiler_cache[key] = _BridgeCacheEntry(
|
|
1119
|
+
semantic_hash=semantic_hash,
|
|
1120
|
+
current=compiled,
|
|
1121
|
+
parameter_versions=dict(model._parameter_versions),
|
|
1122
|
+
snapshots=snapshots,
|
|
1123
|
+
)
|
|
1124
|
+
return compiled
|
|
1125
|
+
data_hash = model.data_hash
|
|
1126
|
+
if data_hash in entry.snapshots:
|
|
1127
|
+
entry.snapshot_hits += 1
|
|
1128
|
+
snap = entry.snapshots[data_hash]
|
|
1129
|
+
hit = replace(
|
|
1130
|
+
snap,
|
|
1131
|
+
compilation_report=replace(
|
|
1132
|
+
snap.compilation_report,
|
|
1133
|
+
semantic_analysis_s=0.0,
|
|
1134
|
+
lowering_s=0.0,
|
|
1135
|
+
total_s=0.0,
|
|
1136
|
+
bridge_search_s=0.0,
|
|
1137
|
+
cache_status="snapshot_hit",
|
|
1138
|
+
changed_parameters=changed,
|
|
1139
|
+
reused_execution_rows=snap.compilation_report.n_execution_constraints,
|
|
1140
|
+
recompiled_execution_rows=0,
|
|
1141
|
+
objective_recompiled=False,
|
|
1142
|
+
),
|
|
1143
|
+
)
|
|
1144
|
+
entry.current = hit
|
|
1145
|
+
entry.parameter_versions = dict(model._parameter_versions)
|
|
1146
|
+
entry.snapshots[data_hash] = hit
|
|
1147
|
+
entry.snapshots.move_to_end(data_hash)
|
|
1148
|
+
return hit
|
|
1149
|
+
compiled = _full_compile_p4(
|
|
1150
|
+
model,
|
|
1151
|
+
cache_status="plan_sensitive_recompile",
|
|
1152
|
+
changed_parameters=changed,
|
|
1153
|
+
bridge_policy=bridge_policy,
|
|
1154
|
+
capabilities=capabilities,
|
|
1155
|
+
)
|
|
1156
|
+
entry.full_compiles += 1
|
|
1157
|
+
entry.current = compiled
|
|
1158
|
+
entry.parameter_versions = dict(model._parameter_versions)
|
|
1159
|
+
entry.snapshots[data_hash] = compiled
|
|
1160
|
+
entry.snapshots.move_to_end(data_hash)
|
|
1161
|
+
while len(entry.snapshots) > _MAX_SNAPSHOTS:
|
|
1162
|
+
entry.snapshots.popitem(last=False)
|
|
1163
|
+
return compiled
|
|
1164
|
+
|
|
1165
|
+
def compile_model(
|
|
1166
|
+
model: Model,
|
|
1167
|
+
*,
|
|
1168
|
+
use_cache: bool = True,
|
|
1169
|
+
bridge_policy: BridgePolicy | None = None,
|
|
1170
|
+
capabilities: BackendCapabilityManifestV2 | None = None,
|
|
1171
|
+
) -> CompiledModel:
|
|
1172
|
+
from .sets import PositiveSemidefiniteCone, RotatedSecondOrderCone, SecondOrderCone
|
|
1173
|
+
if any(isinstance(c.set, (SecondOrderCone, RotatedSecondOrderCone, PositiveSemidefiniteCone)) for c in model.constraints):
|
|
1174
|
+
if any(isinstance(c, IndicatorConstraint) for c in model.constraints):
|
|
1175
|
+
raise CompileError("P6 does not combine indicator/MILP bridges with continuous conic targets")
|
|
1176
|
+
from solverpilot.conic.compiler import compile_conic_model
|
|
1177
|
+
return compile_conic_model(model, use_cache=use_cache)
|
|
1178
|
+
def _node_has_p7_nonlinearity(node):
|
|
1179
|
+
if node.kind in {"sin", "cos", "exp", "log", "sqrt", "tanh", "pow", "div"}:
|
|
1180
|
+
return True
|
|
1181
|
+
return any(_node_has_p7_nonlinearity(a) for a in node.args)
|
|
1182
|
+
|
|
1183
|
+
has_nlp = False
|
|
1184
|
+
has_discrete = any(data.domain is not VariableDomain.CONTINUOUS for data in model._variables.values())
|
|
1185
|
+
if model.objective is not None:
|
|
1186
|
+
deg = model.objective.expression.polynomial_degree
|
|
1187
|
+
has_nlp = _node_has_p7_nonlinearity(model.objective.expression._node) or deg is None or (deg is not None and deg > 2) or (has_discrete and deg is not None and deg > 1)
|
|
1188
|
+
if not has_nlp:
|
|
1189
|
+
for c in model.constraints:
|
|
1190
|
+
if isinstance(c, IndicatorConstraint):
|
|
1191
|
+
continue
|
|
1192
|
+
deg = c.function.polynomial_degree
|
|
1193
|
+
if _node_has_p7_nonlinearity(c.function._node) or deg is None or (deg is not None and deg > 1):
|
|
1194
|
+
has_nlp = True; break
|
|
1195
|
+
if has_nlp:
|
|
1196
|
+
if any(isinstance(c, IndicatorConstraint) for c in model.constraints):
|
|
1197
|
+
raise CompileError("P8 MINLP v1 does not combine indicator constraints with nonlinear orchestration")
|
|
1198
|
+
if any(data.domain is not VariableDomain.CONTINUOUS for data in model._variables.values()):
|
|
1199
|
+
from solverpilot.minlp.compiler import compile_minlp_model
|
|
1200
|
+
return compile_minlp_model(model, use_cache=use_cache)
|
|
1201
|
+
from solverpilot.nlp.compiler import compile_nlp_model
|
|
1202
|
+
return compile_nlp_model(model, use_cache=use_cache)
|
|
1203
|
+
|
|
1204
|
+
if any(isinstance(c, IndicatorConstraint) for c in model.constraints):
|
|
1205
|
+
return _compile_model_p4(
|
|
1206
|
+
model,
|
|
1207
|
+
use_cache=use_cache,
|
|
1208
|
+
bridge_policy=BridgePolicy() if bridge_policy is None else bridge_policy,
|
|
1209
|
+
capabilities=capabilities,
|
|
1210
|
+
)
|
|
1211
|
+
if not use_cache:
|
|
1212
|
+
compiled, _ = _full_compile(model, cache_status="disabled")
|
|
1213
|
+
return compiled
|
|
1214
|
+
|
|
1215
|
+
semantic_hash = model.semantic_hash
|
|
1216
|
+
entry = model._compiler_cache.get(_CACHE_KEY)
|
|
1217
|
+
if entry is None or not isinstance(entry, _CacheEntry) or entry.semantic_hash != semantic_hash:
|
|
1218
|
+
invalidations = 0 if entry is None or not isinstance(entry, _CacheEntry) else entry.semantic_invalidations + 1
|
|
1219
|
+
compiled, template = _full_compile(model, cache_status="miss")
|
|
1220
|
+
snapshots: OrderedDict[str, CompiledModel] = OrderedDict()
|
|
1221
|
+
snapshots[compiled.data_hash] = compiled
|
|
1222
|
+
model._compiler_cache[_CACHE_KEY] = _CacheEntry(
|
|
1223
|
+
semantic_hash=semantic_hash,
|
|
1224
|
+
template=template,
|
|
1225
|
+
current=compiled,
|
|
1226
|
+
parameter_versions=dict(model._parameter_versions),
|
|
1227
|
+
snapshots=snapshots,
|
|
1228
|
+
semantic_invalidations=invalidations,
|
|
1229
|
+
)
|
|
1230
|
+
return compiled
|
|
1231
|
+
|
|
1232
|
+
changed = _changed_parameters(model, entry)
|
|
1233
|
+
data_hash = model.data_hash
|
|
1234
|
+
if data_hash in entry.snapshots:
|
|
1235
|
+
entry.snapshot_hits += 1
|
|
1236
|
+
hit = _snapshot_hit(entry.snapshots[data_hash], changed)
|
|
1237
|
+
entry.current = hit
|
|
1238
|
+
entry.parameter_versions = dict(model._parameter_versions)
|
|
1239
|
+
entry.snapshots[data_hash] = hit
|
|
1240
|
+
entry.snapshots.move_to_end(data_hash)
|
|
1241
|
+
return hit
|
|
1242
|
+
|
|
1243
|
+
compiled = _incremental_compile(model, entry, changed)
|
|
1244
|
+
entry.partial_compiles += 1
|
|
1245
|
+
entry.current = compiled
|
|
1246
|
+
entry.parameter_versions = dict(model._parameter_versions)
|
|
1247
|
+
_remember_snapshot(entry, compiled)
|
|
1248
|
+
return compiled
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def compiler_cache_info(model: Model) -> CompilerCacheInfo:
|
|
1252
|
+
entry = model._compiler_cache.get(_CACHE_KEY)
|
|
1253
|
+
if not isinstance(entry, _CacheEntry):
|
|
1254
|
+
return CompilerCacheInfo(None, 0, 0, 0, 0, 0, None)
|
|
1255
|
+
return CompilerCacheInfo(
|
|
1256
|
+
semantic_hash=entry.semantic_hash,
|
|
1257
|
+
snapshots=len(entry.snapshots),
|
|
1258
|
+
full_compiles=entry.full_compiles,
|
|
1259
|
+
partial_compiles=entry.partial_compiles,
|
|
1260
|
+
snapshot_hits=entry.snapshot_hits,
|
|
1261
|
+
semantic_invalidations=entry.semantic_invalidations,
|
|
1262
|
+
last_data_hash=entry.current.data_hash,
|
|
1263
|
+
)
|