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,1081 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ import csdl_alpha as csdl
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ import scipy.sparse as sps
7
+ import concurrent.futures
8
+ import itertools
9
+ import pickle
10
+ from pathlib import Path
11
+ import string
12
+ import random
13
+ import pyvista as pv
14
+ from typing import Optional, Union, Sequence
15
+
16
+ # from lsdo_function_spaces.core.function_space import FunctionSpace
17
+ import lsdo_function_spaces as lfs
18
+ from lsdo_function_spaces.utils.internal_utilities import get_projection_squared_distances
19
+
20
+ def find_best_surface_chunked(chunk, functions:dict[lfs.Function]=None, options=None):
21
+ # Batch-project all points on each function and choose best per-point by argmin across functions.
22
+ if functions is None:
23
+ functions = global_functions
24
+ if options is None:
25
+ options = global_options
26
+
27
+ priority_inds = options.get('priority_inds', [])
28
+ priority_eps = options.get('priority_eps', 0.0)
29
+ direction_opt = options.get('direction', None)
30
+ direction = direction_opt/np.linalg.norm(direction_opt) if direction_opt is not None else None
31
+ extrema = options.get('extrema', False)
32
+ projection_tolerance = options.get('projection_tolerance', None)
33
+ grid_search_evaluation_cutoff = options.get('grid_search_evaluation_cutoff', None)
34
+ grid_search_subtraction_cutoff = options.get('grid_search_subtraction_cutoff', None)
35
+
36
+ points = np.array(chunk)
37
+ if points.ndim == 1:
38
+ points = points.reshape(1, -1)
39
+
40
+ num_points = points.shape[0]
41
+ func_keys = list(functions.keys())
42
+ num_funcs = len(func_keys)
43
+
44
+ if extrema:
45
+ n = list(functions.values())[0].space.num_parametric_dimensions
46
+ extrema_parametric = np.array(list(itertools.product([0., 1.], repeat=n)))
47
+ func_extrema = {i: function.evaluate(extrema_parametric, non_csdl=True) for i, function in functions.items()}
48
+
49
+ all_min_dists = np.zeros((num_points, num_funcs))
50
+ all_min_param_coords = np.zeros((num_points, num_funcs, extrema_parametric.shape[1]))
51
+ for fi, i in enumerate(func_keys):
52
+ extrema_points = func_extrema[i]
53
+ diffs = extrema_points[np.newaxis, :, :] - points[:, np.newaxis, :]
54
+ dists = np.linalg.norm(diffs, axis=2)
55
+ min_inds = np.argmin(dists, axis=1)
56
+ all_min_dists[:, fi] = dists[np.arange(num_points), min_inds]
57
+ all_min_param_coords[:, fi, :] = extrema_parametric[min_inds]
58
+
59
+ best_func_inds = np.argmin(all_min_dists, axis=1)
60
+ best_coords = all_min_param_coords[np.arange(num_points), best_func_inds]
61
+ return list(zip([func_keys[fi] for fi in best_func_inds], best_coords))
62
+
63
+ parametric_coords_per_func = {}
64
+ errors = np.full((num_points, num_funcs), np.inf)
65
+
66
+ for fi, i in enumerate(func_keys):
67
+ function = functions[i]
68
+ param_coords = function.project(points, direction=direction_opt, grid_search_density_parameter=options.get('grid_search_density_parameter', 1),
69
+ max_newton_iterations=options.get('max_newton_iterations', 100), newton_tolerance=options.get('newton_tolerance', 1e-6),
70
+ projection_tolerance=projection_tolerance, grid_search_evaluation_cutoff=grid_search_evaluation_cutoff,
71
+ grid_search_subtraction_cutoff=grid_search_subtraction_cutoff, do_pickles=False,
72
+ use_line_search=options.get('use_line_search', False))
73
+
74
+ func_vals = function.evaluate(param_coords, coefficients=function.coefficients.value, non_csdl=True)
75
+ if direction is None:
76
+ errs = np.linalg.norm(func_vals - points, axis=1)
77
+ else:
78
+ displacement = points - func_vals
79
+ dir_norm = direction/np.linalg.norm(direction)
80
+ directed = np.linalg.norm(np.cross(displacement, dir_norm), axis=1)
81
+ total = np.linalg.norm(displacement, axis=1)
82
+ errs = directed + 1e-6 * total
83
+
84
+ if i in priority_inds:
85
+ errs = errs - priority_eps
86
+
87
+ errors[:, fi] = errs
88
+ parametric_coords_per_func[i] = param_coords
89
+
90
+ best_func_indices = np.argmin(errors, axis=1)
91
+ coords_stack = np.stack([parametric_coords_per_func[i] for i in func_keys], axis=1)
92
+ best_coords = coords_stack[np.arange(num_points), best_func_indices]
93
+ return list(zip([func_keys[fi] for fi in best_func_indices], best_coords))
94
+
95
+
96
+ @dataclass
97
+ class FunctionSet:
98
+ """Representation of a composite set of functions (e.g., multi-patch surfaces)."""
99
+ functions: dict[int,lfs.Function]
100
+ function_names : dict[int,str] = None
101
+ name : str = None
102
+ space : lfs.FunctionSetSpace = None
103
+
104
+ def __post_init__(self):
105
+ if isinstance(self.functions, list):
106
+ self.functions = {i:function for i, function in enumerate(self.functions)}
107
+
108
+ if isinstance(self.function_names, list):
109
+ self.function_names = {i:function_name for i, function_name in enumerate(self.function_names)}
110
+
111
+ if self.function_names is None:
112
+ self.function_names = {i:None for i in self.functions}
113
+ for i, function in self.functions.items():
114
+ self.function_names[i] = function.name
115
+
116
+ if self.space is None:
117
+ self.space = lfs.FunctionSetSpace(
118
+ num_parametric_dimensions={i:function.space.num_parametric_dimensions for i, function in self.functions.items()},
119
+ spaces={i:function.space for i, function in self.functions.items()})
120
+
121
+ def find_surface_connections(self):
122
+ perfect_connections = {}
123
+ dependent_connections = {}
124
+ search_n = 5
125
+ ones = np.ones((search_n)).reshape((-1,1))
126
+ zeros = np.zeros((search_n)).reshape((-1,1))
127
+ lin = np.linspace(0, 1, search_n).reshape((-1,1))
128
+ # find perfect connections
129
+ function_evals = {}
130
+ for i, function in self.functions.items():
131
+ eval1 = function.evaluate(np.hstack((lin, zeros)), non_csdl=True)
132
+ eval2 = function.evaluate(np.hstack((ones, lin)), non_csdl=True)
133
+ eval3 = function.evaluate(np.hstack((lin, ones)), non_csdl=True)
134
+ eval4 = function.evaluate(np.hstack((zeros, lin)), non_csdl=True)
135
+ function_evals[i] = [eval1, eval2, eval3, eval4]
136
+
137
+ connected_functions = set()
138
+ for i in self.functions:
139
+ evals = function_evals[i]
140
+ for j in self.functions:
141
+ if i == j:
142
+ continue
143
+ if frozenset([i,j]) in connected_functions:
144
+ continue
145
+ evals2 = function_evals[j]
146
+ for k, eval1 in enumerate(evals):
147
+ for l, eval2 in enumerate(evals2):
148
+ if np.linalg.norm(eval1 - eval2) < 1e-6:
149
+ # points1 = [(i, eval1_i) for eval1_i in eval1]
150
+ # points2 = [(j, eval2_i) for eval2_i in eval2]
151
+ # points = np.vstack((eval1, eval2))
152
+ # self.project(points, plot=True)
153
+ perfect_connections[(i,k+1)] = (j,l+1)
154
+ # perfect_connections[(j,l+1)] = (i,k+1)
155
+ connected_functions.add(frozenset([i,j]))
156
+ break
157
+ if np.linalg.norm(eval1 - eval2[::-1]) < 1e-6:
158
+ perfect_connections[(i,k+1)] = (j,-l-1)
159
+ # perfect_connections[(j,l+1)] = (i,-k-1)
160
+ connected_functions.add(frozenset([i,j]))
161
+ break
162
+ return perfect_connections
163
+
164
+ def apply_surface_connections(self, geometry=None):
165
+ connections = self.space.connections
166
+ for face1, face2 in connections.items():
167
+ function1 = self.functions[face1[0]]
168
+ function2 = self.functions[face2[0]]
169
+ coeffs1, coeffs2 = function1.space.stitch(face1[1], function1.coefficients,
170
+ function2.space, face2[1], function2.coefficients)
171
+ function1.coefficients = coeffs1
172
+ function2.coefficients = coeffs2
173
+
174
+ def stack_coefficients(self) -> csdl.Variable:
175
+ '''
176
+ Stacks the coefficients of the functions in the function set.
177
+
178
+ Returns
179
+ -------
180
+ coefficients : csdl.Variable
181
+ The stacked coefficients of the functions in the function set.
182
+ '''
183
+ coefficients = []
184
+ for i, function in self.functions.items():
185
+ shape = function.coefficients.shape
186
+ if len(shape) == 1:
187
+ shape = (1, shape[0])
188
+ if len(shape) >= 2:
189
+ shape = (np.prod(shape[:-1]), shape[-1])
190
+ coefficients.append([function.coefficients.reshape((shape))])
191
+ coefficients = csdl.blockmat(coefficients)
192
+ return coefficients
193
+
194
+ def unstack_coefficients(self, coefficients:csdl.Variable) -> None:
195
+ '''
196
+ Loads stacked coefficients into the function in the function set.
197
+
198
+ Parameters
199
+ ----------
200
+ coefficients : csdl.Variable
201
+ The stacked coefficients of the functions in the function set.
202
+ '''
203
+ start = 0
204
+ for i, function in self.functions.items():
205
+ shape = function.coefficients.shape
206
+ if len(shape) == 1:
207
+ shape = (1, shape[0])
208
+ if len(shape) >= 2:
209
+ shape = (np.prod(shape[:-1]), shape[-1])
210
+ end = start + shape[0]
211
+ function.coefficients = coefficients[start:end].reshape(shape)
212
+ start = end
213
+
214
+ def copy(self) -> lfs.FunctionSet:
215
+ '''
216
+ Copies the function set.
217
+
218
+ Returns
219
+ -------
220
+ function_set : lfs.FunctionSet
221
+ The copied function set.
222
+ '''
223
+ functions = {i:function.copy() for i, function in self.functions.items()}
224
+ function_set = lfs.FunctionSet(functions=functions, function_names=self.function_names, name=self.name)
225
+ return function_set
226
+
227
+ def evaluate_normals(self, parametric_coordinates:list[tuple[int, np.ndarray]], plot:bool=False) -> csdl.Variable:
228
+ '''
229
+ Evaluates the normals of the function set at the given parametric coordinates.
230
+
231
+ Parameters
232
+ ----------
233
+ parametric_coordinates : list[tuple[int, np.ndarray]] -- list length=num_points, tuple_length=2
234
+ The coordinates at which to evaluate the function. The list elements correspond to the coordinate of each point.
235
+ The tuple elements correspond to the index of the function and the parametric coordinates for that point.
236
+
237
+ Returns
238
+ -------
239
+ normals : csdl.Variable
240
+ The normals of the function set at the given coordinates.
241
+ '''
242
+ u_vectors = self.evaluate(parametric_coordinates, parametric_derivative_orders=(1,0))
243
+ v_vectors = self.evaluate(parametric_coordinates, parametric_derivative_orders=(0,1))
244
+ if len(u_vectors.shape) == 1:
245
+ u_vectors = u_vectors.reshape((1, -1))
246
+ v_vectors = v_vectors.reshape((1, -1))
247
+ normals = csdl.cross(v_vectors, u_vectors, axis=1)
248
+ normals = normals / (csdl.expand(csdl.norm(normals + 1e-8, axes=(1,)), (normals.shape), action='i->ij') + 1e-12)
249
+
250
+ if plot:
251
+ import lsdo_function_spaces as lfs
252
+ scale = 2e-1
253
+ points = self.evaluate(parametric_coordinates, non_csdl=True)
254
+ plotting_elements = self.plot(opacity=0.8, show=False)
255
+ arrow_data = pv.PolyData(points)
256
+ arrow_data["vectors"] = normals.value * scale
257
+ arrows = arrow_data.glyph(orient="vectors", scale="vectors", factor=1.0)
258
+ plotting_elements.append({"mesh": arrows, "kwargs": {"color": "red"}})
259
+ lfs.show_plot(plotting_elements, 'normals')
260
+ return normals
261
+
262
+ def evaluate(self, parametric_coordinates:list[tuple[int, npt.NDArray[np.float64]]],
263
+ parametric_derivative_orders:Optional[Union[Sequence[int], Sequence[Sequence[int,...]]]]=None,
264
+ plot:bool=False, non_csdl:bool=False) -> csdl.Variable:
265
+ '''
266
+ Evaluates the function.
267
+
268
+ Parameters
269
+ ----------
270
+ parametric_coordinates : list[tuple[int, np.ndarray]] -- list length=num_points, tuple_length=2
271
+ The coordinates at which to evaluate the function. The list elements correspond to the coordinate of each point.
272
+ The tuple elements correspond to the index of the function and the parametric coordinates for that point.
273
+ The parametric coordinates should be a numpy array of shape (num_parametric_dimensions,).
274
+ parametric_derivative_orders : Optional[Union[Sequence[int], Sequence[Sequence[int,...]]]] = None -- shape=(num_points,num_parametric_dimensions)
275
+ The order of the parametric derivatives to evaluate. If None, the function itself is evaluated.
276
+ plot : bool, optional
277
+ Whether or not to plot the function with the points from the result of the evaluation.
278
+ non_csdl : bool, optional
279
+ If true, will run numpy computations instead of csdl computations, and return a numpy array.
280
+
281
+ Returns
282
+ -------
283
+ function_values : csdl.Variable
284
+ The function evaluated at the given coordinates.
285
+ '''
286
+ if isinstance(parametric_coordinates, tuple):
287
+ parametric_coordinates = [parametric_coordinates]
288
+ # if isinstance(parametric_derivative_orders, tuple):
289
+ # parametric_derivative_orders = [parametric_derivative_orders]
290
+
291
+ # Process parametric coordinates to group them by which function they belong to
292
+ function_indices = []
293
+ function_parametric_coordinates = []
294
+ for parametric_coordinate in parametric_coordinates:
295
+ function_index, coordinates = parametric_coordinate
296
+ function_indices.append(function_index)
297
+ function_parametric_coordinates.append(coordinates)
298
+
299
+ # Evaluate each function at the given coordinates
300
+ basis_matrices = []
301
+ coeff_vectors = []
302
+ reorder_indices = []
303
+ for i, function in self.functions.items():
304
+ indices = np.where(np.array(function_indices) == i)[0]
305
+ para_coords = np.array([function_parametric_coordinates[j] for j in indices]).reshape(-1, function.space.num_parametric_dimensions)
306
+ # if parametric_derivative_orders is not None:
307
+ # para_derivs = [parametric_derivative_orders[j] for j in indices]
308
+
309
+ # if len(para_derivs) >= 1:
310
+ # para_derivs = para_derivs[0] # TODO: Add support for a separate derivative order for each point!
311
+ # else:
312
+ # para_derivs = None
313
+ # if len(indices) > 0:
314
+ # function_values_list.append(function.evaluate(parametric_coordinates=para_coords,
315
+ # parametric_derivative_orders=para_derivs))
316
+ # functions_with_points.append(i)
317
+
318
+ # # Arrange the function values back into the correct element of the array
319
+ # if len(function_values_list) == 0:
320
+ # raise ValueError("No points were evaluated.")
321
+ # if self.functions[functions_with_points[0]].num_physical_dimensions == 1:
322
+ # function_values = csdl.Variable(value=np.zeros((len(parametric_coordinates),)))
323
+
324
+ # else:
325
+ # function_values = csdl.Variable(value=np.zeros((len(parametric_coordinates), function_values_list[0].shape[-1])))
326
+ # for i, function_value in enumerate(function_values_list):
327
+ # indices = (np.array(function_indices) == functions_with_points[i]).nonzero()[0].tolist()
328
+ # indices = list(np.where(np.array(function_indices) == i)[0])
329
+ if len(indices) == 0:
330
+ continue
331
+ reorder_indices += indices.astype(int).tolist()
332
+ para_coords = np.array([function_parametric_coordinates[j] for j in indices]).reshape(-1, function.space.num_parametric_dimensions)
333
+ if parametric_derivative_orders is not None:
334
+ para_derivs = parametric_derivative_orders
335
+ else:
336
+ para_derivs = None
337
+ basis_matrix, coefficients = function.get_matrix_vector(parametric_coordinates=para_coords,
338
+ parametric_derivative_orders=para_derivs,
339
+ non_csdl=non_csdl)
340
+ basis_matrices.append(basis_matrix)
341
+ coeff_vectors.append(coefficients)
342
+
343
+ if len(reorder_indices) != len(parametric_coordinates):
344
+ raise ValueError("Some points were not evaluated.")
345
+
346
+
347
+ basis_matrix = sps.block_diag(basis_matrices, format='csr')
348
+ if len(coeff_vectors) == 0:
349
+ raise ValueError("No points were evaluated.")
350
+ elif len(coeff_vectors) == 1:
351
+ coeff_vector = coeff_vectors[0]
352
+ else:
353
+ if non_csdl:
354
+ coeff_vector = np.vstack(coeff_vectors)
355
+ else:
356
+ coeff_vector = csdl.vstack(coeff_vectors)
357
+
358
+ if non_csdl:
359
+ values = basis_matrix @ coeff_vector
360
+ else:
361
+ values = csdl.Variable(value=np.zeros((basis_matrix.shape[0], coeff_vector.shape[1])))
362
+ for i in csdl.frange(coeff_vector.shape[1]):
363
+ coefficients_column = coeff_vector[:,i].reshape((coeff_vector.shape[0],1))
364
+ values = values.set(csdl.slice[:,i], csdl.sparse.matvec(basis_matrix, coefficients_column).reshape((basis_matrix.shape[0],)))
365
+
366
+ indices_reorder = np.argsort(reorder_indices).tolist()
367
+ function_values = values[indices_reorder]
368
+
369
+ if plot:
370
+ # Plot the function
371
+ plotting_elements = self.plot(opacity=0.8, show=False)
372
+ # Plot the evaluated points
373
+ if non_csdl:
374
+ value = function_values
375
+ else:
376
+ value = function_values.value
377
+ lfs.plot_points(value, color='#C69214', size=10, additional_plotting_elements=plotting_elements)
378
+
379
+ if not len(function_values.shape) == 1:
380
+ if np.prod(function_values.shape) == function_values.shape[1] or np.prod(function_values.shape) == function_values.shape[0]:
381
+ function_values = function_values.reshape((-1,))
382
+
383
+ return function_values
384
+
385
+ def integrate(self, area, grid_n=10, indices=None, quadrature_order=2) -> tuple[csdl.Variable, list[tuple[int, np.ndarray]]]:
386
+ if indices is None:
387
+ indices = list(self.functions)
388
+ parametric_coordinates = []
389
+ values = []
390
+ # TODO: frange?
391
+ for i in indices:
392
+ function = self.functions[i]
393
+ value, coords = function.integrate(area.functions[i], grid_n=grid_n, quadrature_order=quadrature_order)
394
+ for j in range(len(coords)):
395
+ parametric_coordinates.append((i,coords[j]))
396
+ if len(value.shape) == 1:
397
+ value = value.reshape((-1,1))
398
+ values.append(value)
399
+ if len(values) == 1:
400
+ values = values[0]
401
+ elif len(values) > 1:
402
+ values = csdl.vstack(values)
403
+ return values, parametric_coordinates
404
+
405
+ def refit(self, new_function_spaces:dict[lfs.FunctionSpace]|lfs.FunctionSpace, indices_of_functions_to_refit:list[int]=None,
406
+ grid_resolution:tuple=None, parametric_coordinates:dict[tuple[int,np.ndarray]]=None,
407
+ parametric_derivative_orders:list[np.ndarray]=None, regularization_parameter:float=None) -> lfs.FunctionSet:
408
+ '''
409
+ Refits functions in the function set. Either a grid resolution or parametric coordinates must be provided.
410
+ If both are provided, the parametric coordinates will be used. If derivatives are used, the parametric derivative orders must be provided.
411
+
412
+ NOTE: this method will NOT overwrite the coefficients or function space in this object.
413
+ It will return a new function object with the refitted coefficients.
414
+
415
+ Parameters
416
+ ----------
417
+ new_function_spaces : dict[ind, FunctionSpace] -- dictionary length=number of functions being refit
418
+ The new function spaces that the functions will be picked from.
419
+ indices_of_functions_to_refit : list[int] = None -- list length=number of functions being refit
420
+ The indices of the functions to refit. If None, all the functions are refit.
421
+ grid_resolution : tuple = None -- shape=(num_parametric_dimensions,)
422
+ The resolution of the grid to refit the function.
423
+ parametric_coordinates : np.ndarray = None -- shape=(num_points, num_parametric_dimensions)
424
+ The coordinates at which to refit the function.
425
+ parametric_derivative_orders : list[np.ndarray] = None --list_length=num_points, np.ndarray_shape=(num_parametric_dimensions,)
426
+ The orders of the parametric derivatives to refit.
427
+
428
+ Returns
429
+ -------
430
+ lfs.FunctionSet
431
+ The refitted function with the new function space and new coefficients.
432
+ '''
433
+
434
+ if indices_of_functions_to_refit is None:
435
+ indices_of_functions_to_refit = list(self.functions)
436
+
437
+ if isinstance(new_function_spaces, lfs.FunctionSpace):
438
+ new_function_spaces = {ind:new_function_spaces for ind in self.functions}
439
+
440
+
441
+ if len(new_function_spaces) != len(indices_of_functions_to_refit):
442
+ raise ValueError("The number of new function spaces must match the number of functions to refit. " +
443
+ f"({len(new_function_spaces)} != {len(indices_of_functions_to_refit)})")
444
+
445
+ new_functions = {}
446
+ for i, function in self.functions.items():
447
+ if i in indices_of_functions_to_refit:
448
+ new_functions[i] = function.refit(new_function_space=new_function_spaces[i],
449
+ grid_resolution=grid_resolution,
450
+ parametric_coordinates=parametric_coordinates,
451
+ parametric_derivative_orders=parametric_derivative_orders,
452
+ regularization_parameter=regularization_parameter)
453
+ else:
454
+ new_functions[i] = function
455
+
456
+ new_function_set = lfs.FunctionSet(functions=new_functions, function_names=self.function_names)
457
+ return new_function_set
458
+
459
+ def project(self, points:npt.NDArray[np.float64], num_workers:int=None, direction:npt.NDArray[np.float64]=None, grid_search_density_parameter:int=1,
460
+ max_newton_iterations:int=100, newton_tolerance:float=1e-6, projection_tolerance:float=None, plot:bool=False,
461
+ extrema=False, force_reprojection=False, priority_inds:Optional[list[int]]=None, priority_eps:float=1e-3,
462
+ grid_search_evaluation_cutoff:Optional[float]=None, grid_search_subtraction_cutoff:Optional[float]=None,
463
+ grid_search_density_cutoff:int=50, do_pickles:bool=True, use_line_search:bool=False) -> list[tuple[int, npt.NDArray[np.float64]]]:
464
+ """
465
+ Projects a set of points onto the function. The points to project must be provided. If a direction is provided, the projection will find
466
+ the points on the function that are closest to the axis defined by the direction. If no direction is provided, the projection will find the
467
+ points on the function that are closest to the points to project. The grid search density parameter controls the density of the grid search
468
+ used to find the initial guess for the Newton iterations. The max newton iterations and newton tolerance control the convergence of the
469
+ Newton iterations. If plot is True, a plot of the projection will be displayed.
470
+
471
+ NOTE: Distance is measured by the 2-norm.
472
+
473
+ Parameters
474
+ ----------
475
+ points : np.ndarray -- shape=(num_points, num_phyiscal_dimensions)
476
+ The points to project onto the function.
477
+ direction : np.ndarray = None -- shape=(num_parametric_dimensions,)
478
+ The direction of the projection.
479
+ grid_search_density_parameter : int = 1
480
+ The density of the grid search used to find the initial guess for the Newton iterations.
481
+ max_newton_iterations : int = 100
482
+ The maximum number of Newton iterations.
483
+ newton_tolerance : float = 1e-6
484
+ The tolerance for the Newton iterations.
485
+ projection_tolerance : float, optional
486
+ The tolerance for the projection. If None, the projection will not be refined. If not None, the projection will be refined
487
+ using a finer grid search density parameter for the points that are not within the tolerance distance.
488
+ NOTE: This is only for use when the points are within the geometry that they are being projected onto, or, if a direction is
489
+ specified, the axis of the projection intersects the geometry.
490
+ plot : bool = False
491
+ Whether or not to plot the projection.
492
+ extrema : bool = False
493
+ Whether or not to project onto the extrema of the function.
494
+ force_reprojection : bool = False
495
+ Whether or not to force the projection to be recomputed.
496
+ priority_inds : list[int] = None
497
+ The indices of the functions to prioritize in the projection. If None, no functions are prioritized.
498
+ priority_eps : float = 1e-3
499
+ The epsilon value to use for the prioritized functions. If None, no functions are prioritized.
500
+ grid_search_evaluation_cutoff : int = None
501
+ idk what this does
502
+ grid_search_subtraction_cutoff : int = None
503
+ idk what this does
504
+ grid_search_density_cutoff : int = 100
505
+ The cutoff for the grid search density parameter. If the grid search density parameter exceeds this value during refinement,
506
+ the projection will be stopped.
507
+ This is to prevent the projection from taking too long. If the projection is stopped, a warning will be printed.
508
+ use_line_search : bool = False
509
+ If True, use Armijo backtracking for each Newton step of the underlying functions.
510
+ """
511
+ if num_workers is None:
512
+ num_workers = lfs.num_workers
513
+
514
+ if isinstance(points, csdl.Variable):
515
+ points = points.value
516
+
517
+ if do_pickles:
518
+ output = self._check_whether_to_load_projection(points, direction,
519
+ grid_search_density_parameter,
520
+ max_newton_iterations,
521
+ newton_tolerance,
522
+ projection_tolerance,
523
+ extrema,
524
+ priority_inds, priority_eps,
525
+ force_reprojection,
526
+ grid_search_density_cutoff)
527
+ if isinstance(output, list):
528
+ parametric_coordinates = output
529
+ if plot:
530
+ projection_results = self.evaluate(parametric_coordinates).value
531
+ plotting_elements = []
532
+ plotting_elements = lfs.plot_points(points, color='#00ff00', size=10, opacity=0.6, show=False)
533
+ # plotting_elements.append(lfs.plot_points(projection_results, color='#F5F0E6', size=10, show=False))
534
+ plotting_elements = lfs.plot_points(projection_results, color='#ff0000', size=5, show=False,
535
+ additional_plotting_elements=plotting_elements)
536
+ self.plot(opacity=0.3, additional_plotting_elements=plotting_elements, show=True)
537
+ return parametric_coordinates
538
+ else:
539
+ name_space_dict, long_name_space = output
540
+ else:
541
+ name_space_dict = None
542
+ long_name_space = None
543
+
544
+ if priority_inds is None:
545
+ priority_inds = []
546
+
547
+ options = {'direction': direction, 'grid_search_density_parameter': grid_search_density_parameter,
548
+ 'max_newton_iterations': max_newton_iterations, 'newton_tolerance': newton_tolerance,
549
+ 'projection_tolerance': None, 'extrema': extrema,
550
+ 'priority_inds': priority_inds, 'priority_eps': priority_eps,
551
+ 'grid_search_evaluation_cutoff': grid_search_evaluation_cutoff,
552
+ 'grid_search_subtraction_cutoff': grid_search_subtraction_cutoff,
553
+ 'use_line_search': use_line_search}
554
+
555
+
556
+
557
+ if len(points.shape) == 1:
558
+ points = points.reshape(1, -1)
559
+ else:
560
+ points = points.reshape(-1, points.shape[-1])
561
+
562
+ # make sure there aren't more workers than points
563
+ num_workers = min(num_workers, points.shape[0])
564
+
565
+ # ----- Do initial projection on all points -----
566
+ # Divide the points into chunks and run in parallel
567
+ if num_workers > 1:
568
+ chunks = np.array_split(points, num_workers)
569
+
570
+ global global_functions
571
+ global_functions = self.functions
572
+ global global_options
573
+ global_options = options
574
+
575
+ # pool = Pool(num_workers)
576
+ # results = pool.map(find_best_surface_chunked, chunks)
577
+
578
+ try:
579
+ with concurrent.futures.ProcessPoolExecutor(max_workers=num_workers) as executor:
580
+ results = executor.map(find_best_surface_chunked, chunks)
581
+ except (PermissionError, RuntimeError):
582
+ # Fall back to serial if process pools are restricted or unsafe under spawn.
583
+ results = map(lambda c: find_best_surface_chunked(c, self.functions, options), chunks)
584
+
585
+ parametric_coordinates = []
586
+ for result in results:
587
+ parametric_coordinates.extend(result)
588
+ else:
589
+ parametric_coordinates = find_best_surface_chunked(points, self.functions, options)
590
+
591
+
592
+ # ----- If projection tolerance is not None, refine the projection for the necessary points -----
593
+ if projection_tolerance is not None:
594
+
595
+ grid_search_density_parameter *= 1.5
596
+
597
+ while True:
598
+ options['grid_search_density_parameter'] = grid_search_density_parameter
599
+
600
+ squared_distances = get_projection_squared_distances(points, self.evaluate(parametric_coordinates, non_csdl=True), direction)
601
+
602
+ # Get the indices of the points that are not within the projection tolerance
603
+ indices = np.where(squared_distances > projection_tolerance**2)[0]
604
+
605
+ if len(indices) == 0:
606
+ break
607
+
608
+ print(f'refining to {grid_search_density_parameter} for {len(indices)} points')
609
+
610
+ refined_parametric_coordinates = find_best_surface_chunked(points[indices], self.functions, options)
611
+ for i, index in enumerate(indices):
612
+ parametric_coordinates[index] = refined_parametric_coordinates[i]
613
+
614
+ grid_search_density_parameter *= 1.5
615
+ if grid_search_density_parameter > grid_search_density_cutoff:
616
+ print('--'*50)
617
+ print("WARNING: Projection refinement stopped because it took more than 10 refinement steps!")
618
+ print("This is likely because not all of the points are within the function being projected onto.")
619
+ print(f"{len(indices)} points were not within the projection tolerance of {projection_tolerance}.")
620
+ print('--'*50)
621
+ break
622
+
623
+ if do_pickles:
624
+ characters = string.ascii_letters + string.digits # Alphanumeric characters
625
+ # Generate a random string of the specified length
626
+ random_string = ''.join(random.choice(characters) for _ in range(6))
627
+ projections_folder = 'stored_files/projections'
628
+ name_space_file_path = projections_folder + '/name_space_dict.pickle'
629
+ name_space_dict[long_name_space] = random_string
630
+ with open(name_space_file_path, 'wb+') as handle:
631
+ pickle.dump(name_space_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
632
+
633
+ with open(projections_folder + f'/{random_string}.pickle', 'wb+') as handle:
634
+ pickle.dump(parametric_coordinates, handle, protocol=pickle.HIGHEST_PROTOCOL)
635
+
636
+ if plot:
637
+ projection_results = self.evaluate(parametric_coordinates).value
638
+ plotting_elements = []
639
+ plotting_elements = lfs.plot_points(points, color='#00ff00', size=10, opacity=0.6, show=False)
640
+ # plotting_elements.append(lfs.plot_points(projection_results, color='#F5F0E6', size=10, show=False))
641
+ plotting_elements = lfs.plot_points(projection_results, color='#ff0000', size=5, show=False,
642
+ additional_plotting_elements=plotting_elements)
643
+ self.plot(opacity=0.3, additional_plotting_elements=plotting_elements, show=True)
644
+
645
+ return parametric_coordinates
646
+
647
+ def _check_whether_to_load_projection(self, points:np.ndarray, direction:np.ndarray=None, grid_search_density_parameter:int=1,
648
+ max_newton_iterations:int=100, newton_tolerance:float=1e-6, projection_tolerance:float=None,
649
+ extrema:bool=False, priority_inds=None, priority_eps=1e-3,
650
+ force_reprojection:bool=False,
651
+ grid_search_density_cutoff=100) -> bool:
652
+ name_space = f'{self.name}'
653
+
654
+ name_space = ''
655
+ for function_index, function in self.functions.items():
656
+ function_space = function.space
657
+
658
+ coefficients = function.coefficients.value
659
+ coeff_shape = function_space.coefficients_shape
660
+
661
+ # if f'{target}_{str(degree)}_{str(coeff_shape)}_{str(knot_vectors_norm)}' in name_space:
662
+ # pass
663
+ # else:
664
+ # name_space += f'_{str(coefficients)}_{str(coeff_shape)}'
665
+ # name_space += f'_{function_index}_{str(coefficients)}_{str(degree)}_{str(coeff_shape)}_{str(knot_vectors_norm)}'
666
+ name_space += f'_{function_index}_{str(coefficients)}_{str(coeff_shape)}'
667
+
668
+ long_name_space = name_space + f'_{str(points)}_{str(direction)}_{grid_search_density_parameter}_{max_newton_iterations}_{newton_tolerance}_{projection_tolerance}_{extrema}_{priority_inds}_{priority_eps}'
669
+ if projection_tolerance is not None:
670
+ long_name_space += f'_{grid_search_density_cutoff}'
671
+
672
+ projections_folder = 'stored_files/projections'
673
+ name_space_file_path = projections_folder + '/name_space_dict.pickle'
674
+
675
+ name_space_dict_file_path = Path(name_space_file_path)
676
+ if name_space_dict_file_path.is_file():
677
+ with open(name_space_file_path, 'rb') as handle:
678
+ name_space_dict = pickle.load(handle)
679
+ else:
680
+ Path("stored_files/projections").mkdir(parents=True, exist_ok=True)
681
+ name_space_dict = {}
682
+
683
+ if long_name_space in name_space_dict.keys() and not force_reprojection:
684
+ short_name_space = name_space_dict[long_name_space]
685
+ saved_projections_file = projections_folder + f'/{short_name_space}.pickle'
686
+ with open(saved_projections_file, 'rb') as handle:
687
+ parametric_coordinates = pickle.load(handle)
688
+ return parametric_coordinates
689
+ else:
690
+ Path("stored_files/projections").mkdir(parents=True, exist_ok=True)
691
+
692
+ return name_space_dict, long_name_space
693
+
694
+ def set_coefficients(self, coefficients:Sequence[csdl.Variable], function_indices:Optional[Sequence[int]]=None) -> None:
695
+ '''
696
+ Sets the coefficients of the functions in the function set with the given indices.
697
+
698
+ Parameters
699
+ ----------
700
+ coefficients : Union[csdl.Variable, Sequence[csdl.Variable]]
701
+ The coefficients to set the functions to.
702
+ function_indices : list[int]
703
+ The indices of the functions to set the coefficients of. If None, all the functions are set to the coefficients.
704
+ '''
705
+ if function_indices is None:
706
+ function_indices = np.array(list(self.functions.keys()))
707
+
708
+ if len(coefficients) != len(function_indices):
709
+ raise ValueError("The number of coefficients must match the number of functions to set. " +
710
+ f"({len(coefficients)} != {len(function_indices)})")
711
+
712
+ for i, function_index in enumerate(function_indices):
713
+ coefficients_shape = self.functions[function_index].coefficients.shape
714
+ self.functions[function_index].coefficients = coefficients[i].reshape(coefficients_shape)
715
+
716
+ def get_function_indices(self, function_names:list[str]) -> list[int]:
717
+ '''
718
+ Gets the indices of the functions in the function set with the given names.
719
+
720
+ Parameters
721
+ ----------
722
+ function_names : list[str]
723
+ The names of the functions to get the indices of.
724
+
725
+ Returns
726
+ -------
727
+ function_indices : list[int]
728
+ The indices of the functions in the function set with the given names.
729
+ '''
730
+ function_indices = []
731
+ names_keys = list(self.function_names.keys())
732
+ names_vals = list(self.function_names.values())
733
+ for function_name in function_names:
734
+ function_indices.append(names_keys[names_vals.index(function_name)])
735
+ return function_indices
736
+
737
+ def search_for_function_indices(self, search_strings:list[str], ignore_names:Optional[list[str]]=None) -> list[int]:
738
+ '''
739
+ Searches for the indices of the functions in the function set with the given search string.
740
+
741
+ Parameters
742
+ ----------
743
+ search_strings : str | list[str]
744
+ The strings to search for in the function names.
745
+
746
+ Returns
747
+ -------
748
+ function_indices : list[int]
749
+ The indices of the functions in the function set with the given search string.
750
+ '''
751
+ if isinstance(search_strings, str):
752
+ search_strings = [search_strings]
753
+ if ignore_names is None:
754
+ ignore_names = []
755
+
756
+ function_indices = []
757
+ for i, function_name in self.function_names.items():
758
+ if any(s in function_name for s in search_strings) and not any(s in function_name for s in ignore_names):
759
+ function_indices.append(i)
760
+ return function_indices
761
+
762
+ def create_subset(self, function_indices:list[int]=None, function_search_names:list[str]=None, ignore_names:list[str]=None, name:str=None) -> lfs.FunctionSet:
763
+ '''
764
+ Creates a subset of the function set with the given indices. Either the function indices or the function search names must be provided.
765
+
766
+ Parameters
767
+ ----------
768
+ function_indices : list[int]
769
+ The indices of the functions to include in the subset.
770
+ function_search_names : list[str]
771
+ The search strings to use to find the functions to include in the subset.
772
+ name : str
773
+ The name of the subset.
774
+
775
+ Returns
776
+ -------
777
+ subset : lfs.FunctionSet
778
+ The subset of the function set with the given indices.
779
+ '''
780
+ if function_indices is None:
781
+ function_indices = []
782
+ if function_search_names is not None:
783
+ function_indices += self.search_for_function_indices(search_strings=function_search_names, ignore_names=ignore_names)
784
+
785
+ # Remove duplicates
786
+ function_indices = list(set(function_indices))
787
+
788
+ functions = {i:self.functions[i] for i in function_indices}
789
+ function_names = {i:self.function_names[i] for i in function_indices}
790
+ subset = lfs.FunctionSet(functions=functions, function_names=function_names, name=name)
791
+ return subset
792
+
793
+ def plot_but_good(self, opacity:float=1., color="777777", color_map:str='jet', surface_texture:str="", show:bool=True, grid_n=25):
794
+ """
795
+ Plots the function set as a combined mesh.
796
+
797
+ Parameters
798
+ ----------
799
+ opacity : float, optional
800
+ The opacity of the plot. 0 is fully transparent and 1 is fully opaque.
801
+ color : str or lfs.FunctionSet, optional
802
+ Color hex string or FunctionSet for scalar field coloring.
803
+ color_map : str, optional
804
+ Colormap name when coloring by function values.
805
+ surface_texture : str, optional
806
+ Surface texture appearance preset.
807
+ show : bool, optional
808
+ Whether to display the plot.
809
+ grid_n : int, optional
810
+ Sampling grid resolution.
811
+ """
812
+ from lsdo_function_spaces.utils.plotting_functions import get_surface_mesh
813
+
814
+ vertices = []
815
+ faces = []
816
+ c_points = None
817
+ for ind, function in self.functions.items():
818
+ if isinstance(color, lfs.FunctionSet):
819
+ function_color = color.functions[ind]
820
+ fn_vertices, fn_faces, fn_c_points = get_surface_mesh(surface=function, color=function_color, grid_n=grid_n, offset=len(vertices))
821
+ if c_points is None:
822
+ c_points = fn_c_points
823
+ else:
824
+ c_points = np.hstack((c_points, fn_c_points))
825
+ else:
826
+ fn_vertices, fn_faces = get_surface_mesh(surface=function, grid_n=grid_n, offset=len(vertices))
827
+ vertices.extend(fn_vertices)
828
+ faces.extend(fn_faces)
829
+
830
+ faces_array = []
831
+ for face in faces:
832
+ faces_array.extend([len(face), *face])
833
+ mesh = pv.PolyData(np.array(vertices), np.array(faces_array, dtype=np.int64))
834
+
835
+ if c_points is not None:
836
+ mesh["scalars"] = c_points
837
+
838
+ if show:
839
+ plotter = pv.Plotter()
840
+ if c_points is not None:
841
+ plotter.add_mesh(mesh, opacity=opacity, cmap=color_map, show_scalar_bar=True)
842
+ else:
843
+ plotter.add_mesh(mesh, opacity=opacity, color=color)
844
+ plotter.show()
845
+ return mesh
846
+
847
+ def plot(self, camera:Optional[dict[str,tuple[float]]]=None, screenshot:str="",title:Optional[str]=None, interactive:bool=True, point_types:list[str]=['evaluated_points'], plot_types:list[str]=['function'],
848
+ opacity:float=1., color:Union[str,lfs.FunctionSet]='#00629B', color_map:str='jet', surface_texture:str="",
849
+ line_width:float=3., additional_plotting_elements:list=[], show:bool=True) -> list:
850
+ """
851
+ Plots the function set.
852
+
853
+ Parameters
854
+ ----------
855
+ points_type : list = ['evaluated_points']
856
+ The type of points to be plotted. {evaluated_points, coefficients}
857
+ plot_types : list = ['function']
858
+ The type of plot {function, wireframe, point_cloud}
859
+ opactity : float = 1.
860
+ The opacity of the plot. 0 is fully transparent and 1 is fully opaque.
861
+ color : str|lfs.FunctionSet = '#00629B'
862
+ The 6 digit color code to plot the B-spline as. If a FunctionSet is provided, the FunctionSet will be used to color the B-spline.
863
+ surface_texture : str = "" {"metallic", "glossy", ...}, optional
864
+ The surface texture to determine how light bounces off the surface.
865
+ This is kept for API compatibility.
866
+ color_map : str = 'jet'
867
+ The color map to use if the color is a function.
868
+ additional_plotting_elemets : list
869
+ Plotting elements that may have been returned from previous plotting functions that should be plotted with this plot.
870
+ show : bool
871
+ A boolean on whether to show the plot or not. If the plot is not shown, the plotting element is returned.
872
+
873
+ Returns
874
+ -------
875
+ plotting_elements : list
876
+ The plotting elements that were plotted.
877
+ """
878
+ import lsdo_function_spaces.utils.plotting_functions as pf
879
+ # Then there must be a discrete index so loop over subfunctions and plot them
880
+ # Flatten nested lists to handle cases where users pass [plot_points_result]
881
+ plotting_elements = pf._flatten_plotting_elements(additional_plotting_elements.copy())
882
+ color_min = None
883
+ color_max = None
884
+ for i, function in self.functions.items():
885
+ if isinstance(color, lfs.FunctionSet):
886
+ function_color = color.functions[i]
887
+ else:
888
+ function_color = color
889
+ out = function.plot(point_types=point_types, plot_types=plot_types, opacity=opacity, color=function_color, color_map=color_map,
890
+ surface_texture=surface_texture, line_width=line_width,
891
+ additional_plotting_elements=plotting_elements, show=False)
892
+ if isinstance(out, tuple):
893
+ plotting_elements = out[0]
894
+ if color_min is None:
895
+ color_min = out[1]
896
+ color_max = out[2]
897
+ else:
898
+ color_min = min(color_min, out[1])
899
+ color_max = max(color_max, out[2])
900
+ else:
901
+ plotting_elements = out
902
+ if isinstance(color, lfs.FunctionSet):
903
+ print('Color values', color_min, color_max)
904
+ plotting_elements.append(pf.make_scalar_bar_element(color_min, color_max, color_map=color_map))
905
+ if show:
906
+ if self.name is not None:
907
+ if title is not None:
908
+ lfs.show_plot(plotting_elements=plotting_elements, title=title,camera=camera,screenshot=screenshot,interactive=interactive)
909
+ else:
910
+ lfs.show_plot(plotting_elements=plotting_elements, title=self.name,camera=camera,screenshot=screenshot,interactive=interactive)
911
+ else:
912
+ lfs.show_plot(plotting_elements=plotting_elements, title='Function Set Plot',camera=camera,screenshot=screenshot, interactive=interactive)
913
+ return plotting_elements
914
+
915
+ def create_parallel_space(self, function_space:lfs.FunctionSpace) -> lfs.FunctionSetSpace:
916
+ '''
917
+ Creates a parallel function set space with the given function space.
918
+
919
+ Parameters
920
+ ----------
921
+ function_space : lfs.FunctionSpace
922
+ The function space to create the parallel function set space with.
923
+
924
+ Returns
925
+ -------
926
+ parallel_function_set : lfs.FunctionSetSpace
927
+ The parallel function set space with the given function space.
928
+ '''
929
+ parallel_spaces = {}
930
+ for i in self.functions.keys():
931
+ parallel_spaces[i] = function_space
932
+ parallel_function_set = lfs.FunctionSetSpace(num_parametric_dimensions=self.space.num_parametric_dimensions,
933
+ spaces=parallel_spaces, connections=self.space.connections)
934
+ return parallel_function_set
935
+
936
+ def generate_parametric_grid(self, grid_resolution:Union[tuple[int,...], int]) -> list[tuple[int, npt.NDArray[np.float64]]]:
937
+ '''
938
+ Generates a parametric grid for the function set.
939
+
940
+ Parameters
941
+ ----------
942
+ grid_resolution : tuple[int,...] or int
943
+ The resolution of the grid in each parametric dimension.
944
+
945
+ Returns
946
+ -------
947
+ parametric_grid : list[tuple[int, np.ndarray]]
948
+ The grid of parametric coordinates for the FunctionSet (makes a grid of the specified resolution over each function in the set).
949
+ '''
950
+
951
+ return self.space.generate_parametric_grid(grid_resolution=grid_resolution)
952
+
953
+ def __add__(self, other:FunctionSet) -> FunctionSet:
954
+ return lfs.operations.add(self, other)
955
+
956
+ def __radd__(self, other:FunctionSet) -> FunctionSet:
957
+ return lfs.operations.add(self, other)
958
+
959
+ def __sub__(self, other:FunctionSet) -> FunctionSet:
960
+ return lfs.operations.sub(self, other)
961
+
962
+ def __rsub__(self, other:FunctionSet) -> FunctionSet:
963
+ return lfs.operations.sub(other, self)
964
+
965
+ def __mul__(self, other:FunctionSet) -> FunctionSet:
966
+ return lfs.operations.mult(self, other)
967
+
968
+ def __rmul__(self, other:FunctionSet) -> FunctionSet:
969
+ return lfs.operations.mult(self, other)
970
+
971
+ def __truediv__(self, other:FunctionSet) -> FunctionSet:
972
+ return lfs.operations.div(self, other)
973
+
974
+ def __rtruediv__(self, other:FunctionSet) -> FunctionSet:
975
+ return lfs.operations.div(other, self)
976
+
977
+ def __pow__(self, other:FunctionSet) -> FunctionSet:
978
+ return lfs.operations.power(self, other)
979
+
980
+ def __rpow__(self, other:FunctionSet) -> FunctionSet:
981
+ return lfs.operations.power(other, self)
982
+
983
+ def __neg__(self) -> FunctionSet:
984
+ return lfs.operations.negate(self)
985
+
986
+ if __name__ == "__main__":
987
+ import csdl_alpha as csdl
988
+ recorder = csdl.Recorder(inline=True)
989
+ recorder.start()
990
+
991
+ num_coefficients1 = 10
992
+ num_coefficients2 = 5
993
+ degree1 = 4
994
+ degree2 = 3
995
+
996
+ # Create functions that make up set
997
+ space_of_cubic_b_spline_surfaces_with_10_cp = lfs.BSplineSpace(num_parametric_dimensions=2, degree=(degree1,degree1),
998
+ coefficients_shape=(num_coefficients1,num_coefficients1))
999
+ space_of_quadratic_b_spline_surfaces_with_5_cp = lfs.BSplineSpace(num_parametric_dimensions=2, degree=(degree2,degree2),
1000
+ coefficients_shape=(num_coefficients2,num_coefficients2))
1001
+
1002
+ coefficients_line = np.linspace(0., 1., num_coefficients1)
1003
+ coefficients_y, coefficients_x = np.meshgrid(coefficients_line,coefficients_line)
1004
+ coefficients1 = np.stack((coefficients_x, coefficients_y, 0.1*np.random.rand(num_coefficients1,num_coefficients1)), axis=-1)
1005
+ coefficients1 = coefficients1.reshape((num_coefficients1,num_coefficients1,3))
1006
+
1007
+ b_spline1 = lfs.Function(space=space_of_cubic_b_spline_surfaces_with_10_cp, coefficients=coefficients1, name='b_spline1')
1008
+
1009
+ coefficients_line = np.linspace(0., 1., num_coefficients2)
1010
+ coefficients_y, coefficients_x = np.meshgrid(coefficients_line,coefficients_line)
1011
+ coefficients_y += 1.5
1012
+ coefficients2 = np.stack((coefficients_x, coefficients_y, 0.1*np.random.rand(num_coefficients2,num_coefficients2)), axis=-1)
1013
+ coefficients2 = coefficients2.reshape((num_coefficients2,num_coefficients2,3))
1014
+
1015
+ b_spline2 = lfs.Function(space=space_of_quadratic_b_spline_surfaces_with_5_cp, coefficients=coefficients2, name='b_spline2')
1016
+
1017
+ # Make function set and plot
1018
+ my_b_spline_surface_set = lfs.FunctionSet(functions=[b_spline1, b_spline2], function_names=['b_spline1', 'b_spline2'])
1019
+ my_b_spline_surface_set.plot()
1020
+
1021
+
1022
+ # Refit the function set
1023
+ num_coefficients = 5
1024
+ space_of_linear_b_spline_surfaces_with_5_cp = lfs.BSplineSpace(num_parametric_dimensions=2, degree=(1,1),
1025
+ coefficients_shape=(num_coefficients,num_coefficients))
1026
+ new_function_spaces = [space_of_linear_b_spline_surfaces_with_5_cp, space_of_linear_b_spline_surfaces_with_5_cp]
1027
+ fitting_grid_resolution = 50
1028
+ new_function_set = my_b_spline_surface_set.refit(new_function_spaces=new_function_spaces,
1029
+ grid_resolution=(fitting_grid_resolution,fitting_grid_resolution))
1030
+ new_function_set.plot()
1031
+
1032
+
1033
+ # Once again, refit the function set but only refit the first function
1034
+ num_coefficients = 5
1035
+ space_of_linear_b_spline_surfaces_with_5_cp = lfs.BSplineSpace(num_parametric_dimensions=2, degree=(1,1),
1036
+ coefficients_shape=(num_coefficients,num_coefficients))
1037
+ new_function_spaces = [space_of_linear_b_spline_surfaces_with_5_cp]
1038
+ fitting_grid_resolution = 50
1039
+ new_function_set = my_b_spline_surface_set.refit(new_function_spaces=new_function_spaces,
1040
+ indices_of_functions_to_refit=[0],
1041
+ grid_resolution=(fitting_grid_resolution,fitting_grid_resolution))
1042
+ new_function_set.plot()
1043
+
1044
+ # Projection sanity check against the rectangular wing geometry with many points.
1045
+ wing = lfs.import_file('examples/import_files_for_examples/rectangular_wing.stp', parallelize=False)
1046
+
1047
+ # Generate a large set of points distributed spanwise across all wing surfaces
1048
+ spanwise_samples = 100 # dense spanwise distribution
1049
+ chordwise_samples = 100 # dense chordwise distribution
1050
+ all_sample_points = []
1051
+
1052
+ for surface_idx, func in wing.functions.items():
1053
+ # Create a parametric grid for this surface
1054
+ u_params = np.linspace(0.0, 1.0, chordwise_samples)
1055
+ v_params = np.linspace(0.0, 1.0, spanwise_samples)
1056
+ u_grid, v_grid = np.meshgrid(u_params, v_params)
1057
+ parametric_coords = np.column_stack([u_grid.ravel(), v_grid.ravel()])
1058
+
1059
+ # Evaluate the function at the parametric grid
1060
+ surface_points = func.evaluate(parametric_coords, non_csdl=True)
1061
+ # Add small perturbation normal to the surface for realistic projection test
1062
+ all_sample_points.append(surface_points + np.array([0.0, 0.0, 0.02]))
1063
+
1064
+ sample_points = np.vstack(all_sample_points)
1065
+ print(f'Projecting {sample_points.shape[0]} points distributed spanwise over {len(wing.functions)} wing surfaces...')
1066
+
1067
+ from time import perf_counter
1068
+ start_time = perf_counter()
1069
+ projection_results = wing.project(sample_points, do_pickles=False, force_reprojection=True, plot=False)
1070
+ end_time = perf_counter()
1071
+ print(f'Projection completed in {end_time - start_time:.2f} seconds ({sample_points.shape[0] / (end_time - start_time):.0f} points/sec).')
1072
+
1073
+ assert len(projection_results) == sample_points.shape[0], f'Expected {sample_points.shape[0]} results, got {len(projection_results)}'
1074
+ assert all(np.asarray(result[1]).shape == (2,) for result in projection_results), 'Not all results have 2D parametric coordinates'
1075
+ print(f'✓ Batch projection test PASSED: {len(projection_results)} points successfully projected.')
1076
+
1077
+ # Show distribution of projections across surfaces
1078
+ surface_counts = {}
1079
+ for result in projection_results:
1080
+ surface_counts[result[0]] = surface_counts.get(result[0], 0) + 1
1081
+ print(f' Points per surface: {dict(sorted(surface_counts.items()))}')