"""COMBIN14 — 3D 2-node spring-damper (longitudinal mode only).
Default KEYOPT(2) = 0: longitudinal spring oriented along the I→J axis.
Real constants:
real[0] : K — spring stiffness (force / length)
real[1] : CV1 — linear damping (force · time / length, unused for K_e)
real[2] : CV2 — non-linear (cubic) damping (unused here)
real[3] : IL — initial length (unused; reference length is the nodal distance)
Stiffness (Theory Ref § 14.14)::
K_e = K · [[ C, -C],
[-C, C]] with C_ij = d_i · d_j (3 × 3)
where ``(d_x, d_y, d_z)`` is the unit vector along I→J.
COMBIN14 is massless by convention (modal codes treat it as a stiffness-only
contribution); ``me`` returns a 6×6 zero matrix so that assembly loops stay
uniform.
Only the longitudinal KEYOPT(2)=0 mode is implemented. Torsional (3) and
2-D planar (4, 5, 6) variants need separate code paths and will be added
alongside their datasets.
References
----------
* Discrete spring / lumped connector element (3-D longitudinal
mode): Cook, Malkus, Plesha, Witt, *Concepts and Applications
of Finite Element Analysis*, 4th ed., Wiley, 2002, §2.3 and
§2.10 (direction-cosine rotation of a scalar spring between
two nodes in 3-D). Stiffness ``K · (d ⊗ d)`` on the I→J
axis is the standard result.
* Direction-cosine transformation for a bar / spring aligned
with an arbitrary 3-D axis: Zienkiewicz, O.C. and Taylor, R.L.,
*The Finite Element Method*, 7th ed., 2013, §2.4.4.
MAPDL compatibility — specification source
------------------------------------------
* Ansys, Inc., *Ansys Mechanical APDL Element Reference*,
Release 2022R2, section "COMBIN14 — Spring-Damper".
Short factual summary (paraphrased): 2-node spring-damper;
``KEYOPT(2)=0`` is the longitudinal (axial) mode, 3 translational
DOFs per node; spring stiffness from ``REAL[0]``; linear and
non-linear damping real constants are unused by femorph-solver's
linear-elastic path. Torsional (``KEYOPT(2)=3``) and 2-D planar
modes are not yet implemented. 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 _axial_frame(coords: np.ndarray) -> tuple[float, np.ndarray]:
d = coords[1] - coords[0]
L = float(np.linalg.norm(d))
if L == 0.0:
raise ValueError("COMBIN14: coincident nodes, element length is zero")
return L, d / L
[docs]
@register
class COMBIN14(ElementBase):
name = "COMBIN14"
n_nodes = 2
n_dof_per_node = 3 # UX, UY, UZ — longitudinal KEYOPT(2)=0
vtk_cell_type = 3 # VTK_LINE
[docs]
@staticmethod
def ke(
coords: np.ndarray,
material: dict[str, float],
real: np.ndarray,
) -> np.ndarray:
_, d = _axial_frame(np.asarray(coords, dtype=np.float64))
real = np.asarray(real, dtype=np.float64)
if real.size == 0:
raise ValueError("COMBIN14 requires REAL[0]=K (spring constant); got empty real set")
k_spring = float(real[0])
C = np.outer(d, d)
k = k_spring * np.block([[C, -C], [-C, C]])
return np.ascontiguousarray(k)
[docs]
@staticmethod
def me(
coords: np.ndarray,
material: dict[str, float],
real: np.ndarray,
lumped: bool = False,
) -> np.ndarray:
return np.zeros((6, 6), dtype=np.float64)