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