Coverage for src/beamme/core/element_beam.py: 92%

127 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-09-29 11:30 +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"""This file defines the base beam element.""" 

23 

24from typing import Any as _Any 

25from typing import Callable as _Callable 

26from typing import List as _List 

27from typing import Optional as _Optional 

28from typing import Type as _Type 

29 

30import numpy as _np 

31import vtk as _vtk 

32 

33from beamme.core.conf import bme as _bme 

34from beamme.core.element import Element as _Element 

35from beamme.core.material import MaterialBeamBase as _MaterialBeamBase 

36from beamme.core.node import NodeCosserat as _NodeCosserat 

37from beamme.core.rotation import Rotation as _Rotation 

38from beamme.core.vtk_writer import add_point_data_node_sets as _add_point_data_node_sets 

39 

40 

41class Beam(_Element): 

42 """A base class for a beam element.""" 

43 

44 # An array that defines the parameter positions of the element nodes, 

45 # in ascending order. 

46 nodes_create: _Any = [] 

47 

48 # A list of valid material types for this element. 

49 valid_materials: _Any = [] 

50 

51 def __init__(self, material=None, nodes=None): 

52 super().__init__(nodes=nodes, material=material) 

53 

54 @classmethod 

55 def create_beam( 

56 cls: _Type["Beam"], 

57 material: _MaterialBeamBase, 

58 beam_function: _Callable, 

59 *, 

60 start_node: _Optional[_NodeCosserat] = None, 

61 end_node: _Optional[_NodeCosserat] = None, 

62 relative_twist: _Optional[_Rotation] = None, 

63 set_nodal_arc_length: bool = False, 

64 nodal_arc_length_offset: _Optional[float] = None, 

65 ) -> tuple["Beam", _List[_NodeCosserat]]: 

66 """Create the nodes for this beam element. The function returns a list 

67 with the created nodes. 

68 

69 In the case of start_node and end_node, it is checked, that the 

70 function and the node have the same coordinates and rotations. 

71 

72 Args: 

73 material: The material to be used with this beam. 

74 beam_function: 

75 Returns the position, rotation and (optionally) arc length of the 

76 beam along the local coordinate xi. If no arc lengths is provided, 

77 the returned value should simply be None. 

78 start_node: 

79 If this argument is given, this is the node of the beam at xi=-1. 

80 end_node: 

81 If this argument is given, this is the node of the beam at xi=1. 

82 relative_twist: 

83 Apply this relative rotation to all created nodes. This can be used to 

84 "twist" the created beam to match the rotation of given nodes. 

85 set_nodal_arc_length: 

86 Flag if the arc length in the created nodes should be set. 

87 nodal_arc_length_offset: 

88 Offset of the stored nodal arc length w.r.t. to the one generated 

89 by the function. 

90 

91 Returns: 

92 The created beam object and the newly created nodes. The created nodes 

93 are returned, because we can then directly add them to a `Mesh` without 

94 having to check if they are already in the mesh. 

95 """ 

96 

97 def check_node(node, pos, rot, arc_length, name): 

98 """Check if the given node matches with the position and rotation 

99 and optionally also the arc length.""" 

100 

101 if _np.linalg.norm(pos - node.coordinates) > _bme.eps_pos: 

102 raise ValueError( 

103 f"{name} position does not match with function! Got {pos} from function but " 

104 + f"given node value is {node.coordinates}" 

105 ) 

106 if not node.rotation == rot: 

107 raise ValueError(f"{name} rotation does not match with function!") 

108 

109 if arc_length is not None: 

110 if _np.abs(node.arc_length - arc_length) > _bme.eps_pos: 

111 raise ValueError( 

112 f"Arc lengths don't match, got {node.arc_length} and {arc_length}" 

113 ) 

114 

115 # Flags if nodes are given 

116 has_start_node = start_node is not None 

117 has_end_node = end_node is not None 

118 

119 # Create the element 

120 beam = cls(material=material) 

121 

122 # Loop over local nodes. 

123 arc_length = None 

124 for i, xi in enumerate(cls.nodes_create): 

125 # Get the position and rotation at xi 

126 pos, rot, arc_length_from_function = beam_function(xi) 

127 if relative_twist is not None: 

128 rot = rot * relative_twist 

129 if set_nodal_arc_length: 

130 arc_length = arc_length_from_function + nodal_arc_length_offset 

131 

132 # Check if the position and rotation match existing nodes 

133 if i == 0 and has_start_node: 

134 check_node(start_node, pos, rot, arc_length, "start_node") 

135 beam.nodes = [start_node] 

136 elif (i == len(cls.nodes_create) - 1) and has_end_node: 

137 check_node(end_node, pos, rot, arc_length, "end_node") 

