Source code for femorph_solver.elements.mass21

"""MASS21 — concentrated point mass.

femorph-solver implements the *translational-only* variant (``KEYOPT(3) = 2``): a
single-node element carrying three translational DOFs ``UX, UY, UZ``. The
rotary-inertia variants require 6 DOFs/node and will be added alongside
BEAM188 once mixed-DOF assembly is in place.

Real constants (per MAPDL element ref)
--------------------------------------
    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.

MAPDL compatibility — specification source
------------------------------------------
* Ansys, Inc., *Ansys Mechanical APDL Element Reference*,
  Release 2022R2, section "MASS21 — Structural Mass".

Short factual summary (paraphrased): single-node structural
mass element; ``KEYOPT(3)`` selects DOF count; femorph-solver
implements the ``KEYOPT(3)=2`` translational-only variant
(3 DOFs per node, lumped ``MASSX`` / ``MASSY`` / ``MASSZ`` from
real constants).  Rotary-inertia variants are roadmap (6-DOF
node assembly required).  Ansys Element Reference is the compat
spec only.
"""

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("MASS21 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 MASS21(ElementBase): name = "MASS21" n_nodes = 1 n_dof_per_node = 3 # UX, UY, UZ — KEYOPT(3)=2 (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))