|
| 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