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,451 @@
1
+ import numpy as np
2
+ import jax
3
+ import jax.numpy as jnp
4
+ from jax.experimental.sparse import BCOO
5
+
6
+
7
+ def compute_basis_matrix_jax(us, degrees, knot_vectors, der_orders=None):
8
+ """
9
+ JAX-jit-compiled sparse basis-matrix builder with parametric derivatives.
10
+
11
+ us : jnp.ndarray, shape (M, d)
12
+ degrees : tuple of length d
13
+ knot_vectors : tuple of length d, each a jnp.ndarray
14
+ der_orders : tuple of length d, derivative order in each dim
15
+ """
16
+ us = jnp.atleast_2d(us)
17
+ M, dim = us.shape
18
+
19
+ # Ensure knot_vectors are JAX arrays (knot_vectors may be passed as
20
+ # Python tuples of tuples to be hashable for jax.jit static args).
21
+ knot_vectors = tuple(jnp.array(U) for U in knot_vectors)
22
+
23
+ if der_orders is None:
24
+ der_orders = tuple([0] * dim)
25
+
26
+ # 1) per-dim spans, ndu & ders up to der_orders[i]
27
+ n_ctrls = []
28
+ spans = []
29
+ Ns = [] # will hold ders[:, n_i, :] per dimension
30
+
31
+ for i in range(dim):
32
+ p = degrees[i]
33
+ U = knot_vectors[i]
34
+ n = der_orders[i]
35
+
36
+ # number of control points in this dim
37
+ num_cp = len(U) - p - 1
38
+ n_ctrls.append(num_cp)
39
+
40
+ # find spans: shape (M,)
41
+ span = jnp.searchsorted(U, us[:, i], side="right") - 1
42
+ span = jnp.clip(span, p, num_cp - 1)
43
+ spans.append(span)
44
+
45
+ # build ndu table: shape (M, p+1, p+1)
46
+ ndu = jnp.zeros((M, p+1, p+1))
47
+ ndu = ndu.at[:, 0, 0].set(1.0)
48
+ left = jnp.zeros((M, p+1))
49
+ right = jnp.zeros((M, p+1))
50
+
51
+ for j in range(1, p+1):
52
+ # vectorized distances
53
+ left = left.at[:, j].set(us[:, i] - U[span + 1 - j])
54
+ right = right.at[:, j].set(U[span + j] - us[:, i])
55
+ saved = jnp.zeros((M,))
56
+
57
+ for r in range(j):
58
+ # lower triangle
59
+ ndu = ndu.at[:, j, r].set(right[:, r+1] + left[:, j-r])
60
+ temp = ndu[:, r, j-1] / ndu[:, j, r]
61
+ # upper triangle
62
+ ndu = ndu.at[:, r, j].set(saved + right[:, r+1] * temp)
63
+ saved = left[:, j-r] * temp
64
+
65
+ ndu = ndu.at[:, j, j].set(saved)
66
+
67
+ # now DersBasisFuns up to order n (Algorithm A2.3)
68
+ # ders shape (M, n+1, p+1)
69
+ ders = jnp.zeros((M, n+1, p+1))
70
+ ders = ders.at[:, 0, :].set(ndu[:, :, p])
71
+
72
+ # a buffer for alternating
73
+ a = jnp.zeros((2, M, p+1))
74
+ for r in range(p+1):
75
+ # initialize a row
76
+ a = a.at[0, :, 0].set(1.0)
77
+
78
+ for k in range(1, n+1):
79
+ d = jnp.zeros((M,))
80
+ rk = r - k
81
+ pk = p - k
82
+
83
+ # first term
84
+ a = a.at[1, :, 0].set(
85
+ jnp.where(rk >= 0, a[0, :, 0] / ndu[:, pk+1, rk], 0.0)
86
+ )
87
+ d = d + jnp.where(rk >= 0,
88
+ a[1, :, 0] * ndu[:, rk, pk], 0.0)
89
+
90
+ # inner terms
91
+ j1 = 1 if rk >= -1 else -rk
92
+ j2 = k-1 if (r-1) <= pk else p - r
93
+ for j in range(j1, j2+1):
94
+ val = (a[0, :, j] - a[0, :, j-1]) / ndu[:, pk+1, rk+j]
95
+ a = a.at[1, :, j].set(val)
96
+ d = d + val * ndu[:, rk+j, pk]
97
+
98
+ # last term
99
+ a = a.at[1, :, k].set(
100
+ jnp.where(r <= pk, -a[0, :, k-1] / ndu[:, pk+1, r], 0.0)
101
+ )
102
+ d = d + jnp.where(r <= pk,
103
+ a[1, :, k] * ndu[:, r, pk], 0.0)
104
+
105
+ ders = ders.at[:, k, r].set(d)
106
+ # swap rows in a
107
+ a = a.at[0].set(a[1])
108
+ a = a.at[1].set(0.0)
109
+
110
+ # scale derivatives by factorial factors
111
+ for k in range(1, n+1):
112
+ factor = jnp.prod(jnp.arange(p, p-k, -1))
113
+ ders = ders.at[:, k, :].multiply(factor)
114
+
115
+ # pick off the highest derivative requested
116
+ Ns.append(ders[:, n, :])
117
+
118
+ # 2) build all local-offset combinations (static)
119
+ grids = [jnp.arange(p+1) for p in degrees]
120
+ mesh = jnp.meshgrid(*grids, indexing="ij")
121
+ offs = jnp.stack([g.ravel() for g in mesh], axis=-1) # (L, d)
122
+ L = offs.shape[0]
123
+
124
+ # 3) compute C-order strides (python ints)
125
+ strides = np.empty(dim, int)
126
+ acc = 1
127
+ for i in range(dim-1, -1, -1):
128
+ strides[i] = acc
129
+ acc *= n_ctrls[i]
130
+
131
+ # 4) assemble sparse COO entries
132
+ rows = jnp.repeat(jnp.arange(M), L)
133
+
134
+ # columns
135
+ cols = 0
136
+ for i in range(dim):
137
+ base = (spans[i] - degrees[i])[:, None] + offs[None, :, i]
138
+ cols = cols + base * strides[i]
139
+ cols = cols.ravel()
140
+
141
+ # data = product over dims of Ns[i][:, offs[:,i]]
142
+ data = jnp.ones((M, L))
143
+ for i in range(dim):
144
+ data = data * Ns[i][:, offs[:, i]]
145
+ data = data.ravel()
146
+
147
+ # shape as python ints
148
+ shape = (M, int(np.prod(n_ctrls)))
149
+ Bcoo = BCOO((data, jnp.stack((rows, cols), axis=-1)), shape=shape)
150
+ return Bcoo
151
+
152
+
153
+ def compute_basis_stencil_jax(us, degrees, knot_vectors, der_orders=None):
154
+ """
155
+ Build a *stencil* representation of the B-spline basis operator without
156
+ constructing a BCOO sparse matrix.
157
+
158
+ Returns
159
+ -------
160
+ cols : jnp.ndarray, shape (M, L), int32
161
+ Column indices into the flattened control-point array.
162
+ w : jnp.ndarray, shape (M, L), float
163
+ Basis weights (or derivative weights).
164
+ n_ctrl : int
165
+ Total number of control points N (so coeffs_flat has shape (N, ...)).
166
+
167
+ Notes
168
+ -----
169
+ L = prod_i (degrees[i] + 1) nonzeros per row.
170
+ To get values, use `apply_basis_stencil_jax(cols, w, coeffs)`.
171
+ """
172
+ us = jnp.atleast_2d(us)
173
+ M, dim = us.shape
174
+
175
+ # Convert static, hashable knot_vectors (tuples of floats) to JAX arrays
176
+ knot_vectors = tuple(jnp.array(U) for U in knot_vectors)
177
+
178
+ if der_orders is None:
179
+ der_orders = tuple([0] * dim)
180
+
181
+ # 1) per-dim spans and derivative basis values
182
+ n_ctrls = []
183
+ spans = []
184
+ Ns = []
185
+
186
+ for i in range(dim):
187
+ p = degrees[i]
188
+ U = knot_vectors[i]
189
+ n = der_orders[i]
190
+
191
+ num_cp = len(U) - p - 1
192
+ n_ctrls.append(num_cp)
193
+
194
+ span = jnp.searchsorted(U, us[:, i], side="right") - 1
195
+ span = jnp.clip(span, p, num_cp - 1)
196
+ spans.append(span)
197
+
198
+ # ndu table (Algorithm A2.2, Piegl & Tiller)
199
+ ndu = jnp.zeros((M, p+1, p+1))
200
+ ndu = ndu.at[:, 0, 0].set(1.0)
201
+ left = jnp.zeros((M, p+1))
202
+ right = jnp.zeros((M, p+1))
203
+
204
+ for j in range(1, p+1):
205
+ left = left.at[:, j].set(us[:, i] - U[span + 1 - j])
206
+ right = right.at[:, j].set(U[span + j] - us[:, i])
207
+ saved = jnp.zeros((M,))
208
+
209
+ for r in range(j):
210
+ ndu = ndu.at[:, j, r].set(right[:, r+1] + left[:, j-r])
211
+ temp = ndu[:, r, j-1] / ndu[:, j, r]
212
+ ndu = ndu.at[:, r, j].set(saved + right[:, r+1] * temp)
213
+ saved = left[:, j-r] * temp
214
+
215
+ ndu = ndu.at[:, j, j].set(saved)
216
+
217
+ # DersBasisFuns (Algorithm A2.3)
218
+ ders = jnp.zeros((M, n+1, p+1))
219
+ ders = ders.at[:, 0, :].set(ndu[:, :, p])
220
+
221
+ a = jnp.zeros((2, M, p+1))
222
+ for r in range(p+1):
223
+ a = a.at[0, :, 0].set(1.0)
224
+
225
+ for k in range(1, n+1):
226
+ d = jnp.zeros((M,))
227
+ rk = r - k
228
+ pk = p - k
229
+
230
+ a = a.at[1, :, 0].set(jnp.where(rk >= 0, a[0, :, 0] / ndu[:, pk+1, rk], 0.0))
231
+ d = d + jnp.where(rk >= 0, a[1, :, 0] * ndu[:, rk, pk], 0.0)
232
+
233
+ j1 = 1 if rk >= -1 else -rk
234
+ j2 = k-1 if (r-1) <= pk else p - r
235
+ for j in range(j1, j2+1):
236
+ val = (a[0, :, j] - a[0, :, j-1]) / ndu[:, pk+1, rk+j]
237
+ a = a.at[1, :, j].set(val)
238
+ d = d + val * ndu[:, rk+j, pk]
239
+
240
+ a = a.at[1, :, k].set(jnp.where(r <= pk, -a[0, :, k-1] / ndu[:, pk+1, r], 0.0))
241
+ d = d + jnp.where(r <= pk, a[1, :, k] * ndu[:, r, pk], 0.0)
242
+
243
+ ders = ders.at[:, k, r].set(d)
244
+ a = a.at[0].set(a[1])
245
+ a = a.at[1].set(0.0)
246
+
247
+ for k in range(1, n+1):
248
+ factor = jnp.prod(jnp.arange(p, p-k, -1))
249
+ ders = ders.at[:, k, :].multiply(factor)
250
+
251
+ Ns.append(ders[:, n, :]) # (M, p+1)
252
+
253
+ # 2) local offset combinations (L, dim)
254
+ grids = [jnp.arange(p+1) for p in degrees]
255
+ mesh = jnp.meshgrid(*grids, indexing="ij")
256
+ offs = jnp.stack([g.ravel() for g in mesh], axis=-1) # (L, dim)
257
+ L = offs.shape[0]
258
+
259
+ # 3) C-order strides (python ints; safe if degrees/knot_vectors are static)
260
+ strides = np.empty(dim, int)
261
+ acc = 1
262
+ for i in range(dim-1, -1, -1):
263
+ strides[i] = acc
264
+ acc *= n_ctrls[i]
265
+
266
+ # 4) cols: (M, L)
267
+ cols = 0
268
+ for i in range(dim):
269
+ base = (spans[i] - degrees[i])[:, None] + offs[None, :, i]
270
+ cols = cols + base * strides[i]
271
+ cols = cols.astype(jnp.int32)
272
+
273
+ # 5) weights: (M, L)
274
+ w = jnp.ones((M, L))
275
+ for i in range(dim):
276
+ w = w * Ns[i][:, offs[:, i]]
277
+
278
+ n_ctrl = int(np.prod(n_ctrls))
279
+ return cols, w, n_ctrl
280
+
281
+
282
+ def apply_basis_stencil_jax(cols, w, coeffs):
283
+ """
284
+ Apply a stencil basis operator (cols, w) to coefficients.
285
+
286
+ Parameters
287
+ ----------
288
+ cols : (M, L) int32
289
+ w : (M, L) float
290
+ coeffs : (..., P) control points; will be flattened over control-point axes.
291
+
292
+ Returns
293
+ -------
294
+ out : (M, P)
295
+ """
296
+ coeffs2 = coeffs.reshape((-1, coeffs.shape[-1])) # (N, P)
297
+ gathered = coeffs2[cols] # (M, L, P)
298
+ return jnp.einsum("ml,mlp->mp", w, gathered)
299
+
300
+
301
+ def evaluate_b_spline_jax_fast(us, degrees, knot_vectors, coeffs, der_orders=None):
302
+ """
303
+ Fast B-spline evaluation: build (cols, w) stencil and apply via gather+einsum.
304
+ This avoids constructing a BCOO sparse matrix each call.
305
+ """
306
+ # If knot_vectors were passed as static Python tuples (to be hashable),
307
+ # convert them to JAX arrays here. compute_basis_stencil_jax also does
308
+ # this conversion, but doing it here keeps explicit intent.
309
+ knot_vectors = tuple(jnp.array(U) for U in knot_vectors)
310
+ cols, w, _ = compute_basis_stencil_jax(us, degrees, knot_vectors, der_orders)
311
+ return apply_basis_stencil_jax(cols, w, coeffs)
312
+
313
+
314
+ # If you want the old "matrix" object for debugging or compatibility:
315
+ def stencil_to_bcoo(cols, w, n_ctrl):
316
+ """
317
+ Convert stencil representation to a BCOO sparse matrix.
318
+
319
+ cols : (M, L)
320
+ w : (M, L)
321
+ n_ctrl : int
322
+ """
323
+ M, L = cols.shape
324
+ rows = jnp.repeat(jnp.arange(M, dtype=jnp.int32), L)
325
+ data = w.reshape(-1)
326
+ colr = cols.reshape(-1)
327
+ idx = jnp.stack((rows, colr), axis=-1)
328
+ return BCOO((data, idx), shape=(M, n_ctrl))
329
+
330
+
331
+ def evaluate_b_spline_jax(us, degrees, knot_vectors, coeffs, der_orders=None):
332
+ """
333
+ Evaluate B-spline basis functions at parameter u with derivatives.
334
+
335
+ Parameters:
336
+ -----------
337
+ us : jnp.ndarray, shape (M, d)
338
+ Parameter values where the B-spline basis functions are evaluated.
339
+ degrees : tuple of int
340
+ Degrees of the B-spline in each dimension.
341
+ knot_vectors : tuple of jnp.ndarray
342
+ Knot vectors for each dimension.
343
+ coeffs : jnp.ndarray, shape (N, num_phys_dims)
344
+ Coefficients of the B-spline basis functions.
345
+ der_orders : tuple of int, optional
346
+ Derivative orders for each dimension. If None, defaults to (0,) * d.
347
+ """
348
+ num_phys_dims = coeffs.shape[-1]
349
+ ndim = len(degrees)
350
+ if der_orders is None:
351
+ der_orders = (0,) * ndim
352
+
353
+ # Ensure knot_vectors are JAX arrays when passed as static Python tuples
354
+ knot_vectors = tuple(jnp.array(U) for U in knot_vectors)
355
+ # Compute the basis matrix using JAX
356
+ cols, w, _ = compute_basis_stencil_jax(us, degrees, knot_vectors, der_orders)
357
+
358
+ # Apply without constructing a sparse matrix
359
+ return apply_basis_stencil_jax(cols, w, coeffs)
360
+
361
+
362
+ if __name__ == "__main__":
363
+ np.random.seed(42) # For reproducibility
364
+ import lsdo_function_spaces as lfs
365
+ import time
366
+
367
+ jax.config.update("jax_enable_x64", True) # Use 64-bit precision for JAX
368
+
369
+ # Define the B-spline space parameters
370
+ num_cp_x = 10
371
+ num_cp_y = 8
372
+ nx = num_cp_x - 1 # Number of control points - 1
373
+ ny = num_cp_y - 1 # Number of control points - 1
374
+ px = 3 # Degree of the B-spline
375
+ py = 2 # Degree of the B-spline
376
+ p = (px, py)
377
+ coefficients_shape = (num_cp_x, num_cp_y)
378
+ derivative_orders = (1, 1) # derivative orders for the evaluation
379
+
380
+ # Build knot vectors as Python tuples of floats so they are hashable
381
+ # and can be used as static arguments to jax.jit.
382
+ knots = tuple(
383
+ tuple(np.concatenate([
384
+ np.zeros(p[i]),
385
+ np.linspace(0, 1, coefficients_shape[i] - p[i] + 1),
386
+ np.ones(p[i])
387
+ ]).tolist())
388
+ for i in range(len(p))
389
+ )
390
+
391
+ # Define the coefficients
392
+ coeffs_x, coeffs_y = np.meshgrid(np.linspace(0, 5, num_cp_x), np.linspace(0, 2, num_cp_y), indexing='ij')
393
+ coeffs = np.array(np.stack((coeffs_x, coeffs_y, 0.2 * np.random.rand(num_cp_x, num_cp_y)), axis=-1))
394
+ coeffs_jnp = jnp.array(coeffs.reshape(-1, 3))
395
+
396
+ eval_jit_old = jax.jit(
397
+ evaluate_b_spline_jax,
398
+ static_argnames=('degrees', 'knot_vectors', 'der_orders')
399
+ )
400
+
401
+ eval_jit_new = jax.jit(
402
+ evaluate_b_spline_jax_fast,
403
+ static_argnames=('degrees', 'knot_vectors', 'der_orders')
404
+ )
405
+
406
+ # Create parameter coordinates for evaluation
407
+ num_para_coords = 500 # NOTE: the actual number is squared
408
+ u1, v1 = np.meshgrid(np.linspace(0, 1, num_para_coords), np.linspace(0, 1, num_para_coords), indexing='ij')
409
+ us1 = np.array(np.stack((u1.flatten(), v1.flatten()), axis=-1))
410
+ us1_jnp = jnp.array(us1)
411
+
412
+ b_spline_eval_old = eval_jit_old(
413
+ us1_jnp,
414
+ p,
415
+ knots,
416
+ coeffs_jnp,
417
+ derivative_orders
418
+ ).block_until_ready()
419
+
420
+ t1 = time.perf_counter()
421
+ b_spline_eval_old = eval_jit_old(
422
+ us1_jnp,
423
+ p,
424
+ knots,
425
+ coeffs_jnp,
426
+ derivative_orders
427
+ )
428
+ t2 = time.perf_counter()
429
+ print(f"Time to evaluate old JAX B-spline: {t2 - t1:.6f} seconds")
430
+
431
+ b_spline_eval_fast = eval_jit_new(
432
+ us1_jnp,
433
+ p,
434
+ knots,
435
+ coeffs_jnp,
436
+ derivative_orders
437
+ ).block_until_ready()
438
+ t3 = time.perf_counter()
439
+ b_spline_eval_fast = eval_jit_new(
440
+ us1_jnp,
441
+ p,
442
+ knots,
443
+ coeffs_jnp,
444
+ derivative_orders
445
+ ).block_until_ready()
446
+ t4 = time.perf_counter()
447
+ print(f"Time to evaluate fast JAX B-spline: {t4 - t3:.6f} seconds")
448
+
449
+ # print("Old eval:", b_spline_eval_old)
450
+ # print("Fast eval:", b_spline_eval_fast)
451
+ # print("Difference:", jnp.max(jnp.abs(b_spline_eval_old - b_spline_eval_fast)))
@@ -0,0 +1,249 @@
1
+ import numpy as np
2
+ import scipy.sparse as sp
3
+
4
+ def compute_basis_matrix_numpy(us, degrees, knot_vectors, der_orders=None):
5
+ """
6
+ Vectorized computation of nonzero B-spline basis funcs and their derivatives
7
+ for M parameter values, up to derivative order n (n ≤ p).
8
+
9
+ Parameters
10
+ ----------
11
+ us : ndarray, shape (M,)
12
+ Parameter values.
13
+ degrees : tuple(int)
14
+ Degree of the spline.
15
+ knot_vectors : tuple(ndarray)
16
+ Knot vectors.
17
+ der_orders : tuple(int),
18
+ Maximum derivative order (n ≤ p).
19
+
20
+ Returns
21
+ -------
22
+ basis_mat : sparse.coo_matrix
23
+ Sparse basis matrix
24
+ """
25
+ us = us.reshape(us.shape[0], -1) # Ensure us is 2D
26
+ M, dim = us.shape
27
+
28
+ if der_orders is None:
29
+ der_orders = [0] * dim
30
+ elif len(der_orders) != dim:
31
+ if len(der_orders) == 1:
32
+ der_orders = der_orders * dim
33
+ else:
34
+ raise ValueError("der_orders must be either a single int or a tuple of ints with length equal to the number of dimensions.")
35
+
36
+ # 1) find spans for all us
37
+ # span m satisfies U[i] ≤ u_m < U[i+1]
38
+ n_ctrls = []
39
+ spans = []
40
+ Ns = []
41
+
42
+ for i in range(dim):
43
+ p = degrees[i]
44
+ U = knot_vectors[i]
45
+ n = der_orders[i]
46
+
47
+ num_cps = len(U) - p - 1
48
+ n_ctrls.append(num_cps)
49
+
50
+ span = np.searchsorted(U, us[:, i], side="right") - 1
51
+ span = np.clip(span, p, len(U)-p-2) # clamp to valid [p, n_ctrl-1]
52
+ spans.append(span)
53
+
54
+ # 2) build the ndu table for all M points at once: shape (M, p+1, p+1)
55
+ ndu = np.zeros((M, p+1, p+1))
56
+ left = np.zeros((M, p+1))
57
+ right = np.zeros((M, p+1))
58
+
59
+ ndu[:,0,0] = 1.0
60
+ for j in range(1, p+1):
61
+ # left and right distances
62
+ left[:, j] = us[:, i] - U[span + 1 - j]
63
+ right[:, j] = U[span + j] - us[:, i]
64
+ saved = np.zeros(M)
65
+
66
+ for r in range(j):
67
+ ndu[:, j, r] = right[:, r+1] + left[:, j-r]
68
+ temp = ndu[:, r, j-1] / ndu[:, j, r]
69
+ ndu[:, r, j] = saved + right[:, r+1] * temp
70
+ saved = left[:, j-r] * temp
71
+
72
+ ndu[:, j, j] = saved
73
+
74
+ # 3) allocate ders array and load zero-th derivatives
75
+ # print("M:", M, "n:", n, "p:", p)
76
+ ders = np.zeros((M, n+1, p+1))
77
+ ders[:, 0, :] = ndu[:, :, p]
78
+
79
+ if n == 0:
80
+ # If no derivatives requested, just return the zero-th order
81
+ Ns.append(ders[:, 0, :])
82
+ continue
83
+
84
+ # 4) compute derivatives via Alg A2.3
85
+ a = np.zeros((2, M, p+1))
86
+ for r in range(p+1):
87
+ a[0, :, 0] = 1.0
88
+ for k in range(1, n+1):
89
+ if k > p:
90
+ break # No derivatives beyond degree
91
+ d = np.zeros(M)
92
+ rk = r - k
93
+ pk = p - k
94
+
95
+ # first term
96
+ if rk >= 0:
97
+ a[1, :, 0] = a[0, :, 0] / ndu[:, pk+1, rk]
98
+ d += a[1, :, 0] * ndu[:, rk, pk]
99
+ else:
100
+ a[1, :, 0] = 0.0
101
+
102
+ # inner terms
103
+ j1 = 1 if rk >= -1 else -rk
104
+ j2 = k-1 if (r-1) <= pk else p - r
105
+ for j in range(j1, j2+1):
106
+ a[1, :, j] = (a[0, :, j] - a[0, :, j-1]) / ndu[:, pk+1, rk+j]
107
+ d += a[1, :, j] * ndu[:, rk+j, pk]
108
+
109
+ # last term
110
+ if r <= pk:
111
+ a[1, :, k] = -a[0, :, k-1] / ndu[:, pk+1, r]
112
+ d += a[1, :, k] * ndu[:, r, pk]
113
+ else:
114
+ a[1, :, k] = 0.0
115
+
116
+ ders[:, k, r] = d
117
+
118
+ # swap rows for next k
119
+ a[0, :, :], a[1, :, :] = a[1, :, :], a[0, :, :]
120
+
121
+ # 5) multiply through by factorial factors
122
+ # ders[k, :, :] *= p*(p-1)*...*(p-k+1)
123
+ for k in range(1, n+1):
124
+ ders[:, k, :] *= np.prod(np.arange(p, p-k, -1))
125
+
126
+ Ns.append(ders[:, -1, :])
127
+
128
+ # 2) build all local-offset combinations offs of shape (L, d),
129
+ # where L = prod_i (p_i+1)
130
+ grids = [np.arange(p+1) for p in degrees]
131
+ mesh = np.meshgrid(*grids, indexing="ij")
132
+ offs = np.stack([g.ravel() for g in mesh], axis=-1) # shape (L, d)
133
+ L = offs.shape[0]
134
+
135
+ # 3) compute C-order strides for flattening a grid of shape n_ctrls
136
+ # stride[i] = prod(n_ctrls[i+1:])
137
+ strides = np.empty(dim, int)
138
+ acc = 1
139
+ for i in range(dim-1, -1, -1):
140
+ strides[i] = acc
141
+ acc *= n_ctrls[i]
142
+
143
+ # 4) assemble sparse entries
144
+ # rows: 0..M-1 repeated each L times
145
+ rows = np.repeat(np.arange(M), L)
146
+
147
+ # cols: sum_i [ (spans[i] - p_i + offs[:,i]) * strides[i] ], broadcasted over M×L
148
+ cols = np.zeros((M, L), int)
149
+ for i in range(dim):
150
+ base = (spans[i] - degrees[i])[:,None] + offs[None,:,i]
151
+ cols += base * strides[i]
152
+ cols = cols.ravel()
153
+
154
+ # data: product over i of Ns[i][k, offs[:,i]]
155
+ data = np.ones((M, L))
156
+ for i in range(dim):
157
+ data *= Ns[i][:, offs[:,i]]
158
+ data = data.ravel()
159
+
160
+ # 5) build the COO
161
+ shape = (M, np.prod(n_ctrls))
162
+ B_coo = sp.coo_matrix((data, (rows, cols)), shape=shape)
163
+ return B_coo
164
+
165
+
166
+ def evaluate_b_spline_numpy(
167
+ us,
168
+ degrees,
169
+ knot_vectors,
170
+ coefficients,
171
+ der_orders=None,
172
+ ):
173
+ """
174
+ Evaluate B-spline at given parametric coordinates using numpy.
175
+
176
+ Parameters
177
+ ----------
178
+ us : ndarray, shape (M, d)
179
+ Parametric coordinates.
180
+ degrees : tuple(int)
181
+ Degrees of the B-spline in each dimension.
182
+ knot_vectors : tuple(ndarray)
183
+ Knot vectors for each dimension.
184
+ coefficients : ndarray, shape (n_ctrls_1, n_ctrls_2, ..., dim_out)
185
+ Control point coefficients.
186
+ der_orders : tuple(int), optional
187
+ Derivative orders for each dimension. If None, defaults to (0,) * d.
188
+ Returns
189
+ -------
190
+ values : ndarray, shape (M, dim_out)
191
+ Evaluated B-spline values at the given parametric coordinates.
192
+ """
193
+ # if der_orders is None:
194
+ # der_orders = (0,) * len(degrees)
195
+
196
+ B = compute_basis_matrix_numpy(us, degrees, knot_vectors, der_orders)
197
+ return B @ coefficients.reshape(-1, coefficients.shape[-1])
198
+
199
+
200
+ if __name__ == "__main__":
201
+ np.random.seed(42) # For reproducibility
202
+ import time
203
+
204
+ num_cp_x = 10
205
+ num_cp_y = 8
206
+ nx = num_cp_x - 1 # Number of control points - 1
207
+ ny = num_cp_y - 1 # Number of control points - 1
208
+ px = 3 # Degree of the B-spline
209
+ py = 2 # Degree of the B-spline
210
+
211
+ knots_x = np.concatenate(
212
+ [np.zeros(px),
213
+ np.linspace(0, 1, num_cp_x - px + 1),
214
+ np.ones(px)]
215
+ )
216
+ # print("knots_x", knots_x)
217
+ knots_y = np.concatenate(
218
+ [np.zeros(py),
219
+ np.linspace(0, 1, num_cp_y - py + 1),
220
+ np.ones(py)]
221
+ )
222
+
223
+ knots = (knots_x, knots_y) # Knot vectors for each dimension
224
+ knots_jnp = (np.array(knots_x), np.array(knots_y)) # JAX-compatible knot vectors
225
+
226
+ # Degrees of the B-spline in each dimension
227
+ p = (px, py)
228
+
229
+ # create control points
230
+ coeffs_x, coeffs_y = np.meshgrid(np.linspace(0, 5, num_cp_x), np.linspace(0, 2, num_cp_y), indexing='ij')
231
+ coeffs = np.array(np.stack((coeffs_x, coeffs_y, 0.2 * np.random.rand(num_cp_x, num_cp_y)), axis=-1))
232
+
233
+ num_para_coords = 500 # NOTE: the actual number is squared
234
+ u1, v1 = np.meshgrid(np.linspace(0, 1, num_para_coords), np.linspace(0, 1, num_para_coords), indexing='ij')
235
+ us1 = np.array(np.stack((u1.flatten(), v1.flatten()), axis=-1))
236
+
237
+ der_orders = (0, 0) # Derivative orders for each dimension
238
+
239
+ t1 = time.perf_counter()
240
+ b_spline_eval = evaluate_b_spline_numpy(
241
+ us1,
242
+ p,
243
+ knots,
244
+ coeffs, der_orders
245
+ ).reshape(-1, 3)
246
+ t2 = time.perf_counter()
247
+ print(f"Time to evaluate numpy B-spline: {t2 - t1:.6f} seconds")
248
+ print("b_spline_eval", b_spline_eval.shape)
249
+