{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "import pyvista\npyvista.OFF_SCREEN = True\npyvista.set_jupyter_backend('static')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "\n\n# HEX8 \u2014 cantilever-plate modal (2 \u00d7 40 \u00d7 40 hex mesh)\n\nEnd-to-end modal-analysis example: build a 1 m \u00d7 1 m \u00d7 10 mm steel\nplate as a 2-through-thickness, 40 \u00d7 40 in-plane hex mesh in pyvista,\nwrap it in ``femorph_solver.Model``, clamp the ``x = 0`` edge, and extract the\nfirst 10 modes with :meth:`femorph_solver.Model.solve_modal`.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "from __future__ import annotations\n\nimport numpy as np\nimport pyvista as pv\n\nimport femorph_solver\nfrom femorph_solver import ELEMENTS"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Problem data\nSteel, thin plate.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "E = 2.0e11\nNU = 0.3\nRHO = 7850.0\n\nLX, LY, LZ = 1.0, 1.0, 0.01\nNX, NY, NZ = 40, 40, 2"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Build the mesh in pyvista\n``StructuredGrid`` gives a regular block lattice; casting to\n``UnstructuredGrid`` promotes every voxel to a ``VTK_HEXAHEDRON`` cell,\nwhich is exactly the HEX8 connectivity femorph-solver expects. No\n``/PREP7`` commands are replayed.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "xs = np.linspace(0.0, LX, NX + 1)\nys = np.linspace(0.0, LY, NY + 1)\nzs = np.linspace(0.0, LZ, NZ + 1)\nxx, yy, zz = np.meshgrid(xs, ys, zs, indexing=\"ij\")\n\ngrid = pv.StructuredGrid(xx, yy, zz).cast_to_unstructured_grid()\nprint(f\"plate: {grid.n_points} nodes, {grid.n_cells} HEX8 cells\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Wrap the grid as a femorph-solver model\n:func:`Model.from_grid` auto-stamps sequential ids for nodes, elements,\nelement-type, material, and real-constant when the grid doesn't carry\nthem. The caller only needs to declare ``et`` / ``mp``.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "m = femorph_solver.Model.from_grid(grid)\nm.assign(ELEMENTS.HEX8, material={\"EX\": E, \"PRXY\": NU, \"DENS\": RHO})"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Clamp the x=0 edge\nAll DOFs (UX, UY, UZ) fixed on every node with ``x \u2248 0``.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "node_coords = np.asarray(grid.points)\nnode_nums = np.asarray(grid.point_data[\"ansys_node_num\"])\nclamp_mask = node_coords[:, 0] < 1e-9\nm.fix(nodes=node_nums[clamp_mask].tolist(), dof=\"ALL\")\nprint(f\"clamped {int(clamp_mask.sum())} nodes on x=0 edge\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Modal solve\nFirst 10 modes with the consistent mass matrix.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "res = m.solve_modal(n_modes=10)\n\nprint(\"Mode     \u03c9\u00b2 [rad\u00b2/s\u00b2]          f [Hz]\")\nfor i, (omsq, f) in enumerate(zip(res.omega_sq, res.frequency), start=1):\n    print(f\"{i:>3}   {omsq:>18.6e}   {f:>12.4f}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Plot the first mode shape\n:func:`femorph_solver.io.modal_result_to_grid` attaches one ``(n_points, 3)``\ndisplacement array and one magnitude scalar per mode to the grid, so\nplotting any mode is just ``warp_by_vector`` on ``mode_{k}_disp``.\nEstablished commercial solvers (e.g. MAPDL POST1 ``PLDISP``) emit\nthe same per-mode shape data \u2014 this is the canonical post-processing flow.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "grid_plot = femorph_solver.io.modal_result_to_grid(m, res)\nphi1 = grid_plot.point_data[\"mode_1_disp\"]\n\nplotter = pv.Plotter(off_screen=True)\nplotter.add_mesh(grid_plot, style=\"wireframe\", color=\"gray\")\nplotter.add_mesh(\n    grid_plot.warp_by_vector(\"mode_1_disp\", factor=0.2 / np.max(np.abs(phi1))),\n    scalars=\"mode_1_magnitude\",\n    show_edges=False,\n    scalar_bar_args={\"title\": f\"mode 1 ({res.frequency[0]:.1f} Hz)\"},\n)\nplotter.add_axes()\nplotter.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Plot the first six mode shapes as a 2 \u00d7 3 grid\nSame grid carries all 10 modes, so a multi-viewport plotter can\nrender any subset without a second modal solve.  This is how every\nestablished post-processor\nusers typically browse the mode spectrum in POST1.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "plotter = pv.Plotter(shape=(2, 3), off_screen=True, window_size=(1200, 600))\nfor idx in range(6):\n    row, col = divmod(idx, 3)\n    plotter.subplot(row, col)\n    phi_k = grid_plot.point_data[f\"mode_{idx + 1}_disp\"]\n    factor = 0.1 / (np.max(np.abs(phi_k)) + 1e-300)\n    plotter.add_mesh(grid_plot, style=\"wireframe\", color=\"gray\", opacity=0.35)\n    plotter.add_mesh(\n        grid_plot.warp_by_vector(f\"mode_{idx + 1}_disp\", factor=factor),\n        scalars=f\"mode_{idx + 1}_magnitude\",\n        show_edges=False,\n        cmap=\"viridis\",\n        show_scalar_bar=False,\n    )\n    plotter.add_text(\n        f\"mode {idx + 1}: {res.frequency[idx]:.1f} Hz\",\n        position=\"upper_edge\",\n        font_size=10,\n    )\nplotter.show()"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.12.3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}