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
@@ -0,0 +1,429 @@
|
||||
//! The two-node Timoshenko beam (six degrees of freedom per node), with the section properties of
|
||||
//! the model's beam profiles. (Axial-only members are the trusses of [`crate::truss`].)
|
||||
//!
|
||||
//! **Local axes.** x runs from node 1 to node 2; y is the section's orientation vector (the
|
||||
//! profile's local 1 direction, its `width`) made normal to x; z = x × y (the profile's local 2
|
||||
//! direction, its `height`). Degrees of freedom per node: translations along x, y, z, then
|
||||
//! rotations about them; element matrices are built in local axes and rotated to the global ones
|
||||
//! ([`crate::frame`]).
|
||||
//!
|
||||
//! **Beam.** Axial and torsional stiffness are the exact two-node ones (EA/L, GJ/L). Each bending
|
||||
//! plane uses the interdependent interpolation of the Timoshenko beam: cubic deflection and
|
||||
//! quadratic rotation, coupled through Φ = 12 EI / (G A_s L²) so the shear strain is constant and
|
||||
//! the element is exact for end loads (any number of elements gives the nodal displacements of
|
||||
//! Timoshenko beam theory; J. S. Przemieniecki, *Theory of Matrix Structural Analysis*,
|
||||
//! McGraw-Hill 1968, §5.6; Z. Friedman, J. B. Kosmatka, *An improved two-node Timoshenko beam
|
||||
//! finite element*, Computers & Structures 47(3):473–481, 1993, whose shape functions are used
|
||||
//! here). With these functions w(ξ) and θ(ξ), ξ = x/L,
|
||||
//! K = ∫ (EI θ'ᵀ θ' + G A_s (w' − θ)ᵀ (w' − θ)) dx and the consistent mass
|
||||
//! M = ∫ (ρA N_wᵀ N_w + ρI N_θᵀ N_θ) dx (translational and rotary inertia) are integrated with
|
||||
//! four Gauss points, exactly; Φ → 0 recovers the Euler–Bernoulli (Hermite) element. The
|
||||
//! rotation about y is −dw/dx in the x–z plane, so that plane's rotations enter with a minus
|
||||
//! sign. Lumped mass: half the mass on each node's translations, half the polar inertia on the
|
||||
//! twist, and the consistent bending rotary diagonal scaled like the translations (Hinton, Rock &
|
||||
//! Zienkiewicz 1976). End forces are K u minus the consistent loads of the element's body force,
|
||||
//! reported as the section forces at each end (axial force N, shears V_y, V_z, torque T, moments
|
||||
//! M_y, M_z on the cut whose outward normal is +x).
|
||||
//!
|
||||
//! **Section properties.** Area, second moments I_y = ∫ z² dA and I_z = ∫ y² dA, the polar
|
||||
//! moment I_y + I_z (rotary inertia of torsion), the torsion constant J and the shear areas:
|
||||
//! - rectangle: J from Saint-Venant's series, J = a b³/3 (1 − 192 b/(π⁵ a) Σ_{n odd}
|
||||
//! tanh(nπa/2b)/n⁵) for sides a ≥ b (S. P. Timoshenko, J. N. Goodier, *Theory of Elasticity*,
|
||||
//! 3rd ed., McGraw-Hill 1970, §109); shear coefficient 10(1 + ν)/(12 + 11ν);
|
||||
//! - circle and pipe: J = I_y + I_z; shear coefficients 6(1 + ν)/(7 + 6ν) and, for a tube of
|
||||
//! radius ratio m, 6(1 + ν)(1 + m²)² / ((7 + 6ν)(1 + m²)² + (20 + 12ν) m²) (G. R. Cowper,
|
||||
//! *The shear coefficient in Timoshenko's beam theory*, J. Appl. Mech. 33(2):335–340, 1966);
|
||||
//! - I and box (thin-walled): J = Σ b t³/3 over the midline walls (open I) and Bredt's
|
||||
//! 4 A_m² t / p (closed box); shear areas the walls parallel to the shear, between the midlines
|
||||
//! of the walls across it (the web for shear along z, 5/6 of the flanges along y; the two
|
||||
//! webs or the two flanges of a box) (W. D. Pilkey, *Analysis and Design of Elastic Beams*,
|
||||
//! Wiley 2002, ch. 6).
|
||||
|
||||
use modelcraft_model::{BeamProfile, Elastic};
|
||||
|
||||
use crate::dense::Matrix;
|
||||
use crate::dof::{DofSet, ElementDofs};
|
||||
use crate::frame::{Axes, Vec3, cross, dot, norm, rotate_matrix, rotate_to_global, rotate_to_local, sub, to_local, translation, unit};
|
||||
use crate::quadrature::gauss_legendre;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Cross-section properties in the beam's local axes (y along the profile's width, z along its
|
||||
/// height).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Properties {
|
||||
pub area: f64,
|
||||
/// ∫ z² dA: bending with deflection along z.
|
||||
pub iy: f64,
|
||||
/// ∫ y² dA: bending with deflection along y.
|
||||
pub iz: f64,
|
||||
/// Saint-Venant torsion constant.
|
||||
pub torsion: f64,
|
||||
/// Shear areas for shear along y and along z.
|
||||
pub shear_y: f64,
|
||||
pub shear_z: f64,
|
||||
}
|
||||
|
||||
/// Saint-Venant's torsion constant of a solid rectangle.
|
||||
fn rectangle_torsion(width: f64, height: f64) -> f64 {
|
||||
let (a, b) = (width.max(height), width.min(height));
|
||||
let pi = std::f64::consts::PI;
|
||||
let series: f64 = (0..50).map(|k| f64::from(2 * k + 1)).map(|n| (n * pi * a / (2.0 * b)).tanh() / n.powi(5)).sum();
|
||||
a * b * b * b / 3.0 * (1.0 - 192.0 * b / (pi.powi(5) * a) * series)
|
||||
}
|
||||
|
||||
/// The section properties of a profile (`poisson` enters the shear coefficients).
|
||||
pub fn properties(profile: &BeamProfile, poisson: f64) -> Properties {
|
||||
let pi = std::f64::consts::PI;
|
||||
let area = profile.area();
|
||||
match *profile {
|
||||
BeamProfile::Rectangle { width, height } => {
|
||||
let k = 10.0 * (1.0 + poisson) / (12.0 + 11.0 * poisson);
|
||||
Properties {
|
||||
area,
|
||||
iy: width * height.powi(3) / 12.0,
|
||||
iz: height * width.powi(3) / 12.0,
|
||||
torsion: rectangle_torsion(width, height),
|
||||
shear_y: k * area,
|
||||
shear_z: k * area,
|
||||
}
|
||||
}
|
||||
BeamProfile::Circle { radius } => {
|
||||
let i = pi * radius.powi(4) / 4.0;
|
||||
let k = 6.0 * (1.0 + poisson) / (7.0 + 6.0 * poisson);
|
||||
Properties { area, iy: i, iz: i, torsion: 2.0 * i, shear_y: k * area, shear_z: k * area }
|
||||
}
|
||||
BeamProfile::Pipe { radius, thickness } => {
|
||||
let inner = radius - thickness;
|
||||
let i = pi * (radius.powi(4) - inner.powi(4)) / 4.0;
|
||||
let m2 = (inner / radius).powi(2);
|
||||
let k = 6.0 * (1.0 + poisson) * (1.0 + m2).powi(2) / ((7.0 + 6.0 * poisson) * (1.0 + m2).powi(2) + (20.0 + 12.0 * poisson) * m2);
|
||||
Properties { area, iy: i, iz: i, torsion: 2.0 * i, shear_y: k * area, shear_z: k * area }
|
||||
}
|
||||
BeamProfile::I { height, width, web, flange } => {
|
||||
let inside = height - 2.0 * flange;
|
||||
Properties {
|
||||
area,
|
||||
iy: (width * height.powi(3) - (width - web) * inside.powi(3)) / 12.0,
|
||||
iz: (2.0 * flange * width.powi(3) + inside * web.powi(3)) / 12.0,
|
||||
torsion: (2.0 * width * flange.powi(3) + (height - flange) * web.powi(3)) / 3.0,
|
||||
shear_y: 5.0 / 6.0 * 2.0 * width * flange,
|
||||
shear_z: (height - flange) * web,
|
||||
}
|
||||
}
|
||||
BeamProfile::Box { width, height, thickness: t } => {
|
||||
let (wi, hi) = (width - 2.0 * t, height - 2.0 * t);
|
||||
let (wm, hm) = (width - t, height - t);
|
||||
Properties {
|
||||
area,
|
||||
iy: (width * height.powi(3) - wi * hi.powi(3)) / 12.0,
|
||||
iz: (height * width.powi(3) - hi * wi.powi(3)) / 12.0,
|
||||
torsion: 4.0 * (wm * hm).powi(2) * t / (2.0 * (wm + hm)),
|
||||
shear_y: 2.0 * wm * t,
|
||||
shear_z: 2.0 * hm * t,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Young's and shear moduli and Poisson's ratio of an isotropic material.
|
||||
/// (`what` names the section kind in errors.)
|
||||
fn moduli(elastic: &Elastic, what: &str) -> Result<(f64, f64, f64)> {
|
||||
let (young, poisson) = crate::material::isotropic_constants(elastic, what)?;
|
||||
Ok((young, young / (2.0 * (1.0 + poisson)), poisson))
|
||||
}
|
||||
|
||||
/// A beam section: profile properties and an isotropic material.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct BeamSection {
|
||||
pub properties: Properties,
|
||||
pub young: f64,
|
||||
pub shear_modulus: f64,
|
||||
}
|
||||
|
||||
impl BeamSection {
|
||||
pub fn new(profile: &BeamProfile, elastic: &Elastic) -> Result<BeamSection> {
|
||||
profile.validate().map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
let (young, shear_modulus, poisson) = moduli(elastic, "beam")?;
|
||||
Ok(BeamSection { properties: properties(profile, poisson), young, shear_modulus })
|
||||
}
|
||||
}
|
||||
|
||||
/// The section forces at the two ends of a beam: N, V_y, V_z, T, M_y, M_z in its local axes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct BeamForces {
|
||||
pub axes: Axes,
|
||||
pub ends: [[f64; 6]; 2],
|
||||
}
|
||||
|
||||
/// One bending plane's stiffness and mass (deflection, rotation at node 1, then node 2), with
|
||||
/// θ = dw/dx in the plane's own convention.
|
||||
fn bending_plane(ei: f64, gas: f64, rho_a: f64, rho_i: f64, l: f64) -> ([[f64; 4]; 4], [[f64; 4]; 4]) {
|
||||
let phi = 12.0 * ei / (gas * l * l);
|
||||
let mu = 1.0 / (1.0 + phi);
|
||||
let (mut k, mut m) = ([[0.0; 4]; 4], [[0.0; 4]; 4]);
|
||||
for &(x, w) in gauss_legendre(4) {
|
||||
let xi = 0.5 * (x + 1.0);
|
||||
let w = 0.5 * w * l;
|
||||
let x2 = xi * xi;
|
||||
let x3 = x2 * xi;
|
||||
let nw = [
|
||||
mu * (2.0 * x3 - 3.0 * x2 - phi * xi + 1.0 + phi),
|
||||
mu * l * (x3 - (2.0 + phi / 2.0) * x2 + (1.0 + phi / 2.0) * xi),
|
||||
-mu * (2.0 * x3 - 3.0 * x2 - phi * xi),
|
||||
mu * l * (x3 - (1.0 - phi / 2.0) * x2 - phi / 2.0 * xi),
|
||||
];
|
||||
let nt =
|
||||
[mu * 6.0 / l * (x2 - xi), mu * (3.0 * x2 - (4.0 + phi) * xi + 1.0 + phi), -mu * 6.0 / l * (x2 - xi), mu * (3.0 * x2 - (2.0 - phi) * xi)];
|
||||
// θ' and γ = w' − θ.
|
||||
let dt = [
|
||||
mu * 6.0 / (l * l) * (2.0 * xi - 1.0),
|
||||
mu / l * (6.0 * xi - 4.0 - phi),
|
||||
-mu * 6.0 / (l * l) * (2.0 * xi - 1.0),
|
||||
mu / l * (6.0 * xi - 2.0 + phi),
|
||||
];
|
||||
let dw = [
|
||||
mu / l * (6.0 * x2 - 6.0 * xi - phi),
|
||||
mu * (3.0 * x2 - (4.0 + phi) * xi + 1.0 + phi / 2.0),
|
||||
-mu / l * (6.0 * x2 - 6.0 * xi - phi),
|
||||
mu * (3.0 * x2 - (2.0 - phi) * xi - phi / 2.0),
|
||||
];
|
||||
let g: [f64; 4] = std::array::from_fn(|a| dw[a] - nt[a]);
|
||||
for a in 0..4 {
|
||||
for b in 0..4 {
|
||||
k[a][b] += w * (ei * dt[a] * dt[b] + gas * g[a] * g[b]);
|
||||
m[a][b] += w * (rho_a * nw[a] * nw[b] + rho_i * nt[a] * nt[b]);
|
||||
}
|
||||
}
|
||||
}
|
||||
(k, m)
|
||||
}
|
||||
|
||||
/// A two-node beam element.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Beam {
|
||||
axes: Axes,
|
||||
length: f64,
|
||||
section: BeamSection,
|
||||
}
|
||||
|
||||
/// A line element's unit axis (node 1 to node 2) and length.
|
||||
fn line_axis(xyz: &[[f64; 3]]) -> Result<(Vec3, f64)> {
|
||||
let [a, b] = xyz else {
|
||||
return Err(Error::Invalid(format!("a two-node line element has 2 nodes, got {}", xyz.len())));
|
||||
};
|
||||
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
let d = sub(*b, *a);
|
||||
let length = norm(d);
|
||||
let ex = unit(d).ok_or_else(|| Error::Degenerate("a line element has zero length".into()))?;
|
||||
Ok((ex, length))
|
||||
}
|
||||
|
||||
/// The beam's local axes from its end points and orientation vector.
|
||||
fn beam_axes(xyz: &[[f64; 3]], orientation: Vec3) -> Result<(Axes, f64)> {
|
||||
let (ex, length) = line_axis(xyz)?;
|
||||
let o = unit(orientation).ok_or_else(|| Error::Invalid("a beam's orientation vector must be nonzero and finite".into()))?;
|
||||
let proj = sub(o, ex.map(|c| c * dot(o, ex)));
|
||||
let ey = unit(proj)
|
||||
.filter(|_| norm(proj) > 1e-6)
|
||||
.ok_or_else(|| Error::Invalid("the beam section's orientation vector is parallel to the beam".into()))?;
|
||||
Ok(([ex, ey, cross(ex, ey)], length))
|
||||
}
|
||||
|
||||
impl Beam {
|
||||
pub fn new(xyz: &[[f64; 3]], orientation: Vec3, section: BeamSection) -> Result<Beam> {
|
||||
let (axes, length) = beam_axes(xyz, orientation)?;
|
||||
Ok(Beam { axes, length, section })
|
||||
}
|
||||
|
||||
pub fn axes(&self) -> Axes {
|
||||
self.axes
|
||||
}
|
||||
|
||||
pub fn length(&self) -> f64 {
|
||||
self.length
|
||||
}
|
||||
|
||||
/// The element's unknowns: three translations and three rotations per node.
|
||||
pub fn dofs(&self) -> ElementDofs {
|
||||
ElementDofs::new(DofSet::STRUCTURAL)
|
||||
}
|
||||
|
||||
/// Local stiffness and consistent mass, 12 × 12.
|
||||
fn local(&self, density: f64) -> (Matrix, Matrix) {
|
||||
let (p, e, g, l) = (self.section.properties, self.section.young, self.section.shear_modulus, self.length);
|
||||
let (mut k, mut m) = (Matrix::zeros(12), Matrix::zeros(12));
|
||||
let pair = |k: &mut Matrix, m: &mut Matrix, dofs: [usize; 2], stiff: f64, mass: f64| {
|
||||
for (a, &i) in dofs.iter().enumerate() {
|
||||
for (b, &j) in dofs.iter().enumerate() {
|
||||
k.add(i, j, if a == b { stiff } else { -stiff });
|
||||
m.add(i, j, mass / 6.0 * if a == b { 2.0 } else { 1.0 });
|
||||
}
|
||||
}
|
||||
};
|
||||
pair(&mut k, &mut m, [0, 6], e * p.area / l, density * p.area * l);
|
||||
pair(&mut k, &mut m, [3, 9], g * p.torsion / l, density * (p.iy + p.iz) * l);
|
||||
// x–y plane: v and θ_z = dv/dx; x–z plane: w and θ_y = −dw/dx.
|
||||
let planes = [([1, 5, 7, 11], [1.0; 4], p.iz, p.shear_y), ([2, 4, 8, 10], [1.0, -1.0, 1.0, -1.0], p.iy, p.shear_z)];
|
||||
for (dofs, sign, i, shear) in planes {
|
||||
let (kp, mp) = bending_plane(e * i, g * shear, density * p.area, density * i, l);
|
||||
for a in 0..4 {
|
||||
for b in 0..4 {
|
||||
k.add(dofs[a], dofs[b], sign[a] * sign[b] * kp[a][b]);
|
||||
m.add(dofs[a], dofs[b], sign[a] * sign[b] * mp[a][b]);
|
||||
}
|
||||
}
|
||||
}
|
||||
(k, m)
|
||||
}
|
||||
|
||||
pub fn stiffness(&self) -> Matrix {
|
||||
rotate_matrix(&self.local(0.0).0, &self.axes)
|
||||
}
|
||||
|
||||
pub fn mass(&self, density: f64) -> Matrix {
|
||||
rotate_matrix(&self.local(density).1, &self.axes)
|
||||
}
|
||||
|
||||
/// The lumped mass, block diagonal (3 × 3 rotation blocks) in global axes.
|
||||
pub fn lumped_mass(&self, density: f64) -> Matrix {
|
||||
let (_, m) = self.local(density);
|
||||
let (p, l) = (self.section.properties, self.length);
|
||||
let half = density * p.area * l / 2.0;
|
||||
let mut d = Matrix::zeros(12);
|
||||
// Bending rotations scaled like their plane's translations (equal in both planes).
|
||||
for (rotation, deflection) in [(5, 1), (4, 2)] {
|
||||
let scale = 2.0 * half / (m.get(deflection, deflection) + m.get(deflection + 6, deflection + 6));
|
||||
d.add(rotation, rotation, scale * m.get(rotation, rotation));
|
||||
d.add(rotation + 6, rotation + 6, scale * m.get(rotation + 6, rotation + 6));
|
||||
}
|
||||
for node in [0, 6] {
|
||||
for c in 0..3 {
|
||||
d.add(node + c, node + c, half);
|
||||
}
|
||||
d.add(node + 3, node + 3, density * (p.iy + p.iz) * l / 2.0);
|
||||
}
|
||||
rotate_matrix(&d, &self.axes)
|
||||
}
|
||||
|
||||
/// Consistent nodal loads of a uniform force per volume b, in local axes: the consistent
|
||||
/// mass of unit density times the uniform nodal translation.
|
||||
fn local_body_force(&self, b: [f64; 3]) -> Vec<f64> {
|
||||
self.local(1.0).1.mul_vec(&translation(to_local(&self.axes, b), 2))
|
||||
}
|
||||
|
||||
/// Consistent nodal forces and moments (global) of a uniform force per volume b.
|
||||
pub fn body_force(&self, b: [f64; 3]) -> Vec<f64> {
|
||||
rotate_to_global(&self.local_body_force(b), &self.axes)
|
||||
}
|
||||
|
||||
/// Section forces at the ends for the nodal displacements and rotations `u` (global, 12)
|
||||
/// under a uniform force per volume `body` acting on the element.
|
||||
pub fn forces(&self, u: &[f64], body: [f64; 3]) -> Result<BeamForces> {
|
||||
if u.len() != 12 {
|
||||
return Err(Error::Invalid(format!("{} values for a beam element with 12 degrees of freedom", u.len())));
|
||||
}
|
||||
let ku = self.local(0.0).0.mul_vec(&rotate_to_local(u, &self.axes));
|
||||
let end: Vec<f64> = ku.iter().zip(self.local_body_force(body)).map(|(a, b)| a - b).collect();
|
||||
let at = |i: usize| end.get(i).copied().unwrap_or(0.0);
|
||||
Ok(BeamForces { axes: self.axes, ends: [std::array::from_fn(|c| -at(c)), std::array::from_fn(|c| at(6 + c))] })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const STEEL: Elastic = Elastic::Isotropic { young: 210e3, poisson: 0.3 };
|
||||
|
||||
/// Rigid translations and rotations about a point give no force.
|
||||
fn rigid_motions(xyz: &[[f64; 3]]) -> Vec<Vec<f64>> {
|
||||
(0..6)
|
||||
.map(|m| {
|
||||
xyz.iter()
|
||||
.flat_map(|x| {
|
||||
let mut d = [0.0; 6];
|
||||
if m < 3 {
|
||||
d[m] = 1.0;
|
||||
} else {
|
||||
let mut w = [0.0; 3];
|
||||
w[m - 3] = 1.0;
|
||||
let t = cross(w, *x);
|
||||
d[..3].copy_from_slice(&t);
|
||||
d[3..].copy_from_slice(&w);
|
||||
}
|
||||
d
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_beam_element_is_exact_for_a_tip_load() {
|
||||
// Cantilever along a skew axis; tip force along the local z axis: δ = PL³/3EI + PL/(G A_s).
|
||||
let profile = BeamProfile::Rectangle { width: 3.0, height: 5.0 };
|
||||
let section = BeamSection::new(&profile, &STEEL).unwrap();
|
||||
let xyz = [[1.0, 2.0, 3.0], [41.0, 32.0, 3.0]];
|
||||
let beam = Beam::new(&xyz, [0.0, 0.0, 1.0], section).unwrap();
|
||||
let l = beam.length();
|
||||
assert!((l - 50.0).abs() < 1e-12);
|
||||
let k = beam.stiffness();
|
||||
for r in rigid_motions(&xyz) {
|
||||
let f = k.mul_vec(&r);
|
||||
assert!(f.iter().all(|v| v.abs() < 1e-6), "{f:?}");
|
||||
}
|
||||
// Clamp node 1: solve the 6 × 6 block of node 2 for a unit force along local z.
|
||||
let axes = beam.axes();
|
||||
let mut k22 = Matrix::zeros(6);
|
||||
for i in 0..6 {
|
||||
for j in 0..6 {
|
||||
k22.add(i, j, k.get(6 + i, 6 + j));
|
||||
}
|
||||
}
|
||||
let mut f = vec![0.0; 6];
|
||||
f[..3].copy_from_slice(&axes[2]);
|
||||
crate::dense::Cholesky::new(&k22).unwrap().solve(&mut f);
|
||||
let p = section.properties;
|
||||
let want = l.powi(3) / (3.0 * section.young * p.iy) + l / (section.shear_modulus * p.shear_z);
|
||||
let got = dot([f[0], f[1], f[2]], axes[2]);
|
||||
assert!((got - want).abs() < 1e-12 * want, "{got} vs {want}");
|
||||
// Section forces at the clamp: V_z = 1, M_y = −L (the moment about y of a +z load at +x).
|
||||
let mut u = vec![0.0; 12];
|
||||
u[6..].copy_from_slice(&f);
|
||||
let forces = beam.forces(&u, [0.0; 3]).unwrap();
|
||||
assert!((forces.ends[0][2] - 1.0).abs() < 1e-9 && (forces.ends[0][4] + l).abs() < 1e-8, "{forces:?}");
|
||||
assert!((forces.ends[1][2] - 1.0).abs() < 1e-9 && forces.ends[1][4].abs() < 1e-8, "{forces:?}");
|
||||
// Mass: the translational entries sum to the mass along each direction.
|
||||
let m = beam.mass(7.8e-9);
|
||||
let total = 7.8e-9 * p.area * l;
|
||||
for d in 0..3 {
|
||||
let s: f64 = [d, 6 + d].iter().flat_map(|&i| [d, 6 + d].map(|j| m.get(i, j))).sum();
|
||||
assert!((s - total).abs() < 1e-12 * total, "{s} {total}");
|
||||
}
|
||||
let lumped = beam.lumped_mass(7.8e-9);
|
||||
assert!((lumped.get(0, 0) - total / 2.0).abs() < 1e-12 * total);
|
||||
assert!(Beam::new(&xyz, [40.0, 30.0, 0.0], section).is_err(), "orientation along the axis");
|
||||
assert!(Beam::new(&[xyz[0], xyz[0]], [0.0, 0.0, 1.0], section).is_err(), "zero length");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_properties() {
|
||||
// A square: J = 0.1406 a⁴ (Timoshenko & Goodier's table, 0.1406); thin strip J → b t³/3.
|
||||
let sq = properties(&BeamProfile::Rectangle { width: 2.0, height: 2.0 }, 0.3);
|
||||
assert!((sq.torsion / 16.0 - 0.1406).abs() < 1e-4, "{}", sq.torsion / 16.0);
|
||||
let strip = properties(&BeamProfile::Rectangle { width: 1000.0, height: 1.0 }, 0.3);
|
||||
assert!((strip.torsion / (1000.0 / 3.0) - 1.0).abs() < 1e-3, "{}", strip.torsion);
|
||||
// A thin pipe: the shear coefficient tends to 2(1 + ν)/(4 + 3ν), J = 2 π r³ t.
|
||||
let (r, t) = (100.0, 0.01);
|
||||
let pipe = properties(&BeamProfile::Pipe { radius: r, thickness: t }, 0.3);
|
||||
let k = pipe.shear_y / pipe.area;
|
||||
assert!((k - 2.6 / 4.9).abs() < 1e-3, "{k}");
|
||||
assert!((pipe.torsion / (2.0 * std::f64::consts::PI * r.powi(3) * t) - 1.0).abs() < 1e-3);
|
||||
// A box is a pair of I sections in bending: the same I_y with webs of twice the thickness.
|
||||
let b = properties(&BeamProfile::Box { width: 10.0, height: 20.0, thickness: 1.0 }, 0.3);
|
||||
let i = properties(&BeamProfile::I { height: 20.0, width: 10.0, web: 2.0, flange: 1.0 }, 0.3);
|
||||
assert!((b.iy - i.iy).abs() < 1e-9 && (b.area - i.area).abs() < 1e-12);
|
||||
let circle = properties(&BeamProfile::Circle { radius: 2.0 }, 0.3);
|
||||
assert!((circle.torsion - std::f64::consts::PI * 8.0).abs() < 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//! Local frames of structural elements (shells, beams, rods) and the rotation of their matrices
|
||||
//! between the element's local axes and the global ones.
|
||||
//!
|
||||
//! A frame is a rotation R whose rows are the local axes e₁, e₂, e₃ in global components, so
|
||||
//! local components are a_l = R a_g. Element degrees of freedom come in groups of three (the
|
||||
//! translations, then the rotations, of each node), each rotated by R, so the element matrix is
|
||||
//! K_g = Tᵀ K_l T with T = diag(R, R, …) (Cook, Malkus, Plesha & Witt 2002, §2.10 and §7.4).
|
||||
|
||||
use crate::dense::Matrix;
|
||||
|
||||
pub type Vec3 = [f64; 3];
|
||||
|
||||
/// The rows e₁, e₂, e₃ of a rotation from global to local components.
|
||||
pub type Axes = [Vec3; 3];
|
||||
|
||||
pub fn sub(a: Vec3, b: Vec3) -> Vec3 {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
|
||||
pub fn dot(a: Vec3, b: Vec3) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
pub fn cross(a: Vec3, b: Vec3) -> Vec3 {
|
||||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||||
}
|
||||
|
||||
pub fn norm(a: Vec3) -> f64 {
|
||||
dot(a, a).sqrt()
|
||||
}
|
||||
|
||||
/// The unit vector along `a`, or `None` when it is zero or not finite.
|
||||
pub fn unit(a: Vec3) -> Option<Vec3> {
|
||||
let n = norm(a);
|
||||
(n > 0.0 && n.is_finite()).then(|| a.map(|c| c / n))
|
||||
}
|
||||
|
||||
/// R a: global components to local.
|
||||
pub fn to_local(r: &Axes, a: Vec3) -> Vec3 {
|
||||
[dot(r[0], a), dot(r[1], a), dot(r[2], a)]
|
||||
}
|
||||
|
||||
/// Rᵀ a: local components to global.
|
||||
pub fn to_global(r: &Axes, a: Vec3) -> Vec3 {
|
||||
let mut g = [0.0; 3];
|
||||
for (k, axis) in r.iter().enumerate() {
|
||||
for c in 0..3 {
|
||||
g[c] += axis[c] * a[k];
|
||||
}
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
/// Tᵀ K T for a symmetric matrix whose degrees of freedom come in groups of three local
|
||||
/// components; the result is exactly symmetric.
|
||||
pub fn rotate_matrix(k: &Matrix, r: &Axes) -> Matrix {
|
||||
let n = k.n;
|
||||
let groups = n / 3;
|
||||
let mut out = Matrix::zeros(n);
|
||||
for a in 0..groups {
|
||||
for b in a..groups {
|
||||
// Rᵀ K_ab R, mirrored to (b, a): K is symmetric.
|
||||
let mut kr = [[0.0; 3]; 3];
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
kr[i][j] = (0..3).map(|m| k.get(3 * a + i, 3 * b + m) * r[m][j]).sum();
|
||||
}
|
||||
}
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
let v: f64 = (0..3).map(|m| r[m][i] * kr[m][j]).sum();
|
||||
out.add(3 * a + i, 3 * b + j, v);
|
||||
if a != b {
|
||||
out.add(3 * b + j, 3 * a + i, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::element::symmetrize(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// T v: a vector of global components in groups of three to local components.
|
||||
pub fn rotate_to_local(v: &[f64], r: &Axes) -> Vec<f64> {
|
||||
v.as_chunks::<3>().0.iter().flat_map(|g| to_local(r, *g)).collect()
|
||||
}
|
||||
|
||||
/// Tᵀ v: local components in groups of three to global ones.
|
||||
pub fn rotate_to_global(v: &[f64], r: &Axes) -> Vec<f64> {
|
||||
v.as_chunks::<3>().0.iter().flat_map(|g| to_global(r, *g)).collect()
|
||||
}
|
||||
|
||||
/// A uniform translation `b` of `nodes` nodes with six degrees of freedom each (rotations
|
||||
/// zero): the nodal field whose product with a consistent mass gives body-force loads.
|
||||
pub fn translation(b: Vec3, nodes: usize) -> Vec<f64> {
|
||||
(0..nodes).flat_map(|_| [b[0], b[1], b[2], 0.0, 0.0, 0.0]).collect()
|
||||
}
|
||||
|
||||
/// A symmetric tensor in local axes, [[xx, xy, xz], [xy, yy, yz], [xz, yz, zz]], in global
|
||||
/// components as the 6-vector xx, yy, zz, xy, xz, yz: Rᵀ S R.
|
||||
pub fn tensor_to_global(r: &Axes, s: [[f64; 3]; 3]) -> [f64; 6] {
|
||||
let mut g = [[0.0; 3]; 3];
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
g[i][j] = (0..3).map(|a| (0..3).map(|b| r[a][i] * s[a][b] * r[b][j]).sum::<f64>()).sum();
|
||||
}
|
||||
}
|
||||
[g[0][0], g[1][1], g[2][2], g[0][1], g[0][2], g[1][2]]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rotations_round_trip() {
|
||||
let e1 = unit([1.0, 1.0, 0.0]).unwrap();
|
||||
let e3 = [0.0, 0.0, 1.0];
|
||||
let r: Axes = [e1, cross(e3, e1), e3];
|
||||
let v = [0.3, -1.2, 2.0];
|
||||
let back = to_global(&r, to_local(&r, v));
|
||||
assert!(back.iter().zip(v).all(|(a, b)| (a - b).abs() < 1e-15));
|
||||
// A uniaxial stress along e1 is xx = yy = xy = σ/2 globally.
|
||||
let s = tensor_to_global(&r, [[2.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]);
|
||||
assert!((s[0] - 1.0).abs() < 1e-15 && (s[1] - 1.0).abs() < 1e-15 && (s[3] - 1.0).abs() < 1e-15 && s[2] == 0.0, "{s:?}");
|
||||
// Rotating a matrix keeps its quadratic form: (T v)ᵀ K (T v) = vᵀ (Tᵀ K T) v.
|
||||
let mut k = Matrix::zeros(6);
|
||||
for i in 0..6 {
|
||||
for j in 0..6 {
|
||||
k.data[i * 6 + j] = 1.0 / (1.0 + i as f64 + j as f64);
|
||||
}
|
||||
}
|
||||
let g = rotate_matrix(&k, &r);
|
||||
let x = [0.1, 0.2, -0.3, 0.7, 0.0, 1.0];
|
||||
let xl = rotate_to_local(&x, &r);
|
||||
let ql: f64 = (0..6).map(|i| (0..6).map(|j| xl[i] * k.get(i, j) * xl[j]).sum::<f64>()).sum();
|
||||
let qg: f64 = (0..6).map(|i| (0..6).map(|j| x[i] * g.get(i, j) * x[j]).sum::<f64>()).sum();
|
||||
assert!((ql - qg).abs() < 1e-14, "{ql} {qg}");
|
||||
assert!(unit([0.0; 3]).is_none() && unit([f64::NAN, 0.0, 0.0]).is_none());
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@
|
||||
//! and lumped mass, body forces, integration-point stresses and their extrapolation to nodes;
|
||||
//! - [`plane`]: 2D elements (plane stress, plane strain, axisymmetric) on tri3, tri6, quad4 and
|
||||
//! quad8, with two displacement components per node;
|
||||
//! - [`shell`]: flat MITC4 quadrilateral and MITC3 triangle shells with a drilling rotation, six
|
||||
//! degrees of freedom per node;
|
||||
//! - [`beam`]: two-node Timoshenko beams with profile section properties;
|
||||
//! - [`frame`]: local axes of shells and beams and the rotation of their matrices;
|
||||
//! - [`face`]: consistent nodal forces of pressure on element faces;
|
||||
//! - [`uniform`]: uniform-strain (one-point) hexahedron and tetrahedron kinematics, hourglass
|
||||
//! shape vectors and element frequency bounds for explicit dynamics;
|
||||
@@ -29,7 +33,7 @@
|
||||
//! - [`dense`]: the small dense linear algebra inside an element.
|
||||
//!
|
||||
//! Conventions: element matrices are ordered node by node, each node's degrees of freedom in
|
||||
//! [`DofKind`] order (three translations for solids; docs/fem.md); stress
|
||||
//! [`DofKind`] order (three translations for solids, six for shells and beams; docs/fem.md); stress
|
||||
//! and strain 6-vectors in the order xx, yy, zz, xy, xz, yz with engineering shear strains.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -37,10 +41,12 @@
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
pub mod basis;
|
||||
pub mod beam;
|
||||
pub mod dense;
|
||||
pub mod dof;
|
||||
pub mod element;
|
||||
pub mod face;
|
||||
pub mod frame;
|
||||
pub mod jet;
|
||||
pub mod material;
|
||||
pub mod plane;
|
||||
@@ -48,6 +54,7 @@ pub mod plasticity;
|
||||
pub mod quadrature;
|
||||
pub mod rotation;
|
||||
pub mod shape;
|
||||
pub mod shell;
|
||||
pub mod truss;
|
||||
pub mod uniform;
|
||||
|
||||
|
||||
@@ -18,11 +18,27 @@ use crate::{Error, Result};
|
||||
/// A 6 × 6 elasticity matrix, row-major.
|
||||
pub type Elasticity = [[f64; 6]; 6];
|
||||
|
||||
/// The isotropic elasticity matrix.
|
||||
pub fn isotropic(young: f64, poisson: f64) -> Result<Elasticity> {
|
||||
/// Check isotropic constants: E > 0 and −1 < ν < 0.5.
|
||||
fn check_isotropic(young: f64, poisson: f64) -> Result<()> {
|
||||
if !(young > 0.0 && young.is_finite() && poisson > -1.0 && poisson < 0.5) {
|
||||
return Err(Error::Invalid(format!("isotropic constants E = {young}, ν = {poisson} are not a stable material")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The checked (E, ν) of an isotropic material, for the structural sections (shells, beams,
|
||||
/// rods) that need one; `what` names the section kind in the error for other materials.
|
||||
pub fn isotropic_constants(elastic: &Elastic, what: &str) -> Result<(f64, f64)> {
|
||||
let Elastic::Isotropic { young, poisson } = *elastic else {
|
||||
return Err(Error::NotSupported(format!("orthotropic {what} sections (they need material orientations)")));
|
||||
};
|
||||
check_isotropic(young, poisson)?;
|
||||
Ok((young, poisson))
|
||||
}
|
||||
|
||||
/// The isotropic elasticity matrix.
|
||||
pub fn isotropic(young: f64, poisson: f64) -> Result<Elasticity> {
|
||||
check_isotropic(young, poisson)?;
|
||||
let lambda = young * poisson / ((1.0 + poisson) * (1.0 - 2.0 * poisson));
|
||||
let mu = young / (2.0 * (1.0 + poisson));
|
||||
let mut d = [[0.0; 6]; 6];
|
||||
|
||||
@@ -0,0 +1,593 @@
|
||||
//! Flat shell elements: the four-node MITC4 quadrilateral and the three-node MITC3 triangle,
|
||||
//! with six degrees of freedom per node (three translations, three rotations).
|
||||
//!
|
||||
//! Each element is flat: its nodes are projected onto the element's mean plane, where the
|
||||
//! membrane, the Reissner–Mindlin plate and a drilling stiffness are superposed in local axes and
|
||||
//! the result rotated to the global ones (the facet approach; Zienkiewicz, Taylor & Fox 2014,
|
||||
//! *The Finite Element Method for Solid and Structural Mechanics*, 7th ed., ch. 9–10). Curved
|
||||
//! shells are approximated by facets; quadrilaterals on a cylinder whose edges follow the
|
||||
//! generators and the circumference are exactly flat.
|
||||
//!
|
||||
//! - **Kinematics.** With the reference surface at the nodes and z along the element normal,
|
||||
//! u = u₀ + z θ_y, v = v₀ − z θ_x, w = w₀: membrane strains ε = (u₀,x, v₀,y, u₀,y + v₀,x),
|
||||
//! curvatures κ = (θ_y,x, −θ_x,y, θ_y,y − θ_x,x) and transverse shear strains
|
||||
//! γ = (w,x + θ_y, w,y − θ_x).
|
||||
//! - **Section.** Linear elastic and homogeneous through the thickness, so the integration
|
||||
//! through the thickness is done exactly in closed form (any number of section points gives the
|
||||
//! same stiffness): with the mid-surface at z = e from the reference surface (e = −offset · t),
|
||||
//! N = A ε + B κ, M = B ε + D κ with A = Q t, B = Q t e, D = Q (t³/12 + t e²) for the plane
|
||||
//! stress matrix Q, and Q_s = k G t γ with the shear correction k = 5/6.
|
||||
//! - **Transverse shear, quadrilateral (MITC4).** The covariant transverse shear strains are
|
||||
//! sampled at the mid-sides and interpolated linearly across the element: ẽ_rt from the points
|
||||
//! (0, ±1), ẽ_st from (±1, 0), then turned into Cartesian strains with the Jacobian
|
||||
//! (E. N. Dvorkin, K.-J. Bathe, *A continuum mechanics based four-node shell element for
|
||||
//! general nonlinear analysis*, Engineering Computations 1(1):77–88, 1984; in the flat-plate
|
||||
//! form of K.-J. Bathe, E. N. Dvorkin, *A four-node plate bending element based on
|
||||
//! Mindlin/Reissner plate theory and a mixed interpolation*, IJNME 21(2):367–383, 1985). This
|
||||
//! removes shear locking while passing the patch tests; membrane and bending use the bilinear
|
||||
//! fields, all integrated with 2 × 2 Gauss points.
|
||||
//! - **Transverse shear, triangle (MITC3).** The covariant shear strains are tied at the three
|
||||
//! mid-sides: ẽ_rt = e_rt⁽¹⁾ + c s, ẽ_st = e_st⁽²⁾ − c r with c = e_rt⁽³⁾ − e_rt⁽¹⁾ − e_st⁽³⁾ +
|
||||
//! e_st⁽²⁾, tying points (½, 0), (0, ½) and (½, ½), which keeps the tangential shear strain of
|
||||
//! each edge constant along it (P.-S. Lee, K.-J. Bathe, *Development of MITC isotropic
|
||||
//! triangular shell finite elements*, Computers & Structures 82(11–13):945–962, 2004). The
|
||||
//! membrane is the constant-strain triangle.
|
||||
//! - **Drilling rotation.** The rotation about the normal has no stiffness in shell theory; it
|
||||
//! is tied to the membrane's in-plane rotation ω = ½ (v,x − u,y) by the penalty
|
||||
//! ∫ γ t (θ_z − ω)² dA of the variational formulation of T. J. R. Hughes, F. Brezzi, *On drilling
|
||||
//! degrees of freedom*, CMAME 72(1):105–121, 1989, with γ = G/100. Hughes and Brezzi take
|
||||
//! γ = G with a drilling-enhanced (Allman-type) membrane; on the plain bilinear membrane the
|
||||
//! in-plane rotation of a parallelogram is linear, which θ_z interpolates exactly, so the penalty
|
||||
//! does not lock, and the smaller γ keeps distorted elements free of it too. Being a function of
|
||||
//! θ_z − ω it leaves rigid motions free. Scaled with G t like the membrane (not with the bending
|
||||
//! rigidity), it ties each node's rotation about the surface normal to the membrane as the mesh
|
||||
//! is refined, so curved shells modelled with facets converge steadily: the Scordelis–Lo and
|
||||
//! pinched-cylinder benchmarks change by less than 0.5 % for γ from G/1000 to G, whereas a
|
||||
//! penalty that is a fixed fraction of the bending stiffness lets that rotation go soft on fine
|
||||
//! meshes.
|
||||
//! - **Mass.** Consistent: ρ t on the translations, ρ (t³/12 + t e²) on the rotations and
|
||||
//! ρ t e coupling translations and bending rotations when the reference surface is offset. The
|
||||
//! drilling rotation gets the same rotary inertia as the bending ones (shell theory gives it
|
||||
//! none; it is tiny next to the in-plane inertia of the translations and keeps the mass matrix
|
||||
//! positive definite, as the frequency solver's Lanczos iteration needs). Lumped: the diagonal
|
||||
//! scaled to the element's mass (Hinton, Rock & Zienkiewicz 1976), the rotary inertia
|
||||
//! isotropic about the node so the lumped matrix stays diagonal in global axes.
|
||||
//! - **Stresses.** Section forces and the stresses at the bottom, middle and top of the section
|
||||
//! at the integration points, extrapolated bilinearly to the nodes (quadrilateral) or taken at
|
||||
//! the nodes (triangle, constant membrane and bending fields). Transverse shear stresses are
|
||||
//! reported at the mid-surface as the peak of the parabolic distribution, 3/2 Q/t.
|
||||
|
||||
use modelcraft_model::{Elastic, ElementKind};
|
||||
|
||||
use crate::dense::Matrix;
|
||||
use crate::dof::{DofSet, ElementDofs};
|
||||
use crate::frame::{Axes, cross, dot, norm, rotate_matrix, rotate_to_global, rotate_to_local, sub, tensor_to_global, to_local, translation, unit};
|
||||
use crate::quadrature::Rule;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Transverse shear correction factor of a homogeneous section.
|
||||
pub const SHEAR_CORRECTION: f64 = 5.0 / 6.0;
|
||||
|
||||
/// The drilling penalty γ as a fraction of the shear modulus.
|
||||
pub const DRILLING: f64 = 1e-2;
|
||||
|
||||
/// Degrees of freedom per shell node: translations along local (or global) x, y, z, then
|
||||
/// rotations about them.
|
||||
pub const NODE_DOFS: usize = 6;
|
||||
|
||||
/// A shell section with an isotropic material.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ShellSection {
|
||||
pub thickness: f64,
|
||||
/// The mid-surface's position along the element normal from the nodes (the reference
|
||||
/// surface): −offset · thickness.
|
||||
pub eccentricity: f64,
|
||||
pub young: f64,
|
||||
pub poisson: f64,
|
||||
}
|
||||
|
||||
impl ShellSection {
|
||||
/// A section of the model's thickness and offset (a fraction of the thickness, 0.5 puts the
|
||||
/// nodes on the side the normal points to).
|
||||
pub fn new(thickness: f64, offset: f64, elastic: &Elastic) -> Result<ShellSection> {
|
||||
let (young, poisson) = crate::material::isotropic_constants(elastic, "shell")?;
|
||||
if !(thickness > 0.0 && thickness.is_finite() && offset.abs() <= 0.5) {
|
||||
return Err(Error::Invalid(format!(
|
||||
"a shell section needs a positive thickness and an offset in −0.5..=0.5, got {thickness} and {offset}"
|
||||
)));
|
||||
}
|
||||
Ok(ShellSection { thickness, eccentricity: -offset * thickness, young, poisson })
|
||||
}
|
||||
|
||||
/// The plane stress matrix Q.
|
||||
fn plane_stress(&self) -> [[f64; 3]; 3] {
|
||||
let (e, nu) = (self.young, self.poisson);
|
||||
let c = e / (1.0 - nu * nu);
|
||||
[[c, c * nu, 0.0], [c * nu, c, 0.0], [0.0, 0.0, c * (1.0 - nu) / 2.0]]
|
||||
}
|
||||
|
||||
/// The membrane–bending stiffness [[A, B], [B, D]], 6 × 6.
|
||||
fn abd(&self) -> [[f64; 6]; 6] {
|
||||
let q = self.plane_stress();
|
||||
let (t, e) = (self.thickness, self.eccentricity);
|
||||
let (a, b, d) = (t, t * e, t * t * t / 12.0 + t * e * e);
|
||||
let mut out = [[0.0; 6]; 6];
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
out[i][j] = a * q[i][j];
|
||||
out[i][j + 3] = b * q[i][j];
|
||||
out[i + 3][j] = b * q[i][j];
|
||||
out[i + 3][j + 3] = d * q[i][j];
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn shear_modulus(&self) -> f64 {
|
||||
self.young / (2.0 * (1.0 + self.poisson))
|
||||
}
|
||||
|
||||
/// The transverse shear stiffness k G t.
|
||||
fn shear(&self) -> f64 {
|
||||
SHEAR_CORRECTION * self.shear_modulus() * self.thickness
|
||||
}
|
||||
}
|
||||
|
||||
/// Section forces and stresses of a shell element, per node in the element's node order.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ShellStress {
|
||||
/// The element's local axes: e₁, e₂ in its plane, e₃ its normal (the right-hand normal of
|
||||
/// the node order).
|
||||
pub axes: Axes,
|
||||
/// N_x, N_y, N_xy, M_x, M_y, M_xy, Q_x, Q_y in the local axes, per unit length, about the
|
||||
/// reference surface.
|
||||
pub forces: Vec<[f64; 8]>,
|
||||
/// Stress (global xx, yy, zz, xy, xz, yz) at the bottom (−e₃ side), the middle and the top
|
||||
/// (+e₃ side) of the section.
|
||||
pub bottom: Vec<[f64; 6]>,
|
||||
pub middle: Vec<[f64; 6]>,
|
||||
pub top: Vec<[f64; 6]>,
|
||||
}
|
||||
|
||||
/// The shape functions of the element's corners at (r, s) and their derivatives along r and s.
|
||||
fn shape(kind: ElementKind, r: f64, s: f64) -> ([f64; 4], [[f64; 2]; 4]) {
|
||||
if kind == ElementKind::Tri3 {
|
||||
([1.0 - r - s, r, s, 0.0], [[-1.0, -1.0], [1.0, 0.0], [0.0, 1.0], [0.0, 0.0]])
|
||||
} else {
|
||||
let c = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
|
||||
let mut n = [0.0; 4];
|
||||
let mut d = [[0.0; 2]; 4];
|
||||
for (i, ci) in c.iter().enumerate() {
|
||||
n[i] = 0.25 * (1.0 + ci[0] * r) * (1.0 + ci[1] * s);
|
||||
d[i] = [0.25 * ci[0] * (1.0 + ci[1] * s), 0.25 * ci[1] * (1.0 + ci[0] * r)];
|
||||
}
|
||||
(n, d)
|
||||
}
|
||||
}
|
||||
|
||||
/// The map's Jacobian at a point: J = [[x,r, y,r], [x,s, y,s]], its inverse and determinant.
|
||||
struct Map {
|
||||
n: [f64; 4],
|
||||
/// ∂Nᵢ/∂r, ∂Nᵢ/∂s.
|
||||
dn: [[f64; 2]; 4],
|
||||
/// ∂Nᵢ/∂x, ∂Nᵢ/∂y.
|
||||
dx: [[f64; 2]; 4],
|
||||
j: [[f64; 2]; 2],
|
||||
inv: [[f64; 2]; 2],
|
||||
det: f64,
|
||||
}
|
||||
|
||||
/// Most degrees of freedom of a shell element (quad4).
|
||||
const MAX_DOFS: usize = 4 * NODE_DOFS;
|
||||
|
||||
/// A row of a strain operator over the element's local degrees of freedom (zero past them).
|
||||
type Row = [f64; MAX_DOFS];
|
||||
|
||||
/// The covariant shear rows at the element's tying points, which depend on its geometry only:
|
||||
/// quad4 e_rt at (0, 1), (0, −1) and e_st at (1, 0), (−1, 0); tri3 e_rt⁽¹⁾, e_st⁽²⁾ and the
|
||||
/// combination c.
|
||||
struct Tying([Row; 4]);
|
||||
|
||||
/// A flat shell element.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Shell {
|
||||
kind: ElementKind,
|
||||
nodes: usize,
|
||||
axes: Axes,
|
||||
/// The nodes in the element plane (local x, y).
|
||||
local: [[f64; 2]; 4],
|
||||
section: ShellSection,
|
||||
area: f64,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
pub fn new(kind: ElementKind, xyz: &[[f64; 3]], section: ShellSection) -> Result<Shell> {
|
||||
let nodes = match kind {
|
||||
ElementKind::Tri3 => 3,
|
||||
ElementKind::Quad4 => 4,
|
||||
ElementKind::Tri6 | ElementKind::Quad8 => {
|
||||
return Err(Error::NotSupported(format!("{} shells (the solver has quad4 and tri3 shells so far)", kind.name())));
|
||||
}
|
||||
_ => return Err(Error::Invalid(format!("{} is not a shell element", kind.name()))),
|
||||
};
|
||||
if xyz.len() != nodes {
|
||||
return Err(Error::Invalid(format!("a {} element has {nodes} nodes, got {}", kind.name(), xyz.len())));
|
||||
}
|
||||
modelcraft_model::check_coordinates(xyz.as_flattened()).map_err(|e| Error::Invalid(e.to_string()))?;
|
||||
let degenerate = || Error::Degenerate(format!("a {} shell element is degenerate (zero area, inverted or not convex)", kind.name()));
|
||||
let p = |i: usize| xyz.get(i).copied().unwrap_or_default();
|
||||
let (normal, along) = if nodes == 3 {
|
||||
(cross(sub(p(1), p(0)), sub(p(2), p(0))), sub(p(1), p(0)))
|
||||
} else {
|
||||
let along = sub([0, 1, 2].map(|k| p(1)[k] + p(2)[k]), [0, 1, 2].map(|k| p(0)[k] + p(3)[k]));
|
||||
(cross(sub(p(2), p(0)), sub(p(3), p(1))), along)
|
||||
};
|
||||
let e3 = unit(normal).ok_or_else(degenerate)?;
|
||||
let e1 = unit(sub(along, e3.map(|c| c * dot(along, e3)))).ok_or_else(degenerate)?;
|
||||
let e2 = cross(e3, e1);
|
||||
let axes = [e1, e2, e3];
|
||||
let mut centre = [0.0; 3];
|
||||
for x in xyz {
|
||||
for k in 0..3 {
|
||||
centre[k] += x[k] / nodes as f64;
|
||||
}
|
||||
}
|
||||
let mut local = [[0.0; 2]; 4];
|
||||
let mut size = 0.0f64;
|
||||
for (l, x) in local.iter_mut().zip(xyz) {
|
||||
let d = sub(*x, centre);
|
||||
*l = [dot(d, e1), dot(d, e2)];
|
||||
size = size.max(norm(d));
|
||||
}
|
||||
// Corners turn left (counter-clockwise about e₃) by a margin: positive area, convex.
|
||||
for i in 0..nodes {
|
||||
let (a, b, c) = (local[(i + nodes - 1) % nodes], local[i], local[(i + 1) % nodes]);
|
||||
let turn = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]);
|
||||
if !(turn > 1e-10 * size * size) {
|
||||
return Err(degenerate());
|
||||
}
|
||||
}
|
||||
// A strongly warped quadrilateral is not a facet.
|
||||
let warp = xyz.iter().map(|x| dot(sub(*x, centre), e3).abs()).fold(0.0, f64::max);
|
||||
if warp > 0.1 * size {
|
||||
return Err(Error::Degenerate(format!(
|
||||
"a quad4 shell element is warped by {warp:.3e} out of its plane (more than a tenth of its size); split it into triangles"
|
||||
)));
|
||||
}
|
||||
let mut shell = Shell { kind, nodes, axes, local, section, area: 0.0 };
|
||||
shell.area = shell.rule().points().iter().map(|q| shell.map(q.xi[0], q.xi[1]).map_or(0.0, |m| m.det * q.weight)).sum();
|
||||
if !(shell.area > 0.0) {
|
||||
return Err(degenerate());
|
||||
}
|
||||
Ok(shell)
|
||||
}
|
||||
|
||||
pub fn axes(&self) -> Axes {
|
||||
self.axes
|
||||
}
|
||||
|
||||
pub fn area(&self) -> f64 {
|
||||
self.area
|
||||
}
|
||||
|
||||
/// The element's unknowns: three translations and three rotations per node.
|
||||
pub fn dofs(&self) -> ElementDofs {
|
||||
ElementDofs::new(DofSet::STRUCTURAL)
|
||||
}
|
||||
|
||||
/// The size of the element's matrices.
|
||||
fn size(&self) -> usize {
|
||||
NODE_DOFS * self.nodes
|
||||
}
|
||||
|
||||
fn rule(&self) -> Rule {
|
||||
if self.kind == ElementKind::Tri3 { Rule::TriConical(2) } else { Rule::Quad(2) }
|
||||
}
|
||||
|
||||
fn map(&self, r: f64, s: f64) -> Result<Map> {
|
||||
let (n, d) = shape(self.kind, r, s);
|
||||
let mut j = [[0.0; 2]; 2];
|
||||
for (di, x) in d.iter().zip(&self.local).take(self.nodes) {
|
||||
for a in 0..2 {
|
||||
for b in 0..2 {
|
||||
j[a][b] += di[a] * x[b];
|
||||
}
|
||||
}
|
||||
}
|
||||
let det = j[0][0] * j[1][1] - j[0][1] * j[1][0];
|
||||
if !(det > 0.0 && det.is_finite()) {
|
||||
return Err(Error::Degenerate(format!("a {} shell element is inverted or degenerate", self.kind.name())));
|
||||
}
|
||||
let inv = [[j[1][1] / det, -j[0][1] / det], [-j[1][0] / det, j[0][0] / det]];
|
||||
let mut dx = [[0.0; 2]; 4];
|
||||
for (g, di) in dx.iter_mut().zip(&d) {
|
||||
g[0] = inv[0][0] * di[0] + inv[0][1] * di[1];
|
||||
g[1] = inv[1][0] * di[0] + inv[1][1] * di[1];
|
||||
}
|
||||
Ok(Map { n, dn: d, dx, j, inv, det })
|
||||
}
|
||||
|
||||
/// The covariant transverse shear strain e_rt (`along` 0) or e_st (`along` 1) of the
|
||||
/// displacement interpolation at (r, s): w,r + θ_y x,r − θ_x y,r (and with s).
|
||||
fn covariant(&self, r: f64, s: f64, along: usize) -> Result<Row> {
|
||||
let m = self.map(r, s)?;
|
||||
let (xr, yr) = (m.j[along][0], m.j[along][1]);
|
||||
let mut row = [0.0; MAX_DOFS];
|
||||
for i in 0..self.nodes {
|
||||
row[6 * i + 2] = m.dn[i][along];
|
||||
row[6 * i + 4] = m.n[i] * xr;
|
||||
row[6 * i + 3] = -m.n[i] * yr;
|
||||
}
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
fn tying(&self) -> Result<Tying> {
|
||||
if self.kind == ElementKind::Tri3 {
|
||||
let e1 = self.covariant(0.5, 0.0, 0)?;
|
||||
let e2 = self.covariant(0.0, 0.5, 1)?;
|
||||
let e3r = self.covariant(0.5, 0.5, 0)?;
|
||||
let e3s = self.covariant(0.5, 0.5, 1)?;
|
||||
let c: Row = std::array::from_fn(|k| e3r[k] - e1[k] - e3s[k] + e2[k]);
|
||||
Ok(Tying([e1, e2, c, [0.0; MAX_DOFS]]))
|
||||
} else {
|
||||
Ok(Tying([self.covariant(0.0, 1.0, 0)?, self.covariant(0.0, -1.0, 0)?, self.covariant(1.0, 0.0, 1)?, self.covariant(-1.0, 0.0, 1)?]))
|
||||
}
|
||||
}
|
||||
|
||||
/// The assumed covariant shear strains (ẽ_rt, ẽ_st) at (r, s).
|
||||
fn assumed_shear(&self, tying: &Tying, r: f64, s: f64) -> [Row; 2] {
|
||||
let lerp = |a: &Row, wa: f64, b: &Row, wb: f64| -> Row { std::array::from_fn(|k| wa * a[k] + wb * b[k]) };
|
||||
let [t0, t1, t2, t3] = &tying.0;
|
||||
if self.kind == ElementKind::Tri3 {
|
||||
[lerp(t0, 1.0, t2, s), lerp(t1, 1.0, t2, -r)]
|
||||
} else {
|
||||
[lerp(t0, 0.5 * (1.0 + s), t1, 0.5 * (1.0 - s)), lerp(t2, 0.5 * (1.0 + r), t3, 0.5 * (1.0 - r))]
|
||||
}
|
||||
}
|
||||
|
||||
/// The generalized strain operator at (r, s): membrane strains, curvatures, transverse shear
|
||||
/// strains (8 rows) and the drilling row θ_z − ω, with the map.
|
||||
fn operator(&self, tying: &Tying, r: f64, s: f64) -> Result<([Row; 9], Map)> {
|
||||
let m = self.map(r, s)?;
|
||||
let mut b = [[0.0; MAX_DOFS]; 9];
|
||||
for i in 0..self.nodes {
|
||||
let [gx, gy] = m.dx[i];
|
||||
let (u, v, tx, ty, tz) = (6 * i, 6 * i + 1, 6 * i + 3, 6 * i + 4, 6 * i + 5);
|
||||
b[0][u] = gx;
|
||||
b[1][v] = gy;
|
||||
b[2][u] = gy;
|
||||
b[2][v] = gx;
|
||||
b[3][ty] = gx;
|
||||
b[4][tx] = -gy;
|
||||
b[5][ty] = gy;
|
||||
b[5][tx] = -gx;
|
||||
b[8][tz] = m.n[i];
|
||||
b[8][u] = 0.5 * gy;
|
||||
b[8][v] = -0.5 * gx;
|
||||
}
|
||||
let [ert, est] = self.assumed_shear(tying, r, s);
|
||||
for k in 0..MAX_DOFS {
|
||||
b[6][k] = m.inv[0][0] * ert[k] + m.inv[0][1] * est[k];
|
||||
b[7][k] = m.inv[1][0] * ert[k] + m.inv[1][1] * est[k];
|
||||
}
|
||||
Ok((b, m))
|
||||
}
|
||||
|
||||
/// The stiffness matrix in global axes, 6n × 6n, degrees of freedom node by node.
|
||||
pub fn stiffness(&self) -> Result<Matrix> {
|
||||
let n = self.size();
|
||||
let abd = self.section.abd();
|
||||
let ks = self.section.shear();
|
||||
let drill = DRILLING * self.section.shear_modulus() * self.section.thickness;
|
||||
let tying = self.tying()?;
|
||||
let mut k = Matrix::zeros(n);
|
||||
for p in self.rule().points() {
|
||||
let (b, m) = self.operator(&tying, p.xi[0], p.xi[1])?;
|
||||
let w = m.det * p.weight;
|
||||
// C B for the membrane–bending rows.
|
||||
let cb: [Row; 6] = std::array::from_fn(|a| std::array::from_fn(|c| (0..6).map(|d| abd[a][d] * b[d][c]).sum()));
|
||||
for i in 0..n {
|
||||
for j in i..n {
|
||||
let mut v: f64 = (0..6).map(|a| b[a][i] * cb[a][j]).sum();
|
||||
v += ks * (b[6][i] * b[6][j] + b[7][i] * b[7][j]);
|
||||
v += drill * b[8][i] * b[8][j];
|
||||
if v != 0.0 {
|
||||
k.add(i, j, w * v);
|
||||
if i != j {
|
||||
k.add(j, i, w * v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(rotate_matrix(&k, &self.axes))
|
||||
}
|
||||
|
||||
/// The consistent mass matrix in global axes, 6n × 6n.
|
||||
pub fn mass(&self, density: f64) -> Result<Matrix> {
|
||||
Ok(rotate_matrix(&self.local_mass(density)?, &self.axes))
|
||||
}
|
||||
|
||||
/// The consistent mass matrix in the element's axes.
|
||||
fn local_mass(&self, density: f64) -> Result<Matrix> {
|
||||
let (t, e) = (self.section.thickness, self.section.eccentricity);
|
||||
let (m0, m1, m2) = (density * t, density * t * e, density * (t * t * t / 12.0 + t * e * e));
|
||||
let mut mm = Matrix::zeros(self.size());
|
||||
for p in self.rule().points() {
|
||||
let m = self.map(p.xi[0], p.xi[1])?;
|
||||
let w = m.det * p.weight;
|
||||
for i in 0..self.nodes {
|
||||
for j in 0..self.nodes {
|
||||
let nn = w * m.n[i] * m.n[j];
|
||||
for c in 0..3 {
|
||||
mm.add(6 * i + c, 6 * j + c, m0 * nn);
|
||||
}
|
||||
for c in 3..6 {
|
||||
mm.add(6 * i + c, 6 * j + c, m2 * nn);
|
||||
}
|
||||
if m1 != 0.0 {
|
||||
// u θ_y and −v θ_x, both ways.
|
||||
mm.add(6 * i, 6 * j + 4, m1 * nn);
|
||||
mm.add(6 * i + 4, 6 * j, m1 * nn);
|
||||
mm.add(6 * i + 1, 6 * j + 3, -m1 * nn);
|
||||
mm.add(6 * i + 3, 6 * j + 1, -m1 * nn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(mm)
|
||||
}
|
||||
|
||||
/// The lumped mass per degree of freedom (the same in local and global axes).
|
||||
pub fn lumped_mass(&self, density: f64) -> Result<Vec<f64>> {
|
||||
let (t, e) = (self.section.thickness, self.section.eccentricity);
|
||||
let mut diag = vec![0.0; self.size()];
|
||||
let mut sum = 0.0;
|
||||
for p in self.rule().points() {
|
||||
let m = self.map(p.xi[0], p.xi[1])?;
|
||||
for i in 0..self.nodes {
|
||||
let v = m.det * p.weight * m.n[i] * m.n[i];
|
||||
diag[6 * i] += v;
|
||||
sum += v;
|
||||
}
|
||||
}
|
||||
if !(sum > 0.0) {
|
||||
return Err(Error::Degenerate("a shell element has no area".into()));
|
||||
}
|
||||
let scale = self.area / sum;
|
||||
let (m0, m2) = (density * t, density * (t * t * t / 12.0 + t * e * e));
|
||||
for i in 0..self.nodes {
|
||||
let a = diag[6 * i] * scale;
|
||||
for c in 0..3 {
|
||||
diag[6 * i + c] = m0 * a;
|
||||
diag[6 * i + 3 + c] = m2 * a;
|
||||
}
|
||||
}
|
||||
Ok(diag)
|
||||
}
|
||||
|
||||
/// Consistent nodal forces and moments (global) of a uniform force per volume b: the
|
||||
/// consistent mass of unit density times the uniform nodal translation b.
|
||||
pub fn body_force(&self, b: [f64; 3]) -> Result<Vec<f64>> {
|
||||
let r = translation(to_local(&self.axes, b), self.nodes);
|
||||
Ok(rotate_to_global(&self.local_mass(1.0)?.mul_vec(&r), &self.axes))
|
||||
}
|
||||
|
||||
/// Section forces and surface stresses for the element's nodal displacements and rotations
|
||||
/// (global, 6 per node).
|
||||
pub fn stresses(&self, u: &[f64]) -> Result<ShellStress> {
|
||||
if u.len() != self.size() {
|
||||
return Err(Error::Invalid(format!("{} values for a shell element with {} degrees of freedom", u.len(), self.size())));
|
||||
}
|
||||
let ul = rotate_to_local(u, &self.axes);
|
||||
let tying = self.tying()?;
|
||||
let strains = |r: f64, s: f64| -> Result<[f64; 8]> {
|
||||
let (b, _) = self.operator(&tying, r, s)?;
|
||||
Ok(std::array::from_fn(|a| b[a].iter().zip(&ul).map(|(x, y)| x * y).sum()))
|
||||
};
|
||||
let nodal: Vec<[f64; 8]> = if self.kind == ElementKind::Tri3 {
|
||||
[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]].iter().map(|c| strains(c[0], c[1])).collect::<Result<_>>()?
|
||||
} else {
|
||||
// 2 × 2 Gauss values, extrapolated with the bilinear functions of the points.
|
||||
let g = 1.0 / 3f64.sqrt();
|
||||
let corners = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
|
||||
let at: Vec<[f64; 8]> = corners.iter().map(|c| strains(c[0] * g, c[1] * g)).collect::<Result<_>>()?;
|
||||
corners
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let (n, _) = shape(ElementKind::Quad4, c[0] / g, c[1] / g);
|
||||
std::array::from_fn(|k| n.iter().zip(&at).map(|(w, v)| w * v[k]).sum())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let abd = self.section.abd();
|
||||
let q = self.section.plane_stress();
|
||||
let ks = self.section.shear();
|
||||
let (t, e) = (self.section.thickness, self.section.eccentricity);
|
||||
let mut out = ShellStress { axes: self.axes, forces: Vec::new(), bottom: Vec::new(), middle: Vec::new(), top: Vec::new() };
|
||||
for g in &nodal {
|
||||
let mut f = [0.0; 8];
|
||||
for a in 0..6 {
|
||||
f[a] = (0..6).map(|b| abd[a][b] * g[b]).sum();
|
||||
}
|
||||
f[6] = ks * g[6];
|
||||
f[7] = ks * g[7];
|
||||
out.forces.push(f);
|
||||
let at = |z: f64, shear: f64| {
|
||||
let strain = [g[0] + z * g[3], g[1] + z * g[4], g[2] + z * g[5]];
|
||||
let s: [f64; 3] = std::array::from_fn(|a| (0..3).map(|b| q[a][b] * strain[b]).sum());
|
||||
let (txz, tyz) = (shear * f[6] / t, shear * f[7] / t);
|
||||
tensor_to_global(&self.axes, [[s[0], s[2], txz], [s[2], s[1], tyz], [txz, tyz, 0.0]])
|
||||
};
|
||||
out.bottom.push(at(e - t / 2.0, 0.0));
|
||||
out.middle.push(at(e, 1.5));
|
||||
out.top.push(at(e + t / 2.0, 0.0));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const STEEL: Elastic = Elastic::Isotropic { young: 210e3, poisson: 0.3 };
|
||||
|
||||
fn rigid(xyz: &[[f64; 3]], m: usize) -> Vec<f64> {
|
||||
xyz.iter()
|
||||
.flat_map(|x| {
|
||||
let mut d = [0.0; 6];
|
||||
if m < 3 {
|
||||
d[m] = 1.0;
|
||||
} else {
|
||||
let mut w = [0.0; 3];
|
||||
w[m - 3] = 1.0;
|
||||
d[..3].copy_from_slice(&cross(w, *x));
|
||||
d[3..].copy_from_slice(&w);
|
||||
}
|
||||
d
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shells_are_free_of_rigid_motion_and_symmetric() {
|
||||
let section = ShellSection::new(0.5, 0.25, &STEEL).unwrap();
|
||||
// A skew quadrilateral and a triangle in a tilted plane.
|
||||
let tilt = |x: f64, y: f64| [x, 0.6 * y, 0.8 * y + 1.0];
|
||||
let quad = [tilt(0.0, 0.0), tilt(2.0, 0.2), tilt(2.3, 1.7), tilt(-0.2, 1.4)];
|
||||
let tri = [tilt(0.0, 0.0), tilt(2.0, 0.2), tilt(0.4, 1.5)];
|
||||
for (kind, xyz) in [(ElementKind::Quad4, &quad[..]), (ElementKind::Tri3, &tri[..])] {
|
||||
let shell = Shell::new(kind, xyz, section).unwrap();
|
||||
let k = shell.stiffness().unwrap();
|
||||
let scale = (0..k.n).map(|i| k.get(i, i)).fold(0.0, f64::max);
|
||||
for i in 0..k.n {
|
||||
for j in 0..k.n {
|
||||
assert_eq!(k.get(i, j), k.get(j, i));
|
||||
}
|
||||
}
|
||||
for m in 0..6 {
|
||||
let f = k.mul_vec(&rigid(xyz, m));
|
||||
assert!(f.iter().all(|v| v.abs() < 1e-10 * scale), "{kind:?} rigid {m}: {f:?}");
|
||||
}
|
||||
// Mass: translations sum to ρ t A per direction (the offset couples rotations only).
|
||||
let m = shell.mass(2.0).unwrap();
|
||||
let total = 2.0 * 0.5 * shell.area();
|
||||
let s: f64 =
|
||||
(0..k.n).filter(|i| i % 6 == 0).flat_map(|i| (0..k.n).filter(|j| j % 6 == 0).map(move |j| (i, j))).map(|(i, j)| m.get(i, j)).sum();
|
||||
assert!((s - total).abs() < 1e-12 * total, "{s} {total}");
|
||||
let lumped = shell.lumped_mass(2.0).unwrap();
|
||||
let s: f64 = lumped.iter().step_by(6).sum();
|
||||
assert!((s - total).abs() < 1e-12 * total);
|
||||
let body = shell.body_force([0.0, 0.0, -1.0]).unwrap();
|
||||
let fz: f64 = body.iter().skip(2).step_by(6).sum();
|
||||
assert!((fz + 0.5 * shell.area()).abs() < 1e-12, "{fz}");
|
||||
}
|
||||
let flat = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.5, 0.2, 0.0]];
|
||||
assert!(Shell::new(ElementKind::Quad4, &flat, section).is_err(), "not convex");
|
||||
let line = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
|
||||
assert!(Shell::new(ElementKind::Tri3, &line, section).is_err());
|
||||
assert!(Shell::new(ElementKind::Quad8, &flat, section).is_err());
|
||||
assert!(ShellSection::new(1.0, 0.0, &Elastic::EngineeringConstants { young: [1.0; 3], poisson: [0.0; 3], shear: [1.0; 3] }).is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user