Skip to main content

oxiphysics_gpu/
gpu_fem_assembly.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! GPU-accelerated FEM matrix assembly (CPU mock implementation).
5//!
6//! This module provides Finite Element Method (FEM) matrix assembly routines
7//! that mirror a GPU implementation. All operations run on the CPU via plain
8//! loops for portability.
9//!
10//! The formulation assumes 2-node bar/rod elements in 1-D for simplicity,
11//! making the element stiffness matrix 2×2 and DOF management straightforward.
12//! The same patterns extend to 2-D and 3-D elements.
13
14// ── Data structures ──────────────────────────────────────────────────────────
15
16/// A FEM mesh with element connectivity and material parameters.
17///
18/// Elements are 2-node bar/rod elements. Each element connects two nodes.
19/// Global DOF count equals the number of nodes.
20#[derive(Debug, Clone)]
21pub struct GpuFemMesh {
22    /// Node coordinates (one per node).
23    pub node_coords: Vec<f64>,
24    /// Element connectivity: `[node_a0, node_b0, node_a1, node_b1, …]`.
25    pub elements: Vec<usize>,
26    /// Young's modulus for each element.
27    pub youngs_modulus: Vec<f64>,
28    /// Cross-sectional area for each element.
29    pub area: Vec<f64>,
30    /// Global stiffness matrix (n_dofs × n_dofs, row-major).
31    pub k_global: Vec<f64>,
32    /// Global displacement vector (n_dofs).
33    pub displacements: Vec<f64>,
34    /// Global external force vector (n_dofs).
35    pub ext_forces: Vec<f64>,
36    /// Residual vector r = f − K·u (n_dofs).
37    pub residual: Vec<f64>,
38    /// Dirichlet (fixed) DOF flags: `true` means constrained.
39    pub dirichlet_flags: Vec<bool>,
40}
41
42impl GpuFemMesh {
43    /// Create a new `GpuFemMesh` from node coordinates and element connectivity.
44    ///
45    /// `elements` must be a flat list of node-index pairs `[a0, b0, a1, b1, …]`.
46    /// All material parameters default to 1.0.
47    pub fn new(node_coords: Vec<f64>, elements: Vec<usize>) -> Self {
48        let n_nodes = node_coords.len();
49        let n_elems = elements.len() / 2;
50        Self {
51            node_coords,
52            elements,
53            youngs_modulus: vec![1.0; n_elems],
54            area: vec![1.0; n_elems],
55            k_global: vec![0.0; n_nodes * n_nodes],
56            displacements: vec![0.0; n_nodes],
57            ext_forces: vec![0.0; n_nodes],
58            residual: vec![0.0; n_nodes],
59            dirichlet_flags: vec![false; n_nodes],
60        }
61    }
62
63    /// Number of nodes (= DOFs for 1-D bar formulation).
64    pub fn n_dofs(&self) -> usize {
65        self.node_coords.len()
66    }
67
68    /// Number of elements.
69    pub fn n_elements(&self) -> usize {
70        self.elements.len() / 2
71    }
72}
73
74// ── GPU kernel mocks ─────────────────────────────────────────────────────────
75
76/// Compute the 2×2 element stiffness matrix for bar/rod element `e`.
77///
78/// For a 1-D bar element: k_e = (E·A / L) · \[\[1, -1\\], \[-1, 1\]]
79///
80/// Returns `[k00, k01, k10, k11]` in row-major order.
81pub fn gpu_element_stiffness(mesh: &GpuFemMesh, e: usize) -> [f64; 4] {
82    let na = mesh.elements[e * 2];
83    let nb = mesh.elements[e * 2 + 1];
84    let xa = mesh.node_coords[na];
85    let xb = mesh.node_coords[nb];
86    let length = (xb - xa).abs();
87    if length < 1e-15 {
88        return [0.0; 4];
89    }
90    let ke = mesh.youngs_modulus[e] * mesh.area[e] / length;
91    [ke, -ke, -ke, ke]
92}
93
94/// Parallel element stiffness computation — returns all element matrices.
95///
96/// Returns a `Vec` of `[k00, k01, k10, k11]` arrays, one per element.
97pub fn gpu_assemble_global(mesh: &mut GpuFemMesh) {
98    let n_dofs = mesh.n_dofs();
99    mesh.k_global = vec![0.0; n_dofs * n_dofs];
100    let n_elem = mesh.n_elements();
101    for e in 0..n_elem {
102        let ke = gpu_element_stiffness(mesh, e);
103        let na = mesh.elements[e * 2];
104        let nb = mesh.elements[e * 2 + 1];
105        // scatter ke into global K
106        mesh.k_global[na * n_dofs + na] += ke[0];
107        mesh.k_global[na * n_dofs + nb] += ke[1];
108        mesh.k_global[nb * n_dofs + na] += ke[2];
109        mesh.k_global[nb * n_dofs + nb] += ke[3];
110    }
111}
112
113/// Apply Dirichlet boundary conditions by zeroing constrained DOF rows/cols.
114///
115/// For each constrained DOF `i`, sets:
116/// - Row `i` of `K` to zero except `K[i,i] = 1`
117/// - Column `i` of `K` to zero
118/// - `f[i] = 0`
119pub fn gpu_apply_dirichlet(mesh: &mut GpuFemMesh) {
120    let n = mesh.n_dofs();
121    for i in 0..n {
122        if mesh.dirichlet_flags[i] {
123            // zero row i
124            for j in 0..n {
125                mesh.k_global[i * n + j] = 0.0;
126            }
127            // zero column i
128            for j in 0..n {
129                mesh.k_global[j * n + i] = 0.0;
130            }
131            // set diagonal to 1
132            mesh.k_global[i * n + i] = 1.0;
133            // zero rhs
134            mesh.ext_forces[i] = 0.0;
135        }
136    }
137}
138
139/// Compute the residual vector `r = f − K·u` in parallel.
140///
141/// Updates `mesh.residual`.
142pub fn gpu_residual(mesh: &mut GpuFemMesh) {
143    let n = mesh.n_dofs();
144    for i in 0..n {
145        let mut ku_i = 0.0f64;
146        for j in 0..n {
147            ku_i += mesh.k_global[i * n + j] * mesh.displacements[j];
148        }
149        mesh.residual[i] = mesh.ext_forces[i] - ku_i;
150    }
151}
152
153/// Parallel reduction dot product: `a · b`.
154///
155/// Both slices must have the same length.
156pub fn gpu_dot_product(a: &[f64], b: &[f64]) -> f64 {
157    a.iter().zip(b.iter()).map(|(ai, bi)| ai * bi).sum()
158}
159
160/// Return the element stiffness matrices for all elements (parallel mock).
161///
162/// Convenience wrapper that collects [`gpu_element_stiffness`] for every
163/// element.
164pub fn gpu_all_element_stiffness(mesh: &GpuFemMesh) -> Vec<[f64; 4]> {
165    (0..mesh.n_elements())
166        .map(|e| gpu_element_stiffness(mesh, e))
167        .collect()
168}
169
170// ── Tests ────────────────────────────────────────────────────────────────────
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    /// Build a simple 3-node bar mesh: nodes at 0.0, 1.0, 2.0 with 2 elements.
177    fn make_bar_mesh() -> GpuFemMesh {
178        let coords = vec![0.0, 1.0, 2.0];
179        let elems = vec![0, 1, 1, 2];
180        GpuFemMesh::new(coords, elems)
181    }
182
183    #[test]
184    fn test_new_mesh_n_dofs() {
185        let m = make_bar_mesh();
186        assert_eq!(m.n_dofs(), 3);
187    }
188
189    #[test]
190    fn test_new_mesh_n_elements() {
191        let m = make_bar_mesh();
192        assert_eq!(m.n_elements(), 2);
193    }
194
195    #[test]
196    fn test_new_mesh_default_youngs() {
197        let m = make_bar_mesh();
198        assert!((m.youngs_modulus[0] - 1.0).abs() < 1e-12);
199    }
200
201    #[test]
202    fn test_element_stiffness_unit_bar() {
203        // E=1, A=1, L=1 → ke = [[1,-1],[-1,1]]
204        let m = make_bar_mesh();
205        let ke = gpu_element_stiffness(&m, 0);
206        assert!((ke[0] - 1.0).abs() < 1e-12);
207        assert!((ke[1] + 1.0).abs() < 1e-12);
208        assert!((ke[2] + 1.0).abs() < 1e-12);
209        assert!((ke[3] - 1.0).abs() < 1e-12);
210    }
211
212    #[test]
213    fn test_element_stiffness_scaled() {
214        // E=2, A=3, L=1 → ke_diag = 6
215        let mut m = make_bar_mesh();
216        m.youngs_modulus[0] = 2.0;
217        m.area[0] = 3.0;
218        let ke = gpu_element_stiffness(&m, 0);
219        assert!((ke[0] - 6.0).abs() < 1e-12);
220    }
221
222    #[test]
223    fn test_element_stiffness_zero_length() {
224        let coords = vec![0.0, 0.0];
225        let elems = vec![0, 1];
226        let m = GpuFemMesh::new(coords, elems);
227        let ke = gpu_element_stiffness(&m, 0);
228        assert_eq!(ke, [0.0; 4]);
229    }
230
231    #[test]
232    fn test_assemble_global_dimensions() {
233        let mut m = make_bar_mesh();
234        gpu_assemble_global(&mut m);
235        assert_eq!(m.k_global.len(), 9); // 3×3
236    }
237
238    #[test]
239    fn test_assemble_global_diagonal_positive() {
240        let mut m = make_bar_mesh();
241        gpu_assemble_global(&mut m);
242        let n = m.n_dofs();
243        for i in 0..n {
244            assert!(m.k_global[i * n + i] >= 0.0);
245        }
246    }
247
248    #[test]
249    fn test_assemble_global_symmetric() {
250        let mut m = make_bar_mesh();
251        gpu_assemble_global(&mut m);
252        let n = m.n_dofs();
253        for i in 0..n {
254            for j in 0..n {
255                assert!(
256                    (m.k_global[i * n + j] - m.k_global[j * n + i]).abs() < 1e-12,
257                    "K[{i},{j}] != K[{j},{i}]"
258                );
259            }
260        }
261    }
262
263    #[test]
264    fn test_assemble_global_row_sum_zero() {
265        // For a free structure (no BCs) each row should sum to ~0
266        let mut m = make_bar_mesh();
267        gpu_assemble_global(&mut m);
268        let n = m.n_dofs();
269        for i in 0..n {
270            let row_sum: f64 = (0..n).map(|j| m.k_global[i * n + j]).sum();
271            assert!(row_sum.abs() < 1e-10, "row {i} sum = {row_sum}");
272        }
273    }
274
275    #[test]
276    fn test_apply_dirichlet_zeroes_row() {
277        let mut m = make_bar_mesh();
278        gpu_assemble_global(&mut m);
279        m.dirichlet_flags[0] = true;
280        gpu_apply_dirichlet(&mut m);
281        let n = m.n_dofs();
282        // Off-diagonal entries of row 0 should be zero
283        for j in 1..n {
284            assert!((m.k_global[j]).abs() < 1e-12);
285        }
286        // Diagonal should be 1
287        assert!((m.k_global[0]).abs() - 1.0 < 1e-12);
288    }
289
290    #[test]
291    fn test_apply_dirichlet_zeroes_column() {
292        let mut m = make_bar_mesh();
293        gpu_assemble_global(&mut m);
294        m.dirichlet_flags[0] = true;
295        gpu_apply_dirichlet(&mut m);
296        let n = m.n_dofs();
297        for i in 1..n {
298            assert!((m.k_global[i * n]).abs() < 1e-12);
299        }
300    }
301
302    #[test]
303    fn test_apply_dirichlet_zeroes_rhs() {
304        let mut m = make_bar_mesh();
305        gpu_assemble_global(&mut m);
306        m.ext_forces[0] = 99.0;
307        m.dirichlet_flags[0] = true;
308        gpu_apply_dirichlet(&mut m);
309        assert!((m.ext_forces[0]).abs() < 1e-12);
310    }
311
312    #[test]
313    fn test_residual_zero_displacement() {
314        let mut m = make_bar_mesh();
315        gpu_assemble_global(&mut m);
316        m.ext_forces[2] = 1.0;
317        // u = 0 → r = f
318        gpu_residual(&mut m);
319        assert!((m.residual[2] - 1.0).abs() < 1e-12);
320    }
321
322    #[test]
323    fn test_residual_equilibrium() {
324        // If K*u = f exactly, residual should be zero
325        let mut m = make_bar_mesh();
326        gpu_assemble_global(&mut m);
327        m.dirichlet_flags[0] = true;
328        gpu_apply_dirichlet(&mut m);
329        m.ext_forces[2] = 1.0;
330        // Solve manually for 2-element bar, node 0 fixed, node 2 loaded
331        // K after BCs: diag = [1, 2, 1], off-diag per element pattern
332        // For simplicity just set u = K^{-1} f using known solution
333        // u[0]=0, u[1]=1, u[2]=2 (for unit bar: displacement = x * force)
334        m.displacements = vec![0.0, 1.0, 2.0];
335        gpu_residual(&mut m);
336        // residual should not blow up
337        for &r in &m.residual {
338            assert!(r.is_finite());
339        }
340    }
341
342    #[test]
343    fn test_gpu_dot_product_basic() {
344        let a = [1.0, 2.0, 3.0];
345        let b = [4.0, 5.0, 6.0];
346        assert!((gpu_dot_product(&a, &b) - 32.0).abs() < 1e-12);
347    }
348
349    #[test]
350    fn test_gpu_dot_product_empty() {
351        assert!((gpu_dot_product(&[], &[])).abs() < 1e-12);
352    }
353
354    #[test]
355    fn test_gpu_dot_product_unit_vectors() {
356        let a = [1.0, 0.0, 0.0];
357        let b = [0.0, 1.0, 0.0];
358        assert!((gpu_dot_product(&a, &b)).abs() < 1e-12);
359    }
360
361    #[test]
362    fn test_gpu_all_element_stiffness_count() {
363        let m = make_bar_mesh();
364        let all_ke = gpu_all_element_stiffness(&m);
365        assert_eq!(all_ke.len(), m.n_elements());
366    }
367
368    #[test]
369    fn test_gpu_all_element_stiffness_values() {
370        let m = make_bar_mesh();
371        let all_ke = gpu_all_element_stiffness(&m);
372        // Both elements identical → same stiffness matrix
373        assert_eq!(all_ke[0], all_ke[1]);
374    }
375
376    #[test]
377    fn test_fem_mesh_clone() {
378        let m = make_bar_mesh();
379        let m2 = m.clone();
380        assert_eq!(m2.n_dofs(), 3);
381    }
382
383    #[test]
384    fn test_fem_mesh_debug() {
385        let m = make_bar_mesh();
386        let s = format!("{m:?}");
387        assert!(s.contains("GpuFemMesh"));
388    }
389
390    #[test]
391    fn test_assemble_then_apply_dirichlet_both_ends() {
392        let mut m = make_bar_mesh();
393        gpu_assemble_global(&mut m);
394        m.dirichlet_flags[0] = true;
395        m.dirichlet_flags[2] = true;
396        gpu_apply_dirichlet(&mut m);
397        let n = m.n_dofs();
398        // Diagonals of constrained nodes should be 1
399        assert!((m.k_global[0] - 1.0).abs() < 1e-12);
400        assert!((m.k_global[2 * n + 2] - 1.0).abs() < 1e-12);
401    }
402
403    #[test]
404    fn test_residual_updates_all_entries() {
405        let mut m = make_bar_mesh();
406        gpu_assemble_global(&mut m);
407        m.ext_forces = vec![1.0, 0.0, -1.0];
408        gpu_residual(&mut m);
409        assert_eq!(m.residual.len(), m.n_dofs());
410    }
411}