lsdo-function-spaces 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. lsdo_function_spaces/__init__.py +64 -0
  2. lsdo_function_spaces/core/__init__.py +0 -0
  3. lsdo_function_spaces/core/function.py +1322 -0
  4. lsdo_function_spaces/core/function_set.py +1081 -0
  5. lsdo_function_spaces/core/function_set_space.py +379 -0
  6. lsdo_function_spaces/core/function_space.py +482 -0
  7. lsdo_function_spaces/core/operations/__init__.py +0 -0
  8. lsdo_function_spaces/core/operations/basic_ops.py +85 -0
  9. lsdo_function_spaces/core/operations/operations.py +5 -0
  10. lsdo_function_spaces/core/optimization.py +183 -0
  11. lsdo_function_spaces/core/spaces/__init__.py +0 -0
  12. lsdo_function_spaces/core/spaces/b_spline_space.py +418 -0
  13. lsdo_function_spaces/core/spaces/conditional_space.py +65 -0
  14. lsdo_function_spaces/core/spaces/constant_space.py +57 -0
  15. lsdo_function_spaces/core/spaces/idw_space.py +271 -0
  16. lsdo_function_spaces/core/spaces/non_cython_bsplines/__init__.py +0 -0
  17. lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_csdl_custom_ops.py +420 -0
  18. lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection.py +1022 -0
  19. lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection_non_differentiable.py +186 -0
  20. lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection_optimized.py +594 -0
  21. lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_space_new.py +6 -0
  22. lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax.py +172 -0
  23. lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax_factory.py +382 -0
  24. lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax_stencil.py +451 -0
  25. lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_numpy.py +249 -0
  26. lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_numpy_factory.py +391 -0
  27. lsdo_function_spaces/core/spaces/operation_space.py +64 -0
  28. lsdo_function_spaces/core/spaces/polynomial_space.py +79 -0
  29. lsdo_function_spaces/core/spaces/rbf_space.py +136 -0
  30. lsdo_function_spaces/core/spaces/tri_space.py +256 -0
  31. lsdo_function_spaces/utils/__init__.py +0 -0
  32. lsdo_function_spaces/utils/file_io.py +484 -0
  33. lsdo_function_spaces/utils/internal_utilities.py +11 -0
  34. lsdo_function_spaces/utils/plotting_functions.py +357 -0
  35. lsdo_function_spaces/utils/utility_functions.py +148 -0
  36. lsdo_function_spaces-1.0.0.dist-info/METADATA +189 -0
  37. lsdo_function_spaces-1.0.0.dist-info/RECORD +40 -0
  38. lsdo_function_spaces-1.0.0.dist-info/WHEEL +5 -0
  39. lsdo_function_spaces-1.0.0.dist-info/licenses/LICENSE.txt +165 -0
  40. lsdo_function_spaces-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ [![Documentation Status](https://readthedocs.org/projects/lsdo-function-spaces/badge/?version=latest)](https://lsdo-function-spaces.readthedocs.io/en/latest/?badge=latest)
46
+ [![Tests](https://github.com/LSDOlab/lsdo_function_spaces/actions/workflows/actions.yml/badge.svg)](https://github.com/LSDOlab/lsdo_function_spaces/actions)
47
+ ![Python Version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)
48
+ [![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](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)).