138 

139 # Create the node 

140 if (i > 0 or not has_start_node) and ( 

141 i < len(cls.nodes_create) - 1 or not has_end_node 

142 ): 

143 is_middle_node = 0 < i < len(cls.nodes_create) - 1 

144 beam.nodes.append( 

145 _NodeCosserat( 

146 pos, rot, is_middle_node=is_middle_node, arc_length=arc_length 

147 ) 

148 ) 

149 

150 # Get a list with the created nodes. 

151 if has_start_node: 

152 created_nodes = beam.nodes[1:] 

153 else: 

154 created_nodes = beam.nodes.copy() 

155 

156 # Add the end node to the beam. 

157 if has_end_node: 

158 beam.nodes.append(end_node) 

159 

160 # Return the created beam object and the created nodes. 

161 return beam, created_nodes 

162 

163 @classmethod 

164 def get_coupling_dict(cls, coupling_dof_type): 

165 """Return the dict to couple this beam to another beam.""" 

166 

167 match coupling_dof_type: 

168 case _bme.coupling_dof.joint: 

169 if cls.coupling_joint_dict is None: 

170 raise ValueError(f"Joint coupling is not implemented for {cls}") 

171 return cls.coupling_joint_dict 

172 case _bme.coupling_dof.fix: 

173 if cls.coupling_fix_dict is None: 

174 raise ValueError("Fix coupling is not implemented for {cls}") 

175 return cls.coupling_fix_dict 

176 case _: 

177 raise ValueError( 

178 f'Coupling_dof_type "{coupling_dof_type}" is not implemented!' 

179 ) 

180 

181 def flip(self): 

182 """Reverse the nodes of this element. 

183 

184 This is usually used when reflected. 

185 """ 

186 self.nodes = [self.nodes[-1 - i] for i in range(len(self.nodes))] 

187 

188 def _check_material(self): 

189 """Check if the linked material is valid for this type of beam 

190 element.""" 

191 for material_type in type(self).valid_materials: 

192 if isinstance(self.material, material_type): 

193 break 

194 else: 

195 raise TypeError( 

196 f"Beam of type {type(self)} can not have a material of type {type(self.material)}!" 

197 ) 

198 

199 def get_vtk( 

200 self, 

201 vtk_writer_beam, 

202 vtk_writer_solid, 

203 *, 

204 beam_centerline_visualization_segments=1, 

205 **kwargs, 

206 ): 

207 """Add the representation of this element to the VTK writer as a poly 

208 line. 

209 

210 Args 

211 ---- 

212 vtk_writer_beam: 

213 VTK writer for the beams. 

214 vtk_writer_solid: 

215 VTK writer for solid elements, not used in this method. 

216 beam_centerline_visualization_segments: int 

217 Number of segments to be used for visualization of beam centerline between successive 

218 nodes. Default is 1, which means a straight line is drawn between the beam nodes. For 

219 Values greater than 1, a Hermite interpolation of the centerline is assumed for 

220 visualization purposes. 

221 """ 

222 

223 n_nodes = len(self.nodes) 

224 n_segments = n_nodes - 1 

225 n_additional_points_per_segment = beam_centerline_visualization_segments - 1 

226 # Number of points (in addition to the nodes) to be used for output 

227 n_additional_points = n_segments * n_additional_points_per_segment 

228 n_points = n_nodes + n_additional_points 

229 

230 # Dictionary with cell data. 

231 cell_data = self.vtk_cell_data.copy() 

232 cell_data["cross_section_radius"] = self.material.radius 

233 

234 # Dictionary with point data. 

235 point_data = {} 

236 point_data["node_value"] = _np.zeros(n_points) 

237 point_data["base_vector_1"] = _np.zeros((n_points, 3)) 

238 point_data["base_vector_2"] = _np.zeros((n_points, 3)) 

239 point_data["base_vector_3"] = _np.zeros((n_points, 3)) 

240 

241 coordinates = _np.zeros((n_points, 3)) 

242 nodal_rotation_matrices = [ 

243 node.rotation.get_rotation_matrix() for node in self.nodes 

244 ] 

245 

246 for i_node, (node, rotation_matrix) in enumerate( 

247 zip(self.nodes, nodal_rotation_matrices) 

248 ): 

249 coordinates[i_node, :] = node.coordinates 

250 if node.is_middle_node: 

251 point_data["node_value"][i_node] = 0.5 

252 else: 

253 point_data["node_value"][i_node] = 1.0 

254 

255 point_data["base_vector_1"][i_node] = rotation_matrix[:, 0] 

256 point_data["base_vector_2"][i_node] = rotation_matrix[:, 1] 

257 point_data["base_vector_3"][i_node] = rotation_matrix[:, 2] 

