Skip to content

Commit dff40ed

Browse files
LSDYNA beam results example (#1844)
* new ls dyna example * new ls dyna example * new ls dyna example * complete beam example * updates * Fix typo * Fix copyright header * Run pre-commit --------- Co-authored-by: Paul Profizi <[email protected]> Co-authored-by: PProfizi <[email protected]>
1 parent 9bfe8c2 commit dff40ed

File tree

1 file changed

+359
-0
lines changed

1 file changed

+359
-0
lines changed
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
# Copyright (C) 2020 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
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 all
13+
# 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 THE
21+
# SOFTWARE.
22+
23+
"""
24+
.. _lsdyna_operators:
25+
26+
Beam results manipulations
27+
--------------------------
28+
29+
This example provides an overview of the LS-DYNA beam results manipulations.
30+
31+
.. note::
32+
This example requires DPF 6.1 (ansys-dpf-server-2023-2-pre0) or above.
33+
For more information, see :ref:`ref_compatibility`.
34+
35+
"""
36+
37+
import matplotlib.pyplot as plt
38+
39+
from ansys.dpf import core as dpf
40+
from ansys.dpf.core import examples, operators as ops
41+
42+
###############################################################################
43+
# d3plot file data extraction
44+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
45+
# Create the model and print its contents. This LS-DYNA d3plot file contains
46+
# several individual results, each at different times. The d3plot file does not
47+
# contain information related to Units.
48+
#
49+
# In this case, as the simulation was run through Mechanical, a ''file.actunits''
50+
# file is produced. If this file is supplemented in the data_sources, the units
51+
# will be correctly fetched for all results in the file as well as for the mesh.
52+
53+
d3plot = examples.download_d3plot_beam()
54+
my_data_sources = dpf.DataSources()
55+
my_data_sources.set_result_file_path(d3plot[0], key="d3plot")
56+
my_data_sources.add_file_path(d3plot[3], key="actunits")
57+
my_model = dpf.Model(my_data_sources)
58+
print(my_model)
59+
60+
###############################################################################
61+
# Exploring the mesh
62+
# ~~~~~~~~~~~~~~~~~~
63+
#
64+
# The model has solid (3D) elements and beam (1D) elements. Some of the results
65+
# only apply to one type of elements (such as the stress tensor for solids, or
66+
# the axial force for beams, for example).
67+
#
68+
# By splitting the mesh by element shape we see that the ball is made by the solid
69+
# 3D elements and the plate by the beam 1D elements
70+
#
71+
# - Define the analysis mesh
72+
my_meshed_region = my_model.metadata.meshed_region
73+
74+
# - Get separate meshes for each body
75+
my_meshes = ops.mesh.split_mesh(
76+
mesh=my_meshed_region, property=dpf.common.elemental_properties.element_shape
77+
).eval()
78+
79+
# - Define the meshes for each body in separate variables
80+
ball_mesh = my_meshes.get_mesh(label_space_or_index={"body": 1, "elshape": 1})
81+
plate_mesh = my_meshes.get_mesh(label_space_or_index={"body": 2, "elshape": 2})
82+
83+
print(my_meshes)
84+
85+
###############################################################################
86+
# Plate mesh
87+
88+
print("Plate mesh", "\n", plate_mesh)
89+
plate_mesh.plot(title="Plate mesh", text="Plate mesh")
90+
91+
###############################################################################
92+
# Ball mesh
93+
94+
print("Ball mesh", "\n", ball_mesh, "\n")
95+
ball_mesh.plot(title="Ball mesh", text="Ball mesh")
96+
97+
###############################################################################
98+
# Scoping
99+
# ~~~~~~~
100+
#
101+
# - Define the mesh scoping to use it with the operators
102+
my_meshes_scoping = ops.scoping.split_on_property_type(mesh=my_meshed_region).eval()
103+
104+
###############################################################################
105+
# - Define the mesh scoping for each body/element shape in separate variables
106+
ball_scoping = my_meshes_scoping.get_scoping(label_space_or_index={"elshape": 1})
107+
plate_scoping = my_meshes_scoping.get_scoping(label_space_or_index={"elshape": 2})
108+
109+
###############################################################################
110+
# - We will plot the results in a mesh deformed by the displacement.
111+
# The displacement is in a nodal location, so we need to define a nodal scoping for the plate
112+
plate_scoping_nodal = dpf.operators.scoping.transpose(
113+
mesh_scoping=plate_scoping, meshed_region=my_meshed_region
114+
).eval()
115+
116+
###############################################################################
117+
# Beam results
118+
# ~~~~~~~~~~~~
119+
# The next manipulations can be applied to the following beam operators
120+
# that handle the correspondent results :
121+
#
122+
# - beam_axial_force: Beam Axial Force
123+
# - beam_s_shear_force: Beam S Shear Force
124+
# - beam_t_shear_force: Beam T Shear Force
125+
# - beam_s_bending_moment: Beam S Bending Moment
126+
# - beam_t_bending_moment: Beam T Bending Moment
127+
# - beam_torsional_moment: Beam Torsional Moment
128+
# - beam_axial_stress: Beam Axial Stress
129+
# - beam_rs_shear_stress: Beam Rs Shear Stress
130+
# - beam_tr_shear_stress: Beam Tr Shear Stress
131+
# - beam_axial_plastic_strain: Beam Axial Plastic Strain
132+
# - beam_axial_total_strain: Beam Axial Total Strain
133+
#
134+
# We do not demonstrate separately how to use each of them in this example
135+
# once they have similar methods.
136+
#
137+
# So, if you want to operate on other operator, uou just need to change their
138+
# scripting name in the code lines.
139+
140+
###############################################################################
141+
# Comparing results in different time steps
142+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
143+
144+
# 1) Define the time steps set
145+
time_steps_set = [2, 6, 12]
146+
147+
# 2) Prepare the collections to store the results for each time step
148+
149+
# a. To compare the results in the same image you have to copy the mesh for each plot
150+
plate_meshes = dpf.MeshesContainer()
151+
plate_meshes.add_label("time")
152+
153+
# b. The displacements for each time steps to deform the mesh accordingly
154+
plate_displacements = dpf.FieldsContainer()
155+
plate_displacements.add_label(label="time")
156+
157+
# c. The axial force results for each time steps. Here
158+
plate_axial_force = dpf.FieldsContainer()
159+
plate_axial_force.add_label(label="time")
160+
161+
# 3) Use the Plotter class to add the plots in the same image
162+
comparison_plot = dpf.plotter.DpfPlotter()
163+
164+
# Side bar arguments definition
165+
side_bar_args = dict(
166+
title="Beam axial force (N)", fmt="%.2e", title_font_size=15, label_font_size=15
167+
)
168+
169+
# 4) As we want to compare the results in the same plot we will need this variable.
170+
# It represents the distance between the meshes
171+
j = -400
172+
173+
# 5) Copy the mesh of interest. Here it is the plate mesh that we copy along the X axis
174+
# Here we use a loop where each iteration correspond to the manipulations for a given time step
175+
176+
for i in time_steps_set: # Loop through the time steps
177+
# Copy the mesh
178+
plate_meshes.add_mesh(label_space={"time": i}, mesh=plate_mesh.deep_copy())
179+
180+
# 6) Get the plot coordinates that will be changed (so we can compare the results side by side)
181+
coords_to_update = plate_meshes.get_mesh(
182+
label_space_or_index={"time": i}
183+
).nodes.coordinates_field
184+
185+
# 7) Define the coordinates where the new mesh will be placed
186+
overall_field = dpf.fields_factory.create_3d_vector_field(
187+
num_entities=1, location=dpf.locations.overall
188+
)
189+
overall_field.append(data=[j, 0.0, 0.0], scopingid=1)
190+
191+
# 8) Define the updated coordinates
192+
new_coordinates = ops.math.add(fieldA=coords_to_update, fieldB=overall_field).eval()
193+
coords_to_update.data = new_coordinates.data
194+
195+
# 9) Extract the result, here we start by getting the beam_rs_shear_stress
196+
plate_axial_force.add_field(
197+
label_space={"time": i},
198+
field=my_model.results.beam_axial_force(
199+
time_scoping=i, mesh_scoping=plate_scoping_nodal
200+
).eval()[0],
201+
)
202+
# 10) We will also get the displacement to deform the mesh
203+
plate_displacements.add_field(
204+
label_space={"time": i},
205+
field=my_model.results.displacement(
206+
time_scoping=i, mesh_scoping=plate_scoping_nodal
207+
).eval()[0],
208+
)
209+
# 11) Add the result and the mesh to the plot
210+
comparison_plot.add_field(
211+
field=plate_axial_force.get_field(label_space_or_index={"time": i}),
212+
meshed_region=plate_meshes.get_mesh(label_space_or_index={"time": i}),
213+
deform_by=plate_displacements.get_field(label_space_or_index={"time": i}),
214+
scalar_bar_args=side_bar_args,
215+
)
216+
comparison_plot.add_node_labels(
217+
nodes=[289],
218+
labels=[f"Time step = {i}"],
219+
meshed_region=plate_meshes.get_mesh(label_space_or_index={"time": i}),
220+
font_size=10,
221+
)
222+
# 12) Increment the coordinate value for the loop
223+
j = j - 400
224+
225+
226+
# Visualise the plot
227+
comparison_plot.show_figure()
228+
229+
###############################################################################
230+
# Plot a graph over time for the elements with max and min results values
231+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
232+
#
233+
# Here we make a workflow with a more verbose approach. This is useful because we use operators
234+
# having several matching inputs or outputs. So the connexions are more clear, and it is
235+
# easier to use and reuse the workflow.
236+
#
237+
# The following workflow finds the element with the max values over all the time steps and return its ID
238+
239+
# Define the workflow object
240+
max_workflow = dpf.Workflow()
241+
max_workflow.progress_bar = False
242+
# Define the norm operator
243+
max_norm = ops.math.norm_fc()
244+
# Define the max of each entity with the evaluated norm as an input
245+
max_per_ent = ops.min_max.min_max_by_entity(fields_container=max_norm.outputs.fields_container)
246+
# Define the max over all entities
247+
global_max = ops.min_max.min_max(field=max_per_ent.outputs.field_max)
248+
# Get the scoping
249+
max_scop = ops.utility.extract_scoping(field_or_fields_container=global_max.outputs.field_max)
250+
# Get the id
251+
max_id = ops.scoping.scoping_get_attribute(
252+
scoping=max_scop.outputs.mesh_scoping_as_scoping, property_name="ids"
253+
)
254+
255+
# Add the operators to the workflow
256+
max_workflow.add_operators(operators=[max_norm, max_per_ent, global_max, max_scop, max_id])
257+
max_workflow.set_input_name("fields_container", max_norm.inputs.fields_container)
258+
max_workflow.set_output_name("max_id", max_id.outputs.property_as_vector_int32_)
259+
max_workflow.set_output_name("max_entity_scoping", max_scop.outputs.mesh_scoping_as_scoping)
260+
261+
###############################################################################
262+
# Using the workflow to the stresses results on the plate:
263+
#
264+
# - Extract the results
265+
266+
# Get all the time steps
267+
time_all = my_model.metadata.time_freq_support.time_frequencies
268+
269+
# Extract all the stresses results on the plate
270+
plate_beam_axial_stress = my_model.results.beam_axial_stress(
271+
time_scoping=time_all, mesh_scoping=plate_scoping
272+
).eval()
273+
plate_beam_rs_shear_stress = my_model.results.beam_rs_shear_stress(
274+
time_scoping=time_all, mesh_scoping=plate_scoping
275+
).eval()
276+
plate_beam_tr_shear_stress = my_model.results.beam_tr_shear_stress(
277+
time_scoping=time_all, mesh_scoping=plate_scoping
278+
).eval()
279+
280+
###############################################################################
281+
# - As we will use the workflow for different results operators we group them and
282+
# use a loop through the group. Here we prepare where the workflow outputs will be stored
283+
284+
# List of operators to be used in the workflow
285+
beam_stresses = [plate_beam_axial_stress, plate_beam_rs_shear_stress, plate_beam_tr_shear_stress]
286+
graph_labels = [
287+
"Beam axial stress",
288+
"Beam rs shear stress",
289+
"Beam tr shear stress",
290+
]
291+
292+
# List of elements ids that we will get from the workflow
293+
max_stress_elements_ids = []
294+
295+
# Scopings container
296+
max_stress_elements_scopings = dpf.ScopingsContainer()
297+
max_stress_elements_scopings.add_label("stress_result")
298+
299+
###############################################################################
300+
# - The following loop:
301+
# a) Goes through each stress result and get the element id with maximum solicitation
302+
# b) Re-escope the fields container to keep only the data for this element
303+
# c) Plot a stress x time graph
304+
305+
for j in range(0, len(beam_stresses)): # Loop through each stress result
306+
# Use the pre-defined workflow to define the element with maximum solicitation
307+
max_workflow.connect(pin_name="fields_container", inpt=beam_stresses[j])
308+
max_stress_elements_ids.append(
309+
max_workflow.get_output(pin_name="max_id", output_type=dpf.types.vec_int)
310+
)
311+
max_stress_elements_scopings.add_scoping(
312+
label_space={"stress_result": j},
313+
scoping=max_workflow.get_output(
314+
pin_name="max_entity_scoping", output_type=dpf.types.scoping
315+
),
316+
)
317+
318+
# Re-scope the results to keep only the data for the identified element
319+
beam_stresses[j] = ops.scoping.rescope_fc(
320+
fields_container=beam_stresses[j],
321+
mesh_scoping=max_stress_elements_scopings.get_scoping(
322+
label_space_or_index={"stress_result": j}
323+
),
324+
).eval()
325+
326+
# The d3plot file gives us fields containers labeled by time. So in each field we have the stress value in a
327+
# given time for the chosen element. We need to rearrange the fields container into fields.
328+
329+
beam_stresses[j] = ops.utility.merge_to_field_matrix(fields1=beam_stresses[j]).eval()
330+
plt.plot(
331+
time_all.data,
332+
beam_stresses[j].data[0],
333+
label=f"{graph_labels[j]}, element id:{max_stress_elements_ids[j][0]}",
334+
)
335+
336+
# Graph formatting
337+
plt.title("Beam stresses evolution")
338+
plt.xlabel("Time (s)")
339+
plt.ylabel("Beam stresses (MPa)")
340+
plt.legend()
341+
plt.show()
342+
343+
###############################################################################
344+
# Results coordinates system
345+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~
346+
#
347+
# The general results are given in the Cartesian coordinates system by default.
348+
#
349+
# The beam results are given directly in the local directions as scalars.
350+
# For example the beam stresses we have:
351+
#
352+
# - The axial stress, given in the beam axis
353+
# - The stresses defined in the cross-section directions: tr stress in the transverse
354+
# direction (t) and rs stress perpendicular to the tr direction (s).
355+
#
356+
#
357+
# Unfortunately there are no operators for LS-DYNA files that directly allows you to:
358+
# - Rotate results from local coordinate system to global coordinate system;
359+
# - Extract the rotation matrix between the local and global coordinate systems;

0 commit comments

Comments
 (0)