2D elements: plane stress, plane strain and axisymmetric sections on the DOF framework; mesh.planar (M4) (#39)

Model: planar section kinds with Extent, element edges in sets, edge-weighted 2D loads,
centrifugal load, 2D model checks. fem: Plane kernels on Basis::{Tri3,Tri6,Quad4,Quad8}
declaring ElementDofs::new(DofSet::PLANE); QM6 incompatible modes with the
Wilson-Ibrahimbegovic correction. Solver: Kernel enum on the DofLayout; 3D results
bit-identical (hash-checked). Decks: Abaqus CPS/CPE/CAX, Nastran membranes and RFORCE,
LS-DYNA ELFORM 12/13. Mesh: free triangle mesher, planar region meshing, face boundaries.
Engine: mesh.planar with tagged edges. Analytic verification suite in solver/tests/plane.rs.


Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 20:24:56 -04:00
1 parent 4e4d19b88c
commit 4c0fcc80ad
42 files changed
+3270 -128

No files matched your search

+33 -10
View File
@@ -34,6 +34,7 @@
use modelcraft_model::ElementKind;
use crate::basis::Basis;
use crate::dense::{Cholesky, Matrix, inverse3, positive_inverse};
use crate::dof::{DofSet, ElementDofs, Internal};
use crate::material::{Elasticity, stress};
@@ -47,6 +48,9 @@ pub enum Family {
Tetrahedron,
Wedge,
Hexahedron,
/// The 2D elements of plane and axisymmetric models ([`crate::plane`]).
Triangle,
Quadrilateral,
}
/// One row of the element table: everything the kernels need to know about an element kind.
@@ -386,15 +390,7 @@ impl<'a> Solid<'a> {
/// The lumped (diagonal) mass per node: the consistent diagonal scaled to the element's mass.
pub fn lumped_mass(&self, density: f64) -> Result<Vec<f64>> {
let m = self.mass(density)?;
// The consistent entries sum to the element's mass (the shape functions sum to one).
let total: f64 = m.data.iter().sum();
let diag: Vec<f64> = (0..m.n).map(|i| m.get(i, i)).collect();
let sum: f64 = diag.iter().sum();
if !(sum > 0.0) {
return Err(self.degenerate());
}
Ok(diag.iter().map(|d| d * total / sum).collect())
lump(&self.mass(density)?).ok_or_else(|| self.degenerate())
}
/// Consistent nodal forces of a uniform force per volume: fᵢ = ∫ Nᵢ b dV.
@@ -411,6 +407,22 @@ impl<'a> Solid<'a> {
Ok(f)
}
/// Consistent nodal forces of a force per volume that varies with position (such as a
/// centrifugal load): fᵢ = ∫ Nᵢ b(x) dV.
pub fn body_force_field(&self, b: impl Fn([f64; 3]) -> [f64; 3]) -> Result<Vec<[f64; 3]>> {
let mut f = vec![[0.0; 3]; self.ty.nodes];
for p in self.ty.mass.points() {
let g = self.gradients(p.xi)?;
let b = b(self.position(p.xi));
for (fi, n) in f.iter_mut().zip(g.shape.values()) {
for k in 0..3 {
fi[k] += n * b[k] * g.det * p.weight;
}
}
}
Ok(f)
}
/// Strain and stress at every integration point for the nodal displacements `u`.
pub fn stresses(&self, d: &Elasticity, u: &[[f64; 3]]) -> Result<Vec<PointStress>> {
if u.len() != self.ty.nodes {
@@ -475,7 +487,16 @@ impl<'a> Solid<'a> {
}
/// Make a matrix exactly symmetric (the mean of it and its transpose), so assembly can rely on it.
fn symmetrize(k: &mut Matrix) {
/// HRZ lumping of a consistent mass matrix: its diagonal scaled to the element's mass (the
/// entries sum to it, the shape functions summing to one); `None` when the diagonal has no mass.
pub(crate) fn lump(m: &Matrix) -> Option<Vec<f64>> {
let total: f64 = m.data.iter().sum();
let diag: Vec<f64> = (0..m.n).map(|i| m.get(i, i)).collect();
let sum: f64 = diag.iter().sum();
(sum > 0.0).then(|| diag.iter().map(|d| d * total / sum).collect())
}
pub(crate) fn symmetrize(k: &mut Matrix) {
let n = k.n;
for i in 0..n {
for j in i + 1..n {
@@ -589,6 +610,8 @@ pub fn extrapolation(ty: &ElementType, rule: Rule) -> Result<Extrapolation> {
Family::Tetrahedron => shape::tet4(xi),
Family::Wedge => shape::wedge6(xi),
Family::Hexahedron => shape::hex8(xi),
Family::Triangle => Basis::Tri3.eval(xi),
Family::Quadrilateral => Basis::Quad4.eval(xi),
};
let mut corner_rows = vec![0.0; c * g];
if g < c {