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,60 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class LessThan:
8
+ upper: float = 0.0
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class GreaterThan:
13
+ lower: float = 0.0
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class EqualTo:
18
+ value: float = 0.0
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class Interval:
23
+ lower: float
24
+ upper: float
25
+
26
+
27
+ ScalarSet = LessThan | GreaterThan | EqualTo | Interval
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class SecondOrderCone:
32
+ dimension: int
33
+
34
+ def __post_init__(self) -> None:
35
+ if int(self.dimension) < 2:
36
+ raise ValueError("SecondOrderCone dimension must be >= 2")
37
+ object.__setattr__(self, "dimension", int(self.dimension))
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class RotatedSecondOrderCone:
42
+ dimension: int
43
+
44
+ def __post_init__(self) -> None:
45
+ if int(self.dimension) < 3:
46
+ raise ValueError("RotatedSecondOrderCone dimension must be >= 3")
47
+ object.__setattr__(self, "dimension", int(self.dimension))
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class PositiveSemidefiniteCone:
52
+ dimension: int
53
+
54
+ def __post_init__(self) -> None:
55
+ if int(self.dimension) < 1:
56
+ raise ValueError("PositiveSemidefiniteCone dimension must be >= 1")
57
+ object.__setattr__(self, "dimension", int(self.dimension))
58
+
59
+
60
+ ConstraintSet = ScalarSet | SecondOrderCone | RotatedSecondOrderCone | PositiveSemidefiniteCone
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+
7
+ @dataclass(frozen=True, slots=True, order=True)
8
+ class EntityId:
9
+ value: str
10
+ namespace: str = ""
11
+
12
+ def __str__(self) -> str:
13
+ return self.value
14
+
15
+ @property
16
+ def qualified(self) -> str:
17
+ return f"{self.namespace}:{self.value}" if self.namespace else self.value
18
+
19
+
20
+ class SignDomain(str, Enum):
21
+ UNKNOWN = "unknown"
22
+ ZERO = "zero"
23
+ NONNEGATIVE = "nonnegative"
24
+ NONPOSITIVE = "nonpositive"
25
+
26
+
27
+ class Curvature(str, Enum):
28
+ CONSTANT = "constant"
29
+ AFFINE = "affine"
30
+ UNKNOWN = "unknown"
31
+
32
+
33
+ class NumericType(str, Enum):
34
+ REAL = "real"
35
+ INTEGER = "integer"
36
+ BINARY = "binary"
@@ -0,0 +1,7 @@
1
+ from .ir import NLPConstraintBlock, NLPProblem
2
+ from .ad import NLPDerivativeEngine, evaluate_node
3
+ from .compiler import compile_nlp_model
4
+ from .validation import NLPValidationReport, validate_nlp_solution
5
+ from .backend import CasadiIpoptBackend, NLPSolveResult
6
+ from .conformance import NLPConformanceCheck, NLPConformanceReport, conform_casadi_ipopt_backend
7
+ __all__=['NLPConstraintBlock','NLPProblem','NLPDerivativeEngine','evaluate_node','compile_nlp_model','NLPValidationReport','validate_nlp_solution','CasadiIpoptBackend','NLPSolveResult','NLPConformanceCheck','NLPConformanceReport','conform_casadi_ipopt_backend']
solverpilot/nlp/ad.py ADDED
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+ import numpy as np
6
+
7
+ from solverpilot.model.expression import ExprNode
8
+ from .ir import NLPProblem
9
+
10
+
11
+ def _reshape_row_major_ca(ca, vec, shape):
12
+ if shape == ():
13
+ return vec[0] if hasattr(vec, '__len__') else vec
14
+ if len(shape) == 1:
15
+ return ca.reshape(vec, shape[0], 1)
16
+ if len(shape) == 2:
17
+ r, c = shape
18
+ return ca.reshape(vec, c, r).T
19
+ raise ValueError("P7 supports at most 2-D expressions")
20
+
21
+
22
+ def _flatten_row_major_ca(ca, expr, shape):
23
+ if shape == ():
24
+ return ca.vertcat(expr)
25
+ if len(shape) == 1:
26
+ return ca.reshape(expr, shape[0], 1)
27
+ if len(shape) == 2:
28
+ return ca.reshape(expr.T, shape[0]*shape[1], 1)
29
+ raise ValueError("P7 supports at most 2-D expressions")
30
+
31
+
32
+ def _eval_np(node: ExprNode, x: np.ndarray, problem: NLPProblem):
33
+ kind = node.kind
34
+ if kind == 'constant': return np.asarray(node.payload, dtype=float)
35
+ if kind == 'parameter': return np.asarray(problem.parameter_values[str(node.payload)], dtype=float)
36
+ if kind == 'variable':
37
+ off, shape = problem.variable_layout[str(node.payload)]
38
+ size = int(np.prod(shape)) if shape else 1
39
+ arr = np.asarray(x[off:off+size], dtype=float)
40
+ return arr.reshape(shape if shape else ())
41
+ args = [_eval_np(a, x, problem) for a in node.args]
42
+ if kind == 'index': return args[0][node.payload]
43
+ if kind == 'transpose': return np.asarray(args[0]).T
44
+ if kind == 'sum': return np.sum(args[0], axis=node.payload)
45
+ if kind == 'neg': return -args[0]
46
+ if kind == 'add': return args[0] + args[1]
47
+ if kind == 'mul': return args[0] * args[1]
48
+ if kind == 'div': return args[0] / args[1]
49
+ if kind == 'matmul': return args[0] @ args[1]
50
+ if kind == 'pow': return np.power(args[0], int(node.payload))
51
+ if kind == 'sin': return np.sin(args[0])
52
+ if kind == 'cos': return np.cos(args[0])
53
+ if kind == 'exp': return np.exp(args[0])
54
+ if kind == 'log': return np.log(args[0])
55
+ if kind == 'sqrt': return np.sqrt(args[0])
56
+ if kind == 'tanh': return np.tanh(args[0])
57
+ raise ValueError(f'unsupported P7 expression kind {kind!r}')
58
+
59
+
60
+ def evaluate_node(problem: NLPProblem, node: ExprNode, x: np.ndarray):
61
+ with np.errstate(all='ignore'):
62
+ return _eval_np(node, np.asarray(x, dtype=float).reshape(-1), problem)
63
+
64
+
65
+ def _to_ca(ca, node: ExprNode, x, problem: NLPProblem, cache: dict[int, Any]):
66
+ key = id(node)
67
+ if key in cache: return cache[key]
68
+ kind = node.kind
69
+ if kind == 'constant':
70
+ arr = np.asarray(node.payload, dtype=float)
71
+ out = ca.DM(float(arr)) if arr.shape == () else ca.DM(arr if arr.ndim == 2 else arr.reshape(-1,1))
72
+ elif kind == 'parameter':
73
+ arr = np.asarray(problem.parameter_values[str(node.payload)], dtype=float)
74
+ out = ca.DM(float(arr)) if arr.shape == () else ca.DM(arr if arr.ndim == 2 else arr.reshape(-1,1))
75
+ elif kind == 'variable':
76
+ off, shape = problem.variable_layout[str(node.payload)]
77
+ size = int(np.prod(shape)) if shape else 1
78
+ out = _reshape_row_major_ca(ca, x[off:off+size], shape)
79
+ else:
80
+ args = [_to_ca(ca, a, x, problem, cache) for a in node.args]
81
+ if kind == 'index':
82
+ out = args[0][node.payload]
83
+ elif kind == 'transpose': out = args[0].T
84
+ elif kind == 'sum':
85
+ if node.payload is None: out = ca.sum1(ca.sum2(args[0]))
86
+ elif int(node.payload) == 0: out = ca.sum1(args[0])
87
+ elif int(node.payload) == 1: out = ca.sum2(args[0])
88
+ else: raise ValueError('P7 sum supports axes 0/1')
89
+ elif kind == 'neg': out = -args[0]
90
+ elif kind == 'add': out = args[0] + args[1]
91
+ elif kind == 'mul': out = args[0] * args[1]
92
+ elif kind == 'div': out = args[0] / args[1]
93
+ elif kind == 'matmul':
94
+ a0, a1 = node.args
95
+ left = args[0].T if len(a0.shape) == 1 and len(a1.shape) == 2 else args[0]
96
+ out = ca.mtimes(left, args[1])
97
+ elif kind == 'pow': out = ca.power(args[0], int(node.payload))
98
+ elif kind == 'sin': out = ca.sin(args[0])
99
+ elif kind == 'cos': out = ca.cos(args[0])
100
+ elif kind == 'exp': out = ca.exp(args[0])
101
+ elif kind == 'log': out = ca.log(args[0])
102
+ elif kind == 'sqrt': out = ca.sqrt(args[0])
103
+ elif kind == 'tanh': out = ca.tanh(args[0])
104
+ else: raise ValueError(f'unsupported P7 expression kind {kind!r}')
105
+ cache[key] = out
106
+ return out
107
+
108
+
109
+ @dataclass
110
+ class NLPDerivativeEngine:
111
+ problem: NLPProblem
112
+
113
+ def __post_init__(self) -> None:
114
+ import casadi as ca
115
+ self.ca = ca
116
+ self.x = ca.MX.sym('x', self.problem.n_variables)
117
+ cache: dict[int, Any] = {}
118
+ f_raw = _to_ca(ca, self.problem.objective_node, self.x, self.problem, cache)
119
+ obj_size = int(np.prod(self.problem.objective_node.shape)) if self.problem.objective_node.shape else 1
120
+ if obj_size != 1:
121
+ raise ValueError('NLP objective must be scalar')
122
+ self.f = ca.reshape(f_raw, 1, 1)[0]
123
+ g_parts = []
124
+ for block in self.problem.constraints:
125
+ e = _to_ca(ca, block.node, self.x, self.problem, cache)
126
+ g_parts.append(_flatten_row_major_ca(ca, e, block.shape))
127
+ self.g = ca.vertcat(*g_parts) if g_parts else ca.MX.zeros(0,1)
128
+ self.grad = ca.gradient(self.f, self.x)
129
+ self.jac = ca.jacobian(self.g, self.x)
130
+ self.lam = ca.MX.sym('lam', self.problem.n_constraints)
131
+ lag = self.f + ca.dot(self.lam, self.g) if self.problem.n_constraints else self.f
132
+ self.hess = ca.hessian(lag, self.x)[0]
133
+ self._fun = ca.Function('solverpilot_p7_eval', [self.x], [self.f, self.g, self.grad, self.jac])
134
+ self._hfun = ca.Function('solverpilot_p7_hess', [self.x, self.lam], [self.hess])
135
+
136
+ def evaluate(self, x):
137
+ out = self._fun(np.asarray(x,dtype=float).reshape(-1))
138
+ return (float(out[0]), np.asarray(out[1],dtype=float).reshape(-1), np.asarray(out[2],dtype=float).reshape(-1), np.asarray(out[3],dtype=float))
139
+
140
+ def objective(self, x) -> float:
141
+ return self.evaluate(x)[0]
142
+ def constraints(self, x) -> np.ndarray:
143
+ return self.evaluate(x)[1]
144
+ def gradient(self, x) -> np.ndarray:
145
+ return self.evaluate(x)[2]
146
+ def jacobian(self, x) -> np.ndarray:
147
+ return self.evaluate(x)[3]
148
+ def lagrangian_hessian(self, x, multipliers=None) -> np.ndarray:
149
+ lam = np.zeros(self.problem.n_constraints) if multipliers is None else np.asarray(multipliers,dtype=float).reshape(-1)
150
+ return np.asarray(self._hfun(np.asarray(x,dtype=float).reshape(-1), lam), dtype=float)
151
+ def jacobian_sparsity(self) -> tuple[tuple[int,int], ...]:
152
+ sp = self.jac.sparsity(); rows, cols = sp.get_triplet()[0:2]
153
+ return tuple(zip(map(int, rows), map(int, cols)))
154
+ def hessian_sparsity(self) -> tuple[tuple[int,int], ...]:
155
+ sp = self.hess.sparsity(); rows, cols = sp.get_triplet()[0:2]
156
+ return tuple(zip(map(int, rows), map(int, cols)))
157
+ def jvp(self, x, v) -> np.ndarray:
158
+ import casadi as ca
159
+ v_sym = ca.MX.sym('v', self.problem.n_variables)
160
+ fun = ca.Function('solverpilot_p7_jvp', [self.x, v_sym], [ca.jtimes(self.g, self.x, v_sym)])
161
+ return np.asarray(fun(np.asarray(x,float), np.asarray(v,float)), dtype=float).reshape(-1)
162
+ def vjp(self, x, w) -> np.ndarray:
163
+ import casadi as ca
164
+ w_sym = ca.MX.sym('w', self.problem.n_constraints)
165
+ fun = ca.Function('solverpilot_p7_vjp', [self.x, w_sym], [ca.jtimes(self.g, self.x, w_sym, True)])
166
+ return np.asarray(fun(np.asarray(x,float), np.asarray(w,float)), dtype=float).reshape(-1)
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+ import importlib.util
5
+ import numpy as np
6
+ from .ad import NLPDerivativeEngine, evaluate_node
7
+ from .ir import NLPProblem
8
+ from .validation import NLPValidationReport, validate_nlp_solution
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class NLPSolveResult:
12
+ backend:str
13
+ backend_status:str
14
+ x:np.ndarray|None
15
+ objective_reported:float|None
16
+ validation:NLPValidationReport
17
+ local_optimal_candidate:bool
18
+ globally_proven:bool
19
+ multipliers_g:np.ndarray|None
20
+ multipliers_x:np.ndarray|None
21
+ kkt_stationarity_inf:float|None
22
+ raw_statistics:dict[str,Any]
23
+
24
+ @dataclass(slots=True)
25
+ class CasadiIpoptBackend:
26
+ max_iter:int=3000
27
+ tol:float=1e-9
28
+ print_level:int=0
29
+ linear_solver:str|None=None
30
+ @property
31
+ def name(self): return 'casadi-ipopt-nlp-bridge'
32
+ @property
33
+ def binding_version(self):
34
+ try:
35
+ import casadi as ca; return ca.__version__
36
+ except Exception: return None
37
+ def is_available(self):
38
+ if importlib.util.find_spec('casadi') is None: return False
39
+ try:
40
+ import casadi as ca; return bool(ca.has_nlpsol('ipopt'))
41
+ except Exception: return False
42
+ @property
43
+ def capability_manifest_v2(self):
44
+ from solverpilot import __version__
45
+ from solverpilot.capabilities.v2 import BackendCapabilityManifestV2,CapabilityClaim,CapabilityEvidence,CapabilityKey,CapabilityMode,CapabilityStatus,VerificationLevel
46
+ bv=f'bundled-with-casadi-{self.binding_version or "unknown"}'
47
+ ev=CapabilityEvidence('p7-casadi-ipopt-conformance-v1','runtime-conformance',(bv,),(() if self.binding_version is None else (self.binding_version,)),(__version__,),'2026-09-06',('Ipopt local NLP solve + SolverPilot independent validation + derivative cross-check',))
48
+ def v(restr=None):
49
+ restr={} if restr is None else restr
50
+ return CapabilityClaim(CapabilityStatus.RESTRICTED if restr else CapabilityStatus.SUPPORTED,CapabilityMode.EMULATED_SAFE,VerificationLevel.VERIFIED,restr,(ev,))
51
+ claims={CapabilityKey.PROBLEM_NLP:v({'continuous_only':True,'local_solver':True,'global_optimality_not_claimed':True}),CapabilityKey.CONSTRAINT_NONLINEAR:v(),CapabilityKey.DERIVATIVE_GRADIENT:v(),CapabilityKey.DERIVATIVE_JACOBIAN:v(),CapabilityKey.DERIVATIVE_HESSIAN:v(),CapabilityKey.RESULT_PRIMAL:v(),CapabilityKey.RESULT_OBJECTIVE:v(),CapabilityKey.RESULT_DUAL:v({'backend_multiplier_sign_convention':'Ipopt/CasADi'}),CapabilityKey.START_PRIMAL:v()}
52
+ return BackendCapabilityManifestV2(self.name,bv,'casadi',self.binding_version,__version__,claims=claims,metadata={'plugin':'ipopt','local_nlp':True})
53
+ def solve(self,problem:NLPProblem,*,x0=None):
54
+ if not self.is_available(): raise RuntimeError('CasADi Ipopt plugin unavailable')
55
+ import casadi as ca
56
+ eng=NLPDerivativeEngine(problem)
57
+ nlp={'x':eng.x,'f':eng.f}
58
+ if problem.n_constraints: nlp['g']=eng.g
59
+ opts={'print_time':False,'ipopt.print_level':int(self.print_level),'ipopt.max_iter':int(self.max_iter),'ipopt.tol':float(self.tol)}
60
+ if self.linear_solver: opts['ipopt.linear_solver']=self.linear_solver
61
+ solver=ca.nlpsol('solverpilot_p7_ipopt','ipopt',nlp,opts)
62
+ initial_search_attempts=0
63
+ if x0 is None:
64
+ lo,hi=problem.variable_lower,problem.variable_upper
65
+ midpoint=np.where(np.isfinite(lo)&np.isfinite(hi),0.5*(lo+hi),np.where(np.isfinite(lo),np.maximum(lo,0),np.where(np.isfinite(hi),np.minimum(hi,0),0.0)))
66
+ candidates=[midpoint]
67
+ # Deterministic interior-biased alternatives. They are only a starting-point
68
+ # strategy; final validity still comes from independent original-space validation.
69
+ for frac in (0.25,0.75):
70
+ cand=np.where(np.isfinite(lo)&np.isfinite(hi),lo+frac*(hi-lo),midpoint)
71
+ candidates.append(cand)
72
+ candidates.append(np.clip(np.ones(problem.n_variables),lo,hi))
73
+ candidates.append(np.clip(-np.ones(problem.n_variables),lo,hi))
74
+ x0=midpoint
75
+ for cand in candidates:
76
+ initial_search_attempts += 1
77
+ try:
78
+ f=evaluate_node(problem,problem.objective_node,cand)
79
+ gs=[evaluate_node(problem,b.node,cand) for b in problem.constraints]
80
+ if np.isfinite(np.asarray(f,dtype=float)).all() and all(np.isfinite(np.asarray(g,dtype=float)).all() for g in gs):
81
+ x0=np.asarray(cand,dtype=float); break
82
+ except (FloatingPointError,ValueError,TypeError,OverflowError):
83
+ continue
84
+ kw={'x0':np.asarray(x0,float),'lbx':problem.variable_lower,'ubx':problem.variable_upper}
85
+ if problem.n_constraints: kw.update(lbg=problem.constraint_lower,ubg=problem.constraint_upper)
86
+ result=None; err=None
87
+ try: result=solver(**kw)
88
+ except Exception as e: err=f'{type(e).__name__}: {e}'
89
+ try: stats=dict(solver.stats())
90
+ except Exception: stats={}
91
+ x=None; obj=None; lg=None; lx=None
92
+ if result is not None:
93
+ if 'x' in result:
94
+ arr=np.asarray(result['x'],dtype=float).reshape(-1)
95
+ if arr.shape==(problem.n_variables,) and np.all(np.isfinite(arr)): x=arr
96
+ if 'f' in result:
97
+ try: obj=float(result['f'])
98
+ except Exception: pass
99
+ if 'lam_g' in result: lg=np.asarray(result['lam_g'],dtype=float).reshape(-1)
100
+ if 'lam_x' in result: lx=np.asarray(result['lam_x'],dtype=float).reshape(-1)
101
+ validation=validate_nlp_solution(problem,x,objective_reported=obj,atol=max(1e-7,self.tol*100),rtol=max(1e-7,self.tol*100))
102
+ raw=str(stats.get('return_status',''))
103
+ stationarity=None
104
+ if x is not None and lg is not None and lx is not None:
105
+ try:
106
+ grad=eng.gradient(x); jac=eng.jacobian(x)
107
+ stationarity=float(np.max(np.abs(grad + jac.T @ lg + lx)))
108
+ except Exception:
109
+ stationarity=None
110
+ kkt_tol=max(1e-6,self.tol*1000)
111
+ success=bool(stats.get('success')) and validation.valid and stationarity is not None and stationarity <= kkt_tol
112
+ stats.update({'error':err,'initial_point_domain_search_attempts':initial_search_attempts,'domain_hazards':list(problem.metadata.get('domain_hazards',())),'casadi_version':self.binding_version,'nlpsol_plugin':'ipopt','global_optimality_claimed':False,'kkt_stationarity_inf':stationarity,'kkt_stationarity_tolerance':kkt_tol})
113
+ return NLPSolveResult(self.name,raw,x,obj,validation,success,False,lg,lx,stationarity,stats)
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+ from dataclasses import replace
3
+ import numpy as np
4
+ from solverpilot.model.errors import CompileError
5
+ from solverpilot.model.sets import LessThan, GreaterThan, EqualTo, Interval
6
+ from solverpilot.problem import ObjectiveSense, VariableDomain
7
+ from solverpilot.model.compiler import CompiledModel, CompilationReport, _variable_layout
8
+ from .ir import NLPConstraintBlock, NLPProblem
9
+
10
+
11
+ def _broadcast_bound(value, size):
12
+ arr=np.asarray(value,dtype=float)
13
+ try: return np.broadcast_to(arr,(size,)).astype(float,copy=True)
14
+ except Exception as e: raise CompileError('NLP set bound cannot broadcast to expression size') from e
15
+
16
+
17
+ def _node_interval(node, *, variable_layout, variable_lower, variable_upper, parameter_values):
18
+ """Conservative scalar interval over all elements of an expression node.
19
+
20
+ ``None`` means the compiler cannot prove a safe range from declared variable
21
+ bounds and frozen parameter values. The analysis is intentionally fail-closed.
22
+ """
23
+ kind=node.kind
24
+ if kind=='constant':
25
+ a=np.asarray(node.payload,dtype=float); return (float(np.min(a)),float(np.max(a)))
26
+ if kind=='parameter':
27
+ a=np.asarray(parameter_values[str(node.payload)],dtype=float); return (float(np.min(a)),float(np.max(a)))
28
+ if kind=='variable':
29
+ off,shape=variable_layout[str(node.payload)]; size=int(np.prod(shape)) if shape else 1
30
+ return (float(np.min(variable_lower[off:off+size])),float(np.max(variable_upper[off:off+size])))
31
+ if kind in {'index','transpose'}:
32
+ return _node_interval(node.args[0],variable_layout=variable_layout,variable_lower=variable_lower,variable_upper=variable_upper,parameter_values=parameter_values)
33
+ if kind=='sum':
34
+ child=_node_interval(node.args[0],variable_layout=variable_layout,variable_lower=variable_lower,variable_upper=variable_upper,parameter_values=parameter_values)
35
+ if child is None: return None
36
+ count=int(np.prod(node.args[0].shape)) if node.payload is None else node.args[0].shape[int(node.payload)]
37
+ lo,hi=child
38
+ if lo>=0 or hi<=0: return (lo*count,hi*count)
39
+ return None
40
+ vals=[_node_interval(a,variable_layout=variable_layout,variable_lower=variable_lower,variable_upper=variable_upper,parameter_values=parameter_values) for a in node.args]
41
+ if any(v is None for v in vals): return None
42
+ if kind=='neg': return (-vals[0][1],-vals[0][0])
43
+ if kind=='add': return (vals[0][0]+vals[1][0],vals[0][1]+vals[1][1])
44
+ if kind=='mul':
45
+ a,b=vals; c=[a[0]*b[0],a[0]*b[1],a[1]*b[0],a[1]*b[1]]; return (min(c),max(c))
46
+ if kind=='div':
47
+ a,b=vals
48
+ if b[0] <= 0 <= b[1]: return None
49
+ c=[a[0]/b[0],a[0]/b[1],a[1]/b[0],a[1]/b[1]]; return (min(c),max(c))
50
+ if kind=='pow':
51
+ exponent=int(node.payload); a=vals[0]
52
+ if exponent%2==0:
53
+ hi=max(abs(a[0]),abs(a[1]))**exponent
54
+ lo=0.0 if a[0] <= 0 <= a[1] else min(abs(a[0]),abs(a[1]))**exponent
55
+ return (lo,hi)
56
+ return (a[0]**exponent,a[1]**exponent)
57
+ if kind=='exp': return (float(np.exp(vals[0][0])),float(np.exp(vals[0][1])))
58
+ if kind=='log':
59
+ if vals[0][0] <= 0: return None
60
+ return (float(np.log(vals[0][0])),float(np.log(vals[0][1])))
61
+ if kind=='sqrt':
62
+ if vals[0][0] < 0: return None
63
+ return (float(np.sqrt(vals[0][0])),float(np.sqrt(vals[0][1])))
64
+ if kind=='tanh': return (float(np.tanh(vals[0][0])),float(np.tanh(vals[0][1])))
65
+ return None
66
+
67
+ def _collect_domain_hazards(node, *, variable_layout, variable_lower, variable_upper, parameter_values, where):
68
+ hazards=[]
69
+ for child in node.args:
70
+ hazards.extend(_collect_domain_hazards(child,variable_layout=variable_layout,variable_lower=variable_lower,variable_upper=variable_upper,parameter_values=parameter_values,where=where))
71
+ if node.kind not in {'log','sqrt','div'}:
72
+ return hazards
73
+ target=node.args[0] if node.kind in {'log','sqrt'} else node.args[1]
74
+ interval=_node_interval(target,variable_layout=variable_layout,variable_lower=variable_lower,variable_upper=variable_upper,parameter_values=parameter_values)
75
+ safe=False
76
+ if interval is not None:
77
+ lo,hi=interval
78
+ safe=(node.kind=='log' and lo>0) or (node.kind=='sqrt' and lo>=0) or (node.kind=='div' and not (lo<=0<=hi))
79
+ if not safe:
80
+ hazards.append({'where':where,'kind':node.kind,'interval':None if interval is None else [float(interval[0]),float(interval[1])],'status':'not_proven_safe_from_declared_bounds'})
81
+ return hazards
82
+
83
+ def compile_nlp_model(model, *, use_cache=True):
84
+ if model.objective is None: raise CompileError('model has no objective')
85
+ if model.objective.sense is not ObjectiveSense.MINIMIZE: raise CompileError('P7 NLP compiler currently supports minimization only')
86
+ if model.objective.expression.shape != (): raise CompileError('P7 NLP objective must be scalar')
87
+ if any(d.domain is not VariableDomain.CONTINUOUS for d in model._variables.values()): raise CompileError('P7 NLP supports continuous variables only; MINLP is P8')
88
+ offsets,n,vl,vu,domains,source_vars=_variable_layout(model)
89
+ layout={vid:(offsets[vid][0],data.shape) for vid,data in model._variables.items()}
90
+ pvals={pid:data.value.copy() for pid,data in model._parameters.items()}
91
+ domain_hazards=_collect_domain_hazards(
92
+ model.objective.expression._node, variable_layout=layout, variable_lower=vl, variable_upper=vu,
93
+ parameter_values=pvals, where='NLP objective'
94
+ )
95
+ blocks=[]; source_constraints={}
96
+ for c in model.constraints:
97
+ if not hasattr(c,'set') or c.__class__.__name__=='IndicatorConstraint': raise CompileError('P7 NLP compiler does not lower indicator constraints; compile through P4/P8 orchestration')
98
+ if not isinstance(c.set,(LessThan,GreaterThan,EqualTo,Interval)): raise CompileError(f'P7 NLP supports scalar-set constraints only, got {type(c.set).__name__}')
99
+ domain_hazards.extend(_collect_domain_hazards(
100
+ c.function._node, variable_layout=layout, variable_lower=vl, variable_upper=vu,
101
+ parameter_values=pvals, where=f'NLP constraint {c.entity_id.value}'
102
+ ))
103
+ size=int(np.prod(c.function.shape)) if c.function.shape else 1
104
+ if isinstance(c.set,LessThan): lo=np.full(size,-np.inf); hi=_broadcast_bound(c.set.upper,size)
105
+ elif isinstance(c.set,GreaterThan): lo=_broadcast_bound(c.set.lower,size); hi=np.full(size,np.inf)
106
+ elif isinstance(c.set,EqualTo): lo=hi=_broadcast_bound(c.set.value,size)
107
+ else: lo=_broadcast_bound(c.set.lower,size); hi=_broadcast_bound(c.set.upper,size)
108
+ blocks.append(NLPConstraintBlock(c.entity_id.value,c.function._node,lo,hi,c.function.shape,tuple(sorted(c.function.parameter_dependencies))))
109
+ source_constraints[c.entity_id.value]={'semantic_kind':'nonlinear_function_in_set','nlp_rows':size,'shape':list(c.function.shape)}
110
+ problem=NLPProblem(n,vl,vu,layout,pvals,model.objective.expression._node,tuple(blocks),model.objective.sense.value,model.name,{'semantic_hash':model.semantic_hash,'compiled_by':'solverpilot-p7','domain_hazards':tuple(domain_hazards),'domain_safety_proven':not bool(domain_hazards)})
111
+ import hashlib,json
112
+ payload={'semantic_hash':model.semantic_hash,'target':'nlp-ir-v1','policy':'p7-smooth-core-v1','objective_degree':model.objective.expression.polynomial_degree,'constraint_kinds':[c.function._node.kind for c in model.constraints]}
113
+ ch=hashlib.sha256(json.dumps(payload,sort_keys=True,separators=(',',':')).encode()).hexdigest()
114
+ pmap={pid:{'shape':list(data.shape),'patchable':False,'parameter_compile_class':'p7_full_nlp_relower','plan_sensitive':bool(pid in model.objective.expression.parameter_dependencies or any(pid in c.function.parameter_dependencies for c in model.constraints))} for pid,data in model._parameters.items()}
115
+ report=CompilationReport(target='nlp-ir-v1',semantic_analysis_s=0.0,lowering_s=0.0,total_s=0.0,n_semantic_variables=len(model._variables),n_execution_variables=n,n_semantic_constraints=len(model._constraints),n_execution_constraints=problem.n_constraints,cache_status='disabled' if not use_cache else 'p7_rebuild',changed_parameters=(),reused_execution_rows=0,recompiled_execution_rows=problem.n_constraints,objective_recompiled=True,execution_mutations=(),n_transformations=0)
116
+ return CompiledModel(execution_ir=problem,semantic_hash=model.semantic_hash,data_hash=model.data_hash,compilation_hash=ch,parameter_map=pmap,source_map={'variables':source_vars,'constraints':source_constraints,'objective':{model.objective.entity_id.value:{'execution_objective':True}}},transformation_tape=(),capability_signature='p7-nlp-smooth-core-casadi-ad-v1',mutation_plan={'mode':'p7_full_nlp_relower','backend_session_patch':False},plan_sensitive_dependencies=tuple(sorted(set().union(*[set(c.function.parameter_dependencies) for c in model.constraints],set(model.objective.expression.parameter_dependencies)))),precondition_certificate_hashes=(),compilation_report=report,reconstruction_contract={'primal':'identity_by_source_map','dual':'backend_specific','original_space_validation':'solverpilot.nlp.validate_nlp_solution'},schema_version='solverpilot.compiled-model.p7.v1')
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import numpy as np
5
+
6
+ from solverpilot.model import Model, sin, exp
7
+ from .backend import CasadiIpoptBackend
8
+ from .compiler import compile_nlp_model
9
+ from .ad import NLPDerivativeEngine
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class NLPConformanceCheck:
14
+ name: str
15
+ passed: bool
16
+ detail: str
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class NLPConformanceReport:
21
+ backend: str
22
+ available: bool
23
+ passed: bool
24
+ checks: tuple[NLPConformanceCheck, ...]
25
+
26
+
27
+ def conform_casadi_ipopt_backend(backend: CasadiIpoptBackend | None = None) -> NLPConformanceReport:
28
+ backend = CasadiIpoptBackend(tol=1e-10, max_iter=2000) if backend is None else backend
29
+ checks: list[NLPConformanceCheck] = []
30
+ if not backend.is_available():
31
+ return NLPConformanceReport(backend.name, False, False, (NLPConformanceCheck('availability', False, 'CasADi Ipopt plugin unavailable'),))
32
+
33
+ # Local nonlinear objective with known minimizer.
34
+ m=Model(); x=m.variable(2,lower=-3,upper=3); c=np.array([0.4,-0.7])
35
+ m.minimize((x[0]-c[0])**4+(x[1]-c[1])**4+0.2*((x[0]-c[0])**2+(x[1]-c[1])**2))
36
+ p=compile_nlp_model(m).execution_ir; r=backend.solve(p,x0=[0,0])
37
+ ok=bool(r.validation.valid and r.local_optimal_candidate and not r.globally_proven and np.max(np.abs(r.x-c))<2e-5)
38
+ checks.append(NLPConformanceCheck('local_nlp_solve',ok,f'x_error={np.max(np.abs(r.x-c)) if r.x is not None else None}, status={r.backend_status}'))
39
+
40
+ # Nonlinear inequality with analytic projection.
41
+ m=Model(); x=m.variable(2,lower=0,upper=2); m.minimize((x[0]-1)**2+(x[1]-1)**2); m.add(x[0]**2+x[1]**2<=1)
42
+ p=compile_nlp_model(m).execution_ir; r=backend.solve(p,x0=[0.2,0.3]); target=np.array([1/np.sqrt(2)]*2)
43
+ ok=bool(r.validation.valid and r.local_optimal_candidate and not r.globally_proven and np.max(np.abs(r.x-target))<5e-5)
44
+ checks.append(NLPConformanceCheck('nonlinear_constraint',ok,f'x_error={np.max(np.abs(r.x-target)) if r.x is not None else None}, violation={r.validation.max_constraint_violation}'))
45
+
46
+ # AD correctness at a deterministic point.
47
+ m=Model(); x=m.variable(2,lower=[0.2,-2],upper=[2,2]); m.minimize(exp(0.2*x[0])+sin(x[0]*x[1])+x[0]/(1+x[1]**2)); m.add(sin(x[0])+x[1]**3<=2)
48
+ ad=NLPDerivativeEngine(compile_nlp_model(m).execution_ir); z=np.array([1.1,-0.25]); h=1e-6
49
+ gfd=np.zeros(2); Jfd=np.zeros((1,2))
50
+ for i in range(2):
51
+ zp=z.copy(); zm=z.copy(); zp[i]+=h; zm[i]-=h
52
+ gfd[i]=(ad.objective(zp)-ad.objective(zm))/(2*h)
53
+ Jfd[:,i]=(ad.constraints(zp)-ad.constraints(zm))/(2*h)
54
+ ge=float(np.max(np.abs(ad.gradient(z)-gfd))); je=float(np.max(np.abs(ad.jacobian(z)-Jfd)))
55
+ ok=ge<3e-6 and je<3e-6
56
+ checks.append(NLPConformanceCheck('automatic_differentiation',ok,f'gradient_error={ge}, jacobian_error={je}'))
57
+
58
+ return NLPConformanceReport(backend.name, True, all(c.passed for c in checks), tuple(checks))
solverpilot/nlp/ir.py ADDED
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from types import MappingProxyType
5
+ from typing import Any, Mapping
6
+
7
+ import numpy as np
8
+
9
+ from solverpilot.model.expression import ExprNode
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class NLPConstraintBlock:
14
+ source_id: str
15
+ node: ExprNode
16
+ lower: np.ndarray
17
+ upper: np.ndarray
18
+ shape: tuple[int, ...]
19
+ parameter_dependencies: tuple[str, ...] = ()
20
+
21
+ def __post_init__(self) -> None:
22
+ lo = np.asarray(self.lower, dtype=float).reshape(-1).copy()
23
+ hi = np.asarray(self.upper, dtype=float).reshape(-1).copy()
24
+ if lo.shape != hi.shape:
25
+ raise ValueError("constraint lower/upper shape mismatch")
26
+ lo.flags.writeable = False; hi.flags.writeable = False
27
+ object.__setattr__(self, "lower", lo); object.__setattr__(self, "upper", hi)
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class NLPProblem:
32
+ n_variables: int
33
+ variable_lower: np.ndarray
34
+ variable_upper: np.ndarray
35
+ variable_layout: Mapping[str, tuple[int, tuple[int, ...]]]
36
+ parameter_values: Mapping[str, np.ndarray]
37
+ objective_node: ExprNode
38
+ constraints: tuple[NLPConstraintBlock, ...]
39
+ objective_sense: str
40
+ name: str | None = None
41
+ metadata: Mapping[str, Any] = MappingProxyType({})
42
+
43
+ def __post_init__(self) -> None:
44
+ vl = np.asarray(self.variable_lower, dtype=float).reshape(-1).copy()
45
+ vu = np.asarray(self.variable_upper, dtype=float).reshape(-1).copy()
46
+ if vl.shape != (self.n_variables,) or vu.shape != (self.n_variables,):
47
+ raise ValueError("variable bound shape mismatch")
48
+ if np.any(vl > vu):
49
+ raise ValueError("variable lower bound exceeds upper bound")
50
+ vl.flags.writeable = False; vu.flags.writeable = False
51
+ object.__setattr__(self, "variable_lower", vl); object.__setattr__(self, "variable_upper", vu)
52
+ object.__setattr__(self, "variable_layout", MappingProxyType(dict(self.variable_layout)))
53
+ pv = {k: np.asarray(v, dtype=float).copy() for k, v in self.parameter_values.items()}
54
+ for v in pv.values(): v.flags.writeable = False
55
+ object.__setattr__(self, "parameter_values", MappingProxyType(pv))
56
+ object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
57
+
58
+ @property
59
+ def n_constraints(self) -> int:
60
+ return int(sum(c.lower.size for c in self.constraints))
61
+
62
+ @property
63
+ def constraint_lower(self) -> np.ndarray:
64
+ if not self.constraints: return np.empty(0, dtype=float)
65
+ return np.concatenate([c.lower for c in self.constraints])
66
+
67
+ @property
68
+ def constraint_upper(self) -> np.ndarray:
69
+ if not self.constraints: return np.empty(0, dtype=float)
70
+ return np.concatenate([c.upper for c in self.constraints])
@@ -0,0 +1 @@
1
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"solverpilot.nlp-ir-v1","type":"object","required":["schema_version","n_variables","n_constraints","derivatives"],"properties":{"schema_version":{"const":"solverpilot.nlp-ir.v1"},"n_variables":{"type":"integer","minimum":0},"n_constraints":{"type":"integer","minimum":0},"derivatives":{"type":"object","required":["gradient","jacobian","lagrangian_hessian"],"properties":{"gradient":{"const":"automatic_differentiation"},"jacobian":{"const":"automatic_differentiation_sparse"},"lagrangian_hessian":{"const":"automatic_differentiation_sparse"}}}}}