Skip to main content

runmat_analysis_fea/assembly/elements/
shell.rs

1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4pub const SHELL_NODE_DOF_COUNT: usize = 6;
5pub const SHELL_ELEMENT_NODE_COUNT: usize = 3;
6pub const SHELL_ELEMENT_DOF_COUNT: usize = SHELL_NODE_DOF_COUNT * SHELL_ELEMENT_NODE_COUNT;
7
8pub type ShellMatrix18 = [[f64; SHELL_ELEMENT_DOF_COUNT]; SHELL_ELEMENT_DOF_COUNT];
9
10#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub struct ShellSection {
12    pub thickness_m: f64,
13    pub shear_correction: f64,
14    pub drilling_stiffness_scale: f64,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
18pub struct ShellMaterial {
19    pub youngs_modulus_pa: f64,
20    pub poisson_ratio: f64,
21    pub shear_modulus_pa: f64,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct ShellElementGeometry {
26    pub nodes_m: [[f64; 3]; SHELL_ELEMENT_NODE_COUNT],
27    pub reference_axis: [f64; 3],
28}
29
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct ShellLocalFrame {
32    pub x: [f64; 3],
33    pub y: [f64; 3],
34    pub z: [f64; 3],
35    pub area_m2: f64,
36}
37
38#[derive(Debug, Error, Clone, PartialEq)]
39pub enum ShellElementError {
40    #[error("shell element area must be positive and finite")]
41    DegenerateArea,
42    #[error("shell reference axis must be finite and non-parallel to the shell normal")]
43    DegenerateReferenceAxis,
44    #[error("shell thickness, shear correction, and drilling scale must be positive and finite")]
45    InvalidSection,
46    #[error("shell material properties must be positive and finite")]
47    InvalidMaterial,
48}
49
50impl ShellSection {
51    pub fn validate(self) -> Result<(), ShellElementError> {
52        if positive_finite(self.thickness_m)
53            && positive_finite(self.shear_correction)
54            && positive_finite(self.drilling_stiffness_scale)
55        {
56            Ok(())
57        } else {
58            Err(ShellElementError::InvalidSection)
59        }
60    }
61}
62
63impl ShellMaterial {
64    pub fn validate(self) -> Result<(), ShellElementError> {
65        if positive_finite(self.youngs_modulus_pa)
66            && positive_finite(self.shear_modulus_pa)
67            && self.poisson_ratio.is_finite()
68            && self.poisson_ratio > -1.0
69            && self.poisson_ratio < 0.5
70        {
71            Ok(())
72        } else {
73            Err(ShellElementError::InvalidMaterial)
74        }
75    }
76}
77
78impl ShellElementGeometry {
79    pub fn local_frame(self) -> Result<ShellLocalFrame, ShellElementError> {
80        let edge_a = sub(self.nodes_m[1], self.nodes_m[0]);
81        let edge_b = sub(self.nodes_m[2], self.nodes_m[0]);
82        let normal_area = cross(edge_a, edge_b);
83        let normal_norm = norm(normal_area);
84        if !positive_finite(normal_norm) {
85            return Err(ShellElementError::DegenerateArea);
86        }
87        let z = scale(normal_area, 1.0 / normal_norm);
88        if !self.reference_axis.iter().all(|value| value.is_finite()) {
89            return Err(ShellElementError::DegenerateReferenceAxis);
90        }
91        let reference_projection = sub(self.reference_axis, scale(z, dot(self.reference_axis, z)));
92        let projection_norm = norm(reference_projection);
93        let x = if projection_norm > 1.0e-12 && projection_norm.is_finite() {
94            scale(reference_projection, 1.0 / projection_norm)
95        } else {
96            let edge_norm = norm(edge_a);
97            if !positive_finite(edge_norm) {
98                return Err(ShellElementError::DegenerateReferenceAxis);
99            }
100            scale(edge_a, 1.0 / edge_norm)
101        };
102        let y = cross(z, x);
103        Ok(ShellLocalFrame {
104            x,
105            y,
106            z,
107            area_m2: 0.5 * normal_norm,
108        })
109    }
110}
111
112pub fn global_stiffness_matrix(
113    section: ShellSection,
114    material: ShellMaterial,
115    geometry: ShellElementGeometry,
116) -> Result<ShellMatrix18, ShellElementError> {
117    section.validate()?;
118    material.validate()?;
119    let frame = geometry.local_frame()?;
120    let mut k = [[0.0; SHELL_ELEMENT_DOF_COUNT]; SHELL_ELEMENT_DOF_COUNT];
121
122    let membrane = material.youngs_modulus_pa * section.thickness_m * frame.area_m2;
123    let bending = material.youngs_modulus_pa * section.thickness_m.powi(3) * frame.area_m2
124        / (12.0 * (1.0 - material.poisson_ratio.powi(2)).max(1.0e-9));
125    let shear =
126        material.shear_modulus_pa * section.thickness_m * section.shear_correction * frame.area_m2;
127    let drilling = membrane * section.drilling_stiffness_scale;
128
129    for (a, b) in [(0usize, 1usize), (1, 2), (2, 0)] {
130        let length = distance(geometry.nodes_m[a], geometry.nodes_m[b]).max(1.0e-12);
131        let membrane_k = membrane / length.powi(2);
132        let shear_k = shear / length.powi(2);
133        let bending_k = bending / length.powi(2);
134        let drilling_k = drilling / length.powi(2);
135        add_pair_spring(&mut k, a, b, 0, membrane_k);
136        add_pair_spring(&mut k, a, b, 1, membrane_k);
137        add_pair_spring(&mut k, a, b, 2, shear_k);
138        add_pair_spring(&mut k, a, b, 3, bending_k);
139        add_pair_spring(&mut k, a, b, 4, bending_k);
140        add_pair_spring(&mut k, a, b, 5, drilling_k);
141    }
142
143    let stabilization = (membrane + shear + bending + drilling).max(1.0) * 1.0e-12;
144    for (index, row) in k.iter_mut().enumerate() {
145        row[index] += stabilization;
146    }
147    Ok(k)
148}
149
150fn add_pair_spring(
151    matrix: &mut ShellMatrix18,
152    node_a: usize,
153    node_b: usize,
154    component: usize,
155    stiffness: f64,
156) {
157    let row = node_a * SHELL_NODE_DOF_COUNT + component;
158    let col = node_b * SHELL_NODE_DOF_COUNT + component;
159    matrix[row][row] += stiffness;
160    matrix[col][col] += stiffness;
161    matrix[row][col] -= stiffness;
162    matrix[col][row] -= stiffness;
163}
164
165fn positive_finite(value: f64) -> bool {
166    value.is_finite() && value > 0.0
167}
168
169fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
170    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
171}
172
173fn scale(a: [f64; 3], factor: f64) -> [f64; 3] {
174    [a[0] * factor, a[1] * factor, a[2] * factor]
175}
176
177fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
178    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
179}
180
181fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
182    [
183        a[1] * b[2] - a[2] * b[1],
184        a[2] * b[0] - a[0] * b[2],
185        a[0] * b[1] - a[1] * b[0],
186    ]
187}
188
189fn norm(a: [f64; 3]) -> f64 {
190    dot(a, a).sqrt()
191}
192
193fn distance(a: [f64; 3], b: [f64; 3]) -> f64 {
194    norm(sub(a, b))
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    fn section() -> ShellSection {
202        ShellSection {
203            thickness_m: 0.002,
204            shear_correction: 5.0 / 6.0,
205            drilling_stiffness_scale: 1.0e-4,
206        }
207    }
208
209    fn material() -> ShellMaterial {
210        ShellMaterial {
211            youngs_modulus_pa: 200.0e9,
212            poisson_ratio: 0.3,
213            shear_modulus_pa: 76.9e9,
214        }
215    }
216
217    fn geometry() -> ShellElementGeometry {
218        ShellElementGeometry {
219            nodes_m: [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
220            reference_axis: [1.0, 0.0, 0.0],
221        }
222    }
223
224    #[test]
225    fn shell_local_frame_is_orthonormal() {
226        let frame = geometry().local_frame().expect("frame should build");
227        assert_close(frame.area_m2, 0.5, 1.0e-12);
228        assert_close(dot(frame.x, frame.y), 0.0, 1.0e-12);
229        assert_close(dot(frame.x, frame.z), 0.0, 1.0e-12);
230        assert_close(dot(frame.y, frame.z), 0.0, 1.0e-12);
231        assert_close(norm(frame.x), 1.0, 1.0e-12);
232        assert_close(norm(frame.y), 1.0, 1.0e-12);
233        assert_close(norm(frame.z), 1.0, 1.0e-12);
234    }
235
236    #[test]
237    fn shell_global_stiffness_is_symmetric() {
238        let k = global_stiffness_matrix(section(), material(), geometry())
239            .expect("stiffness should build");
240        for row in 0..SHELL_ELEMENT_DOF_COUNT {
241            for col in 0..SHELL_ELEMENT_DOF_COUNT {
242                assert_close(k[row][col], k[col][row], 1.0e-6);
243            }
244        }
245    }
246
247    #[test]
248    fn shell_frame_rejects_degenerate_inputs() {
249        assert_eq!(
250            ShellElementGeometry {
251                nodes_m: [[0.0, 0.0, 0.0]; 3],
252                reference_axis: [1.0, 0.0, 0.0],
253            }
254            .local_frame()
255            .expect_err("zero-area shell should fail"),
256            ShellElementError::DegenerateArea
257        );
258    }
259
260    fn assert_close(actual: f64, expected: f64, tolerance: f64) {
261        assert!(
262            (actual - expected).abs() <= tolerance,
263            "actual={actual} expected={expected} tolerance={tolerance}",
264        );
265    }
266}