use crate::error::IntegrateError;
use scirs2_core::ndarray::Array2;
use scirs2_core::parallel_ops::*;
#[derive(Debug, Clone)]
pub struct Element2D {
pub nodes: [[f64; 2]; 3],
pub material_id: usize,
}
#[derive(Debug, Clone)]
pub struct StiffnessMatrix {
pub rows: Vec<usize>,
pub cols: Vec<usize>,
pub vals: Vec<f64>,
pub size: usize,
}
impl StiffnessMatrix {
pub fn empty(size: usize) -> Self {
StiffnessMatrix {
rows: Vec::new(),
cols: Vec::new(),
vals: Vec::new(),
size,
}
}
pub fn nnz_raw(&self) -> usize {
self.vals.len()
}
pub fn to_dense(&self) -> Array2<f64> {
let mut mat = Array2::<f64>::zeros((self.size, self.size));
for ((r, c), v) in self.rows.iter().zip(self.cols.iter()).zip(self.vals.iter()) {
mat[[*r, *c]] += v;
}
mat
}
fn merge(&mut self, other: StiffnessMatrix) {
self.rows.extend(other.rows);
self.cols.extend(other.cols);
self.vals.extend(other.vals);
}
}
pub fn element_stiffness(
elem: &Element2D,
d_matrix: &Array2<f64>,
) -> Result<[[f64; 6]; 6], IntegrateError> {
let [x1, y1] = elem.nodes[0];
let [x2, y2] = elem.nodes[1];
let [x3, y3] = elem.nodes[2];
let two_area = (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1);
if two_area.abs() < 1e-15 {
return Err(IntegrateError::ValueError(
"Degenerate triangle: zero or near-zero area".to_string(),
));
}
let inv_two_area = 1.0 / two_area;
let b1x = (y2 - y3) * inv_two_area;
let b1y = (x3 - x2) * inv_two_area;
let b2x = (y3 - y1) * inv_two_area;
let b2y = (x1 - x3) * inv_two_area;
let b3x = (y1 - y2) * inv_two_area;
let b3y = (x2 - x1) * inv_two_area;
let b = [
[b1x, 0.0, b2x, 0.0, b3x, 0.0],
[0.0, b1y, 0.0, b2y, 0.0, b3y],
[b1y, b1x, b2y, b2x, b3y, b3x],
];
let area = two_area.abs() * 0.5;
let d = [
[d_matrix[[0, 0]], d_matrix[[0, 1]], d_matrix[[0, 2]]],
[d_matrix[[1, 0]], d_matrix[[1, 1]], d_matrix[[1, 2]]],
[d_matrix[[2, 0]], d_matrix[[2, 1]], d_matrix[[2, 2]]],
];
let mut db = [[0.0_f64; 6]; 3];
for i in 0..3 {
for j in 0..6 {
for k in 0..3 {
db[i][j] += d[i][k] * b[k][j];
}
}
}
let mut ke = [[0.0_f64; 6]; 6];
for i in 0..6 {
for j in 0..6 {
for k in 0..3 {
ke[i][j] += b[k][i] * db[k][j];
}
}
}
for i in 0..6 {
for j in 0..6 {
ke[i][j] *= area;
}
}
Ok(ke)
}
pub fn assemble_stiffness_cpu(
elements: &[Element2D],
d_matrix: &Array2<f64>,
n_nodes: usize,
) -> Result<StiffnessMatrix, IntegrateError> {
if d_matrix.shape() != [3, 3] {
return Err(IntegrateError::DimensionMismatch(format!(
"D matrix must be 3×3, got {:?}",
d_matrix.shape()
)));
}
if n_nodes < 3 * elements.len() {
return Err(IntegrateError::DimensionMismatch(format!(
"assemble_stiffness_cpu requires n_nodes >= 3 * elements.len() \
({n_nodes} < {}); each element uses 3 independent nodes. \
For shared nodes use assemble_stiffness_mesh.",
3 * elements.len()
)));
}
let n_dof = 2 * n_nodes;
let n_threads = num_threads().max(1);
let chunk_size = (elements.len() / n_threads).max(1);
let chunks: Vec<(usize, &[Element2D])> = elements
.chunks(chunk_size)
.scan(0_usize, |offset, chunk| {
let start = *offset;
*offset += chunk.len();
Some((start, chunk))
})
.collect();
let locals: Vec<StiffnessMatrix> = parallel_map_result(&chunks, |(start_idx, chunk)| {
assemble_chunk(*start_idx, chunk, d_matrix, n_dof)
})?;
let mut global = StiffnessMatrix::empty(n_dof);
for local in locals {
global.merge(local);
}
Ok(global)
}
fn assemble_chunk(
elem_offset: usize,
elements: &[Element2D],
d_matrix: &Array2<f64>,
n_dof: usize,
) -> Result<StiffnessMatrix, IntegrateError> {
let mut local = StiffnessMatrix::empty(n_dof);
for (i, elem) in elements.iter().enumerate() {
let ke = element_stiffness(elem, d_matrix)?;
let k = elem_offset + i;
let dofs: [usize; 6] = [6 * k, 6 * k + 1, 6 * k + 2, 6 * k + 3, 6 * k + 4, 6 * k + 5];
for (li, &gi) in dofs.iter().enumerate() {
for (lj, &gj) in dofs.iter().enumerate() {
if gi < n_dof && gj < n_dof {
local.rows.push(gi);
local.cols.push(gj);
local.vals.push(ke[li][lj]);
}
}
}
}
Ok(local)
}
#[derive(Debug, Clone)]
pub struct MeshElement2D {
pub node_ids: [usize; 3],
pub nodes: [[f64; 2]; 3],
pub material_id: usize,
}
impl MeshElement2D {
pub fn to_element2d(&self) -> Element2D {
Element2D {
nodes: self.nodes,
material_id: self.material_id,
}
}
}
pub fn assemble_stiffness_mesh(
mesh_elements: &[MeshElement2D],
d_matrix: &Array2<f64>,
n_nodes: usize,
) -> Result<StiffnessMatrix, IntegrateError> {
if d_matrix.shape() != [3, 3] {
return Err(IntegrateError::DimensionMismatch(format!(
"D matrix must be 3×3, got {:?}",
d_matrix.shape()
)));
}
let n_dof = 2 * n_nodes;
let chunk_size = (mesh_elements.len() / num_threads().max(1)).max(1);
let chunks: Vec<&[MeshElement2D]> = mesh_elements.chunks(chunk_size).collect();
let locals: Vec<StiffnessMatrix> =
parallel_map_result(&chunks, |chunk| assemble_mesh_chunk(chunk, d_matrix, n_dof))?;
let mut global = StiffnessMatrix::empty(n_dof);
for local in locals {
global.merge(local);
}
Ok(global)
}
fn assemble_mesh_chunk(
chunk: &[MeshElement2D],
d_matrix: &Array2<f64>,
n_dof: usize,
) -> Result<StiffnessMatrix, IntegrateError> {
let mut local = StiffnessMatrix::empty(n_dof);
for me in chunk {
let elem = me.to_element2d();
let ke = element_stiffness(&elem, d_matrix)?;
let gdofs: [usize; 6] = [
2 * me.node_ids[0],
2 * me.node_ids[0] + 1,
2 * me.node_ids[1],
2 * me.node_ids[1] + 1,
2 * me.node_ids[2],
2 * me.node_ids[2] + 1,
];
for (li, &gi) in gdofs.iter().enumerate() {
for (lj, &gj) in gdofs.iter().enumerate() {
if gi < n_dof && gj < n_dof {
local.rows.push(gi);
local.cols.push(gj);
local.vals.push(ke[li][lj]);
}
}
}
}
Ok(local)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
fn isotropic_d() -> Array2<f64> {
let e = 1.0_f64;
let nu = 0.3_f64;
let c = e / (1.0 - nu * nu);
array![
[c, c * nu, 0.0],
[c * nu, c, 0.0],
[0.0, 0.0, c * (1.0 - nu) / 2.0],
]
}
#[test]
fn test_element_stiffness_matrix_2d() {
let elem = Element2D {
nodes: [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
material_id: 0,
};
let d = isotropic_d();
let ke = element_stiffness(&elem, &d).expect("stiffness computation failed");
for i in 0..6 {
for j in 0..6 {
assert!(
(ke[i][j] - ke[j][i]).abs() < 1e-10,
"K_e not symmetric at [{i},{j}]: ke[i][j]={} ke[j][i]={}",
ke[i][j],
ke[j][i]
);
}
}
}
#[test]
fn test_element_stiffness_positive_semidefinite() {
let elem = Element2D {
nodes: [[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]],
material_id: 0,
};
let d = isotropic_d();
let ke = element_stiffness(&elem, &d).expect("stiffness computation failed");
for i in 0..6 {
assert!(
ke[i][i] >= -1e-12,
"Negative diagonal at [{i}]: {}",
ke[i][i]
);
}
let frob: f64 = ke
.iter()
.flat_map(|row| row.iter())
.map(|v| v * v)
.sum::<f64>()
.sqrt();
assert!(frob > 0.0, "Stiffness matrix is all-zero");
}
#[test]
fn test_cpu_assembly_mesh_of_10_elements() {
let d = isotropic_d();
let mut elements = Vec::new();
let n_nodes = 12_usize;
for k in 0..10_usize {
let x0 = (k % 5) as f64;
let y0 = (k / 5) as f64;
elements.push(MeshElement2D {
node_ids: [k % n_nodes, (k + 1) % n_nodes, (k + 3) % n_nodes],
nodes: [[x0, y0], [x0 + 1.0, y0], [x0 + 0.5, y0 + 1.0]],
material_id: 0,
});
}
let km = assemble_stiffness_mesh(&elements, &d, n_nodes).expect("mesh assembly failed");
assert!(!km.vals.is_empty(), "No entries assembled");
for (&r, &c) in km.rows.iter().zip(km.cols.iter()) {
assert!(r < 2 * n_nodes, "Row index out of bounds: {r}");
assert!(c < 2 * n_nodes, "Col index out of bounds: {c}");
}
}
#[test]
fn test_stiffness_matrix_symmetry_via_dense() {
let d = isotropic_d();
let elements = vec![
MeshElement2D {
node_ids: [0, 1, 2],
nodes: [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
material_id: 0,
},
MeshElement2D {
node_ids: [1, 3, 2],
nodes: [[1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
material_id: 0,
},
];
let km = assemble_stiffness_mesh(&elements, &d, 4).expect("assembly failed");
let dense = km.to_dense();
let n = dense.shape()[0];
for i in 0..n {
for j in 0..n {
assert!(
(dense[[i, j]] - dense[[j, i]]).abs() < 1e-10,
"Global K not symmetric at [{i},{j}]"
);
}
}
}
#[test]
fn test_degenerate_element_returns_error() {
let elem = Element2D {
nodes: [[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]],
material_id: 0,
};
let d = isotropic_d();
assert!(element_stiffness(&elem, &d).is_err());
}
}