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

130 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-28 04:21 +0000

1# The MIT License (MIT) 

2# 

3# Copyright (c) 2018-2025 MeshPy 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 meshpy.core.conf import mpy as _mpy 

31from meshpy.core.geometry_set import GeometrySet as _GeometrySet 

32from meshpy.core.mesh import Mesh as _Mesh 

33from meshpy.core.mesh_utils import ( 

34 get_coupled_nodes_to_master_map as _get_coupled_nodes_to_master_map, 

35) 

36from meshpy.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( 

143 ["** " + line for line in _mpy.input_file_meshpy_header] 

144 ) 

145 input_file_lines.extend(self.get_nodes_lines()) 

146 input_file_lines.extend(self.get_element_lines()) 

147 input_file_lines.extend(self.get_material_lines()) 

148 input_file_lines.extend(self.get_set_lines()) 

149 return "\n".join(input_file_lines) 

150 

151 def calculate_cross_section_normal_data(self, normal_definition): 

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

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

154 elements. 

155 

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

157 

158 Args 

159 ---- 

160 normal_definition: AbaqusBeamNormalDefinition 

161 How the beam cross-section should be defined. 

162 """ 

163 

164 def normalize(vector): 

165 """Normalize a vector.""" 

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

167 

168 # Reset possibly existing data stored in the elements 

169 # element.n1_orientation_node: list(float) 

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

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

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

173 # element.n1_node_id: str 

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

175 # node. 

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

177 # A list containing possible explicit normal definitions for each 

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

179 # *NORMAL section of the input file. 

180 

181 for element in self.mesh.elements: 

182 element.n1_position = None 

183 element.n1_node_id = None 

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

185 

186 if ( 

187 normal_definition == AbaqusBeamNormalDefinition.normal 

188 or normal_definition == AbaqusBeamNormalDefinition.normal_and_extra_node 

189 ): 

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

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

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

193 # the tangent. 

194 

195 for element in self.mesh.elements: 

196 node_1 = element.nodes[0].coordinates 

197 node_2 = element.nodes[1].coordinates 

198 t = normalize(node_2 - node_1) 

199 

200 rotation = element.nodes[0].rotation 

201 cross_section_rotation = _smallest_rotation(rotation, t) 

202 

203 if ( 

204 normal_definition 

205 == AbaqusBeamNormalDefinition.normal_and_extra_node 

206 ): 

207 element.n1_position = node_1 + cross_section_rotation * [ 

208 0.0, 

209 1.0, 

210 0.0, 

211 ] 

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

213 else: 

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

215 

216 def get_nodes_lines(self): 

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

218 

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

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

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

222 # same global ID while still keeping the individual nodes in MeshPy. 

223 _, unique_nodes = _get_coupled_nodes_to_master_map( 

224 self.mesh, assign_i_global=True 

225 ) 

226 

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

228 input_file_lines = ["*Node"] 

229 for node in unique_nodes: 

230 input_file_lines.append( 

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

232 node.i_global + 1, *node.coordinates 

233 ) 

234 ) 

235 

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

237 node_counter = len(unique_nodes) 

238 for element in self.mesh.elements: 

239 if element.n1_position is not None: 

240 node_counter += 1 

241 input_file_lines.append( 

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

243 node_counter, *element.n1_position 

244 ) 

245 ) 

246 element.n1_node_id = node_counter 

247 

248 return input_file_lines 

249 

250 def get_element_lines(self): 

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

252 

253 # Sort the elements after their types. 

254 element_types = {} 

255 for element in self.mesh.elements: 

256 element_type = element.beam_type 

257 if element_type in element_types.keys(): 

258 element_types[element_type].append(element) 

259 else: 

260 element_types[element_type] = [element] 

261 

262 # Write the element connectivity. 

263 element_count = 0 

264 element_lines = [] 

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

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

267 # Number the elements of this type 

268 set_i_global(elements, start_index=element_count) 

269 

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

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

272 for element in elements: 

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

274 if element.n1_node_id is not None: 

275 node_ids.append(element.n1_node_id) 

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

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

278 

279 # Set explicit normal definitions for the nodes 

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

281 if n2 is not None: 

282 node = element.nodes[i_node] 

283 normal_lines.append( 

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

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

286 ) 

287 ) 

288 

289 element_count += len(elements) 

290 

291 if len(normal_lines) > 1: 

292 return element_lines + normal_lines 

293 else: 

294 return element_lines 

295 

296 def get_material_lines(self): 

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

298 with the same material.""" 

299 

300 materials = {} 

301 for element in self.mesh.elements: 

302 element_material = element.material 

303 if element_material in materials.keys(): 

304 materials[element_material].append(element) 

305 else: 

306 materials[element_material] = [element] 

307 

308 # Create the element sets for the different materials. 

309 input_file_lines = [] 

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

311 material_name = material.dump_to_list()[0] 

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

313 return input_file_lines 

314 

315 def get_set_lines(self): 

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

317 

318 input_file_lines = [] 

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

320 if point_set.name is None: 

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

322 input_file_lines.extend( 

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

324 ) 

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

326 if line_set.name is None: 

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

328 if isinstance(line_set, _GeometrySet): 

329 input_file_lines.extend( 

330 get_set_lines( 

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

332 ) 

333 ) 

334 else: 

335 raise ValueError( 

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

337 ) 

338 return input_file_lines