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

130 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-30 18:48 +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 mpy as _mpy 

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 

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

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

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

57 

58 # Set the values for i_global. 

59 for i, item in enumerate(data_list): 

60 item.i_global = i + start_index 

61 

62 

63def get_set_lines(set_type, items, name): 

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

65 row)""" 

66 max_entries_per_line = 16 

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

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

69 set_ids.sort() 

70 set_ids = [ 

71 set_ids[i : i + max_entries_per_line] 

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

73 ] 

74 for ids in set_ids: 

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

76 return lines 

77 

78 

79class AbaqusBeamNormalDefinition(_Enum): 

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

81 

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

83 and the function `AbaqusInputFile.calculate_cross_section_normal_data`. 

84 """ 

85 

86 normal_and_extra_node = _auto() 

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

88 

89 normal = _auto() 

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

91 

92 

93class AbaqusInputFile(object): 

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

95 

96 def __init__(self, mesh: _Mesh): 

97 """Initialize the input file. 

98 

99 Args 

100 ---- 

101 mesh: Mesh() 

102 Mesh to be used in this input file. 

103 """ 

104 self.mesh = mesh 

105 

106 def write_input_file( 

107 self, 

108 file_path, 

109 *, 

110 normal_definition=AbaqusBeamNormalDefinition.normal_and_extra_node, 

111 ): 

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

113 

114 Args 

115 ---- 

116 file_path: path 

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

118 normal_definition: AbaqusBeamNormalDefinition 

119 How the beam cross-section should be defined. 

120 """ 

121 

122 # Write the input file to disk 

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

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

125 input_file.write("\n") 

126 

127 def get_input_file_string(self, normal_definition): 

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

129 

130 # Perform some checks on the mesh. 

131 if _mpy.check_overlapping_elements: 

132 self.mesh.check_overlapping_elements() 

133 

134 # Assign global indices to all materials 

135 set_i_global(self.mesh.materials) 

136 

137 # Calculate the required cross-section normal data 

138 self.calculate_cross_section_normal_data(normal_definition) 

139 

140 # Add the lines to the input file 

141 input_file_lines = [] 

142 input_file_lines.extend(["** " + line for line in _mpy.input_file_header]) 

143 input_file_lines.extend(self.get_nodes_lines()) 

144 input_file_lines.extend(self.get_element_lines()) 

145 input_file_lines.extend(self.get_material_lines()) 

146 input_file_lines.extend(self.get_set_lines()) 

147 return "\n".join(input_file_lines) 

148 

149 def calculate_cross_section_normal_data(self, normal_definition): 

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

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

152 elements. 

153 

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

155 

156 Args 

157 ---- 

158 normal_definition: AbaqusBeamNormalDefinition 

159 How the beam cross-section should be defined. 

160 """ 

161 

162 def normalize(vector): 

163 """Normalize a vector.""" 

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

165 

166 # Reset possibly existing data stored in the elements 

167 # element.n1_orientation_node: list(float) 

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

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

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

171 # element.n1_node_id: str 

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

173 # node. 

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

175 # A list containing possible explicit normal definitions for each 

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

177 # *NORMAL section of the input file. 

178 

179 for element in self.mesh.elements: 

180 element.n1_position = None 

181 element.n1_node_id = None 

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

183 

184 if ( 

185 normal_definition == AbaqusBeamNormalDefinition.normal 

186 or normal_definition == AbaqusBeamNormalDefinition.normal_and_extra_node 

187 ): 

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

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

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

191 # the tangent. 

192 

193 for element in self.mesh.elements: 

194 node_1 = element.nodes[0].coordinates 

195 node_2 = element.nodes[1].coordinates 

196 t = normalize(node_2 - node_1) 

197 

198 rotation = element.nodes[0].rotation 

199 cross_section_rotation = _smallest_rotation(rotation, t) 

200 

201 if ( 

202 normal_definition 

203 == AbaqusBeamNormalDefinition.normal_and_extra_node 

204 ): 

205 element.n1_position = node_1 + cross_section_rotation * [ 

206 0.0, 

207 1.0, 

208 0.0, 

209 ] 

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

211 else: 

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

213 

214 def get_nodes_lines(self): 

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

216 

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

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

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

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

221 _, unique_nodes = _get_coupled_nodes_to_master_map( 

222 self.mesh, assign_i_global=True 

223 ) 

224 

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

226 input_file_lines = ["*Node"] 

227 for node in unique_nodes: 

228 input_file_lines.append( 

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

230 node.i_global + 1, *node.coordinates 

231 ) 

232 ) 

233 

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

235 node_counter = len(unique_nodes) 

236 for element in self.mesh.elements: 

237 if element.n1_position is not None: 

238 node_counter += 1 

239 input_file_lines.append( 

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

241 node_counter, *element.n1_position 

242 ) 

243 ) 

244 element.n1_node_id = node_counter 

245 

246 return input_file_lines 

247 

248 def get_element_lines(self): 

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

250 

251 # Sort the elements after their types. 

252 element_types = {} 

253 for element in self.mesh.elements: 

254 element_type = element.beam_type 

255 if element_type in element_types.keys(): 

256 element_types[element_type].append(element) 

257 else: 

258 element_types[element_type] = [element] 

259 

260 # Write the element connectivity. 

261 element_count = 0 

262 element_lines = [] 

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

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

265 # Number the elements of this type 

266 set_i_global(elements, start_index=element_count) 

267 

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

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

270 for element in elements: 

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

272 if element.n1_node_id is not None: 

273 node_ids.append(element.n1_node_id) 

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

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

276 

277 # Set explicit normal definitions for the nodes 

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

279 if n2 is not None: 

280 node = element.nodes[i_node] 

281 normal_lines.append( 

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

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

284 ) 

285 ) 

286 

287 element_count += len(elements) 

288 

289 if len(normal_lines) > 1: 

290 return element_lines + normal_lines 

291 else: 

292 return element_lines 

293 

294 def get_material_lines(self): 

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

296 with the same material.""" 

297 

298 materials = {} 

299 for element in self.mesh.elements: 

300 element_material = element.material 

301 if element_material in materials.keys(): 

302 materials[element_material].append(element) 

303 else: 

304 materials[element_material] = [element] 

305 

306 # Create the element sets for the different materials. 

307 input_file_lines = [] 

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

309 material_name = material.dump_to_list()[0] 

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

311 return input_file_lines 

312 

313 def get_set_lines(self): 

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

315 

316 input_file_lines = [] 

317 for point_set in self.mesh.geometry_sets[_mpy.geo.point]: 

318 if point_set.name is None: 

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

320 input_file_lines.extend( 

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

322 ) 

323 for line_set in self.mesh.geometry_sets[_mpy.geo.line]: 

324 if line_set.name is None: 

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

326 if isinstance(line_set, _GeometrySet): 

327 input_file_lines.extend( 

328 get_set_lines( 

329 "Elset", line_set.geometry_objects[_mpy.geo.line], line_set.name 

330 ) 

331 ) 

332 else: 

333 raise ValueError( 

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

335 ) 

336 return input_file_lines