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,716 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Iterable
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from solverpilot.problem import LinearProblem, ObjectiveSense, QuadraticProblem, VariableDomain
|
|
12
|
+
|
|
13
|
+
from .errors import DomainError, OwnershipError, ShapeError, SymbolicTruthValueError
|
|
14
|
+
from .expression import ExprNode, Expression, _shape_tuple, _sign_of_constant
|
|
15
|
+
from .sets import (ConstraintSet, EqualTo, GreaterThan, LessThan, ScalarSet, SecondOrderCone, RotatedSecondOrderCone, PositiveSemidefiniteCone)
|
|
16
|
+
from .types import Curvature, EntityId, SignDomain
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _readonly_array(value: Any, shape: tuple[int, ...], *, finite: bool, name: str) -> np.ndarray:
|
|
20
|
+
arr = np.asarray(value, dtype=np.float64)
|
|
21
|
+
try:
|
|
22
|
+
arr = np.broadcast_to(arr, shape if shape else ()).copy()
|
|
23
|
+
except ValueError as exc:
|
|
24
|
+
raise ShapeError(f"{name} cannot broadcast to shape {shape}") from exc
|
|
25
|
+
if np.isnan(arr).any():
|
|
26
|
+
raise DomainError(f"{name} must not contain NaN")
|
|
27
|
+
if finite and np.isinf(arr).any():
|
|
28
|
+
raise DomainError(f"{name} must be finite")
|
|
29
|
+
arr.flags.writeable = False
|
|
30
|
+
return arr
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _canonical_float(x: float) -> str:
|
|
34
|
+
x = float(x)
|
|
35
|
+
if np.isnan(x):
|
|
36
|
+
raise DomainError("NaN cannot be canonically serialized")
|
|
37
|
+
if np.isposinf(x):
|
|
38
|
+
return "+inf"
|
|
39
|
+
if np.isneginf(x):
|
|
40
|
+
return "-inf"
|
|
41
|
+
return x.hex()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _array_semantic_payload(arr: np.ndarray) -> dict[str, Any]:
|
|
45
|
+
flat = np.asarray(arr, dtype=np.float64).reshape(-1)
|
|
46
|
+
return {"shape": list(arr.shape), "values": [_canonical_float(x) for x in flat]}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class _VariableData:
|
|
51
|
+
entity_id: EntityId
|
|
52
|
+
shape: tuple[int, ...]
|
|
53
|
+
lower: np.ndarray
|
|
54
|
+
upper: np.ndarray
|
|
55
|
+
domain: VariableDomain
|
|
56
|
+
name: str | None
|
|
57
|
+
origin_id: str | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(slots=True)
|
|
61
|
+
class _ParameterData:
|
|
62
|
+
entity_id: EntityId
|
|
63
|
+
shape: tuple[int, ...]
|
|
64
|
+
value: np.ndarray
|
|
65
|
+
sign: SignDomain
|
|
66
|
+
name: str | None
|
|
67
|
+
origin_id: str | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class PendingConstraint:
|
|
72
|
+
function: Expression
|
|
73
|
+
relation: str
|
|
74
|
+
|
|
75
|
+
def __bool__(self) -> bool:
|
|
76
|
+
raise SymbolicTruthValueError("symbolic constraint cannot be converted to bool")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class Constraint:
|
|
81
|
+
entity_id: EntityId
|
|
82
|
+
function: Expression
|
|
83
|
+
set: ConstraintSet
|
|
84
|
+
name: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class IndicatorConstraint:
|
|
89
|
+
entity_id: EntityId
|
|
90
|
+
indicator: "Variable"
|
|
91
|
+
active_value: int
|
|
92
|
+
function: Expression
|
|
93
|
+
set: ConstraintSet
|
|
94
|
+
name: str | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class Objective:
|
|
99
|
+
entity_id: EntityId
|
|
100
|
+
expression: Expression
|
|
101
|
+
sense: ObjectiveSense
|
|
102
|
+
name: str | None = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Variable(Expression):
|
|
106
|
+
__slots__ = ("_entity_id",)
|
|
107
|
+
|
|
108
|
+
def __init__(self, model: "Model", data: _VariableData) -> None:
|
|
109
|
+
sign = model._variable_sign(data)
|
|
110
|
+
node = ExprNode(
|
|
111
|
+
"variable", data.shape, payload=data.entity_id.value,
|
|
112
|
+
variable_dependencies=frozenset({data.entity_id.value}),
|
|
113
|
+
sign=sign, degree=1, curvature=Curvature.AFFINE,
|
|
114
|
+
)
|
|
115
|
+
super().__init__(model, node)
|
|
116
|
+
self._entity_id = data.entity_id
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def id(self) -> EntityId:
|
|
120
|
+
return self._entity_id
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def name(self) -> str | None:
|
|
124
|
+
return self._model._variables[self.id.value].name
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def lower(self) -> np.ndarray:
|
|
128
|
+
return self._model._variables[self.id.value].lower
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def upper(self) -> np.ndarray:
|
|
132
|
+
return self._model._variables[self.id.value].upper
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def domain(self) -> VariableDomain:
|
|
136
|
+
return self._model._variables[self.id.value].domain
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Parameter(Expression):
|
|
140
|
+
__slots__ = ("_entity_id",)
|
|
141
|
+
|
|
142
|
+
def __init__(self, model: "Model", data: _ParameterData) -> None:
|
|
143
|
+
node = ExprNode(
|
|
144
|
+
"parameter", data.shape, payload=data.entity_id.value,
|
|
145
|
+
parameter_dependencies=frozenset({data.entity_id.value}),
|
|
146
|
+
sign=data.sign, degree=0, curvature=Curvature.CONSTANT,
|
|
147
|
+
)
|
|
148
|
+
super().__init__(model, node)
|
|
149
|
+
self._entity_id = data.entity_id
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def id(self) -> EntityId:
|
|
153
|
+
return self._entity_id
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def name(self) -> str | None:
|
|
157
|
+
return self._model._parameters[self.id.value].name
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def value(self) -> np.ndarray:
|
|
161
|
+
return self._model._parameters[self.id.value].value.copy()
|
|
162
|
+
|
|
163
|
+
@value.setter
|
|
164
|
+
def value(self, new_value: Any) -> None:
|
|
165
|
+
self._model._set_parameter_value(self.id.value, new_value)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Model:
|
|
169
|
+
def __init__(self, name: str | None = None) -> None:
|
|
170
|
+
self.name = name
|
|
171
|
+
self._namespace = uuid.uuid4().hex
|
|
172
|
+
self._next_ids = {"v": 0, "p": 0, "c": 0, "o": 0}
|
|
173
|
+
self._variables: dict[str, _VariableData] = {}
|
|
174
|
+
self._parameters: dict[str, _ParameterData] = {}
|
|
175
|
+
self._constraints: list[Constraint | IndicatorConstraint] = []
|
|
176
|
+
self._objective: Objective | None = None
|
|
177
|
+
# P2 compiler/cache state. Expression nodes remain immutable; only the
|
|
178
|
+
# model container tracks semantic/data revisions.
|
|
179
|
+
self._semantic_revision = 0
|
|
180
|
+
self._data_revision = 0
|
|
181
|
+
self._parameter_versions: dict[str, int] = {}
|
|
182
|
+
self._semantic_hash_cache: tuple[int, str] | None = None
|
|
183
|
+
self._data_hash_cache: tuple[int, int, str] | None = None
|
|
184
|
+
self._compiler_cache: dict[str, Any] = {}
|
|
185
|
+
|
|
186
|
+
def _new_id(self, prefix: str) -> EntityId:
|
|
187
|
+
self._next_ids[prefix] += 1
|
|
188
|
+
return EntityId(f"{prefix}{self._next_ids[prefix]:06d}", self._namespace)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def variables(self) -> tuple[Variable, ...]:
|
|
192
|
+
return tuple(Variable(self, d) for d in self._variables.values())
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def parameters(self) -> tuple[Parameter, ...]:
|
|
196
|
+
return tuple(Parameter(self, d) for d in self._parameters.values())
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def constraints(self) -> tuple[Constraint | IndicatorConstraint, ...]:
|
|
200
|
+
return tuple(self._constraints)
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def objective(self) -> Objective | None:
|
|
204
|
+
return self._objective
|
|
205
|
+
|
|
206
|
+
def variable(
|
|
207
|
+
self,
|
|
208
|
+
shape: int | Iterable[int] | tuple[int, ...] | None = None,
|
|
209
|
+
*,
|
|
210
|
+
lower: Any = -np.inf,
|
|
211
|
+
upper: Any = np.inf,
|
|
212
|
+
domain: VariableDomain | str = VariableDomain.CONTINUOUS,
|
|
213
|
+
name: str | None = None,
|
|
214
|
+
) -> Variable:
|
|
215
|
+
shape_t = _shape_tuple(shape)
|
|
216
|
+
dom = VariableDomain(domain)
|
|
217
|
+
lo = _readonly_array(lower, shape_t, finite=False, name="variable lower bound")
|
|
218
|
+
hi = _readonly_array(upper, shape_t, finite=False, name="variable upper bound")
|
|
219
|
+
if np.any(lo > hi):
|
|
220
|
+
raise DomainError("variable lower bound exceeds upper bound")
|
|
221
|
+
if dom is VariableDomain.BINARY and (np.any(lo < 0) or np.any(hi > 1)):
|
|
222
|
+
raise DomainError("binary variable bounds must lie inside [0, 1]")
|
|
223
|
+
entity_id = self._new_id("v")
|
|
224
|
+
data = _VariableData(entity_id, shape_t, lo, hi, dom, name)
|
|
225
|
+
self._variables[entity_id.value] = data
|
|
226
|
+
self._mark_semantic_change()
|
|
227
|
+
return Variable(self, data)
|
|
228
|
+
|
|
229
|
+
def binary(self, shape: int | Iterable[int] | tuple[int, ...] | None = None, *, name: str | None = None) -> Variable:
|
|
230
|
+
return self.variable(shape, lower=0.0, upper=1.0, domain=VariableDomain.BINARY, name=name)
|
|
231
|
+
|
|
232
|
+
def integer(
|
|
233
|
+
self,
|
|
234
|
+
shape: int | Iterable[int] | tuple[int, ...] | None = None,
|
|
235
|
+
*,
|
|
236
|
+
lower: Any = -np.inf,
|
|
237
|
+
upper: Any = np.inf,
|
|
238
|
+
name: str | None = None,
|
|
239
|
+
) -> Variable:
|
|
240
|
+
return self.variable(shape, lower=lower, upper=upper, domain=VariableDomain.INTEGER, name=name)
|
|
241
|
+
|
|
242
|
+
def parameter(
|
|
243
|
+
self,
|
|
244
|
+
shape: int | Iterable[int] | tuple[int, ...] | None = None,
|
|
245
|
+
*,
|
|
246
|
+
value: Any | None = None,
|
|
247
|
+
sign: SignDomain | str = SignDomain.UNKNOWN,
|
|
248
|
+
name: str | None = None,
|
|
249
|
+
) -> Parameter:
|
|
250
|
+
shape_t = _shape_tuple(shape)
|
|
251
|
+
sign_v = SignDomain(sign)
|
|
252
|
+
if value is None:
|
|
253
|
+
value = np.zeros(shape_t if shape_t else (), dtype=np.float64)
|
|
254
|
+
arr = _readonly_array(value, shape_t, finite=True, name="parameter value")
|
|
255
|
+
self._validate_parameter_sign(arr, sign_v)
|
|
256
|
+
entity_id = self._new_id("p")
|
|
257
|
+
data = _ParameterData(entity_id, shape_t, arr, sign_v, name)
|
|
258
|
+
self._parameters[entity_id.value] = data
|
|
259
|
+
self._parameter_versions[entity_id.value] = 0
|
|
260
|
+
self._mark_semantic_change()
|
|
261
|
+
return Parameter(self, data)
|
|
262
|
+
|
|
263
|
+
def constant(self, value: Any) -> Expression:
|
|
264
|
+
arr = np.asarray(value, dtype=np.float64)
|
|
265
|
+
if np.isnan(arr).any() or np.isinf(arr).any():
|
|
266
|
+
raise DomainError("expression constants must be finite")
|
|
267
|
+
arr = np.array(arr, dtype=np.float64, copy=True)
|
|
268
|
+
arr.flags.writeable = False
|
|
269
|
+
node = ExprNode("constant", tuple(arr.shape), payload=arr, sign=_sign_of_constant(arr), degree=0, curvature=Curvature.CONSTANT)
|
|
270
|
+
return Expression(self, node)
|
|
271
|
+
|
|
272
|
+
def add_in_set(self, function: Any, set_: ConstraintSet, *, name: str | None = None) -> Constraint:
|
|
273
|
+
expr = function if isinstance(function, Expression) else self.constant(function)
|
|
274
|
+
if expr._model is not self:
|
|
275
|
+
raise OwnershipError("constraint function belongs to another model")
|
|
276
|
+
if not isinstance(set_, (LessThan, GreaterThan, EqualTo, SecondOrderCone, RotatedSecondOrderCone, PositiveSemidefiniteCone)):
|
|
277
|
+
from .sets import Interval
|
|
278
|
+
if not isinstance(set_, Interval):
|
|
279
|
+
raise TypeError("unsupported constraint set")
|
|
280
|
+
if isinstance(set_, SecondOrderCone) and expr.shape != (set_.dimension,):
|
|
281
|
+
raise ShapeError(f"SOC function must have shape ({set_.dimension},), got {expr.shape}")
|
|
282
|
+
if isinstance(set_, RotatedSecondOrderCone) and expr.shape != (set_.dimension,):
|
|
283
|
+
raise ShapeError(f"rotated SOC function must have shape ({set_.dimension},), got {expr.shape}")
|
|
284
|
+
if isinstance(set_, PositiveSemidefiniteCone) and expr.shape != (set_.dimension, set_.dimension):
|
|
285
|
+
raise ShapeError(f"PSD function must have shape ({set_.dimension}, {set_.dimension}), got {expr.shape}")
|
|
286
|
+
constraint = Constraint(self._new_id("c"), expr, set_, name)
|
|
287
|
+
self._constraints.append(constraint)
|
|
288
|
+
self._mark_semantic_change()
|
|
289
|
+
return constraint
|
|
290
|
+
|
|
291
|
+
def add(self, relation: PendingConstraint, *, name: str | None = None) -> Constraint:
|
|
292
|
+
if not isinstance(relation, PendingConstraint):
|
|
293
|
+
raise TypeError("Model.add expects a symbolic relation such as x <= b")
|
|
294
|
+
if relation.function._model is not self:
|
|
295
|
+
raise OwnershipError("constraint belongs to another model")
|
|
296
|
+
if relation.relation == "le":
|
|
297
|
+
set_ = LessThan(0.0)
|
|
298
|
+
elif relation.relation == "ge":
|
|
299
|
+
set_ = GreaterThan(0.0)
|
|
300
|
+
elif relation.relation == "eq":
|
|
301
|
+
set_ = EqualTo(0.0)
|
|
302
|
+
else:
|
|
303
|
+
raise AssertionError(relation.relation)
|
|
304
|
+
return self.add_in_set(relation.function, set_, name=name)
|
|
305
|
+
|
|
306
|
+
def indicator(
|
|
307
|
+
self,
|
|
308
|
+
indicator: Variable,
|
|
309
|
+
relation: PendingConstraint,
|
|
310
|
+
*,
|
|
311
|
+
active_value: int = 1,
|
|
312
|
+
name: str | None = None,
|
|
313
|
+
) -> IndicatorConstraint:
|
|
314
|
+
"""Add a scalar binary indicator constraint without prematurely linearizing it.
|
|
315
|
+
|
|
316
|
+
P4 deliberately preserves the semantic indicator until compilation chooses an exact
|
|
317
|
+
lowering path. The current execution IR can transport only linear rows, so safe
|
|
318
|
+
compilation uses fixed-premise simplification or a bound-certified Big-M bridge.
|
|
319
|
+
"""
|
|
320
|
+
if not isinstance(indicator, Variable) or indicator._model is not self:
|
|
321
|
+
raise OwnershipError("indicator variable must be a Variable owned by this model")
|
|
322
|
+
if indicator.shape != ():
|
|
323
|
+
raise ShapeError("P4 indicator variable must be scalar")
|
|
324
|
+
if indicator.domain is not VariableDomain.BINARY:
|
|
325
|
+
raise DomainError("P4 indicator variable must be binary")
|
|
326
|
+
if active_value not in {0, 1}:
|
|
327
|
+
raise DomainError("indicator active_value must be 0 or 1")
|
|
328
|
+
if not isinstance(relation, PendingConstraint):
|
|
329
|
+
raise TypeError("Model.indicator expects a symbolic relation such as x <= b")
|
|
330
|
+
if relation.function._model is not self:
|
|
331
|
+
raise OwnershipError("indicator body belongs to another model")
|
|
332
|
+
if relation.function.shape != ():
|
|
333
|
+
raise ShapeError("P4 indicator body must be scalar")
|
|
334
|
+
if relation.relation == "le":
|
|
335
|
+
set_ = LessThan(0.0)
|
|
336
|
+
elif relation.relation == "ge":
|
|
337
|
+
set_ = GreaterThan(0.0)
|
|
338
|
+
elif relation.relation == "eq":
|
|
339
|
+
set_ = EqualTo(0.0)
|
|
340
|
+
else:
|
|
341
|
+
raise AssertionError(relation.relation)
|
|
342
|
+
constraint = IndicatorConstraint(self._new_id("c"), indicator, int(active_value), relation.function, set_, name)
|
|
343
|
+
self._constraints.append(constraint)
|
|
344
|
+
self._mark_semantic_change()
|
|
345
|
+
return constraint
|
|
346
|
+
|
|
347
|
+
def _concat(self, *values: Any) -> Expression:
|
|
348
|
+
exprs: list[Expression] = []
|
|
349
|
+
total = 0
|
|
350
|
+
deps_v: frozenset[str] = frozenset()
|
|
351
|
+
deps_p: frozenset[str] = frozenset()
|
|
352
|
+
degree = 0
|
|
353
|
+
for value in values:
|
|
354
|
+
expr = value if isinstance(value, Expression) else self.constant(value)
|
|
355
|
+
if expr._model is not self:
|
|
356
|
+
raise OwnershipError("concatenated expression belongs to another model")
|
|
357
|
+
if expr.ndim > 1:
|
|
358
|
+
raise ShapeError("P6 cone packing supports scalar/vector expressions only")
|
|
359
|
+
exprs.append(expr)
|
|
360
|
+
total += expr.size
|
|
361
|
+
deps_v = deps_v | expr.variable_dependencies
|
|
362
|
+
deps_p = deps_p | expr.parameter_dependencies
|
|
363
|
+
if expr.polynomial_degree is None:
|
|
364
|
+
degree = None
|
|
365
|
+
elif degree is not None:
|
|
366
|
+
degree = max(degree, expr.polynomial_degree)
|
|
367
|
+
curvature = Curvature.CONSTANT if degree == 0 else Curvature.AFFINE if degree is not None and degree <= 1 else Curvature.UNKNOWN
|
|
368
|
+
return Expression(self, ExprNode("concat", (total,), args=tuple(e._node for e in exprs), variable_dependencies=deps_v, parameter_dependencies=deps_p, sign=SignDomain.UNKNOWN, degree=degree, curvature=curvature))
|
|
369
|
+
|
|
370
|
+
def soc(self, t: Any, vector: Any, *, name: str | None = None) -> Constraint:
|
|
371
|
+
"""Add the exact semantic cone constraint ``||vector||_2 <= t``."""
|
|
372
|
+
t_expr = t if isinstance(t, Expression) else self.constant(t)
|
|
373
|
+
v_expr = vector if isinstance(vector, Expression) else self.constant(vector)
|
|
374
|
+
if t_expr._model is not self or v_expr._model is not self:
|
|
375
|
+
raise OwnershipError("SOC expressions must belong to this model")
|
|
376
|
+
if t_expr.shape != ():
|
|
377
|
+
raise ShapeError("SOC t must be scalar")
|
|
378
|
+
if v_expr.ndim != 1:
|
|
379
|
+
raise ShapeError("SOC vector must be one-dimensional")
|
|
380
|
+
packed = self._concat(t_expr, v_expr)
|
|
381
|
+
return self.add_in_set(packed, SecondOrderCone(packed.size), name=name)
|
|
382
|
+
|
|
383
|
+
def rotated_soc(self, u: Any, v: Any, vector: Any, *, name: str | None = None) -> Constraint:
|
|
384
|
+
"""Add ``u>=0, v>=0, 2*u*v >= ||vector||_2^2`` as a semantic cone."""
|
|
385
|
+
u_expr = u if isinstance(u, Expression) else self.constant(u)
|
|
386
|
+
v_expr = v if isinstance(v, Expression) else self.constant(v)
|
|
387
|
+
x_expr = vector if isinstance(vector, Expression) else self.constant(vector)
|
|
388
|
+
if any(e._model is not self for e in (u_expr, v_expr, x_expr)):
|
|
389
|
+
raise OwnershipError("rotated SOC expressions must belong to this model")
|
|
390
|
+
if u_expr.shape != () or v_expr.shape != ():
|
|
391
|
+
raise ShapeError("rotated SOC u and v must be scalars")
|
|
392
|
+
if x_expr.ndim != 1:
|
|
393
|
+
raise ShapeError("rotated SOC vector must be one-dimensional")
|
|
394
|
+
packed = self._concat(u_expr, v_expr, x_expr)
|
|
395
|
+
return self.add_in_set(packed, RotatedSecondOrderCone(packed.size), name=name)
|
|
396
|
+
|
|
397
|
+
def psd(self, matrix: Any, *, name: str | None = None) -> Constraint:
|
|
398
|
+
"""Add affine symmetric matrix membership in the PSD cone."""
|
|
399
|
+
expr = matrix if isinstance(matrix, Expression) else self.constant(matrix)
|
|
400
|
+
if expr._model is not self:
|
|
401
|
+
raise OwnershipError("PSD expression belongs to another model")
|
|
402
|
+
if expr.ndim != 2 or expr.shape[0] != expr.shape[1]:
|
|
403
|
+
raise ShapeError("PSD expression must be a square matrix")
|
|
404
|
+
return self.add_in_set(expr, PositiveSemidefiniteCone(expr.shape[0]), name=name)
|
|
405
|
+
|
|
406
|
+
def minimize(self, expression: Any, *, name: str | None = None) -> Objective:
|
|
407
|
+
return self._set_objective(expression, ObjectiveSense.MINIMIZE, name=name)
|
|
408
|
+
|
|
409
|
+
def maximize(self, expression: Any, *, name: str | None = None) -> Objective:
|
|
410
|
+
return self._set_objective(expression, ObjectiveSense.MAXIMIZE, name=name)
|
|
411
|
+
|
|
412
|
+
def _set_objective(self, expression: Any, sense: ObjectiveSense, *, name: str | None) -> Objective:
|
|
413
|
+
expr = expression if isinstance(expression, Expression) else self.constant(expression)
|
|
414
|
+
if expr._model is not self:
|
|
415
|
+
raise OwnershipError("objective belongs to another model")
|
|
416
|
+
if expr.shape != ():
|
|
417
|
+
raise ShapeError(f"objective must be scalar, got shape {expr.shape}")
|
|
418
|
+
objective = Objective(self._new_id("o"), expr, sense, name)
|
|
419
|
+
self._objective = objective
|
|
420
|
+
self._mark_semantic_change()
|
|
421
|
+
return objective
|
|
422
|
+
|
|
423
|
+
def compile(self, *, use_cache: bool = True, bridge_policy=None, capabilities=None):
|
|
424
|
+
from .compiler import compile_model
|
|
425
|
+
return compile_model(self, use_cache=use_cache, bridge_policy=bridge_policy, capabilities=capabilities)
|
|
426
|
+
|
|
427
|
+
def clear_compile_cache(self) -> None:
|
|
428
|
+
self._compiler_cache.clear()
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def compiler_cache_info(self):
|
|
432
|
+
from .compiler import compiler_cache_info
|
|
433
|
+
return compiler_cache_info(self)
|
|
434
|
+
|
|
435
|
+
def solve(self, **kwargs):
|
|
436
|
+
compiled = self.compile()
|
|
437
|
+
return compiled.solve(**kwargs)
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def semantic_hash(self) -> str:
|
|
441
|
+
cached = self._semantic_hash_cache
|
|
442
|
+
if cached is not None and cached[0] == self._semantic_revision:
|
|
443
|
+
return cached[1]
|
|
444
|
+
payload = self._semantic_payload(include_parameter_values=False)
|
|
445
|
+
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()
|
|
446
|
+
value = hashlib.sha256(raw).hexdigest()
|
|
447
|
+
self._semantic_hash_cache = (self._semantic_revision, value)
|
|
448
|
+
return value
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def data_hash(self) -> str:
|
|
452
|
+
cached = self._data_hash_cache
|
|
453
|
+
if cached is not None and cached[:2] == (self._semantic_revision, self._data_revision):
|
|
454
|
+
return cached[2]
|
|
455
|
+
payload = {
|
|
456
|
+
"semantic_hash": self.semantic_hash,
|
|
457
|
+
"parameters": {
|
|
458
|
+
key: _array_semantic_payload(data.value)
|
|
459
|
+
for key, data in sorted(self._parameters.items())
|
|
460
|
+
},
|
|
461
|
+
}
|
|
462
|
+
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()
|
|
463
|
+
value = hashlib.sha256(raw).hexdigest()
|
|
464
|
+
self._data_hash_cache = (self._semantic_revision, self._data_revision, value)
|
|
465
|
+
return value
|
|
466
|
+
|
|
467
|
+
def _semantic_payload(self, *, include_parameter_values: bool) -> dict[str, Any]:
|
|
468
|
+
variables = []
|
|
469
|
+
for key, data in self._variables.items():
|
|
470
|
+
variables.append({
|
|
471
|
+
"id": key,
|
|
472
|
+
"shape": list(data.shape),
|
|
473
|
+
"lower": _array_semantic_payload(data.lower),
|
|
474
|
+
"upper": _array_semantic_payload(data.upper),
|
|
475
|
+
"domain": data.domain.value,
|
|
476
|
+
})
|
|
477
|
+
parameters = []
|
|
478
|
+
for key, data in self._parameters.items():
|
|
479
|
+
item = {"id": key, "shape": list(data.shape), "sign": data.sign.value}
|
|
480
|
+
if include_parameter_values:
|
|
481
|
+
item["value"] = _array_semantic_payload(data.value)
|
|
482
|
+
parameters.append(item)
|
|
483
|
+
has_cones = any(isinstance(c.set, (SecondOrderCone, RotatedSecondOrderCone, PositiveSemidefiniteCone)) for c in self._constraints)
|
|
484
|
+
nonlinear_kinds = {"sin", "cos", "exp", "log", "sqrt", "tanh", "pow", "div"}
|
|
485
|
+
def has_nonlinear(node):
|
|
486
|
+
return node.kind in nonlinear_kinds or any(has_nonlinear(a) for a in node.args)
|
|
487
|
+
has_nlp = (self._objective is not None and has_nonlinear(self._objective.expression._node)) or any(has_nonlinear(c.function._node) for c in self._constraints if hasattr(c, "function"))
|
|
488
|
+
has_discrete = any(d.domain in {VariableDomain.INTEGER, VariableDomain.BINARY} for d in self._variables.values())
|
|
489
|
+
has_nonlinear_degree = (self._objective is not None and (self._objective.expression.polynomial_degree is None or self._objective.expression.polynomial_degree > 1)) or any((c.function.polynomial_degree is None or c.function.polynomial_degree > 1) for c in self._constraints if hasattr(c, "function"))
|
|
490
|
+
schema = "solverpilot.semantic-model.p8.v1" if (has_discrete and (has_nlp or has_nonlinear_degree)) else ("solverpilot.semantic-model.p7.v1" if has_nlp else ("solverpilot.semantic-model.p6.v1" if has_cones else "solverpilot.semantic-model.p1.v1"))
|
|
491
|
+
return {
|
|
492
|
+
"schema": schema,
|
|
493
|
+
"variables": variables,
|
|
494
|
+
"parameters": parameters,
|
|
495
|
+
"constraints": [self._serialize_constraint(c) for c in self._constraints],
|
|
496
|
+
"objective": None if self._objective is None else {
|
|
497
|
+
"id": self._objective.entity_id.value,
|
|
498
|
+
"sense": self._objective.sense.value,
|
|
499
|
+
"expression": self._serialize_node(self._objective.expression._node, include_parameter_values=False),
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
def _serialize_constraint(self, constraint: Constraint | IndicatorConstraint) -> dict[str, Any]:
|
|
504
|
+
base = {
|
|
505
|
+
"id": constraint.entity_id.value,
|
|
506
|
+
"function": self._serialize_node(constraint.function._node, include_parameter_values=False),
|
|
507
|
+
"set": self._serialize_set(constraint.set),
|
|
508
|
+
}
|
|
509
|
+
if isinstance(constraint, IndicatorConstraint):
|
|
510
|
+
base.update({
|
|
511
|
+
"kind": "IndicatorConstraint",
|
|
512
|
+
"indicator": constraint.indicator.id.value,
|
|
513
|
+
"active_value": constraint.active_value,
|
|
514
|
+
})
|
|
515
|
+
return base
|
|
516
|
+
|
|
517
|
+
def _serialize_set(self, set_: ScalarSet) -> dict[str, Any]:
|
|
518
|
+
if isinstance(set_, LessThan):
|
|
519
|
+
return {"kind": "LessThan", "upper": _canonical_float(set_.upper)}
|
|
520
|
+
if isinstance(set_, GreaterThan):
|
|
521
|
+
return {"kind": "GreaterThan", "lower": _canonical_float(set_.lower)}
|
|
522
|
+
if isinstance(set_, EqualTo):
|
|
523
|
+
return {"kind": "EqualTo", "value": _canonical_float(set_.value)}
|
|
524
|
+
from .sets import Interval
|
|
525
|
+
if isinstance(set_, Interval):
|
|
526
|
+
return {"kind": "Interval", "lower": _canonical_float(set_.lower), "upper": _canonical_float(set_.upper)}
|
|
527
|
+
if isinstance(set_, SecondOrderCone):
|
|
528
|
+
return {"kind": "SecondOrderCone", "dimension": set_.dimension}
|
|
529
|
+
if isinstance(set_, RotatedSecondOrderCone):
|
|
530
|
+
return {"kind": "RotatedSecondOrderCone", "dimension": set_.dimension}
|
|
531
|
+
if isinstance(set_, PositiveSemidefiniteCone):
|
|
532
|
+
return {"kind": "PositiveSemidefiniteCone", "dimension": set_.dimension}
|
|
533
|
+
raise TypeError(f"unsupported constraint set {type(set_).__name__}")
|
|
534
|
+
|
|
535
|
+
def _serialize_node(self, node: ExprNode, *, include_parameter_values: bool) -> dict[str, Any]:
|
|
536
|
+
out: dict[str, Any] = {"kind": node.kind, "shape": list(node.shape)}
|
|
537
|
+
if node.kind == "constant":
|
|
538
|
+
out["value"] = _array_semantic_payload(node.payload)
|
|
539
|
+
elif node.kind in {"variable", "parameter"}:
|
|
540
|
+
out["entity"] = node.payload
|
|
541
|
+
if node.kind == "parameter" and include_parameter_values:
|
|
542
|
+
out["value"] = _array_semantic_payload(self._parameters[node.payload].value)
|
|
543
|
+
elif node.kind == "index":
|
|
544
|
+
key = node.payload if isinstance(node.payload, tuple) else (node.payload,)
|
|
545
|
+
out["index"] = [
|
|
546
|
+
{"int": int(x)} if isinstance(x, (int, np.integer)) else {"slice": [x.start, x.stop, x.step]}
|
|
547
|
+
for x in key
|
|
548
|
+
]
|
|
549
|
+
elif node.kind == "sum":
|
|
550
|
+
out["axis"] = node.payload
|
|
551
|
+
elif node.kind == "pow":
|
|
552
|
+
out["exponent"] = int(node.payload)
|
|
553
|
+
out["args"] = [self._serialize_node(arg, include_parameter_values=include_parameter_values) for arg in node.args]
|
|
554
|
+
return out
|
|
555
|
+
|
|
556
|
+
def _node_sort_key(self, node: ExprNode) -> str:
|
|
557
|
+
return json.dumps(self._serialize_node(node, include_parameter_values=False), sort_keys=True, separators=(",", ":"))
|
|
558
|
+
|
|
559
|
+
def _constant_value(self, node: ExprNode) -> np.ndarray:
|
|
560
|
+
if node.kind != "constant":
|
|
561
|
+
raise DomainError("expression is not a fixed constant")
|
|
562
|
+
return np.asarray(node.payload, dtype=np.float64)
|
|
563
|
+
|
|
564
|
+
def _variable_sign(self, data: _VariableData) -> SignDomain:
|
|
565
|
+
if np.all(data.lower == 0) and np.all(data.upper == 0):
|
|
566
|
+
return SignDomain.ZERO
|
|
567
|
+
if np.all(data.lower >= 0):
|
|
568
|
+
return SignDomain.NONNEGATIVE
|
|
569
|
+
if np.all(data.upper <= 0):
|
|
570
|
+
return SignDomain.NONPOSITIVE
|
|
571
|
+
return SignDomain.UNKNOWN
|
|
572
|
+
|
|
573
|
+
def _indexed_sign(self, node: ExprNode, key: Any) -> SignDomain:
|
|
574
|
+
if node.kind == "constant":
|
|
575
|
+
return _sign_of_constant(np.asarray(node.payload)[key])
|
|
576
|
+
if node.kind == "variable":
|
|
577
|
+
data = self._variables[node.payload]
|
|
578
|
+
lo = np.asarray(data.lower)[key]
|
|
579
|
+
hi = np.asarray(data.upper)[key]
|
|
580
|
+
if np.all(lo == 0) and np.all(hi == 0):
|
|
581
|
+
return SignDomain.ZERO
|
|
582
|
+
if np.all(lo >= 0):
|
|
583
|
+
return SignDomain.NONNEGATIVE
|
|
584
|
+
if np.all(hi <= 0):
|
|
585
|
+
return SignDomain.NONPOSITIVE
|
|
586
|
+
if node.kind == "parameter":
|
|
587
|
+
return self._parameters[node.payload].sign
|
|
588
|
+
return SignDomain.UNKNOWN
|
|
589
|
+
|
|
590
|
+
def _validate_parameter_sign(self, value: np.ndarray, sign: SignDomain) -> None:
|
|
591
|
+
if sign is SignDomain.NONNEGATIVE and np.any(value < 0):
|
|
592
|
+
raise DomainError("parameter declared nonnegative has negative values")
|
|
593
|
+
if sign is SignDomain.NONPOSITIVE and np.any(value > 0):
|
|
594
|
+
raise DomainError("parameter declared nonpositive has positive values")
|
|
595
|
+
if sign is SignDomain.ZERO and np.any(value != 0):
|
|
596
|
+
raise DomainError("parameter declared zero has nonzero values")
|
|
597
|
+
|
|
598
|
+
def _set_parameter_value(self, entity: str, value: Any) -> None:
|
|
599
|
+
data = self._parameters[entity]
|
|
600
|
+
arr = _readonly_array(value, data.shape, finite=True, name="parameter value")
|
|
601
|
+
self._validate_parameter_sign(arr, data.sign)
|
|
602
|
+
if np.array_equal(arr, data.value):
|
|
603
|
+
return
|
|
604
|
+
data.value = arr
|
|
605
|
+
self._parameter_versions[entity] = self._parameter_versions.get(entity, 0) + 1
|
|
606
|
+
self._data_revision += 1
|
|
607
|
+
self._data_hash_cache = None
|
|
608
|
+
|
|
609
|
+
def _mark_semantic_change(self) -> None:
|
|
610
|
+
self._semantic_revision += 1
|
|
611
|
+
self._data_revision += 1
|
|
612
|
+
self._semantic_hash_cache = None
|
|
613
|
+
self._data_hash_cache = None
|
|
614
|
+
# Semantic changes invalidate compiler templates. Parameter value updates
|
|
615
|
+
# deliberately do not clear this cache.
|
|
616
|
+
self._compiler_cache.clear()
|
|
617
|
+
|
|
618
|
+
@classmethod
|
|
619
|
+
def from_problem(cls, problem: LinearProblem | QuadraticProblem, *, name: str | None = None) -> "Model":
|
|
620
|
+
"""Import the current legacy LP/QP execution IR into the P1 semantic layer.
|
|
621
|
+
|
|
622
|
+
This is a migration/debug path, not a replacement for the zero-overhead legacy direct path.
|
|
623
|
+
It preserves variable ordering, bounds/domains, objective convention and row bounds.
|
|
624
|
+
"""
|
|
625
|
+
from .sets import Interval
|
|
626
|
+
linear = problem.linear if isinstance(problem, QuadraticProblem) else problem
|
|
627
|
+
model = cls(linear.name if name is None else name)
|
|
628
|
+
xs: list[Variable] = []
|
|
629
|
+
for j in range(linear.n_variables):
|
|
630
|
+
xs.append(model.variable(
|
|
631
|
+
lower=float(linear.variable_lower[j]),
|
|
632
|
+
upper=float(linear.variable_upper[j]),
|
|
633
|
+
domain=VariableDomain(str(linear.domains[j])),
|
|
634
|
+
name=f"x{j}",
|
|
635
|
+
))
|
|
636
|
+
for i in range(linear.n_constraints):
|
|
637
|
+
row = linear.A.getrow(i)
|
|
638
|
+
expr: Expression = model.constant(0.0)
|
|
639
|
+
for j, value in zip(row.indices, row.data):
|
|
640
|
+
expr = expr + float(value) * xs[int(j)]
|
|
641
|
+
lo = float(linear.constraint_lower[i]); hi = float(linear.constraint_upper[i])
|
|
642
|
+
if lo == hi:
|
|
643
|
+
model.add_in_set(expr, EqualTo(lo), name=f"row{i}")
|
|
644
|
+
elif np.isneginf(lo):
|
|
645
|
+
model.add_in_set(expr, LessThan(hi), name=f"row{i}")
|
|
646
|
+
elif np.isposinf(hi):
|
|
647
|
+
model.add_in_set(expr, GreaterThan(lo), name=f"row{i}")
|
|
648
|
+
else:
|
|
649
|
+
model.add_in_set(expr, Interval(lo, hi), name=f"row{i}")
|
|
650
|
+
objective: Expression = model.constant(float(linear.objective_offset))
|
|
651
|
+
for j, value in enumerate(linear.c):
|
|
652
|
+
if value != 0.0:
|
|
653
|
+
objective = objective + float(value) * xs[j]
|
|
654
|
+
if isinstance(problem, QuadraticProblem):
|
|
655
|
+
coo = problem.P.tocoo()
|
|
656
|
+
for i, j, value in zip(coo.row, coo.col, coo.data):
|
|
657
|
+
if value != 0.0:
|
|
658
|
+
objective = objective + 0.5 * float(value) * xs[int(i)] * xs[int(j)]
|
|
659
|
+
if linear.objective_sense is ObjectiveSense.MINIMIZE:
|
|
660
|
+
model.minimize(objective)
|
|
661
|
+
else:
|
|
662
|
+
model.maximize(objective)
|
|
663
|
+
return model
|
|
664
|
+
|
|
665
|
+
def clone(self) -> "Model":
|
|
666
|
+
clone = Model(self.name)
|
|
667
|
+
node_map: dict[str, Expression] = {}
|
|
668
|
+
for key, data in self._variables.items():
|
|
669
|
+
v = clone.variable(data.shape, lower=data.lower, upper=data.upper, domain=data.domain, name=data.name)
|
|
670
|
+
node_map[key] = v
|
|
671
|
+
for key, data in self._parameters.items():
|
|
672
|
+
p = clone.parameter(data.shape, value=data.value, sign=data.sign, name=data.name)
|
|
673
|
+
node_map[key] = p
|
|
674
|
+
|
|
675
|
+
def rebuild(node: ExprNode) -> Expression:
|
|
676
|
+
if node.kind == "variable" or node.kind == "parameter":
|
|
677
|
+
return node_map[node.payload]
|
|
678
|
+
if node.kind == "constant":
|
|
679
|
+
return clone.constant(node.payload)
|
|
680
|
+
args = [rebuild(x) for x in node.args]
|
|
681
|
+
if node.kind == "add": return args[0] + args[1]
|
|
682
|
+
if node.kind == "mul": return args[0] * args[1]
|
|
683
|
+
if node.kind == "matmul": return args[0] @ args[1]
|
|
684
|
+
if node.kind == "neg": return -args[0]
|
|
685
|
+
if node.kind == "index": return args[0][node.payload]
|
|
686
|
+
if node.kind == "transpose": return args[0].T
|
|
687
|
+
if node.kind == "sum": return args[0].sum(axis=node.payload)
|
|
688
|
+
if node.kind == "concat": return clone._concat(*args)
|
|
689
|
+
raise DomainError(f"cannot clone unsupported expression node {node.kind!r}")
|
|
690
|
+
|
|
691
|
+
for c in self._constraints:
|
|
692
|
+
expr = rebuild(c.function._node)
|
|
693
|
+
if isinstance(c.set, (SecondOrderCone, RotatedSecondOrderCone, PositiveSemidefiniteCone)):
|
|
694
|
+
clone.add_in_set(expr, c.set, name=c.name)
|
|
695
|
+
continue
|
|
696
|
+
if isinstance(c.set, LessThan): relation = expr <= c.set.upper
|
|
697
|
+
elif isinstance(c.set, GreaterThan): relation = expr >= c.set.lower
|
|
698
|
+
elif isinstance(c.set, EqualTo): relation = expr == c.set.value
|
|
699
|
+
else:
|
|
700
|
+
if isinstance(c, IndicatorConstraint):
|
|
701
|
+
raise DomainError("P4 clone does not accept Interval indicator bodies")
|
|
702
|
+
clone.add(expr >= c.set.lower, name=c.name)
|
|
703
|
+
clone.add(expr <= c.set.upper, name=c.name)
|
|
704
|
+
continue
|
|
705
|
+
if isinstance(c, IndicatorConstraint):
|
|
706
|
+
new_indicator = node_map[c.indicator.id.value]
|
|
707
|
+
if not isinstance(new_indicator, Variable):
|
|
708
|
+
raise DomainError("cloned indicator did not resolve to a Variable")
|
|
709
|
+
clone.indicator(new_indicator, relation, active_value=c.active_value, name=c.name)
|
|
710
|
+
else:
|
|
711
|
+
clone.add(relation, name=c.name)
|
|
712
|
+
if self._objective is not None:
|
|
713
|
+
expr = rebuild(self._objective.expression._node)
|
|
714
|
+
if self._objective.sense is ObjectiveSense.MINIMIZE: clone.minimize(expr, name=self._objective.name)
|
|
715
|
+
else: clone.maximize(expr, name=self._objective.name)
|
|
716
|
+
return clone
|