nlpoptnet 0.1.0__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.
- jaxmodel/__init__.py +54 -0
- jaxmodel/approximations.py +195 -0
- jaxmodel/autodiff.py +30 -0
- jaxmodel/blocks.py +171 -0
- jaxmodel/bounds.py +15 -0
- jaxmodel/builder.py +80 -0
- jaxmodel/constraints.py +285 -0
- jaxmodel/highlevel.py +326 -0
- jaxmodel/model.py +347 -0
- jaxmodel/objective.py +109 -0
- jaxmodel/parameters.py +36 -0
- jaxmodel/types.py +13 -0
- jaxmodel/variables.py +111 -0
- nlpoptnet/__init__.py +8 -0
- nlpoptnet/api.py +2315 -0
- nlpoptnet/sampling.py +214 -0
- nlpoptnet/serialization.py +175 -0
- nlpoptnet/utils.py +54 -0
- nlpoptnet-0.1.0.dist-info/METADATA +193 -0
- nlpoptnet-0.1.0.dist-info/RECORD +39 -0
- nlpoptnet-0.1.0.dist-info/WHEEL +5 -0
- nlpoptnet-0.1.0.dist-info/licenses/LICENSE +201 -0
- nlpoptnet-0.1.0.dist-info/top_level.txt +3 -0
- opt/CP_jax.py +499 -0
- opt/CP_jax_implicit.py +342 -0
- opt/__init__.py +26 -0
- opt/aot_artifacts.py +216 -0
- opt/model_bridge.py +124 -0
- opt/models/__init__.py +5 -0
- opt/models/backbone.py +56 -0
- opt/native_projection.py +831 -0
- opt/projection/__init__.py +5 -0
- opt/projection/layer.py +137 -0
- opt/solvers/__init__.py +20 -0
- opt/subproblem/__init__.py +17 -0
- opt/training/__init__.py +24 -0
- opt/training/config.py +98 -0
- opt/training/jaxmodel_pipeline.py +273 -0
- opt/training/loops.py +70 -0
jaxmodel/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Public exports for the symbolic JAX modeling layer used by NLPOptNet."""
|
|
2
|
+
|
|
3
|
+
from .variables import VariableBuilder, VariableSpec
|
|
4
|
+
from .parameters import ParameterSpec
|
|
5
|
+
from .bounds import BoundSpec
|
|
6
|
+
from .builder import NLPBuilder
|
|
7
|
+
from .highlevel import HighLevelNLPBuilder
|
|
8
|
+
from .objective import QuadraticObjective
|
|
9
|
+
from .constraints import (
|
|
10
|
+
ConstraintEntry,
|
|
11
|
+
eq_scalar,
|
|
12
|
+
ineq_scalar,
|
|
13
|
+
eq_block,
|
|
14
|
+
ineq_block,
|
|
15
|
+
quadratic_eq_scalar,
|
|
16
|
+
quadratic_ineq_scalar,
|
|
17
|
+
)
|
|
18
|
+
from .blocks import (
|
|
19
|
+
affine_eq_from_parts,
|
|
20
|
+
affine_ineq_from_parts,
|
|
21
|
+
nonlinear_eq_from_parts,
|
|
22
|
+
nonlinear_ineq_from_parts,
|
|
23
|
+
)
|
|
24
|
+
from .approximations import (
|
|
25
|
+
LinearizationData,
|
|
26
|
+
QuadraticObjectiveData,
|
|
27
|
+
SQPSubproblemData,
|
|
28
|
+
)
|
|
29
|
+
from .model import JaxNLPModel
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"VariableBuilder",
|
|
33
|
+
"VariableSpec",
|
|
34
|
+
"ParameterSpec",
|
|
35
|
+
"BoundSpec",
|
|
36
|
+
"NLPBuilder",
|
|
37
|
+
"HighLevelNLPBuilder",
|
|
38
|
+
"QuadraticObjective",
|
|
39
|
+
"ConstraintEntry",
|
|
40
|
+
"eq_scalar",
|
|
41
|
+
"ineq_scalar",
|
|
42
|
+
"eq_block",
|
|
43
|
+
"ineq_block",
|
|
44
|
+
"quadratic_eq_scalar",
|
|
45
|
+
"quadratic_ineq_scalar",
|
|
46
|
+
"affine_eq_from_parts",
|
|
47
|
+
"affine_ineq_from_parts",
|
|
48
|
+
"nonlinear_eq_from_parts",
|
|
49
|
+
"nonlinear_ineq_from_parts",
|
|
50
|
+
"LinearizationData",
|
|
51
|
+
"QuadraticObjectiveData",
|
|
52
|
+
"SQPSubproblemData",
|
|
53
|
+
"JaxNLPModel",
|
|
54
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Linear and quadratic approximations used to form SQP-style subproblems."""
|
|
2
|
+
|
|
3
|
+
import jax.numpy as jnp
|
|
4
|
+
from typing import NamedTuple, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LinearizationData(NamedTuple):
|
|
8
|
+
"""Affine approximation data for equality and inequality residuals."""
|
|
9
|
+
|
|
10
|
+
A: jnp.ndarray
|
|
11
|
+
b: jnp.ndarray
|
|
12
|
+
C: jnp.ndarray
|
|
13
|
+
d: jnp.ndarray
|
|
14
|
+
h_val: jnp.ndarray
|
|
15
|
+
g_val: jnp.ndarray
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QuadraticObjectiveData(NamedTuple):
|
|
19
|
+
"""Quadratic approximation data for a local objective model."""
|
|
20
|
+
|
|
21
|
+
grad_f: jnp.ndarray
|
|
22
|
+
Q: jnp.ndarray
|
|
23
|
+
Q_diag: jnp.ndarray
|
|
24
|
+
c: jnp.ndarray
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SQPSubproblemData(NamedTuple):
|
|
28
|
+
"""Structured data needed to solve a local SQP subproblem."""
|
|
29
|
+
|
|
30
|
+
objective: QuadraticObjectiveData
|
|
31
|
+
constraints: LinearizationData
|
|
32
|
+
l: Optional[jnp.ndarray]
|
|
33
|
+
u: Optional[jnp.ndarray]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def linearize_constraints_data(eq_fun, ineq_fun, jac_eq, jac_ineq, params, y):
|
|
37
|
+
"""Return matrix data for affine residual linearizations at ``y``."""
|
|
38
|
+
A = jac_eq(y, params)
|
|
39
|
+
h = eq_fun(params, y)
|
|
40
|
+
b = A @ y - h
|
|
41
|
+
|
|
42
|
+
C = jac_ineq(y, params)
|
|
43
|
+
g = ineq_fun(params, y)
|
|
44
|
+
d = C @ y - g
|
|
45
|
+
|
|
46
|
+
return LinearizationData(A=A, b=b, C=C, d=d, h_val=h, g_val=g)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def linearize_constraints(eq_fun, ineq_fun, jac_eq, jac_ineq, params, y):
|
|
50
|
+
"""Return affine constraint linearizations as a dictionary."""
|
|
51
|
+
data = linearize_constraints_data(
|
|
52
|
+
eq_fun=eq_fun,
|
|
53
|
+
ineq_fun=ineq_fun,
|
|
54
|
+
jac_eq=jac_eq,
|
|
55
|
+
jac_ineq=jac_ineq,
|
|
56
|
+
params=params,
|
|
57
|
+
y=y,
|
|
58
|
+
)
|
|
59
|
+
return dict(data._asdict())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def quadraticize_objective_data(
|
|
63
|
+
grad_fun,
|
|
64
|
+
hess_fun,
|
|
65
|
+
diag_hess_fun,
|
|
66
|
+
params,
|
|
67
|
+
y,
|
|
68
|
+
rho: float = 1.0,
|
|
69
|
+
use_diagonal_hessian: bool = True,
|
|
70
|
+
diag_floor: Optional[float] = None,
|
|
71
|
+
):
|
|
72
|
+
"""Return the quadratic approximation data for the objective at ``y``."""
|
|
73
|
+
grad = grad_fun(y, params)
|
|
74
|
+
|
|
75
|
+
if use_diagonal_hessian:
|
|
76
|
+
qdiag = diag_hess_fun(params, y)
|
|
77
|
+
if diag_floor is not None:
|
|
78
|
+
qdiag = jnp.maximum(qdiag, diag_floor)
|
|
79
|
+
Q = jnp.diag(rho * qdiag)
|
|
80
|
+
else:
|
|
81
|
+
Q = rho * hess_fun(y, params)
|
|
82
|
+
|
|
83
|
+
c = grad - Q @ y
|
|
84
|
+
|
|
85
|
+
return QuadraticObjectiveData(grad_f=grad, Q=Q, Q_diag=jnp.diag(Q), c=c)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def quadraticize_objective(
|
|
89
|
+
grad_fun,
|
|
90
|
+
hess_fun,
|
|
91
|
+
diag_hess_fun,
|
|
92
|
+
params,
|
|
93
|
+
y,
|
|
94
|
+
rho: float = 1.0,
|
|
95
|
+
use_diagonal_hessian: bool = True,
|
|
96
|
+
diag_floor: Optional[float] = None,
|
|
97
|
+
):
|
|
98
|
+
"""Return the quadratic approximation of the objective as a dictionary."""
|
|
99
|
+
data = quadraticize_objective_data(
|
|
100
|
+
grad_fun=grad_fun,
|
|
101
|
+
hess_fun=hess_fun,
|
|
102
|
+
diag_hess_fun=diag_hess_fun,
|
|
103
|
+
params=params,
|
|
104
|
+
y=y,
|
|
105
|
+
rho=rho,
|
|
106
|
+
use_diagonal_hessian=use_diagonal_hessian,
|
|
107
|
+
diag_floor=diag_floor,
|
|
108
|
+
)
|
|
109
|
+
return dict(data._asdict())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_sqp_subproblem_data(
|
|
113
|
+
eq_fun,
|
|
114
|
+
ineq_fun,
|
|
115
|
+
jac_eq,
|
|
116
|
+
jac_ineq,
|
|
117
|
+
grad_fun,
|
|
118
|
+
hess_fun,
|
|
119
|
+
diag_hess_fun,
|
|
120
|
+
lower_fun,
|
|
121
|
+
upper_fun,
|
|
122
|
+
params,
|
|
123
|
+
y,
|
|
124
|
+
rho: float = 1.0,
|
|
125
|
+
use_diagonal_hessian: bool = True,
|
|
126
|
+
diag_floor: Optional[float] = None,
|
|
127
|
+
):
|
|
128
|
+
"""Build a structured SQP subproblem approximation at ``y``."""
|
|
129
|
+
qdata = quadraticize_objective_data(
|
|
130
|
+
grad_fun=grad_fun,
|
|
131
|
+
hess_fun=hess_fun,
|
|
132
|
+
diag_hess_fun=diag_hess_fun,
|
|
133
|
+
params=params,
|
|
134
|
+
y=y,
|
|
135
|
+
rho=rho,
|
|
136
|
+
use_diagonal_hessian=use_diagonal_hessian,
|
|
137
|
+
diag_floor=diag_floor,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
cdata = linearize_constraints_data(
|
|
141
|
+
eq_fun=eq_fun,
|
|
142
|
+
ineq_fun=ineq_fun,
|
|
143
|
+
jac_eq=jac_eq,
|
|
144
|
+
jac_ineq=jac_ineq,
|
|
145
|
+
params=params,
|
|
146
|
+
y=y,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return SQPSubproblemData(
|
|
150
|
+
objective=qdata,
|
|
151
|
+
constraints=cdata,
|
|
152
|
+
l=None if lower_fun is None else lower_fun(params),
|
|
153
|
+
u=None if upper_fun is None else upper_fun(params),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def build_sqp_data(
|
|
158
|
+
eq_fun,
|
|
159
|
+
ineq_fun,
|
|
160
|
+
jac_eq,
|
|
161
|
+
jac_ineq,
|
|
162
|
+
grad_fun,
|
|
163
|
+
hess_fun,
|
|
164
|
+
diag_hess_fun,
|
|
165
|
+
lower_fun,
|
|
166
|
+
upper_fun,
|
|
167
|
+
params,
|
|
168
|
+
y,
|
|
169
|
+
rho: float = 1.0,
|
|
170
|
+
use_diagonal_hessian: bool = True,
|
|
171
|
+
diag_floor: Optional[float] = None,
|
|
172
|
+
):
|
|
173
|
+
"""Build SQP approximation data as a flat dictionary."""
|
|
174
|
+
data = build_sqp_subproblem_data(
|
|
175
|
+
eq_fun=eq_fun,
|
|
176
|
+
ineq_fun=ineq_fun,
|
|
177
|
+
jac_eq=jac_eq,
|
|
178
|
+
jac_ineq=jac_ineq,
|
|
179
|
+
grad_fun=grad_fun,
|
|
180
|
+
hess_fun=hess_fun,
|
|
181
|
+
diag_hess_fun=diag_hess_fun,
|
|
182
|
+
lower_fun=lower_fun,
|
|
183
|
+
upper_fun=upper_fun,
|
|
184
|
+
params=params,
|
|
185
|
+
y=y,
|
|
186
|
+
rho=rho,
|
|
187
|
+
use_diagonal_hessian=use_diagonal_hessian,
|
|
188
|
+
diag_floor=diag_floor,
|
|
189
|
+
)
|
|
190
|
+
return {
|
|
191
|
+
**dict(data.objective._asdict()),
|
|
192
|
+
**dict(data.constraints._asdict()),
|
|
193
|
+
"l": data.l,
|
|
194
|
+
"u": data.u,
|
|
195
|
+
}
|
jaxmodel/autodiff.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Automatic-differentiation helpers for flat JAX objectives and constraints."""
|
|
2
|
+
|
|
3
|
+
import jax
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_grad_y_objective(obj_flat):
|
|
8
|
+
"""Build the gradient of a flat objective with respect to variables."""
|
|
9
|
+
return jax.grad(lambda y, p: obj_flat(p, y), argnums=0)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_hess_y_objective(obj_flat):
|
|
13
|
+
"""Build the Hessian of a flat objective with respect to variables."""
|
|
14
|
+
return jax.hessian(lambda y, p: obj_flat(p, y), argnums=0)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_diag_hess_y_objective(grad_fun):
|
|
18
|
+
"""Build a diagonal-Hessian evaluator from a gradient function."""
|
|
19
|
+
def diag_hess(params, y):
|
|
20
|
+
n = y.shape[0]
|
|
21
|
+
basis = jnp.eye(n, dtype=y.dtype)
|
|
22
|
+
_, jvp_fun = jax.linearize(lambda yy: grad_fun(yy, params), y)
|
|
23
|
+
cols = jax.vmap(jvp_fun)(basis)
|
|
24
|
+
return jnp.diag(cols.T)
|
|
25
|
+
return diag_hess
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_jac_y_vector_fun(vec_fun):
|
|
29
|
+
"""Build the Jacobian of a vector-valued residual with respect to variables."""
|
|
30
|
+
return jax.jacrev(lambda y, p: vec_fun(p, y), argnums=0)
|
jaxmodel/blocks.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Helpers for assembling structured affine and nonlinear residual blocks."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Sequence, Tuple, Callable
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
from .variables import VariableSpec
|
|
6
|
+
from .constraints import eq_block, ineq_block
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _build_affine_jacobian_matrix(
|
|
10
|
+
var_spec: VariableSpec,
|
|
11
|
+
y_blocks: Sequence[Tuple[jnp.ndarray, str]],
|
|
12
|
+
) -> jnp.ndarray:
|
|
13
|
+
"""Assemble a dense Jacobian from named affine variable blocks."""
|
|
14
|
+
n = var_spec.total_size
|
|
15
|
+
if len(y_blocks) == 0:
|
|
16
|
+
return jnp.zeros((0, n), dtype=jnp.float64)
|
|
17
|
+
|
|
18
|
+
m = int(jnp.asarray(y_blocks[0][0]).shape[0])
|
|
19
|
+
full = jnp.zeros((m, n), dtype=jnp.asarray(y_blocks[0][0]).dtype)
|
|
20
|
+
|
|
21
|
+
for mat, yname in y_blocks:
|
|
22
|
+
block = jnp.asarray(mat)
|
|
23
|
+
sl = var_spec.slices[yname]
|
|
24
|
+
full = full.at[:, sl].add(block)
|
|
25
|
+
return full
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def affine_eq_from_parts(
|
|
29
|
+
var_spec: VariableSpec,
|
|
30
|
+
y_blocks: Sequence[Tuple[jnp.ndarray, str]],
|
|
31
|
+
rhs_const: Optional[jnp.ndarray] = None,
|
|
32
|
+
x_blocks: Optional[Sequence[Tuple[jnp.ndarray, str]]] = None,
|
|
33
|
+
name: str = "affine_eq",
|
|
34
|
+
):
|
|
35
|
+
"""Build an affine equality block from named variable and parameter pieces."""
|
|
36
|
+
rhs_const = 0.0 if rhs_const is None else rhs_const
|
|
37
|
+
x_blocks = [] if x_blocks is None else list(x_blocks)
|
|
38
|
+
|
|
39
|
+
def fun(params, vars):
|
|
40
|
+
lhs = 0.0
|
|
41
|
+
for A, yname in y_blocks:
|
|
42
|
+
lhs = lhs + A @ jnp.ravel(vars[yname])
|
|
43
|
+
|
|
44
|
+
rhs = jnp.asarray(rhs_const)
|
|
45
|
+
for D, xname in x_blocks:
|
|
46
|
+
rhs = rhs + D @ jnp.ravel(params[xname])
|
|
47
|
+
|
|
48
|
+
return lhs - rhs
|
|
49
|
+
|
|
50
|
+
jac_matrix = _build_affine_jacobian_matrix(var_spec, y_blocks)
|
|
51
|
+
jac_y_fun = lambda params, y, J=jac_matrix: J
|
|
52
|
+
|
|
53
|
+
return eq_block(
|
|
54
|
+
fun,
|
|
55
|
+
var_spec,
|
|
56
|
+
name=name,
|
|
57
|
+
structure="affine",
|
|
58
|
+
jac_y_fun=jac_y_fun,
|
|
59
|
+
metadata={
|
|
60
|
+
"type": "affine_block",
|
|
61
|
+
"jac_matrix": jac_matrix,
|
|
62
|
+
"rhs_const": jnp.asarray(rhs_const),
|
|
63
|
+
"x_blocks": list(x_blocks),
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def affine_ineq_from_parts(
|
|
69
|
+
var_spec: VariableSpec,
|
|
70
|
+
y_blocks: Sequence[Tuple[jnp.ndarray, str]],
|
|
71
|
+
rhs_const: Optional[jnp.ndarray] = None,
|
|
72
|
+
x_blocks: Optional[Sequence[Tuple[jnp.ndarray, str]]] = None,
|
|
73
|
+
name: str = "affine_ineq",
|
|
74
|
+
):
|
|
75
|
+
"""Build an affine inequality block from named variable and parameter pieces."""
|
|
76
|
+
rhs_const = 0.0 if rhs_const is None else rhs_const
|
|
77
|
+
x_blocks = [] if x_blocks is None else list(x_blocks)
|
|
78
|
+
|
|
79
|
+
def fun(params, vars):
|
|
80
|
+
lhs = 0.0
|
|
81
|
+
for C, yname in y_blocks:
|
|
82
|
+
lhs = lhs + C @ jnp.ravel(vars[yname])
|
|
83
|
+
|
|
84
|
+
rhs = jnp.asarray(rhs_const)
|
|
85
|
+
for E, xname in x_blocks:
|
|
86
|
+
rhs = rhs + E @ jnp.ravel(params[xname])
|
|
87
|
+
|
|
88
|
+
return lhs - rhs
|
|
89
|
+
|
|
90
|
+
jac_matrix = _build_affine_jacobian_matrix(var_spec, y_blocks)
|
|
91
|
+
jac_y_fun = lambda params, y, J=jac_matrix: J
|
|
92
|
+
|
|
93
|
+
return ineq_block(
|
|
94
|
+
fun,
|
|
95
|
+
var_spec,
|
|
96
|
+
name=name,
|
|
97
|
+
structure="affine",
|
|
98
|
+
jac_y_fun=jac_y_fun,
|
|
99
|
+
metadata={
|
|
100
|
+
"type": "affine_block",
|
|
101
|
+
"jac_matrix": jac_matrix,
|
|
102
|
+
"rhs_const": jnp.asarray(rhs_const),
|
|
103
|
+
"x_blocks": list(x_blocks),
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def nonlinear_eq_from_parts(
|
|
109
|
+
var_spec: VariableSpec,
|
|
110
|
+
linear_y_terms: Optional[Sequence[Tuple[jnp.ndarray, str]]] = None,
|
|
111
|
+
nonlinear_y_terms: Optional[Sequence[Tuple[jnp.ndarray, Callable, str]]] = None,
|
|
112
|
+
rhs_const: Optional[jnp.ndarray] = None,
|
|
113
|
+
x_blocks: Optional[Sequence[Tuple[jnp.ndarray, str]]] = None,
|
|
114
|
+
x_direct_name: Optional[str] = None,
|
|
115
|
+
name: str = "nonlinear_eq",
|
|
116
|
+
):
|
|
117
|
+
"""Build a nonlinear equality block from linear and transformed terms."""
|
|
118
|
+
linear_y_terms = [] if linear_y_terms is None else list(linear_y_terms)
|
|
119
|
+
nonlinear_y_terms = [] if nonlinear_y_terms is None else list(nonlinear_y_terms)
|
|
120
|
+
x_blocks = [] if x_blocks is None else list(x_blocks)
|
|
121
|
+
rhs_const = 0.0 if rhs_const is None else rhs_const
|
|
122
|
+
|
|
123
|
+
def fun(params, vars):
|
|
124
|
+
lhs = 0.0
|
|
125
|
+
for A, yname in linear_y_terms:
|
|
126
|
+
lhs = lhs + A @ jnp.ravel(vars[yname])
|
|
127
|
+
|
|
128
|
+
for B, phi, yname in nonlinear_y_terms:
|
|
129
|
+
lhs = lhs + B @ phi(jnp.ravel(vars[yname]))
|
|
130
|
+
|
|
131
|
+
rhs = jnp.asarray(rhs_const)
|
|
132
|
+
for D, xname in x_blocks:
|
|
133
|
+
rhs = rhs + D @ jnp.ravel(params[xname])
|
|
134
|
+
|
|
135
|
+
if x_direct_name is not None:
|
|
136
|
+
rhs = rhs + jnp.ravel(params[x_direct_name])
|
|
137
|
+
|
|
138
|
+
return lhs - rhs
|
|
139
|
+
|
|
140
|
+
return eq_block(fun, var_spec, name=name)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def nonlinear_ineq_from_parts(
|
|
144
|
+
var_spec: VariableSpec,
|
|
145
|
+
linear_y_terms: Optional[Sequence[Tuple[jnp.ndarray, str]]] = None,
|
|
146
|
+
nonlinear_y_terms: Optional[Sequence[Tuple[jnp.ndarray, Callable, str]]] = None,
|
|
147
|
+
rhs_const: Optional[jnp.ndarray] = None,
|
|
148
|
+
x_blocks: Optional[Sequence[Tuple[jnp.ndarray, str]]] = None,
|
|
149
|
+
name: str = "nonlinear_ineq",
|
|
150
|
+
):
|
|
151
|
+
"""Build a nonlinear inequality block from linear and transformed terms."""
|
|
152
|
+
linear_y_terms = [] if linear_y_terms is None else list(linear_y_terms)
|
|
153
|
+
nonlinear_y_terms = [] if nonlinear_y_terms is None else list(nonlinear_y_terms)
|
|
154
|
+
x_blocks = [] if x_blocks is None else list(x_blocks)
|
|
155
|
+
rhs_const = 0.0 if rhs_const is None else rhs_const
|
|
156
|
+
|
|
157
|
+
def fun(params, vars):
|
|
158
|
+
lhs = 0.0
|
|
159
|
+
for C, yname in linear_y_terms:
|
|
160
|
+
lhs = lhs + C @ jnp.ravel(vars[yname])
|
|
161
|
+
|
|
162
|
+
for F, phi, yname in nonlinear_y_terms:
|
|
163
|
+
lhs = lhs + F @ phi(jnp.ravel(vars[yname]))
|
|
164
|
+
|
|
165
|
+
rhs = jnp.asarray(rhs_const)
|
|
166
|
+
for E, xname in x_blocks:
|
|
167
|
+
rhs = rhs + E @ jnp.ravel(params[xname])
|
|
168
|
+
|
|
169
|
+
return lhs - rhs
|
|
170
|
+
|
|
171
|
+
return ineq_block(fun, var_spec, name=name)
|
jaxmodel/bounds.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Bound specifications for optimization variables."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Callable
|
|
5
|
+
import jax.numpy as jnp
|
|
6
|
+
|
|
7
|
+
Array = jnp.ndarray
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class BoundSpec:
|
|
12
|
+
"""Hold callable lower and upper bound evaluators."""
|
|
13
|
+
|
|
14
|
+
lower_fun: Optional[Callable[[dict], Array]] = None
|
|
15
|
+
upper_fun: Optional[Callable[[dict], Array]] = None
|
jaxmodel/builder.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Low-level builder for constructing :class:`jaxmodel.model.JaxNLPModel`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Callable, Optional, Sequence
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
|
|
9
|
+
from .bounds import BoundSpec
|
|
10
|
+
from .constraints import ConstraintEntry, eq_block, eq_scalar, ineq_block, ineq_scalar
|
|
11
|
+
from .model import JaxNLPModel
|
|
12
|
+
from .parameters import ParameterSpec
|
|
13
|
+
from .variables import VariableSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class NLPBuilder:
|
|
18
|
+
"""Incrementally assemble a :class:`JaxNLPModel` from its components."""
|
|
19
|
+
|
|
20
|
+
var_spec: VariableSpec
|
|
21
|
+
parameter_spec: Optional[ParameterSpec] = None
|
|
22
|
+
dtype: jnp.dtype = jnp.float64
|
|
23
|
+
_objective_fun: Optional[Callable] = None
|
|
24
|
+
_constraints: list[ConstraintEntry] = field(default_factory=list)
|
|
25
|
+
_bounds: BoundSpec = field(default_factory=BoundSpec)
|
|
26
|
+
|
|
27
|
+
def set_objective(self, objective_fun) -> "NLPBuilder":
|
|
28
|
+
"""Set the scalar objective callable or structured objective."""
|
|
29
|
+
self._objective_fun = objective_fun
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
def add_constraint(self, constraint: ConstraintEntry) -> "NLPBuilder":
|
|
33
|
+
"""Add one constraint entry to the model."""
|
|
34
|
+
self._constraints.append(constraint)
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def add_constraints(self, constraints: Sequence[ConstraintEntry]) -> "NLPBuilder":
|
|
38
|
+
"""Add multiple constraint entries to the model."""
|
|
39
|
+
self._constraints.extend(constraints)
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def add_eq_scalar(self, fun, name: str = "eq_scalar") -> "NLPBuilder":
|
|
43
|
+
"""Add a scalar equality residual."""
|
|
44
|
+
return self.add_constraint(eq_scalar(fun, self.var_spec, name=name))
|
|
45
|
+
|
|
46
|
+
def add_ineq_scalar(self, fun, name: str = "ineq_scalar") -> "NLPBuilder":
|
|
47
|
+
"""Add a scalar inequality residual."""
|
|
48
|
+
return self.add_constraint(ineq_scalar(fun, self.var_spec, name=name))
|
|
49
|
+
|
|
50
|
+
def add_eq_block(self, fun, name: str = "eq_block") -> "NLPBuilder":
|
|
51
|
+
"""Add a vector-valued equality block."""
|
|
52
|
+
return self.add_constraint(eq_block(fun, self.var_spec, name=name))
|
|
53
|
+
|
|
54
|
+
def add_ineq_block(self, fun, name: str = "ineq_block") -> "NLPBuilder":
|
|
55
|
+
"""Add a vector-valued inequality block."""
|
|
56
|
+
return self.add_constraint(ineq_block(fun, self.var_spec, name=name))
|
|
57
|
+
|
|
58
|
+
def set_bounds(
|
|
59
|
+
self,
|
|
60
|
+
lower_fun=None,
|
|
61
|
+
upper_fun=None,
|
|
62
|
+
) -> "NLPBuilder":
|
|
63
|
+
"""Attach callable lower and upper bounds."""
|
|
64
|
+
self._bounds = BoundSpec(lower_fun=lower_fun, upper_fun=upper_fun)
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def build(self, jit_compile: bool = True) -> JaxNLPModel:
|
|
68
|
+
"""Finalize and construct the :class:`JaxNLPModel`."""
|
|
69
|
+
if self._objective_fun is None:
|
|
70
|
+
raise ValueError("Objective function must be set before build().")
|
|
71
|
+
|
|
72
|
+
return JaxNLPModel(
|
|
73
|
+
var_spec=self.var_spec,
|
|
74
|
+
objective_fun=self._objective_fun,
|
|
75
|
+
constraints=self._constraints,
|
|
76
|
+
bounds=self._bounds,
|
|
77
|
+
parameter_spec=self.parameter_spec,
|
|
78
|
+
dtype=self.dtype,
|
|
79
|
+
jit_compile=jit_compile,
|
|
80
|
+
)
|