solverpilot 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. solverpilot/__init__.py +250 -0
  2. solverpilot/_immutability.py +27 -0
  3. solverpilot/_version.py +3 -0
  4. solverpilot/applications/__init__.py +3 -0
  5. solverpilot/applications/routing/__init__.py +19 -0
  6. solverpilot/applications/routing/compile_milp.py +321 -0
  7. solverpilot/applications/routing/diagnostics.py +78 -0
  8. solverpilot/applications/routing/errors.py +14 -0
  9. solverpilot/applications/routing/heuristics.py +167 -0
  10. solverpilot/applications/routing/model.py +259 -0
  11. solverpilot/applications/routing/reference.py +81 -0
  12. solverpilot/applications/routing/solution.py +89 -0
  13. solverpilot/applications/routing/timeline.py +79 -0
  14. solverpilot/applications/routing/validation.py +169 -0
  15. solverpilot/applications/tsp/__init__.py +41 -0
  16. solverpilot/applications/tsp/compile_cp.py +121 -0
  17. solverpilot/applications/tsp/compile_milp.py +139 -0
  18. solverpilot/applications/tsp/distance.py +60 -0
  19. solverpilot/applications/tsp/errors.py +17 -0
  20. solverpilot/applications/tsp/heuristics.py +112 -0
  21. solverpilot/applications/tsp/model.py +175 -0
  22. solverpilot/applications/tsp/reference.py +121 -0
  23. solverpilot/applications/tsp/solution.py +65 -0
  24. solverpilot/applications/tsp/validation.py +92 -0
  25. solverpilot/backends/__init__.py +51 -0
  26. solverpilot/backends/_casadi_highs_worker.py +92 -0
  27. solverpilot/backends/base.py +28 -0
  28. solverpilot/backends/bundled_capi.py +910 -0
  29. solverpilot/backends/casadi_conic.py +303 -0
  30. solverpilot/backends/health.py +240 -0
  31. solverpilot/backends/highspy_native.py +217 -0
  32. solverpilot/backends/nlopt_native.py +237 -0
  33. solverpilot/backends/osqp_native.py +297 -0
  34. solverpilot/backends/protocol_v2.py +119 -0
  35. solverpilot/backends/pyscipopt_native.py +200 -0
  36. solverpilot/backends/registry.py +38 -0
  37. solverpilot/backends/scipy_highs.py +138 -0
  38. solverpilot/backends/scipy_highs_lp.py +195 -0
  39. solverpilot/backends/scipy_slsqp_qp.py +133 -0
  40. solverpilot/backends/scipy_vendored_highs_dev.py +363 -0
  41. solverpilot/benchmark/__init__.py +23 -0
  42. solverpilot/benchmark/acquire.py +199 -0
  43. solverpilot/benchmark/baselines.py +70 -0
  44. solverpilot/benchmark/bundle.py +37 -0
  45. solverpilot/benchmark/campaign.py +150 -0
  46. solverpilot/benchmark/environment.py +115 -0
  47. solverpilot/benchmark/integrity.py +46 -0
  48. solverpilot/benchmark/model.py +146 -0
  49. solverpilot/benchmark/policy.py +182 -0
  50. solverpilot/benchmark/registry.py +38 -0
  51. solverpilot/benchmark/report.py +44 -0
  52. solverpilot/benchmark/runner.py +516 -0
  53. solverpilot/benchmark/slurm.py +55 -0
  54. solverpilot/benchmark/specs.py +75 -0
  55. solverpilot/benchmark/splits.py +181 -0
  56. solverpilot/benchmark/summary.py +384 -0
  57. solverpilot/benchmark/verify.py +142 -0
  58. solverpilot/benchmark/worker.py +71 -0
  59. solverpilot/bridges/__init__.py +19 -0
  60. solverpilot/bridges/engine.py +44 -0
  61. solverpilot/bridges/indicator.py +232 -0
  62. solverpilot/bridges/registry.py +78 -0
  63. solverpilot/bridges/transformation-tape.schema.json +53 -0
  64. solverpilot/bridges/types.py +149 -0
  65. solverpilot/bridges/validation.py +110 -0
  66. solverpilot/capabilities/__init__.py +32 -0
  67. solverpilot/capabilities/backend-capability-v2.schema.json +21 -0
  68. solverpilot/capabilities/conformance.py +151 -0
  69. solverpilot/capabilities/enums.py +27 -0
  70. solverpilot/capabilities/manifest.py +19 -0
  71. solverpilot/capabilities/ontology.py +73 -0
  72. solverpilot/capabilities/requirements.py +37 -0
  73. solverpilot/capabilities/resolve.py +30 -0
  74. solverpilot/capabilities/v2.py +711 -0
  75. solverpilot/cli/__init__.py +1 -0
  76. solverpilot/cli/backend_health.py +77 -0
  77. solverpilot/cli/benchmark.py +331 -0
  78. solverpilot/cli/benchmark_worker.py +33 -0
  79. solverpilot/cli/capabilities.py +65 -0
  80. solverpilot/conic/__init__.py +11 -0
  81. solverpilot/conic/backend.py +263 -0
  82. solverpilot/conic/compiler.py +310 -0
  83. solverpilot/conic/conformance.py +60 -0
  84. solverpilot/conic/conic-ir-v1.schema.json +24 -0
  85. solverpilot/conic/ir.py +299 -0
  86. solverpilot/conic/validation.py +149 -0
  87. solverpilot/cp/__init__.py +7 -0
  88. solverpilot/cp/conformance.py +32 -0
  89. solverpilot/cp/cp-ir-v1.schema.json +13 -0
  90. solverpilot/cp/ir.py +170 -0
  91. solverpilot/cp/model.py +143 -0
  92. solverpilot/cp/ortools_backend.py +423 -0
  93. solverpilot/cp/ortools_worker.py +75 -0
  94. solverpilot/cp/reference.py +51 -0
  95. solverpilot/cp/serialization.py +117 -0
  96. solverpilot/cp/validate.py +83 -0
  97. solverpilot/diagnose/__init__.py +33 -0
  98. solverpilot/diagnose/infeasibility.py +463 -0
  99. solverpilot/diagnose/model.py +87 -0
  100. solverpilot/evaluation/__init__.py +53 -0
  101. solverpilot/evaluation/metrics.py +150 -0
  102. solverpilot/evaluation/oracle.py +288 -0
  103. solverpilot/evaluation/proof.py +53 -0
  104. solverpilot/evaluation/references.py +73 -0
  105. solverpilot/evaluation/regret.py +281 -0
  106. solverpilot/exceptions.py +34 -0
  107. solverpilot/experimental/__init__.py +32 -0
  108. solverpilot/extensions/__init__.py +56 -0
  109. solverpilot/extensions/entrypoints.py +34 -0
  110. solverpilot/extensions/errors.py +35 -0
  111. solverpilot/extensions/manager.py +651 -0
  112. solverpilot/extensions/manifest.py +232 -0
  113. solverpilot/extensions/model.py +101 -0
  114. solverpilot/extensions/versioning.py +136 -0
  115. solverpilot/history/__init__.py +22 -0
  116. solverpilot/history/errors.py +15 -0
  117. solverpilot/history/model.py +129 -0
  118. solverpilot/history/sqlite.py +651 -0
  119. solverpilot/inspect/__init__.py +3 -0
  120. solverpilot/inspect/fingerprint.py +199 -0
  121. solverpilot/intelligence/__init__.py +36 -0
  122. solverpilot/intelligence/errors.py +16 -0
  123. solverpilot/intelligence/fingerprint.py +108 -0
  124. solverpilot/intelligence/leakage.py +69 -0
  125. solverpilot/intelligence/schema.py +314 -0
  126. solverpilot/intelligence/shift.py +208 -0
  127. solverpilot/io/__init__.py +41 -0
  128. solverpilot/io/binary64.py +49 -0
  129. solverpilot/io/csv.py +230 -0
  130. solverpilot/io/errors.py +25 -0
  131. solverpilot/io/hashing.py +111 -0
  132. solverpilot/io/json.py +90 -0
  133. solverpilot/io/mapping.py +195 -0
  134. solverpilot/io/model.py +407 -0
  135. solverpilot/io/numeric.py +108 -0
  136. solverpilot/io/provenance.py +114 -0
  137. solverpilot/io/result.py +45 -0
  138. solverpilot/io/source.py +104 -0
  139. solverpilot/io/strict_json.py +133 -0
  140. solverpilot/minlp/__init__.py +6 -0
  141. solverpilot/minlp/compiler.py +66 -0
  142. solverpilot/minlp/conformance.py +34 -0
  143. solverpilot/minlp/convexity.py +85 -0
  144. solverpilot/minlp/ir.py +35 -0
  145. solverpilot/minlp/minlp-ir-v1.schema.json +1 -0
  146. solverpilot/minlp/orchestrator.py +220 -0
  147. solverpilot/minlp/validation.py +28 -0
  148. solverpilot/model/__init__.py +27 -0
  149. solverpilot/model/compiler.py +1263 -0
  150. solverpilot/model/errors.py +22 -0
  151. solverpilot/model/expression.py +373 -0
  152. solverpilot/model/model.py +716 -0
  153. solverpilot/model/sets.py +60 -0
  154. solverpilot/model/types.py +36 -0
  155. solverpilot/nlp/__init__.py +7 -0
  156. solverpilot/nlp/ad.py +166 -0
  157. solverpilot/nlp/backend.py +113 -0
  158. solverpilot/nlp/compiler.py +116 -0
  159. solverpilot/nlp/conformance.py +58 -0
  160. solverpilot/nlp/ir.py +70 -0
  161. solverpilot/nlp/nlp-ir-v1.schema.json +1 -0
  162. solverpilot/nlp/validation.py +60 -0
  163. solverpilot/plan/__init__.py +33 -0
  164. solverpilot/plan/model.py +79 -0
  165. solverpilot/plan/planner.py +201 -0
  166. solverpilot/plan/production.py +627 -0
  167. solverpilot/plan/selective_lp.py +65 -0
  168. solverpilot/problem/__init__.py +16 -0
  169. solverpilot/problem/enums.py +20 -0
  170. solverpilot/problem/hashing.py +88 -0
  171. solverpilot/problem/linear.py +196 -0
  172. solverpilot/problem/mps.py +447 -0
  173. solverpilot/problem/quadratic.py +192 -0
  174. solverpilot/reporting/__init__.py +27 -0
  175. solverpilot/reporting/explain.py +327 -0
  176. solverpilot/reporting/model.py +264 -0
  177. solverpilot/reporting/render.py +55 -0
  178. solverpilot/runtime/__init__.py +19 -0
  179. solverpilot/runtime/auto.py +193 -0
  180. solverpilot/runtime/budgeting.py +43 -0
  181. solverpilot/runtime/executor.py +126 -0
  182. solverpilot/runtime/portfolio.py +197 -0
  183. solverpilot/runtime/result.py +72 -0
  184. solverpilot/session/__init__.py +29 -0
  185. solverpilot/session/conformance_v2.py +239 -0
  186. solverpilot/session/mutations.py +115 -0
  187. solverpilot/session/persistent-session-trace.schema.json +45 -0
  188. solverpilot/session/persistent.py +491 -0
  189. solverpilot/session/reuse.py +81 -0
  190. solverpilot/session/session.py +236 -0
  191. solverpilot/trace/__init__.py +3 -0
  192. solverpilot/trace/schema.py +47 -0
  193. solverpilot/validate/__init__.py +10 -0
  194. solverpilot/validate/core.py +179 -0
  195. solverpilot/validate/result.py +55 -0
  196. solverpilot-0.1.dist-info/METADATA +554 -0
  197. solverpilot-0.1.dist-info/RECORD +201 -0
  198. solverpilot-0.1.dist-info/WHEEL +5 -0
  199. solverpilot-0.1.dist-info/entry_points.txt +4 -0
  200. solverpilot-0.1.dist-info/licenses/LICENSE +202 -0
  201. solverpilot-0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ )