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
« 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."""
25from enum import Enum as _Enum
26from enum import auto as _auto
28import numpy as _np
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
39# Format template for different number types.
40F_INT = "{:6d}"
41F_FLOAT = "{: .14e}"
44def set_i_global(data_list, *, start_index=0):
45 """Set i_global in every item of data_list.
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 """
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!")
59 # Set the values for i_global.
60 for i, item in enumerate(data_list):
61 item.i_global = i + start_index
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
80class AbaqusBeamNormalDefinition(_Enum):
81 """Enum for different ways to define the beam cross-section normal.
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 """
87 normal_and_extra_node = _auto()
88 """Create an extra node and the nodal normal information for each node."""
90 normal = _auto()
91 """Create the nodal normal information for each node."""
94class AbaqusInputFile(object):
95 """This class represents an Abaqus input file."""
97 def __init__(self, mesh: _Mesh):
98 """Initialize the input file.
100 Args
101 ----
102 mesh: Mesh()
103 Mesh to be used in this input file.
104 """
105 self.mesh = mesh
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.
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 """
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")
128 def get_input_file_string(self, normal_definition):
129 """Generate the string for the Abaqus input file."""
131 # Assign global indices to all materials
132 set_i_global(self.mesh.materials)
134 # Calculate the required cross-section normal data
135 self.calculate_cross_section_normal_data(normal_definition)
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)
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.
151 For more information see the Abaqus documentation on: "Beam element cross-section orientation"
153 Args
154 ----
155 normal_definition: AbaqusBeamNormalDefinition
156 How the beam cross-section should be defined.
157 """
159 def normalize(vector):
160 """Normalize a vector."""
161 return vector / _np.linalg.norm(vector)
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.
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))]
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.
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)
195 rotation = element.nodes[0].rotation
196 cross_section_rotation = _smallest_rotation(rotation, t)
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}")
211 def get_nodes_lines(self):
212 """Get the lines for the input file that represent the nodes."""
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 )
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 )
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
243 return input_file_lines
245 def get_element_lines(self):
246 """Get the lines for the input file that represent the elements."""
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]
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)
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))
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 )
284 element_count += len(elements)
286 if len(normal_lines) > 1:
287 return element_lines + normal_lines
288 else:
289 return element_lines
291 def get_material_lines(self):
292 """Get the lines for the input file that represent the element sets
293 with the same material."""
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]
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
310 def get_set_lines(self):
311 """Add lines to the input file that represent node and element sets."""
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