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

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.""" 

23 

24import itertools as _itertools 

25 

26import numpy as _np 

27 

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 

35 

36 

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. 

41 

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. 

45 

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 ) 

57 

58 

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. 

63 

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. 

69 

70 Returns: 

71 GeometryName: 

72 Set with the control points that form the topology of the mesh. 

73 

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' 

78 

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 """ 

88 

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}") 

93 

94 # Make sure the material is in the mesh 

95 mesh.add_material(material) 

96 

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 ] 

104 

105 # Create elements 

106 _check_nurbs_dimension_and_element_type( 

107 len(splinepy_obj.knot_vectors), element_type 

108 ) 

109 

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 ) 

116 

117 # Add element and control points to the mesh 

118 mesh.elements.append(element) 

119 mesh.nodes.extend(control_points) 

120 

121 # Create geometry sets that will be returned 

122 return_set = create_geometry_sets(element) 

123 

124 return return_set 

125 

126 

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. 

131 

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. 

137 

138 Returns: 

139 GeometryName: 

140 Set with the control points that form the topology of the mesh. 

141 

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' 

146 

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 """ 

156 

157 # Make sure the material is in the mesh 

158 mesh.add_material(material) 

159 

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 ) 

173 

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 ) 

182 

183 # Add element and control points to the mesh 

184 mesh.elements.append(element) 

185 mesh.nodes.extend(control_points) 

186 

187 # Create geometry sets that will be returned 

188 return_set = create_geometry_sets(element) 

189 

190 return return_set 

191 

192 

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] 

200 

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 ] 

208 

209 control_points.append(_ControlPoint(coord, weight)) 

210 

211 return control_points 

212 

213 

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 ) 

227 

228 weight = geomdl_obj.ctrlptsw[cp_id][3] 

229 

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 ] 

237 

238 control_points.append(_ControlPoint(coord, weight)) 

239 

240 return control_points 

241 

242 

243def create_geometry_sets(element: _NURBSSurface | _NURBSVolume) -> _GeometryName: 

244 """Create the geometry sets for NURBS patches of all dimensions. 

245 

246 Args: 

247 element: The NURBS patch for which the geometry sets should be created. 

248 

249 Returns: 

250 The geometry set container for the given NURBS patch. 

251 """ 

252 

253 # Create return set 

254 return_set = _GeometryName() 

255 

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"} 

262 

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() 

265 

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 ) 

277 

278 if nurbs_dimension > 0: 

279 # Add edge sets 

280 

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") 

287 

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) 

300 

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 ) 

306 

307 if nurbs_dimension == 2: 

308 # Add surface sets for surface NURBS 

309 return_set["surf"] = _GeometrySetNodes(_bme.geo.surface, element.nodes) 

310 

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]}" 

319 

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 ) 

325 

326 # Add volume sets 

327 return_set["vol"] = _GeometrySetNodes(_bme.geo.volume, element.nodes) 

328 

329 return return_set