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

129 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 module defines the class that is used to create an input file for 

23Abaqus.""" 

24 

25from enum import Enum as _Enum 

26from enum import auto as _auto 

27 

28import numpy as _np 

29 

30from beamme.core.conf import INPUT_FILE_HEADER as _INPUT_FILE_HEADER 

31from beamme.core.conf import bme as _bme 

32from beamme.core.geometry_set import GeometrySet as _GeometrySet 

33from beamme.core.mesh import Mesh as _Mesh 

34from beamme.core.mesh_utils import ( 

35 get_coupled_nodes_to_master_map as _get_coupled_nodes_to_master_map, 

36) 

37from beamme.core.rotation import smallest_rotation as _smallest_rotation 

38 

39# Format template for different number types. 

40F_INT = "{:6d}" 

41F_FLOAT = "{: .14e}" 

42 

43 

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

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

46 

47 Args 

48 ---- 

49 data_list: 

50 List containing the items that should be numbered 

51 start_index: int 

52 Starting index of the numbering 

53 """ 

54 

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

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

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

58 

59 # Set the values for i_global. 

60 for i, item in enumerate(data_list): 

61 item.i_global = i + start_index 

62 

63 

64def get_set_lines(set_type, items, name): 

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

66 row)""" 

67 max_entries_per_line = 16 

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

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

70 set_ids.sort() 

71 set_ids = [ 

72 set_ids[i : i + max_entries_per_line] 

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

74 ] 

75 for ids in set_ids: 

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

77 return lines 

78 

79 

80class AbaqusBeamNormalDefinition(_Enum): 

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

82 

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

84 and the function `AbaqusInputFile.calculate_cross_section_normal_data`. 

85 """ 

86 

87 normal_and_extra_node = _auto() 

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

89 

90 normal = _auto() 

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

92 

93 

94class AbaqusInputFile(object): 

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

96 

97 def __init__(self, mesh: _Mesh): 

98 """Initialize the input file. 

99 

100 Args 

101 ---- 

102 mesh: Mesh() 

103 Mesh to be used in this input file. 

104 """ 

105 self.mesh = mesh 

106 

107 def write_input_file( 

108 self, 

109 file_path, 

110 *, 

111 normal_definition=AbaqusBeamNormalDefinition.normal_and_extra_node, 

112 ): 

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

114 

115 Args 

116 ---- 

117 file_path: path 

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

119 normal_definition: AbaqusBeamNormalDefinition 

120 How the beam cross-section should be defined. 

121 """ 

122 

123 # Write the input file to disk 

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

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

126 input_file.write("\n") 

127 

128 def get_input_file_string(self, normal_definition): 

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

130 

131 # Assign global indices to all materials 

132 set_i_global(self.mesh.materials) 

133 

134 # Calculate the required cross-section normal data 

135 self.calculate_cross_section_normal_data(normal_definition) 

136 

137 # Add the lines to the input file 

138 input_file_lines = [] 

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

140 input_file_lines.extend(self.get_nodes_lines()) 

141 input_file_lines.extend(self.get_element_lines()) 

142 input_file_lines.extend(self.get_material_lines()) 

143 input_file_lines.extend(self.get_set_lines()) 

144 return "\n".join(input_file_lines) 

145 

146 def calculate_cross_section_normal_data(self, normal_definition): 

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

148 section orientation in Abaqus. The evaluated data is stored in the 

149 elements. 

150 

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

152 

153 Args 

154 ---- 

155 normal_definition: AbaqusBeamNormalDefinition 

156 How the beam cross-section should be defined. 

157 """ 

158 

159 def normalize(vector): 

160 """Normalize a vector.""" 

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

162 

163 # Reset possibly existing data stored in the elements 

164 # element.n1_orientation_node: list(float) 

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

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

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

168 # element.n1_node_id: str 

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

170 # node. 

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

172 # A list containing possible explicit normal definitions for each 

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

