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