Source code for femorph_solver.elements.point_mass

"""PointMass — concentrated point mass.

femorph-solver implements the *translational-only* variant: a
single-node element carrying three translational DOFs ``UX, UY, UZ``.
The rotary-inertia variant requires 6 DOFs/node and will be added
alongside Beam2 once mixed-DOF assembly is in place.

Real constants
--------------
    real[0]: MASSX — translational mass along x (mandatory)
    real[1]: MASSY — translational mass along y (defaults to MASSX)
    real[2]: MASSZ — translational mass along z (defaults to MASSX)

Matrices
--------

    K_e = 0₃×₃     (a point mass has no stiffness contribution)
    M_e = diag(MASSX, MASSY, MASSZ)

Lumped and consistent formulations coincide for a one-node element.

References
----------
* Concentrated / point-mass element (lumped nodal mass, no stiffness
  contribution): Cook, Malkus, Plesha, Witt, *Concepts and
  Applications of Finite Element Analysis*, 4th ed., Wiley, 2002,
  §16.3 (discussion of lumped nodal masses).  Bathe, K.J., *Finite
  Element Procedures*, 2nd ed., Prentice Hall, 2014, §4.2.4.
* Anisotropic diagonal mass tensor (``diag(MASSX, MASSY, MASSZ)``,
  supports different masses on each translational axis): standard
  generalisation of the isotropic ``m·I₃`` lumped mass.
"""

from __future__ import annotations

import numpy as np

from femorph_solver.elements._base import ElementBase
from femorph_solver.elements._registry import register


def _mass_vector(real: np.ndarray) -> np.ndarray:
    r = np.asarray(real, dtype=np.float64)
    if r.size == 0:
        raise ValueError("PointMass requires REAL[0]=MASSX (point mass); got empty real set")
    mx = float(r[0])
    my = float(r[1]) if r.size > 1 else mx
    mz = float(r[2]) if r.size > 2 else mx
    return np.array([mx, my, mz], dtype=np.float64)


[docs] @register class PointMass(ElementBase): name = "POINT_MASS" n_nodes = 1 n_dof_per_node = 3 # UX, UY, UZ — translational-only (no rotary inertia) vtk_cell_type = 1 # VTK_VERTEX
[docs] @staticmethod def ke( coords: np.ndarray, material: dict[str, float], real: np.ndarray, ) -> np.ndarray: return np.zeros((3, 3), dtype=np.float64)
[docs] @staticmethod def me( coords: np.ndarray, material: dict[str, float], real: np.ndarray, lumped: bool = False, ) -> np.ndarray: return np.diag(_mass_vector(real))