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
« 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."""
25from enum import Enum as _Enum
26from enum import auto as _auto
28import numpy as _np
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
38# Format template for different number types.
39F_INT = "{:6d}"
40F_FLOAT = "{: .14e}"
43def set_i_global(data_list, *, start_index=0):
44 """Set i_global in every item of data_list.
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 """
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!")
58 # Set the values for i_global.
59 for i, item in enumerate(data_list):
60 item.i_global = i + start_index
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
79class AbaqusBeamNormalDefinition(_Enum):
80 """Enum for different ways to define the beam cross-section normal.
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 """
86 normal_and_extra_node = _auto()
87 """Create an extra node and the nodal normal information for each node."""
89 normal = _auto()
90 """Create the nodal normal information for each node."""
93class AbaqusInputFile(object):
94 """This class represents an Abaqus input file."""
96 def __init__(self, mesh: _Mesh):
97 """Initialize the input file.
99 Args
100 ----
101 mesh: Mesh()
102 Mesh to be used in this input file.
103 """
104 self.mesh = mesh
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.
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 """
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")
127 def get_input_file_string(self, normal_definition):
128 """Generate the string for the Abaqus input file."""
130 # Perform some checks on the mesh.
131 if _mpy.check_overlapping_elements:
132 self.mesh.check_overlapping_elements()
134 # Assign global indices to all materials
135 set_i_global(self.mesh.materials)
137 # Calculate the required cross-section normal data
138 self.calculate_cross_section_normal_data(normal_definition)
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)
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.
154 For more information see the Abaqus documentation on: "Beam element cross-section orientation"
156 Args
157 ----
158 normal_definition: AbaqusBeamNormalDefinition
159 How the beam cross-section should be defined.
160 """
162 def normalize(vector):
163 """Normalize a vector."""
164 return vector / _np.linalg.norm(vector)
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.
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))]
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.
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)
198 rotation = element.nodes[0].rotation
199 cross_section_rotation = _smallest_rotation(rotation, t)
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}")
214 def get_nodes_lines(self):
215 """Get the lines for the input file that represent the nodes."""
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 )
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 )
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
246 return input_file_lines
248 def get_element_lines(self):
249 """Get the lines for the input file that represent the elements."""
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]
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)
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))
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 )
287 element_count += len(elements)
289 if len(normal_lines) > 1:
290 return element_lines + normal_lines
291 else:
292 return element_lines
294 def get_material_lines(self):
295 """Get the lines for the input file that represent the element sets
296 with the same material."""
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]
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
313 def get_set_lines(self):
314 """Add lines to the input file that represent node and element sets."""
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