SOLID185 — cantilever-plate modal (2 × 40 × 40 hex mesh)#

End-to-end modal-analysis example: build a 1 m × 1 m × 10 mm steel plate as a 2-through-thickness, 40 × 40 in-plane hex mesh in pyvista, wrap it in femorph_solver.Model, clamp the x = 0 edge, and extract the first 10 modes with femorph_solver.Model.modal_solve().

from __future__ import annotations

import numpy as np
import pyvista as pv

import femorph_solver

Problem data#

Steel, thin plate.

E = 2.0e11
NU = 0.3
RHO = 7850.0

LX, LY, LZ = 1.0, 1.0, 0.01
NX, NY, NZ = 40, 40, 2

Build the mesh in pyvista#

StructuredGrid gives a regular block lattice; casting to UnstructuredGrid promotes every voxel to a VTK_HEXAHEDRON cell, which is exactly the SOLID185 connectivity femorph-solver expects. No /PREP7 commands are replayed.

xs = np.linspace(0.0, LX, NX + 1)
ys = np.linspace(0.0, LY, NY + 1)
zs = np.linspace(0.0, LZ, NZ + 1)
xx, yy, zz = np.meshgrid(xs, ys, zs, indexing="ij")

grid = pv.StructuredGrid(xx, yy, zz).cast_to_unstructured_grid()
print(f"plate: {grid.n_points} nodes, {grid.n_cells} SOLID185 cells")
plate: 5043 nodes, 3200 SOLID185 cells

Wrap the grid as a femorph-solver model#

Model.from_grid() auto-stamps sequential ids for nodes, elements, element-type, material, and real-constant when the grid doesn’t carry them. The caller only needs to declare et / mp.

m = femorph_solver.Model.from_grid(grid)
m.et(1, "SOLID185")
m.mp("EX", 1, E)
m.mp("PRXY", 1, NU)
m.mp("DENS", 1, RHO)
/home/runner/_work/solver/solver/examples/analyses/modal/example_plate_modes.py:53: DeprecationWarning: Model.et(...) is a MAPDL-dialect shortcut and has moved off the Model public surface.  Use `APDL(model).et(et_id, name)` for line-by-line APDL deck porting, or the native `Model.assign("HEX8", material)` for new code.
  m.et(1, "SOLID185")
/home/runner/_work/solver/solver/examples/analyses/modal/example_plate_modes.py:54: DeprecationWarning: Model.mp(...) is a MAPDL-dialect shortcut and has moved off the Model public surface.  Use `APDL(model).mp(prop, mat_id, value)` for line-by-line APDL deck porting, or the native `Model.assign(element, {prop: value, ...})` for new code.
  m.mp("EX", 1, E)
/home/runner/_work/solver/solver/examples/analyses/modal/example_plate_modes.py:55: DeprecationWarning: Model.mp(...) is a MAPDL-dialect shortcut and has moved off the Model public surface.  Use `APDL(model).mp(prop, mat_id, value)` for line-by-line APDL deck porting, or the native `Model.assign(element, {prop: value, ...})` for new code.
  m.mp("PRXY", 1, NU)
/home/runner/_work/solver/solver/examples/analyses/modal/example_plate_modes.py:56: DeprecationWarning: Model.mp(...) is a MAPDL-dialect shortcut and has moved off the Model public surface.  Use `APDL(model).mp(prop, mat_id, value)` for line-by-line APDL deck porting, or the native `Model.assign(element, {prop: value, ...})` for new code.
  m.mp("DENS", 1, RHO)

Clamp the x=0 edge#

All DOFs (UX, UY, UZ) fixed on every node with x 0.

node_coords = np.asarray(grid.points)
node_nums = np.asarray(grid.point_data["ansys_node_num"])
clamp_mask = node_coords[:, 0] < 1e-9
for nn in node_nums[clamp_mask]:
    m.d(int(nn), "ALL")
print(f"clamped {int(clamp_mask.sum())} nodes on x=0 edge")
/home/runner/_work/solver/solver/examples/analyses/modal/example_plate_modes.py:66: DeprecationWarning: Model.d(...) is a MAPDL-dialect shortcut and has moved off the Model public surface.  Use `APDL(model).d(node, label, value)` for line-by-line APDL deck porting, or the native `Model.fix(nodes=..., where=..., dof=...)` for new code.
  m.d(int(nn), "ALL")
clamped 123 nodes on x=0 edge

Plot the first mode shape#

femorph_solver.io.modal_result_to_grid() attaches one (n_points, 3) displacement array and one magnitude scalar per mode to the grid, so plotting any mode is just warp_by_vector on mode_{k}_disp. MAPDL does the same under POST1’s PLDISP.

grid_plot = femorph_solver.io.modal_result_to_grid(m, res)
phi1 = grid_plot.point_data["mode_1_disp"]

plotter = pv.Plotter(off_screen=True)
plotter.add_mesh(grid_plot, style="wireframe", color="gray")
plotter.add_mesh(
    grid_plot.warp_by_vector("mode_1_disp", factor=0.2 / np.max(np.abs(phi1))),
    scalars="mode_1_magnitude",
    show_edges=False,
    scalar_bar_args={"title": f"mode 1 ({res.frequency[0]:.1f} Hz)"},
)
plotter.add_axes()
plotter.show()
example plate modes

Plot the first six mode shapes as a 2 × 3 grid#

Same grid carries all 10 modes, so a multi-viewport plotter can render any subset without a second modal solve. This is how MAPDL users typically browse the mode spectrum in POST1.

plotter = pv.Plotter(shape=(2, 3), off_screen=True, window_size=(1200, 600))
for idx in range(6):
    row, col = divmod(idx, 3)
    plotter.subplot(row, col)
    phi_k = grid_plot.point_data[f"mode_{idx + 1}_disp"]
    factor = 0.1 / (np.max(np.abs(phi_k)) + 1e-300)
    plotter.add_mesh(grid_plot, style="wireframe", color="gray", opacity=0.35)
    plotter.add_mesh(
        grid_plot.warp_by_vector(f"mode_{idx + 1}_disp", factor=factor),
        scalars=f"mode_{idx + 1}_magnitude",
        show_edges=False,
        cmap="viridis",
        show_scalar_bar=False,
    )
    plotter.add_text(
        f"mode {idx + 1}: {res.frequency[idx]:.1f} Hz",
        position="upper_edge",
        font_size=10,
    )
plotter.show()
example plate modes

Total running time of the script: (0 minutes 3.011 seconds)

Gallery generated by Sphinx-Gallery