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,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()