{
  "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# POINT_MASS \u2014 single-node lumped mass\n\nThe 1-node concentrated-mass element contributes a diagonal\n``3 x 3`` block to the global mass matrix:\n\n\\begin{align}M_e = \\mathrm{diag}(m_x,\\, m_y,\\, m_z),\\end{align}\n\nwhere $(m_x, m_y, m_z)$ are the per-axis masses (typically\nall equal to a single isotropic mass $m$).  Consistent\nand lumped formulations coincide for a one-node element.\n\nThe rotary-inertia variant (``KEYOPT(3) != 2``) adds three\ndiagonal rotary-inertia entries; that path is signposted in\nthe kernel module header as a future addition.\n\n## References\n* Cook, R. D., Malkus, D. S., Plesha, M. E., Witt, R. J. (2002)\n  *Concepts and Applications of Finite Element Analysis*, 4th\n  ed., Wiley, \u00a711.3.\n* Bathe, K.-J. (2014) *Finite Element Procedures*, 2nd ed.,\n  Prentice Hall, \u00a74.2.2.\n\nImplementation: :class:`femorph_solver.elements.point_mass.PointMass`.\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"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Render the single-node element\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "origin = np.array([[0.0, 0.0, 0.0]])\n\nplotter = pv.Plotter(off_screen=True, window_size=(480, 360))\nplotter.add_points(\n    origin, render_points_as_spheres=True, point_size=40, color=\"#d62728\", label=\"point-mass node\"\n)\n# Three axis-aligned arrows showing the 3 mass DOFs (m_x, m_y, m_z).\nfor vec, _label, col in [\n    (np.array([1.0, 0.0, 0.0]), \"m_x\", \"#1f77b4\"),\n    (np.array([0.0, 1.0, 0.0]), \"m_y\", \"#2ca02c\"),\n    (np.array([0.0, 0.0, 1.0]), \"m_z\", \"#ff7f0e\"),\n]:\n    plotter.add_arrows(origin, vec[None, :], mag=0.8, color=col)\nplotter.add_axes(line_width=4, color=\"black\")\nplotter.view_isometric()\nplotter.camera.zoom(0.9)\nplotter.add_legend(face=None, size=(0.22, 0.07), bcolor=\"white\")\nplotter.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Sanity \u2014 global mass contribution\n\nA point-mass at node ``n`` adds ``M_e`` to rows / columns\n``(3n, 3n+1, 3n+2)`` of the global mass matrix.  Verify the\ncontribution is exactly diagonal.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "m_x, m_y, m_z = 7.5, 7.5, 7.5  # isotropic 7.5 kg point mass\nM_e = np.diag([m_x, m_y, m_z])\nprint(\"point-mass contribution at one node:\")\nprint(f\"  M_e (diag) = {np.diag(M_e)}\")\nnp.testing.assert_allclose(M_e - np.diag(np.diag(M_e)), 0.0, atol=1e-15)\nprint(\"OK \u2014 M_e is purely diagonal.\")"
      ]
    }
  ],
  "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
}