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