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,183 @@
1
+ import csdl_alpha as csdl
2
+ import numpy as np
3
+
4
+ from typing import Union
5
+ from dataclasses import dataclass
6
+
7
+ @dataclass
8
+ class Optimization:
9
+ objective:csdl.Variable=None
10
+ design_variables:list[csdl.Variable]=None
11
+ constraints:list[csdl.Variable]=None
12
+ constraint_penalties:list[Union[float,np.ndarray]]=None
13
+ state_residual_pairs:list[tuple[csdl.Variable,csdl.Variable]]=None
14
+
15
+ def __post_init__(self):
16
+ if self.design_variables is None:
17
+ self.design_variables = []
18
+ if self.constraints is None:
19
+ self.constraints = []
20
+ if self.constraint_penalties is None:
21
+ self.constraint_penalties = []
22
+ if self.state_residual_pairs is None:
23
+ self.state_residual_pairs = []
24
+
25
+ self.design_variable_initial_values = []
26
+
27
+
28
+ def add_objective(self, objective:csdl.Variable):
29
+ '''
30
+ Add the objective variable to the optimization problem.
31
+ '''
32
+ self.objective = objective
33
+
34
+
35
+ def add_design_variable(self, design_variable:csdl.Variable, initial_value:Union[csdl.Variable,float,np.ndarray]=None):
36
+ '''
37
+ Add a design variable to the optimization problem.
38
+ '''
39
+ self.design_variables.append(design_variable)
40
+ self.design_variable_initial_values.append(initial_value)
41
+ if initial_value is not None:
42
+ if isinstance(initial_value, csdl.Variable):
43
+ design_variable.set_value(initial_value.value)
44
+ else:
45
+ design_variable.set_value(initial_value)
46
+
47
+
48
+ def add_constraint(self, constraint:csdl.Variable, penalty:Union[float,np.ndarray,csdl.Variable]=None):
49
+ '''
50
+ Add a constraint to the optimization problem.
51
+
52
+ Parameters
53
+ ----------
54
+ constraint : csdl.Variable
55
+ The constraint to be added.
56
+ penalty : Union[float,np.ndarray,csdl.Variable], optional
57
+ The penalty for the constraint, by default None. If None, the penalty is treated as a lagrange multiplier.
58
+ If a penalty is given, it is used as the penalty scaling factor for a quadratic constraint penalty.
59
+ '''
60
+ self.constraints.append(constraint)
61
+ self.constraint_penalties.append(penalty)
62
+
63
+
64
+ def compute_lagrangian(self):
65
+ '''
66
+ Constructs the CSDL variable for the lagrangian of the optimization problem.
67
+ '''
68
+ self.lagrange_multipliers = []
69
+ lagrangian = self.objective
70
+ for constraint, penalty in zip(self.constraints, self.constraint_penalties):
71
+ if penalty is not None:
72
+ lagrangian += penalty*constraint
73
+ self.lagrange_multipliers.append(None)
74
+ else:
75
+ constraint_lagrange_multipliers = csdl.Variable(shape=(constraint.size,), value=0.,
76
+ name=f'{constraint.name}_lagrange_multipliers')
77
+ self.lagrange_multipliers.append(constraint_lagrange_multipliers)
78
+ lagrangian = lagrangian + csdl.vdot(constraint_lagrange_multipliers, constraint)
79
+ self.lagrangian = lagrangian
80
+ return lagrangian
81
+
82
+
83
+ def compute_objective_gradient(self, objective:csdl.Variable=None, design_variables:list[csdl.Variable]=None):
84
+ '''
85
+ Constructs the CSDL variable for the objective gradient wrt each design variable.
86
+ '''
87
+ if objective is None:
88
+ objective = self.objective
89
+ if design_variables is None:
90
+ design_variables = self.design_variables
91
+
92
+ df_dx = csdl.derivative(self.objective, self.design_variables)
93
+ self.df_dx = df_dx
94
+ return df_dx
95
+
96
+
97
+ def compute_lagrangian_gradient(self, lagrangian:csdl.Variable=None, design_variables:list[csdl.Variable]=None):
98
+ '''
99
+ Constructs the CSDL variable for the lagrangian gradient wrt each design variable.
100
+ '''
101
+ if lagrangian is None:
102
+ lagrangian = self.lagrangian
103
+ if design_variables is None:
104
+ design_variables = self.design_variables
105
+
106
+ dL_dx = csdl.derivative(self.lagrangian, self.design_variables, loop=False)
107
+ self.dL_dx = dL_dx
108
+ return dL_dx
109
+
110
+
111
+ def compute_constraint_jacobian(self, constraints:list[csdl.Variable]=None, design_variables:list[csdl.Variable]=None):
112
+ '''
113
+ Constructs the CSDL variables for the jacobian of each constraint wrt each design variable
114
+ '''
115
+ if constraints is None:
116
+ constraints = self.constraints
117
+ if design_variables is None:
118
+ design_variables = self.design_variables
119
+
120
+ dc_dx = csdl.derivative(self.constraints, self.design_variables)
121
+ self.dc_dx = dc_dx
122
+ return dc_dx
123
+
124
+
125
+ def setup(self):
126
+ '''
127
+ Sets up the optimization problem as an implicit model to drive the gradient to 0.
128
+ '''
129
+ if self.objective is not None:
130
+ lagrangian = self.compute_lagrangian()
131
+ dL_dx = self.compute_lagrangian_gradient(lagrangian=lagrangian, design_variables=self.design_variables)
132
+
133
+ for constraint, constraint_lagrange_multipliers in zip(self.constraints, self.lagrange_multipliers):
134
+ if constraint_lagrange_multipliers is not None:
135
+ self.state_residual_pairs.append((constraint_lagrange_multipliers, constraint))
136
+
137
+ for i, design_variable in enumerate(self.design_variables):
138
+ residual = dL_dx[design_variable].reshape((design_variable.size,))
139
+ residual.add_name(f'{design_variable.name}_residual')
140
+ if self.design_variable_initial_values[i] is not None:
141
+ self.state_residual_pairs.append(((design_variable, self.design_variable_initial_values[i]), residual))
142
+ else:
143
+ self.state_residual_pairs.append((design_variable, residual))
144
+
145
+
146
+ class NewtonOptimizer:
147
+ '''
148
+ A Newton Optimizer class.
149
+
150
+ NOTE: This is a temporary implementation until integration with the CSDL solvers is done
151
+ (the CSDL solvers need the add_optimization functionality)
152
+ '''
153
+ def __init__(self) -> None:
154
+ # self.solver = csdl.nonlinear_solvers.Newton()
155
+ self.solver = csdl.nonlinear_solvers.Newton(residual_jac_kwargs={"loop": True, "concatenate_ofs": True})
156
+ self.has_been_setup = False
157
+
158
+ def add_optimization(self, optimization:Optimization):
159
+ '''
160
+ Add an optimization problem to the optimizer.
161
+ '''
162
+ self.optimization = optimization
163
+
164
+ def setup(self):
165
+ self.optimization.setup()
166
+ for state, residual in self.optimization.state_residual_pairs:
167
+ if isinstance(state, tuple):
168
+ self.solver.add_state(state[0], residual, initial_value=state[1])
169
+ else:
170
+ if state.shape != residual.shape:
171
+ residual = residual.reshape(state.shape)
172
+ self.solver.add_state(state, residual)
173
+ self.has_been_setup = True
174
+
175
+ def run(self):
176
+ '''
177
+ Runs the Newton Optimization.
178
+
179
+ NOTE: CSDL state/design variables are already updated and therefore are not needed to be returned.
180
+ '''
181
+ if not self.has_been_setup:
182
+ self.setup()
183
+ self.solver.run()
File without changes
@@ -0,0 +1,418 @@
1
+ """B-spline function space representation and operations (pure Python)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from typing import Optional, Sequence, Tuple, Union
7
+
8
+ import csdl_alpha as csdl
9
+ import jax
10
+ import jax.numpy as jnp
11
+ import numpy as np
12
+ import numpy.typing as npt
13
+ import scipy.sparse as sps
14
+ from scipy.spatial import cKDTree
15
+
16
+ import lsdo_function_spaces as lfs
17
+ from lsdo_function_spaces.core.function_space import LinearFunctionSpace
18
+ from lsdo_function_spaces.core.spaces.non_cython_bsplines.b_spline_csdl_custom_ops import (
19
+ BasisMatrixCustomOp,
20
+ BSplineEvalCustomOp,
21
+ )
22
+ from lsdo_function_spaces.core.spaces.non_cython_bsplines.b_spline_patch_projection import (
23
+ compute_point_to_bspline_projection,
24
+ )
25
+ from lsdo_function_spaces.core.spaces.non_cython_bsplines.compute_basis_matrix_numpy import (
26
+ compute_basis_matrix_numpy,
27
+ )
28
+
29
+
30
+ class BSplineSpace(LinearFunctionSpace):
31
+ """B-spline Function Space for curves, surfaces, and trivariate volumes.
32
+
33
+ Inherits from :class:`LinearFunctionSpace`. Pure Python implementation
34
+ accelerated with NumPy, SciPy, and JAX (no Cython required).
35
+
36
+ Parameters
37
+ ----------
38
+ num_parametric_dimensions : int
39
+ The number of parametric dimensions (1 for curve, 2 for surface, 3 for volume).
40
+ degree : Union[int, Tuple[int, ...]]
41
+ Polynomial degree of the B-spline basis in each parametric dimension.
42
+ coefficients_shape : Tuple[int, ...]
43
+ Shape of control points / coefficients in each parametric dimension.
44
+ knots : Optional[Union[Tuple[np.ndarray, ...], np.ndarray]], optional
45
+ Knot vectors for each parametric dimension. If None, open uniform knot
46
+ vectors on [0, 1] are automatically generated.
47
+ knot_indices : Optional[List[np.ndarray]], optional
48
+ Indices of knots per dimension (maintained for backwards compatibility).
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ num_parametric_dimensions: int,
54
+ degree: Union[int, Tuple[int, ...]],
55
+ coefficients_shape: Tuple[int, ...],
56
+ knots: Optional[Union[Tuple[np.ndarray, ...], np.ndarray]] = None,
57
+ knot_indices: Optional[Sequence[np.ndarray]] = None,
58
+ ):
59
+ self.degree = degree
60
+ self.knots = knots
61
+ self.knot_indices = list(knot_indices) if knot_indices is not None else None
62
+ super().__init__(num_parametric_dimensions, coefficients_shape)
63
+
64
+ if isinstance(self.degree, int):
65
+ self.degree = (self.degree,) * self.num_parametric_dimensions
66
+
67
+ for i in range(self.num_parametric_dimensions):
68
+ if self.degree[i] < 0:
69
+ raise ValueError(f"Degree in axis {i} must be non-negative.")
70
+ if self.degree[i] >= self.coefficients_shape[i]:
71
+ raise ValueError(
72
+ f"Degree in axis {i} must be less than the number of coefficients in each dimension."
73
+ )
74
+
75
+ # Handle 1D concatenated knot vectors for backward compatibility
76
+ if self.knots is not None and isinstance(self.knots, np.ndarray) and self.knots.ndim == 1:
77
+ split_knots = []
78
+ idx = 0
79
+ for i in range(self.num_parametric_dimensions):
80
+ n_knots = self.coefficients_shape[i] + self.degree[i] + 1
81
+ split_knots.append(self.knots[idx : idx + n_knots])
82
+ idx += n_knots
83
+ self.knots = tuple(split_knots)
84
+ elif self.knots is not None and isinstance(self.knots, (list, tuple)):
85
+ self.knots = tuple(np.asarray(k, dtype=float) for k in self.knots)
86
+
87
+ if self.knots is None:
88
+ # Create open uniform knot vectors on [0, 1] for each dimension
89
+ self.knots = tuple(
90
+ np.concatenate([
91
+ np.zeros(self.degree[i]),
92
+ np.linspace(0, 1, self.coefficients_shape[i] - self.degree[i] + 1),
93
+ np.ones(self.degree[i]),
94
+ ])
95
+ for i in range(self.num_parametric_dimensions)
96
+ )
97
+
98
+ if self.knot_indices is None:
99
+ self.knot_indices = []
100
+ knot_index = 0
101
+ for i in range(self.num_parametric_dimensions):
102
+ num_knots_i = self.coefficients_shape[i] + self.degree[i] + 1
103
+ self.knot_indices.append(np.arange(knot_index, knot_index + num_knots_i))
104
+ knot_index += num_knots_i
105
+
106
+ def _evaluate(
107
+ self,
108
+ coefficients: Union[np.ndarray, csdl.Variable],
109
+ parametric_coordinates: Union[np.ndarray, csdl.Variable],
110
+ parametric_derivative_orders: Optional[Tuple[int, ...]] = None,
111
+ ) -> Union[np.ndarray, csdl.Variable]:
112
+ """Evaluate B-spline functions at the given parametric coordinates."""
113
+ if not isinstance(coefficients, (np.ndarray, csdl.Variable)):
114
+ raise TypeError(
115
+ f"coefficients must be a numpy array or a CSDL variable, "
116
+ f"but got type {type(coefficients)}."
117
+ )
118
+
119
+ if not isinstance(parametric_coordinates, (np.ndarray, csdl.Variable)):
120
+ raise TypeError(
121
+ f"parametric_coordinates must be a numpy array or a CSDL variable, "
122
+ f"but got type {type(parametric_coordinates)}."
123
+ )
124
+
125
+ try:
126
+ parametric_coordinates = parametric_coordinates.reshape(-1, self.num_parametric_dimensions)
127
+ except ValueError:
128
+ raise ValueError(
129
+ f"parametric_coordinates must have shape (num_points, {self.num_parametric_dimensions}), "
130
+ f"but got shape {parametric_coordinates.shape}."
131
+ )
132
+
133
+ non_csdl = isinstance(coefficients, np.ndarray)
134
+
135
+ if isinstance(parametric_coordinates, np.ndarray):
136
+ basis_matrix = compute_basis_matrix_numpy(
137
+ us=parametric_coordinates,
138
+ degrees=self.degree,
139
+ knot_vectors=self.knots,
140
+ der_orders=parametric_derivative_orders,
141
+ )
142
+ if coefficients.shape != (basis_matrix.shape[1], coefficients.size // basis_matrix.shape[1]):
143
+ coefficients = coefficients.reshape(
144
+ (basis_matrix.shape[1], coefficients.size // basis_matrix.shape[1])
145
+ )
146
+
147
+ if non_csdl:
148
+ values = basis_matrix @ coefficients
149
+ if values.shape[0] == 1:
150
+ values = values.flatten()
151
+ else:
152
+ values = csdl.Variable(value=np.zeros((basis_matrix.shape[0], coefficients.shape[1])))
153
+ for i in csdl.frange(coefficients.shape[1]):
154
+ coefficients_column = coefficients[:, i].reshape((coefficients.shape[0], 1))
155
+ values = values.set(
156
+ csdl.slice[:, i],
157
+ csdl.sparse.matvec(basis_matrix, coefficients_column).reshape(
158
+ (basis_matrix.shape[0],)
159
+ ),
160
+ )
161
+ values = values.reshape((parametric_coordinates.shape[0], coefficients.shape[-1]))
162
+
163
+ return values
164
+
165
+ else:
166
+ b_spline_eval_op = BSplineEvalCustomOp(
167
+ knots=self.knots,
168
+ degree=self.degree,
169
+ coefficients_shape=self.coefficients_shape,
170
+ der_orders=parametric_derivative_orders,
171
+ )
172
+
173
+ values = b_spline_eval_op.evaluate(
174
+ parametric_coordinates=parametric_coordinates,
175
+ coefficients=coefficients,
176
+ )
177
+
178
+ if non_csdl:
179
+ values = values.value
180
+
181
+ return values
182
+
183
+ def _generate_parametric_grid(self, knot_vectors: Sequence[np.ndarray], N: int) -> np.ndarray:
184
+ """Generate a tensor-grid of parametric sample points."""
185
+ samples_1d = []
186
+ for U in knot_vectors:
187
+ knots = np.unique(U)
188
+ pts = []
189
+ for j in range(len(knots) - 1):
190
+ a, b = knots[j], knots[j + 1]
191
+ pts.append(np.linspace(a, b, N, endpoint=False))
192
+ pts.append(np.array([knots[-1]]))
193
+ samples_1d.append(np.concatenate(pts))
194
+
195
+ mesh = np.meshgrid(*samples_1d, indexing="ij")
196
+ coord_arrays = [m.flatten() for m in mesh]
197
+ grid = np.stack(coord_arrays, axis=-1)
198
+ return grid
199
+
200
+ def _project(
201
+ self,
202
+ points_in_space: Union[np.ndarray, csdl.Variable],
203
+ coefficients: Union[np.ndarray, csdl.Variable],
204
+ plot: bool = False,
205
+ grid_search_density: int = 100,
206
+ ) -> np.ndarray:
207
+ """Project points in physical space onto the B-spline entity."""
208
+ if isinstance(coefficients, csdl.Variable):
209
+ coefficients = coefficients.value
210
+
211
+ if not isinstance(points_in_space, (np.ndarray, csdl.Variable)):
212
+ raise TypeError(
213
+ f"points_in_space must be a numpy array or a CSDL variable, "
214
+ f"but got type {type(points_in_space)}."
215
+ )
216
+ if isinstance(points_in_space, csdl.Variable):
217
+ raise NotImplementedError(
218
+ "Projection of CSDL variables is not implemented yet. "
219
+ "Please provide a numpy array of points in space."
220
+ )
221
+ fun = lfs.Function(
222
+ space=self,
223
+ coefficients=coefficients,
224
+ )
225
+
226
+ para_grid = self._generate_parametric_grid(
227
+ knot_vectors=self.knots,
228
+ N=grid_search_density,
229
+ )
230
+
231
+ basis_mat = compute_basis_matrix_numpy(
232
+ us=para_grid,
233
+ degrees=self.degree,
234
+ knot_vectors=self.knots,
235
+ )
236
+
237
+ surface_grid = basis_mat @ coefficients.reshape(-1, coefficients.shape[-1])
238
+
239
+ kd_tree = cKDTree(surface_grid)
240
+ nearest_index = kd_tree.query(points_in_space, k=1)[1]
241
+ nearest_para_points = para_grid[nearest_index]
242
+
243
+ batched_projection = jax.jit(
244
+ jax.vmap(
245
+ lambda pt, u0, cps: compute_point_to_bspline_projection(
246
+ point=pt,
247
+ degrees=self.degree,
248
+ coefficients=cps,
249
+ para_coords=u0,
250
+ knots=tuple([jnp.array(kv_i) for kv_i in self.knots]),
251
+ ),
252
+ in_axes=(0, 0, None),
253
+ )
254
+ )
255
+
256
+ para, res, converged, final_i, J, _, _ = batched_projection(
257
+ points_in_space,
258
+ nearest_para_points,
259
+ coefficients,
260
+ )
261
+ para = np.array(para).reshape(-1, self.num_parametric_dimensions)
262
+
263
+ if not converged.all():
264
+ warnings.warn(
265
+ f"{np.sum(~converged)} out of {len(converged)} projection points did not fully converge.",
266
+ UserWarning,
267
+ stacklevel=2,
268
+ )
269
+
270
+ if plot:
271
+ point_cloud = lfs.plot_points(
272
+ points=points_in_space,
273
+ color="#00FF1A",
274
+ opacity=0.5,
275
+ size=8,
276
+ show=False,
277
+ )
278
+
279
+ projected_points = fun.evaluate(
280
+ parametric_coordinates=para,
281
+ ).value
282
+ project_point_cloud = lfs.plot_points(
283
+ points=projected_points,
284
+ color="#FF0000",
285
+ size=4,
286
+ show=False,
287
+ )
288
+
289
+ fun.plot(additional_plotting_elements=[point_cloud, project_point_cloud])
290
+
291
+ return para
292
+
293
+ def compute_basis_matrix(
294
+ self,
295
+ parametric_coordinates: Union[np.ndarray, csdl.Variable],
296
+ parametric_derivative_orders: Optional[Tuple[int, ...]] = None,
297
+ expansion_factor: Optional[int] = None,
298
+ ) -> Union[sps.coo_matrix, csdl.Variable]:
299
+ """Compute the B-spline basis matrix for given parametric coordinates."""
300
+ if isinstance(parametric_coordinates, csdl.Variable):
301
+ basis_mat_custom_op = BasisMatrixCustomOp(
302
+ knots=self.knots,
303
+ degree=self.degree,
304
+ coefficients_shape=self.coefficients_shape,
305
+ der_orders=parametric_derivative_orders,
306
+ )
307
+ try:
308
+ parametric_coordinates = parametric_coordinates.reshape(-1, self.num_parametric_dimensions)
309
+ except ValueError:
310
+ raise ValueError(
311
+ f"parametric_coordinates must have shape (num_points, {self.num_parametric_dimensions}), "
312
+ f"but got shape {parametric_coordinates.shape}."
313
+ )
314
+ return basis_mat_custom_op.evaluate(parametric_coordinates)
315
+
316
+ elif isinstance(parametric_coordinates, np.ndarray):
317
+ try:
318
+ parametric_coordinates = parametric_coordinates.reshape(-1, self.num_parametric_dimensions)
319
+ except ValueError:
320
+ raise ValueError(
321
+ f"parametric_coordinates must have shape (num_points, {self.num_parametric_dimensions}), "
322
+ f"but got shape {parametric_coordinates.shape}."
323
+ )
324
+
325
+ res = compute_basis_matrix_numpy(
326
+ us=parametric_coordinates,
327
+ degrees=self.degree,
328
+ knot_vectors=self.knots,
329
+ der_orders=parametric_derivative_orders,
330
+ )
331
+ if expansion_factor is not None and expansion_factor > 1:
332
+ res = sps.kron(res, sps.eye(expansion_factor), format='csr')
333
+ return res
334
+
335
+ else:
336
+ raise TypeError(
337
+ f"parametric_coordinates must be a numpy array or a CSDL variable, "
338
+ f"but got type {type(parametric_coordinates)}."
339
+ )
340
+
341
+ def _compute_distance_bounds(
342
+ self, point: np.ndarray, function: lfs.Function, direction: Optional[np.ndarray] = None
343
+ ) -> float:
344
+ """Compute distance bounds for a given point relative to function bounding box."""
345
+ if not hasattr(function, "bounding_box"):
346
+ coefficients = function.coefficients.value.reshape((-1, function.num_physical_dimensions))
347
+ function.bounding_box = np.zeros((2, coefficients.shape[-1]))
348
+ if self.num_parametric_dimensions == 1:
349
+ function.bounding_box[0, 0] = np.min(coefficients)
350
+ function.bounding_box[1, 0] = np.max(coefficients)
351
+ else:
352
+ function.bounding_box[0, :] = np.min(coefficients, axis=0)
353
+ function.bounding_box[1, :] = np.max(coefficients, axis=0)
354
+
355
+ if direction is None:
356
+ neg = function.bounding_box[0] - point
357
+ pos = point - function.bounding_box[1]
358
+ distance_vector = np.maximum(np.maximum(neg, pos), 0)
359
+ return float(np.linalg.norm(distance_vector))
360
+ else:
361
+ closest_point = np.zeros((len(point),))
362
+ for i in range(len(point)):
363
+ if point[i] < function.bounding_box[0, i]:
364
+ closest_point[i] = function.bounding_box[0, i]
365
+ elif point[i] > function.bounding_box[1, i]:
366
+ closest_point[i] = function.bounding_box[1, i]
367
+ else:
368
+ closest_point[i] = point[i]
369
+ t = np.dot(direction, (closest_point - point)) / np.dot(direction, direction)
370
+ closest_point_on_line = point + t * direction
371
+ return float(np.linalg.norm(closest_point_on_line - closest_point))
372
+
373
+ def stitch(self, self_face: int, self_coeffs: np.ndarray, other: BSplineSpace, other_face: int, other_coeffs: np.ndarray):
374
+ """Stitch two B-spline function spaces along adjacent faces."""
375
+ ind_array = np.arange(np.prod(self.coefficients_shape)).reshape(self.coefficients_shape)
376
+
377
+ if len(self_coeffs.shape) > 2:
378
+ self_coeffs = self_coeffs.reshape((-1, self_coeffs.shape[-1]))
379
+ if len(other_coeffs.shape) > 2:
380
+ other_coeffs = other_coeffs.reshape((-1, other_coeffs.shape[-1]))
381
+
382
+ if self_face == 1:
383
+ self_inds = ind_array[:, 0]
384
+ elif self_face == 2:
385
+ self_inds = ind_array[-1, :]
386
+ elif self_face == 3:
387
+ self_inds = ind_array[:, -1]
388
+ elif self_face == 4:
389
+ self_inds = ind_array[0, :]
390
+ self_inds = [int(ind) for ind in self_inds]
391
+
392
+ if other_face == 1:
393
+ other_inds = ind_array[:, 0]
394
+ elif other_face == 2:
395
+ other_inds = ind_array[-1, :]
396
+ elif other_face == 3:
397
+ other_inds = ind_array[:, -1]
398
+ elif other_face == 4:
399
+ other_inds = ind_array[0, :]
400
+ other_inds = [int(ind) for ind in other_inds]
401
+
402
+ return self_inds, other_inds
403
+
404
+
405
+ class BSplineSpaceNew(BSplineSpace):
406
+ """Deprecated alias for :class:`BSplineSpace`.
407
+
408
+ .. deprecated:: 1.0.0
409
+ Use :class:`BSplineSpace` instead.
410
+ """
411
+
412
+ def __init__(self, *args, **kwargs):
413
+ warnings.warn(
414
+ "BSplineSpaceNew is deprecated; use BSplineSpace instead.",
415
+ DeprecationWarning,
416
+ stacklevel=2,
417
+ )
418
+ super().__init__(*args, **kwargs)
@@ -0,0 +1,65 @@
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
+
8
+ class ConditionalSpace(LinearFunctionSpace):
9
+ """
10
+ Conditional Function Space.
11
+
12
+ This function space applies a user-defined condition predicate to
13
+ evaluate basis representations conditionally in parametric space.
14
+
15
+ Parameters
16
+ ----------
17
+ num_parametric_dimensions : int
18
+ The number of parametric dimensions.
19
+ condition : callable
20
+ Condition callable evaluated on coordinates.
21
+ """
22
+
23
+ def __init__(self, num_parametric_dimensions:int, condition:callable):
24
+ super().__init__(num_parametric_dimensions, (1,))
25
+ self.condition = condition
26
+
27
+
28
+ def compute_basis_matrix(self, parametric_coordinates:np.ndarray, parametric_derivative_orders: np.ndarray=None, expansion_factor:int=None) -> np.ndarray:
29
+ """
30
+ Compute the basis matrix for the given parametric coordinates.
31
+
32
+ Parameters
33
+ ----------
34
+ parametric_coordinates : np.ndarray
35
+ The parametric coordinates for which to compute the basis matrix.
36
+ parametric_derivative_orders : np.ndarray, optional
37
+ The derivative orders of the parametric coordinates. Default is None.
38
+ expansion_factor : int, optional
39
+ The expansion factor. Default is None.
40
+
41
+ Returns
42
+ -------
43
+ np.ndarray
44
+ The computed basis matrix.
45
+
46
+ Raises
47
+ ------
48
+ NotImplementedError
49
+ If parametric_derivative_orders or expansion_factor is not None.
50
+
51
+ """
52
+ if parametric_derivative_orders is not None:
53
+ raise NotImplementedError('IDWFunctionSpace does not support derivatives')
54
+ if expansion_factor is not None:
55
+ raise NotImplementedError('IDWFunctionSpace does not support expansion factors')
56
+
57
+ if len(parametric_coordinates.shape) == 1:
58
+ parametric_coordinates = parametric_coordinates.reshape(1, -1)
59
+
60
+ weights = self.condition(parametric_coordinates).astype(int)
61
+
62
+ if len(weights.shape) == 1:
63
+ weights = weights.reshape(-1, 1)
64
+
65
+ return weights