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,357 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pyvista as pv
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _normalize_points(points: np.ndarray) -> np.ndarray:
|
|
10
|
+
if points.shape[-1] > 3:
|
|
11
|
+
raise ValueError(
|
|
12
|
+
'The points must have 3 or fewer physical dimensions (the size of the last axis).'
|
|
13
|
+
f' The provided points have {points.shape[-1]} physical dimensions. You probably want to reshape.'
|
|
14
|
+
)
|
|
15
|
+
points = points.reshape((points.size // points.shape[-1], points.shape[-1]))
|
|
16
|
+
if points.shape[-1] == 1:
|
|
17
|
+
points = np.hstack((points, np.zeros((points.shape[0], 2))))
|
|
18
|
+
elif points.shape[-1] == 2:
|
|
19
|
+
points = np.hstack((points, np.zeros((points.shape[0], 1))))
|
|
20
|
+
return points
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _normalize_grid(points: np.ndarray) -> np.ndarray:
|
|
24
|
+
if points.shape[-1] > 3:
|
|
25
|
+
raise ValueError(
|
|
26
|
+
'The points must have 3 or fewer physical dimensions (the size of the last axis).'
|
|
27
|
+
f' The provided points have {points.shape[-1]} physical dimensions. You probably want to reshape.'
|
|
28
|
+
)
|
|
29
|
+
if points.shape[-1] == 1:
|
|
30
|
+
zeros = np.zeros(points.shape[:-1] + (2,))
|
|
31
|
+
points = np.concatenate((points, zeros), axis=-1)
|
|
32
|
+
elif points.shape[-1] == 2:
|
|
33
|
+
zeros = np.zeros(points.shape[:-1] + (1,))
|
|
34
|
+
points = np.concatenate((points, zeros), axis=-1)
|
|
35
|
+
return points
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _extract_scalars(color: np.ndarray, num_points: int):
|
|
39
|
+
if color.ndim == 1:
|
|
40
|
+
if color.shape[0] != num_points:
|
|
41
|
+
raise ValueError("Color array length must match number of points.")
|
|
42
|
+
return color, False
|
|
43
|
+
if color.ndim == 2 and color.shape[0] == num_points:
|
|
44
|
+
if color.shape[1] == 1:
|
|
45
|
+
return color.reshape(-1), False
|
|
46
|
+
if color.shape[1] == 3:
|
|
47
|
+
return color, True
|
|
48
|
+
flattened = color.reshape(-1)
|
|
49
|
+
if flattened.size == num_points:
|
|
50
|
+
return flattened, False
|
|
51
|
+
if flattened.size == num_points * 3:
|
|
52
|
+
return flattened.reshape((num_points, 3)), True
|
|
53
|
+
raise ValueError("Color array size does not match number of points.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _make_plot_element(mesh, **kwargs) -> dict:
|
|
57
|
+
return {"mesh": mesh, "kwargs": kwargs}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _flatten_plotting_elements(elements: list) -> list:
|
|
61
|
+
"""Recursively flatten nested lists of plotting elements."""
|
|
62
|
+
flattened = []
|
|
63
|
+
for item in elements:
|
|
64
|
+
if isinstance(item, list):
|
|
65
|
+
flattened.extend(_flatten_plotting_elements(item))
|
|
66
|
+
else:
|
|
67
|
+
flattened.append(item)
|
|
68
|
+
return flattened
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def show_plot(plotting_elements:list, title:str, axes:bool=True, view_up:str="z", interactive:bool=True, camera:dict={}, screenshot:str=""):
|
|
72
|
+
'''
|
|
73
|
+
Shows the plot.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
-----------
|
|
77
|
+
plotting_elements : list
|
|
78
|
+
The list of PyVista plotting elements to plot.
|
|
79
|
+
title : str
|
|
80
|
+
The title of the plot.
|
|
81
|
+
axes : bool = True
|
|
82
|
+
A boolean on whether to show the axes or not.
|
|
83
|
+
viewup : str = "z"
|
|
84
|
+
The direction of the view up.
|
|
85
|
+
interactive : bool = True
|
|
86
|
+
A boolean on whether the plot is interactive or not.
|
|
87
|
+
'''
|
|
88
|
+
plotter = pv.Plotter()
|
|
89
|
+
if axes:
|
|
90
|
+
plotter.show_axes()
|
|
91
|
+
|
|
92
|
+
# Flatten nested lists to handle cases where users pass [plot_points_result]
|
|
93
|
+
plotting_elements = _flatten_plotting_elements(plotting_elements)
|
|
94
|
+
|
|
95
|
+
for element in plotting_elements:
|
|
96
|
+
if isinstance(element, dict) and "mesh" in element:
|
|
97
|
+
mesh = element["mesh"]
|
|
98
|
+
kwargs = element.get("kwargs", {})
|
|
99
|
+
plotter.add_mesh(mesh, **kwargs)
|
|
100
|
+
elif isinstance(element, tuple) and len(element) == 2:
|
|
101
|
+
mesh, kwargs = element
|
|
102
|
+
plotter.add_mesh(mesh, **kwargs)
|
|
103
|
+
elif isinstance(element, pv.Actor):
|
|
104
|
+
plotter.add_actor(element)
|
|
105
|
+
elif isinstance(element, pv.DataSet):
|
|
106
|
+
plotter.add_mesh(element)
|
|
107
|
+
|
|
108
|
+
if view_up:
|
|
109
|
+
view_map = {"x": (1, 0, 0), "y": (0, 1, 0), "z": (0, 0, 1)}
|
|
110
|
+
if isinstance(view_up, str):
|
|
111
|
+
view_up = view_map.get(view_up)
|
|
112
|
+
if view_up is not None:
|
|
113
|
+
plotter.camera.SetViewUp(*view_up)
|
|
114
|
+
|
|
115
|
+
if camera:
|
|
116
|
+
camera_obj = plotter.camera
|
|
117
|
+
if "position" in camera:
|
|
118
|
+
camera_obj.position = camera["position"]
|
|
119
|
+
if "focal_point" in camera:
|
|
120
|
+
camera_obj.focal_point = camera["focal_point"]
|
|
121
|
+
if "viewup" in camera:
|
|
122
|
+
viewup = camera["viewup"]
|
|
123
|
+
if isinstance(viewup, str):
|
|
124
|
+
viewup = {"x": (1, 0, 0), "y": (0, 1, 0), "z": (0, 0, 1)}.get(viewup)
|
|
125
|
+
if viewup is not None:
|
|
126
|
+
camera_obj.SetViewUp(*viewup)
|
|
127
|
+
if "distance" in camera:
|
|
128
|
+
camera_obj.distance = camera["distance"]
|
|
129
|
+
|
|
130
|
+
if screenshot:
|
|
131
|
+
plotter.show(title=title, interactive=interactive, screenshot=screenshot)
|
|
132
|
+
else:
|
|
133
|
+
plotter.show(title=title, interactive=interactive)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def plot_points(points:np.ndarray, opacity:float=1., color:Union[str, np.ndarray]='#00629B', color_map:str='jet', size=6.,
|
|
137
|
+
additional_plotting_elements:list=[], show:bool=True):
|
|
138
|
+
'''
|
|
139
|
+
Plots a point cloud.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
-----------
|
|
143
|
+
points : np.ndarray
|
|
144
|
+
The points to plot.
|
|
145
|
+
opactity : float = 1.
|
|
146
|
+
The opacity of the plot. 0 is fully transparent and 1 is fully opaque.
|
|
147
|
+
color : str | np.ndarray = '#00629B'
|
|
148
|
+
The 6 digit color code to plot the points as. A numpy array of colors can be provided to color the points individually according to a cmap.
|
|
149
|
+
color_map : str = 'jet'
|
|
150
|
+
The color map to use if the color is a numpy array.
|
|
151
|
+
size : float = 6.
|
|
152
|
+
The size (radius) of the points.
|
|
153
|
+
additional_plotting_elemets : list = []
|
|
154
|
+
PyVista plotting elements that may have been returned from previous plotting functions that should be plotted with this plot.
|
|
155
|
+
show : bool = True
|
|
156
|
+
A boolean on whether to show the plot or not. If the plot is not shown, the plotting element is still returned.
|
|
157
|
+
'''
|
|
158
|
+
plotting_elements = _flatten_plotting_elements(additional_plotting_elements.copy())
|
|
159
|
+
|
|
160
|
+
original_dim = points.shape[-1]
|
|
161
|
+
original_dim = points.shape[-1]
|
|
162
|
+
points = _normalize_points(points)
|
|
163
|
+
plotting_points = pv.PolyData(points)
|
|
164
|
+
kwargs = dict(opacity=opacity, point_size=size, render_points_as_spheres=True)
|
|
165
|
+
if isinstance(color, str):
|
|
166
|
+
kwargs["color"] = color
|
|
167
|
+
elif isinstance(color, np.ndarray):
|
|
168
|
+
scalars, rgb = _extract_scalars(color, points.shape[0])
|
|
169
|
+
kwargs["scalars"] = scalars
|
|
170
|
+
kwargs["cmap"] = color_map
|
|
171
|
+
kwargs["rgb"] = rgb
|
|
172
|
+
|
|
173
|
+
plotting_elements.append(_make_plot_element(plotting_points, **kwargs))
|
|
174
|
+
|
|
175
|
+
if points.shape[-1] == 3:
|
|
176
|
+
view_up = "z"
|
|
177
|
+
else:
|
|
178
|
+
view_up = "y"
|
|
179
|
+
|
|
180
|
+
if show:
|
|
181
|
+
view_up = "z" if original_dim == 3 else "y"
|
|
182
|
+
show_plot(plotting_elements, 'Points', axes=1, view_up=view_up, interactive=True)
|
|
183
|
+
return plotting_elements
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def plot_curve(points:np.ndarray, opacity:float=1., color:Union[str, np.ndarray]='#00629B', color_map:str='jet', line_width:float=3.,
|
|
188
|
+
additional_plotting_elements:list=[], show:bool=True):
|
|
189
|
+
'''
|
|
190
|
+
Plots the B-spline Surface.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
-----------
|
|
194
|
+
points : np.ndarray -- shape=(num_points, num_physical_dimensions)
|
|
195
|
+
The points of the curve to be plotted.
|
|
196
|
+
opactity : float
|
|
197
|
+
The opacity of the plot. 0 is fully transparent and 1 is fully opaque.
|
|
198
|
+
color : str = '#00629B'
|
|
199
|
+
The 6 digit color code to plot the curve as. A numpy array of colors can be provided to color the points individually according to a cmap.
|
|
200
|
+
color_map : str = 'jet
|
|
201
|
+
The color map to use if the color is a numpy array.
|
|
202
|
+
additional_plotting_elemets : list
|
|
203
|
+
PyVista plotting elements that may have been returned from previous plotting functions that should be plotted with this plot.
|
|
204
|
+
show : bool
|
|
205
|
+
A boolean on whether to show the plot or not. If the plot is not shown, the plotting element is returned.
|
|
206
|
+
'''
|
|
207
|
+
# NOTE: The function object performs the evaluation(s) to get the points (and colors if applicable) and then these functions do the plotting.
|
|
208
|
+
|
|
209
|
+
plotting_elements = _flatten_plotting_elements(additional_plotting_elements.copy())
|
|
210
|
+
|
|
211
|
+
points = _normalize_points(points)
|
|
212
|
+
plotting_line = pv.lines_from_points(points, close=False)
|
|
213
|
+
kwargs = dict(opacity=opacity, line_width=line_width, render_lines_as_tubes=True)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if isinstance(color, str):
|
|
217
|
+
kwargs["color"] = color
|
|
218
|
+
elif isinstance(color, np.ndarray):
|
|
219
|
+
scalars, rgb = _extract_scalars(color, points.shape[0])
|
|
220
|
+
kwargs["scalars"] = scalars
|
|
221
|
+
kwargs["cmap"] = color_map
|
|
222
|
+
kwargs["rgb"] = rgb
|
|
223
|
+
|
|
224
|
+
plotting_elements.append(_make_plot_element(plotting_line, **kwargs))
|
|
225
|
+
|
|
226
|
+
if show:
|
|
227
|
+
if original_dim < 3:
|
|
228
|
+
view_up = "y"
|
|
229
|
+
else:
|
|
230
|
+
view_up = "z"
|
|
231
|
+
# plotter.show(plotting_elements, f'B-spline Curve', axes=1, view_up=view_up, interactive=True)
|
|
232
|
+
show_plot(plotting_elements, 'Curve', axes=1, view_up=view_up, interactive=True)
|
|
233
|
+
return plotting_elements
|
|
234
|
+
|
|
235
|
+
return plotting_elements
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def plot_surface(points:np.ndarray, plot_types:list=['function'], opacity:float=1.,
|
|
239
|
+
color:Union[str, np.ndarray]='#00629B', color_map:str='jet', surface_texture:str="",
|
|
240
|
+
line_width:float=3., additional_plotting_elements:list=[], show:bool=True):
|
|
241
|
+
'''
|
|
242
|
+
Plots the B-spline Surface.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
-----------
|
|
246
|
+
points : np.ndarray -- shape=(num_points_u, num_points_v, num_physical_dimensions)
|
|
247
|
+
The type of points to be plotted. {evaluated_points, coefficients}
|
|
248
|
+
plot_types : list
|
|
249
|
+
The type of plot {function, wireframe}
|
|
250
|
+
opactity : float
|
|
251
|
+
The opacity of the plot. 0 is fully transparent and 1 is fully opaque.
|
|
252
|
+
color : str = '#00629B'
|
|
253
|
+
The 6 digit color code to plot the surface as. A numpy array of colors can be provided to color the points individually according to a cmap.
|
|
254
|
+
color_map : str = 'jet'
|
|
255
|
+
The color map to use if the color is a numpy array.
|
|
256
|
+
surface_texture : str = "" {"metallic", "glossy", ...}, optional
|
|
257
|
+
The surface texture to determine how light bounces off the surface.
|
|
258
|
+
This is kept for API compatibility.
|
|
259
|
+
additional_plotting_elemets : list
|
|
260
|
+
PyVista plotting elements that may have been returned from previous plotting functions that should be plotted with this plot.
|
|
261
|
+
show : bool
|
|
262
|
+
A boolean on whether to show the plot or not. If the plot is not shown, the plotting element is returned.
|
|
263
|
+
'''
|
|
264
|
+
plotting_elements = _flatten_plotting_elements(additional_plotting_elements.copy())
|
|
265
|
+
|
|
266
|
+
num_plot_u = points.shape[0]
|
|
267
|
+
num_plot_v = points.shape[1]
|
|
268
|
+
original_dim = points.shape[-1]
|
|
269
|
+
|
|
270
|
+
import csdl_alpha as csdl
|
|
271
|
+
if isinstance(points, csdl.Variable):
|
|
272
|
+
points = points.value
|
|
273
|
+
|
|
274
|
+
points = _normalize_grid(points)
|
|
275
|
+
x = points[:, :, 0]
|
|
276
|
+
y = points[:, :, 1]
|
|
277
|
+
z = points[:, :, 2]
|
|
278
|
+
mesh = pv.StructuredGrid(x, y, z)
|
|
279
|
+
|
|
280
|
+
num_points = num_plot_u * num_plot_v
|
|
281
|
+
scalar_kwargs = {}
|
|
282
|
+
if isinstance(color, np.ndarray):
|
|
283
|
+
color_values = color
|
|
284
|
+
if color_values.shape[:2] == (num_plot_u, num_plot_v):
|
|
285
|
+
color_values = color_values.reshape((num_points, -1)) if color_values.ndim > 2 else color_values.reshape(-1)
|
|
286
|
+
scalars, rgb = _extract_scalars(color_values, num_points)
|
|
287
|
+
scalar_kwargs = {"scalars": scalars, "cmap": color_map, "rgb": rgb}
|
|
288
|
+
|
|
289
|
+
if 'function' in plot_types:
|
|
290
|
+
kwargs = dict(opacity=opacity)
|
|
291
|
+
if isinstance(color, str):
|
|
292
|
+
kwargs["color"] = color
|
|
293
|
+
else:
|
|
294
|
+
kwargs.update(scalar_kwargs)
|
|
295
|
+
plotting_elements.append(_make_plot_element(mesh, **kwargs))
|
|
296
|
+
if 'wireframe' in plot_types:
|
|
297
|
+
kwargs = dict(opacity=opacity, style="wireframe", line_width=line_width)
|
|
298
|
+
if isinstance(color, str):
|
|
299
|
+
kwargs["color"] = color
|
|
300
|
+
else:
|
|
301
|
+
kwargs.update(scalar_kwargs)
|
|
302
|
+
plotting_elements.append(_make_plot_element(mesh, **kwargs))
|
|
303
|
+
|
|
304
|
+
if show:
|
|
305
|
+
if original_dim < 3:
|
|
306
|
+
view_up = "y"
|
|
307
|
+
else:
|
|
308
|
+
view_up = "z"
|
|
309
|
+
show_plot(plotting_elements, 'Surface', axes=1, view_up=view_up, interactive=True)
|
|
310
|
+
|
|
311
|
+
return plotting_elements
|
|
312
|
+
|
|
313
|
+
def get_surface_mesh(surface, color=None, grid_n=50, offset=0):
|
|
314
|
+
import lsdo_function_spaces as fs
|
|
315
|
+
surface:fs.Function = surface
|
|
316
|
+
|
|
317
|
+
# Generate meshgrid of parametric coordinates
|
|
318
|
+
mesh_grid_input = []
|
|
319
|
+
for dimension_index in range(2):
|
|
320
|
+
mesh_grid_input.append(np.linspace(0., 1., grid_n))
|
|
321
|
+
parametric_coordinates_tuple = np.meshgrid(*mesh_grid_input, indexing='ij')
|
|
322
|
+
for dimensions_index in range(2):
|
|
323
|
+
parametric_coordinates_tuple[dimensions_index] = parametric_coordinates_tuple[dimensions_index].reshape((-1,1))
|
|
324
|
+
grid = np.hstack(parametric_coordinates_tuple)
|
|
325
|
+
|
|
326
|
+
# grid = surface.space.generate_parametric_grid(grid_n)
|
|
327
|
+
points = surface.evaluate(grid, non_csdl=True).reshape((grid_n, grid_n, surface.num_physical_dimensions))
|
|
328
|
+
vertices = []
|
|
329
|
+
faces = []
|
|
330
|
+
for u_index in range(grid_n):
|
|
331
|
+
for v_index in range(grid_n):
|
|
332
|
+
vertex = tuple(points[u_index, v_index, :])
|
|
333
|
+
vertices.append(vertex)
|
|
334
|
+
if u_index != 0 and v_index != 0:
|
|
335
|
+
face = tuple((
|
|
336
|
+
(u_index-1)*grid_n+(v_index-1)+offset,
|
|
337
|
+
(u_index-1)*grid_n+(v_index)+offset,
|
|
338
|
+
(u_index)*grid_n+(v_index)+offset,
|
|
339
|
+
(u_index)*grid_n+(v_index-1)+offset,
|
|
340
|
+
))
|
|
341
|
+
faces.append(face)
|
|
342
|
+
if color is not None:
|
|
343
|
+
c_points = color.evaluate(grid, non_csdl=True)
|
|
344
|
+
if len(c_points.shape) > 1:
|
|
345
|
+
if c_points.shape[1] > 1:
|
|
346
|
+
c_points = np.linalg.norm(c_points, axis=1)
|
|
347
|
+
return vertices, faces, c_points
|
|
348
|
+
return vertices, faces
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def make_scalar_bar_element(color_min: float, color_max: float, color_map: str = "jet"):
|
|
352
|
+
points = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]])
|
|
353
|
+
mesh = pv.PolyData(points)
|
|
354
|
+
scalars = np.array([color_min, color_max])
|
|
355
|
+
kwargs = dict(scalars=scalars, cmap=color_map, show_scalar_bar=True, opacity=0.0)
|
|
356
|
+
return _make_plot_element(mesh, **kwargs)
|
|
357
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numpy.typing as npt
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import lsdo_function_spaces as lfs
|
|
5
|
+
|
|
6
|
+
def create_b_spline_from_corners(corners:npt.NDArray[np.float64], degree:tuple[int,...]=(3,), num_coefficients:tuple[int,...]=(10,), knot_vectors:Optional[tuple[npt.NDArray[np.float64]]]=None,
|
|
7
|
+
name:str='b_spline_hyper_volume') -> lfs.Function:
|
|
8
|
+
'''
|
|
9
|
+
Creates a B-Spline volume from a set of corners.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
corners : np.ndarray
|
|
14
|
+
The corners of the hyper-volume. The shape of the corners array should be (nu1, nu2, nu3, ..., num_physical_dimensions)
|
|
15
|
+
where u_{i} corresponds to number of corners along a parametric dimensions.
|
|
16
|
+
degree : tuple
|
|
17
|
+
The degree of the B-Spline in each dimension.
|
|
18
|
+
num_coefficients : tuple
|
|
19
|
+
The number of coefficients between each corner in each dimension.
|
|
20
|
+
knot_vectors : tuple
|
|
21
|
+
The knot vectors for each dimension. If None, then open uniform knot vectors are generated.
|
|
22
|
+
name : str = 'b_spline_hyper_volume'
|
|
23
|
+
The name of the B-Spline.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
b_spline : lfs.Function
|
|
28
|
+
The B-Spline hyper-volume.
|
|
29
|
+
'''
|
|
30
|
+
|
|
31
|
+
num_dimensions = len(corners.shape)-1
|
|
32
|
+
|
|
33
|
+
if isinstance(degree, int):
|
|
34
|
+
degree = (degree,)*num_dimensions
|
|
35
|
+
if len(degree) != num_dimensions:
|
|
36
|
+
degree = tuple(np.tile(degree, num_dimensions))
|
|
37
|
+
if isinstance(num_coefficients, int):
|
|
38
|
+
num_coefficients = (num_coefficients,)*num_dimensions
|
|
39
|
+
if len(num_coefficients) != num_dimensions:
|
|
40
|
+
num_coefficients = tuple(np.tile(num_coefficients, num_dimensions))
|
|
41
|
+
if knot_vectors is not None:
|
|
42
|
+
if len(knot_vectors) != num_dimensions:
|
|
43
|
+
knot_vectors = tuple(np.tile(knot_vectors, num_dimensions))
|
|
44
|
+
|
|
45
|
+
total_knot_vector = []
|
|
46
|
+
for knot_vector in knot_vectors:
|
|
47
|
+
total_knot_vector.append(knot_vector)
|
|
48
|
+
knot_vectors = np.hstack(total_knot_vector)
|
|
49
|
+
else:
|
|
50
|
+
knot_vectors = None # Just let the B-spline space generate the knot vectors.
|
|
51
|
+
|
|
52
|
+
# Build up hyper-volume based on corners given
|
|
53
|
+
previous_dimension_hyper_volume = corners
|
|
54
|
+
dimension_hyper_volumes = corners.copy()
|
|
55
|
+
for dimension_index in np.arange(num_dimensions, 0, -1)-1:
|
|
56
|
+
dimension_hyper_volumes_shape = np.array(previous_dimension_hyper_volume.shape)
|
|
57
|
+
dimension_num_hyper_volumes = dimension_hyper_volumes_shape[dimension_index]-1
|
|
58
|
+
dimension_hyper_volumes_shape[dimension_index] = dimension_num_hyper_volumes * (num_coefficients[dimension_index]-1) + 1
|
|
59
|
+
dimension_hyper_volumes_shape = tuple(dimension_hyper_volumes_shape)
|
|
60
|
+
dimension_hyper_volumes = np.zeros(dimension_hyper_volumes_shape)
|
|
61
|
+
|
|
62
|
+
# Move dimension index to front so we can index the correct dimension
|
|
63
|
+
linspace_index_front = np.moveaxis(dimension_hyper_volumes, dimension_index, 0)
|
|
64
|
+
previous_index_front = np.moveaxis(previous_dimension_hyper_volume, dimension_index, 0)
|
|
65
|
+
include_endpoint = False
|
|
66
|
+
# Perform interpolations
|
|
67
|
+
for dimension_level_index in range(previous_dimension_hyper_volume.shape[dimension_index]-1):
|
|
68
|
+
if dimension_level_index == previous_dimension_hyper_volume.shape[dimension_index]-2: # last hyper-volume/segment along dimension
|
|
69
|
+
include_endpoint = True
|
|
70
|
+
dimension_hyper_volume_num_sections = num_coefficients[dimension_index]
|
|
71
|
+
linspace_index_front[dimension_level_index*(dimension_hyper_volume_num_sections-1):] = \
|
|
72
|
+
np.linspace(previous_index_front[dimension_level_index], previous_index_front[dimension_level_index+1],
|
|
73
|
+
dimension_hyper_volume_num_sections, endpoint=include_endpoint)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
dimension_hyper_volume_num_sections = num_coefficients[dimension_index] - 1
|
|
77
|
+
linspace_index_front[dimension_level_index*dimension_hyper_volume_num_sections:
|
|
78
|
+
(dimension_level_index+1)*dimension_hyper_volume_num_sections] = \
|
|
79
|
+
np.linspace(previous_index_front[dimension_level_index], previous_index_front[dimension_level_index+1],
|
|
80
|
+
dimension_hyper_volume_num_sections, endpoint=include_endpoint)
|
|
81
|
+
# Move axis back to proper location
|
|
82
|
+
dimension_hyper_volumes = np.moveaxis(linspace_index_front, 0, dimension_index)
|
|
83
|
+
previous_dimension_hyper_volume = dimension_hyper_volumes.copy()
|
|
84
|
+
|
|
85
|
+
b_spline_space = lfs.BSplineSpace(num_parametric_dimensions=num_dimensions, degree=degree,
|
|
86
|
+
coefficients_shape=dimension_hyper_volumes.shape[:-1], knots=knot_vectors)
|
|
87
|
+
b_spline = lfs.Function(space=b_spline_space, coefficients=dimension_hyper_volumes, name=name)
|
|
88
|
+
|
|
89
|
+
return b_spline
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_enclosure_block(points:np.ndarray, num_coefficients:tuple[int], degree:tuple[int], knot_vectors:tuple=None,
|
|
93
|
+
num_parametric_dimensions:int=3, name:str='hyper_volume') -> lfs.Function:
|
|
94
|
+
'''
|
|
95
|
+
Creates an nd volume that tightly fits around a set of entities.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
points: np.ndarray
|
|
100
|
+
The points that the hyper-volume should enclose. The shape of the points array should be (num_points, num_physical_dimensions).
|
|
101
|
+
num_coefficients: tuple[int]
|
|
102
|
+
The number of coefficients in each dimension.
|
|
103
|
+
degree: tuple[int]
|
|
104
|
+
The degree of the B-Spline in each dimension.
|
|
105
|
+
knot_vectors: tuple = None
|
|
106
|
+
The knot vectors for each dimension. If None, then open uniform knot vectors are generated.
|
|
107
|
+
num_parametric_dimensions: int = 3
|
|
108
|
+
The number of parametric dimensions.
|
|
109
|
+
name: str = 'hyper_volume'
|
|
110
|
+
The name of the hyper-volume.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
hyper_volume: lfs.Function
|
|
115
|
+
The hyper-volume that encloses the points.
|
|
116
|
+
'''
|
|
117
|
+
if isinstance(num_coefficients, int):
|
|
118
|
+
num_coefficients = (num_coefficients,)*num_parametric_dimensions
|
|
119
|
+
if isinstance(degree, int):
|
|
120
|
+
degree = (degree,)*num_parametric_dimensions
|
|
121
|
+
|
|
122
|
+
num_physical_dimensions = points.shape[-1]
|
|
123
|
+
|
|
124
|
+
mins = np.min(points.reshape((-1,num_physical_dimensions)), axis=0).reshape((-1,1))
|
|
125
|
+
maxs = np.max(points.reshape((-1,num_physical_dimensions)), axis=0).reshape((-1,1))
|
|
126
|
+
|
|
127
|
+
mins_and_maxs = np.hstack((mins, maxs))
|
|
128
|
+
|
|
129
|
+
corners_shape = (2,)*num_parametric_dimensions + (num_physical_dimensions,)
|
|
130
|
+
corners = np.zeros(corners_shape)
|
|
131
|
+
corners_flattened = np.zeros((np.prod(corners_shape),))
|
|
132
|
+
physical_dimension_index = 0
|
|
133
|
+
for i in range(len(corners_flattened)):
|
|
134
|
+
parametric_dimension_counter = int(i/num_physical_dimensions)
|
|
135
|
+
binary_parametric_dimension_counter = bin(parametric_dimension_counter)[2:].zfill(num_physical_dimensions)
|
|
136
|
+
min_or_max = int(binary_parametric_dimension_counter[physical_dimension_index])
|
|
137
|
+
corners_flattened[i] = mins_and_maxs[physical_dimension_index, min_or_max]
|
|
138
|
+
|
|
139
|
+
physical_dimension_index += 1
|
|
140
|
+
if physical_dimension_index == num_physical_dimensions:
|
|
141
|
+
physical_dimension_index = 0
|
|
142
|
+
|
|
143
|
+
corners = corners_flattened.reshape(corners_shape)
|
|
144
|
+
|
|
145
|
+
hyper_volume = create_b_spline_from_corners(name=name, corners=corners, degree=degree, num_coefficients=num_coefficients,
|
|
146
|
+
knot_vectors=knot_vectors)
|
|
147
|
+
|
|
148
|
+
return hyper_volume
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lsdo_function_spaces
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A package for functional representation and function spaces in multidisciplinary design optimization.
|
|
5
|
+
Author-email: Andrew Fletcher <afletcher168@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Documentation, https://lsdo-function-spaces.readthedocs.io/en/latest/
|
|
8
|
+
Project-URL: Repository, https://github.com/LSDOlab/lsdo_function_spaces
|
|
9
|
+
Project-URL: Issue Tracker, https://github.com/LSDOlab/lsdo_function_spaces/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE.txt
|
|
22
|
+
Requires-Dist: numpy
|
|
23
|
+
Requires-Dist: scipy
|
|
24
|
+
Requires-Dist: pyvista
|
|
25
|
+
Requires-Dist: joblib
|
|
26
|
+
Requires-Dist: pandas
|
|
27
|
+
Requires-Dist: scikit-learn
|
|
28
|
+
Requires-Dist: jax
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest; extra == "test"
|
|
31
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Requires-Dist: myst-nb; extra == "docs"
|
|
34
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
35
|
+
Requires-Dist: sphinx_rtd_theme; extra == "docs"
|
|
36
|
+
Requires-Dist: sphinx-copybutton; extra == "docs"
|
|
37
|
+
Requires-Dist: sphinx-autoapi>=3.0; extra == "docs"
|
|
38
|
+
Requires-Dist: numpydoc; extra == "docs"
|
|
39
|
+
Requires-Dist: gitpython; extra == "docs"
|
|
40
|
+
Requires-Dist: sphinxcontrib-bibtex; extra == "docs"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# lsdo_function_spaces
|
|
44
|
+
|
|
45
|
+
[](https://lsdo-function-spaces.readthedocs.io/en/latest/?badge=latest)
|
|
46
|
+
[](https://github.com/LSDOlab/lsdo_function_spaces/actions)
|
|
47
|
+

|
|
48
|
+
[](LICENSE.txt)
|
|
49
|
+
|
|
50
|
+
**lsdo_function_spaces** is a pure-Python library for constructing continuous, high-dimensional, differentiable function representations (B-splines, tensor-product splines, scattered data spaces, and multivariate polynomials) tailored for gradient-based Multidisciplinary Design Optimization (MDO) and scientific computing.
|
|
51
|
+
|
|
52
|
+
Developed by the [Large-Scale Design Optimization (LSDO) Lab](https://lsdo.eng.ucsd.edu/) at the University of California, San Diego.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Key Features
|
|
57
|
+
|
|
58
|
+
* **Differentiable Function Spaces**: Unifies Cox-de Boor B-spline curves, surfaces, and volumes, Shepard inverse distance weighting (IDW), radial basis functions (RBF), and multivariate polynomials under an extensible functional abstraction.
|
|
59
|
+
* **100% Pure Python & JAX Accelerated**: Zero Cython or C compiler dependencies. High-performance vectorized evaluation and projection algorithms implemented cleanly in NumPy with optional JAX JIT acceleration.
|
|
60
|
+
* **Vectorized Inverse Point Projection**: Robust, vectorized Gauss-Newton and Levenberg-Marquardt algorithms with active-set bounds handling for projecting point clouds onto parametric spline curves, surfaces, and volumes.
|
|
61
|
+
* **End-to-End CSDL Graph Integration**: Custom Vector-Jacobian Products (VJPs) and automatic differentiation through `csdl_alpha`, enabling exact analytic adjoint sensitivities across complex computational graphs.
|
|
62
|
+
* **Interactive & Headless 3D Visualization**: Native PyVista support for visualizing spline control meshes, evaluated surface geometries, and discrete point clouds.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Architecture Overview
|
|
67
|
+
|
|
68
|
+
| Subpackage | Key Classes / Functions | Description |
|
|
69
|
+
|:---|:---|:---|
|
|
70
|
+
| **`lsdo_function_spaces.core.spaces`** | `BSplineSpace`, `FunctionSpace`, `ShepardSpace`, `PolynomialSpace`, `RBFSpace` | Core function space representations, basis function evaluations, and tensor-product constructions. |
|
|
71
|
+
| **`lsdo_function_spaces.core.function`** | `Function`, `FunctionSet` | Concrete functional instances binding coefficient vectors to function spaces, supporting evaluation and composition. |
|
|
72
|
+
| **`lsdo_function_spaces.core.spaces.non_cython_bsplines`** | `project_points_gauss_newton_numpy`, `project_points_lm_numpy`, `LMParams` | Vectorized inverse point projection engines utilizing active-set Levenberg-Marquardt and Gauss-Newton solvers. |
|
|
73
|
+
| **`lsdo_function_spaces.core.b_spline_csdl_custom_ops`** | Custom CSDL evaluation operations | Differentiable computational graph operations providing exact forward and reverse-mode derivative evaluations. |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Quickstart
|
|
78
|
+
|
|
79
|
+
### Creating and Evaluating a B-spline Surface
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import numpy as np
|
|
83
|
+
import csdl_alpha as csdl
|
|
84
|
+
from lsdo_function_spaces import BSplineSpace, Function
|
|
85
|
+
|
|
86
|
+
# 1. Define a 2D B-spline function space (degrees 3x3, 6x6 control points)
|
|
87
|
+
space = BSplineSpace(
|
|
88
|
+
num_dimensions=2,
|
|
89
|
+
order=(4, 4),
|
|
90
|
+
num_coefficients=(6, 6),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# 2. Assign control point coefficients (e.g. parabolic saddle surface)
|
|
94
|
+
u = np.linspace(0, 1, 6)
|
|
95
|
+
v = np.linspace(0, 1, 6)
|
|
96
|
+
U, V = np.meshgrid(u, v, indexing="ij")
|
|
97
|
+
coefficients = np.stack([U, V, U**2 - V**2], axis=-1).reshape(-1, 3)
|
|
98
|
+
|
|
99
|
+
func = Function(space=space, coefficients=coefficients)
|
|
100
|
+
|
|
101
|
+
# 3. Evaluate surface at query parametric coordinates
|
|
102
|
+
eval_pts = np.array([
|
|
103
|
+
[0.2, 0.3],
|
|
104
|
+
[0.5, 0.5],
|
|
105
|
+
[0.8, 0.9],
|
|
106
|
+
])
|
|
107
|
+
physical_coords = func.evaluate(eval_pts)
|
|
108
|
+
print("Evaluated physical coordinates:\n", physical_coords)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Inverse Parametric Point Projection
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import numpy as np
|
|
115
|
+
from lsdo_function_spaces.core.spaces.non_cython_bsplines.b_spline_patch_projection_optimized import (
|
|
116
|
+
project_points_lm_numpy,
|
|
117
|
+
LMParams,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Project arbitrary 3D points onto a B-spline surface patch
|
|
121
|
+
points = np.array([[0.25, 0.35, 0.1]])
|
|
122
|
+
u0s = np.array([[0.5, 0.5]]) # Initial parametric guess
|
|
123
|
+
|
|
124
|
+
u_opt, converged, lam, res = project_points_lm_numpy(
|
|
125
|
+
points=points,
|
|
126
|
+
u0s=u0s,
|
|
127
|
+
coeffs=coefficients,
|
|
128
|
+
degrees=(3, 3),
|
|
129
|
+
knot_vectors=space.knot_vectors,
|
|
130
|
+
params=LMParams(max_iter=50, tol_grad=1e-8),
|
|
131
|
+
)
|
|
132
|
+
print("Converged parametric coordinate:", u_opt)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Installation
|
|
138
|
+
|
|
139
|
+
### Prerequisites & Installation
|
|
140
|
+
`lsdo_function_spaces` requires Python $\ge 3.9$ and standard scientific Python packages:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
# 1. Install prerequisites
|
|
144
|
+
pip install numpy scipy jax networkx
|
|
145
|
+
pip install git+https://github.com/LSDOlab/CSDL_alpha.git@dev_andrew
|
|
146
|
+
|
|
147
|
+
# 2. Install lsdo_function_spaces (User)
|
|
148
|
+
pip install lsdo_function_spaces
|
|
149
|
+
# Or install development version directly from GitHub:
|
|
150
|
+
pip install git+https://github.com/LSDOlab/lsdo_function_spaces.git
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### For Developers
|
|
154
|
+
Clone the repository and install in editable mode with testing and documentation dependencies:
|
|
155
|
+
```sh
|
|
156
|
+
git clone https://github.com/LSDOlab/lsdo_function_spaces.git
|
|
157
|
+
cd lsdo_function_spaces
|
|
158
|
+
pip install -e ".[test,docs]"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Testing
|
|
164
|
+
|
|
165
|
+
Run the full test suite with coverage:
|
|
166
|
+
```sh
|
|
167
|
+
pytest -v tests/ --cov=lsdo_function_spaces --cov-report=term-missing
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For environments without a physical display (e.g. CI runners), run with `xvfb`:
|
|
171
|
+
```sh
|
|
172
|
+
xvfb-run --auto-servernum pytest -v tests/
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Documentation
|
|
178
|
+
|
|
179
|
+
Build the HTML documentation locally:
|
|
180
|
+
```sh
|
|
181
|
+
sphinx-build -b html docs docs/_build/html -q -W
|
|
182
|
+
```
|
|
183
|
+
View the generated documentation by opening `docs/_build/html/index.html` in any web browser.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
This project is licensed under the terms of the **GNU Lesser General Public License v3.0** ([LGPL-3.0](LICENSE.txt)).
|