Coverage for src/beamme/abaqus/input_file.py: 93%

129 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-28 15:20 +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 module defines the class that is used to create an input file for Abaqus.""" 

23 

24from enum import Enum as _Enum 

25from enum import auto as _auto 

26 

27import numpy as _np 

28 

29from beamme.core.conf import INPUT_FILE_HEADER as _INPUT_FILE_HEADER 

30from beamme.core.conf import bme as _bme 

31from beamme.core.geometry_set import GeometrySet as _GeometrySet 

32from beamme.core.mesh import Mesh as _Mesh 

33from beamme.core.mesh_utils import ( 

34 get_coupled_nodes_to_master_map as _get_coupled_nodes_to_master_map, 

35) 

36from beamme.core.rotation import smallest_rotation as _smallest_rotation 

37 

38# Format template for different number types. 

39F_INT = "{:6d}" 

40F_FLOAT = "{: .14e}" 

41 

42 

43def set_i_global(data_list, *, start_index=0): 

44 """Set i_global in every item of data_list. 

45 

46 Args 

47 ---- 

48 data_list: 

49 List containing the items that should be numbered 

50 start_index: int 

51 Starting index of the numbering 

52 """ 

53 # A check is performed that every entry in data_list is unique. 

54 if len(data_list) != len(set(data_list)): 

55 raise ValueError("Elements in data_list are not unique!") 

56 

57 # Set the values for i_global. 

58 for i, item in enumerate(data_list): 

59 item.i_global = i + start_index 

60 

61 

62def get_set_lines(set_type, items, name): 

63 """Get the Abaqus input file lines for a set of items (max 16 items per row)""" 

64 max_entries_per_line = 16 

65 lines = ["*{}, {}={}".format(set_type, set_type.lower(), name)] 

66 set_ids = [item.i_global + 1 for item in items] 

67 set_ids.sort() 

68 set_ids = [ 

69 set_ids[i : i + max_entries_per_line] 

70 for i in range(0, len(set_ids), max_entries_per_line) 

71 ] 

72 for ids in set_ids: 

73 lines.append(", ".join([F_INT.format(id) for id in ids])) 

74 return lines 

75 

76 

77class AbaqusBeamNormalDefinition(_Enum): 

78 """Enum for different ways to define the beam cross-section normal. 

79 

80 For more information see the Abaqus documentation on: "Beam element cross-section orientation" 

81 and the function `AbaqusInputFile.calculate_cross_section_normal_data`. 

82 """ 

83 

84 normal_and_extra_node = _auto() 

85 """Create an extra node and the nodal normal information for each node.""" 

86 

87 normal = _auto() 

88 """Create the nodal normal information for each node.""" 

89 

90 

91class AbaqusInputFile(object): 

92 """This class represents an Abaqus input file.""" 

93 

94 def __init__(self, mesh: _Mesh): 

95 """Initialize the input file. 

96 

97 Args 

98 ---- 

99 mesh: Mesh() 

100 Mesh to be used in this input file. 

101 """ 

102 self.mesh = mesh 

103 

104 def write_input_file( 

105 self, 

106 file_path, 

107 *, 

108 normal_definition=AbaqusBeamNormalDefinition.normal_and_extra_node, 

109 ): 

110 """Write the ASCII input file to disk. 

111 

112 Args 

113 ---- 

114 file_path: path 

115 Path on the disk, where the input file should be stored. 

116 normal_definition: AbaqusBeamNormalDefinition 

117 How the beam cross-section should be defined. 

