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 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
+ )