lsdo-function-spaces 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lsdo_function_spaces/__init__.py +64 -0
- lsdo_function_spaces/core/__init__.py +0 -0
- lsdo_function_spaces/core/function.py +1322 -0
- lsdo_function_spaces/core/function_set.py +1081 -0
- lsdo_function_spaces/core/function_set_space.py +379 -0
- lsdo_function_spaces/core/function_space.py +482 -0
- lsdo_function_spaces/core/operations/__init__.py +0 -0
- lsdo_function_spaces/core/operations/basic_ops.py +85 -0
- lsdo_function_spaces/core/operations/operations.py +5 -0
- lsdo_function_spaces/core/optimization.py +183 -0
- lsdo_function_spaces/core/spaces/__init__.py +0 -0
- lsdo_function_spaces/core/spaces/b_spline_space.py +418 -0
- lsdo_function_spaces/core/spaces/conditional_space.py +65 -0
- lsdo_function_spaces/core/spaces/constant_space.py +57 -0
- lsdo_function_spaces/core/spaces/idw_space.py +271 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/__init__.py +0 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_csdl_custom_ops.py +420 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection.py +1022 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection_non_differentiable.py +186 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection_optimized.py +594 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_space_new.py +6 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax.py +172 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax_factory.py +382 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax_stencil.py +451 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_numpy.py +249 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_numpy_factory.py +391 -0
- lsdo_function_spaces/core/spaces/operation_space.py +64 -0
- lsdo_function_spaces/core/spaces/polynomial_space.py +79 -0
- lsdo_function_spaces/core/spaces/rbf_space.py +136 -0
- lsdo_function_spaces/core/spaces/tri_space.py +256 -0
- lsdo_function_spaces/utils/__init__.py +0 -0
- lsdo_function_spaces/utils/file_io.py +484 -0
- lsdo_function_spaces/utils/internal_utilities.py +11 -0
- lsdo_function_spaces/utils/plotting_functions.py +357 -0
- lsdo_function_spaces/utils/utility_functions.py +148 -0
- lsdo_function_spaces-1.0.0.dist-info/METADATA +189 -0
- lsdo_function_spaces-1.0.0.dist-info/RECORD +40 -0
- lsdo_function_spaces-1.0.0.dist-info/WHEEL +5 -0
- lsdo_function_spaces-1.0.0.dist-info/licenses/LICENSE.txt +165 -0
- lsdo_function_spaces-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
|