118 """ 

119 # Write the input file to disk 

120 with open(file_path, "w") as input_file: 

121 input_file.write(self.get_input_file_string(normal_definition)) 

122 input_file.write("\n") 

123 

124 def get_input_file_string(self, normal_definition): 

125 """Generate the string for the Abaqus input file.""" 

126 # Assign global indices to all materials 

127 set_i_global(self.mesh.materials) 

128 

129 # Calculate the required cross-section normal data 

130 self.calculate_cross_section_normal_data(normal_definition) 

131 

132 # Add the lines to the input file 

133 input_file_lines = [] 

134 input_file_lines.extend(["** " + line for line in _INPUT_FILE_HEADER]) 

135 input_file_lines.extend(self.get_nodes_lines()) 

136 input_file_lines.extend(self.get_element_lines()) 

137 input_file_lines.extend(self.get_material_lines()) 

138 input_file_lines.extend(self.get_set_lines()) 

139 return "\n".join(input_file_lines) 

140 

141 def calculate_cross_section_normal_data(self, normal_definition): 

142 """Evaluate all data that is required to fully specify the cross- section 

143 orientation in Abaqus. The evaluated data is stored in the elements. 

144 

145 For more information see the Abaqus documentation on: "Beam element cross-section orientation" 

146 

147 Args 

148 ---- 

149 normal_definition: AbaqusBeamNormalDefinition 

150 How the beam cross-section should be defined. 

151 """ 

152 

153 def normalize(vector): 

154 """Normalize a vector.""" 

155 return vector / _np.linalg.norm(vector) 

156 

157 # Reset possibly existing data stored in the elements 

158 # element.n1_orientation_node: list(float) 

159 # The coordinates of an additional (dummy) node connected to the 

160 # element to define its approximate n1 direction. It this is None, 

161 # no additional node will be added to the input file. 

162 # element.n1_node_id: str 

163 # The global ID in the input file for the additional orientation 

164 # node. 

165 # element.n2: list(list(float)): 

166 # A list containing possible explicit normal definitions for each 

167 # element node. All entries that are not None will be added to the 

168 # *NORMAL section of the input file. 

169 

170 for element in self.mesh.elements: 

171 element.n1_position = None 

172 element.n1_node_id = None 

173 element.n2 = [None for i_node in range(len(element.nodes))] 

174 

175 if ( 

176 normal_definition == AbaqusBeamNormalDefinition.normal 

177 or normal_definition == AbaqusBeamNormalDefinition.normal_and_extra_node 

178 ): 

179 # In this case we take the beam tangent from the first to the second node 

180 # and calculate an ortho-normal triad based on this direction. We do this 

181 # via a smallest rotation mapping from the triad of the first node onto 

182 # the tangent. 

183 

184 for element in self.mesh.elements: 

185 node_1 = element.nodes[0].coordinates 

186 node_2 = element.nodes[1].coordinates 

187 t = normalize(node_2 - node_1) 

188 

189 rotation = element.nodes[0].rotation 

190 cross_section_rotation = _smallest_rotation(rotation, t) 

191 

192 if ( 

193 normal_definition 

194 == AbaqusBeamNormalDefinition.normal_and_extra_node 

195 ): 

196 element.n1_position = node_1 + cross_section_rotation * [ 

197 0.0, 

198 1.0, 

199 0.0, 

200 ] 

201 element.n2[0] = cross_section_rotation * [0.0, 0.0, 1.0] 

202 else: 

203 raise ValueError(f"Got unexpected normal_definition {normal_definition}") 

204 

205 def get_nodes_lines(self): 

206 """Get the lines for the input file that represent the nodes.""" 

207 # The nodes require postprocessing, as we have to identify coupled nodes in Abaqus. 

208 # Internally in Abaqus, coupled nodes are a single node with different normals for the 

209 # connected element. Therefore, for nodes which are coupled to each other, we keep the 

210 # same global ID while still keeping the individual nodes. 

211 _, unique_nodes = _get_coupled_nodes_to_master_map( 

212 self.mesh, assign_i_global=True 

213 ) 

214 

215 # Number the remaining nodes and create nodes for the input file 

216 input_file_lines = ["*Node"] 

217 for node in unique_nodes: 

218 input_file_lines.append( 

219 (", ".join([F_INT] + 3 * [F_FLOAT])).format( 

220 node.i_global + 1, *node.coordinates 

221 ) 

222 ) 

223 

224 # Check if we need to write additional nodes for the element cross-section directions 

225 node_counter = len(unique_nodes) 

226 for element in self.mesh.elements: 

227 if element.n1_position is not None: 

228 node_counter += 1 

229 input_file_lines.append( 

230 (", ".join([F_INT] + 3 * [F_FLOAT])).format( 

231 node_counter, *element.n1_position 

232 ) 

233 ) 

234 element.n1_node_id = node_counter 

235 

236 return input_file_lines 

237 

238 def get_element_lines(self): 

239 """Get the lines for the input file that represent the elements.""" 

240 # Sort the elements after their types. 

241 element_types = {} 

242 for element in self.mesh.elements: 

243 element_type = element.beam_type 

244 if element_type in element_types.keys(): 

245 element_types[element_type].append(element) 

246 else: 

247 element_types[element_type] = [element] 

248 

249 # Write the element connectivity. 

250 element_count = 0 

251 element_lines = [] 

252 normal_lines = ["*Normal, type=element"] 

253 for element_type, elements in element_types.items(): 

254 # Number the elements of this type 

255 set_i_global(elements, start_index=element_count) 

256 

257 # Set the element connectivity, possibly including the n1 direction node 

258 element_lines.append("*Element, type={}".format(element_type)) 

259 for element in elements: 

260 node_ids = [node.i_global + 1 for node in element.nodes] 

261 if element.n1_node_id is not None: 

262 node_ids.append(element.n1_node_id) 

263 line_ids = [element.i_global + 1] + node_ids 

264 element_lines.append(", ".join(F_INT.format(i) for i in line_ids)) 

265 

266 # Set explicit normal definitions for the nodes 

267 for i_node, n2 in enumerate(element.n2): 

268 if n2 is not None: 

269 node = element.nodes[i_node] 

270 normal_lines.append( 

271 (", ".join(2 * [F_INT] + 3 * [F_FLOAT])).format( 

272 element.i_global + 1, node.i_global + 1, *n2 

273 ) 

274 ) 

275 

276 element_count += len(elements) 

277 

278 if len(normal_lines) > 1: 

279 return element_lines + normal_lines 

280 else: 

281 return element_lines 

282 

283 def get_material_lines(self): 

284 """Get the lines for the input file that represent the element sets with the 

