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
« 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."""
25from enum import Enum as _Enum
26from enum import auto as _auto
28import numpy as _np
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
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(
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)
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.
156 For more information see the Abaqus documentation on: "Beam element cross-section orientation"
158 Args
159 ----
160 normal_definition: AbaqusBeamNormalDefinition
161 How the beam cross-section should be defined.
162 """
164 def normalize(vector):
165 """Normalize a vector."""
166 return vector / _np.linalg.norm(vector)
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.
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))]
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.
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)
200 rotation = element.nodes[0].rotation
201 cross_section_rotation = _smallest_rotation(rotation, t)
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}")
216 def get_nodes_lines(self):
217 """Get the lines for the input file that represent the nodes."""
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 )
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 )
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
248 return input_file_lines
250 def get_element_lines(self):
251 """Get the lines for the input file that represent the elements."""
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]
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)
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))
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 )
289 element_count += len(elements)
291 if len(normal_lines) > 1:
292 return element_lines + normal_lines
293 else:
294 return element_lines
296 def get_material_lines(self):
297 """Get the lines for the input file that represent the element sets
298 with the same material."""
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]
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
315 def get_set_lines(self):
316 """Add lines to the input file that represent node and element sets."""
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