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

131 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 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 # Perform some checks on the mesh. 

132 if _bme.check_overlapping_elements: 

133 self.mesh.check_overlapping_elements() 

134 

135 # Assign global indices to all materials 

136 set_i_global(self.mesh.materials) 

137 

138 # Calculate the required cross-section normal data 

139 self.calculate_cross_section_normal_data(normal_definition) 

140 

141 # Add the lines to the input file 

142 input_file_lines = [] 

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

144 input_file_lines.extend(self.get_nodes_lines()) 

145 input_file_lines.extend(self.get_element_lines()) 

146 input_file_lines.extend(self.get_material_lines()) 

147 input_file_lines.extend(self.get_set_lines()) 

148 return "\n".join(input_file_lines) 

149 

150 def calculate_cross_section_normal_data(self, normal_definition): 

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

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

153 elements. 

154 

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

156 

157 Args 

158 ---- 

159 normal_definition: AbaqusBeamNormalDefinition 

160 How the beam cross-section should be defined. 

161 """ 

162 

163 def normalize(vector): 

164 """Normalize a vector.""" 

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

166 

167 # Reset possibly existing data stored in the elements 

168 # element.n1_orientation_node: list(float) 

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

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

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

172 # element.n1_node_id: str 

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

174 # node. 

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

176 # A list containing possible explicit normal definitions for each 

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

178 # *NORMAL section of the input file. 

179 

180 for element in self.mesh.elements: 

181 element.n1_position = None 

182 element.n1_node_id = None 

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

184 

185 if ( 

186 normal_definition == AbaqusBeamNormalDefinition.normal 

187 or normal_definition == AbaqusBeamNormalDefinition.normal_and_extra_node 

188 ): 

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

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

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

192 # the tangent. 

193 

194 for element in self.mesh.elements: 

195 node_1 = element.nodes[0].coordinates 

196 node_2 = element.nodes[1].coordinates 

197 t = normalize(node_2 - node_1) 

198 

199 rotation = element.nodes[0].rotation 

200 cross_section_rotation = _smallest_rotation(rotation, t) 

201 

202 if ( 

203 normal_definition 

204 == AbaqusBeamNormalDefinition.normal_and_extra_node 

205 ): 

206 element.n1_position = node_1 + cross_section_rotation * [ 

207 0.0, 

208 1.0, 

209 0.0, 

210 ] 

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

212 else: 

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

214 

215 def get_nodes_lines(self): 

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

217 

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

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

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

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

222 _, unique_nodes = _get_coupled_nodes_to_master_map( 

223 self.mesh, assign_i_global=True 

224 ) 

225 

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

227 input_file_lines = ["*Node"] 

228 for node in unique_nodes: 

229 input_file_lines.append( 

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

231 node.i_global + 1, *node.coordinates 

232 ) 

233 ) 

234 

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

236 node_counter = len(unique_nodes) 

237 for element in self.mesh.elements: 

238 if element.n1_position is not None: 

239 node_counter += 1 

240 input_file_lines.append( 

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

242 node_counter, *element.n1_position 

243 ) 

244 ) 

245 element.n1_node_id = node_counter 

246 

247 return input_file_lines 

248 

249 def get_element_lines(self): 

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

251 

252 # Sort the elements after their types. 

253 element_types = {} 

254 for element in self.mesh.elements: 

255 element_type = element.beam_type 

256 if element_type in element_types.keys(): 

257 element_types[element_type].append(element) 

258 else: 

259 element_types[element_type] = [element] 

260 

261 # Write the element connectivity. 

262 element_count = 0 

263 element_lines = [] 

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

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

266 # Number the elements of this type 

267 set_i_global(elements, start_index=element_count) 

268 

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

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

271 for element in elements: 

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

273 if element.n1_node_id is not None: 

274 node_ids.append(element.n1_node_id) 

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

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

277 

278 # Set explicit normal definitions for the nodes 

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

280 if n2 is not None: 

281 node = element.nodes[i_node] 

282 normal_lines.append( 

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

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

285 ) 

286 ) 

287 

288 element_count += len(elements) 

289 

290 if len(normal_lines) > 1: 

291 return element_lines + normal_lines 

292 else: 

293 return element_lines 

294 

295 def get_material_lines(self): 

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

297 with the same material.""" 

298 

299 materials = {} 

300 for element in self.mesh.elements: 

301 element_material = element.material 

302 if element_material in materials.keys(): 

303 materials[element_material].append(element) 

304 else: 

305 materials[element_material] = [element] 

306 

307 # Create the element sets for the different materials. 

308 input_file_lines = [] 

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

310 material_name = material.dump_to_list()[0] 

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

312 return input_file_lines 

313 

314 def get_set_lines(self): 

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

316 

317 input_file_lines = [] 

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

319 if point_set.name is None: 

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

321 input_file_lines.extend( 

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

323 ) 

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

325 if line_set.name is None: 

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

327 if isinstance(line_set, _GeometrySet): 

328 input_file_lines.extend( 

329 get_set_lines( 

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

331 ) 

332 ) 

333 else: 

334 raise ValueError( 

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

336 ) 

337 return input_file_lines