174 # *NORMAL section of the input file. 

175 

176 for element in self.mesh.elements: 

177 element.n1_position = None 

178 element.n1_node_id = None 

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

180 

181 if ( 

182 normal_definition == AbaqusBeamNormalDefinition.normal 

183 or normal_definition == AbaqusBeamNormalDefinition.normal_and_extra_node 

184 ): 

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

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

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

188 # the tangent. 

189 

190 for element in self.mesh.elements: 

191 node_1 = element.nodes[0].coordinates 

192 node_2 = element.nodes[1].coordinates 

193 t = normalize(node_2 - node_1) 

194 

195 rotation = element.nodes[0].rotation 

196 cross_section_rotation = _smallest_rotation(rotation, t) 

197 

198 if ( 

199 normal_definition 

200 == AbaqusBeamNormalDefinition.normal_and_extra_node 

201 ): 

202 element.n1_position = node_1 + cross_section_rotation * [ 

203 0.0, 

204 1.0, 

205 0.0, 

206 ] 

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

208 else: 

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

210 

211 def get_nodes_lines(self): 

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

213 

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

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

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

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

218 _, unique_nodes = _get_coupled_nodes_to_master_map( 

219 self.mesh, assign_i_global=True 

220 ) 

221 

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

223 input_file_lines = ["*Node"] 

224 for node in unique_nodes: 

225 input_file_lines.append( 

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

227 node.i_global + 1, *node.coordinates 

228 ) 

229 ) 

230 

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

232 node_counter = len(unique_nodes) 

233 for element in self.mesh.elements: 

234 if element.n1_position is not None: 

235 node_counter += 1 

236 input_file_lines.append( 

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

238 node_counter, *element.n1_position 

239 ) 

240 ) 

241 element.n1_node_id = node_counter 

242 

243 return input_file_lines 

244 

245 def get_element_lines(self): 

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

247 

248 # Sort the elements after their types. 

249 element_types = {} 

250 for element in self.mesh.elements: 

251 element_type = element.beam_type 

252 if element_type in element_types.keys(): 

253 element_types[element_type].append(element) 

254 else: 

255 element_types[element_type] = [element] 

256 

257 # Write the element connectivity. 

258 element_count = 0 

259 element_lines = [] 

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

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

262 # Number the elements of this type 

263 set_i_global(elements, start_index=element_count) 

264 

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

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

267 for element in elements: 

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

269 if element.n1_node_id is not None: 

270 node_ids.append(element.n1_node_id) 

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

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

273 

274 # Set explicit normal definitions for the nodes 

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

276 if n2 is not None: 

277 node = element.nodes[i_node] 

278 normal_lines.append( 

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

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

281 ) 

282 ) 

283 

284 element_count += len(elements) 

285 

286 if len(normal_lines) > 1: 

287 return element_lines + normal_lines 

288 else: 

289 return element_lines 

290 

291 def get_material_lines(self): 

292 """Get the lines for the input file that represent the element sets 

293 with the same material.""" 

294 

295 materials = {} 

296 for element in self.mesh.elements: 

297 element_material = element.material 

298 if element_material in materials.keys(): 

299 materials[element_material].append(element) 

300 else: 

301 materials[element_material] = [element] 

302 

303 # Create the element sets for the different materials. 

304 input_file_lines = [] 

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

306 material_name = material.dump_to_list()[0] 

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

308 return input_file_lines 

309 

310 def get_set_lines(self): 

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

312 

313 input_file_lines = [] 

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

315 if point_set.name is None: 

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

317 input_file_lines.extend( 

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

319 ) 

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

321 if line_set.name is None: 

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

323 if isinstance(line_set, _GeometrySet): 

324 input_file_lines.extend( 

325 get_set_lines( 

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

327 ) 

328 ) 

329 else: 

330 raise ValueError( 

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

332 ) 

333 return input_file_lines