Coverage for src / beamme / mesh_creation_functions / nurbs_generic.py: 95%
108 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 12:57 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 12:57 +0000
1# The MIT License (MIT)
2#
3# Copyright (c) 2018-2025 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
25from typing import Type as _Type
27import numpy as _np
29from beamme.core.conf import bme as _bme
30from beamme.core.geometry_set import GeometryName as _GeometryName
31from beamme.core.geometry_set import GeometrySetNodes as _GeometrySetNodes
32from beamme.core.mesh import Mesh as _Mesh
33from beamme.core.node import ControlPoint as _ControlPoint
34from beamme.core.nurbs_patch import NURBSSurface as _NURBSSurface
35from beamme.core.nurbs_patch import NURBSVolume as _NURBSVolume
38def add_splinepy_nurbs_to_mesh(
39 mesh: _Mesh,
40 splinepy_obj,
41 *,
42 material=None,
43 data: dict | None = None,
44) -> _GeometryName:
45 """Add a splinepy NURBS to the mesh.
47 Args:
48 mesh: Mesh that the created NURBS geometry will be added to.
49 splinepy_obj (splinepy object): NURBS geometry created using splinepy.
50 material (Material): Material for this geometry.
51 data: General element data, e.g., material, formulation, ...
53 Returns:
54 GeometryName:
55 Set with the control points that form the topology of the mesh.
57 For a surface, the following information is stored:
58 Vertices: 'vertex_u_min_v_min', 'vertex_u_max_v_min', 'vertex_u_min_v_max', 'vertex_u_max_v_max'
59 Edges: 'line_v_min', 'line_u_max', 'line_v_max', 'line_u_min'
60 Surface: 'surf'
62 For a volume, the following information is stored:
63 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',
64 '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'
65 Edges: 'line_v_min_w_min', 'line_u_max_w_min', 'line_v_max_w_min', 'line_u_min_w_min',
66 'line_u_min_v_min', 'line_u_max_v_min', 'line_u_min_v_max', 'line_u_max_v_max'
67 'line_v_min_w_max', 'line_u_max_w_max', 'line_v_max_w_max', 'line_u_min_w_max'
68 Surfaces: 'surf_w_min', 'surf_w_max', 'surf_v_min', 'surf_v_max', 'surf_v_max', 'surf_u_min'
69 Volume: 'vol'
70 """
72 # Make sure that the control points are 3D
73 nurbs_cp_dim = splinepy_obj.control_points.shape[1]
74 if not nurbs_cp_dim == 3:
75 raise ValueError(f"Invalid control point dimension: {nurbs_cp_dim}")
77 # Make sure the material is in the mesh
78 mesh.add_material(material)
80 # Fill control points
81 control_points = [
82 _ControlPoint(coord, weight[0])
83 for coord, weight in zip(
84 _np.asarray(splinepy_obj.control_points), _np.asarray(splinepy_obj.weights)
85 )
86 ]
88 # Fill element
89 manifold_dim = len(splinepy_obj.knot_vectors)
90 nurbs_object: _Type[_NURBSSurface] | _Type[_NURBSVolume]
91 if manifold_dim == 2:
92 nurbs_object = _NURBSSurface
93 elif manifold_dim == 3:
94 nurbs_object = _NURBSVolume
95 else:
96 raise NotImplementedError(
97 "Error, not implemented for a NURBS {}!".format(type(splinepy_obj))
98 )
100 element = nurbs_object(
101 [_np.asarray(knot_vector) for knot_vector in splinepy_obj.knot_vectors],
102 _np.asarray(splinepy_obj.degrees),
103 nodes=control_points,
104 material=material,
105 data=data,
106 )
108 # Add element and control points to the mesh
109 mesh.elements.append(element)
110 mesh.nodes.extend(control_points)
112 # Create geometry sets that will be returned
113 return_set = create_geometry_sets(element)
115 return return_set
118def add_geomdl_nurbs_to_mesh(
119 mesh: _Mesh,
120 geomdl_obj,
121 *,
122 material=None,
123 data: dict | None = None,
124) -> _GeometryName:
125 """Add a geomdl NURBS to the mesh.
127 Args:
128 mesh: Mesh that the created NURBS geometry will be added to.
129 geomdl_obj (geomdl object): NURBS geometry created using geomdl.
130 material (Material): Material for this geometry.
131 data: General element data, e.g., material, formulation, ...
133 Returns:
134 GeometryName:
135 Set with the control points that form the topology of the mesh.
137 For a surface, the following information is stored:
138 Vertices: 'vertex_u_min_v_min', 'vertex_u_max_v_min', 'vertex_u_min_v_max', 'vertex_u_max_v_max'
139 Edges: 'line_v_min', 'line_u_max', 'line_v_max', 'line_u_min'
140 Surface: 'surf'
142 For a volume, the following information is stored:
143 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',
144 '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'
145 Edges: 'line_v_min_w_min', 'line_u_max_w_min', 'line_v_max_w_min', 'line_u_min_w_min',
146 'line_u_min_v_min', 'line_u_max_v_min', 'line_u_min_v_max', 'line_u_max_v_max'
147 'line_v_min_w_max', 'line_u_max_w_max', 'line_v_max_w_max', 'line_u_min_w_max'
148 Surfaces: 'surf_w_min', 'surf_w_max', 'surf_v_min', 'surf_v_max', 'surf_v_max', 'surf_u_min'
149 Volume: 'vol'
150 """
152 # Make sure the material is in the mesh
153 mesh.add_material(material)
155 # Fill control points
156 control_points = []
157 nurbs_dimension = len(geomdl_obj.knotvector)
158 if nurbs_dimension == 2:
159 control_points = create_control_points_surface(geomdl_obj)
160 elif nurbs_dimension == 3:
161 control_points = create_control_points_volume(geomdl_obj)
162 else:
163 raise NotImplementedError(
164 "Error, not implemented for NURBS with dimension {}!".format(
165 nurbs_dimension
166 )
167 )
169 # Fill element
170 manifold_dim = len(geomdl_obj.knotvector)
171 nurbs_object: _Type[_NURBSSurface] | _Type[_NURBSVolume]
172 if manifold_dim == 2:
173 nurbs_object = _NURBSSurface
174 elif manifold_dim == 3:
175 nurbs_object = _NURBSVolume
176 else:
177 raise NotImplementedError(
178 "Error, not implemented for a NURBS {}!".format(type(geomdl_obj))
179 )
181 element = nurbs_object(
182 geomdl_obj.knotvector,
183 geomdl_obj.degree,
184 nodes=control_points,
185 material=material,
186 data=data,
187 )
189 # Add element and control points to the mesh
190 mesh.elements.append(element)
191 mesh.nodes.extend(control_points)
193 # Create geometry sets that will be returned
194 return_set = create_geometry_sets(element)
196 return return_set
199def create_control_points_surface(geomdl_obj):
200 """Creates a list with the ControlPoint objects of a surface created with
201 geomdl."""
202 control_points = []
203 for dir_v in range(geomdl_obj.ctrlpts_size_v):
204 for dir_u in range(geomdl_obj.ctrlpts_size_u):
205 weight = geomdl_obj.ctrlpts2d[dir_u][dir_v][3]
207 # As the control points are scaled with their weight, divide them to get
208 # their coordinates
209 coord = [
210 geomdl_obj.ctrlpts2d[dir_u][dir_v][0] / weight,
211 geomdl_obj.ctrlpts2d[dir_u][dir_v][1] / weight,
212 geomdl_obj.ctrlpts2d[dir_u][dir_v][2] / weight,
213 ]
215 control_points.append(_ControlPoint(coord, weight))
217 return control_points
220def create_control_points_volume(geomdl_obj):
221 """Creates a list with the ControlPoint objects of a volume created with
222 geomdl."""
223 control_points = []
224 for dir_w in range(geomdl_obj.ctrlpts_size_w):
225 for dir_v in range(geomdl_obj.ctrlpts_size_v):
226 for dir_u in range(geomdl_obj.ctrlpts_size_u):
227 # Obtain the id of the control point
228 cp_id = (
229 dir_v
230 + geomdl_obj.ctrlpts_size_v * dir_u
231 + geomdl_obj.ctrlpts_size_u * geomdl_obj.ctrlpts_size_v * dir_w
232 )
234 weight = geomdl_obj.ctrlptsw[cp_id][3]
236 # As the control points are scaled with their weight, divide them to get
237 # their coordinates
238 coord = [
239 geomdl_obj.ctrlptsw[cp_id][0] / weight,
240 geomdl_obj.ctrlptsw[cp_id][1] / weight,
241 geomdl_obj.ctrlptsw[cp_id][2] / weight,
242 ]
244 control_points.append(_ControlPoint(coord, weight))
246 return control_points
249def create_geometry_sets(element: _NURBSSurface | _NURBSVolume) -> _GeometryName:
250 """Create the geometry sets for NURBS patches of all dimensions.
252 Args:
253 element: The NURBS patch for which the geometry sets should be created.
255 Returns:
256 The geometry set container for the given NURBS patch.
257 """
259 # Create return set
260 return_set = _GeometryName()
262 # Get general data needed for the set creation
263 num_cps_uvw = element.get_number_of_control_points_per_dir()
264 nurbs_dimension = len(element.knot_vectors)
265 n_cp = _np.prod(num_cps_uvw)
266 axes = ["u", "v", "w"][:nurbs_dimension]
267 name_map = {0: "min", -1: "max", 1: "min_next", -2: "max_next"}
269 # This is a tensor array that contains the CP indices
270 cp_indices_dim = _np.arange(n_cp, dtype=int).reshape(*num_cps_uvw[::-1]).transpose()
272 if nurbs_dimension >= 0:
273 # Add point sets
274 directions = [0, -1]
275 for corner in _itertools.product(*([directions] * nurbs_dimension)):
276 name = "vertex_" + "_".join(
277 f"{axis}_{name_map[coord]}" for axis, coord in zip(axes, corner)
278 )
279 index = cp_indices_dim[corner]
280 return_set[name] = _GeometrySetNodes(
281 _bme.geo.point, nodes=element.nodes[index]
282 )
284 if nurbs_dimension > 0:
285 # Add edge sets
287 if nurbs_dimension == 2:
288 directions = [0, 1, -2, -1]
289 elif nurbs_dimension == 3:
290 directions = [0, -1]
291 else:
292 raise ValueError("NURBS dimension 1 not implemented")
294 # Iterate over each axis (the axis that varies — the "edge" direction)
295 for edge_axis in range(nurbs_dimension):
296 # The other axes will be fixed
297 fixed_axes = [i for i in range(nurbs_dimension) if i != edge_axis]
298 for fixed_dir in _itertools.product(*([directions] * len(fixed_axes))):
299 # Build slicing tuple for indexing cp_indices_dim
300 slicer: list[slice | int] = [slice(None)] * nurbs_dimension
301 name_parts = []
302 for axis_idx, dir_val in zip(fixed_axes, fixed_dir):
303 slicer[axis_idx] = dir_val
304 name_parts.append(f"{axes[axis_idx]}_{name_map[dir_val]}")
305 name = "line_" + "_".join(name_parts)
307 # Get node indices along the edge
308 edge_indices = cp_indices_dim[tuple(slicer)].flatten()
309 return_set[name] = _GeometrySetNodes(
310 _bme.geo.line, nodes=[element.nodes[i] for i in edge_indices]
311 )
313 if nurbs_dimension == 2:
314 # Add surface sets for surface NURBS
315 return_set["surf"] = _GeometrySetNodes(_bme.geo.surface, element.nodes)
317 if nurbs_dimension == 3:
318 # Add surface sets for volume NURBS
319 for fixed_axis in range(nurbs_dimension):
320 for dir_val in directions:
321 # Build slice and name
322 slicer = [slice(None)] * nurbs_dimension
323 slicer[fixed_axis] = dir_val
324 surface_name = f"surf_{axes[fixed_axis]}_{name_map[dir_val]}"
326 surface_indices = cp_indices_dim[tuple(slicer)].flatten()
327 return_set[surface_name] = _GeometrySetNodes(
328 _bme.geo.surface,
329 nodes=[element.nodes[i] for i in surface_indices],
330 )
332 # Add volume sets
333 return_set["vol"] = _GeometrySetNodes(_bme.geo.volume, element.nodes)
335 return return_set