Coverage for src/beamme/mesh_creation_functions/nurbs_generic.py: 95%
102 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-08 11:03 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-08 11:03 +0000
1# The MIT License (MIT)
2#
3# Copyright (c) 2018-2026 BeamMe Authors
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy
6# of this software and associated documentation files (the "Software"), to deal
7# in the Software without restriction, including without limitation the rights
8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9# copies of the Software, and to permit persons to whom the Software is
10# furnished to do so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in
13# all copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21# THE SOFTWARE.
22"""Generic function used to create NURBS meshes."""
24import itertools as _itertools
26import numpy as _np
28from beamme.core.conf import bme as _bme
29from beamme.core.geometry_set import GeometryName as _GeometryName
30from beamme.core.geometry_set import GeometrySetNodes as _GeometrySetNodes
31from beamme.core.mesh import Mesh as _Mesh
32from beamme.core.node import ControlPoint as _ControlPoint
33from beamme.core.nurbs_patch import NURBSSurface as _NURBSSurface
34from beamme.core.nurbs_patch import NURBSVolume as _NURBSVolume
37def _check_nurbs_dimension_and_element_type(
38 nurbs_dimension: int, element_type: type
39) -> None:
40 """Check if the element type is compatible with the NURBS dimension.
42 Args:
43 nurbs_dimension: The dimension of the NURBS patch (2 for surface, 3 for volume).
44 element_type: The type of element to be created.
46 Raises:
47 ValueError: If the element type is not compatible with the NURBS dimension.
48 """
49 if nurbs_dimension == 2 and not issubclass(element_type, _NURBSSurface):
50 raise ValueError(
51 "Error, expected element type to be a NURBSSurface for a NURBS surface!"
52 )
53 elif nurbs_dimension == 3 and not issubclass(element_type, _NURBSVolume):
54 raise ValueError(
55 "Error, expected element type to be a NURBSVolume for a NURBS volume!"
56 )
59def add_splinepy_nurbs_to_mesh(
60 mesh: _Mesh, element_type: type, splinepy_obj, *, material=None
61) -> _GeometryName:
62 """Add a splinepy NURBS to the mesh.
64 Args:
65 mesh: Mesh that the created NURBS geometry will be added to.
66 element_type: The type of element to be created.
67 splinepy_obj (splinepy object): NURBS geometry created using splinepy.
68 material (Material): Material for this geometry.
70 Returns:
71 GeometryName:
72 Set with the control points that form the topology of the mesh.
74 For a surface, the following information is stored:
75 Vertices: 'vertex_u_min_v_min', 'vertex_u_max_v_min', 'vertex_u_min_v_max', 'vertex_u_max_v_max'
76 Edges: 'line_v_min', 'line_u_max', 'line_v_max', 'line_u_min'
77 Surface: 'surf'
79 For a volume, the following information is stored:
80 Vertices: 'vertex_u_min_v_min_w_min', 'vertex_u_max_v_min_w_min', 'vertex_u_min_v_max_w_min', 'vertex_u_max_v_max_w_min',
81 'vertex_u_min_v_min_w_max', 'vertex_u_max_v_min_w_max', 'vertex_u_min_v_max_w_max', 'vertex_u_max_v_max_w_max'
82 Edges: 'line_v_min_w_min', 'line_u_max_w_min', 'line_v_max_w_min', 'line_u_min_w_min',
83 'line_u_min_v_min', 'line_u_max_v_min', 'line_u_min_v_max', 'line_u_max_v_max'
84 'line_v_min_w_max', 'line_u_max_w_max', 'line_v_max_w_max', 'line_u_min_w_max'
85 Surfaces: 'surf_w_min', 'surf_w_max', 'surf_v_min', 'surf_v_max', 'surf_v_max', 'surf_u_min'
86 Volume: 'vol'
87 """
89 # Make sure that the control points are 3D
90 nurbs_cp_dim = splinepy_obj.control_points.shape[1]
91 if not nurbs_cp_dim == 3:
92 raise ValueError(f"Invalid control point dimension: {nurbs_cp_dim}")
94 # Make sure the material is in the mesh
95 mesh.add_material(material)
97 # Fill control points
98 control_points = [
99 _ControlPoint(coord, weight[0])
100 for coord, weight in zip(
101 _np.asarray(splinepy_obj.control_points), _np.asarray(splinepy_obj.weights)
102 )
103 ]
105 # Create elements
106 _check_nurbs_dimension_and_element_type(
107 len(splinepy_obj.knot_vectors), element_type
108 )
110 element = element_type(
111 [_np.asarray(knot_vector) for knot_vector in splinepy_obj.knot_vectors],
112 _np.asarray(splinepy_obj.degrees),
113 nodes=control_points,
114 material=material,
115 )
117 # Add element and control points to the mesh
118 mesh.elements.append(element)
119 mesh.nodes.extend(control_points)
121 # Create geometry sets that will be returned
122 return_set = create_geometry_sets(element)
124 return return_set
127def add_geomdl_nurbs_to_mesh(
128 mesh: _Mesh, element_type: type, geomdl_obj, *, material=None
129) -> _GeometryName:
130 """Add a geomdl NURBS to the mesh.
132 Args:
133 mesh: Mesh that the created NURBS geometry will be added to.
134 element_type: The type of element to be created.
135 geomdl_obj (geomdl object): NURBS geometry created using geomdl.
136 material (Material): Material for this geometry.
138 Returns:
139 GeometryName:
140 Set with the control points that form the topology of the mesh.
142 For a surface, the following information is stored:
143 Vertices: 'vertex_u_min_v_min', 'vertex_u_max_v_min', 'vertex_u_min_v_max', 'vertex_u_max_v_max'
144 Edges: 'line_v_min', 'line_u_max', 'line_v_max', 'line_u_min'
145 Surface: 'surf'
147 For a volume, the following information is stored:
148 Vertices: 'vertex_u_min_v_min_w_min', 'vertex_u_max_v_min_w_min', 'vertex_u_min_v_max_w_min', 'vertex_u_max_v_max_w_min',
149 'vertex_u_min_v_min_w_max', 'vertex_u_max_v_min_w_max', 'vertex_u_min_v_max_w_max', 'vertex_u_max_v_max_w_max'
150 Edges: 'line_v_min_w_min', 'line_u_max_w_min', 'line_v_max_w_min', 'line_u_min_w_min',
151 'line_u_min_v_min', 'line_u_max_v_min', 'line_u_min_v_max', 'line_u_max_v_max'
152 'line_v_min_w_max', 'line_u_max_w_max', 'line_v_max_w_max', 'line_u_min_w_max'
153 Surfaces: 'surf_w_min', 'surf_w_max', 'surf_v_min', 'surf_v_max', 'surf_v_max', 'surf_u_min'
154 Volume: 'vol'
155 """
157 # Make sure the material is in the mesh
158 mesh.add_material(material)
160 # Fill control points
161 control_points = []
162 nurbs_dimension = len(geomdl_obj.knotvector)
163 if nurbs_dimension == 2:
164 control_points = create_control_points_surface(geomdl_obj)
165 elif nurbs_dimension == 3:
166 control_points = create_control_points_volume(geomdl_obj)
167 else:
168 raise NotImplementedError(
169 "Error, not implemented for NURBS with dimension {}!".format(
170 nurbs_dimension
171 )
172 )
174 # Create elements
175 _check_nurbs_dimension_and_element_type(len(geomdl_obj.knotvector), element_type)
176 element = element_type(
177 geomdl_obj.knotvector,
178 geomdl_obj.degree,
179 nodes=control_points,
180 material=material,
181 )
183 # Add element and control points to the mesh
184 mesh.elements.append(element)
185 mesh.nodes.extend(control_points)
187 # Create geometry sets that will be returned
188 return_set = create_geometry_sets(element)
190 return return_set
193def create_control_points_surface(geomdl_obj):
194 """Creates a list with the ControlPoint objects of a surface created with
195 geomdl."""
196 control_points = []
197 for dir_v in range(geomdl_obj.ctrlpts_size_v):
198 for dir_u in range(geomdl_obj.ctrlpts_size_u):
199 weight = geomdl_obj.ctrlpts2d[dir_u][dir_v][3]
201 # As the control points are scaled with their weight, divide them to get
202 # their coordinates
203 coord = [
204 geomdl_obj.ctrlpts2d[dir_u][dir_v][0] / weight,
205 geomdl_obj.ctrlpts2d[dir_u][dir_v][1] / weight,
206 geomdl_obj.ctrlpts2d[dir_u][dir_v][2] / weight,
207 ]
209 control_points.append(_ControlPoint(coord, weight))
211 return control_points
214def create_control_points_volume(geomdl_obj):
215 """Creates a list with the ControlPoint objects of a volume created with
216 geomdl."""
217 control_points = []
218 for dir_w in range(geomdl_obj.ctrlpts_size_w):
219 for dir_v in range(geomdl_obj.ctrlpts_size_v):
220 for dir_u in range(geomdl_obj.ctrlpts_size_u):
221 # Obtain the id of the control point
222 cp_id = (
223 dir_v
224 + geomdl_obj.ctrlpts_size_v * dir_u
225 + geomdl_obj.ctrlpts_size_u * geomdl_obj.ctrlpts_size_v * dir_w
226 )
228 weight = geomdl_obj.ctrlptsw[cp_id][3]
230 # As the control points are scaled with their weight, divide them to get
231 # their coordinates
232 coord = [
233 geomdl_obj.ctrlptsw[cp_id][0] / weight,
234 geomdl_obj.ctrlptsw[cp_id][1] / weight,
235 geomdl_obj.ctrlptsw[cp_id][2] / weight,
236 ]
238 control_points.append(_ControlPoint(coord, weight))
240 return control_points
243def create_geometry_sets(element: _NURBSSurface | _NURBSVolume) -> _GeometryName:
244 """Create the geometry sets for NURBS patches of all dimensions.
246 Args:
247 element: The NURBS patch for which the geometry sets should be created.
249 Returns:
250 The geometry set container for the given NURBS patch.
251 """
253 # Create return set
254 return_set = _GeometryName()
256 # Get general data needed for the set creation
257 num_cps_uvw = element.get_number_of_control_points_per_dir()
258 nurbs_dimension = len(element.knot_vectors)
259 n_cp = _np.prod(num_cps_uvw)
260 axes = ["u", "v", "w"][:nurbs_dimension]
261 name_map = {0: "min", -1: "max", 1: "min_next", -2: "max_next"}
263 # This is a tensor array that contains the CP indices
264 cp_indices_dim = _np.arange(n_cp, dtype=int).reshape(*num_cps_uvw[::-1]).transpose()
266 if nurbs_dimension >= 0:
267 # Add point sets
268 directions = [0, -1]
269 for corner in _itertools.product(*([directions] * nurbs_dimension)):
270 name = "vertex_" + "_".join(
271 f"{axis}_{name_map[coord]}" for axis, coord in zip(axes, corner)
272 )
273 index = cp_indices_dim[corner]
274 return_set[name] = _GeometrySetNodes(
275 _bme.geo.point, nodes=element.nodes[index]
276 )
278 if nurbs_dimension > 0:
279 # Add edge sets
281 if nurbs_dimension == 2:
282 directions = [0, 1, -2, -1]
283 elif nurbs_dimension == 3:
284 directions = [0, -1]
285 else:
286 raise ValueError("NURBS dimension 1 not implemented")
288 # Iterate over each axis (the axis that varies — the "edge" direction)
289 for edge_axis in range(nurbs_dimension):
290 # The other axes will be fixed
291 fixed_axes = [i for i in range(nurbs_dimension) if i != edge_axis]
292 for fixed_dir in _itertools.product(*([directions] * len(fixed_axes))):
293 # Build slicing tuple for indexing cp_indices_dim
294 slicer: list[slice | int] = [slice(None)] * nurbs_dimension
295 name_parts = []
296 for axis_idx, dir_val in zip(fixed_axes, fixed_dir):
297 slicer[axis_idx] = dir_val
298 name_parts.append(f"{axes[axis_idx]}_{name_map[dir_val]}")
299 name = "line_" + "_".join(name_parts)
301 # Get node indices along the edge
302 edge_indices = cp_indices_dim[tuple(slicer)].flatten()
303 return_set[name] = _GeometrySetNodes(
304 _bme.geo.line, nodes=[element.nodes[i] for i in edge_indices]
305 )
307 if nurbs_dimension == 2:
308 # Add surface sets for surface NURBS
309 return_set["surf"] = _GeometrySetNodes(_bme.geo.surface, element.nodes)
311 if nurbs_dimension == 3:
312 # Add surface sets for volume NURBS
313 for fixed_axis in range(nurbs_dimension):
314 for dir_val in directions:
315 # Build slice and name
316 slicer = [slice(None)] * nurbs_dimension
317 slicer[fixed_axis] = dir_val
318 surface_name = f"surf_{axes[fixed_axis]}_{name_map[dir_val]}"
320 surface_indices = cp_indices_dim[tuple(slicer)].flatten()
321 return_set[surface_name] = _GeometrySetNodes(
322 _bme.geo.surface,
323 nodes=[element.nodes[i] for i in surface_indices],
324 )
326 # Add volume sets
327 return_set["vol"] = _GeometrySetNodes(_bme.geo.volume, element.nodes)
329 return return_set