Coverage for src/beamme/abaqus/input_file.py: 93%
131 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-29 11:30 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-29 11:30 +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 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 # Perform some checks on the mesh.
132 if _bme.check_overlapping_elements:
133 self.mesh.check_overlapping_elements()
135 # Assign global indices to all materials
136 set_i_global(self.mesh.materials)
138 # Calculate the required cross-section normal data
139 self.calculate_cross_section_normal_data(normal_definition)
141 # Add the lines to the input file
142 input_file_lines = []
143 input_file_lines.extend(["** " + line for line in _INPUT_FILE_HEADER])
144 input_file_lines.extend(self.get_nodes_lines())
145 input_file_lines.extend(self.get_element_lines())
146 input_file_lines.extend(self.get_material_lines())
147 input_file_lines.extend(self.get_set_lines())
148 return "\n".join(input_file_lines)
150 def calculate_cross_section_normal_data(self, normal_definition):
151 """Evaluate all data that is required to fully specify the cross-
152 section orientation in Abaqus. The evaluated data is stored in the
153 elements.
155 For more information see the Abaqus documentation on: "Beam element cross-section orientation"
157 Args
158 ----
159 normal_definition: AbaqusBeamNormalDefinition
160 How the beam cross-section should be defined.
161 """
163 def normalize(vector):
164 """Normalize a vector."""
165 return vector / _np.linalg.norm(vector)
167 # Reset possibly existing data stored in the elements
168 # element.n1_orientation_node: list(float)
169 # The coordinates of an additional (dummy) node connected to the
170 # element to define its approximate n1 direction. It this is None,
171 # no additional node will be added to the input file.
172 # element.n1_node_id: str
173 # The global ID in the input file for the additional orientation
174 # node.
175 # element.n2: list(list(float)):
176 # A list containing possible explicit normal definitions for each
177 # element node. All entries that are not None will be added to the
178 # *NORMAL section of the input file.
180 for element in self.mesh.elements:
181 element.n1_position = None
182 element.n1_node_id = None
183 element.n2 = [None for i_node in range(len(element.nodes))]
185 if (
186 normal_definition == AbaqusBeamNormalDefinition.normal
187 or normal_definition == AbaqusBeamNormalDefinition.normal_and_extra_node
188 ):
189 # In this case we take the beam tangent from the first to the second node
190 # and calculate an ortho-normal triad based on this direction. We do this
191 # via a smallest rotation mapping from the triad of the first node onto
192 # the tangent.
194 for element in self.mesh.elements:
195 node_1 = element.nodes[0].coordinates
196 node_2 = element.nodes[1].coordinates
197 t = normalize(node_2 - node_1)
199 rotation = element.nodes[0].rotation
200 cross_section_rotation = _smallest_rotation(rotation, t)
202 if (
203 normal_definition
204 == AbaqusBeamNormalDefinition.normal_and_extra_node
205 ):
206 element.n1_position = node_1 + cross_section_rotation * [
207 0.0,
208 1.0,
209 0.0,
210 ]
211 element.n2[0] = cross_section_rotation * [0.0, 0.0, 1.0]
212 else:
213 raise ValueError(f"Got unexpected normal_definition {normal_definition}")
215 def get_nodes_lines(self):
216 """Get the lines for the input file that represent the nodes."""
218 # The nodes require postprocessing, as we have to identify coupled nodes in Abaqus.
219 # Internally in Abaqus, coupled nodes are a single node with different normals for the
220 # connected element. Therefore, for nodes which are coupled to each other, we keep the
221 # same global ID while still keeping the individual nodes.
222 _, unique_nodes = _get_coupled_nodes_to_master_map(
223 self.mesh, assign_i_global=True
224 )
226 # Number the remaining nodes and create nodes for the input file
227 input_file_lines = ["*Node"]
228 for node in unique_nodes:
229 input_file_lines.append(
230 (", ".join([F_INT] + 3 * [F_FLOAT])).format(
231 node.i_global + 1, *node.coordinates
232 )
233 )
235 # Check if we need to write additional nodes for the element cross-section directions
236 node_counter = len(unique_nodes)
237 for element in self.mesh.elements:
238 if element.n1_position is not None:
239 node_counter += 1
240 input_file_lines.append(
241 (", ".join([F_INT] + 3 * [F_FLOAT])).format(
242 node_counter, *element.n1_position
243 )
244 )
245 element.n1_node_id = node_counter
247 return input_file_lines
249 def get_element_lines(self):
250 """Get the lines for the input file that represent the elements."""
252 # Sort the elements after their types.
253 element_types = {}
254 for element in self.mesh.elements:
255 element_type = element.beam_type
256 if element_type in element_types.keys():
257 element_types[element_type].append(element)
258 else:
259 element_types[element_type] = [element]
261 # Write the element connectivity.
262 element_count = 0
263 element_lines = []
264 normal_lines = ["*Normal, type=element"]
265 for element_type, elements in element_types.items():
266 # Number the elements of this type
267 set_i_global(elements, start_index=element_count)
269 # Set the element connectivity, possibly including the n1 direction node
270 element_lines.append("*Element, type={}".format(element_type))
271 for element in elements:
272 node_ids = [node.i_global + 1 for node in element.nodes]
273 if element.n1_node_id is not None:
274 node_ids.append(element.n1_node_id)
275 line_ids = [element.i_global + 1] + node_ids
276 element_lines.append(", ".join(F_INT.format(i) for i in line_ids))
278 # Set explicit normal definitions for the nodes
279 for i_node, n2 in enumerate(element.n2):
280 if n2 is not None:
281 node = element.nodes[i_node]
282 normal_lines.append(
283 (", ".join(2 * [F_INT] + 3 * [F_FLOAT])).format(
284 element.i_global + 1, node.i_global + 1, *n2
285 )
286 )
288 element_count += len(elements)
290 if len(normal_lines) > 1:
291 return element_lines + normal_lines
292 else:
293 return element_lines
295 def get_material_lines(self):
296 """Get the lines for the input file that represent the element sets
297 with the same material."""
299 materials = {}
300 for element in self.mesh.elements:
301 element_material = element.material
302 if element_material in materials.keys():
303 materials[element_material].append(element)
304 else:
305 materials[element_material] = [element]
307 # Create the element sets for the different materials.
308 input_file_lines = []
309 for material, elements in materials.items():
310 material_name = material.dump_to_list()[0]
311 input_file_lines.extend(get_set_lines("Elset", elements, material_name))
312 return input_file_lines
314 def get_set_lines(self):
315 """Add lines to the input file that represent node and element sets."""
317 input_file_lines = []
318 for point_set in self.mesh.geometry_sets[_bme.geo.point]:
319 if point_set.name is None:
320 raise ValueError("Sets added to the mesh have to have a valid name!")
321 input_file_lines.extend(
322 get_set_lines("Nset", point_set.get_points(), point_set.name)
323 )
324 for line_set in self.mesh.geometry_sets[_bme.geo.line]:
325 if line_set.name is None:
326 raise ValueError("Sets added to the mesh have to have a valid name!")
327 if isinstance(line_set, _GeometrySet):
328 input_file_lines.extend(
329 get_set_lines(
330 "Elset", line_set.geometry_objects[_bme.geo.line], line_set.name
331 )
332 )
333 else:
334 raise ValueError(
335 "Line sets can only be exported to Abaqus if they are defined with the beam elements"
336 )
337 return input_file_lines