forked from 1b-agent/fieldcraft-upstream
Shells and beams: MITC4/MITC3 shells, Timoshenko beams, six-DOF nodes (M4) (#47)
* fem: MITC4/MITC3 flat shells and Timoshenko beams with profile properties (ElementDofs STRUCTURAL) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * solver: shells and beams on the DofLayout beside solids, 2D elements and trusses; analytic verification and tagged benchmarks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * engine: probe rotations, reaction moments and shell and beam element results; docs for shells and beams Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 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:
18 files changed
+2411
-48
No files matched your search
@@ -1,8 +1,10 @@
|
||||
//! Linear static and frequency analysis of a model's step, and their results.
|
||||
|
||||
use modelcraft_fem::ElementType;
|
||||
use modelcraft_fem::beam::BeamForces;
|
||||
use modelcraft_fem::element::{Extrapolation, extrapolation};
|
||||
use modelcraft_fem::quadrature::Rule;
|
||||
use modelcraft_fem::shell::ShellStress;
|
||||
use modelcraft_fem::{DofKind, DofLayout, PointStress, von_mises};
|
||||
use modelcraft_model::{ElementKind, Model, Procedure, Resolved, Step};
|
||||
use serde::Serialize;
|
||||
@@ -12,7 +14,7 @@ use crate::constraints::{Budget, Reduction, coupling_rows};
|
||||
use crate::eigen::{Operator, Wanted, lanczos};
|
||||
use crate::ordering::nested_dissection;
|
||||
use crate::sparse::{Csr, NodeGraph, add_block, add_diagonal, add_scalar};
|
||||
use crate::system::{Mesh, inelastic_warnings, loads, prescribed, translation_block};
|
||||
use crate::system::{Mesh, Structural, StructuralKernel, inelastic_warnings, loads, prescribed, structural_body_forces, translation_block};
|
||||
use crate::{Error, Limits, Result};
|
||||
|
||||
/// Size and effort of a solution.
|
||||
@@ -29,6 +31,22 @@ pub struct Stats {
|
||||
pub lanczos_vectors: usize,
|
||||
}
|
||||
|
||||
/// Section forces and surface stresses of one shell element.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ShellResult {
|
||||
pub label: u32,
|
||||
pub kind: ElementKind,
|
||||
pub stress: ShellStress,
|
||||
}
|
||||
|
||||
/// End section forces of one beam element.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BeamResult {
|
||||
pub label: u32,
|
||||
pub kind: ElementKind,
|
||||
pub forces: BeamForces,
|
||||
}
|
||||
|
||||
/// Stresses of one element.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ElementStress {
|
||||
@@ -57,13 +75,23 @@ pub struct StaticResult {
|
||||
/// Node labels with degrees of freedom, sorted; the per-node vectors follow this order.
|
||||
pub nodes: Vec<u32>,
|
||||
pub displacement: Vec<[f64; 3]>,
|
||||
/// Rotations about X, Y, Z (zero at nodes without rotations: those of solids and trusses
|
||||
/// only).
|
||||
pub rotation: Vec<[f64; 3]>,
|
||||
/// Reaction forces (zero where nothing is prescribed).
|
||||
pub reaction: Vec<[f64; 3]>,
|
||||
/// Reaction moments at held rotations.
|
||||
pub reaction_moment: Vec<[f64; 3]>,
|
||||
/// Stress averaged at the nodes over the elements that share them (xx, yy, zz, xy, xz, yz).
|
||||
pub stress: Vec<[f64; 6]>,
|
||||
/// Von Mises stress of the averaged nodal stress.
|
||||
pub von_mises: Vec<f64>,
|
||||
pub elements: Vec<ElementStress>,
|
||||
/// Shell section forces and surface stresses, by label (their top surface, the side their
|
||||
/// normal points to, joins the nodal stress average).
|
||||
pub shells: Vec<ShellResult>,
|
||||
/// Beam end section forces, by label.
|
||||
pub beams: Vec<BeamResult>,
|
||||
/// The coupled reference points, by name.
|
||||
pub reference_points: Vec<PointResult>,
|
||||
/// Sum of the applied forces and of the reaction forces (moments are not included).
|
||||
@@ -82,6 +110,8 @@ pub struct Mode {
|
||||
pub frequency: f64,
|
||||
/// Mass-normalized (φᵀ M φ = 1), per node in [`FrequencyResult::nodes`] order.
|
||||
pub shape: Vec<[f64; 3]>,
|
||||
/// The rotations of the shape (zero at nodes without rotations).
|
||||
pub rotation: Vec<[f64; 3]>,
|
||||
/// Participation factors Γ = φᵀ M r for unit translations along X, Y, Z.
|
||||
pub participation: [f64; 3],
|
||||
/// Effective mass Γ² along X, Y, Z.
|
||||
@@ -161,6 +191,15 @@ fn assemble(mesh: &Mesh, graph: &NodeGraph, mass: Option<MassKind>) -> Result<As
|
||||
}
|
||||
}
|
||||
}
|
||||
for s in &mesh.structural {
|
||||
let kernel = s.kernel()?;
|
||||
let at = |err| Error::element(s.label, err);
|
||||
add_block(&mut k, layout, &s.nodes, Structural::KINDS, &kernel.stiffness().map_err(at)?.data)?;
|
||||
if let (Some(mm), Some(kind)) = (m.as_mut(), mass) {
|
||||
let rho = s.density.ok_or_else(|| Error::Invalid(format!("element {} has no density", s.label)))?;
|
||||
add_block(mm, layout, &s.nodes, Structural::KINDS, &kernel.mass(rho, kind).map_err(at)?.data)?;
|
||||
}
|
||||
}
|
||||
mesh.discrete.add_stiffness(&mut k)?;
|
||||
if let Some(mm) = m.as_mut() {
|
||||
mesh.discrete.add_mass(mm)?;
|
||||
@@ -195,11 +234,17 @@ fn directions<const N: usize>(full: &[f64], layout: &DofLayout, slot: usize) ->
|
||||
std::array::from_fn(|c| DofKind::from_direction(c).and_then(|k| layout.dof(slot, k)).and_then(|d| full.get(d)).copied().unwrap_or(0.0))
|
||||
}
|
||||
|
||||
/// The mesh nodes' translations in a full vector, per node, and the reference points'
|
||||
/// translations and rotations, per point.
|
||||
fn split(full: &[f64], mesh: &Mesh) -> (Vec<[f64; 3]>, Vec<[f64; 6]>) {
|
||||
/// A full vector split into the mesh nodes' translations and rotations (zero where a node has
|
||||
/// none), per node, and the reference points' translations and rotations, per point.
|
||||
fn split(full: &[f64], mesh: &Mesh) -> (Vec<[f64; 3]>, Vec<[f64; 3]>, Vec<[f64; 6]>) {
|
||||
let l = &mesh.layout;
|
||||
((0..l.nodes()).map(|n| directions(full, l, n)).collect(), (0..mesh.points.len()).map(|k| directions(full, l, l.point_slot(k))).collect())
|
||||
let (translation, rotation) = (0..l.nodes())
|
||||
.map(|n| {
|
||||
let [a, b, c, d, e, f] = directions::<6>(full, l, n);
|
||||
([a, b, c], [d, e, f])
|
||||
})
|
||||
.unzip();
|
||||
(translation, rotation, (0..mesh.points.len()).map(|k| directions(full, l, l.point_slot(k))).collect())
|
||||
}
|
||||
|
||||
/// The matrix pattern of the mesh (node blocks of its elements, trusses and discrete elements)
|
||||
@@ -252,7 +297,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
let resolved = model.resolve()?;
|
||||
let step = model.select_step(step, |p| matches!(p, Procedure::Static {}), "static")?;
|
||||
let mesh = Mesh::new(model, &resolved, limits.max_dofs)?;
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).collect();
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).chain(mesh.warnings.iter().cloned()).collect();
|
||||
warnings.extend(inelastic_warnings(model));
|
||||
let (graph, order) = pattern(&mesh);
|
||||
ignored_dashpots(&mesh, &mut warnings);
|
||||
@@ -272,9 +317,9 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
if u.iter().any(|x| !x.is_finite()) {
|
||||
return Err(Error::Singular("the solution is not finite; the model is ill-conditioned".into()));
|
||||
}
|
||||
let (displacement, point_motion) = split(&u, &mesh);
|
||||
let (displacement, rotation, point_motion) = split(&u, &mesh);
|
||||
let reactions = red.reactions(&k, &u, &f);
|
||||
let (reaction, point_reaction) = split(&reactions, &mesh);
|
||||
let (reaction, reaction_moment, point_reaction) = split(&reactions, &mesh);
|
||||
let (mut applied_total, mut reaction_total) = ([0.0; 3], [0.0; 3]);
|
||||
for (dof, (fv, rv)) in f.iter().zip(&reactions).enumerate() {
|
||||
let Some(d) = mesh.kind(dof).and_then(DofKind::direction) else { continue };
|
||||
@@ -337,6 +382,31 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
elements.push(ElementStress { label: t.label, kind: t.kind, points, nodal });
|
||||
}
|
||||
elements.sort_by_key(|e| e.label);
|
||||
let (mut shells, mut beams) = (Vec::new(), Vec::new());
|
||||
let per_volume = structural_body_forces(model, &resolved, &mesh, step)?;
|
||||
for (s, body) in mesh.structural.iter().zip(&per_volume) {
|
||||
let at = |err| Error::element(s.label, err);
|
||||
let dofs = mesh
|
||||
.layout
|
||||
.element_dofs(&s.nodes, Structural::KINDS)
|
||||
.ok_or_else(|| Error::Internal(format!("element {} degrees of freedom", s.label)))?;
|
||||
let ue: Vec<f64> = dofs.iter().map(|d| u.get(*d).copied().unwrap_or(0.0)).collect();
|
||||
match s.kernel()? {
|
||||
StructuralKernel::Shell(shell) => {
|
||||
let stress = shell.stresses(&ue).map_err(at)?;
|
||||
for (n, v) in s.nodes.iter().zip(&stress.top) {
|
||||
if let (Some(acc), Some(c)) = (sum.get_mut(*n), count.get_mut(*n)) {
|
||||
for k in 0..6 {
|
||||
acc[k] += v[k];
|
||||
}
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
shells.push(ShellResult { label: s.label, kind: s.kind, stress });
|
||||
}
|
||||
StructuralKernel::Beam(beam) => beams.push(BeamResult { label: s.label, kind: s.kind, forces: beam.forces(&ue, *body).map_err(at)? }),
|
||||
}
|
||||
}
|
||||
let stress: Vec<[f64; 6]> = sum.iter().zip(&count).map(|(s, c)| s.map(|v| if *c > 0 { v / f64::from(*c) } else { 0.0 })).collect();
|
||||
let von_mises = stress.iter().map(von_mises).collect();
|
||||
let scale = applied_total.iter().chain(&reaction_total).fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
@@ -349,10 +419,14 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|
||||
step: step.name.clone(),
|
||||
nodes: mesh.labels.clone(),
|
||||
displacement,
|
||||
rotation,
|
||||
reaction,
|
||||
reaction_moment,
|
||||
stress,
|
||||
von_mises,
|
||||
elements,
|
||||
shells,
|
||||
beams,
|
||||
reference_points,
|
||||
applied_total,
|
||||
reaction_total,
|
||||
@@ -398,7 +472,7 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
return Err(Error::Invalid(format!("the shift must be a finite number, got {s}")));
|
||||
}
|
||||
let mesh = Mesh::new(model, &resolved, limits.max_dofs)?;
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).collect();
|
||||
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).chain(mesh.warnings.iter().cloned()).collect();
|
||||
warnings.extend(inelastic_warnings(model));
|
||||
let (graph, order) = pattern(&mesh);
|
||||
ignored_dashpots(&mesh, &mut warnings);
|
||||
@@ -406,14 +480,18 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
let red = reduction(&setup, false, &order, budget(limits, &mesh, &graph, 2), &mut warnings)?;
|
||||
let Assembled { k, m } = assemble(&mesh, &graph, Some(options.mass))?;
|
||||
let m = m.ok_or_else(|| Error::Internal("mass matrix".into()))?;
|
||||
// Every element's mass entries sum to its mass, once per direction.
|
||||
// (the mean over the directions for a point mass given per direction; rotary inertia is left
|
||||
// out), added in storage order.
|
||||
let translation = |d: usize| mesh.kind(d).and_then(DofKind::direction).is_some_and(|c| c < 3);
|
||||
// Every element's mass entries between translations sum to its mass, once per direction
|
||||
// (the mean over the directions for a point mass given per direction; rotary inertia, and
|
||||
// the translation–rotation coupling of shells and beams, are left out), added in storage
|
||||
// order.
|
||||
let translation: Vec<bool> = (0..m.n).map(|d| mesh.kind(d).and_then(DofKind::direction).is_some_and(|c| c < 3)).collect();
|
||||
let mut translational = 0.0;
|
||||
for d in (0..m.n).filter(|d| translation(*d)) {
|
||||
for v in m.row(d).1 {
|
||||
translational += v;
|
||||
for d in (0..m.n).filter(|d| translation.get(*d).copied().unwrap_or(false)) {
|
||||
let (cols, vals) = m.row(d);
|
||||
for (c, v) in cols.iter().zip(vals) {
|
||||
if translation.get(*c).copied().unwrap_or(false) {
|
||||
translational += v;
|
||||
}
|
||||
}
|
||||
}
|
||||
let total_mass = translational / mesh.translation_kinds().len() as f64;
|
||||
@@ -468,11 +546,12 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
|
||||
let mut out = Vec::with_capacity(pairs.values.len());
|
||||
for (lambda, phi) in pairs.values.iter().zip(&pairs.vectors) {
|
||||
let participation = [0, 1, 2].map(|d| mr.get(d).map_or(0.0, |y| y.iter().zip(phi).map(|(a, b)| a * b).sum()));
|
||||
let (shape, point_shape) = split(&red.expand(phi, false), &mesh);
|
||||
let (shape, rotation, point_shape) = split(&red.expand(phi, false), &mesh);
|
||||
out.push(Mode {
|
||||
eigenvalue: *lambda,
|
||||
frequency: lambda.max(0.0).sqrt() / two_pi,
|
||||
shape,
|
||||
rotation,
|
||||
participation,
|
||||
effective_mass: participation.map(|g| g * g),
|
||||
point_shape,
|
||||
@@ -501,6 +580,8 @@ pub enum Location {
|
||||
|
||||
/// The result of one step, as a session keeps it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
// One per solved step; boxing the larger variant would buy nothing.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum StepResult {
|
||||
Static(StaticResult),
|
||||
Frequency(FrequencyResult),
|
||||
|
||||
Reference in new issue
Block a user