285 same material.""" 

286 materials = {} 

287 for element in self.mesh.elements: 

288 element_material = element.material 

289 if element_material in materials.keys(): 

290 materials[element_material].append(element) 

291 else: 

292 materials[element_material] = [element] 

293 

294 # Create the element sets for the different materials. 

295 input_file_lines = [] 

296 for material, elements in materials.items(): 

297 material_name = material.dump_to_list()[0] 

298 input_file_lines.extend(get_set_lines("Elset", elements, material_name)) 

299 return input_file_lines 

300 

301 def get_set_lines(self): 

302 """Add lines to the input file that represent node and element sets.""" 

303 input_file_lines = [] 

304 for point_set in self.mesh.geometry_sets[_bme.geo.point]: 

305 if point_set.name is None: 

306 raise ValueError("Sets added to the mesh have to have a valid name!") 

307 input_file_lines.extend( 

308 get_set_lines("Nset", point_set.get_points(), point_set.name) 

309 ) 

310 for line_set in self.mesh.geometry_sets[_bme.geo.line]: 

311 if line_set.name is None: 

312 raise ValueError("Sets added to the mesh have to have a valid name!") 

313 if isinstance(line_set, _GeometrySet): 

314 input_file_lines.extend( 

315 get_set_lines( 

316 "Elset", line_set.geometry_objects[_bme.geo.line], line_set.name 

317 ) 

318 ) 

319 else: 

320 raise ValueError( 

321 "Line sets can only be exported to Abaqus if they are defined with the beam elements" 

322 ) 

323 return input_file_lines