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))