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

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

23 

24import itertools as _itertools 

25from typing import Type as _Type 

26 

27import numpy as _np 

28 

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 

36 

37 

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. 

46 

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

52 

53 Returns: 

54 GeometryName: 

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

56 

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' 

61 

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

71 

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

76 

77 # Make sure the material is in the mesh 

78 mesh.add_material(material) 

79 

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 ] 

87 

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 ) 

99 

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 ) 

107 

108 # Add element and control points to the mesh 

109 mesh.elements.append(element) 

110 mesh.nodes.extend(control_points) 

111 

112 # Create geometry sets that will be returned 

113 return_set = create_geometry_sets(element) 

114 

115 return return_set 

116 

117 

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. 

126 

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

132 

133 Returns: 

134 GeometryName: 

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

136 

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' 

141 

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

151 

152 # Make sure the material is in the mesh 

153 mesh.add_material(material) 

154 

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 ) 

168 

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 ) 

180 

181 element = nurbs_object( 

182 geomdl_obj.knotvector, 

183 geomdl_obj.degree, 

184 nodes=control_points, 

185 material=material, 

186 data=data, 

187 ) 

188 

189 # Add element and control points to the mesh 

190 mesh.elements.append(element) 

191 mesh.nodes.extend(control_points) 

192 

193 # Create geometry sets that will be returned 

194 return_set = create_geometry_sets(element) 

195 

196 return return_set 

197 

198 

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] 

206 

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 ] 

214 

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

216 

217 return control_points 

218 

219 

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 ) 

233 

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

235 

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 ] 

243 

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

245 

246 return control_points 

247 

248 

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

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

251 

252 Args: 

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

254 

255 Returns: 

256 The geometry set container for the given NURBS patch. 

257 """ 

258 

259 # Create return set 

260 return_set = _GeometryName() 

261 

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

268 

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

271 

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 ) 

283 

284 if nurbs_dimension > 0: 

285 # Add edge sets 

286 

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

293 

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) 

306 

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 ) 

312 

313 if nurbs_dimension == 2: 

314 # Add surface sets for surface NURBS 

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

316 

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

325 

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 ) 

331 

332 # Add volume sets 

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

334 

335 return return_set