lsdo-function-spaces 1.0.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.
- lsdo_function_spaces/__init__.py +64 -0
- lsdo_function_spaces/core/__init__.py +0 -0
- lsdo_function_spaces/core/function.py +1322 -0
- lsdo_function_spaces/core/function_set.py +1081 -0
- lsdo_function_spaces/core/function_set_space.py +379 -0
- lsdo_function_spaces/core/function_space.py +482 -0
- lsdo_function_spaces/core/operations/__init__.py +0 -0
- lsdo_function_spaces/core/operations/basic_ops.py +85 -0
- lsdo_function_spaces/core/operations/operations.py +5 -0
- lsdo_function_spaces/core/optimization.py +183 -0
- lsdo_function_spaces/core/spaces/__init__.py +0 -0
- lsdo_function_spaces/core/spaces/b_spline_space.py +418 -0
- lsdo_function_spaces/core/spaces/conditional_space.py +65 -0
- lsdo_function_spaces/core/spaces/constant_space.py +57 -0
- lsdo_function_spaces/core/spaces/idw_space.py +271 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/__init__.py +0 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_csdl_custom_ops.py +420 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection.py +1022 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection_non_differentiable.py +186 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection_optimized.py +594 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_space_new.py +6 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax.py +172 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax_factory.py +382 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax_stencil.py +451 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_numpy.py +249 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_numpy_factory.py +391 -0
- lsdo_function_spaces/core/spaces/operation_space.py +64 -0
- lsdo_function_spaces/core/spaces/polynomial_space.py +79 -0
- lsdo_function_spaces/core/spaces/rbf_space.py +136 -0
- lsdo_function_spaces/core/spaces/tri_space.py +256 -0
- lsdo_function_spaces/utils/__init__.py +0 -0
- lsdo_function_spaces/utils/file_io.py +484 -0
- lsdo_function_spaces/utils/internal_utilities.py +11 -0
- lsdo_function_spaces/utils/plotting_functions.py +357 -0
- lsdo_function_spaces/utils/utility_functions.py +148 -0
- lsdo_function_spaces-1.0.0.dist-info/METADATA +189 -0
- lsdo_function_spaces-1.0.0.dist-info/RECORD +40 -0
- lsdo_function_spaces-1.0.0.dist-info/WHEEL +5 -0
- lsdo_function_spaces-1.0.0.dist-info/licenses/LICENSE.txt +165 -0
- lsdo_function_spaces-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Optimized B-spline surface projection (point -> parametric coords).
|
|
4
|
+
|
|
5
|
+
Key features:
|
|
6
|
+
1. High-efficiency Gauss-Newton / Levenberg-Marquardt (LM) distance minimization.
|
|
7
|
+
2. JAX implementation (jit + vmap friendly).
|
|
8
|
+
3. NumPy vectorized implementation.
|
|
9
|
+
|
|
10
|
+
Algorithm overview:
|
|
11
|
+
- Avoids second derivatives (surface Hessians) entirely.
|
|
12
|
+
- Minimizes squared distance: f(xi) = 1/2 * || S(xi) - p ||^2.
|
|
13
|
+
- Gauss-Newton step: (J^T J) delta = - J^T r.
|
|
14
|
+
- LM step: (J^T J + lambda I) delta = - J^T r.
|
|
15
|
+
|
|
16
|
+
Both implementations implement:
|
|
17
|
+
- Box constraints via clipping.
|
|
18
|
+
- Active-set masking near bounds.
|
|
19
|
+
- Early stopping using convergence masks.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Callable, Optional, Sequence, Tuple
|
|
25
|
+
|
|
26
|
+
import numpy as _np
|
|
27
|
+
|
|
28
|
+
# ----------------------------
|
|
29
|
+
# JAX implementation
|
|
30
|
+
# ----------------------------
|
|
31
|
+
try:
|
|
32
|
+
import jax
|
|
33
|
+
import jax.numpy as jnp
|
|
34
|
+
except Exception: # pragma: no cover
|
|
35
|
+
jax = None
|
|
36
|
+
jnp = None
|
|
37
|
+
|
|
38
|
+
# Import your JAX evaluator if available.
|
|
39
|
+
try:
|
|
40
|
+
from lsdo_function_spaces.core.spaces.non_cython_bsplines.compute_basis_matrix_jax_stencil import (
|
|
41
|
+
evaluate_b_spline_jax,
|
|
42
|
+
)
|
|
43
|
+
except Exception: # pragma: no cover
|
|
44
|
+
evaluate_b_spline_jax = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class LMParams:
|
|
49
|
+
max_iter: int = 50
|
|
50
|
+
tol_grad: float = 1e-12 # ||g|| threshold
|
|
51
|
+
tol_step: float = 1e-12 # ||δ|| threshold
|
|
52
|
+
lambda0: float = 1e-3 # initial damping
|
|
53
|
+
lambda_min: float = 1e-12
|
|
54
|
+
lambda_max: float = 1e12
|
|
55
|
+
lambda_up: float = 10.0 # multiply λ when step not accepted
|
|
56
|
+
lambda_down: float = 0.1 # multiply λ when step accepted
|
|
57
|
+
accept_ratio: float = 1e-4 # predicted vs actual improvement threshold
|
|
58
|
+
# bounds handling:
|
|
59
|
+
use_active_set: bool = True
|
|
60
|
+
bound_eps: float = 0.0 # treat u<=eps as lower-active, u>=1-eps as upper-active
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _ensure_knot_arrays_jax(knots):
|
|
64
|
+
# Allow knots as tuple-of-tuples floats; convert once.
|
|
65
|
+
return tuple(jnp.array(k) for k in knots)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _eval_surface_and_jac_jax(
|
|
69
|
+
u: "jnp.ndarray",
|
|
70
|
+
degrees: Tuple[int, ...],
|
|
71
|
+
coeffs: "jnp.ndarray",
|
|
72
|
+
knots: Tuple[Sequence[float], ...],
|
|
73
|
+
) -> Tuple["jnp.ndarray", "jnp.ndarray"]:
|
|
74
|
+
"""Evaluate S(u) and J(u) where J is (phys_dim, n_param)."""
|
|
75
|
+
n_param = len(degrees)
|
|
76
|
+
uu = u.reshape(1, n_param)
|
|
77
|
+
|
|
78
|
+
# knots are expected to already be JAX arrays when called from the factory;
|
|
79
|
+
# if you pass Python tuples-of-floats directly, they will be converted once in the factory.
|
|
80
|
+
S = evaluate_b_spline_jax(us=uu, degrees=degrees, knot_vectors=knots, coeffs=coeffs)[0] # (phys,)
|
|
81
|
+
# partials
|
|
82
|
+
dS_list = []
|
|
83
|
+
for i in range(n_param):
|
|
84
|
+
der = tuple(1 if k == i else 0 for k in range(n_param))
|
|
85
|
+
dS_i = evaluate_b_spline_jax(us=uu, degrees=degrees, knot_vectors=knots, coeffs=coeffs, der_orders=der)[0]
|
|
86
|
+
dS_list.append(dS_i)
|
|
87
|
+
J = jnp.stack(dS_list, axis=1) # (phys, n_param)
|
|
88
|
+
return S, J
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _solve_2x2_spd_jax(A: "jnp.ndarray", b: "jnp.ndarray") -> "jnp.ndarray":
|
|
92
|
+
"""
|
|
93
|
+
Solve A x = b for A (2,2) SPD, vector b (2,).
|
|
94
|
+
Uses explicit formula for speed and stability in small systems.
|
|
95
|
+
"""
|
|
96
|
+
a00, a01 = A[0, 0], A[0, 1]
|
|
97
|
+
a10, a11 = A[1, 0], A[1, 1]
|
|
98
|
+
det = a00 * a11 - a01 * a10
|
|
99
|
+
# If det is tiny, fallback to linalg.solve (rare if λ>0)
|
|
100
|
+
def _fallback(_):
|
|
101
|
+
return jnp.linalg.solve(A, b)
|
|
102
|
+
def _explicit(_):
|
|
103
|
+
inv = jnp.array([[ a11, -a01],
|
|
104
|
+
[-a10, a00]]) / det
|
|
105
|
+
return inv @ b
|
|
106
|
+
return jax.lax.cond(jnp.abs(det) < 1e-30, _fallback, _explicit, operand=0)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _active_mask_jax(u: "jnp.ndarray", g: "jnp.ndarray", eps: float):
|
|
110
|
+
# active where free to move
|
|
111
|
+
lower_active = u <= eps
|
|
112
|
+
upper_active = u >= (1.0 - eps)
|
|
113
|
+
# if at lower bound and gradient wants to decrease u (g < 0? depends on sign)
|
|
114
|
+
# We solve δ from A δ = -g, so desired move is roughly -g.
|
|
115
|
+
# If u at lower bound and -g would be negative -> g positive blocks.
|
|
116
|
+
block_lower = lower_active & (g > 0.0)
|
|
117
|
+
block_upper = upper_active & (g < 0.0)
|
|
118
|
+
inactive = block_lower | block_upper | (g == 0.0)
|
|
119
|
+
return ~inactive
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def project_point_gauss_newton_jax(
|
|
123
|
+
point: "jnp.ndarray",
|
|
124
|
+
u0: "jnp.ndarray",
|
|
125
|
+
degrees: Tuple[int, ...],
|
|
126
|
+
coeffs: "jnp.ndarray",
|
|
127
|
+
knots: Tuple[Sequence[float], ...],
|
|
128
|
+
*,
|
|
129
|
+
max_iter: int = 50,
|
|
130
|
+
tol_grad: float = 1e-12,
|
|
131
|
+
tol_step: float = 1e-12,
|
|
132
|
+
use_active_set: bool = True,
|
|
133
|
+
bound_eps: float = 0.0,
|
|
134
|
+
):
|
|
135
|
+
"""Fast Gauss-Newton projection for one point."""
|
|
136
|
+
knots = _ensure_knot_arrays_jax(knots)
|
|
137
|
+
if evaluate_b_spline_jax is None:
|
|
138
|
+
raise ImportError("evaluate_b_spline_jax not found; check your imports.")
|
|
139
|
+
|
|
140
|
+
n_param = len(degrees)
|
|
141
|
+
u0 = u0.reshape(n_param,)
|
|
142
|
+
|
|
143
|
+
def body(i, state):
|
|
144
|
+
u, converged = state
|
|
145
|
+
S, J = _eval_surface_and_jac_jax(u, degrees, coeffs, knots)
|
|
146
|
+
r = S - point
|
|
147
|
+
g = J.T @ r # (n_param,)
|
|
148
|
+
|
|
149
|
+
# active-set masking (keeps a fixed-size solve)
|
|
150
|
+
if use_active_set:
|
|
151
|
+
active = _active_mask_jax(u, g, bound_eps)
|
|
152
|
+
else:
|
|
153
|
+
active = jnp.ones((n_param,), dtype=bool)
|
|
154
|
+
|
|
155
|
+
# Build normal equations A = J^T J
|
|
156
|
+
A = J.T @ J # (n_param,n_param)
|
|
157
|
+
# Mask inactive directions by adding identity on inactive coords.
|
|
158
|
+
I_inactive = jnp.diag((~active).astype(A.dtype))
|
|
159
|
+
A_mask = A * (active[:, None] & active[None, :]) + I_inactive
|
|
160
|
+
g_mask = g * active
|
|
161
|
+
|
|
162
|
+
# Solve A δ = -g
|
|
163
|
+
rhs = -g_mask
|
|
164
|
+
if n_param == 2:
|
|
165
|
+
delta = _solve_2x2_spd_jax(A_mask, rhs)
|
|
166
|
+
else:
|
|
167
|
+
delta = jnp.linalg.solve(A_mask, rhs)
|
|
168
|
+
|
|
169
|
+
delta = delta * active
|
|
170
|
+
u_new = jnp.clip(u + delta, 0.0, 1.0)
|
|
171
|
+
|
|
172
|
+
g_norm = jnp.linalg.norm(g_mask)
|
|
173
|
+
d_norm = jnp.linalg.norm(delta)
|
|
174
|
+
|
|
175
|
+
converged_new = (g_norm < tol_grad) | (d_norm < tol_step)
|
|
176
|
+
return (u_new, converged_new)
|
|
177
|
+
|
|
178
|
+
# Use fori_loop with an early-stop emulation: keep updating but freeze when converged
|
|
179
|
+
def scan_body(carry, i):
|
|
180
|
+
u, converged = carry
|
|
181
|
+
u_new, conv_new = body(i, (u, converged))
|
|
182
|
+
u_out = jnp.where(converged, u, u_new)
|
|
183
|
+
conv_out = converged | conv_new
|
|
184
|
+
return (u_out, conv_out), None
|
|
185
|
+
|
|
186
|
+
(u_star, conv), _ = jax.lax.scan(scan_body, (u0, False), jnp.arange(max_iter))
|
|
187
|
+
n_iter = max_iter # scan doesn't easily return first-stop without extra work
|
|
188
|
+
return u_star, conv, n_iter
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def project_point_lm_jax(
|
|
192
|
+
point: "jnp.ndarray",
|
|
193
|
+
u0: "jnp.ndarray",
|
|
194
|
+
degrees: Tuple[int, ...],
|
|
195
|
+
coeffs: "jnp.ndarray",
|
|
196
|
+
knots: Tuple[Sequence[float], ...],
|
|
197
|
+
params: LMParams = LMParams(),
|
|
198
|
+
):
|
|
199
|
+
"""Levenberg-Marquardt projection for one point (distance minimization)."""
|
|
200
|
+
knots = _ensure_knot_arrays_jax(knots)
|
|
201
|
+
if evaluate_b_spline_jax is None:
|
|
202
|
+
raise ImportError("evaluate_b_spline_jax not found; check your imports.")
|
|
203
|
+
|
|
204
|
+
n_param = len(degrees)
|
|
205
|
+
u0 = u0.reshape(n_param,)
|
|
206
|
+
|
|
207
|
+
def one_iter(carry, _):
|
|
208
|
+
u, lam, converged = carry
|
|
209
|
+
|
|
210
|
+
S, J = _eval_surface_and_jac_jax(u, degrees, coeffs, knots)
|
|
211
|
+
r = S - point
|
|
212
|
+
f = 0.5 * (r @ r)
|
|
213
|
+
|
|
214
|
+
g = J.T @ r # (n_param,)
|
|
215
|
+
A = J.T @ J # (n_param,n_param)
|
|
216
|
+
A_lm = A + lam * jnp.eye(n_param, dtype=A.dtype)
|
|
217
|
+
|
|
218
|
+
# active-set
|
|
219
|
+
if params.use_active_set:
|
|
220
|
+
active = _active_mask_jax(u, g, params.bound_eps)
|
|
221
|
+
else:
|
|
222
|
+
active = jnp.ones((n_param,), dtype=bool)
|
|
223
|
+
I_inactive = jnp.diag((~active).astype(A.dtype))
|
|
224
|
+
A_mask = A_lm * (active[:, None] & active[None, :]) + I_inactive
|
|
225
|
+
g_mask = g * active
|
|
226
|
+
|
|
227
|
+
rhs = -g_mask
|
|
228
|
+
if n_param == 2:
|
|
229
|
+
delta = _solve_2x2_spd_jax(A_mask, rhs)
|
|
230
|
+
else:
|
|
231
|
+
delta = jnp.linalg.solve(A_mask, rhs)
|
|
232
|
+
delta = delta * active
|
|
233
|
+
|
|
234
|
+
u_trial = jnp.clip(u + delta, 0.0, 1.0)
|
|
235
|
+
|
|
236
|
+
# Evaluate trial objective
|
|
237
|
+
S_t, _ = _eval_surface_and_jac_jax(u_trial, degrees, coeffs, knots)
|
|
238
|
+
r_t = S_t - point
|
|
239
|
+
f_t = 0.5 * (r_t @ r_t)
|
|
240
|
+
|
|
241
|
+
# Predicted reduction (quadratic model): m(0)-m(δ) ≈ -g^T δ - 0.5 δ^T A δ
|
|
242
|
+
# Use undamped A (GN Hessian approx) for prediction.
|
|
243
|
+
pred = -(g_mask @ delta) - 0.5 * (delta @ (A @ delta))
|
|
244
|
+
act = f - f_t
|
|
245
|
+
|
|
246
|
+
# Accept if actual improvement positive and ratio decent
|
|
247
|
+
ratio = jnp.where(pred > 0, act / pred, 0.0)
|
|
248
|
+
accept = (act > 0) & (ratio > params.accept_ratio)
|
|
249
|
+
|
|
250
|
+
u_new = jnp.where(accept, u_trial, u)
|
|
251
|
+
lam_new = jnp.where(accept, lam * params.lambda_down, lam * params.lambda_up)
|
|
252
|
+
lam_new = jnp.clip(lam_new, params.lambda_min, params.lambda_max)
|
|
253
|
+
|
|
254
|
+
g_norm = jnp.linalg.norm(g_mask)
|
|
255
|
+
d_norm = jnp.linalg.norm(delta)
|
|
256
|
+
converged_new = (g_norm < params.tol_grad) | (d_norm < params.tol_step)
|
|
257
|
+
|
|
258
|
+
# Freeze if converged already
|
|
259
|
+
u_out = jnp.where(converged, u, u_new)
|
|
260
|
+
lam_out = jnp.where(converged, lam, lam_new)
|
|
261
|
+
conv_out = converged | converged_new
|
|
262
|
+
return (u_out, lam_out, conv_out), None
|
|
263
|
+
|
|
264
|
+
(u_star, lam_star, conv), _ = jax.lax.scan(
|
|
265
|
+
one_iter, (u0, params.lambda0, False), xs=None, length=params.max_iter
|
|
266
|
+
)
|
|
267
|
+
return u_star, conv, params.max_iter, lam_star
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def make_projector_lm_jax(
|
|
271
|
+
degrees: Tuple[int, ...],
|
|
272
|
+
knots: Tuple[Sequence[float], ...],
|
|
273
|
+
*,
|
|
274
|
+
params: LMParams = LMParams(),
|
|
275
|
+
jit: bool = True,
|
|
276
|
+
):
|
|
277
|
+
"""Factory that returns a batched projector."""
|
|
278
|
+
knots = _ensure_knot_arrays_jax(knots)
|
|
279
|
+
if jax is None:
|
|
280
|
+
raise ImportError("JAX is not available.")
|
|
281
|
+
if evaluate_b_spline_jax is None:
|
|
282
|
+
raise ImportError("evaluate_b_spline_jax not found; check your imports.")
|
|
283
|
+
|
|
284
|
+
def _single(point, u0, coeffs):
|
|
285
|
+
return project_point_lm_jax(point, u0, degrees, coeffs, knots, params)
|
|
286
|
+
|
|
287
|
+
vmapped = jax.vmap(_single, in_axes=(0, 0, None))
|
|
288
|
+
|
|
289
|
+
def proj(points, u0s, coeffs):
|
|
290
|
+
u_star, conv, _, _ = vmapped(points, u0s, coeffs)
|
|
291
|
+
return u_star, conv
|
|
292
|
+
|
|
293
|
+
if jit:
|
|
294
|
+
# degrees/knots captured in closure => static
|
|
295
|
+
proj = jax.jit(proj)
|
|
296
|
+
return proj
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ----------------------------
|
|
300
|
+
# NumPy implementation
|
|
301
|
+
# ----------------------------
|
|
302
|
+
try:
|
|
303
|
+
# If you have your optimized stencil evaluator factory, you can swap it in.
|
|
304
|
+
from lsdo_function_spaces.core.spaces.non_cython_bsplines.compute_basis_matrix_numpy_factory import make_bspline_evaluator_numpy
|
|
305
|
+
except Exception: # pragma: no cover
|
|
306
|
+
make_bspline_evaluator_numpy = None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _solve_2x2_spd_numpy(A: _np.ndarray, b: _np.ndarray) -> _np.ndarray:
|
|
310
|
+
"""Vectorized solve for batches of 2x2 systems."""
|
|
311
|
+
a00 = A[:, 0, 0]
|
|
312
|
+
a01 = A[:, 0, 1]
|
|
313
|
+
a10 = A[:, 1, 0]
|
|
314
|
+
a11 = A[:, 1, 1]
|
|
315
|
+
det = a00 * a11 - a01 * a10
|
|
316
|
+
# safe inverse
|
|
317
|
+
inv00 = a11 / det
|
|
318
|
+
inv01 = -a01 / det
|
|
319
|
+
inv10 = -a10 / det
|
|
320
|
+
inv11 = a00 / det
|
|
321
|
+
x0 = inv00 * b[:, 0] + inv01 * b[:, 1]
|
|
322
|
+
x1 = inv10 * b[:, 0] + inv11 * b[:, 1]
|
|
323
|
+
return _np.stack([x0, x1], axis=1)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _active_mask_numpy(u: _np.ndarray, g: _np.ndarray, eps: float) -> _np.ndarray:
|
|
327
|
+
lower_active = u <= eps
|
|
328
|
+
upper_active = u >= (1.0 - eps)
|
|
329
|
+
block_lower = lower_active & (g > 0.0)
|
|
330
|
+
block_upper = upper_active & (g < 0.0)
|
|
331
|
+
inactive = block_lower | block_upper | (g == 0.0)
|
|
332
|
+
return ~inactive
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _build_masked_lm_system_numpy(
|
|
336
|
+
A: _np.ndarray,
|
|
337
|
+
g: _np.ndarray,
|
|
338
|
+
active: _np.ndarray,
|
|
339
|
+
lam: _np.ndarray,
|
|
340
|
+
) -> tuple[_np.ndarray, _np.ndarray]:
|
|
341
|
+
"""Build the masked LM linear systems.
|
|
342
|
+
|
|
343
|
+
Active coordinates use the damped GN system. Inactive coordinates are
|
|
344
|
+
frozen with an identity diagonal and zero RHS so the system remains SPD.
|
|
345
|
+
"""
|
|
346
|
+
M, n_param, _ = A.shape
|
|
347
|
+
A_lm = A.copy()
|
|
348
|
+
idx = _np.arange(n_param)
|
|
349
|
+
A_lm[:, idx, idx] += lam[:, None]
|
|
350
|
+
|
|
351
|
+
mask2 = active[:, :, None] & active[:, None, :]
|
|
352
|
+
A_mask = A_lm * mask2.astype(A.dtype)
|
|
353
|
+
for k in range(n_param):
|
|
354
|
+
A_mask[:, k, k] += (~active[:, k]).astype(A.dtype)
|
|
355
|
+
|
|
356
|
+
rhs = -(g * active)
|
|
357
|
+
return A_mask, rhs
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def make_projector_lm_numpy(
|
|
361
|
+
degrees: Tuple[int, ...],
|
|
362
|
+
knot_vectors: Tuple[_np.ndarray, ...],
|
|
363
|
+
der_orders: Optional[Tuple[int, ...]] = None,
|
|
364
|
+
):
|
|
365
|
+
"""
|
|
366
|
+
Factory returning evaluators for S(u) and partials using your stencil-based NumPy evaluator.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
eval_S(us, coeffs) -> (M, phys_dim)
|
|
370
|
+
eval_partials(us, coeffs) -> (M, phys_dim, n_param)
|
|
371
|
+
"""
|
|
372
|
+
if make_bspline_evaluator_numpy is None:
|
|
373
|
+
raise ImportError("make_bspline_evaluator_numpy not found; ensure compute_basis_matrix_numpy_factory.py is on path.")
|
|
374
|
+
|
|
375
|
+
eval_S = make_bspline_evaluator_numpy(degrees=degrees, knot_vectors=knot_vectors, der_orders=None)
|
|
376
|
+
|
|
377
|
+
n_param = len(degrees)
|
|
378
|
+
eval_d = []
|
|
379
|
+
for i in range(n_param):
|
|
380
|
+
der = tuple(1 if k == i else 0 for k in range(n_param))
|
|
381
|
+
eval_d.append(make_bspline_evaluator_numpy(degrees=degrees, knot_vectors=knot_vectors, der_orders=der))
|
|
382
|
+
|
|
383
|
+
def eval_partials(us, coeffs):
|
|
384
|
+
# stack along last axis -> (M, phys_dim, n_param)
|
|
385
|
+
parts = [fn(us, coeffs) for fn in eval_d] # list of (M, phys)
|
|
386
|
+
return _np.stack(parts, axis=2)
|
|
387
|
+
|
|
388
|
+
return eval_S, eval_partials
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def project_points_gauss_newton_numpy(
|
|
392
|
+
points: _np.ndarray, # (M, phys_dim)
|
|
393
|
+
u0s: _np.ndarray, # (M, n_param)
|
|
394
|
+
coeffs: _np.ndarray,
|
|
395
|
+
degrees: Tuple[int, ...],
|
|
396
|
+
knot_vectors: Tuple[_np.ndarray, ...],
|
|
397
|
+
*,
|
|
398
|
+
max_iter: int = 50,
|
|
399
|
+
tol_grad: float = 1e-12,
|
|
400
|
+
tol_step: float = 1e-12,
|
|
401
|
+
use_active_set: bool = True,
|
|
402
|
+
bound_eps: float = 0.0,
|
|
403
|
+
):
|
|
404
|
+
"""Vectorized Gauss-Newton projection for many points (NumPy)."""
|
|
405
|
+
M = points.shape[0]
|
|
406
|
+
n_param = len(degrees)
|
|
407
|
+
|
|
408
|
+
eval_S, eval_partials = make_projector_lm_numpy(degrees, knot_vectors)
|
|
409
|
+
|
|
410
|
+
u = _np.clip(u0s.copy(), 0.0, 1.0)
|
|
411
|
+
converged = _np.zeros((M,), dtype=bool)
|
|
412
|
+
|
|
413
|
+
for _ in range(max_iter):
|
|
414
|
+
S = eval_S(u, coeffs) # (M, phys)
|
|
415
|
+
J = eval_partials(u, coeffs) # (M, phys, n_param)
|
|
416
|
+
r = S - points # (M, phys)
|
|
417
|
+
|
|
418
|
+
# g = J^T r -> (M, n_param)
|
|
419
|
+
g = _np.einsum("mpk,mp->mk", J, r)
|
|
420
|
+
|
|
421
|
+
if use_active_set:
|
|
422
|
+
active = _active_mask_numpy(u, g, bound_eps) # (M, n_param)
|
|
423
|
+
else:
|
|
424
|
+
active = _np.ones_like(u, dtype=bool)
|
|
425
|
+
|
|
426
|
+
# A = J^T J -> (M, n_param, n_param)
|
|
427
|
+
A = _np.einsum("mpk, mpl -> mkl", J, J)
|
|
428
|
+
|
|
429
|
+
# Mask inactive dirs by adding identity on inactive coords
|
|
430
|
+
# For n_param==2, do it explicitly.
|
|
431
|
+
if n_param != 2:
|
|
432
|
+
# generic: add diag(~active)
|
|
433
|
+
for k in range(n_param):
|
|
434
|
+
A[:, k, k] += (~active[:, k]).astype(A.dtype)
|
|
435
|
+
# also zero off-diags for inactive pairs
|
|
436
|
+
mask2 = active[:, :, None] & active[:, None, :]
|
|
437
|
+
A = A * mask2
|
|
438
|
+
rhs = -(g * active)
|
|
439
|
+
# solve batch using np.linalg.solve (works for small n_param)
|
|
440
|
+
delta = _np.linalg.solve(A, rhs[..., None]).squeeze(-1)
|
|
441
|
+
else:
|
|
442
|
+
mask2 = active[:, :, None] & active[:, None, :]
|
|
443
|
+
A_mask = A * mask2
|
|
444
|
+
A_mask[:, 0, 0] += (~active[:, 0]).astype(A.dtype)
|
|
445
|
+
A_mask[:, 1, 1] += (~active[:, 1]).astype(A.dtype)
|
|
446
|
+
|
|
447
|
+
rhs = -(g * active)
|
|
448
|
+
delta = _solve_2x2_spd_numpy(A_mask, rhs)
|
|
449
|
+
delta *= active
|
|
450
|
+
|
|
451
|
+
u_new = _np.clip(u + delta, 0.0, 1.0)
|
|
452
|
+
|
|
453
|
+
g_norm = _np.linalg.norm(g * active, axis=1)
|
|
454
|
+
d_norm = _np.linalg.norm(delta, axis=1)
|
|
455
|
+
conv_new = (g_norm < tol_grad) | (d_norm < tol_step)
|
|
456
|
+
|
|
457
|
+
# freeze those already converged
|
|
458
|
+
u = _np.where(converged[:, None], u, u_new)
|
|
459
|
+
converged = converged | conv_new
|
|
460
|
+
|
|
461
|
+
if converged.all():
|
|
462
|
+
break
|
|
463
|
+
|
|
464
|
+
return u, converged
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def project_points_lm_numpy(
|
|
468
|
+
points: _np.ndarray,
|
|
469
|
+
u0s: _np.ndarray,
|
|
470
|
+
coeffs: _np.ndarray,
|
|
471
|
+
degrees: Tuple[int, ...],
|
|
472
|
+
knot_vectors: Tuple[_np.ndarray, ...],
|
|
473
|
+
*,
|
|
474
|
+
params: LMParams = LMParams(),
|
|
475
|
+
):
|
|
476
|
+
"""Vectorized Levenberg-Marquardt projection for many points (NumPy)."""
|
|
477
|
+
M = points.shape[0]
|
|
478
|
+
n_param = len(degrees)
|
|
479
|
+
|
|
480
|
+
eval_S, eval_partials = make_projector_lm_numpy(degrees, knot_vectors)
|
|
481
|
+
|
|
482
|
+
u = _np.clip(u0s.copy(), 0.0, 1.0)
|
|
483
|
+
lam = _np.full((M,), params.lambda0, dtype=float)
|
|
484
|
+
converged = _np.zeros((M,), dtype=bool)
|
|
485
|
+
|
|
486
|
+
for _ in range(params.max_iter):
|
|
487
|
+
S = eval_S(u, coeffs) # (M, phys)
|
|
488
|
+
J = eval_partials(u, coeffs) # (M, phys, n_param)
|
|
489
|
+
r = S - points # (M, phys)
|
|
490
|
+
f = 0.5 * _np.einsum("mp,mp->m", r, r) # (M,)
|
|
491
|
+
|
|
492
|
+
g = _np.einsum("mpk,mp->mk", J, r) # (M, n_param)
|
|
493
|
+
A = _np.einsum("mpk,mpl->mkl", J, J) # (M, n_param, n_param)
|
|
494
|
+
|
|
495
|
+
if params.use_active_set:
|
|
496
|
+
active = _active_mask_numpy(u, g, params.bound_eps)
|
|
497
|
+
else:
|
|
498
|
+
active = _np.ones_like(u, dtype=bool)
|
|
499
|
+
|
|
500
|
+
A_mask, rhs = _build_masked_lm_system_numpy(A, g, active, lam)
|
|
501
|
+
|
|
502
|
+
if n_param == 2:
|
|
503
|
+
delta = _solve_2x2_spd_numpy(A_mask, rhs)
|
|
504
|
+
else:
|
|
505
|
+
delta = _np.linalg.solve(A_mask, rhs[..., None]).squeeze(-1)
|
|
506
|
+
delta *= active
|
|
507
|
+
|
|
508
|
+
u_trial = _np.clip(u + delta, 0.0, 1.0)
|
|
509
|
+
delta_eff = u_trial - u
|
|
510
|
+
|
|
511
|
+
S_t = eval_S(u_trial, coeffs)
|
|
512
|
+
r_t = S_t - points
|
|
513
|
+
f_t = 0.5 * _np.einsum("mp,mp->m", r_t, r_t)
|
|
514
|
+
|
|
515
|
+
# Predicted reduction from the *damped* local LM model evaluated at the
|
|
516
|
+
# effective (possibly clipped) step.
|
|
517
|
+
g_mask = g * active
|
|
518
|
+
A_active = A * (active[:, :, None] & active[:, None, :]).astype(A.dtype)
|
|
519
|
+
g_dot_d = _np.einsum("mk,mk->m", g_mask, delta_eff)
|
|
520
|
+
Ad = _np.einsum("mkl,ml->mk", A_active, delta_eff)
|
|
521
|
+
dAd = _np.einsum("mk,mk->m", delta_eff, Ad)
|
|
522
|
+
d2 = _np.einsum("mk,mk->m", delta_eff, delta_eff)
|
|
523
|
+
pred = -(g_dot_d + 0.5 * dAd + 0.5 * lam * d2)
|
|
524
|
+
|
|
525
|
+
act = f - f_t
|
|
526
|
+
# ratio = _np.where(pred > 0.0, act / pred, -_np.inf)
|
|
527
|
+
pred_tol = 1e-14
|
|
528
|
+
pred_safe = _np.where(_np.isfinite(pred) & (pred > pred_tol), pred, _np.nan)
|
|
529
|
+
ratio = act / pred_safe
|
|
530
|
+
ratio = _np.where(_np.isfinite(ratio), ratio, _np.inf)
|
|
531
|
+
|
|
532
|
+
accept = (act > 0.0) & (ratio > params.accept_ratio)
|
|
533
|
+
|
|
534
|
+
u_new = _np.where(accept[:, None], u_trial, u)
|
|
535
|
+
lam_new = _np.where(accept, lam * params.lambda_down, lam * params.lambda_up)
|
|
536
|
+
lam_new = _np.clip(lam_new, params.lambda_min, params.lambda_max)
|
|
537
|
+
|
|
538
|
+
accepted_step_norm = _np.linalg.norm(_np.where(accept[:, None], delta_eff, 0.0), axis=1)
|
|
539
|
+
g_norm = _np.linalg.norm(g_mask, axis=1)
|
|
540
|
+
conv_new = (g_norm < params.tol_grad) | (accept & (accepted_step_norm < params.tol_step))
|
|
541
|
+
|
|
542
|
+
u = _np.where(converged[:, None], u, u_new)
|
|
543
|
+
lam = _np.where(converged, lam, lam_new)
|
|
544
|
+
converged = converged | conv_new
|
|
545
|
+
|
|
546
|
+
if converged.all():
|
|
547
|
+
break
|
|
548
|
+
|
|
549
|
+
residual = g_norm # eval_S(u, coeffs) - points
|
|
550
|
+
return u, converged, lam, residual
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
if __name__ == "__main__":
|
|
554
|
+
import numpy as np
|
|
555
|
+
np.random.seed(42) # For reproducibility
|
|
556
|
+
|
|
557
|
+
# Define the B-spline space parameters
|
|
558
|
+
num_cp_x = 10
|
|
559
|
+
num_cp_y = 8
|
|
560
|
+
nx = num_cp_x - 1 # Number of control points - 1
|
|
561
|
+
ny = num_cp_y - 1 # Number of control points - 1
|
|
562
|
+
px = 3 # Degree of the B-spline
|
|
563
|
+
py = 2 # Degree of the B-spline
|
|
564
|
+
p = (px, py)
|
|
565
|
+
coefficients_shape = (num_cp_x, num_cp_y)
|
|
566
|
+
derivative_orders = (1, 0) # # derivatives for the evaluation
|
|
567
|
+
|
|
568
|
+
knots_x = np.concatenate(
|
|
569
|
+
[np.zeros(px),
|
|
570
|
+
np.linspace(0, 1, num_cp_x - px + 1),
|
|
571
|
+
np.ones(px)]
|
|
572
|
+
)
|
|
573
|
+
knots_y = np.concatenate(
|
|
574
|
+
[np.zeros(py),
|
|
575
|
+
np.linspace(0, 1, num_cp_y - py + 1),
|
|
576
|
+
np.ones(py)]
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Make knot vectors hashable Python tuples so they can be used as static
|
|
580
|
+
# arguments to jax.jit. Functions in this module convert them back to
|
|
581
|
+
# JAX arrays when needed.
|
|
582
|
+
knots = (
|
|
583
|
+
tuple(knots_x.tolist()),
|
|
584
|
+
tuple(knots_y.tolist()),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
coeffs_x, coeffs_y = np.meshgrid(np.linspace(0, 5, num_cp_x), np.linspace(0, 2, num_cp_y), indexing='ij')
|
|
588
|
+
coeffs = np.array(np.stack((coeffs_x, coeffs_y, 0.2 * np.random.rand(num_cp_x, num_cp_y)), axis=-1))
|
|
589
|
+
coeffs_jnp = jnp.array(coeffs)
|
|
590
|
+
|
|
591
|
+
random_points_in_space = np.random.rand(100, 3) # [1, :].reshape(-1, 3) # Random points in space
|
|
592
|
+
random_points_in_space[:, 0] *= 5
|
|
593
|
+
random_points_in_space[:, 1] *= 2
|
|
594
|
+
random_points_in_space[:, 2] = 5
|