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,391 @@
|
|
|
1
|
+
|
|
2
|
+
import numpy as np
|
|
3
|
+
import scipy.sparse as sp
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Callable, Optional, Sequence, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class BSplineSpaceCache:
|
|
10
|
+
"""Cached, spline-space-dependent data for fast repeated evaluation.
|
|
11
|
+
|
|
12
|
+
Notes
|
|
13
|
+
-----
|
|
14
|
+
This cache assumes:
|
|
15
|
+
- degrees and knot_vectors are fixed
|
|
16
|
+
- control-net grid shape (n_ctrls per dim) is fixed by knot_vectors and degrees
|
|
17
|
+
- flattening is C-order with strides computed accordingly
|
|
18
|
+
"""
|
|
19
|
+
degrees: Tuple[int, ...]
|
|
20
|
+
knot_vectors: Tuple[np.ndarray, ...]
|
|
21
|
+
n_ctrls: Tuple[int, ...]
|
|
22
|
+
dim: int
|
|
23
|
+
offs: np.ndarray # (L, dim) local offset combinations
|
|
24
|
+
strides: np.ndarray # (dim,) C-order strides
|
|
25
|
+
L: int # number of nonzeros per row (prod(p_i+1))
|
|
26
|
+
n_total: int # total control points (prod(n_ctrls))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _as_tuple_int(x: Sequence[int]) -> Tuple[int, ...]:
|
|
30
|
+
return tuple(int(v) for v in x)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _as_tuple_arrays(knot_vectors: Sequence[np.ndarray]) -> Tuple[np.ndarray, ...]:
|
|
34
|
+
return tuple(np.asarray(kv) for kv in knot_vectors)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _normalize_us_numpy(us: np.ndarray, expected_dim: Optional[int] = None) -> np.ndarray:
|
|
38
|
+
"""Normalize parametric coordinates to shape (M, dim).
|
|
39
|
+
|
|
40
|
+
Accepts either a single point of shape (dim,) or a batch of shape (M, dim).
|
|
41
|
+
If expected_dim is provided, the last axis must match it.
|
|
42
|
+
"""
|
|
43
|
+
us = np.asarray(us, dtype=float)
|
|
44
|
+
if us.ndim == 1:
|
|
45
|
+
if expected_dim is None:
|
|
46
|
+
raise ValueError("For 1D input, expected_dim must be provided to disambiguate shape.")
|
|
47
|
+
if us.shape[0] != expected_dim:
|
|
48
|
+
raise ValueError(f"Single parametric point has dim={us.shape[0]}, expected {expected_dim}")
|
|
49
|
+
us = us.reshape(1, expected_dim)
|
|
50
|
+
elif us.ndim == 2:
|
|
51
|
+
if expected_dim is not None and us.shape[1] != expected_dim:
|
|
52
|
+
raise ValueError(f"us has dim={us.shape[1]}, expected {expected_dim}")
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError(f"us must have shape (dim,) or (M, dim); got shape {us.shape}")
|
|
55
|
+
return us
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def make_bspline_space_cache(
|
|
59
|
+
degrees: Sequence[int],
|
|
60
|
+
knot_vectors: Sequence[np.ndarray],
|
|
61
|
+
) -> BSplineSpaceCache:
|
|
62
|
+
"""Precompute offsets + strides for a fixed tensor-product B-spline space."""
|
|
63
|
+
degrees = _as_tuple_int(degrees)
|
|
64
|
+
knot_vectors = _as_tuple_arrays(knot_vectors)
|
|
65
|
+
dim = len(degrees)
|
|
66
|
+
if len(knot_vectors) != dim:
|
|
67
|
+
raise ValueError(f"degrees has dim={dim} but knot_vectors has len={len(knot_vectors)}")
|
|
68
|
+
|
|
69
|
+
n_ctrls = []
|
|
70
|
+
for p, U in zip(degrees, knot_vectors):
|
|
71
|
+
num_cps = len(U) - p - 1
|
|
72
|
+
if num_cps <= 0:
|
|
73
|
+
raise ValueError("Invalid knot vector / degree combination: num control points <= 0")
|
|
74
|
+
n_ctrls.append(int(num_cps))
|
|
75
|
+
n_ctrls = tuple(n_ctrls)
|
|
76
|
+
|
|
77
|
+
# Local offset combinations offs of shape (L, dim), L = prod_i (p_i+1)
|
|
78
|
+
grids = [np.arange(p + 1, dtype=int) for p in degrees]
|
|
79
|
+
mesh = np.meshgrid(*grids, indexing="ij")
|
|
80
|
+
offs = np.stack([g.ravel() for g in mesh], axis=-1) # (L, dim)
|
|
81
|
+
L = int(offs.shape[0])
|
|
82
|
+
|
|
83
|
+
# C-order strides for flattening a grid of shape n_ctrls
|
|
84
|
+
strides = np.empty(dim, dtype=int)
|
|
85
|
+
acc = 1
|
|
86
|
+
for i in range(dim - 1, -1, -1):
|
|
87
|
+
strides[i] = acc
|
|
88
|
+
acc *= n_ctrls[i]
|
|
89
|
+
|
|
90
|
+
n_total = int(np.prod(np.array(n_ctrls, dtype=int)))
|
|
91
|
+
|
|
92
|
+
return BSplineSpaceCache(
|
|
93
|
+
degrees=degrees,
|
|
94
|
+
knot_vectors=knot_vectors,
|
|
95
|
+
n_ctrls=n_ctrls,
|
|
96
|
+
dim=dim,
|
|
97
|
+
offs=offs,
|
|
98
|
+
strides=strides,
|
|
99
|
+
L=L,
|
|
100
|
+
n_total=n_total,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def compute_basis_stencil_numpy(
|
|
105
|
+
us: np.ndarray,
|
|
106
|
+
degrees: Sequence[int],
|
|
107
|
+
knot_vectors: Sequence[np.ndarray],
|
|
108
|
+
der_orders: Optional[Sequence[int]] = None,
|
|
109
|
+
cache: Optional[BSplineSpaceCache] = None,
|
|
110
|
+
) -> Tuple[np.ndarray, np.ndarray, BSplineSpaceCache]:
|
|
111
|
+
"""Compute the per-point sparse stencil (cols, weights) without building a sparse matrix.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
cols : ndarray, shape (M, L)
|
|
116
|
+
Flattened control-point indices for each query point.
|
|
117
|
+
w : ndarray, shape (M, L)
|
|
118
|
+
Corresponding tensor-product basis weights (or requested derivative weights).
|
|
119
|
+
cache : BSplineSpaceCache
|
|
120
|
+
The cache used/created (useful to reuse across calls).
|
|
121
|
+
"""
|
|
122
|
+
expected_dim = len(degrees) if cache is None else cache.dim
|
|
123
|
+
us = _normalize_us_numpy(us, expected_dim=expected_dim)
|
|
124
|
+
M, dim = us.shape
|
|
125
|
+
|
|
126
|
+
if cache is None:
|
|
127
|
+
cache = make_bspline_space_cache(degrees, knot_vectors)
|
|
128
|
+
else:
|
|
129
|
+
# Basic sanity check
|
|
130
|
+
if dim != cache.dim:
|
|
131
|
+
raise ValueError(f"us has dim={dim} but cache.dim={cache.dim}")
|
|
132
|
+
|
|
133
|
+
degrees_t = cache.degrees
|
|
134
|
+
knot_vectors_t = cache.knot_vectors
|
|
135
|
+
|
|
136
|
+
if der_orders is None:
|
|
137
|
+
der_orders = (0,) * dim
|
|
138
|
+
der_orders = _as_tuple_int(der_orders)
|
|
139
|
+
if len(der_orders) != dim:
|
|
140
|
+
raise ValueError(f"der_orders must have length {dim}, got {len(der_orders)}")
|
|
141
|
+
if any(n < 0 for n in der_orders):
|
|
142
|
+
raise ValueError(f"der_orders must be nonnegative, got {der_orders}")
|
|
143
|
+
|
|
144
|
+
spans = []
|
|
145
|
+
Ns = []
|
|
146
|
+
n_ctrls = cache.n_ctrls
|
|
147
|
+
|
|
148
|
+
# Per-dimension basis (or derivative) values at each point
|
|
149
|
+
for i in range(dim):
|
|
150
|
+
p = degrees_t[i]
|
|
151
|
+
U = knot_vectors_t[i]
|
|
152
|
+
n = der_orders[i]
|
|
153
|
+
num_cps = n_ctrls[i]
|
|
154
|
+
zero_derivative = n > p
|
|
155
|
+
|
|
156
|
+
# span: U[span] <= u < U[span+1]
|
|
157
|
+
span = np.searchsorted(U, us[:, i], side="right") - 1
|
|
158
|
+
span = np.clip(span, p, len(U) - p - 2) # valid: [p, num_cps-1]
|
|
159
|
+
spans.append(span)
|
|
160
|
+
|
|
161
|
+
if zero_derivative:
|
|
162
|
+
Ns.append(np.zeros((M, p + 1), dtype=float))
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Build NDU table for all M points: (M, p+1, p+1)
|
|
166
|
+
ndu = np.zeros((M, p + 1, p + 1), dtype=float)
|
|
167
|
+
left = np.zeros((M, p + 1), dtype=float)
|
|
168
|
+
right = np.zeros((M, p + 1), dtype=float)
|
|
169
|
+
|
|
170
|
+
ndu[:, 0, 0] = 1.0
|
|
171
|
+
for j in range(1, p + 1):
|
|
172
|
+
left[:, j] = us[:, i] - U[span + 1 - j]
|
|
173
|
+
right[:, j] = U[span + j] - us[:, i]
|
|
174
|
+
saved = np.zeros(M, dtype=float)
|
|
175
|
+
|
|
176
|
+
for r in range(j):
|
|
177
|
+
ndu[:, j, r] = right[:, r + 1] + left[:, j - r]
|
|
178
|
+
temp = ndu[:, r, j - 1] / ndu[:, j, r]
|
|
179
|
+
ndu[:, r, j] = saved + right[:, r + 1] * temp
|
|
180
|
+
saved = left[:, j - r] * temp
|
|
181
|
+
|
|
182
|
+
ndu[:, j, j] = saved
|
|
183
|
+
|
|
184
|
+
# Zero-th derivative basis: last column of NDU
|
|
185
|
+
if n == 0:
|
|
186
|
+
Ns.append(ndu[:, :, p]) # (M, p+1)
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# Full derivative table ders: (M, n+1, p+1)
|
|
190
|
+
ders = np.zeros((M, n + 1, p + 1), dtype=float)
|
|
191
|
+
ders[:, 0, :] = ndu[:, :, p]
|
|
192
|
+
|
|
193
|
+
# Alg A2.3 (Piegl & Tiller The NURBS book) for derivatives
|
|
194
|
+
a = np.zeros((2, M, p + 1), dtype=float)
|
|
195
|
+
for r in range(p + 1):
|
|
196
|
+
a[0, :, 0] = 1.0
|
|
197
|
+
for k in range(1, n + 1):
|
|
198
|
+
d = np.zeros(M, dtype=float)
|
|
199
|
+
rk = r - k
|
|
200
|
+
pk = p - k
|
|
201
|
+
|
|
202
|
+
if rk >= 0:
|
|
203
|
+
a[1, :, 0] = a[0, :, 0] / ndu[:, pk + 1, rk]
|
|
204
|
+
d += a[1, :, 0] * ndu[:, rk, pk]
|
|
205
|
+
else:
|
|
206
|
+
a[1, :, 0] = 0.0
|
|
207
|
+
|
|
208
|
+
j1 = 1 if rk >= -1 else -rk
|
|
209
|
+
j2 = k - 1 if (r - 1) <= pk else p - r
|
|
210
|
+
for j in range(j1, j2 + 1):
|
|
211
|
+
a[1, :, j] = (a[0, :, j] - a[0, :, j - 1]) / ndu[:, pk + 1, rk + j]
|
|
212
|
+
d += a[1, :, j] * ndu[:, rk + j, pk]
|
|
213
|
+
|
|
214
|
+
if r <= pk:
|
|
215
|
+
a[1, :, k] = -a[0, :, k - 1] / ndu[:, pk + 1, r]
|
|
216
|
+
d += a[1, :, k] * ndu[:, r, pk]
|
|
217
|
+
else:
|
|
218
|
+
a[1, :, k] = 0.0
|
|
219
|
+
|
|
220
|
+
ders[:, k, r] = d
|
|
221
|
+
|
|
222
|
+
# swap rows
|
|
223
|
+
a[0, :, :], a[1, :, :] = a[1, :, :], a[0, :, :]
|
|
224
|
+
|
|
225
|
+
# Multiply by factorial factors: p*(p-1)*...*(p-k+1)
|
|
226
|
+
fact = 1.0
|
|
227
|
+
for k in range(1, n + 1):
|
|
228
|
+
fact *= (p - (k - 1))
|
|
229
|
+
ders[:, k, :] *= fact
|
|
230
|
+
|
|
231
|
+
# Use the requested derivative order in this dim: ders[:, n, :]
|
|
232
|
+
Ns.append(ders[:, n, :]) # (M, p+1)
|
|
233
|
+
|
|
234
|
+
offs = cache.offs # (L, dim)
|
|
235
|
+
L = cache.L
|
|
236
|
+
|
|
237
|
+
# cols: (M, L)
|
|
238
|
+
cols = np.zeros((M, L), dtype=int)
|
|
239
|
+
for i in range(dim):
|
|
240
|
+
base = (spans[i] - degrees_t[i])[:, None] + offs[None, :, i]
|
|
241
|
+
cols += base * cache.strides[i]
|
|
242
|
+
|
|
243
|
+
# weights: (M, L) = product_i Ns_i[:, offs[:,i]]
|
|
244
|
+
w = np.ones((M, L), dtype=float)
|
|
245
|
+
for i in range(dim):
|
|
246
|
+
w *= Ns[i][:, offs[:, i]]
|
|
247
|
+
|
|
248
|
+
return cols, w, cache
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def apply_basis_stencil_numpy(
|
|
252
|
+
cols: np.ndarray,
|
|
253
|
+
w: np.ndarray,
|
|
254
|
+
coefficients: np.ndarray,
|
|
255
|
+
) -> np.ndarray:
|
|
256
|
+
"""Apply a basis stencil (cols, w) to coefficients via gather + contraction.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
cols : (M, L) int
|
|
261
|
+
w : (M, L) float
|
|
262
|
+
coefficients : (..., dim_out)
|
|
263
|
+
Control point coefficients on a tensor grid.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
values : (M, dim_out)
|
|
268
|
+
"""
|
|
269
|
+
coeffs = np.asarray(coefficients)
|
|
270
|
+
dim_out = coeffs.shape[-1]
|
|
271
|
+
coeffs_flat = coeffs.reshape(-1, dim_out) # C-order flatten
|
|
272
|
+
gathered = coeffs_flat[cols] # (M, L, dim_out)
|
|
273
|
+
# Weighted sum over L
|
|
274
|
+
return np.einsum("ml,mld->md", w, gathered)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def compute_basis_matrix_numpy(
|
|
278
|
+
us: np.ndarray,
|
|
279
|
+
degrees: Sequence[int],
|
|
280
|
+
knot_vectors: Sequence[np.ndarray],
|
|
281
|
+
der_orders: Optional[Sequence[int]] = None,
|
|
282
|
+
cache: Optional[BSplineSpaceCache] = None,
|
|
283
|
+
) -> sp.coo_matrix:
|
|
284
|
+
"""Compatibility API: build a SciPy COO basis matrix.
|
|
285
|
+
|
|
286
|
+
This is slower than the stencil apply path because it constructs a sparse matrix object.
|
|
287
|
+
Prefer `compute_basis_stencil_numpy` + `apply_basis_stencil_numpy` for performance.
|
|
288
|
+
"""
|
|
289
|
+
cols, w, cache = compute_basis_stencil_numpy(us, degrees, knot_vectors, der_orders, cache=cache)
|
|
290
|
+
expected_dim = len(degrees) if cache is None else cache.dim
|
|
291
|
+
us = _normalize_us_numpy(us, expected_dim=expected_dim)
|
|
292
|
+
M = us.shape[0]
|
|
293
|
+
L = cache.L
|
|
294
|
+
|
|
295
|
+
rows = np.repeat(np.arange(M, dtype=int), L)
|
|
296
|
+
cols_flat = cols.reshape(-1)
|
|
297
|
+
data = w.reshape(-1)
|
|
298
|
+
return sp.coo_matrix((data, (rows, cols_flat)), shape=(M, cache.n_total))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def evaluate_b_spline_numpy(
|
|
302
|
+
us: np.ndarray,
|
|
303
|
+
degrees: Sequence[int],
|
|
304
|
+
knot_vectors: Sequence[np.ndarray],
|
|
305
|
+
coefficients: np.ndarray,
|
|
306
|
+
der_orders: Optional[Sequence[int]] = None,
|
|
307
|
+
cache: Optional[BSplineSpaceCache] = None,
|
|
308
|
+
) -> np.ndarray:
|
|
309
|
+
"""Fast evaluation using stencil gather+einsum (same output as sparse-matmul path)."""
|
|
310
|
+
cols, w, cache = compute_basis_stencil_numpy(us, degrees, knot_vectors, der_orders, cache=cache)
|
|
311
|
+
return apply_basis_stencil_numpy(cols, w, coefficients)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def make_bspline_evaluator_numpy(
|
|
315
|
+
degrees: Sequence[int],
|
|
316
|
+
knot_vectors: Sequence[np.ndarray],
|
|
317
|
+
der_orders: Optional[Sequence[int]] = None,
|
|
318
|
+
) -> Callable[[np.ndarray, np.ndarray], np.ndarray]:
|
|
319
|
+
"""Factory that caches spline-space data and returns a fast evaluator.
|
|
320
|
+
|
|
321
|
+
The returned function has signature:
|
|
322
|
+
eval_fn(us, coefficients) -> values
|
|
323
|
+
|
|
324
|
+
It reuses the cached offsets/strides/control-net sizing across calls.
|
|
325
|
+
"""
|
|
326
|
+
cache = make_bspline_space_cache(degrees, knot_vectors)
|
|
327
|
+
degrees_t = cache.degrees
|
|
328
|
+
knot_vectors_t = cache.knot_vectors
|
|
329
|
+
if der_orders is None:
|
|
330
|
+
der_orders_t = (0,) * cache.dim
|
|
331
|
+
else:
|
|
332
|
+
der_orders_t = _as_tuple_int(der_orders)
|
|
333
|
+
|
|
334
|
+
def eval_fn(us: np.ndarray, coefficients: np.ndarray) -> np.ndarray:
|
|
335
|
+
cols, w, _ = compute_basis_stencil_numpy(
|
|
336
|
+
us,
|
|
337
|
+
degrees_t,
|
|
338
|
+
knot_vectors_t,
|
|
339
|
+
der_orders_t,
|
|
340
|
+
cache=cache,
|
|
341
|
+
)
|
|
342
|
+
return apply_basis_stencil_numpy(cols, w, coefficients)
|
|
343
|
+
|
|
344
|
+
return eval_fn
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
# Quick sanity + speed demo (mirrors your original main)
|
|
349
|
+
np.random.seed(42)
|
|
350
|
+
import time
|
|
351
|
+
|
|
352
|
+
num_cp_x = 10
|
|
353
|
+
num_cp_y = 8
|
|
354
|
+
px = 3
|
|
355
|
+
py = 2
|
|
356
|
+
|
|
357
|
+
knots_x = np.concatenate([np.zeros(px), np.linspace(0, 1, num_cp_x - px + 1), np.ones(px)])
|
|
358
|
+
knots_y = np.concatenate([np.zeros(py), np.linspace(0, 1, num_cp_y - py + 1), np.ones(py)])
|
|
359
|
+
knots = (knots_x, knots_y)
|
|
360
|
+
degrees = (px, py)
|
|
361
|
+
|
|
362
|
+
coeffs_x, coeffs_y = np.meshgrid(
|
|
363
|
+
np.linspace(0, 5, num_cp_x),
|
|
364
|
+
np.linspace(0, 2, num_cp_y),
|
|
365
|
+
indexing="ij",
|
|
366
|
+
)
|
|
367
|
+
coeffs = np.stack((coeffs_x, coeffs_y, 0.2 * np.random.rand(num_cp_x, num_cp_y)), axis=-1)
|
|
368
|
+
|
|
369
|
+
num_para_coords = 500
|
|
370
|
+
u1, v1 = np.meshgrid(np.linspace(0, 1, num_para_coords), np.linspace(0, 1, num_para_coords), indexing="ij")
|
|
371
|
+
us1 = np.stack((u1.ravel(), v1.ravel()), axis=-1)
|
|
372
|
+
|
|
373
|
+
der_orders = (1, 1)
|
|
374
|
+
|
|
375
|
+
# Old-style (sparse matrix) via compatibility API
|
|
376
|
+
t1 = time.perf_counter()
|
|
377
|
+
B = compute_basis_matrix_numpy(us1, degrees, knots, der_orders)
|
|
378
|
+
y_sparse = (B @ coeffs.reshape(-1, coeffs.shape[-1])).reshape(-1, 3)
|
|
379
|
+
t2 = time.perf_counter()
|
|
380
|
+
|
|
381
|
+
# New fast path via factory
|
|
382
|
+
eval_fn = make_bspline_evaluator_numpy(degrees, knots, der_orders)
|
|
383
|
+
t3 = time.perf_counter()
|
|
384
|
+
y_fast = eval_fn(us1, coeffs).reshape(-1, 3)
|
|
385
|
+
t4 = time.perf_counter()
|
|
386
|
+
|
|
387
|
+
max_err = np.max(np.abs(y_sparse - y_fast))
|
|
388
|
+
print(f"sparse time: {t2 - t1:.6f}s")
|
|
389
|
+
print(f"fast time: {t4 - t3:.6f}s")
|
|
390
|
+
print(f"% speedup: {(t2 - t1) / (t4 - t3):.2f}x")
|
|
391
|
+
print(f"max abs err: {max_err:.3e}")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.sparse as sps
|
|
3
|
+
from lsdo_function_spaces import FunctionSpace, Function
|
|
4
|
+
from scipy.spatial.distance import cdist
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Union
|
|
7
|
+
import csdl_alpha as csdl
|
|
8
|
+
|
|
9
|
+
class OperationFunctionSpace(FunctionSpace):
|
|
10
|
+
def __init__(self, inputs:list, operation:callable, num_parametric_dimensions:int):
|
|
11
|
+
'''
|
|
12
|
+
Function space that applies an operation to a list of inputs.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
inputs : list[Function]
|
|
17
|
+
List of functions/variables to apply the operation to.
|
|
18
|
+
operation : callable
|
|
19
|
+
Operation to apply to the functions - usually an internal csdl function, like csdl.add or csdl.cross.
|
|
20
|
+
'''
|
|
21
|
+
self.inputs = inputs
|
|
22
|
+
self.operation = operation
|
|
23
|
+
|
|
24
|
+
num_coefficients = 0
|
|
25
|
+
for input in inputs:
|
|
26
|
+
if isinstance(input, Function):
|
|
27
|
+
num_coefficients += np.prod(input.space.coefficients_shape)
|
|
28
|
+
|
|
29
|
+
super().__init__(num_parametric_dimensions, (0,))
|
|
30
|
+
|
|
31
|
+
def _evaluate(self, coefficients, parametric_coordinates, parametric_derivative_orders):
|
|
32
|
+
'''
|
|
33
|
+
Evaluates the function.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
parametric_coordinates : np.ndarray -- shape=(num_points, num_parametric_dimensions)
|
|
38
|
+
The coordinates at which to evaluate the function.
|
|
39
|
+
parametric_derivative_order : tuple = None -- shape=(num_points,num_parametric_dimensions)
|
|
40
|
+
The order of the parametric derivatives to evaluate.
|
|
41
|
+
coefficients : csdl.Variable = None -- shape=coefficients_shape
|
|
42
|
+
The coefficients of the function.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
function_values : csdl.Variable
|
|
47
|
+
The function evaluated at the given coordinates.
|
|
48
|
+
'''
|
|
49
|
+
import lsdo_function_spaces as lfs
|
|
50
|
+
|
|
51
|
+
if parametric_derivative_orders is not None:
|
|
52
|
+
raise ValueError('Derivatives not supported in operation function spaces')
|
|
53
|
+
# if coefficients is not None:
|
|
54
|
+
# raise ValueError('Coefficients not supported in operation function spaces')
|
|
55
|
+
|
|
56
|
+
op_inputs = []
|
|
57
|
+
for input in self.inputs:
|
|
58
|
+
if isinstance(input, lfs.Function):
|
|
59
|
+
op_inputs.append(input.evaluate(parametric_coordinates))
|
|
60
|
+
else:
|
|
61
|
+
op_inputs.append(input)
|
|
62
|
+
|
|
63
|
+
return self.operation(*op_inputs)
|
|
64
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.sparse as sps
|
|
3
|
+
from ..function_space import LinearFunctionSpace
|
|
4
|
+
from scipy.spatial.distance import cdist
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Union
|
|
7
|
+
from numpy.polynomial import polynomial as poly
|
|
8
|
+
|
|
9
|
+
class PolynomialSpace(LinearFunctionSpace):
|
|
10
|
+
"""
|
|
11
|
+
Polynomial Function Space.
|
|
12
|
+
|
|
13
|
+
This function space represents multivariate polynomial basis functions up to
|
|
14
|
+
a specified degree/order in each parametric dimension.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
num_parametric_dimensions : int
|
|
19
|
+
The number of parametric dimensions.
|
|
20
|
+
order : Union[int, tuple[int, ...]]
|
|
21
|
+
The polynomial order/degree in each parametric dimension.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, num_parametric_dimensions:int, order:Union[int, tuple[int, ...]]):
|
|
25
|
+
"""
|
|
26
|
+
Initialize a Polynomial function space.
|
|
27
|
+
"""
|
|
28
|
+
self.order = order
|
|
29
|
+
if isinstance(self.order, int):
|
|
30
|
+
self.order = (self.order,)*num_parametric_dimensions
|
|
31
|
+
|
|
32
|
+
self.size = np.prod([order + 1 for order in self.order])
|
|
33
|
+
super().__init__(num_parametric_dimensions, (self.size,))
|
|
34
|
+
|
|
35
|
+
def compute_basis_matrix(self, parametric_coordinates:np.ndarray, parametric_derivative_orders:np.ndarray=None, expansion_factor:int=None) -> np.ndarray:
|
|
36
|
+
"""
|
|
37
|
+
Compute the basis matrix for the given parametric coordinates.
|
|
38
|
+
"""
|
|
39
|
+
if parametric_derivative_orders is not None:
|
|
40
|
+
raise NotImplementedError('PolynomialSpace does not support derivatives')
|
|
41
|
+
if self.num_parametric_dimensions == 1:
|
|
42
|
+
weights = poly.polyvander(parametric_coordinates, self.order)
|
|
43
|
+
elif self.num_parametric_dimensions == 2:
|
|
44
|
+
if len(parametric_coordinates.shape) == 1:
|
|
45
|
+
parametric_coordinates = parametric_coordinates.reshape(-1, 2)
|
|
46
|
+
weights = poly.polyvander2d(parametric_coordinates[:, 0], parametric_coordinates[:, 1], self.order)
|
|
47
|
+
elif self.num_parametric_dimensions == 3:
|
|
48
|
+
weights = poly.polyvander3d(parametric_coordinates[:, 0], parametric_coordinates[:, 1], parametric_coordinates[:, 2], self.order)
|
|
49
|
+
else:
|
|
50
|
+
raise NotImplementedError('PolynomialSpace only supports up to 3 dimensions')
|
|
51
|
+
|
|
52
|
+
return weights
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# def test_polynomial_space():
|
|
57
|
+
# num_parametric_dimensions = 2
|
|
58
|
+
# order = 2
|
|
59
|
+
# space = PolynomialSpace(num_parametric_dimensions=num_parametric_dimensions, order=order)
|
|
60
|
+
# parametric_coordinates = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
|
|
61
|
+
# basis_matrix = space.compute_basis_matrix(parametric_coordinates)
|
|
62
|
+
# assert basis_matrix.shape == (3, 9)
|
|
63
|
+
# assert np.allclose(basis_matrix, [[1., 0.1, 0.01, 0.2, 0.02, 0.01, 0.04, 0.008, 0.004],
|
|
64
|
+
# [1., 0.3, 0.09, 0.4, 0.12, 0.04, 0.16, 0.048, 0.016],
|
|
65
|
+
# [1., 0.5, 0.25, 0.6, 0.3, 0.15, 0.36, 0.18, 0.09]])
|
|
66
|
+
|
|
67
|
+
# order = (2, 3)
|
|
68
|
+
# space = PolynomialSpace(num_parametric_dimensions=num_parametric_dimensions, order=order)
|
|
69
|
+
# parametric_coordinates = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
|
|
70
|
+
# basis_matrix = space.compute_basis_matrix(parametric_coordinates)
|
|
71
|
+
# assert basis_matrix.shape == (3, 18)
|
|
72
|
+
# assert np.allclose(basis_matrix, [[1., 0.1, 0.01, 0.2, 0.02, 0.01, 0.04, 0.008, 0.004, 0.008, 0.0016, 0.0008, 0.016, 0.0032, 0.0016, 0.032, 0.0064, 0.0032],
|
|
73
|
+
# [1., 0.3, 0.09, 0.4, 0.12, 0.04, 0.16, 0.048, 0.016, 0.024, 0.0072, 0.0024, 0.064, 0.0192, 0, 0.128, 0.0384, 0.0128],
|
|
74
|
+
# [1., 0.5, 0.25, 0.6, 0.3, 0.15, 0.36, 0.18, 0.09, 0.04, 0.02, 0.01, 0.1, 0.05, 0.025, 0.2, 0.1, 0.05]])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# if __name__ == '__main__':
|
|
78
|
+
# test_polynomial_space()
|
|
79
|
+
# print('PolynomialSpace tests passed.')
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.sparse as sps
|
|
3
|
+
from ..function_space import LinearFunctionSpace
|
|
4
|
+
from scipy.spatial.distance import cdist
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Union
|
|
7
|
+
import csdl_alpha as csdl
|
|
8
|
+
|
|
9
|
+
class RBFFunctionSpace(LinearFunctionSpace):
|
|
10
|
+
"""
|
|
11
|
+
Radial Basis Function (RBF) Function Space.
|
|
12
|
+
|
|
13
|
+
This function space evaluates basis functions centered at support points
|
|
14
|
+
using radial basis kernels such as Gaussian, Polyharmonic, and Multiquadrics.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
num_parametric_dimensions : int
|
|
19
|
+
The number of parametric dimensions.
|
|
20
|
+
radial_function : str, optional
|
|
21
|
+
The type of radial basis function kernel ('gaussian', 'polyharmonic_spline',
|
|
22
|
+
'inverse_quadratic', 'inverse_multiquadric', 'bump'). Default is 'gaussian'.
|
|
23
|
+
points : np.ndarray, optional
|
|
24
|
+
Explicit support center points. If None, a uniform grid is generated based on grid_size.
|
|
25
|
+
grid_size : Union[int, tuple], optional
|
|
26
|
+
The size of the center point grid in each dimension. Default is 10.
|
|
27
|
+
epsilon : float, optional
|
|
28
|
+
Shape parameter for Gaussian, inverse quadratic, and multiquadric kernels. Default is 1.
|
|
29
|
+
k : int, optional
|
|
30
|
+
Power parameter for polyharmonic splines. Default is 2.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, num_parametric_dimensions:int, radial_function:str='gaussian', points:np.ndarray=None, grid_size:Union[int, tuple]=10, epsilon:float=1, k:int=2):
|
|
34
|
+
"""
|
|
35
|
+
Initialize an RBF function space.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
self.grid_size = grid_size
|
|
39
|
+
self.points = points
|
|
40
|
+
self.radial_function = radial_function
|
|
41
|
+
self.epsilon = epsilon
|
|
42
|
+
self.k = k
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if self.points is None:
|
|
46
|
+
if isinstance(self.grid_size, int):
|
|
47
|
+
self.grid_size = (self.grid_size,)*num_parametric_dimensions
|
|
48
|
+
linspaces = [np.linspace(0, 1, n) for n in self.grid_size]
|
|
49
|
+
self.points = np.array(np.meshgrid(*linspaces)).T.reshape(-1, num_parametric_dimensions)
|
|
50
|
+
|
|
51
|
+
super().__init__(num_parametric_dimensions, self.points.shape[0])
|
|
52
|
+
|
|
53
|
+
def compute_basis_matrix(self, parametric_coordinates:np.ndarray, parametric_derivative_orders: np.ndarray=None, expansion_factor:int=None) -> np.ndarray:
|
|
54
|
+
"""
|
|
55
|
+
Compute the basis matrix for the given parametric coordinates.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
parametric_coordinates : np.ndarray
|
|
60
|
+
The parametric coordinates for which to compute the basis matrix.
|
|
61
|
+
parametric_derivative_orders : np.ndarray, optional
|
|
62
|
+
The derivative orders of the parametric coordinates. Default is None.
|
|
63
|
+
expansion_factor : int, optional
|
|
64
|
+
The expansion factor. Default is None.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
np.ndarray
|
|
69
|
+
The computed basis matrix.
|
|
70
|
+
|
|
71
|
+
Raises
|
|
72
|
+
------
|
|
73
|
+
NotImplementedError
|
|
74
|
+
If parametric_derivative_orders or expansion_factor is not None.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
# if parametric_derivative_orders is not None:
|
|
78
|
+
# raise NotImplementedError('IDWFunctionSpace does not support derivatives')
|
|
79
|
+
# if expansion_factor is not None:
|
|
80
|
+
# raise NotImplementedError('IDWFunctionSpace does not support expansion factors')
|
|
81
|
+
|
|
82
|
+
if len(parametric_coordinates.shape) == 1:
|
|
83
|
+
parametric_coordinates = parametric_coordinates.reshape(1, -1)
|
|
84
|
+
|
|
85
|
+
dist = cdist(parametric_coordinates, self.points, 'euclidean')
|
|
86
|
+
if not hasattr(self, f'_{self.radial_function}'):
|
|
87
|
+
raise ValueError(f"Radial function '{self.radial_function}' is not supported.")
|
|
88
|
+
phi = getattr(self, f'_{self.radial_function}')(dist)
|
|
89
|
+
|
|
90
|
+
# sum the basis functions so the total influence per evaluation point is 1
|
|
91
|
+
phi = phi / np.sum(phi, axis=1, keepdims=True)
|
|
92
|
+
|
|
93
|
+
return phi
|
|
94
|
+
|
|
95
|
+
def _gaussian(self, x):
|
|
96
|
+
return np.exp(-(self.epsilon*x)**2)
|
|
97
|
+
|
|
98
|
+
def _polyharmonic_spline(self, x):
|
|
99
|
+
if self.k % 2 == 0:
|
|
100
|
+
return x**(self.k-1) * np.log(x**x)
|
|
101
|
+
else:
|
|
102
|
+
return x**self.k
|
|
103
|
+
|
|
104
|
+
def _inverse_quadratic(self, x):
|
|
105
|
+
return 1/(1 + (self.epsilon*x)**2)
|
|
106
|
+
|
|
107
|
+
def _inverse_multiquadric(self, x):
|
|
108
|
+
return 1/np.sqrt(1 + (self.epsilon*x)**2)
|
|
109
|
+
|
|
110
|
+
def _bump(self, x):
|
|
111
|
+
return np.piecewise(x, [x < 1/self.epsilon], [lambda x: np.exp(1/((self.epsilon*x)**2-1)), 0])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_rbf_space():
|
|
117
|
+
import numpy as np
|
|
118
|
+
import csdl_alpha as csdl
|
|
119
|
+
|
|
120
|
+
rec = csdl.Recorder(inline=True)
|
|
121
|
+
rec.start()
|
|
122
|
+
|
|
123
|
+
space = RBFFunctionSpace(num_parametric_dimensions=2,
|
|
124
|
+
radial_function='bump',
|
|
125
|
+
grid_size=20)
|
|
126
|
+
parametric_coordinates = np.random.rand(10, 2)
|
|
127
|
+
data = 10*np.random.rand(10, 1)
|
|
128
|
+
function = space.fit_function(data, parametric_coordinates)
|
|
129
|
+
eval_data = function.evaluate(parametric_coordinates)
|
|
130
|
+
|
|
131
|
+
print(eval_data.value - data)
|
|
132
|
+
|
|
133
|
+
# print(function.coefficients.value)
|
|
134
|
+
|
|
135
|
+
if __name__ == '__main__':
|
|
136
|
+
test_rbf_space()
|