Coverage for src/meshpy/core/element_beam.py: 91%

120 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-28 04:21 +0000

1# The MIT License (MIT) 

2# 

3# Copyright (c) 2018-2025 MeshPy 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 in MeshPy.""" 

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 

28 

29import numpy as _np 

30import vtk as _vtk 

31 

32from meshpy.core.conf import mpy as _mpy 

33from meshpy.core.element import Element as _Element 

34from meshpy.core.node import NodeCosserat as _NodeCosserat 

35from meshpy.core.rotation import Rotation as _Rotation 

36from meshpy.core.vtk_writer import add_point_data_node_sets as _add_point_data_node_sets 

37 

38 

39class Beam(_Element): 

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

41 

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

43 # in ascending order. 

44 nodes_create: _Any = [] 

45 

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

47 valid_material: _Any = [] 

48 

49 # Coupling strings. 

50 coupling_fix_string: _Optional[str] = None 

51 coupling_joint_string: _Optional[str] = None 

52 

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

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

55 

56 def create_beam( 

57 self, 

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 ) -> _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 beam_function: function(xi) 

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

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

76 the returned value should simply be None. 

77 start_node: Node 

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

79 end_node: Node 

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

81 relative_twist: Rotation 

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

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

84 set_nodal_arc_length: 

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

86 nodal_arc_length_offset: 

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

88 by the function. 

89 """ 

90 

91 if len(self.nodes) > 0: 

92 raise ValueError("The beam should not have any local nodes yet!") 

93 

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

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

96 and optionally also the arc length.""" 

97 

98 if _np.linalg.norm(pos - node.coordinates) > _mpy.eps_pos: 

99 raise ValueError( 

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

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

102 ) 

103 if not node.rotation == rot: 

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

105 

106 if arc_length is not None: 

107 if _np.abs(node.arc_length - arc_length) > _mpy.eps_pos: 

108 raise ValueError( 

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

110 ) 

111 

112 # Flags if nodes are given 

113 has_start_node = start_node is not None 

114 has_end_node = end_node is not None 

115 

116 # Loop over local nodes. 

117 arc_length = None 

118 for i, xi in enumerate(self.nodes_create): 

119 # Get the position and rotation at xi 

120 pos, rot, arc_length_from_function = beam_function(xi) 

121 if relative_twist is not None: 

122 rot = rot * relative_twist 

123 if set_nodal_arc_length: 

124 arc_length = arc_length_from_function + nodal_arc_length_offset 

125 

126 # Check if the position and rotation match existing nodes 

127 if i == 0 and has_start_node: 

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

129 self.nodes = [start_node] 

130 elif (i == len(self.nodes_create) - 1) and has_end_node: 

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

132 

133 # Create the node 

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

135 i < len(self.nodes_create) - 1 or not has_end_node 

136 ): 

137 is_middle_node = 0 < i < len(self.nodes_create) - 1 

138 self.nodes.append( 

139 _NodeCosserat( 

140 pos, rot, is_middle_node=is_middle_node, arc_length=arc_length 

141 ) 

142 ) 

143 

144 # Get a list with the created nodes. 

145 if has_start_node: 

146 created_nodes = self.nodes[1:] 

147 else: 

148 created_nodes = self.nodes.copy() 

149 

150 # Add the end node to the beam. 

151 if has_end_node: 

152 self.nodes.append(end_node) 

153 

154 # Return the created nodes. 

155 return created_nodes 

156 

157 @classmethod 

158 def get_coupling_dict(cls, coupling_dof_type): 

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

160 

161 match coupling_dof_type: 

162 case _mpy.coupling_dof.joint: 

163 if cls.coupling_joint_dict is None: 

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

165 return cls.coupling_joint_dict 

166 case _mpy.coupling_dof.fix: 

167 if cls.coupling_fix_dict is None: 

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

169 return cls.coupling_fix_dict 

170 case _: 

171 raise ValueError( 

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

173 ) 

174 

175 def flip(self): 

176 """Reverse the nodes of this element. 

177 

178 This is usually used when reflected. 

179 """ 

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

