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.
Files changed (40) hide show
  1. lsdo_function_spaces/__init__.py +64 -0
  2. lsdo_function_spaces/core/__init__.py +0 -0
  3. lsdo_function_spaces/core/function.py +1322 -0
  4. lsdo_function_spaces/core/function_set.py +1081 -0
  5. lsdo_function_spaces/core/function_set_space.py +379 -0
  6. lsdo_function_spaces/core/function_space.py +482 -0
  7. lsdo_function_spaces/core/operations/__init__.py +0 -0
  8. lsdo_function_spaces/core/operations/basic_ops.py +85 -0
  9. lsdo_function_spaces/core/operations/operations.py +5 -0
  10. lsdo_function_spaces/core/optimization.py +183 -0
  11. lsdo_function_spaces/core/spaces/__init__.py +0 -0
  12. lsdo_function_spaces/core/spaces/b_spline_space.py +418 -0
  13. lsdo_function_spaces/core/spaces/conditional_space.py +65 -0
  14. lsdo_function_spaces/core/spaces/constant_space.py +57 -0
  15. lsdo_function_spaces/core/spaces/idw_space.py +271 -0
  16. lsdo_function_spaces/core/spaces/non_cython_bsplines/__init__.py +0 -0
  17. lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_csdl_custom_ops.py +420 -0
  18. lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection.py +1022 -0
  19. lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection_non_differentiable.py +186 -0
  20. lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection_optimized.py +594 -0
  21. lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_space_new.py +6 -0
  22. lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax.py +172 -0
  23. lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax_factory.py +382 -0
  24. lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax_stencil.py +451 -0
  25. lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_numpy.py +249 -0
  26. lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_numpy_factory.py +391 -0
  27. lsdo_function_spaces/core/spaces/operation_space.py +64 -0
  28. lsdo_function_spaces/core/spaces/polynomial_space.py +79 -0
  29. lsdo_function_spaces/core/spaces/rbf_space.py +136 -0
  30. lsdo_function_spaces/core/spaces/tri_space.py +256 -0
  31. lsdo_function_spaces/utils/__init__.py +0 -0
  32. lsdo_function_spaces/utils/file_io.py +484 -0
  33. lsdo_function_spaces/utils/internal_utilities.py +11 -0
  34. lsdo_function_spaces/utils/plotting_functions.py +357 -0
  35. lsdo_function_spaces/utils/utility_functions.py +148 -0
  36. lsdo_function_spaces-1.0.0.dist-info/METADATA +189 -0
  37. lsdo_function_spaces-1.0.0.dist-info/RECORD +40 -0
  38. lsdo_function_spaces-1.0.0.dist-info/WHEEL +5 -0
  39. lsdo_function_spaces-1.0.0.dist-info/licenses/LICENSE.txt +165 -0
  40. 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
@@ -0,0 +1,6 @@
1
+ """Backward compatibility module for BSplineSpaceNew."""
2
+
3
+ import warnings
4
+ from lsdo_function_spaces.core.spaces.b_spline_space import BSplineSpace, BSplineSpaceNew
5
+
6
+ __all__ = ["BSplineSpace", "BSplineSpaceNew"]