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,22 @@
1
+ class ModelingError(Exception):
2
+ """Base error for the semantic modeling layer."""
3
+
4
+
5
+ class ShapeError(ModelingError, ValueError):
6
+ pass
7
+
8
+
9
+ class DomainError(ModelingError, ValueError):
10
+ pass
11
+
12
+
13
+ class OwnershipError(ModelingError, ValueError):
14
+ pass
15
+
16
+
17
+ class SymbolicTruthValueError(ModelingError, TypeError):
18
+ pass
19
+
20
+
21
+ class CompileError(ModelingError, ValueError):
22
+ pass
@@ -0,0 +1,373 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, Iterable
5
+
6
+ import numpy as np
7
+
8
+ from .errors import DomainError, OwnershipError, ShapeError, SymbolicTruthValueError
9
+ from .types import Curvature, SignDomain
10
+
11
+ if TYPE_CHECKING:
12
+ from .model import Model
13
+ from .sets import ScalarSet
14
+
15
+
16
+ def _shape_tuple(shape: int | Iterable[int] | tuple[int, ...] | None) -> tuple[int, ...]:
17
+ if shape is None:
18
+ return ()
19
+ if isinstance(shape, int):
20
+ if shape < 0:
21
+ raise ShapeError("shape dimensions must be nonnegative")
22
+ return (shape,)
23
+ out = tuple(int(x) for x in shape)
24
+ if any(x < 0 for x in out):
25
+ raise ShapeError("shape dimensions must be nonnegative")
26
+ return out
27
+
28
+
29
+ def _sign_of_constant(value: np.ndarray) -> SignDomain:
30
+ if value.size == 0 or np.all(value == 0):
31
+ return SignDomain.ZERO
32
+ if np.all(value >= 0):
33
+ return SignDomain.NONNEGATIVE
34
+ if np.all(value <= 0):
35
+ return SignDomain.NONPOSITIVE
36
+ return SignDomain.UNKNOWN
37
+
38
+
39
+ def _neg_sign(sign: SignDomain) -> SignDomain:
40
+ if sign is SignDomain.NONNEGATIVE:
41
+ return SignDomain.NONPOSITIVE
42
+ if sign is SignDomain.NONPOSITIVE:
43
+ return SignDomain.NONNEGATIVE
44
+ return sign
45
+
46
+
47
+ def _add_sign(a: SignDomain, b: SignDomain) -> SignDomain:
48
+ if a is SignDomain.ZERO:
49
+ return b
50
+ if b is SignDomain.ZERO:
51
+ return a
52
+ if a is b and a in {SignDomain.NONNEGATIVE, SignDomain.NONPOSITIVE}:
53
+ return a
54
+ return SignDomain.UNKNOWN
55
+
56
+
57
+ def _mul_sign(a: SignDomain, b: SignDomain) -> SignDomain:
58
+ if SignDomain.ZERO in {a, b}:
59
+ return SignDomain.ZERO
60
+ if a is SignDomain.UNKNOWN or b is SignDomain.UNKNOWN:
61
+ return SignDomain.UNKNOWN
62
+ if a is b:
63
+ return SignDomain.NONNEGATIVE
64
+ return SignDomain.NONPOSITIVE
65
+
66
+
67
+ def _broadcast_shape(a: tuple[int, ...], b: tuple[int, ...]) -> tuple[int, ...]:
68
+ try:
69
+ return tuple(np.broadcast_shapes(a, b))
70
+ except ValueError as exc:
71
+ raise ShapeError(f"cannot broadcast shapes {a} and {b}") from exc
72
+
73
+
74
+ def _matmul_shape(a: tuple[int, ...], b: tuple[int, ...]) -> tuple[int, ...]:
75
+ if len(a) == 0 or len(b) == 0:
76
+ raise ShapeError("matrix multiplication requires non-scalar operands")
77
+ if len(a) > 2 or len(b) > 2:
78
+ raise ShapeError("P1 matrix multiplication supports at most 2-D operands")
79
+ if len(a) == 1 and len(b) == 1:
80
+ if a[0] != b[0]:
81
+ raise ShapeError(f"matmul dimension mismatch: {a} @ {b}")
82
+ return ()
83
+ if len(a) == 2 and len(b) == 1:
84
+ if a[1] != b[0]:
85
+ raise ShapeError(f"matmul dimension mismatch: {a} @ {b}")
86
+ return (a[0],)
87
+ if len(a) == 1 and len(b) == 2:
88
+ if a[0] != b[0]:
89
+ raise ShapeError(f"matmul dimension mismatch: {a} @ {b}")
90
+ return (b[1],)
91
+ if a[1] != b[0]:
92
+ raise ShapeError(f"matmul dimension mismatch: {a} @ {b}")
93
+ return (a[0], b[1])
94
+
95
+
96
+ def _slice_length(sl: slice, n: int) -> int:
97
+ start, stop, step = sl.indices(n)
98
+ return len(range(start, stop, step))
99
+
100
+
101
+ def _index_shape(shape: tuple[int, ...], key: Any) -> tuple[int, ...]:
102
+ if not isinstance(key, tuple):
103
+ key = (key,)
104
+ if len(key) > len(shape):
105
+ raise ShapeError("too many indices for expression")
106
+ out: list[int] = []
107
+ for dim, item in zip(shape, key):
108
+ if isinstance(item, (int, np.integer)):
109
+ idx = int(item)
110
+ if idx < -dim or idx >= dim:
111
+ raise ShapeError("expression index out of range")
112
+ elif isinstance(item, slice):
113
+ out.append(_slice_length(item, dim))
114
+ else:
115
+ raise ShapeError("P1 supports integer and slice indexing only")
116
+ out.extend(shape[len(key):])
117
+ return tuple(out)
118
+
119
+
120
+ @dataclass(frozen=True, slots=True, eq=False)
121
+ class ExprNode:
122
+ kind: str
123
+ shape: tuple[int, ...]
124
+ args: tuple["ExprNode", ...] = ()
125
+ payload: Any = None
126
+ variable_dependencies: frozenset[str] = frozenset()
127
+ parameter_dependencies: frozenset[str] = frozenset()
128
+ sign: SignDomain = SignDomain.UNKNOWN
129
+ degree: int | None = None
130
+ curvature: Curvature = Curvature.UNKNOWN
131
+
132
+
133
+ class Expression:
134
+ __slots__ = ("_model", "_node")
135
+ __array_priority__ = 10000
136
+
137
+ def __init__(self, model: "Model", node: ExprNode) -> None:
138
+ self._model = model
139
+ self._node = node
140
+
141
+ @property
142
+ def shape(self) -> tuple[int, ...]:
143
+ return self._node.shape
144
+
145
+ @property
146
+ def ndim(self) -> int:
147
+ return len(self.shape)
148
+
149
+ @property
150
+ def size(self) -> int:
151
+ return int(np.prod(self.shape, dtype=np.int64)) if self.shape else 1
152
+
153
+ @property
154
+ def variable_dependencies(self) -> frozenset[str]:
155
+ return self._node.variable_dependencies
156
+
157
+ @property
158
+ def parameter_dependencies(self) -> frozenset[str]:
159
+ return self._node.parameter_dependencies
160
+
161
+ @property
162
+ def sign(self) -> SignDomain:
163
+ return self._node.sign
164
+
165
+ @property
166
+ def polynomial_degree(self) -> int | None:
167
+ return self._node.degree
168
+
169
+ @property
170
+ def curvature(self) -> Curvature:
171
+ return self._node.curvature
172
+
173
+ def _coerce(self, other: Any) -> "Expression":
174
+ if isinstance(other, Expression):
175
+ if other._model is not self._model:
176
+ raise OwnershipError("cross-model expression reference is not allowed")
177
+ return other
178
+ return self._model.constant(other)
179
+
180
+ def _binary(self, other: Any, kind: str) -> "Expression":
181
+ rhs = self._coerce(other)
182
+ if kind in {"add", "mul"}:
183
+ shape = _broadcast_shape(self.shape, rhs.shape)
184
+ elif kind == "matmul":
185
+ shape = _matmul_shape(self.shape, rhs.shape)
186
+ else:
187
+ raise AssertionError(kind)
188
+ deps_v = self.variable_dependencies | rhs.variable_dependencies
189
+ deps_p = self.parameter_dependencies | rhs.parameter_dependencies
190
+ if kind == "add":
191
+ degree = None if None in {self.polynomial_degree, rhs.polynomial_degree} else max(self.polynomial_degree, rhs.polynomial_degree)
192
+ sign = _add_sign(self.sign, rhs.sign)
193
+ elif kind == "mul":
194
+ degree = None if None in {self.polynomial_degree, rhs.polynomial_degree} else self.polynomial_degree + rhs.polynomial_degree
195
+ sign = _mul_sign(self.sign, rhs.sign)
196
+ else:
197
+ degree = None if None in {self.polynomial_degree, rhs.polynomial_degree} else self.polynomial_degree + rhs.polynomial_degree
198
+ sign = SignDomain.UNKNOWN
199
+ curvature = Curvature.CONSTANT if degree == 0 else Curvature.AFFINE if degree is not None and degree <= 1 else Curvature.UNKNOWN
200
+ args = (self._node, rhs._node)
201
+ if kind in {"add", "mul"}:
202
+ args = tuple(sorted(args, key=self._model._node_sort_key))
203
+ return Expression(self._model, ExprNode(kind, shape, args=args, variable_dependencies=deps_v, parameter_dependencies=deps_p, sign=sign, degree=degree, curvature=curvature))
204
+
205
+ def __add__(self, other: Any) -> "Expression":
206
+ return self._binary(other, "add")
207
+
208
+ def __radd__(self, other: Any) -> "Expression":
209
+ return self._coerce(other)._binary(self, "add")
210
+
211
+ def __sub__(self, other: Any) -> "Expression":
212
+ return self + (-self._coerce(other))
213
+
214
+ def __rsub__(self, other: Any) -> "Expression":
215
+ return self._coerce(other) + (-self)
216
+
217
+ def __neg__(self) -> "Expression":
218
+ degree = self.polynomial_degree
219
+ curvature = Curvature.CONSTANT if degree == 0 else Curvature.AFFINE if degree is not None and degree <= 1 else Curvature.UNKNOWN
220
+ return Expression(self._model, ExprNode("neg", self.shape, args=(self._node,), variable_dependencies=self.variable_dependencies, parameter_dependencies=self.parameter_dependencies, sign=_neg_sign(self.sign), degree=degree, curvature=curvature))
221
+
222
+ def __mul__(self, other: Any) -> "Expression":
223
+ return self._binary(other, "mul")
224
+
225
+ def __rmul__(self, other: Any) -> "Expression":
226
+ return self._coerce(other)._binary(self, "mul")
227
+
228
+ def __truediv__(self, other: Any) -> "Expression":
229
+ rhs = self._coerce(other)
230
+ if not rhs.variable_dependencies and not rhs.parameter_dependencies and rhs.polynomial_degree == 0:
231
+ value = self._model._constant_value(rhs._node)
232
+ if np.any(value == 0):
233
+ raise DomainError("division by zero")
234
+ return self * self._model.constant(1.0 / value)
235
+ shape = _broadcast_shape(self.shape, rhs.shape)
236
+ return Expression(self._model, ExprNode("div", shape, args=(self._node, rhs._node), variable_dependencies=self.variable_dependencies | rhs.variable_dependencies, parameter_dependencies=self.parameter_dependencies | rhs.parameter_dependencies, sign=SignDomain.UNKNOWN, degree=None, curvature=Curvature.UNKNOWN))
237
+
238
+ def __rtruediv__(self, other: Any) -> "Expression":
239
+ return self._coerce(other).__truediv__(self)
240
+
241
+ def __matmul__(self, other: Any) -> "Expression":
242
+ return self._binary(other, "matmul")
243
+
244
+ def __rmatmul__(self, other: Any) -> "Expression":
245
+ return self._coerce(other)._binary(self, "matmul")
246
+
247
+ def __pow__(self, exponent: int) -> "Expression":
248
+ exponent = int(exponent)
249
+ if exponent < 0:
250
+ raise DomainError("negative symbolic powers are outside P7 smooth-core scope")
251
+ if exponent == 0:
252
+ return self._model.constant(np.ones(self.shape if self.shape else (), dtype=float))
253
+ if exponent == 1:
254
+ return self
255
+ if exponent == 2:
256
+ return self * self
257
+ deps_v = self.variable_dependencies
258
+ deps_p = self.parameter_dependencies
259
+ degree = None if self.polynomial_degree is None else self.polynomial_degree * exponent
260
+ sign = SignDomain.NONNEGATIVE if exponent % 2 == 0 else self.sign
261
+ return Expression(self._model, ExprNode("pow", self.shape, args=(self._node,), payload=exponent, variable_dependencies=deps_v, parameter_dependencies=deps_p, sign=sign, degree=degree, curvature=Curvature.UNKNOWN))
262
+
263
+ def _smooth_unary(self, kind: str) -> "Expression":
264
+ sign = SignDomain.UNKNOWN
265
+ if kind in {"exp", "sqrt"}:
266
+ sign = SignDomain.NONNEGATIVE
267
+ return Expression(self._model, ExprNode(kind, self.shape, args=(self._node,), variable_dependencies=self.variable_dependencies, parameter_dependencies=self.parameter_dependencies, sign=sign, degree=None, curvature=Curvature.UNKNOWN))
268
+
269
+ def sin(self) -> "Expression": return self._smooth_unary("sin")
270
+ def cos(self) -> "Expression": return self._smooth_unary("cos")
271
+ def exp(self) -> "Expression": return self._smooth_unary("exp")
272
+ def log(self) -> "Expression": return self._smooth_unary("log")
273
+ def sqrt(self) -> "Expression": return self._smooth_unary("sqrt")
274
+ def tanh(self) -> "Expression": return self._smooth_unary("tanh")
275
+
276
+ def __getitem__(self, key: Any) -> "Expression":
277
+ shape = _index_shape(self.shape, key)
278
+ sign = self.sign
279
+ if self._node.kind in {"variable", "parameter", "constant"}:
280
+ sign = self._model._indexed_sign(self._node, key)
281
+ return Expression(self._model, ExprNode("index", shape, args=(self._node,), payload=key, variable_dependencies=self.variable_dependencies, parameter_dependencies=self.parameter_dependencies, sign=sign, degree=self.polynomial_degree, curvature=self.curvature))
282
+
283
+ @property
284
+ def T(self) -> "Expression":
285
+ if self.ndim < 2:
286
+ return self
287
+ if self.ndim != 2:
288
+ raise ShapeError("P1 transpose supports at most 2-D expressions")
289
+ return Expression(self._model, ExprNode("transpose", (self.shape[1], self.shape[0]), args=(self._node,), variable_dependencies=self.variable_dependencies, parameter_dependencies=self.parameter_dependencies, sign=self.sign, degree=self.polynomial_degree, curvature=self.curvature))
290
+
291
+ def sum(self, axis: int | None = None) -> "Expression":
292
+ if axis is not None:
293
+ axis = int(axis)
294
+ if axis < 0:
295
+ axis += self.ndim
296
+ if axis < 0 or axis >= self.ndim:
297
+ raise ShapeError("sum axis out of range")
298
+ shape = self.shape[:axis] + self.shape[axis + 1:]
299
+ else:
300
+ shape = ()
301
+ return Expression(self._model, ExprNode("sum", shape, args=(self._node,), payload=axis, variable_dependencies=self.variable_dependencies, parameter_dependencies=self.parameter_dependencies, sign=self.sign, degree=self.polynomial_degree, curvature=self.curvature))
302
+
303
+ def __le__(self, other: Any):
304
+ from .model import PendingConstraint
305
+ rhs = self._coerce(other)
306
+ return PendingConstraint(self - rhs, "le")
307
+
308
+ def __ge__(self, other: Any):
309
+ from .model import PendingConstraint
310
+ rhs = self._coerce(other)
311
+ return PendingConstraint(self - rhs, "ge")
312
+
313
+ def __eq__(self, other: Any): # type: ignore[override]
314
+ from .model import PendingConstraint
315
+ rhs = self._coerce(other)
316
+ return PendingConstraint(self - rhs, "eq")
317
+
318
+ def __bool__(self) -> bool:
319
+ raise SymbolicTruthValueError("symbolic expressions/relations cannot be converted to bool")
320
+
321
+ def __float__(self) -> float:
322
+ raise TypeError("symbolic expressions cannot be converted to float")
323
+
324
+ def __array__(self, dtype=None):
325
+ raise TypeError("symbolic expressions cannot be converted to NumPy arrays")
326
+
327
+ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
328
+ if method != "__call__" or kwargs.get("out") is not None:
329
+ return NotImplemented
330
+ if ufunc is np.add:
331
+ return _as_expression(inputs[0], self._model) + inputs[1]
332
+ if ufunc is np.subtract:
333
+ return _as_expression(inputs[0], self._model) - inputs[1]
334
+ if ufunc is np.multiply:
335
+ return _as_expression(inputs[0], self._model) * inputs[1]
336
+ if ufunc is np.divide:
337
+ return _as_expression(inputs[0], self._model) / inputs[1]
338
+ if ufunc is np.matmul:
339
+ return _as_expression(inputs[0], self._model) @ inputs[1]
340
+ if ufunc is np.negative:
341
+ return -_as_expression(inputs[0], self._model)
342
+ if ufunc is np.sin:
343
+ return _as_expression(inputs[0], self._model).sin()
344
+ if ufunc is np.cos:
345
+ return _as_expression(inputs[0], self._model).cos()
346
+ if ufunc is np.exp:
347
+ return _as_expression(inputs[0], self._model).exp()
348
+ if ufunc is np.log:
349
+ return _as_expression(inputs[0], self._model).log()
350
+ if ufunc is np.sqrt:
351
+ return _as_expression(inputs[0], self._model).sqrt()
352
+ if ufunc is np.tanh:
353
+ return _as_expression(inputs[0], self._model).tanh()
354
+ return NotImplemented
355
+
356
+ def __array_function__(self, func, types, args, kwargs):
357
+ if func is np.sum:
358
+ expr = _as_expression(args[0], self._model)
359
+ return expr.sum(axis=kwargs.get("axis"))
360
+ if func is np.dot:
361
+ return _as_expression(args[0], self._model) @ args[1]
362
+ return NotImplemented
363
+
364
+ def __repr__(self) -> str:
365
+ return f"Expression(kind={self._node.kind!r}, shape={self.shape}, degree={self.polynomial_degree})"
366
+
367
+
368
+ def _as_expression(value: Any, model: "Model") -> Expression:
369
+ if isinstance(value, Expression):
370
+ if value._model is not model:
371
+ raise OwnershipError("cross-model expression reference is not allowed")
372
+ return value
373
+ return model.constant(value)