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

78 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"""This file defines the base beam element.""" 

23 

24from typing import Any as _Any 

25 

26import numpy as _np 

27import pyvista as _pv 

28import vtk as _vtk 

29 

30from beamme.core.conf import bme as _bme 

31from beamme.core.element import Element as _Element 

32from beamme.core.vtk_writer import add_point_data_node_sets as _add_point_data_node_sets 

33 

34 

35class Beam(_Element): 

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

37 

38 # Cell type for representing this element in vtk. 

39 vtk_cell_type = _pv.CellType.POLY_LINE 

40 

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

42 # in ascending order. 

43 nodes_create: _Any = [] 

44 

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

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

47 

48 @classmethod 

49 def get_coupling_dict(cls, coupling_dof_type): 

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

51 

52 match coupling_dof_type: 

53 case _bme.coupling_dof.joint: 

54 if cls.coupling_joint_dict is None: 

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

56 return cls.coupling_joint_dict 

57 case _bme.coupling_dof.fix: 

58 if cls.coupling_fix_dict is None: 

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

60 return cls.coupling_fix_dict 

61 case _: 

62 raise ValueError( 

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

64 ) 

65 

66 def flip(self): 

67 """Reverse the nodes of this element. 

68 

69 This is usually used when reflected. 

70 """ 

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

72 

73 def get_vtk( 

74 self, 

75 vtk_writer_beam, 

76 vtk_writer_solid, 

77 *, 

78 beam_centerline_visualization_segments=1, 

79 **kwargs, 

80 ): 

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

82 line. 

83 

84 Args 

85 ---- 

86 vtk_writer_beam: 

87 VTK writer for the beams. 

88 vtk_writer_solid: 

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

90 beam_centerline_visualization_segments: int 

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

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

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

94 visualization purposes. 

95 """ 

96 

97 n_nodes = len(self.nodes) 

98 n_segments = n_nodes - 1 

99 n_additional_points_per_segment = beam_centerline_visualization_segments - 1 

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

101 n_additional_points = n_segments * n_additional_points_per_segment 

102 n_points = n_nodes + n_additional_points 

103 

104 # Dictionary with cell data. 

105 cell_data = self.vtk_cell_data.copy() 

106 if self.material.radius is not None: 

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

108 

109 # Dictionary with point data. 

110 point_data = {} 

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

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

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

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

115 

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

117 nodal_rotation_matrices = [ 

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

119 ] 

120 

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

122 zip(self.nodes, nodal_rotation_matrices) 

123 ): 

124 coordinates[i_node, :] = node.coordinates 

125 if node.is_middle_node: 

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

127 else: 

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

129 

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

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

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

133 

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

135 # points for a smooth beam visualization. 

136 if beam_centerline_visualization_segments == 1: 

137 point_connectivity = _np.arange(n_nodes) 

138 else: 

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

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

141 # nodes which we have already added above. 

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

143 hermite_shape_functions_pos = _np.array( 

144 [ 

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

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

147 ] 

148 ).transpose() 

149 hermite_shape_functions_tan = _np.array( 

150 [ 

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

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

153 ] 

154 ).transpose() 

155 

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

157 

158 for i_segment in range(n_segments): 

159 positions = _np.array( 

160 [ 

161 self.nodes[i_node].coordinates 

162 for i_node in [i_segment, i_segment + 1] 

163 ] 

164 ) 

165 tangents = _np.array( 

166 [ 

167 nodal_rotation_matrices[i_node][:, 0] 

168 for i_node in [i_segment, i_segment + 1] 

169 ] 

170 ) 

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

172 interpolated_coordinates = _np.dot( 

173 hermite_shape_functions_pos, positions 

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

175 

176 index_first_point = ( 

177 n_nodes + i_segment * n_additional_points_per_segment 

178 ) 

179 index_last_point = ( 

180 n_nodes + (i_segment + 1) * n_additional_points_per_segment 

181 ) 

182 

183 coordinates[index_first_point:index_last_point] = ( 

184 interpolated_coordinates 

185 ) 

186 point_connectivity[ 

187 i_segment * beam_centerline_visualization_segments 

188 ] = i_segment 

189 point_connectivity[ 

190 (i_segment + 1) * beam_centerline_visualization_segments 

191 ] = i_segment + 1 

192 point_connectivity[ 

193 i_segment * beam_centerline_visualization_segments + 1 : ( 

194 i_segment + 1 

195 ) 

196 * beam_centerline_visualization_segments 

197 ] = _np.arange(index_first_point, index_last_point) 

198 

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

200 _add_point_data_node_sets( 

201 point_data, self.nodes, extra_points=n_additional_points 

202 ) 

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

204 vtk_writer_beam.add_cell( 

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

206 ) 

207 

208 

209def generate_beam_class(n_nodes: int): 

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

211 

212 Args: 

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

214 

215 Returns: 

216 A beam object that has n_nodes along the centerline. 

217 """ 

218 

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

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

221 

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

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

224 

225 

226Beam2 = generate_beam_class(2) 

227Beam3 = generate_beam_class(3) 

228Beam4 = generate_beam_class(4) 

229Beam5 = generate_beam_class(5)