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,1322 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import csdl_alpha as csdl
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+ import scipy.sparse as sps
8
+
9
+ import sys
10
+ import pickle
11
+ from pathlib import Path
12
+
13
+ # NumPy 1.x / 2.x pickle compatibility shim
14
+ if "numpy._core" not in sys.modules:
15
+ try:
16
+ import numpy.core as _np_core
17
+ sys.modules["numpy._core"] = _np_core
18
+ for _sub in ("numeric", "multiarray", "umath", "numerictypes", "fromnumeric"):
19
+ if hasattr(_np_core, _sub):
20
+ sys.modules[f"numpy._core.{_sub}"] = getattr(_np_core, _sub)
21
+ except (ImportError, AttributeError):
22
+ pass
23
+
24
+ import string
25
+ import random
26
+ from time import perf_counter
27
+ from typing import Union, Optional, Sequence
28
+
29
+ # from lsdo_function_spaces.core.function_space import FunctionSpace
30
+ import lsdo_function_spaces as lfs
31
+ from lsdo_function_spaces.utils.internal_utilities import get_projection_squared_distances
32
+
33
+
34
+
35
+ class Function:
36
+ def __init__(self, space:lfs.FunctionSpace, coefficients:csdl.Variable, name:Optional[str]=None):
37
+ '''
38
+ Function class. This class is used to represent a function in a given function space. The function space is used to evaluate the function at
39
+ given coordinates, refit the function, and project points onto the function.
40
+
41
+ Attributes
42
+ ----------
43
+ space : lfs.FunctionSpace
44
+ The function space in which the function resides.
45
+ coefficients : csdl.Variable -- shape=coefficients_shape
46
+ The coefficients of the function.
47
+ name : str = None
48
+ If applicable, the name of the function.
49
+ '''
50
+
51
+ self.space = space
52
+ self.coefficients = coefficients
53
+ self.name = name
54
+ self.triangulation = None
55
+
56
+ if not isinstance(self.coefficients, csdl.Variable):
57
+ self.coefficients = csdl.Variable(value=self.coefficients)
58
+
59
+ if len(self.coefficients.shape) == 1:
60
+ self.num_physical_dimensions = 1
61
+ else:
62
+ self.num_physical_dimensions = self.coefficients.shape[-1]
63
+
64
+
65
+ def _compute_distance_bounds(self, point, direction=None):
66
+ return self.space._compute_distance_bounds(point, self, direction=direction)
67
+
68
+ def copy(self) -> lfs.Function:
69
+ '''
70
+ Returns a copy of the function.
71
+ '''
72
+ return lfs.Function(space=self.space, coefficients=self.coefficients*1., name=self.name)
73
+
74
+ def get_matrix_vector(self, parametric_coordinates:np.ndarray, parametric_derivative_orders:list[tuple]=None, coefficients:csdl.Variable=None,
75
+ non_csdl:bool=False):
76
+ '''
77
+ Gets the basis matrix and coefficients whose product is the function evaluated at the given coordinates.
78
+
79
+ Parameters
80
+ ----------
81
+ parametric_coordinates : np.ndarray -- shape=(num_points, num_parametric_dimensions)
82
+ The coordinates at which to evaluate the function.
83
+ parametric_derivative_order : tuple = None -- shape=(num_points,num_parametric_dimensions)
84
+ The order of the parametric derivatives to evaluate.
85
+ coefficients : csdl.Variable = None -- shape=coefficients_shape
86
+ The coefficients of the function.
87
+ plot : bool = False
88
+ Whether or not to plot the function with the points from the result of the evaluation.
89
+ non_csdl : bool = False
90
+ If true, will run numpy computations instead of csdl computations, and return a numpy array.
91
+
92
+ Returns
93
+ -------
94
+ basis_matrix : np.ndarray | sps.csr_matrix
95
+ The basis matrix evaluated at the given coordinates.
96
+ coefficients : csdl.Variable
97
+ The coefficients of the function.
98
+ '''
99
+ if coefficients is None:
100
+ coefficients = self.coefficients
101
+
102
+ if non_csdl and isinstance(coefficients, csdl.Variable):
103
+ coefficients = coefficients.value
104
+
105
+ basis_matrix = self.space.compute_basis_matrix(parametric_coordinates, parametric_derivative_orders)
106
+ if coefficients.shape != (basis_matrix.shape[1], self.num_physical_dimensions):
107
+ coefficients = coefficients.reshape((basis_matrix.shape[1], self.num_physical_dimensions))
108
+
109
+ return basis_matrix, coefficients
110
+
111
+ def evaluate(self, parametric_coordinates:npt.NDArray[np.float64], parametric_derivative_orders:Optional[Sequence[int]]=None, coefficients:Optional[csdl.Variable]=None,
112
+ plot:bool=False, non_csdl:bool=False) -> Union[csdl.Variable, npt.NDArray[np.float64]]:
113
+ '''
114
+ Evaluates the function.
115
+
116
+ Parameters
117
+ ----------
118
+ parametric_coordinates : npt.NDArray[np.float64] -- shape=(num_points, num_parametric_dimensions)
119
+ The coordinates at which to evaluate the function.
120
+ parametric_derivative_order : Sequence[int] = None -- shape=(num_points,num_parametric_dimensions)
121
+ The order of the parametric derivatives to evaluate.
122
+ coefficients : csdl.Variable = None -- shape=coefficients_shape
123
+ The coefficients of the function.
124
+ plot : bool = False
125
+ Whether or not to plot the function with the points from the result of the evaluation.
126
+ non_csdl : bool = False
127
+ If true, will run numpy computations instead of csdl computations, and return a numpy array.
128
+
129
+ Returns
130
+ -------
131
+ function_values : csdl.Variable
132
+ The function evaluated at the given coordinates.
133
+ '''
134
+ if coefficients is None:
135
+ coefficients = self.coefficients
136
+
137
+ if non_csdl and isinstance(coefficients, csdl.Variable):
138
+ coefficients = coefficients.value
139
+
140
+ if non_csdl:
141
+ values : npt.NDArray[np.float64] = self.space._evaluate(coefficients, parametric_coordinates, parametric_derivative_orders)
142
+ if values.shape[-1] == 1:
143
+ values = values.flatten()
144
+ else:
145
+ values : csdl.Variable = self.space._evaluate(coefficients, parametric_coordinates, parametric_derivative_orders)
146
+ if values.shape[-1] == 1:
147
+ values = values.reshape((-1,))
148
+
149
+ if plot:
150
+ # Plot the function
151
+ plotting_elements = self.plot(opacity=0.8, show=False)
152
+ # Plot the evaluated points
153
+ if non_csdl:
154
+ vals = values
155
+ else:
156
+ vals = values.value
157
+ lfs.plot_points(vals, color='#C69214', size=10, additional_plotting_elements=plotting_elements)
158
+
159
+ return values
160
+
161
+ def integrate(self, area, grid_n=10, quadrature_order=2):
162
+ """
163
+ Integrate the function over the area (2D). Uses gaussian quadrature for the integration.
164
+ """
165
+
166
+ # Generate parametric grid
167
+ parametric_grid = np.zeros((grid_n, grid_n, 2))
168
+ for i in range(grid_n):
169
+ for j in range(grid_n):
170
+ parametric_grid[i,j] = np.array([i/(grid_n-1), j/(grid_n-1)])
171
+
172
+ # get quadrature points and weights
173
+ quadrature_points, quadrature_weights = np.polynomial.legendre.leggauss(quadrature_order)
174
+ quadrature_points = (quadrature_points + 1)/2
175
+ quadrature_weights = quadrature_weights/2
176
+ quadrature_coords = np.zeros((quadrature_order**2, 2))
177
+ quadrature_coord_weights = np.zeros((quadrature_order**2,))
178
+ for i in range(quadrature_order):
179
+ for j in range(quadrature_order):
180
+ quadrature_coords[i*quadrature_order+j] = np.array([quadrature_points[i], quadrature_points[j]])
181
+ quadrature_coord_weights[i*quadrature_order+j] = quadrature_weights[i]*quadrature_weights[j]
182
+ quadrature_coord_weights = csdl.Variable(value=quadrature_coord_weights)
183
+
184
+ # get the parametric coordinates of the quadrature points
185
+ quadrature_parametric_coords = np.zeros((grid_n-1, grid_n-1, quadrature_order**2, 2))
186
+ for i in range(grid_n-1):
187
+ for j in range(grid_n-1):
188
+ for k in range(quadrature_order**2):
189
+ quadrature_parametric_coords[i,j,k] = parametric_grid[i,j] + quadrature_coords[k]/(grid_n-1)
190
+
191
+ # evaluate the function at the quadrature points
192
+ quadrature_values = self.evaluate(parametric_coordinates=quadrature_parametric_coords.reshape(-1,2)).reshape((grid_n-1, grid_n-1, quadrature_order**2, self.num_physical_dimensions))
193
+
194
+ # compute the integral
195
+ values = csdl.Variable(value=np.zeros((grid_n-1, grid_n-1, self.num_physical_dimensions)))
196
+ for i in csdl.frange(grid_n-1):
197
+ for j in csdl.frange(grid_n-1):
198
+ for k in csdl.frange(quadrature_order**2):
199
+ values = values.set(csdl.slice[i,j], values[i,j] + quadrature_values[i,j,k]*quadrature_coord_weights[k])
200
+
201
+ # compute areas of the quadrilaterals
202
+ grid_values = area.evaluate(parametric_coordinates=parametric_grid.reshape(-1,2)).reshape((grid_n, grid_n, -1))
203
+ output = csdl.Variable(value=np.zeros(values.shape))
204
+ for i in csdl.frange(grid_n-1):
205
+ for j in csdl.frange(grid_n-1):
206
+ area_1 = csdl.norm(csdl.cross(grid_values[i+1,j]-grid_values[i,j], grid_values[i,j+1]-grid_values[i,j]) + 1e-8)/2
207
+ area_2 = csdl.norm(csdl.cross(grid_values[i,j+1]-grid_values[i+1,j+1], grid_values[i+1,j]-grid_values[i+1,j+1]) + 1e-8)/2
208
+ output = output.set(csdl.slice[i,j], (area_1+area_2)*values[i,j])
209
+
210
+ # Get the parametric coordinates of the grid center points
211
+ grid_centers = np.zeros((grid_n-1, grid_n-1, self.space.num_parametric_dimensions))
212
+ for i in range(grid_n-1):
213
+ for j in range(grid_n-1):
214
+ grid_centers[i,j] = (parametric_grid[i+1, j] + parametric_grid[i, j] + parametric_grid[i, j+1] + parametric_grid[i+1, j+1])/4
215
+
216
+ return output.reshape((-1, self.num_physical_dimensions)), grid_centers.reshape(-1, self.space.num_parametric_dimensions)
217
+
218
+ def refit(self, new_function_space:lfs.FunctionSpace, grid_resolution:tuple=None,
219
+ parametric_coordinates:np.ndarray=None, parametric_derivative_orders:np.ndarray=None,
220
+ regularization_parameter:float=None) -> Function:
221
+ '''
222
+ Optimally refits the function. Either a grid resolution or parametric coordinates must be provided.
223
+ If both are provided, the parametric coordinates will be used. If derivatives are used, the parametric derivative orders must be provided.
224
+
225
+ NOTE: this method will not overwrite the coefficients or function space in this object.
226
+ It will return a new function object with the refitted coefficients.
227
+
228
+ Parameters
229
+ ----------
230
+ new_function_space : FunctionSpace
231
+ The new function space that the function will be picked from.
232
+ grid_resolution : tuple = None -- shape=(num_parametric_dimensions,)
233
+ The resolution of the grid to refit the function.
234
+ parametric_coordinates : np.ndarray = None -- shape=(num_points, num_parametric_dimensions)
235
+ The coordinates at which to refit the function.
236
+ parametric_derivative_orders : np.ndarray = None -- shape=(num_points, num_parametric_dimensions)
237
+ The orders of the parametric derivatives to refit.
238
+
239
+ Returns
240
+ -------
241
+ Function
242
+ The refitted function with the new function space and new coefficients.
243
+ '''
244
+
245
+ if parametric_coordinates is None and grid_resolution is None:
246
+ # raise ValueError("Either grid resolution or parametric coordinates must be provided.")
247
+ grid_resolution = (100,)*self.space.num_parametric_dimensions
248
+ if parametric_coordinates is not None and grid_resolution is not None:
249
+ print("Warning: Both grid resolution and parametric coordinates were provided. Using parametric coordinates.")
250
+ # raise Warning("Both grid resolution and parametric coordinates were provided. Using parametric coordinates.")
251
+
252
+ if parametric_coordinates is None:
253
+ # if grid_resolution is not None: # Don't need this line because we already error checked at the beginning.
254
+ mesh_grid_input = []
255
+ for dimension_index in range(self.space.num_parametric_dimensions):
256
+ mesh_grid_input.append(np.linspace(0., 1., grid_resolution[dimension_index]))
257
+
258
+ parametric_coordinates_tuple = list(np.meshgrid(*mesh_grid_input, indexing='ij'))
259
+ for dimensions_index in range(self.space.num_parametric_dimensions):
260
+ parametric_coordinates_tuple[dimensions_index] = parametric_coordinates_tuple[dimensions_index].reshape((-1,1))
261
+
262
+ parametric_coordinates = np.hstack(parametric_coordinates_tuple)
263
+
264
+ # JUST CALL self.evaluate()!
265
+ # basis_matrix = self.space.compute_basis_matrix(parametric_coordinates, parametric_derivative_orders)
266
+ # coefficients_reshaped = self.coefficients.reshape((self.coefficients.size//self.num_physical_dimensions, self.num_physical_dimensions))
267
+ # fitting_values = csdl.Variable(value=np.zeros((parametric_coordinates.shape[0], self.num_physical_dimensions)))
268
+ # for i in range(self.num_physical_dimensions):
269
+ # fitting_values = fitting_values.set(csdl.slice[:,i], csdl.sparse.matvec(basis_matrix,
270
+ # coefficients_reshaped[:,i].reshape((coefficients_reshaped.shape[0],1))).flatten())
271
+ # # fitting_values = basis_matrix.dot(self.coefficients.value.reshape((-1,self.num_physical_dimensions)))
272
+ fitting_values = self.evaluate(parametric_coordinates, parametric_derivative_orders=parametric_derivative_orders)
273
+
274
+ coefficients = new_function_space.fit(
275
+ values=fitting_values,
276
+ parametric_coordinates=parametric_coordinates,
277
+ parametric_derivative_orders=parametric_derivative_orders,
278
+ regularization_parameter=regularization_parameter)
279
+
280
+ new_function = Function(space=new_function_space, coefficients=coefficients)
281
+ return new_function
282
+
283
+ def project(self, points:np.ndarray, direction:np.ndarray=None, grid_search_density_parameter:int=1,
284
+ max_newton_iterations:int=100, newton_tolerance:float=1e-12, projection_tolerance:float=None,
285
+ plot:bool=False, force_reproject:bool=False,
286
+ grid_search_evaluation_cutoff:int=None, grid_search_subtraction_cutoff:int=None,
287
+ do_pickles=True, grid_search_density_cutoff=50, verbose:bool=False,
288
+ use_line_search:bool=False) -> csdl.Variable:
289
+ '''
290
+ Projects a set of points onto the function. The points to project must be provided. If a direction is provided, the projection will find
291
+ 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
292
+ points on the function that are closest to the points to project. The grid search density parameter controls the density of the grid search
293
+ used to find the initial guess for the Newton iterations. The max newton iterations and newton tolerance control the convergence of the
294
+ Newton iterations. If plot is True, a plot of the projection will be displayed.
295
+
296
+ NOTE: Distance is measured by the 2-norm.
297
+
298
+ Parameters
299
+ ----------
300
+ points : np.ndarray -- shape=(num_points, num_phyiscal_dimensions)
301
+ The points to project onto the function.
302
+ direction : np.ndarray = None -- shape=(num_parametric_dimensions,)
303
+ The direction of the projection.
304
+ grid_search_density_parameter : int = 1
305
+ The density of the grid search used to find the initial guess for the Newton iterations.
306
+ max_newton_iterations : int = 100
307
+ The maximum number of Newton iterations.
308
+ newton_tolerance : float = 1e-6
309
+ The tolerance for the Newton iterations.
310
+ projection_tolerance : float = None
311
+ The tolerance for the projection. If None, the projection will not be refined. If not None, the projection will be refined
312
+ using a finer grid search density parameter for the points that are not within the tolerance distance.
313
+ NOTE: This is only for use when the points are within the geometry that they are being projected onto, or, if a direction is provided,
314
+ the axis defined by the direction intersects the geometry.
315
+ plot : bool = False
316
+ Whether or not to plot the projection.
317
+ force_reproject : bool = False
318
+ If True, the projection will be recomputed even if it has already been computed and saved to a file.
319
+ grid_search_evaluation_cutoff : int = None
320
+ The cutoff for the number of points to evaluate in the grid search. If the number of points is greater than this, the grid search
321
+ will be evaluated in sections. If None, no bunching will be done.
322
+ grid_search_subtraction_cutoff : int = None
323
+ The cutoff for the number of points to subtract in the grid search. If the number of points is greater than this, the grid search
324
+ will be subtracted in sections. If None, no bunching will be done.
325
+ do_pickles : bool = True
326
+ If True, the projection will be saved to a file. The file will be saved in the stored_files/projections directory.
327
+ grid_search_density_cutoff : int = 50
328
+ The cutoff for the grid search density during refinement. If the grid search density is greater than this, the refinement will be
329
+ terminated and a warning will be printed.
330
+ use_line_search : bool = False
331
+ If True, use Armijo backtracking for each Newton step. If False, apply the stabilized Newton step directly.
332
+ '''
333
+ if isinstance(points, csdl.Variable):
334
+ points = points.value
335
+
336
+ if do_pickles:
337
+ output = self._check_whether_to_load_projection(points, direction,
338
+ grid_search_density_parameter,
339
+ max_newton_iterations,
340
+ newton_tolerance,
341
+ force_reproject)
342
+ if isinstance(output, np.ndarray):
343
+ parametric_coordinates = output
344
+
345
+ if projection_tolerance is not None:
346
+ parametric_coordinates = self.refine_projection(points, parametric_coordinates, direction,
347
+ grid_search_density_parameter, max_newton_iterations,
348
+ newton_tolerance, projection_tolerance,
349
+ grid_search_evaluation_cutoff, grid_search_subtraction_cutoff,
350
+ do_pickles=do_pickles, grid_search_density_cutoff=grid_search_density_cutoff,
351
+ use_line_search=use_line_search)
352
+
353
+ if plot:
354
+ projection_results = self.evaluate(parametric_coordinates).value
355
+ plotting_elements = []
356
+ plotting_elements = lfs.plot_points(points, color='#00629B', size=10, show=False)
357
+ plotting_elements = lfs.plot_points(projection_results, color='#C69214', size=10, show=False,
358
+ additional_plotting_elements=plotting_elements)
359
+ self.plot(opacity=0.8, additional_plotting_elements=plotting_elements, show=True)
360
+ return parametric_coordinates
361
+ else:
362
+ name_space_dict, long_name_space = output
363
+
364
+ num_physical_dimensions = points.shape[-1]
365
+
366
+ points = points.reshape((-1, num_physical_dimensions))
367
+
368
+ # grid_search_resolution = 10*grid_search_density_parameter//self.space.num_parametric_dimensions + 1
369
+ if not hasattr(self, '_grid_searches'):
370
+ self._grid_searches = {}
371
+ if grid_search_density_parameter not in self._grid_searches:
372
+
373
+ grid_search_resolution = self.space._generate_projection_grid_search_resolution(grid_search_density_parameter)
374
+
375
+ if grid_search_resolution is None:
376
+ grid_search_resolution = 10*grid_search_density_parameter//self.space.num_parametric_dimensions + 1
377
+ # grid_search_resolution = 100
378
+
379
+ # Generate parametric grid
380
+ parametric_grid_search = self.space.generate_parametric_grid(grid_search_resolution)
381
+
382
+ num_grid_points = np.prod(grid_search_resolution)
383
+ # cutoff_size = 3.e7
384
+ # cutoff_size = 2.5e7
385
+ # cutoff_size = 1.5e7
386
+ # cutoff_size = 1.e7
387
+ # cutoff_size = 5.e6
388
+ if verbose:
389
+ print('grid search evaluation size: ', num_grid_points)
390
+ if grid_search_evaluation_cutoff is not None and num_grid_points > grid_search_evaluation_cutoff:
391
+ num_sections = int(np.ceil(num_grid_points/grid_search_evaluation_cutoff))
392
+ section_size = int(np.ceil(num_grid_points/num_sections))
393
+ grid_search_values = np.zeros((num_grid_points, self.coefficients.shape[-1]))
394
+ start_index = 0
395
+ for i in range(num_sections):
396
+ # print(i, '/', num_sections)
397
+ end_index = start_index + section_size
398
+ grid_search_values[start_index:end_index] = self.evaluate(parametric_coordinates=parametric_grid_search[start_index:end_index],
399
+ coefficients=self.coefficients.value, non_csdl=True)
400
+ start_index = end_index
401
+ else:
402
+ # Evaluate grid of points
403
+ grid_search_values = self.evaluate(parametric_coordinates=parametric_grid_search, coefficients=self.coefficients.value, non_csdl=True)
404
+ expanded_points_size = points.shape[0]*grid_search_values.shape[0]
405
+ self._grid_searches[grid_search_density_parameter] = (parametric_grid_search, grid_search_values, expanded_points_size)
406
+ else:
407
+ parametric_grid_search, grid_search_values, expanded_points_size = self._grid_searches[grid_search_density_parameter]
408
+ # cutoff_size = 2.5e7
409
+ # cutoff_size = 5.e7
410
+ # cutoff_size = 1.e8
411
+ # cutoff_size = 1.5e8
412
+ # cutoff_size = 2.5e8
413
+ if verbose:
414
+ print('grid search subtraction size: ', expanded_points_size)
415
+ grid_search_start_time = perf_counter()
416
+ if grid_search_subtraction_cutoff is not None and expanded_points_size > grid_search_subtraction_cutoff:
417
+ # grid search sections of points at a time
418
+ num_sections = int(np.ceil(expanded_points_size/grid_search_subtraction_cutoff))
419
+ section_size = int(np.ceil(points.shape[0]/num_sections))
420
+ closest_point_indices = np.zeros((points.shape[0],), dtype=int)
421
+ for i in range(num_sections):
422
+ # print(i, '/', num_sections)
423
+ start_index = i*section_size
424
+ end_index = min((i+1)*section_size, points.shape[0])
425
+ points_expanded = np.repeat(points[start_index:end_index,np.newaxis,:], grid_search_values.shape[0], axis=1)
426
+ grid_search_displacements = grid_search_values - points_expanded
427
+ grid_search_distances = np.linalg.norm(grid_search_displacements, axis=2)
428
+
429
+ # Perform a grid search
430
+ if direction is None:
431
+ closest_point_indices[start_index:end_index] = np.argmin(grid_search_distances, axis=1)
432
+ else:
433
+ direction = direction/np.linalg.norm(direction)
434
+ rho = 1e-3
435
+ grid_search_distances_along_axis = np.dot(grid_search_displacements, direction)
436
+ grid_search_distances_from_axis_squared = (1 + rho)*grid_search_distances**2 - grid_search_distances_along_axis**2
437
+ closest_point_indices[start_index:end_index] = np.argmin(grid_search_distances_from_axis_squared, axis=1)
438
+ else:
439
+ points_expanded = np.repeat(points[:,np.newaxis,:], grid_search_values.shape[0], axis=1)
440
+ grid_search_displacements = grid_search_values - points_expanded
441
+ grid_search_distances = np.linalg.norm(grid_search_displacements, axis=2)
442
+
443
+ # Perform a grid search
444
+ if direction is None:
445
+ # If no direction is provided, the projection will find the points on the function that are closest to the points to project.
446
+ # The grid search will be used to find the initial guess for the Newton iterations
447
+
448
+ # Find closest point on function to each point to project
449
+ # closest_point_indices = np.argmin(np.linalg.norm(grid_search_values - points, axis=1))
450
+ closest_point_indices = np.argmin(grid_search_distances, axis=1)
451
+
452
+ else:
453
+ # If a direction is provided, the projection will find the points on the function that are closest to the axis defined by the direction.
454
+ # The grid search will be used to find the initial guess for the Newton iterations
455
+ direction = direction/np.linalg.norm(direction)
456
+ rho = 1e-3
457
+ grid_search_distances_along_axis = np.dot(grid_search_displacements, direction)
458
+ grid_search_distances_from_axis_squared = (1 + rho)*grid_search_distances**2 - grid_search_distances_along_axis**2
459
+ closest_point_indices = np.argmin(grid_search_distances_from_axis_squared, axis=1)
460
+
461
+ grid_search_time = perf_counter() - grid_search_start_time
462
+ if verbose:
463
+ print(f'grid search time: {grid_search_time:.6f} s')
464
+
465
+ # Use the parametric coordinate corresponding to each closest point as the initial guess for the Newton iterations
466
+ initial_guess = parametric_grid_search[closest_point_indices]
467
+
468
+
469
+ # current_guess = initial_guess.copy()
470
+ # # As a first implementation approach, loop over points to project and perform Newton optimization for each point
471
+ # for i in range(points.shape[0]):
472
+ # for j in range(max_newton_iterations):
473
+ # # Perform B-spline evaluations needed for gradient and hessian (0th, 1st, and 2nd order derivatives needed)
474
+ # function_value = self.evaluate(current_guess[i]).value
475
+
476
+ # displacement = (points[i] - function_value).flatten()
477
+ # d_displacement_d_parametric = np.zeros((num_physical_dimensions, self.space.num_parametric_dimensions,))
478
+ # d2_displacement_d_parametric2 = np.zeros((num_physical_dimensions, self.space.num_parametric_dimensions, self.space.num_parametric_dimensions))
479
+ # for k in range(self.space.num_parametric_dimensions):
480
+ # parametric_derivative_orders = np.zeros((self.space.num_parametric_dimensions,), dtype=int)
481
+ # parametric_derivative_orders[k] = 1
482
+ # d_displacement_d_parametric[:,k] = -self.space.compute_basis_matrix(
483
+ # current_guess[i], parametric_derivative_orders=parametric_derivative_orders
484
+ # ).dot(self.coefficients.value.reshape((-1,num_physical_dimensions)))
485
+ # for m in range(self.space.num_parametric_dimensions):
486
+ # parametric_derivative_orders = np.zeros((self.space.num_parametric_dimensions,))
487
+ # if m == k:
488
+ # parametric_derivative_orders[m] = 2
489
+ # else:
490
+ # parametric_derivative_orders[k] = 1
491
+ # parametric_derivative_orders[m] = 1
492
+ # d2_displacement_d_parametric2[:,k,m] = -self.space.compute_basis_matrix(
493
+ # current_guess[i], parametric_derivative_orders=parametric_derivative_orders
494
+ # ).dot(self.coefficients.value.reshape((-1,num_physical_dimensions)))
495
+
496
+ # # Construct the gradient and hessian
497
+ # gradient = 2*displacement.dot(d_displacement_d_parametric)
498
+ # hessian = 2*(np.tensordot(d_displacement_d_parametric, d_displacement_d_parametric, axes=[0,0])
499
+ # + np.tensordot(displacement, d2_displacement_d_parametric2, axes=[0,0]))
500
+
501
+ # # Remove dof that are on constrant boundary and want to leave (active subspace method)
502
+ # coorinates_to_remove_on_lower_boundary = np.logical_and(current_guess[i] == 0, gradient > 0)
503
+ # coorinates_to_remove_on_upper_boundary = np.logical_and(current_guess[i] == 1, gradient < 0)
504
+ # coorinates_to_remove = np.logical_or(coorinates_to_remove_on_lower_boundary, coorinates_to_remove_on_upper_boundary)
505
+ # coordinates_to_keep = np.arange(self.space.num_parametric_dimensions)[np.logical_not(coorinates_to_remove)]
506
+
507
+ # # coordinates_to_keep = np.setdiff1d(np.arange(self.space.num_parametric_dimensions), coorinates_to_remove)
508
+ # reduced_gradient = gradient[coordinates_to_keep]
509
+ # reduced_hessian = hessian[np.ix_(coordinates_to_keep, coordinates_to_keep)]
510
+
511
+ # # # Finite difference check gradient
512
+ # # finite_difference_gradient = np.zeros((self.space.num_parametric_dimensions,))
513
+ # # for k in range(self.space.num_parametric_dimensions):
514
+ # # delta = 1e-6
515
+ # # current_guess_plus_delta = current_guess[i].copy()
516
+ # # current_guess_plus_delta[k] += delta
517
+ # # function_value_plus_delta = self.evaluate(current_guess_plus_delta).value
518
+ # # displacement_plus_delta = (points[i] - function_value_plus_delta).flatten()
519
+ # # objective = displacement_plus_delta.dot(displacement_plus_delta)
520
+ # # finite_difference_gradient[k] = (objective - displacement.dot(displacement))/delta
521
+
522
+ # # Check for convergence
523
+ # if np.linalg.norm(reduced_gradient) < newton_tolerance:
524
+ # break
525
+
526
+ # # Solve the linear system
527
+ # # delta = np.linalg.solve(hessian, -gradient)
528
+ # delta = np.linalg.solve(reduced_hessian, -reduced_gradient)
529
+
530
+ # # Update the initial guess
531
+ # current_guess[i,coordinates_to_keep] += delta
532
+ # # If any of the coordinates are outside the bounds, set them to the bounds
533
+ # current_guess[i] = np.clip(current_guess[i], 0., 1.)
534
+
535
+ # Experimental implementation that does all the Newton optimizations at once to vectorize many of the computations
536
+ current_guess = initial_guess.copy()
537
+ points_left_to_converge = np.arange(points.shape[0])
538
+ newton_start_time = perf_counter()
539
+ total_line_search_iterations = 0
540
+ max_line_search_iterations = 0
541
+ for j in range(max_newton_iterations):
542
+ # Perform B-spline evaluations needed for gradient and hessian (0th, 1st, and 2nd order derivatives needed)
543
+ function_values = self.evaluate(parametric_coordinates=current_guess[points_left_to_converge], coefficients=self.coefficients.value, non_csdl=True)
544
+ displacements = (points[points_left_to_converge] - function_values).reshape(points_left_to_converge.shape[0], num_physical_dimensions)
545
+
546
+ d_displacement_d_parametric = np.zeros((points_left_to_converge.shape[0], num_physical_dimensions, self.space.num_parametric_dimensions))
547
+ d2_displacement_d_parametric2 = np.zeros((points_left_to_converge.shape[0], num_physical_dimensions,
548
+ self.space.num_parametric_dimensions, self.space.num_parametric_dimensions))
549
+
550
+ for k in range(self.space.num_parametric_dimensions):
551
+ parametric_derivative_orders = np.zeros((self.space.num_parametric_dimensions,), dtype=int)
552
+ parametric_derivative_orders[k] = 1
553
+ # d_displacement_d_parametric[:, :, k] = -np.tensordot(
554
+ # self.space.compute_basis_matrix(current_guess, parametric_derivative_orders=parametric_derivative_orders),
555
+ # self.coefficients.value.reshape(-1, num_physical_dimensions), axes=[1,0])
556
+ d_displacement_d_parametric[:, :, k] = -self.space.compute_basis_matrix(current_guess[points_left_to_converge],
557
+ parametric_derivative_orders=parametric_derivative_orders).dot(
558
+ self.coefficients.value.reshape(-1, num_physical_dimensions))
559
+ # NOTE on indices: i=points, j=coefficients, k=physical dimensions
560
+
561
+ for m in range(self.space.num_parametric_dimensions):
562
+ parametric_derivative_orders = np.zeros((self.space.num_parametric_dimensions,), dtype=int)
563
+ if m == k:
564
+ parametric_derivative_orders[m] = 2
565
+ else:
566
+ parametric_derivative_orders[k] = 1
567
+ parametric_derivative_orders[m] = 1
568
+ # d2_displacement_d_parametric2[:, :, k, m] = -np.einsum(
569
+ # self.space.compute_basis_matrix(current_guess, parametric_derivative_orders=parametric_derivative_orders),
570
+ # self.coefficients.value.reshape((-1, num_physical_dimensions)), 'ij,jk->ik')
571
+ d2_displacement_d_parametric2[:, :, k, m] = -self.space.compute_basis_matrix(current_guess[points_left_to_converge],
572
+ parametric_derivative_orders=parametric_derivative_orders).dot(
573
+ self.coefficients.value.reshape((-1, num_physical_dimensions)))
574
+ # NOTE on indices: i=points, j=coefficients, k=physical dimensions
575
+
576
+ # Construct the gradient and hessian
577
+ if direction is None:
578
+ gradient = 2 * np.einsum('ij,ijk->ik', displacements, d_displacement_d_parametric)
579
+ hessian = 2 * (np.einsum('ijk,ijm->ikm', d_displacement_d_parametric, d_displacement_d_parametric)
580
+ + np.einsum('ij,ijkm->ikm', displacements, d2_displacement_d_parametric2))
581
+ else:
582
+ displacement_dot_d_displacement_d_parametric = np.einsum('ij,ijk->ik', displacements, d_displacement_d_parametric)
583
+ direction_dot_displacement = np.einsum('j,ij->i', direction, displacements)
584
+ direction_dot_d_displacement_d_parametric = np.einsum('j,ijk->ik', direction, d_displacement_d_parametric)
585
+ direction_dot_d2_displacement_d_parametric2 = np.einsum('j,ijkm->ikm', direction, d2_displacement_d_parametric2)
586
+ gradient = 2 * ((1 + rho)*displacement_dot_d_displacement_d_parametric
587
+ - direction_dot_displacement[:, np.newaxis] * direction_dot_d_displacement_d_parametric)
588
+ hessian = 2 * ( (1 + rho)*(
589
+ np.einsum('ijk,ijm->ikm', d_displacement_d_parametric, d_displacement_d_parametric)
590
+ + np.einsum('ij,ijkm->ikm', displacements, d2_displacement_d_parametric2))
591
+ - np.einsum('ik,im->ikm', direction_dot_d_displacement_d_parametric, direction_dot_d_displacement_d_parametric)
592
+ - np.einsum('i,ikm->ikm', direction_dot_displacement, direction_dot_d2_displacement_d_parametric2)
593
+ )
594
+
595
+ if direction is None:
596
+ current_objective_values = np.einsum('ij,ij->i', displacements, displacements)
597
+ else:
598
+ current_objective_values = ((1 + rho) * np.einsum('ij,ij->i', displacements, displacements)
599
+ - direction_dot_displacement**2)
600
+
601
+ # Remove dof that are on constrant boundary and want to leave (active set method)
602
+ coordinates_to_remove_on_lower_boundary = np.logical_and(current_guess[points_left_to_converge] == 0, gradient > 0)
603
+ coordinates_to_remove_on_upper_boundary = np.logical_and(current_guess[points_left_to_converge] == 1, gradient < 0)
604
+ coordinates_to_from_zero_hessian_column = np.where(~hessian.any(axis=1))[0] # Axis is 1 because we want to remove the column
605
+ coordinates_to_remove_boolean = np.logical_or(coordinates_to_remove_on_lower_boundary, coordinates_to_remove_on_upper_boundary)
606
+ coordinates_to_remove_boolean[coordinates_to_from_zero_hessian_column] = True
607
+
608
+ coordinates_to_keep_boolean = np.logical_not(coordinates_to_remove_boolean)
609
+ indices_to_keep = []
610
+ for i in range(points_left_to_converge.shape[0]):
611
+ indices_to_keep.append(np.arange(self.space.num_parametric_dimensions)[coordinates_to_keep_boolean[i]])
612
+
613
+ reduced_gradients = []
614
+ reduced_hessians = []
615
+ reduced_objective_values = []
616
+ total_gradient_norm = 0.
617
+ counter = 0
618
+ for i in range(points_left_to_converge.shape[0]):
619
+ reduced_gradient = gradient[i, indices_to_keep[counter]]
620
+
621
+ if np.linalg.norm(reduced_gradient) < newton_tolerance:
622
+ points_left_to_converge = np.delete(points_left_to_converge, counter)
623
+ current_objective_values = np.delete(current_objective_values, counter)
624
+ del indices_to_keep[counter]
625
+ continue
626
+
627
+ # This is after check so it doesn't throw error
628
+ reduced_hessian = hessian[np.ix_(np.array([i]), indices_to_keep[counter], indices_to_keep[counter])][0]
629
+
630
+ reduced_gradients.append(reduced_gradient)
631
+ reduced_hessians.append(reduced_hessian)
632
+ reduced_objective_values.append(current_objective_values[counter])
633
+ total_gradient_norm += np.linalg.norm(reduced_gradient)
634
+ counter += 1
635
+
636
+ # Check for convergence
637
+ if np.linalg.norm(total_gradient_norm) < newton_tolerance:
638
+ break
639
+
640
+ # Solve the linear systems
641
+ for i, index in enumerate(points_left_to_converge):
642
+ # delta = np.linalg.solve(reduced_hessians[i], -reduced_gradients[i])
643
+
644
+ reduced_hessian = 0.5 * (reduced_hessians[i] + reduced_hessians[i].T)
645
+ eigenvalues, eigenvectors = np.linalg.eigh(reduced_hessian)
646
+ flipped_eigenvalues = np.maximum(np.abs(eigenvalues), 1e-12)
647
+ stabilized_inverse = eigenvectors @ np.diag(1.0 / flipped_eigenvalues) @ eigenvectors.T
648
+ delta = stabilized_inverse @ (-reduced_gradients[i])
649
+
650
+ if not use_line_search:
651
+ current_guess[index, indices_to_keep[i]] += delta
652
+ continue
653
+
654
+ step_size = 1.0
655
+ armijo_c1 = 1e-4
656
+ backtracking_contraction = 0.9
657
+ min_step_size = 1e-8
658
+ current_objective = reduced_objective_values[i]
659
+ directional_derivative = reduced_gradients[i].dot(delta)
660
+
661
+ accepted_step = None
662
+ trial_guess = current_guess[index].copy()
663
+ line_search_iterations = 0
664
+ for _ in range(50):
665
+ line_search_iterations += 1
666
+ trial_guess[:] = current_guess[index]
667
+ trial_guess[indices_to_keep[i]] += step_size * delta
668
+ trial_guess = np.clip(trial_guess, 0., 1.)
669
+
670
+ trial_function_value = self.evaluate(
671
+ parametric_coordinates=trial_guess.reshape(1, -1),
672
+ coefficients=self.coefficients.value,
673
+ non_csdl=True,
674
+ ).reshape(num_physical_dimensions)
675
+ trial_displacement = points[index] - trial_function_value
676
+
677
+ if direction is None:
678
+ trial_objective = trial_displacement.dot(trial_displacement)
679
+ else:
680
+ trial_objective = ((1 + rho) * trial_displacement.dot(trial_displacement)
681
+ - (direction.dot(trial_displacement))**2)
682
+
683
+ if trial_objective <= current_objective + armijo_c1 * step_size * directional_derivative:
684
+ total_line_search_iterations += line_search_iterations
685
+ max_line_search_iterations = max(max_line_search_iterations, line_search_iterations)
686
+ accepted_step = trial_guess.copy()
687
+ break
688
+
689
+ step_size *= backtracking_contraction
690
+ if step_size < min_step_size:
691
+ total_line_search_iterations += line_search_iterations
692
+ max_line_search_iterations = max(max_line_search_iterations, line_search_iterations)
693
+ accepted_step = trial_guess.copy()
694
+ break
695
+
696
+ if accepted_step is None:
697
+ accepted_step = trial_guess.copy()
698
+
699
+ # Update the initial guess
700
+ current_guess[index] = accepted_step
701
+
702
+ # If any of the coordinates are outside the bounds, set them to the bounds
703
+ current_guess[points_left_to_converge] = np.clip(current_guess[points_left_to_converge], 0., 1.)
704
+
705
+ newton_time = perf_counter() - newton_start_time
706
+ if verbose:
707
+ print(f'newton time: {newton_time:.6f} s')
708
+ print(f'line search iterations total: {total_line_search_iterations}, max per point: {max_line_search_iterations}')
709
+
710
+ if projection_tolerance is not None:
711
+ current_guess = self.refine_projection(points, current_guess, direction,
712
+ grid_search_density_parameter, max_newton_iterations,
713
+ newton_tolerance, projection_tolerance=projection_tolerance,
714
+ do_pickles=False, use_line_search=use_line_search)
715
+
716
+ if plot:
717
+ projection_results = self.evaluate(current_guess).value
718
+ plotting_elements = []
719
+ plotting_elements = lfs.plot_points(points, color='#00629B', size=10, show=False)
720
+ plotting_elements = lfs.plot_points(projection_results, color='#C69214', size=10, show=False,
721
+ additional_plotting_elements=plotting_elements)
722
+ # print("plotting function now")
723
+ self.plot(opacity=0.8, additional_plotting_elements=plotting_elements, show=True, color="#FF8400")
724
+
725
+ if do_pickles:
726
+ # Save the projection
727
+ characters = string.ascii_letters + string.digits # Alphanumeric characters
728
+ # Generate a random string of the specified length
729
+ random_string = ''.join(random.choice(characters) for _ in range(6))
730
+ projections_folder = 'stored_files/projections'
731
+ name_space_file_path = projections_folder + '/name_space_dict.pickle'
732
+ name_space_dict[long_name_space] = random_string
733
+ with open(name_space_file_path, 'wb+') as handle:
734
+ pickle.dump(name_space_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
735
+
736
+ with open(projections_folder + f'/{random_string}.pickle', 'wb+') as handle:
737
+ pickle.dump(current_guess, handle, protocol=pickle.HIGHEST_PROTOCOL)
738
+
739
+ return current_guess
740
+
741
+ def refine_projection(self, points:np.ndarray, parametric_coordinates:np.ndarray, direction:np.ndarray, initial_grid_search_density_parameter:int=1,
742
+ max_newton_iterations:int=100, newton_tolerance:float=1e-6, projection_tolerance:float=1e-6,
743
+ grid_search_evaluation_cutoff:int=None, grid_search_subtraction_cutoff:int=None,
744
+ do_pickles=True, grid_search_density_cutoff=50, use_line_search:bool=False) -> np.ndarray:
745
+ '''
746
+ For projections where the points are in the geometry, this method finds the points that are not within the tolerance distance and reprojects
747
+ those points using a finer grid search density parameter.
748
+ '''
749
+ if isinstance(points, csdl.Variable):
750
+ points = points.value
751
+ points_flattened = points.reshape((-1,3))
752
+ previous_projection_results = self.evaluate(parametric_coordinates=parametric_coordinates, non_csdl=True)
753
+
754
+
755
+ squared_distances = get_projection_squared_distances(points_flattened, previous_projection_results, direction)
756
+ points_to_reproject = np.where(squared_distances > projection_tolerance**2)[0]
757
+ distances = np.sqrt(squared_distances[points_to_reproject])
758
+
759
+ if len(points_to_reproject) == 0:
760
+ return parametric_coordinates
761
+ else:
762
+ counter = 0
763
+ grid_search_density_parameter = initial_grid_search_density_parameter*1.2
764
+ while len(points_to_reproject) > 0:
765
+ # print('Total tolerance norm: ', np.linalg.norm(distances))
766
+ # print(f'Refining projection on {len(points_to_reproject)} points with grid search density parameter:', grid_search_density_parameter)
767
+ new_parametric_coordinates = self.project(points_flattened[points_to_reproject], direction, grid_search_density_parameter=grid_search_density_parameter,
768
+ max_newton_iterations=max_newton_iterations, newton_tolerance=newton_tolerance, force_reproject=False,
769
+ grid_search_evaluation_cutoff=grid_search_evaluation_cutoff,
770
+ grid_search_subtraction_cutoff=grid_search_subtraction_cutoff,
771
+ use_line_search=use_line_search)
772
+ parametric_coordinates[points_to_reproject] = new_parametric_coordinates
773
+ new_projection_results = self.evaluate(parametric_coordinates=new_parametric_coordinates, non_csdl=True)
774
+
775
+ # distances = np.linalg.norm(points_flattened[points_to_reproject] - new_projection_results, axis=1)
776
+ # points_to_reproject = points_to_reproject[np.where(distances > projection_tolerance)[0]]
777
+
778
+ squared_distances = get_projection_squared_distances(points_flattened[points_to_reproject], new_projection_results, direction)
779
+ new_points_to_reproject = np.where(squared_distances > projection_tolerance**2)[0]
780
+ distances = np.sqrt(squared_distances[new_points_to_reproject])
781
+ points_to_reproject = points_to_reproject[new_points_to_reproject]
782
+
783
+ grid_search_density_parameter *= 1.5
784
+ counter += 1
785
+ if grid_search_density_parameter > grid_search_density_cutoff:
786
+ print('--'*50)
787
+ print("WARNING: Projection refinement stopped because it took more than 10 refinement steps!")
788
+ print("This is likely because not all of the points are within the function being projected onto.")
789
+ print("Error remaining: ", np.linalg.norm(distances))
790
+ print('--'*50)
791
+ break
792
+
793
+ if do_pickles:
794
+ name_space_dict, long_name_space = self._check_whether_to_load_projection(points, direction,
795
+ grid_search_density_parameter,
796
+ max_newton_iterations,
797
+ newton_tolerance,
798
+ force_reproject=True)
799
+
800
+ # Save the projection
801
+ characters = string.ascii_letters + string.digits # Alphanumeric characters
802
+ # Generate a random string of the specified length
803
+ random_string = ''.join(random.choice(characters) for _ in range(6))
804
+ projections_folder = 'stored_files/projections'
805
+ name_space_file_path = projections_folder + '/name_space_dict.pickle'
806
+ name_space_dict[long_name_space] = random_string
807
+ with open(name_space_file_path, 'wb+') as handle:
808
+ pickle.dump(name_space_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
809
+
810
+ with open(projections_folder + f'/{random_string}.pickle', 'wb+') as handle:
811
+ pickle.dump(parametric_coordinates, handle, protocol=pickle.HIGHEST_PROTOCOL)
812
+
813
+ return parametric_coordinates
814
+
815
+ def _check_whether_to_load_projection(self, points:np.ndarray, direction:np.ndarray=None, grid_search_density_parameter:int=1,
816
+ max_newton_iterations:int=100, newton_tolerance:float=1e-6, force_reproject:bool=False) -> bool:
817
+ # name_space = f'{self.name}'
818
+
819
+ # name_space = ''
820
+ # for function in self.functions.values():
821
+ # function_space = function.space
822
+
823
+ # coefficients = function.coefficients.value
824
+ # degree = function_space.degree
825
+ # coeff_shape = function_space.coefficients_shape
826
+ # knot_vectors_norm = round(np.linalg.norm(function_space.knots), 2)
827
+
828
+ # # if f'{target}_{str(degree)}_{str(coeff_shape)}_{str(knot_vectors_norm)}' in name_space:
829
+ # # pass
830
+ # # else:
831
+ # name_space += f'_{str(coefficients)}_{str(degree)}_{str(coeff_shape)}_{str(knot_vectors_norm)}'
832
+
833
+ function_info = f'{self.name}_{self.coefficients.value}'
834
+ projection_info = f'{points}_{direction}_{grid_search_density_parameter}_{max_newton_iterations}_{newton_tolerance}'
835
+ long_name_space = f'{function_info}_{projection_info}'
836
+
837
+ projections_folder = 'stored_files/projections'
838
+ name_space_file_path = projections_folder + '/name_space_dict.pickle'
839
+
840
+ name_space_dict_file_path = Path(name_space_file_path)
841
+ if name_space_dict_file_path.is_file():
842
+ try:
843
+ with open(name_space_file_path, 'rb') as handle:
844
+ name_space_dict = pickle.load(handle)
845
+ except Exception:
846
+ name_space_dict = {}
847
+ else:
848
+ Path("stored_files/projections").mkdir(parents=True, exist_ok=True)
849
+ name_space_dict = {}
850
+
851
+ if long_name_space in name_space_dict.keys() and not force_reproject:
852
+ short_name_space = name_space_dict[long_name_space]
853
+ saved_projections_file = projections_folder + f'/{short_name_space}.pickle'
854
+ try:
855
+ with open(saved_projections_file, 'rb') as handle:
856
+ parametric_coordinates = pickle.load(handle)
857
+ return parametric_coordinates
858
+ except Exception:
859
+ pass
860
+
861
+ Path("stored_files/projections").mkdir(parents=True, exist_ok=True)
862
+ return name_space_dict, long_name_space
863
+
864
+ def plot(self, point_types:list[str]=['evaluated_points'], plot_types:list[str]=['function'],
865
+ opacity:float=1., color:str|Function='#00629B', color_map:str='jet', surface_texture:str="",
866
+ line_width:float=3., additional_plotting_elements:list=[], show:bool=True) -> list:
867
+ '''
868
+ Plots the B-spline Surface.
869
+
870
+ Parameters
871
+ -----------
872
+ points_type : list = ['evaluated_points']
873
+ The type of points to be plotted. {evaluated_points, coefficients}
874
+ plot_types : list = ['function']
875
+ The type of plot {function, wireframe, point_cloud}
876
+ opactity : float = 1.
877
+ The opacity of the plot. 0 is fully transparent and 1 is fully opaque.
878
+ color : str = '#00629B'
879
+ The 6 digit color code to plot the B-spline as. If a function is provided, the function will be used to color the B-spline.
880
+ surface_texture : str = "" {"metallic", "glossy", ...}, optional
881
+ The surface texture to determine how light bounces off the surface.
882
+ This is kept for API compatibility.
883
+ color_map : str = 'jet'
884
+ The color map to use if the color is a function.
885
+ additional_plotting_elemets : list
886
+ PyVista plotting elements that may have been returned from previous plotting functions that should be plotted with this plot.
887
+ show : bool
888
+ A boolean on whether to show the plot or not. If the plot is not shown, the plotting element is returned.
889
+
890
+ Returns
891
+ -------
892
+ plotting_elements : list
893
+ The PyVista plotting elements that were plotted.
894
+ '''
895
+ import lsdo_function_spaces.utils.plotting_functions as pf
896
+ if self.coefficients is None:
897
+ raise ValueError("The coefficients of the function are not defined.")
898
+
899
+ # Flatten nested lists to handle cases where users pass [plot_points_result]
900
+ plotting_elements = pf._flatten_plotting_elements(additional_plotting_elements.copy())
901
+ for point_type in point_types:
902
+ if point_type not in ['evaluated_points', 'coefficients']:
903
+ raise ValueError(f"Invalid point type. Must be 'evaluated_points' or 'coefficients'. Got {point_type}.")
904
+
905
+ if self.space.num_parametric_dimensions == 1:
906
+ # NOTE: Curve plotting not currently implemented for points in 3D space because I don't have a num_physical_dimensions attribute.
907
+ plotting_elements = self.plot_curve(point_type=point_type, opacity=opacity, color=color, color_map=color_map,
908
+ line_width=line_width, additional_plotting_elements=plotting_elements, show=show)
909
+
910
+ elif self.space.num_parametric_dimensions == 2:
911
+ out = self.plot_surface(point_type=point_type, plot_types=plot_types, opacity=opacity, color=color, color_map=color_map,
912
+ surface_texture=surface_texture, line_width=line_width,
913
+ additional_plotting_elements=plotting_elements, show=show)
914
+ if isinstance(out, tuple):
915
+ plotting_elements = out[0]
916
+ cmin = out[1]
917
+ cmax = out[2]
918
+ else:
919
+ plotting_elements = out
920
+ elif self.space.num_parametric_dimensions == 3:
921
+ plotting_elements = self.plot_volume(point_type=point_type, plot_types=plot_types, opacity=opacity, color=color, color_map=color_map,
922
+ surface_texture=surface_texture, line_width=line_width,
923
+ additional_plotting_elements=plotting_elements, show=show)
924
+ else:
925
+ raise ValueError("The number of parametric dimensions must be 1, 2, or 3 in order to plot.")
926
+ # elif isinstance(self.space, lfs.FunctionSetSpace):
927
+ # # Then there must be a discrete index so loop over subfunctions and plot them
928
+ # plotting_elements = []
929
+ # for index, subfunction_space_index in self.space.index_to_space.items():
930
+ # subfunction_space = self.space.spaces[subfunction_space_index]
931
+ # subfunction = Function(space=subfunction_space, coefficients=self.coefficients[self.space.index_to_coefficient_indices[index]])
932
+ # plotting_elements += subfunction.plot(point_types=point_types, plot_types=plot_types, opacity=opacity, color=color, color_map=color_map,
933
+ # surface_texture=surface_texture, line_width=line_width,
934
+ # additional_plotting_elements=additional_plotting_elements, show=False)
935
+ # if show:
936
+ # lfs.show_plot(plotting_elements=plotting_elements, title='B-Spline Set Plot')
937
+ # return plotting_elements
938
+ if isinstance(color, Function):
939
+ return plotting_elements, cmin, cmax
940
+ return plotting_elements
941
+
942
+ def plot_points(self, point_type:str='evaluated_points', opacity:float=1., color:str|lfs.Function='#00629B', color_map:str='jet',
943
+ size:float=10., additional_plotting_elements:list=[], show:bool=True) -> list:
944
+ '''
945
+ Plots the points of the function.
946
+
947
+ Parameters
948
+ -----------
949
+ points_type : str = 'evaluated_points'
950
+ The type of points to be plotted. {evaluated_points, coefficients}
951
+ opactity : float = 1.
952
+ The opacity of the plot. 0 is fully transparent and 1 is fully opaque.
953
+ color : str = '#00629B'
954
+ The 6 digit color code to plot the points as. If a function is provided, the function will be used to color the points.
955
+ color_map : str = 'jet'
956
+ The color map to use if the color is a function.
957
+ size : float = 10.
958
+ The size of the points.
959
+ additional_plotting_elemets : list = []
960
+ PyVista plotting elements that may have been returned from previous plotting functions that should be plotted with this plot.
961
+ show : bool = True
962
+ A boolean on whether to show the plot or not. If the plot is not shown, the plotting element is returned.
963
+
964
+ Returns
965
+ -------
966
+ plotting_elements : list
967
+ The PyVista plotting elements that were plotted.
968
+ '''
969
+ import lsdo_function_spaces.utils.plotting_functions as pf
970
+ raise NotImplementedError("This function is not implemented yet.")
971
+
972
+ def plot_curve(self, point_type:str='evaluated_points', opacity:float=1., color:str|lfs.Function='#00629B', color_map:str='jet',
973
+ line_width:float=3., additional_plotting_elements:list=[], show:bool=True):
974
+ '''
975
+ Plots the function as a curve. NOTE: This should only be called if the function is a curve!
976
+
977
+ Parameters
978
+ -----------
979
+ points_type : str = 'evaluated_points'
980
+ The type of points to be plotted. {evaluated_points, coefficients}
981
+ opactity : float = 1.
982
+ The opacity of the plot. 0 is fully transparent and 1 is fully opaque.
983
+ color : str = '#00629B'
984
+ The 6 digit color code to plot the function as. If a function is provided, the function will be used to color the curve.
985
+ color_map : str = 'jet'
986
+ The color map to use if the color is a function.
987
+ additional_plotting_elemets : list = []
988
+ Plotting elements that may have been returned from previous plotting functions that should be plotted with this plot.
989
+ show : bool = True
990
+ A boolean on whether to show the plot or not. If the plot is not shown, the plotting element is returned.
991
+
992
+ Returns
993
+ -------
994
+ plotting_elements : list
995
+ The plotting elements that were plotted.
996
+ '''
997
+ import lsdo_function_spaces.utils.plotting_functions as pf
998
+ if self.space.num_parametric_dimensions != 1:
999
+ raise ValueError("This function is not a curve and cannot be plotted as one.")
1000
+
1001
+ # Flatten nested lists to handle cases where users pass [plot_points_result]
1002
+ plotting_elements = pf._flatten_plotting_elements(additional_plotting_elements.copy())
1003
+
1004
+ # region Generate the points to plot
1005
+ if point_type == 'evaluated_points':
1006
+ num_points = 100
1007
+ parametric_coordinates = np.linspace(0., 1., num_points).reshape((-1,1))
1008
+ function_values = self.evaluate(parametric_coordinates, non_csdl=True)
1009
+ if len(function_values.shape) == 1:
1010
+ function_values = function_values.reshape((-1,1)) # Keep physical dimension separate for plotting
1011
+
1012
+ # scale u axis to be more visually clear based on scaling of parameter
1013
+ if function_values.shape[-1] < 3: # Plot against u coordinate
1014
+ u_axis_scaling = np.max(function_values) - np.min(function_values)
1015
+ if u_axis_scaling != 0:
1016
+ parametric_coordinates = parametric_coordinates# * u_axis_scaling
1017
+ points = np.hstack((parametric_coordinates, function_values))
1018
+ else:
1019
+ points = function_values
1020
+
1021
+ if isinstance(color, Function):
1022
+ if color.space.num_parametric_dimensions != 1:
1023
+ raise ValueError("The color function must be 1D to plot as a curve.")
1024
+
1025
+ color = color.evaluate(parametric_coordinates, non_csdl=True)
1026
+ elif point_type == 'coefficients':
1027
+ # NOTE: Check this line below!! I think this should really be the knot vector but I don't want to hardcode the existence of the knot vector.
1028
+ parametric_coordinates = np.linspace(0., 1., self.coefficients.shape[0]).reshape((-1,1))
1029
+
1030
+ # scale u axis to be more visually clear based on scaling of parameter
1031
+ u_axis_scaling = np.max(self.coefficients.value) - np.min(self.coefficients.value)
1032
+ if u_axis_scaling != 0:
1033
+ parametric_coordinates = parametric_coordinates# * u_axis_scaling
1034
+
1035
+ if len(self.coefficients.shape) == 1:
1036
+ points = np.hstack((parametric_coordinates, self.coefficients.value.reshape((-1,1))))
1037
+ else:
1038
+ points = np.hstack((parametric_coordinates, self.coefficients.value))
1039
+
1040
+ if isinstance(color, Function):
1041
+ if color.space.num_parametric_dimensions != 1:
1042
+ raise ValueError("The color function must be 1D to plot as a curve.")
1043
+
1044
+ color = color.coefficients.value
1045
+ if color.size != points.size:
1046
+ # If the number of coefficients are different, just evaluate the color function at the locations of the coefficients of the function.
1047
+ color = color.evaluate(parametric_coordinates, non_csdl=True)
1048
+ else:
1049
+ raise ValueError("Invalid point type. Must be 'evaluated_points' or 'coefficients'.")
1050
+ # endregion Generate the points to plot
1051
+
1052
+ # Call general plot curve function to plot the points with the colors
1053
+ plotting_elements = pf.plot_curve(points=points, opacity=opacity, color=color, color_map=color_map, line_width=line_width,
1054
+ additional_plotting_elements=plotting_elements, show=show)
1055
+ return plotting_elements
1056
+
1057
+ def plot_surface(self, point_type:str='evaluated_points', plot_types:list=['function'], opacity:float=1., color:str|lfs.Function='#00629B',
1058
+ color_map:str='jet', surface_texture:str="", line_width:float=3., additional_plotting_elements:list=[], show:bool=True):
1059
+ '''
1060
+ Plots the function as a surface. NOTE: This should only be called if the function is a surface!
1061
+
1062
+ Parameters
1063
+ -----------
1064
+ points_type : str = 'evaluated_points'
1065
+ The type of points to be plotted. {evaluated_points, coefficients}
1066
+ plot_types : list = ['function']
1067
+ The type of plot {function, wireframe, point_cloud}
1068
+ opactity : float = 1.
1069
+ The opacity of the plot. 0 is fully transparent and 1 is fully opaque.
1070
+ color : str = '#00629B'
1071
+ The 6 digit color code to plot the function as. If a function is provided, the function will be used to color the surface.
1072
+ color_map : str = 'jet'
1073
+ The color map to use if the color is a function.
1074
+ surface_texture : str = ""
1075
+ The surface texture to determine how light bounces off the surface.
1076
+ This is kept for API compatibility.
1077
+ line_width : float = 3.
1078
+ The width of the lines if the plot type is wireframe.
1079
+ additional_plotting_elemets : list = []
1080
+ Plotting elements that may have been returned from previous plotting functions that should be plotted with this plot.
1081
+ show : bool = True
1082
+ A boolean on whether to show the plot or not. If the plot is not shown, the plotting element is returned.
1083
+
1084
+ Returns
1085
+ -------
1086
+ plotting_elements : list
1087
+ The plotting elements that were plotted.
1088
+ '''
1089
+ import lsdo_function_spaces.utils.plotting_functions as pf
1090
+ if self.space.num_parametric_dimensions != 2:
1091
+ raise ValueError("This function is not a surface and cannot be plotted as one.")
1092
+
1093
+ # Flatten nested lists to handle cases where users pass [plot_points_result]
1094
+ plotting_elements = pf._flatten_plotting_elements(additional_plotting_elements.copy())
1095
+ color_is_function = False
1096
+
1097
+ # region Generate the points to plot
1098
+ if point_type == 'evaluated_points':
1099
+ # num_points = 1000 # Generate meshgrid of parametric coordinates
1100
+ # num_points = 500 # Generate meshgrid of parametric coordinates
1101
+ # num_points = 200 # Generate meshgrid of parametric coordinates
1102
+ # num_points = 100 # Generate meshgrid of parametric coordinates
1103
+ num_points = 50 # Generate meshgrid of parametric coordinates
1104
+ mesh_grid_input = []
1105
+ for dimension_index in range(self.space.num_parametric_dimensions):
1106
+ mesh_grid_input.append(np.linspace(0., 1., num_points))
1107
+ parametric_coordinates_tuple = np.meshgrid(*mesh_grid_input, indexing='ij')
1108
+ # np.meshgrid returns a tuple of arrays; convert to list so we can reshape elements
1109
+ parametric_coordinates_tuple = [pc.reshape((-1, 1)) for pc in parametric_coordinates_tuple]
1110
+ parametric_coordinates = np.hstack(parametric_coordinates_tuple)
1111
+
1112
+ function_values = self.evaluate(parametric_coordinates, non_csdl=True).reshape((num_points,num_points,-1))
1113
+ if isinstance(function_values, csdl.Variable):
1114
+ function_values = function_values.value
1115
+ points = function_values
1116
+
1117
+ if isinstance(color, Function):
1118
+ color_is_function = True
1119
+ if color.space.num_parametric_dimensions != 2:
1120
+ raise ValueError("The color function must be 2D to plot as a surface.")
1121
+ color = color.evaluate(parametric_coordinates, non_csdl=True)
1122
+ color_max = np.max(color)
1123
+ color_min = np.min(color)
1124
+ if len(color.shape) > 1:
1125
+ if color.shape[1] > 1:
1126
+ color = np.linalg.norm(color, axis=1)
1127
+ elif point_type == 'coefficients':
1128
+ points = self.coefficients.value # Do I need to reshape this?
1129
+
1130
+ if isinstance(color, Function):
1131
+ if color.space.num_parametric_dimensions != 2:
1132
+ raise ValueError("The color function must be 2D to plot as a surface.")
1133
+
1134
+ color = color.coefficients.value
1135
+ if color.size != points.size:
1136
+ # If the number of coefficients are different, just evaluate the color function at the locations of the coefficients of the function.
1137
+ # Generate meshgrid of parametric coordinates
1138
+ mesh_grid_input = []
1139
+ for dimension_index in range(self.space.num_parametric_dimensions):
1140
+ mesh_grid_input.append(np.linspace(0., 1., self.coefficients.shape[dimension_index]))
1141
+ parametric_coordinates_tuple = np.meshgrid(*mesh_grid_input, indexing='ij')
1142
+ for dimensions_index in range(self.space.num_parametric_dimensions):
1143
+ parametric_coordinates_tuple[dimensions_index] = parametric_coordinates_tuple[dimensions_index].reshape((-1,1))
1144
+ parametric_coordinates = np.hstack(parametric_coordinates_tuple)
1145
+ color = color.evaluate(parametric_coordinates, non_csdl=True)
1146
+ else:
1147
+ raise ValueError("Invalid point type. Must be 'evaluated_points' or 'coefficients'.")
1148
+ # endregion Generate the points to plot
1149
+
1150
+ # Call general plot surface function to plot the points with the colors
1151
+ for plot_type in plot_types:
1152
+ if plot_type not in ['function', 'wireframe', 'point_cloud']:
1153
+ raise ValueError("Invalid plot type. Must be 'function', 'wireframe', or 'point_cloud'.")
1154
+ if plot_type == 'point_cloud':
1155
+ plotting_elements = pf.plot_points(points=points, opacity=opacity, color=color, color_map=color_map, size=10.,
1156
+ additional_plotting_elements=plotting_elements, show=False)
1157
+ elif plot_type in ['function', 'wireframe']:
1158
+ plotting_elements = pf.plot_surface(points=points, plot_types=[plot_type], opacity=opacity, color=color, color_map=color_map,
1159
+ surface_texture=surface_texture, line_width=line_width,
1160
+ additional_plotting_elements=plotting_elements, show=False)
1161
+ if show:
1162
+ if self.name is not None:
1163
+ pf.show_plot(plotting_elements, title=self.name, axes=1, interactive=True)
1164
+ else:
1165
+ pf.show_plot(plotting_elements, title="Surface", axes=1, interactive=True)
1166
+ if color_is_function:
1167
+ return plotting_elements, color_min, color_max
1168
+ return plotting_elements
1169
+
1170
+ def plot_volume(self, point_type:str='evaluated_points', plot_types:list=['function'], opacity:float=1., color:str|lfs.Function='#00629B',
1171
+ color_map:str='jet', surface_texture:str="", line_width:float=3., additional_plotting_elements:list=[], show:bool=True):
1172
+ '''
1173
+ Plots the function as a volume. NOTE: This should only be called if the function is a volume!
1174
+
1175
+ Parameters
1176
+ -----------
1177
+ points_type : str = 'evaluated_points'
1178
+ The type of points to be plotted. {evaluated_points, coefficients}
1179
+ plot_types : list = ['function']
1180
+ The type of plot {function}
1181
+ opactity : float = 1.
1182
+ The opacity of the plot. 0 is fully transparent and 1 is fully opaque.
1183
+ color : str = '#00629B'
1184
+ The 6 digit color code to plot the function as. If a function is provided, the function will be used to color the volume.
1185
+ color_map : str = 'jet'
1186
+ The color map to use if the color is a function.
1187
+ surface_texture : str = ""
1188
+ The surface texture to determine how light bounces off the surface.
1189
+ This is kept for API compatibility.
1190
+ line_width : float = 3.
1191
+ The width of the lines if the plot type is wireframe.
1192
+ additional_plotting_elemets : list = []
1193
+ Plotting elements that may have been returned from previous plotting functions that should be plotted with this plot.
1194
+ show : bool = True
1195
+ A boolean on whether to show the plot or not. If the plot is not shown, the plotting elements are still returned.
1196
+
1197
+ Returns
1198
+ -------
1199
+ plotting_elements : list
1200
+ The plotting elements that were plotted.
1201
+ '''
1202
+ import lsdo_function_spaces.utils.plotting_functions as pf
1203
+ if self.space.num_parametric_dimensions != 3:
1204
+ raise ValueError("This function is not a volume and cannot be plotted as one.")
1205
+
1206
+ # region Generate the points to plot
1207
+ if point_type == 'evaluated_points':
1208
+ num_points = 200
1209
+
1210
+ # Generate meshgrid of parametric coordinates
1211
+ linspace_dimension = np.linspace(0., 1., num_points)
1212
+ linspace_meshgrid = np.meshgrid(linspace_dimension, linspace_dimension)
1213
+ linspace_dimension1 = linspace_meshgrid[1].reshape((-1,1))
1214
+ linspace_dimension2 = linspace_meshgrid[0].reshape((-1,1))
1215
+ zeros_dimension = np.zeros((num_points**2,)).reshape((-1,1))
1216
+ ones_dimension = np.ones((num_points**2,)).reshape((-1,1))
1217
+
1218
+ parametric_coordinates = []
1219
+ parametric_coordinates.append(np.column_stack((linspace_dimension1, linspace_dimension2, zeros_dimension)))
1220
+ parametric_coordinates.append(np.column_stack((linspace_dimension1, linspace_dimension2, ones_dimension)))
1221
+ parametric_coordinates.append(np.column_stack((linspace_dimension1, zeros_dimension, linspace_dimension2)))
1222
+ parametric_coordinates.append(np.column_stack((linspace_dimension1, ones_dimension, linspace_dimension2)))
1223
+ parametric_coordinates.append(np.column_stack((zeros_dimension, linspace_dimension1, linspace_dimension2)))
1224
+ parametric_coordinates.append(np.column_stack((ones_dimension, linspace_dimension1, linspace_dimension2)))
1225
+
1226
+ points = []
1227
+ for parametric_coordinate_set in parametric_coordinates:
1228
+ points.append(self.evaluate(parametric_coordinates=parametric_coordinate_set, non_csdl=True).reshape((num_points,num_points,-1)))
1229
+
1230
+ plotting_colors = []
1231
+ if isinstance(color, Function):
1232
+ if color.space.num_parametric_dimensions != 3:
1233
+ raise ValueError("The color function must be 3D to plot as a volume.")
1234
+
1235
+ for parametric_coordinate_set in parametric_coordinates:
1236
+ plotting_colors.append(color.evaluate(parametric_coordinates=parametric_coordinate_set, non_csdl=True))
1237
+ color = plotting_colors
1238
+
1239
+ elif point_type == 'coefficients':
1240
+ points = []
1241
+ points.append(self.coefficients.value[0,:,:])
1242
+ points.append(self.coefficients.value[-1,:,:])
1243
+ points.append(self.coefficients.value[:,0,:])
1244
+ points.append(self.coefficients.value[:,-1,:])
1245
+ points.append(self.coefficients.value[:,:,0])
1246
+ points.append(self.coefficients.value[:,:,-1])
1247
+
1248
+ if isinstance(color, Function):
1249
+ if color.space.num_parametric_dimensions != 3:
1250
+ raise ValueError("The color function must be 3D to plot as a volume.")
1251
+
1252
+ color = color.coefficients.value
1253
+ if color.size != points.size:
1254
+ raise NotImplementedError("For volumes, please use evaluated points to plot or "
1255
+ + "use a color function that has the same structure of coefficients.")
1256
+ else:
1257
+ raise ValueError("Invalid point type. Must be 'evaluated_points' or 'coefficients'.")
1258
+ # endregion Generate the points to plot
1259
+
1260
+ # Call general plot volume function to plot the points with the colors
1261
+ # Flatten nested lists to handle cases where users pass [plot_points_result]
1262
+ plotting_elements = pf._flatten_plotting_elements(additional_plotting_elements.copy())
1263
+ for plot_type in plot_types:
1264
+ if plot_type not in ['function', 'wireframe', 'point_cloud']:
1265
+ raise ValueError("Invalid plot type. Must be 'function', 'wireframe', or 'point_cloud'.")
1266
+
1267
+ for i in range(6):
1268
+ if isinstance(color, list):
1269
+ plotting_color = color[i]
1270
+ else:
1271
+ plotting_color = color
1272
+
1273
+ if plot_type == 'point_cloud':
1274
+ plotting_elements = pf.plot_points(points=points[i].reshape((-1,self.coefficients.shape[-1])), color=plotting_color, size=10.,
1275
+ additional_plotting_elements=plotting_elements, show=False)
1276
+ elif plot_type in ['function', 'wireframe']:
1277
+ plotting_elements = pf.plot_surface(points=points[i], plot_types=[plot_type], opacity=opacity, color=plotting_color,
1278
+ color_map=color_map, surface_texture=surface_texture, line_width=line_width,
1279
+ additional_plotting_elements=plotting_elements, show=False)
1280
+
1281
+ if show:
1282
+ if self.name is not None:
1283
+ pf.show_plot(plotting_elements, title=self.name, axes=1, interactive=True)
1284
+ else:
1285
+ pf.show_plot(plotting_elements, title="Volume", axes=1, interactive=True)
1286
+ return plotting_elements
1287
+
1288
+ def generate_triangulation(): pass
1289
+
1290
+ def __add__(self, other:Function) -> Function:
1291
+ return lfs.operations.add(self, other)
1292
+
1293
+ def __radd__(self, other:Function) -> Function:
1294
+ return lfs.operations.add(self, other)
1295
+
1296
+ def __sub__(self, other:Function) -> Function:
1297
+ return lfs.operations.sub(self, other)
1298
+
1299
+ def __rsub__(self, other:Function) -> Function:
1300
+ return lfs.operations.sub(other, self)
1301
+
1302
+ def __mul__(self, other:Function) -> Function:
1303
+ return lfs.operations.mult(self, other)
1304
+
1305
+ def __rmul__(self, other:Function) -> Function:
1306
+ return lfs.operations.mult(self, other)
1307
+
1308
+ def __truediv__(self, other:Function) -> Function:
1309
+ return lfs.operations.div(self, other)
1310
+
1311
+ def __rtruediv__(self, other:Function) -> Function:
1312
+ return lfs.operations.div(other, self)
1313
+
1314
+ def __pow__(self, other:Function) -> Function:
1315
+ return lfs.operations.power(self, other)
1316
+
1317
+ def __rpow__(self, other:Function) -> Function:
1318
+ return lfs.operations.power(other, self)
1319
+
1320
+ def __neg__(self) -> Function:
1321
+ return lfs.operations.negate(self)
1322
+