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,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"}}}}}
|