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,172 @@
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
+ if der_orders is None:
20
+ der_orders = tuple([0] * dim)
21
+
22
+ # 1) per-dim spans, ndu & ders up to der_orders[i]
23
+ n_ctrls = []
24
+ spans = []
25
+ Ns = [] # will hold ders[:, n_i, :] per dimension
26
+
27
+ for i in range(dim):
28
+ p = degrees[i]
29
+ num_cp = len(knot_vectors[i]) - p - 1
30
+ U = jnp.asarray(knot_vectors[i])
31
+ n = der_orders[i]
32
+ n_ctrls.append(num_cp)
33
+
34
+ # find spans: shape (M,)
35
+ span = jnp.searchsorted(U, us[:, i], side="right") - 1
36
+ span = jnp.clip(span, p, num_cp - 1)
37
+ spans.append(span)
38
+
39
+ # build ndu table: shape (M, p+1, p+1)
40
+ ndu = jnp.zeros((M, p+1, p+1))
41
+ ndu = ndu.at[:, 0, 0].set(1.0)
42
+ left = jnp.zeros((M, p+1))
43
+ right = jnp.zeros((M, p+1))
44
+
45
+ for j in range(1, p+1):
46
+ # vectorized distances
47
+ left = left.at[:, j].set(us[:, i] - U[span + 1 - j])
48
+ right = right.at[:, j].set(U[span + j] - us[:, i])
49
+ saved = jnp.zeros((M,))
50
+
51
+ for r in range(j):
52
+ # lower triangle
53
+ ndu = ndu.at[:, j, r].set(right[:, r+1] + left[:, j-r])
54
+ temp = ndu[:, r, j-1] / ndu[:, j, r]
55
+ # upper triangle
56
+ ndu = ndu.at[:, r, j].set(saved + right[:, r+1] * temp)
57
+ saved = left[:, j-r] * temp
58
+
59
+ ndu = ndu.at[:, j, j].set(saved)
60
+
61
+ # now DersBasisFuns up to order n (Algorithm A2.3)
62
+ # ders shape (M, n+1, p+1)
63
+ ders = jnp.zeros((M, n+1, p+1))
64
+ ders = ders.at[:, 0, :].set(ndu[:, :, p])
65
+
66
+ # a buffer for alternating
67
+ a = jnp.zeros((2, M, p+1))
68
+ for r in range(p+1):
69
+ # initialize a row
70
+ a = a.at[0, :, 0].set(1.0)
71
+
72
+ for k in range(1, n+1):
73
+ d = jnp.zeros((M,))
74
+ rk = r - k
75
+ pk = p - k
76
+
77
+ # first term
78
+ a = a.at[1, :, 0].set(
79
+ jnp.where(rk >= 0, a[0, :, 0] / ndu[:, pk+1, rk], 0.0)
80
+ )
81
+ d = d + jnp.where(rk >= 0,
82
+ a[1, :, 0] * ndu[:, rk, pk], 0.0)
83
+
84
+ # inner terms
85
+ j1 = 1 if rk >= -1 else -rk
86
+ j2 = k-1 if (r-1) <= pk else p - r
87
+ for j in range(j1, j2+1):
88
+ val = (a[0, :, j] - a[0, :, j-1]) / ndu[:, pk+1, rk+j]
89
+ a = a.at[1, :, j].set(val)
90
+ d = d + val * ndu[:, rk+j, pk]
91
+
92
+ # last term
93
+ a = a.at[1, :, k].set(
94
+ jnp.where(r <= pk, -a[0, :, k-1] / ndu[:, pk+1, r], 0.0)
95
+ )
96
+ d = d + jnp.where(r <= pk,
97
+ a[1, :, k] * ndu[:, r, pk], 0.0)
98
+
99
+ ders = ders.at[:, k, r].set(d)
100
+ # swap rows in a
101
+ a = a.at[0].set(a[1])
102
+ a = a.at[1].set(0.0)
103
+
104
+ # scale derivatives by factorial factors
105
+ for k in range(1, n+1):
106
+ factor = jnp.prod(jnp.arange(p, p-k, -1))
107
+ ders = ders.at[:, k, :].multiply(factor)
108
+
109
+ # pick off the highest derivative requested
110
+ Ns.append(ders[:, n, :])
111
+
112
+ # 2) build all local-offset combinations (static)
113
+ grids = [jnp.arange(p+1) for p in degrees]
114
+ mesh = jnp.meshgrid(*grids, indexing="ij")
115
+ offs = jnp.stack([g.ravel() for g in mesh], axis=-1) # (L, d)
116
+ L = offs.shape[0]
117
+
118
+ # 3) compute C-order strides (python ints)
119
+ strides = np.empty(dim, int)
120
+ acc = 1
121
+ for i in range(dim-1, -1, -1):
122
+ strides[i] = acc
123
+ acc *= n_ctrls[i]
124
+
125
+ # 4) assemble sparse COO entries
126
+ rows = jnp.repeat(jnp.arange(M), L)
127
+
128
+ # columns
129
+ cols = 0
130
+ for i in range(dim):
131
+ base = (spans[i] - degrees[i])[:, None] + offs[None, :, i]
132
+ cols = cols + base * strides[i]
133
+ cols = cols.ravel()
134
+
135
+ # data = product over dims of Ns[i][:, offs[:,i]]
136
+ data = jnp.ones((M, L))
137
+ for i in range(dim):
138
+ data = data * Ns[i][:, offs[:, i]]
139
+ data = data.ravel()
140
+
141
+ # shape as python ints
142
+ shape = (M, int(np.prod(n_ctrls)))
143
+ Bcoo = BCOO((data, jnp.stack((rows, cols), axis=-1)), shape=shape)
144
+ return Bcoo
145
+
146
+ def evaluate_b_spline_jax(us, degrees, knot_vectors, coeffs, der_orders=None):
147
+ """
148
+ Evaluate B-spline basis functions at parameter u with derivatives.
149
+
150
+ Parameters:
151
+ -----------
152
+ us : jnp.ndarray, shape (M, d)
153
+ Parameter values where the B-spline basis functions are evaluated.
154
+ degrees : tuple of int
155
+ Degrees of the B-spline in each dimension.
156
+ knot_vectors : tuple of jnp.ndarray
157
+ Knot vectors for each dimension.
158
+ coeffs : jnp.ndarray, shape (N, num_phys_dims)
159
+ Coefficients of the B-spline basis functions.
160
+ der_orders : tuple of int, optional
161
+ Derivative orders for each dimension. If None, defaults to (0,) * d.
162
+ """
163
+ num_phys_dims = coeffs.shape[-1]
164
+ ndim = len(degrees)
165
+ if der_orders is None:
166
+ der_orders = (0,) * ndim
167
+
168
+ # Compute the basis matrix using JAX
169
+ basis_matrix = compute_basis_matrix_jax(us, degrees, knot_vectors, der_orders)
170
+
171
+ # Evaluate the B-spline basis functions
172
+ return basis_matrix @ coeffs.reshape(-1, num_phys_dims)
@@ -0,0 +1,382 @@
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
+ """Original matrix-builder API (returns a BCOO sparse matrix).
9
+
10
+ This is kept for backwards compatibility with the baseline file you shared.
11
+
12
+ Parameters
13
+ ----------
14
+ us : jnp.ndarray, shape (M, d)
15
+ degrees : tuple[int]
16
+ knot_vectors : tuple[jnp.ndarray]
17
+ der_orders : tuple[int] | None
18
+ """
19
+ knot_vectors = tuple(jnp.array(U) for U in knot_vectors)
20
+ us = jnp.atleast_2d(us)
21
+ M, dim = us.shape
22
+
23
+ if der_orders is None:
24
+ der_orders = tuple([0] * dim)
25
+
26
+ n_ctrls = []
27
+ spans = []
28
+ Ns = [] # ders[:, n_i, :] per dimension
29
+
30
+ for i in range(dim):
31
+ p = degrees[i]
32
+ U = knot_vectors[i]
33
+ n = der_orders[i]
34
+
35
+ num_cp = len(U) - p - 1
36
+ n_ctrls.append(num_cp)
37
+
38
+ span = jnp.searchsorted(U, us[:, i], side="right") - 1
39
+ span = jnp.clip(span, p, num_cp - 1)
40
+ spans.append(span)
41
+
42
+ ndu = jnp.zeros((M, p + 1, p + 1))
43
+ ndu = ndu.at[:, 0, 0].set(1.0)
44
+ left = jnp.zeros((M, p + 1))
45
+ right = jnp.zeros((M, p + 1))
46
+
47
+ for j in range(1, p + 1):
48
+ left = left.at[:, j].set(us[:, i] - U[span + 1 - j])
49
+ right = right.at[:, j].set(U[span + j] - us[:, i])
50
+ saved = jnp.zeros((M,))
51
+
52
+ for r in range(j):
53
+ ndu = ndu.at[:, j, r].set(right[:, r + 1] + left[:, j - r])
54
+ temp = ndu[:, r, j - 1] / ndu[:, j, r]
55
+ ndu = ndu.at[:, r, j].set(saved + right[:, r + 1] * temp)
56
+ saved = left[:, j - r] * temp
57
+
58
+ ndu = ndu.at[:, j, j].set(saved)
59
+
60
+ # DersBasisFuns up to order n (Algorithm A2.3)
61
+ ders = jnp.zeros((M, n + 1, p + 1))
62
+ ders = ders.at[:, 0, :].set(ndu[:, :, p])
63
+
64
+ a = jnp.zeros((2, M, p + 1))
65
+ for r in range(p + 1):
66
+ a = a.at[0, :, 0].set(1.0)
67
+
68
+ for k in range(1, n + 1):
69
+ d = jnp.zeros((M,))
70
+ rk = r - k
71
+ pk = p - k
72
+
73
+ a = a.at[1, :, 0].set(
74
+ jnp.where(rk >= 0, a[0, :, 0] / ndu[:, pk + 1, rk], 0.0)
75
+ )
76
+ d = d + jnp.where(rk >= 0, a[1, :, 0] * ndu[:, rk, pk], 0.0)
77
+
78
+ j1 = 1 if rk >= -1 else -rk
79
+ j2 = k - 1 if (r - 1) <= pk else p - r
80
+ for j in range(j1, j2 + 1):
81
+ val = (a[0, :, j] - a[0, :, j - 1]) / ndu[:, pk + 1, rk + j]
82
+ a = a.at[1, :, j].set(val)
83
+ d = d + val * ndu[:, rk + j, pk]
84
+
85
+ a = a.at[1, :, k].set(
86
+ jnp.where(r <= pk, -a[0, :, k - 1] / ndu[:, pk + 1, r], 0.0)
87
+ )
88
+ d = d + jnp.where(r <= pk, a[1, :, k] * ndu[:, r, pk], 0.0)
89
+
90
+ ders = ders.at[:, k, r].set(d)
91
+ a = a.at[0].set(a[1])
92
+ a = a.at[1].set(0.0)
93
+
94
+ for k in range(1, n + 1):
95
+ factor = jnp.prod(jnp.arange(p, p - k, -1))
96
+ ders = ders.at[:, k, :].multiply(factor)
97
+
98
+ Ns.append(ders[:, n, :])
99
+
100
+ # local-offset combinations
101
+ grids = [jnp.arange(p + 1) for p in degrees]
102
+ mesh = jnp.meshgrid(*grids, indexing="ij")
103
+ offs = jnp.stack([g.ravel() for g in mesh], axis=-1) # (L, d)
104
+ L = offs.shape[0]
105
+
106
+ # C-order strides (python ints)
107
+ strides = np.empty(dim, int)
108
+ acc = 1
109
+ for i in range(dim - 1, -1, -1):
110
+ strides[i] = acc
111
+ acc *= n_ctrls[i]
112
+
113
+ rows = jnp.repeat(jnp.arange(M), L)
114
+
115
+ cols = 0
116
+ for i in range(dim):
117
+ base = (spans[i] - degrees[i])[:, None] + offs[None, :, i]
118
+ cols = cols + base * strides[i]
119
+ cols = cols.ravel()
120
+
121
+ data = jnp.ones((M, L))
122
+ for i in range(dim):
123
+ data = data * Ns[i][:, offs[:, i]]
124
+ data = data.ravel()
125
+
126
+ shape = (M, int(np.prod(n_ctrls)))
127
+ return BCOO((data, jnp.stack((rows, cols), axis=-1)), shape=shape)
128
+
129
+
130
+ def evaluate_b_spline_jax(us, degrees, knot_vectors, coeffs, der_orders=None):
131
+ """Baseline evaluation API (matrix-vector): y = B(us) @ coeffs_flat."""
132
+ num_phys_dims = coeffs.shape[-1]
133
+ ndim = len(degrees)
134
+ if der_orders is None:
135
+ der_orders = (0,) * ndim
136
+
137
+ B = compute_basis_matrix_jax(us, degrees, knot_vectors, der_orders)
138
+ return B @ coeffs.reshape(-1, num_phys_dims)
139
+
140
+
141
+ def make_bspline_evaluator(degrees, knot_vectors, der_orders=None, *, jit=True):
142
+ """Create a fast B-spline evaluator compiled once per spline space.
143
+
144
+ This returns a callable with signature:
145
+
146
+ eval_fn(us, coeffs) -> values
147
+
148
+ where:
149
+ - us: (M, d) parametric coordinates
150
+ - coeffs: (..., num_phys_dims) control points / coefficients (any shape),
151
+ flattened internally in C-order, exactly like the baseline.
152
+
153
+ The returned function computes the same result as `evaluate_b_spline_jax(...)`
154
+ from the original file (same basis math and coefficient flattening), but uses
155
+ a stencil-style gather+einsum instead of constructing a sparse BCOO matrix.
156
+
157
+ Notes
158
+ -----
159
+ * `degrees`, `knot_vectors`, `der_orders` are closed over, so JAX compiles once
160
+ per unique spline space. Only `us` and `coeffs` are dynamic arguments.
161
+ * Output matches the baseline up to floating-point roundoff.
162
+ """
163
+ degrees = tuple(int(p) for p in degrees)
164
+ dim = len(degrees)
165
+
166
+ knot_vectors = tuple(jnp.asarray(U) for U in knot_vectors)
167
+ if der_orders is None:
168
+ der_orders = (0,) * dim
169
+ der_orders = tuple(int(n) for n in der_orders)
170
+
171
+ # control-point grid sizes per dim
172
+ n_ctrls = tuple(int(len(knot_vectors[i]) - degrees[i] - 1) for i in range(dim))
173
+
174
+ # Precompute local offsets (L, dim) and per-dim offset column vectors
175
+ grids = [np.arange(p + 1, dtype=np.int32) for p in degrees]
176
+ mesh = np.meshgrid(*grids, indexing="ij")
177
+ offs_np = np.stack([g.ravel() for g in mesh], axis=-1).astype(np.int32) # (L, dim)
178
+ offs = jnp.asarray(offs_np, dtype=jnp.int32)
179
+ L = int(offs_np.shape[0])
180
+ offs_cols = [offs[:, i] for i in range(dim)] # each (L,)
181
+
182
+ # C-order strides (python ints -> embedded constant array)
183
+ strides_np = np.empty(dim, dtype=np.int32)
184
+ acc = 1
185
+ for i in range(dim - 1, -1, -1):
186
+ strides_np[i] = acc
187
+ acc *= n_ctrls[i]
188
+ strides = jnp.asarray(strides_np, dtype=jnp.int32)
189
+
190
+ def _basis_1d(u, p, U, n):
191
+ """Compute span and the n-th derivative basis vector (size p+1) for M points."""
192
+ u = jnp.atleast_1d(u)
193
+ M = u.shape[0]
194
+ num_cp = int(len(U) - p - 1)
195
+
196
+ span = jnp.searchsorted(U, u, side="right") - 1
197
+ span = jnp.clip(span, p, num_cp - 1)
198
+
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(u - U[span + 1 - j])
206
+ right = right.at[:, j].set(U[span + j] - u)
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
+ ders = jnp.zeros((M, n + 1, p + 1))
218
+ ders = ders.at[:, 0, :].set(ndu[:, :, p])
219
+
220
+ a = jnp.zeros((2, M, p + 1))
221
+ for r in range(p + 1):
222
+ a = a.at[0, :, 0].set(1.0)
223
+
224
+ for k in range(1, n + 1):
225
+ d = jnp.zeros((M,))
226
+ rk = r - k
227
+ pk = p - k
228
+
229
+ a = a.at[1, :, 0].set(
230
+ jnp.where(rk >= 0, a[0, :, 0] / ndu[:, pk + 1, rk], 0.0)
231
+ )
232
+ d = d + jnp.where(rk >= 0, a[1, :, 0] * ndu[:, rk, pk], 0.0)
233
+
234
+ j1 = 1 if rk >= -1 else -rk
235
+ j2 = k - 1 if (r - 1) <= pk else p - r
236
+ for j in range(j1, j2 + 1):
237
+ val = (a[0, :, j] - a[0, :, j - 1]) / ndu[:, pk + 1, rk + j]
238
+ a = a.at[1, :, j].set(val)
239
+ d = d + val * ndu[:, rk + j, pk]
240
+
241
+ a = a.at[1, :, k].set(
242
+ jnp.where(r <= pk, -a[0, :, k - 1] / ndu[:, pk + 1, r], 0.0)
243
+ )
244
+ d = d + jnp.where(r <= pk, a[1, :, k] * ndu[:, r, pk], 0.0)
245
+
246
+ ders = ders.at[:, k, r].set(d)
247
+ a = a.at[0].set(a[1])
248
+ a = a.at[1].set(0.0)
249
+
250
+ for k in range(1, n + 1):
251
+ factor = jnp.prod(jnp.arange(p, p - k, -1))
252
+ ders = ders.at[:, k, :].multiply(factor)
253
+
254
+ return span, ders[:, n, :]
255
+
256
+ def _eval(us, coeffs):
257
+ us = jnp.atleast_2d(us)
258
+ M = us.shape[0]
259
+
260
+ num_phys_dims = int(coeffs.shape[-1])
261
+ coeffs_flat = coeffs.reshape(-1, num_phys_dims)
262
+
263
+ spans = []
264
+ Ns = []
265
+ for i in range(dim):
266
+ span_i, N_i = _basis_1d(us[:, i], degrees[i], knot_vectors[i], der_orders[i])
267
+ spans.append(span_i)
268
+ Ns.append(N_i) # (M, p_i+1)
269
+
270
+ cols = jnp.zeros((M, L), dtype=jnp.int32)
271
+ for i in range(dim):
272
+ base = (spans[i] - degrees[i])[:, None] + offs_cols[i][None, :]
273
+ cols = cols + base.astype(jnp.int32) * strides[i]
274
+
275
+ w = jnp.ones((M, L), dtype=coeffs_flat.dtype)
276
+ for i in range(dim):
277
+ w = w * Ns[i][:, offs_cols[i]]
278
+
279
+ gathered = coeffs_flat[cols] # (M, L, P)
280
+ return jnp.einsum("ml,mlp->mp", w, gathered)
281
+
282
+ return jax.jit(_eval) if jit else _eval
283
+
284
+
285
+ make_bspline_evaluator_jax = make_bspline_evaluator
286
+
287
+
288
+
289
+ if __name__ == "__main__":
290
+ np.random.seed(42) # For reproducibility
291
+ import lsdo_function_spaces as lfs
292
+ import time
293
+
294
+ jax.config.update("jax_enable_x64", True) # Use 64-bit precision for JAX
295
+
296
+ # Define the B-spline space parameters
297
+ num_cp_x = 10
298
+ num_cp_y = 8
299
+ nx = num_cp_x - 1 # Number of control points - 1
300
+ ny = num_cp_y - 1 # Number of control points - 1
301
+ px = 3 # Degree of the B-spline
302
+ py = 2 # Degree of the B-spline
303
+ p = (px, py)
304
+ coefficients_shape = (num_cp_x, num_cp_y)
305
+ derivative_orders = (2, 1) # derivative orders for the evaluation
306
+ knots = tuple(
307
+ tuple(
308
+ np.concatenate([
309
+ np.zeros(p[i]),
310
+ np.linspace(0, 1, coefficients_shape[i] - p[i] + 1),
311
+ np.ones(p[i])
312
+ ]).tolist()
313
+ )
314
+ for i in range(len(p))
315
+ )
316
+
317
+ # Define the coefficients
318
+ coeffs_x, coeffs_y = np.meshgrid(np.linspace(0, 5, num_cp_x), np.linspace(0, 2, num_cp_y), indexing='ij')
319
+ coeffs = np.array(np.stack((coeffs_x, coeffs_y, 0.2 * np.random.rand(num_cp_x, num_cp_y)), axis=-1))
320
+ coeffs_jnp = jnp.array(coeffs.reshape(-1, 3))
321
+
322
+ # Create parameter coordinates for evaluation
323
+ num_para_coords = 500 # NOTE: the actual number is squared
324
+ u1, v1 = np.meshgrid(np.linspace(0, 1, num_para_coords), np.linspace(0, 1, num_para_coords), indexing='ij')
325
+ us1 = np.array(np.stack((u1.flatten(), v1.flatten()), axis=-1))
326
+ us1_jnp = jnp.array(us1)
327
+
328
+
329
+ eval_fn = make_bspline_evaluator(
330
+ degrees=p,
331
+ knot_vectors=knots,
332
+ der_orders=derivative_orders, # or None
333
+ jit=True,
334
+ )
335
+
336
+ t1 = time.perf_counter()
337
+ b_spline_eval = eval_fn(
338
+ us1_jnp,
339
+ coeffs_jnp,
340
+ ).block_until_ready()
341
+ t2 = time.perf_counter()
342
+ print(f"Time to evaluate JAX B-spline (stencil): {t2 - t1:.6f} seconds")
343
+
344
+ t3 = time.perf_counter()
345
+ b_spline_eval = eval_fn(
346
+ us1_jnp,
347
+ coeffs_jnp,
348
+ ).block_until_ready()
349
+ t4 = time.perf_counter()
350
+ print(f"Time to evaluate JAX B-spline (stencil, 2nd call): {t4 - t3:.6f} seconds")
351
+
352
+ eval_jit_old = jax.jit(
353
+ evaluate_b_spline_jax,
354
+ static_argnames=('degrees', 'knot_vectors', 'der_orders')
355
+ )
356
+
357
+ t5 = time.perf_counter()
358
+ b_spline_eval_old = eval_jit_old(
359
+ us1_jnp,
360
+ p,
361
+ knots,
362
+ coeffs_jnp,
363
+ derivative_orders
364
+ ).block_until_ready()
365
+ t6 = time.perf_counter()
366
+ print(f"Time to evaluate 'old' JAX B-spline (baseline): {t6 - t5:.6f} seconds")
367
+
368
+ t1 = time.perf_counter()
369
+ b_spline_eval_old = eval_jit_old(
370
+ us1_jnp,
371
+ p,
372
+ knots,
373
+ coeffs_jnp,
374
+ derivative_orders
375
+ )
376
+ t2 = time.perf_counter()
377
+ print(f"Time to evaluate 'old' JAX B-spline: {t2 - t1:.6f} seconds")
378
+
379
+ # Verify that both methods give the same result
380
+ assert jnp.allclose(b_spline_eval, b_spline_eval_old, atol=1e-10), "Results from both methods do not match!"
381
+ print("Results from both methods match!")
382
+