181 

182 def _check_material(self): 

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

184 element.""" 

185 for material_type in self.valid_material: 

186 if isinstance(self.material, material_type): 

187 break 

188 else: 

189 raise TypeError( 

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

191 ) 

192 

193 def get_vtk( 

194 self, 

195 vtk_writer_beam, 

196 vtk_writer_solid, 

197 *, 

198 beam_centerline_visualization_segments=1, 

199 **kwargs, 

200 ): 

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

202 line. 

203 

204 Args 

205 ---- 

206 vtk_writer_beam: 

207 VTK writer for the beams. 

208 vtk_writer_solid: 

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

210 beam_centerline_visualization_segments: int 

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

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

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

214 visualization purposes. 

215 """ 

216 

217 n_nodes = len(self.nodes) 

218 n_segments = n_nodes - 1 

219 n_additional_points_per_segment = beam_centerline_visualization_segments - 1 

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

221 n_additional_points = n_segments * n_additional_points_per_segment 

222 n_points = n_nodes + n_additional_points 

223 

224 # Dictionary with cell data. 

225 cell_data = self.vtk_cell_data.copy() 

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

227 

228 # Dictionary with point data. 

229 point_data = {} 

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

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

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

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

234 

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

236 nodal_rotation_matrices = [ 

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

238 ] 

239 

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

241 zip(self.nodes, nodal_rotation_matrices) 

242 ): 

243 coordinates[i_node, :] = node.coordinates 

244 if node.is_middle_node: 

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

246 else: 

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

248 

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

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

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

252 

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

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

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

256 element_partner_indices = list( 

257 [ 

258 node.element_partner_index 

259 for node in self.nodes 

260 if node.element_partner_index is not None 

261 ] 

262 ) 

263 if len(element_partner_indices) > 1: 

264 raise ValueError( 

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

266 "functionality" 

267 ) 

268 elif len(element_partner_indices) == 1: 

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

270 

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

272 # points for a smooth beam visualization. 

273 if beam_centerline_visualization_segments == 1: 

274 point_connectivity = _np.arange(n_nodes) 

275 else: 

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

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

278 # nodes which we have already added above. 

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

280 hermite_shape_functions_pos = _np.array( 

281 [ 

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

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

284 ] 

285 ).transpose() 

286 hermite_shape_functions_tan = _np.array( 

287 [ 

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

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

290 ] 

291 ).transpose() 

292 

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

294 

295 for i_segment in range(n_segments): 

296 positions = _np.array( 

297 [ 

298 self.nodes[i_node].coordinates 

299 for i_node in [i_segment, i_segment + 1] 

300 ] 

301 ) 

302 tangents = _np.array( 

303 [ 

304 nodal_rotation_matrices[i_node][:, 0] 

305 for i_node in [i_segment, i_segment + 1] 

306 ] 

307 ) 

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

309 interpolated_coordinates = _np.dot( 

310 hermite_shape_functions_pos, positions 

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

312 

313 index_first_point = ( 

314 n_nodes + i_segment * n_additional_points_per_segment 

315 ) 

316 index_last_point = ( 

317 n_nodes + (i_segment + 1) * n_additional_points_per_segment 

318 ) 

319 

320 coordinates[index_first_point:index_last_point] = ( 

321 interpolated_coordinates 

322 ) 

323 point_connectivity[ 

324 i_segment * beam_centerline_visualization_segments 

325 ] = i_segment 

326 point_connectivity[ 

327 (i_segment + 1) * beam_centerline_visualization_segments 

328 ] = i_segment + 1 

329 point_connectivity[ 

330 i_segment * beam_centerline_visualization_segments + 1 : ( 

331 i_segment + 1 

332 ) 

333 * beam_centerline_visualization_segments 

334 ] = _np.arange(index_first_point, index_last_point) 

335 

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

337 _add_point_data_node_sets( 

338 point_data, self.nodes, extra_points=n_additional_points 

339 ) 

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

341 vtk_writer_beam.add_cell( 

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

343 )