258 

259 # We can output the element partner index, which is a debug quantity to help find elements 

260 # with matching middle nodes. This is usually an indicator for an issue with the mesh. 

261 # TODO: Check if we really need this partner index output any more 

262 element_partner_indices = list( 

263 [ 

264 node.element_partner_index 

265 for node in self.nodes 

266 if node.element_partner_index is not None 

267 ] 

268 ) 

269 if len(element_partner_indices) > 1: 

270 raise ValueError( 

271 "More than one element partner indices are currently not supported in the output" 

272 "functionality" 

273 ) 

274 elif len(element_partner_indices) == 1: 

275 cell_data["partner_index"] = element_partner_indices[0] + 1 

276 

277 # Check if we have everything we need to write output or if we need to calculate additional 

278 # points for a smooth beam visualization. 

279 if beam_centerline_visualization_segments == 1: 

280 point_connectivity = _np.arange(n_nodes) 

281 else: 

282 # We need the centerline shape function matrices, so calculate them once and use for 

283 # all segments that we need. Drop the first and last value, since they represent the 

284 # nodes which we have already added above. 

285 xi = _np.linspace(-1, 1, beam_centerline_visualization_segments + 1)[1:-1] 

286 hermite_shape_functions_pos = _np.array( 

287 [ 

288 0.25 * (2.0 + xi) * (1.0 - xi) ** 2, 

289 0.25 * (2.0 - xi) * (1.0 + xi) ** 2, 

290 ] 

291 ).transpose() 

292 hermite_shape_functions_tan = _np.array( 

293 [ 

294 0.125 * (1.0 + xi) * (1.0 - xi) ** 2, 

295 -0.125 * (1.0 - xi) * (1.0 + xi) ** 2, 

296 ] 

297 ).transpose() 

298 

299 point_connectivity = _np.zeros(n_points, dtype=int) 

300 

301 for i_segment in range(n_segments): 

302 positions = _np.array( 

303 [ 

304 self.nodes[i_node].coordinates 

305 for i_node in [i_segment, i_segment + 1] 

306 ] 

307 ) 

308 tangents = _np.array( 

309 [ 

310 nodal_rotation_matrices[i_node][:, 0] 

311 for i_node in [i_segment, i_segment + 1] 

312 ] 

313 ) 

314 length_factor = _np.linalg.norm(positions[1] - positions[0]) 

315 interpolated_coordinates = _np.dot( 

316 hermite_shape_functions_pos, positions 

317 ) + length_factor * _np.dot(hermite_shape_functions_tan, tangents) 

318 

319 index_first_point = ( 

320 n_nodes + i_segment * n_additional_points_per_segment 

321 ) 

322 index_last_point = ( 

323 n_nodes + (i_segment + 1) * n_additional_points_per_segment 

324 ) 

325 

326 coordinates[index_first_point:index_last_point] = ( 

327 interpolated_coordinates 

328 ) 

329 point_connectivity[ 

330 i_segment * beam_centerline_visualization_segments 

331 ] = i_segment 

332 point_connectivity[ 

333 (i_segment + 1) * beam_centerline_visualization_segments 

334 ] = i_segment + 1 

335 point_connectivity[ 

336 i_segment * beam_centerline_visualization_segments + 1 : ( 

337 i_segment + 1 

338 ) 

339 * beam_centerline_visualization_segments 

340 ] = _np.arange(index_first_point, index_last_point) 

341 

342 # Get the point data sets and add everything to the output file. 

343 _add_point_data_node_sets( 

344 point_data, self.nodes, extra_points=n_additional_points 

345 ) 

346 indices = vtk_writer_beam.add_points(coordinates, point_data=point_data) 

347 vtk_writer_beam.add_cell( 

348 _vtk.vtkPolyLine, indices[point_connectivity], cell_data=cell_data 

349 ) 

350 

351 

352def generate_beam_class(n_nodes: int): 

353 """Return a class representing a general beam with n_nodes in BeamMe. 

354 

355 Args: 

356 n_nodes: Number of equally spaced nodes along the beam centerline. 

357 

358 Returns: 

359 A beam object that has n_nodes along the centerline. 

360 """ 

361 

362 # Define the class variable responsible for creating the nodes. 

363 nodes_create = _np.linspace(-1, 1, num=n_nodes) 

364 

365 # Create the beam class which inherits from the base beam class. 

366 return type(f"Beam{n_nodes}", (Beam,), {"nodes_create": nodes_create}) 

367 

368 

369Beam2 = generate_beam_class(2) 

370Beam3 = generate_beam_class(3) 

371Beam4 = generate_beam_class(4) 

372Beam5 = generate_beam_class(5)