Skip to main content

solid_mechanics/
fem.rs

1//! Finite element basics — 1D bar elements, assembly, boundary conditions.
2
3use nalgebra::DMatrix;
4use serde::{Deserialize, Serialize};
5
6/// A 1D two-node bar element with constant cross-section.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct BarElement1D {
9    /// Young's modulus (Pa)
10    pub e: f64,
11    /// Cross-sectional area (m²)
12    pub a: f64,
13    /// Element length (m)
14    pub length: f64,
15    /// Global node indices [node_i, node_j]
16    pub nodes: [usize; 2],
17}
18
19impl BarElement1D {
20    /// Create a new 1D bar element.
21    pub fn new(e: f64, a: f64, length: f64, node_i: usize, node_j: usize) -> Self {
22        Self { e, a, length, nodes: [node_i, node_j] }
23    }
24
25    /// Element stiffness: k = EA/L
26    pub fn stiffness(&self) -> f64 {
27        self.e * self.a / self.length
28    }
29
30    /// Local 2×2 element stiffness matrix.
31    pub fn local_stiffness_matrix(&self) -> [[f64; 2]; 2] {
32        let k = self.stiffness();
33        [[k, -k], [-k, k]]
34    }
35
36    /// Axial stress in the element given nodal displacements.
37    pub fn axial_stress(&self, u_i: f64, u_j: f64) -> f64 {
38        self.e * (u_j - u_i) / self.length
39    }
40
41    /// Axial force in the element.
42    pub fn axial_force(&self, u_i: f64, u_j: f64) -> f64 {
43        self.stiffness() * (u_j - u_i)
44    }
45
46    /// Strain energy in the element.
47    pub fn strain_energy(&self, u_i: f64, u_j: f64) -> f64 {
48        let du = u_j - u_i;
49        0.5 * self.stiffness() * du * du
50    }
51}
52
53/// 1D FEM assembler for bar elements.
54#[derive(Debug, Clone)]
55pub struct FemAssembler1D {
56    /// Number of nodes
57    pub num_nodes: usize,
58    /// Elements
59    pub elements: Vec<BarElement1D>,
60}
61
62impl FemAssembler1D {
63    /// Create a new assembler with a given number of nodes.
64    pub fn new(num_nodes: usize) -> Self {
65        Self { num_nodes, elements: Vec::new() }
66    }
67
68    /// Add an element.
69    pub fn add_element(&mut self, element: BarElement1D) {
70        self.elements.push(element);
71    }
72
73    /// Create a uniform bar discretized into n elements.
74    pub fn uniform_bar(e: f64, a: f64, total_length: f64, n_elements: usize) -> Self {
75        let n_nodes = n_elements + 1;
76        let le = total_length / n_elements as f64;
77        let mut assembler = Self::new(n_nodes);
78        for i in 0..n_elements {
79            assembler.add_element(BarElement1D::new(e, a, le, i, i + 1));
80        }
81        assembler
82    }
83
84    /// Assemble the global stiffness matrix.
85    pub fn assemble_global_stiffness(&self) -> DMatrix<f64> {
86        let n = self.num_nodes;
87        let mut k_global = DMatrix::zeros(n, n);
88
89        for elem in &self.elements {
90            let ke = elem.local_stiffness_matrix();
91            let ni = elem.nodes[0];
92            let nj = elem.nodes[1];
93
94            k_global[(ni, ni)] += ke[0][0];
95            k_global[(ni, nj)] += ke[0][1];
96            k_global[(nj, ni)] += ke[1][0];
97            k_global[(nj, nj)] += ke[1][1];
98        }
99
100        k_global
101    }
102
103    /// Apply boundary conditions by removing fixed DOFs.
104    /// `fixed_nodes` contains indices of nodes with zero displacement.
105    /// `forces` is the full force vector.
106    /// Returns (reduced stiffness, reduced force vector, mapping from free DOF to global DOF).
107    pub fn apply_boundary_conditions(
108        &self,
109        k_global: &DMatrix<f64>,
110        forces: &[f64],
111        fixed_nodes: &[usize],
112    ) -> (DMatrix<f64>, Vec<f64>, Vec<usize>) {
113        let fixed_set: std::collections::HashSet<usize> = fixed_nodes.iter().cloned().collect();
114        let free_dofs: Vec<usize> = (0..self.num_nodes)
115            .filter(|i| !fixed_set.contains(i))
116            .collect();
117
118        let nf = free_dofs.len();
119        let mut k_reduced = DMatrix::zeros(nf, nf);
120        let mut f_reduced = vec![0.0; nf];
121
122        for (ir, &i) in free_dofs.iter().enumerate() {
123            f_reduced[ir] = forces[i];
124            for (jc, &j) in free_dofs.iter().enumerate() {
125                k_reduced[(ir, jc)] = k_global[(i, j)];
126            }
127        }
128
129        (k_reduced, f_reduced, free_dofs)
130    }
131
132    /// Solve the FEM system for displacements.
133    /// Returns the full displacement vector (including zeros at fixed nodes).
134    pub fn solve(
135        &self,
136        forces: &[f64],
137        fixed_nodes: &[usize],
138    ) -> Option<Vec<f64>> {
139        let k_global = self.assemble_global_stiffness();
140        let (k_red, f_red, free_dofs) = self.apply_boundary_conditions(&k_global, forces, fixed_nodes);
141
142        // Solve K_red * u_red = f_red
143        let u_red = k_red.lu().solve(&nalgebra::DVector::from_vec(f_red))?;
144
145        // Reconstruct full displacement vector
146        let mut u_full = vec![0.0; self.num_nodes];
147        for (i, &dof) in free_dofs.iter().enumerate() {
148            u_full[dof] = u_red[i];
149        }
150
151        Some(u_full)
152    }
153
154    /// Compute element stresses from the full displacement vector.
155    pub fn element_stresses(&self, displacements: &[f64]) -> Vec<f64> {
156        self.elements.iter().map(|elem| {
157            let ui = displacements[elem.nodes[0]];
158            let uj = displacements[elem.nodes[1]];
159            elem.axial_stress(ui, uj)
160        }).collect()
161    }
162
163    /// Compute total strain energy.
164    pub fn total_strain_energy(&self, displacements: &[f64]) -> f64 {
165        self.elements.iter().map(|elem| {
166            let ui = displacements[elem.nodes[0]];
167            let uj = displacements[elem.nodes[1]];
168            elem.strain_energy(ui, uj)
169        }).sum()
170    }
171
172    /// Verify equilibrium: K * u = F (external force check).
173    pub fn check_equilibrium(
174        &self,
175        displacements: &[f64],
176        forces: &[f64],
177    ) -> f64 {
178        let k = self.assemble_global_stiffness();
179        let u = nalgebra::DVector::from_vec(displacements.to_vec());
180        let f = nalgebra::DVector::from_vec(forces.to_vec());
181        (k * u - f).norm()
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use approx::assert_relative_eq;
189
190    #[test]
191    fn test_bar_element_stiffness() {
192        let bar = BarElement1D::new(200e9, 1e-4, 1.0, 0, 1);
193        assert_relative_eq!(bar.stiffness(), 2e7);
194    }
195
196    #[test]
197    fn test_local_stiffness_symmetry() {
198        let bar = BarElement1D::new(200e9, 1e-4, 1.0, 0, 1);
199        let k = bar.local_stiffness_matrix();
200        assert_relative_eq!(k[0][1], -k[0][0]);
201        assert_relative_eq!(k[1][0], -k[1][1]);
202    }
203
204    #[test]
205    fn test_single_bar_fixed_free() {
206        let bar = BarElement1D::new(200e9, 1e-4, 1.0, 0, 1);
207        let mut fem = FemAssembler1D::new(2);
208        fem.add_element(bar);
209        let forces = vec![0.0, 10000.0];
210        let fixed = vec![0];
211        let u = fem.solve(&forces, &fixed).unwrap();
212        // u1 = FL/(EA) = 10000 / (200e9 * 1e-4) = 5e-4
213        assert_relative_eq!(u[1], 5e-4, epsilon = 1e-8);
214    }
215
216    #[test]
217    fn test_uniform_bar_convergence() {
218        // Fixed-free bar with end load: analytical u = FL/(EA)
219        let e = 200e9;
220        let a = 1e-4;
221        let l = 5.0;
222        let f = 50000.0;
223        let analytical = f * l / (e * a);
224
225        // Convergence: more elements → same result
226        for n in [1, 5, 10, 50] {
227            let fem = FemAssembler1D::uniform_bar(e, a, l, n);
228            let mut forces = vec![0.0; n + 1];
229            forces[n] = f;
230            let u = fem.solve(&forces, &[0]).unwrap();
231            assert_relative_eq!(u[n], analytical, epsilon = 1e-6);
232        }
233    }
234
235    #[test]
236    fn test_stress_uniform_in_uniform_bar() {
237        let e = 200e9;
238        let a = 1e-4;
239        let l = 5.0;
240        let f = 50000.0;
241        let fem = FemAssembler1D::uniform_bar(e, a, l, 5);
242        let mut forces = vec![0.0; 6];
243        forces[5] = f;
244        let u = fem.solve(&forces, &[0]).unwrap();
245        let stresses = fem.element_stresses(&u);
246        // All elements should have same stress: σ = F/A
247        let expected_stress = f / a;
248        for &s in &stresses {
249            assert_relative_eq!(s, expected_stress, epsilon = 1e-3);
250        }
251    }
252
253    #[test]
254    fn test_strain_energy_conservation() {
255        let e = 200e9;
256        let a = 1e-4;
257        let l = 3.0;
258        let f = 30000.0;
259        let fem = FemAssembler1D::uniform_bar(e, a, l, 10);
260        let mut forces = vec![0.0; 11];
261        forces[10] = f;
262        let u = fem.solve(&forces, &[0]).unwrap();
263        let se = fem.total_strain_energy(&u);
264        // Strain energy = 0.5 * F * δ = 0.5 * F * FL/(EA) = F²L/(2EA)
265        let expected = f * f * l / (2.0 * e * a);
266        assert_relative_eq!(se, expected, epsilon = 1e-2);
267    }
268
269    #[test]
270    fn test_equilibrium_check() {
271        let e = 200e9;
272        let a = 1e-4;
273        let l = 2.0;
274        let fem = FemAssembler1D::uniform_bar(e, a, l, 4);
275        let f_end = 10000.0;
276        let mut forces = vec![0.0; 5];
277        forces[4] = f_end;
278        let u = fem.solve(&forces, &[0]).unwrap();
279
280        // Verify K*u = F at free DOFs only (skip fixed DOF 0)
281        let k = fem.assemble_global_stiffness();
282        for i in 1..5 {
283            let mut ku_i = 0.0_f64;
284            for j in 0..5 {
285                ku_i += k[(i, j)] * u[j];
286            }
287            assert_relative_eq!(ku_i, forces[i], epsilon = 0.1);
288        }
289    }
290}