|
| 1 | +# Copyright (C) 2023 - 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 | +Plate Thickness Optimization |
| 25 | +------------------ |
| 26 | +This example shows how to use PyDyna to import a mesh, set up a plate thickness |
| 27 | +optimization analysis, run the analysis iteratively until a target displacement |
| 28 | +is reached,and then display the results of that optimization. |
| 29 | +
|
| 30 | +.. LINKS AND REFERENCES |
| 31 | +.. _ls_dyna_knowledge_base: https://lsdyna.ansys.com/knowledge-base/ |
| 32 | +""" |
| 33 | + |
| 34 | +############################################################################### |
| 35 | +# Perform required imports |
| 36 | +# ~~~~~~~~~~~~~~~~~~~~~~~~ |
| 37 | +# Import required packages, including those for the keywords, deck, and solver. |
| 38 | + |
| 39 | +import os |
| 40 | +import pathlib |
| 41 | +import shutil |
| 42 | +import tempfile |
| 43 | + |
| 44 | +import ansys.dpf.core as dpf |
| 45 | +from ansys.dpf.core import operators as ops |
| 46 | +import matplotlib.pyplot as plt |
| 47 | +import pandas as pd |
| 48 | + |
| 49 | +from ansys.dyna.core import Deck |
| 50 | +from ansys.dyna.core import keywords as kwd |
| 51 | +from ansys.dyna.core.pre.examples.download_utilities import EXAMPLES_PATH, DownloadManager |
| 52 | +from ansys.dyna.core.run.linux_runner import LinuxRunner |
| 53 | +from ansys.dyna.core.run.options import MemoryUnit, MpiOption, Precision |
| 54 | +from ansys.dyna.core.run.windows_runner import WindowsRunner |
| 55 | + |
| 56 | +# sphinx_gallery_thumbnail_path = '_static/pre/opt/plate_thickness.png' |
| 57 | + |
| 58 | + |
| 59 | +workdir = tempfile.TemporaryDirectory() |
| 60 | + |
| 61 | +mesh_file_name = "bar_impact_mesh.k" |
| 62 | + |
| 63 | +mesh_file = DownloadManager().download_file( |
| 64 | + mesh_file_name, "ls-dyna", "Bar_Impact", destination=os.path.join(EXAMPLES_PATH, "Bar_Impact") |
| 65 | +) |
| 66 | + |
| 67 | +# If you'd like to insert your own path to a local mesh file you can do so by replacing the line |
| 68 | +# above with: |
| 69 | +# mesh_file = "C:\Path\to\file\\bar_impact_mesh.k" |
| 70 | + |
| 71 | +############################################################################### |
| 72 | +# Set analysis parameters |
| 73 | +# ~~~~~~~~~~~~~~~~~~~~~~~~ |
| 74 | +# Define the number of iterations, thickness increment, target displacement, |
| 75 | +# and initial velocity for the analysis |
| 76 | + |
| 77 | +max_iterations = 20 |
| 78 | +initial_thickness = 0.1 |
| 79 | +thickness_increment = 0.05 |
| 80 | +target_displacement = 1.0 |
| 81 | +initial_velocity = 275.0e2 |
| 82 | + |
| 83 | +############################################################################### |
| 84 | +# Create a deck and keywords |
| 85 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 86 | +# Create a deck, which is the container for all the keywords. |
| 87 | +# Then, create and append individual keywords to the deck. |
| 88 | + |
| 89 | + |
| 90 | +def create_input_deck(thickness): |
| 91 | + deck = Deck() |
| 92 | + deck.title = "Bar Thickness - %.4s" % thickness |
| 93 | + |
| 94 | + # Define bar material |
| 95 | + mat_1 = kwd.Mat003(mid=1) |
| 96 | + mat_1.ro = 7.85000e-9 |
| 97 | + mat_1.e = 150000.0 |
| 98 | + mat_1.pr = 0.34 |
| 99 | + mat_1.sigy = 390.0 |
| 100 | + mat_1.etan = 90.0 |
| 101 | + |
| 102 | + # Define bar section |
| 103 | + sec_1 = kwd.SectionSolid(secid=1) |
| 104 | + sec_1.elform = 1 |
| 105 | + |
| 106 | + # Define plate material |
| 107 | + mat_2 = kwd.Mat003(mid=2) |
| 108 | + mat_2.ro = 7.85000e-9 |
| 109 | + mat_2.e = 1500000.0 |
| 110 | + mat_2.pr = 0.34 |
| 111 | + mat_2.sigy = 3900.0 |
| 112 | + mat_2.etan = 900.0 |
| 113 | + |
| 114 | + # Define plate section |
| 115 | + sec_2 = kwd.SectionShell(secid=2) |
| 116 | + sec_2.elform = 2 |
| 117 | + sec_2.t1 = thickness |
| 118 | + sec_2.t2 = thickness |
| 119 | + sec_2.t3 = thickness |
| 120 | + sec_2.t4 = thickness |
| 121 | + |
| 122 | + # Define bar part |
| 123 | + part_1 = kwd.Part() |
| 124 | + part_1.parts = pd.DataFrame({"pid": [1], "mid": [mat_1.mid], "secid": [sec_1.secid]}) |
| 125 | + |
| 126 | + # Define plate part |
| 127 | + part_2 = kwd.Part() |
| 128 | + part_2.parts = pd.DataFrame({"pid": [2], "mid": [mat_2.mid], "secid": [sec_2.secid]}) |
| 129 | + |
| 130 | + # Define coordinate system |
| 131 | + cs_1 = kwd.DefineCoordinateSystem(cid=1) |
| 132 | + cs_1.xl = 1.0 |
| 133 | + cs_1.yp = 1.0 |
| 134 | + |
| 135 | + # Define initial velocity |
| 136 | + init_vel = kwd.InitialVelocityGeneration() |
| 137 | + init_vel.id = part_1.parts["pid"][0] |
| 138 | + init_vel.styp = 2 |
| 139 | + init_vel.vy = initial_velocity |
| 140 | + init_vel.icid = cs_1.cid |
| 141 | + |
| 142 | + # Define box for node set |
| 143 | + box_1 = kwd.DefineBox(boxid=1, xmn=-500, xmx=500, ymn=39.0, ymx=40.1, zmn=-500, zmx=500) |
| 144 | + |
| 145 | + # Create node set |
| 146 | + set_node_1 = kwd.SetNodeGeneral() |
| 147 | + set_node_1.sid = 1 |
| 148 | + set_node_1.option = "BOX" |
| 149 | + set_node_1.e1 = box_1.boxid |
| 150 | + |
| 151 | + # Define boxes for boundary conditions |
| 152 | + box_plate_zN = kwd.DefineBox(boxid=2, xmn=-0.1, xmx=10.1, ymn=41.0, ymx=43.0, zmn=-10.1, zmx=-9.9) |
| 153 | + box_plate_zP = kwd.DefineBox(boxid=3, xmn=0.1, xmx=9.9, ymn=41.0, ymx=43.0, zmn=-0.1, zmx=0.1) |
| 154 | + box_plate_xP = kwd.DefineBox(boxid=4, xmn=9.9, xmx=10.1, ymn=41.0, ymx=43.0, zmn=-10.1, zmx=0.1) |
| 155 | + box_plate_xN = kwd.DefineBox(boxid=5, xmn=-0.1, xmx=0.1, ymn=41.0, ymx=43.0, zmn=-9.9, zmx=-0.1) |
| 156 | + |
| 157 | + # Create node set for fixed BC |
| 158 | + set_node_Fixed = kwd.SetNodeGeneral() |
| 159 | + set_node_Fixed.sid = 2 |
| 160 | + set_node_Fixed.option = "BOX" |
| 161 | + set_node_Fixed.e1 = box_plate_zN.boxid |
| 162 | + set_node_Fixed.e2 = box_plate_xP.boxid |
| 163 | + |
| 164 | + # Define fixed Boundary Conditions |
| 165 | + fixed_bc = kwd.BoundarySpcSet(dofx=1, dofy=1, dofz=1, dofrx=1, dofry=1, dofrz=1) |
| 166 | + fixed_bc.nsid = set_node_Fixed.sid |
| 167 | + |
| 168 | + # Create node set for symmetric BC normal to Z Axis |
| 169 | + set_node_zNormal = kwd.SetNodeGeneral() |
| 170 | + set_node_zNormal.sid = 3 |
| 171 | + set_node_zNormal.option = "BOX" |
| 172 | + set_node_zNormal.e1 = box_plate_zP.boxid |
| 173 | + |
| 174 | + # Define zNormal Boundary Conditions |
| 175 | + zNormal_bc = kwd.BoundarySpcSet(dofx=0, dofy=0, dofz=1, dofrx=1, dofry=1, dofrz=0) |
| 176 | + zNormal_bc.nsid = set_node_zNormal.sid |
| 177 | + |
| 178 | + # Create node set for symmetric BC normal to X Axis |
| 179 | + set_node_xNormal = kwd.SetNodeGeneral() |
| 180 | + set_node_xNormal.sid = 4 |
| 181 | + set_node_xNormal.option = "BOX" |
| 182 | + set_node_xNormal.e1 = box_plate_xN.boxid |
| 183 | + |
| 184 | + # Define xNormal Boundary Conditions |
| 185 | + xNormal_bc = kwd.BoundarySpcSet(dofx=1, dofy=0, dofz=0, dofrx=0, dofry=1, dofrz=1) |
| 186 | + xNormal_bc.nsid = set_node_xNormal.sid |
| 187 | + |
| 188 | + # Define box for node set of plate |
| 189 | + box_plate = kwd.DefineBox(boxid=6, xmn=-1, xmx=11, ymn=39.0, ymx=40.1, zmn=-11, zmx=1) |
| 190 | + |
| 191 | + # Create node set for plate |
| 192 | + set_node_plate = kwd.SetNodeGeneral() |
| 193 | + set_node_plate.sid = 5 |
| 194 | + set_node_plate.option = "BOX" |
| 195 | + set_node_plate.e1 = box_plate.boxid |
| 196 | + |
| 197 | + # Define contact |
| 198 | + contact = kwd.ContactAutomaticSingleSurface(surfa=0) |
| 199 | + contact.fs = 0.3 |
| 200 | + |
| 201 | + # Define control termination |
| 202 | + control_term = kwd.ControlTermination(endtim=2.00000e-4, dtmin=0.001) |
| 203 | + |
| 204 | + # Define database cards |
| 205 | + deck_dt_out = 8.00000e-8 |
| 206 | + deck_glstat = kwd.DatabaseGlstat(dt=deck_dt_out, binary=3) |
| 207 | + deck_matsum = kwd.DatabaseMatsum(dt=deck_dt_out, binary=3) |
| 208 | + deck_nodout = kwd.DatabaseNodout(dt=deck_dt_out, binary=3) |
| 209 | + deck_elout = kwd.DatabaseElout(dt=deck_dt_out, binary=3) |
| 210 | + deck_rwforc = kwd.DatabaseRwforc(dt=deck_dt_out, binary=3) |
| 211 | + deck_d3plot = kwd.DatabaseBinaryD3Plot(dt=4.00000e-6) |
| 212 | + |
| 213 | + # Define deck history node |
| 214 | + deck_hist_node_1 = kwd.DatabaseHistoryNodeSet() |
| 215 | + deck_hist_node_1.id1 = set_node_1.sid |
| 216 | + |
| 217 | + # Append all cards to input deck |
| 218 | + deck.extend( |
| 219 | + [ |
| 220 | + deck_glstat, |
| 221 | + deck_matsum, |
| 222 | + deck_nodout, |
| 223 | + deck_elout, |
| 224 | + deck_rwforc, |
| 225 | + deck_d3plot, |
| 226 | + set_node_1, |
| 227 | + control_term, |
| 228 | + contact, |
| 229 | + box_1, |
| 230 | + box_plate_zN, |
| 231 | + box_plate_zP, |
| 232 | + box_plate_xP, |
| 233 | + box_plate_xN, |
| 234 | + box_plate, |
| 235 | + set_node_Fixed, |
| 236 | + set_node_zNormal, |
| 237 | + set_node_xNormal, |
| 238 | + set_node_plate, |
| 239 | + fixed_bc, |
| 240 | + zNormal_bc, |
| 241 | + xNormal_bc, |
| 242 | + init_vel, |
| 243 | + cs_1, |
| 244 | + part_1, |
| 245 | + mat_1, |
| 246 | + sec_1, |
| 247 | + part_2, |
| 248 | + mat_2, |
| 249 | + sec_2, |
| 250 | + deck_hist_node_1, |
| 251 | + ] |
| 252 | + ) |
| 253 | + |
| 254 | + return deck |
| 255 | + |
| 256 | + |
| 257 | +def write_input_deck(**kwargs): |
| 258 | + thickness = kwargs.get("thickness") |
| 259 | + wd = kwargs.get("wd") |
| 260 | + if not all((thickness, wd)): |
| 261 | + raise Exception("Missing input!") |
| 262 | + deck = create_input_deck(thickness) |
| 263 | + # Import mesh |
| 264 | + deck.append(kwd.Include(filename=mesh_file_name)) |
| 265 | + |
| 266 | + # Write LS-DYNA input deck |
| 267 | + os.makedirs(wd, exist_ok=True) |
| 268 | + deck.export_file(os.path.join(wd, "input.k")) |
| 269 | + shutil.copyfile(mesh_file, os.path.join(wd, mesh_file_name)) |
| 270 | + |
| 271 | + |
| 272 | +############################################################################### |
| 273 | +# Define the Dyna solver function |
| 274 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 275 | +# |
| 276 | + |
| 277 | + |
| 278 | +def run_job(directory): |
| 279 | + if os.name == "nt": |
| 280 | + runner = WindowsRunner( |
| 281 | + ncpu=2, memory=2, precision=Precision.SINGLE, mpi_option=MpiOption.MPP_INTEL_MPI, memory_unit=MemoryUnit.MB |
| 282 | + ) |
| 283 | + elif os.name == "posix": |
| 284 | + runner = LinuxRunner( |
| 285 | + ncpu=2, memory=2, precision=Precision.DOUBLE, mpi_option=MpiOption.MPP_INTEL_MPI, memory_unit=MemoryUnit.MB |
| 286 | + ) |
| 287 | + runner.set_input("input.k", directory) |
| 288 | + runner.run() # Run LS-DYNA simulation |
| 289 | + assert os.path.isfile(os.path.join(directory, "d3plot")), "No result file found" |
| 290 | + |
| 291 | + |
| 292 | +############################################################################### |
| 293 | +# Define the DPF output function |
| 294 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 295 | +# |
| 296 | + |
| 297 | + |
| 298 | +def get_plate_displacement(directory): |
| 299 | + ds = dpf.DataSources() |
| 300 | + result_file = os.path.join(directory, "d3plot") |
| 301 | + assert os.path.isfile(result_file) |
| 302 | + ds.set_result_file_path(result_file, "d3plot") |
| 303 | + model = dpf.Model(ds) |
| 304 | + |
| 305 | + # Create mesh operator |
| 306 | + mes_op = dpf.operators.mesh.mesh_provider() |
| 307 | + mes_op.inputs.data_sources.connect(ds) |
| 308 | + # Isolate Part 2 |
| 309 | + mes_op.connect(25, [2]) |
| 310 | + # Extract mesh |
| 311 | + part_mesh = mes_op.outputs.mesh() |
| 312 | + |
| 313 | + # Create scoping operator from mesh using from_mesh scoping operator |
| 314 | + part_mesh_op = dpf.operators.scoping.from_mesh() |
| 315 | + part_mesh_op.inputs.mesh.connect(part_mesh) |
| 316 | + part_scoping = part_mesh_op.outputs.scoping() |
| 317 | + |
| 318 | + # create displacement entity, apply part scoping |
| 319 | + disp = model.results.displacement |
| 320 | + disp.on_mesh_scoping(part_scoping) |
| 321 | + |
| 322 | + disp_op = disp.on_all_time_freqs() |
| 323 | + |
| 324 | + # Find min and max displacement |
| 325 | + min_max_op = ops.min_max.min_max_fc(ops.math.norm_fc(disp_op)) |
| 326 | + |
| 327 | + min_displ = min_max_op.outputs.field_min() |
| 328 | + max_displ = min_max_op.outputs.field_max() |
| 329 | + |
| 330 | + max_disp_data = max_displ.data |
| 331 | + min_disp_data = min_displ.data |
| 332 | + |
| 333 | + tdata = model.metadata.time_freq_support.time_frequencies.data |
| 334 | + |
| 335 | + return tdata, max_disp_data, min_disp_data |
| 336 | + |
| 337 | + |
| 338 | +############################################################################### |
| 339 | +# Run solver iteratively until target displacement is reached |
| 340 | +# ~~~~~~~~~~~~~~~~~~~~~~ |
| 341 | +# |
| 342 | + |
| 343 | +for iteration in range(0, max_iterations): |
| 344 | + # Define thickness based on iteration |
| 345 | + thickness = initial_thickness + thickness_increment * iteration |
| 346 | + wd = os.path.join(workdir.name, "thickness_%.4s" % thickness) |
| 347 | + print(wd) |
| 348 | + pathlib.Path(wd).mkdir(exist_ok=True) |
| 349 | + # Create LS-Dyna input deck with new thickness |
| 350 | + write_input_deck(thickness=thickness, wd=wd) |
| 351 | + # Run Solver |
| 352 | + try: |
| 353 | + run_job(wd) |
| 354 | + # Run PyDPF Post |
| 355 | + time_data, max_disp_data, min_disp_data = get_plate_displacement(wd) |
| 356 | + reduced_time_data = [t * 1000 for t in time_data] |
| 357 | + # Determine if target displacement is reached |
| 358 | + if max(max_disp_data) <= target_displacement: |
| 359 | + print("Final Thickness: %.4s, Max Plate Displacement: %.5s" % (thickness, max(max_disp_data))) |
| 360 | + plt.plot( |
| 361 | + reduced_time_data, max_disp_data, "r", label="Max Plate Displacement with %.4s thickness" % thickness |
| 362 | + ) |
| 363 | + break |
| 364 | + # Add series to the plot |
| 365 | + plt.plot(reduced_time_data, max_disp_data, "b") |
| 366 | + |
| 367 | + except Exception as e: |
| 368 | + print(e) |
| 369 | +############################################################################### |
| 370 | +# Generate graphical output |
| 371 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 372 | +# |
| 373 | + |
| 374 | +plt.xlabel("Time (e^-3 s)") |
| 375 | +plt.ylabel("Displacement (mm)") |
| 376 | +plt.legend() |
| 377 | +plt.show() |
0 commit comments