Coverage for src/beamme/abaqus/input_file.py: 93%
129 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-28 15:20 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-28 15:20 +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 Abaqus."""
24from enum import Enum as _Enum
25from enum import auto as _auto
27import numpy as _np
29from beamme.core.conf import INPUT_FILE_HEADER as _INPUT_FILE_HEADER
30from beamme.core.conf import bme as _bme
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 """
53 # A check is performed that every entry in data_list is unique.
54 if len(data_list) != len(set(data_list)):
55 raise ValueError("Elements in data_list are not unique!")
57 # Set the values for i_global.
58 for i, item in enumerate(data_list):
59 item.i_global = i + start_index
62def get_set_lines(set_type, items, name):
63 """Get the Abaqus input file lines for a set of items (max 16 items per row)"""
64 max_entries_per_line = 16
65 lines = ["*{}, {}={}".format(set_type, set_type.lower(), name)]
66 set_ids = [item.i_global + 1 for item in items]
67 set_ids.sort()
68 set_ids = [
69 set_ids[i : i + max_entries_per_line]
70 for i in range(0, len(set_ids), max_entries_per_line)
71 ]
72 for ids in set_ids:
73 lines.append(", ".join([F_INT.format(id) for id in ids]))
74 return lines
77class AbaqusBeamNormalDefinition(_Enum):
78 """Enum for different ways to define the beam cross-section normal.
80 For more information see the Abaqus documentation on: "Beam element cross-section orientation"
81 and the function `AbaqusInputFile.calculate_cross_section_normal_data`.
82 """
84 normal_and_extra_node = _auto()
85 """Create an extra node and the nodal normal information for each node."""
87 normal = _auto()
88 """Create the nodal normal information for each node."""
91class AbaqusInputFile(object):
92 """This class represents an Abaqus input file."""
94 def __init__(self, mesh: _Mesh):
95 """Initialize the input file.
97 Args
98 ----
99 mesh: Mesh()
100 Mesh to be used in this input file.
101 """
102 self.mesh = mesh
104 def write_input_file(
105 self,
106 file_path,
107 *,
108 normal_definition=AbaqusBeamNormalDefinition.normal_and_extra_node,
109 ):
110 """Write the ASCII input file to disk.
112 Args
113 ----
114 file_path: path
115 Path on the disk, where the input file should be stored.
116 normal_definition: AbaqusBeamNormalDefinition
117 How the beam cross-section should be defined.
118 """
119 # Write the input file to disk
120 with open(file_path, "w") as input_file:
121 input_file.write(self.get_input_file_string(normal_definition))
122 input_file.write("\n")
124 def get_input_file_string(self, normal_definition):
125 """Generate the string for the Abaqus input file."""
126 # Assign global indices to all materials
127 set_i_global(self.mesh.materials)
129 # Calculate the required cross-section normal data
130 self.calculate_cross_section_normal_data(normal_definition)
132 # Add the lines to the input file
133 input_file_lines = []
134 input_file_lines.extend(["** " + line for line in _INPUT_FILE_HEADER])
135 input_file_lines.extend(self.get_nodes_lines())
136 input_file_lines.extend(self.get_element_lines())
137 input_file_lines.extend(self.get_material_lines())
138 input_file_lines.extend(self.get_set_lines())
139 return "\n".join(input_file_lines)
141 def calculate_cross_section_normal_data(self, normal_definition):
142 """Evaluate all data that is required to fully specify the cross- section
143 orientation in Abaqus. The evaluated data is stored in the elements.
145 For more information see the Abaqus documentation on: "Beam element cross-section orientation"
147 Args
148 ----
149 normal_definition: AbaqusBeamNormalDefinition
150 How the beam cross-section should be defined.
151 """
153 def normalize(vector):
154 """Normalize a vector."""
155 return vector / _np.linalg.norm(vector)
157 # Reset possibly existing data stored in the elements
158 # element.n1_orientation_node: list(float)
159 # The coordinates of an additional (dummy) node connected to the
160 # element to define its approximate n1 direction. It this is None,
161 # no additional node will be added to the input file.
162 # element.n1_node_id: str
163 # The global ID in the input file for the additional orientation
164 # node.
165 # element.n2: list(list(float)):
166 # A list containing possible explicit normal definitions for each
167 # element node. All entries that are not None will be added to the
168 # *NORMAL section of the input file.
170 for element in self.mesh.elements:
171 element.n1_position = None
172 element.n1_node_id = None
173 element.n2 = [None for i_node in range(len(element.nodes))]
175 if (
176 normal_definition == AbaqusBeamNormalDefinition.normal
177 or normal_definition == AbaqusBeamNormalDefinition.normal_and_extra_node
178 ):
179 # In this case we take the beam tangent from the first to the second node
180 # and calculate an ortho-normal triad based on this direction. We do this
181 # via a smallest rotation mapping from the triad of the first node onto
182 # the tangent.
184 for element in self.mesh.elements:
185 node_1 = element.nodes[0].coordinates
186 node_2 = element.nodes[1].coordinates
187 t = normalize(node_2 - node_1)
189 rotation = element.nodes[0].rotation
190 cross_section_rotation = _smallest_rotation(rotation, t)
192 if (
193 normal_definition
194 == AbaqusBeamNormalDefinition.normal_and_extra_node
195 ):
196 element.n1_position = node_1 + cross_section_rotation * [
197 0.0,
198 1.0,
199 0.0,
200 ]
201 element.n2[0] = cross_section_rotation * [0.0, 0.0, 1.0]
202 else:
203 raise ValueError(f"Got unexpected normal_definition {normal_definition}")
205 def get_nodes_lines(self):
206 """Get the lines for the input file that represent the nodes."""
207 # The nodes require postprocessing, as we have to identify coupled nodes in Abaqus.
208 # Internally in Abaqus, coupled nodes are a single node with different normals for the
209 # connected element. Therefore, for nodes which are coupled to each other, we keep the
210 # same global ID while still keeping the individual nodes.
211 _, unique_nodes = _get_coupled_nodes_to_master_map(
212 self.mesh, assign_i_global=True
213 )
215 # Number the remaining nodes and create nodes for the input file
216 input_file_lines = ["*Node"]
217 for node in unique_nodes:
218 input_file_lines.append(
219 (", ".join([F_INT] + 3 * [F_FLOAT])).format(
220 node.i_global + 1, *node.coordinates
221 )
222 )
224 # Check if we need to write additional nodes for the element cross-section directions
225 node_counter = len(unique_nodes)
226 for element in self.mesh.elements:
227 if element.n1_position is not None:
228 node_counter += 1
229 input_file_lines.append(
230 (", ".join([F_INT] + 3 * [F_FLOAT])).format(
231 node_counter, *element.n1_position
232 )
233 )
234 element.n1_node_id = node_counter
236 return input_file_lines
238 def get_element_lines(self):
239 """Get the lines for the input file that represent the elements."""
240 # Sort the elements after their types.
241 element_types = {}
242 for element in self.mesh.elements:
243 element_type = element.beam_type
244 if element_type in element_types.keys():
245 element_types[element_type].append(element)
246 else:
247 element_types[element_type] = [element]
249 # Write the element connectivity.
250 element_count = 0
251 element_lines = []
252 normal_lines = ["*Normal, type=element"]
253 for element_type, elements in element_types.items():
254 # Number the elements of this type
255 set_i_global(elements, start_index=element_count)
257 # Set the element connectivity, possibly including the n1 direction node
258 element_lines.append("*Element, type={}".format(element_type))
259 for element in elements:
260 node_ids = [node.i_global + 1 for node in element.nodes]
261 if element.n1_node_id is not None:
262 node_ids.append(element.n1_node_id)
263 line_ids = [element.i_global + 1] + node_ids
264 element_lines.append(", ".join(F_INT.format(i) for i in line_ids))
266 # Set explicit normal definitions for the nodes
267 for i_node, n2 in enumerate(element.n2):
268 if n2 is not None:
269 node = element.nodes[i_node]
270 normal_lines.append(
271 (", ".join(2 * [F_INT] + 3 * [F_FLOAT])).format(
272 element.i_global + 1, node.i_global + 1, *n2
273 )
274 )
276 element_count += len(elements)
278 if len(normal_lines) > 1:
279 return element_lines + normal_lines
280 else:
281 return element_lines
283 def get_material_lines(self):
284 """Get the lines for the input file that represent the element sets with the
285 same material."""
286 materials = {}
287 for element in self.mesh.elements:
288 element_material = element.material
289 if element_material in materials.keys():
290 materials[element_material].append(element)
291 else:
292 materials[element_material] = [element]
294 # Create the element sets for the different materials.
295 input_file_lines = []
296 for material, elements in materials.items():
297 material_name = material.dump_to_list()[0]
298 input_file_lines.extend(get_set_lines("Elset", elements, material_name))
299 return input_file_lines
301 def get_set_lines(self):
302 """Add lines to the input file that represent node and element sets."""
303 input_file_lines = []
304 for point_set in self.mesh.geometry_sets[_bme.geo.point]:
305 if point_set.name is None:
306 raise ValueError("Sets added to the mesh have to have a valid name!")
307 input_file_lines.extend(
308 get_set_lines("Nset", point_set.get_points(), point_set.name)
309 )
310 for line_set in self.mesh.geometry_sets[_bme.geo.line]:
311 if line_set.name is None:
312 raise ValueError("Sets added to the mesh have to have a valid name!")
313 if isinstance(line_set, _GeometrySet):
314 input_file_lines.extend(
315 get_set_lines(
316 "Elset", line_set.geometry_objects[_bme.geo.line], line_set.name
317 )
318 )
319 else:
320 raise ValueError(
321 "Line sets can only be exported to Abaqus if they are defined with the beam elements"
322 )
323 return input_file_lines