use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DofType {
Displacement,
Temperature,
Pressure,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoundaryCondition {
pub dof: DofType,
pub value: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FemNode {
pub id: usize,
pub x: f64,
pub y: f64,
pub boundary_condition: Option<BoundaryCondition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FemMaterial {
pub youngs_modulus: f64,
pub poissons_ratio: f64,
pub thermal_conductivity: f64,
pub density: f64,
}
impl Default for FemMaterial {
fn default() -> Self {
Self {
youngs_modulus: 200e9,
poissons_ratio: 0.3,
thermal_conductivity: 50.0,
density: 7850.0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ElementType {
Bar1D,
Triangle2D,
Quad2D,
Beam1D,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FemElement {
pub id: usize,
pub node_ids: Vec<usize>,
pub material: FemMaterial,
pub element_type: ElementType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodalLoad {
pub node_id: usize,
pub fx: f64,
pub fy: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElementHeatFlux {
pub element_id: usize,
pub q: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FemMesh {
pub nodes: Vec<FemNode>,
pub elements: Vec<FemElement>,
}
impl FemMesh {
pub fn new() -> Self {
Self::default()
}
pub fn add_node(&mut self, x: f64, y: f64) -> usize {
let id = self.nodes.len();
self.nodes.push(FemNode {
id,
x,
y,
boundary_condition: None,
});
id
}
pub fn add_element(
&mut self,
node_ids: Vec<usize>,
material: FemMaterial,
element_type: ElementType,
) -> usize {
let id = self.elements.len();
self.elements.push(FemElement {
id,
node_ids,
material,
element_type,
});
id
}
pub fn set_boundary_condition(&mut self, node_id: usize, dof: DofType, value: f64) {
if let Some(node) = self.nodes.get_mut(node_id) {
node.boundary_condition = Some(BoundaryCondition { dof, value });
}
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn element_count(&self) -> usize {
self.elements.len()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FemSolution {
pub displacements: Vec<(f64, f64)>,
pub von_mises_stress: Vec<f64>,
pub max_displacement: f64,
pub converged: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThermalSolution {
pub temperatures: Vec<f64>,
pub heat_flux: Vec<(f64, f64)>,
pub max_temperature: f64,
pub converged: bool,
}
fn gaussian_elimination(a: &mut [Vec<f64>], b: &mut [f64]) -> Option<Vec<f64>> {
let n = b.len();
for col in 0..n {
let mut max_row = col;
let mut max_val = a[col][col].abs();
for (row, a_row) in a.iter().enumerate().skip(col + 1).take(n - col - 1) {
if a_row[col].abs() > max_val {
max_val = a_row[col].abs();
max_row = row;
}
}
if max_val < 1e-30 {
return None; }
a.swap(col, max_row);
b.swap(col, max_row);
let pivot = a[col][col];
for row in (col + 1)..n {
let factor = a[row][col] / pivot;
let pivot_row_vals: Vec<f64> = a[col][col..n].to_vec();
for (a_row_k, &av) in a[row][col..n].iter_mut().zip(pivot_row_vals.iter()) {
*a_row_k -= factor * av;
}
b[row] -= factor * b[col];
}
}
let mut x = vec![0.0f64; n];
for i in (0..n).rev() {
x[i] = b[i];
for j in (i + 1)..n {
x[i] -= a[i][j] * x[j];
}
x[i] /= a[i][i];
}
Some(x)
}
fn bar1d_element_stiffness(
n_dofs: usize,
elem: &FemElement,
nodes: &[FemNode],
k_global: &mut [Vec<f64>],
cross_section_area: f64,
) {
if elem.node_ids.len() < 2 {
return;
}
let ni = elem.node_ids[0];
let nj = elem.node_ids[1];
let xi = nodes[ni].x;
let xj = nodes[nj].x;
let yi = nodes[ni].y;
let yj = nodes[nj].y;
let dx = xj - xi;
let dy = yj - yi;
let length = (dx * dx + dy * dy).sqrt();
if length < 1e-30 {
return;
}
let ae_over_l = elem.material.youngs_modulus * cross_section_area / length;
let c = dx / length;
let s = dy / length;
let dofs = [2 * ni, 2 * ni + 1, 2 * nj, 2 * nj + 1];
let ke_local = [
[c * c, c * s, -c * c, -c * s],
[c * s, s * s, -c * s, -s * s],
[-c * c, -c * s, c * c, c * s],
[-c * s, -s * s, c * s, s * s],
];
for (a, &ga) in dofs.iter().enumerate() {
for (b, &gb) in dofs.iter().enumerate() {
if ga < n_dofs && gb < n_dofs {
k_global[ga][gb] += ae_over_l * ke_local[a][b];
}
}
}
}
fn triangle2d_element_stiffness(
n_dofs: usize,
elem: &FemElement,
nodes: &[FemNode],
k_global: &mut [Vec<f64>],
thickness: f64,
) {
if elem.node_ids.len() < 3 {
return;
}
let ni = elem.node_ids[0];
let nj = elem.node_ids[1];
let nk = elem.node_ids[2];
let xi = nodes[ni].x;
let yi = nodes[ni].y;
let xj = nodes[nj].x;
let yj = nodes[nj].y;
let xk = nodes[nk].x;
let yk = nodes[nk].y;
let area = 0.5 * ((xj - xi) * (yk - yi) - (xk - xi) * (yj - yi));
if area.abs() < 1e-30 {
return;
}
let e = elem.material.youngs_modulus;
let nu = elem.material.poissons_ratio;
let factor = e / (1.0 - nu * nu);
let b_mat = [
[yj - yk, 0.0, yk - yi, 0.0, yi - yj, 0.0],
[0.0, xk - xj, 0.0, xi - xk, 0.0, xj - xi],
[xk - xj, yj - yk, xi - xk, yk - yi, xj - xi, yi - yj],
];
let scale = 1.0 / (2.0 * area);
let b_mat: Vec<Vec<f64>> = b_mat
.iter()
.map(|row| row.iter().map(|&v| v * scale).collect())
.collect();
let d_mat = [
[factor, factor * nu, 0.0],
[factor * nu, factor, 0.0],
[0.0, 0.0, factor * (1.0 - nu) / 2.0],
];
let mut ke = vec![vec![0.0f64; 6]; 6];
for i in 0..6 {
for j in 0..6 {
let mut sum = 0.0;
for p in 0..3 {
for q in 0..3 {
sum += b_mat[p][i] * d_mat[p][q] * b_mat[q][j];
}
}
ke[i][j] = thickness * area.abs() * sum;
}
}
let dofs = [2 * ni, 2 * ni + 1, 2 * nj, 2 * nj + 1, 2 * nk, 2 * nk + 1];
for (a, &ga) in dofs.iter().enumerate() {
for (b, &gb) in dofs.iter().enumerate() {
if ga < n_dofs && gb < n_dofs {
k_global[ga][gb] += ke[a][b];
}
}
}
}
fn bar1d_thermal_stiffness(
n_nodes: usize,
elem: &FemElement,
nodes: &[FemNode],
k_global: &mut [Vec<f64>],
cross_section_area: f64,
) {
if elem.node_ids.len() < 2 {
return;
}
let ni = elem.node_ids[0];
let nj = elem.node_ids[1];
let dx = nodes[nj].x - nodes[ni].x;
let dy = nodes[nj].y - nodes[ni].y;
let length = (dx * dx + dy * dy).sqrt();
if length < 1e-30 {
return;
}
let k_coeff = elem.material.thermal_conductivity * cross_section_area / length;
let pairs = [
(ni, ni, k_coeff),
(nj, nj, k_coeff),
(ni, nj, -k_coeff),
(nj, ni, -k_coeff),
];
for (r, c, val) in pairs {
if r < n_nodes && c < n_nodes {
k_global[r][c] += val;
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FemSolver {
pub cross_section_area: f64,
pub thickness: f64,
}
impl FemSolver {
pub fn new() -> Self {
Self {
cross_section_area: 1e-4,
thickness: 0.01,
}
}
pub fn with_params(cross_section_area: f64, thickness: f64) -> Self {
Self {
cross_section_area,
thickness,
}
}
pub fn solve_static(&self, mesh: &FemMesh, loads: &[NodalLoad]) -> FemSolution {
let n_nodes = mesh.node_count();
let n_dofs = 2 * n_nodes;
let mut k = vec![vec![0.0f64; n_dofs]; n_dofs];
for elem in &mesh.elements {
match elem.element_type {
ElementType::Bar1D | ElementType::Beam1D => {
bar1d_element_stiffness(
n_dofs,
elem,
&mesh.nodes,
&mut k,
self.cross_section_area,
);
}
ElementType::Triangle2D => {
triangle2d_element_stiffness(n_dofs, elem, &mesh.nodes, &mut k, self.thickness);
}
ElementType::Quad2D => {
if elem.node_ids.len() >= 4 {
let tri_a = FemElement {
id: elem.id,
node_ids: vec![elem.node_ids[0], elem.node_ids[1], elem.node_ids[2]],
material: elem.material.clone(),
element_type: ElementType::Triangle2D,
};
let tri_b = FemElement {
id: elem.id,
node_ids: vec![elem.node_ids[0], elem.node_ids[2], elem.node_ids[3]],
material: elem.material.clone(),
element_type: ElementType::Triangle2D,
};
triangle2d_element_stiffness(
n_dofs,
&tri_a,
&mesh.nodes,
&mut k,
self.thickness,
);
triangle2d_element_stiffness(
n_dofs,
&tri_b,
&mesh.nodes,
&mut k,
self.thickness,
);
}
}
}
}
let mut f = vec![0.0f64; n_dofs];
for load in loads {
let dof_x = 2 * load.node_id;
let dof_y = 2 * load.node_id + 1;
if dof_x < n_dofs {
f[dof_x] += load.fx;
}
if dof_y < n_dofs {
f[dof_y] += load.fy;
}
}
let mut bc_map: HashMap<usize, f64> = HashMap::new();
for node in &mesh.nodes {
if let Some(ref bc) = node.boundary_condition {
match bc.dof {
DofType::Displacement => {
bc_map.insert(2 * node.id, bc.value);
bc_map.insert(2 * node.id + 1, bc.value);
}
DofType::Temperature | DofType::Pressure => {
bc_map.insert(2 * node.id, bc.value);
}
}
}
}
let penalty = 1e30;
for (&dof, &val) in &bc_map {
if dof < n_dofs {
k[dof][dof] += penalty;
f[dof] += penalty * val;
}
}
{
let max_diag = (0..n_dofs).map(|i| k[i][i].abs()).fold(0.0f64, f64::max);
let stab = if max_diag > 0.0 { max_diag * 1e-6 } else { 1.0 };
for (i, k_row) in k.iter_mut().enumerate().take(n_dofs) {
if k_row[i].abs() < 1e-30 && !bc_map.contains_key(&i) {
k_row[i] += stab;
}
}
}
let mut k_copy = k.clone();
let mut f_copy = f.clone();
let solution = gaussian_elimination(&mut k_copy, &mut f_copy);
match solution {
None => FemSolution {
displacements: vec![(0.0, 0.0); n_nodes],
von_mises_stress: vec![0.0; mesh.element_count()],
max_displacement: 0.0,
converged: false,
},
Some(u) => {
let displacements: Vec<(f64, f64)> =
(0..n_nodes).map(|i| (u[2 * i], u[2 * i + 1])).collect();
let von_mises_stress =
compute_von_mises(&u, mesh, self.cross_section_area, self.thickness);
let max_displacement = displacements
.iter()
.map(|(dx, dy)| (dx * dx + dy * dy).sqrt())
.fold(0.0f64, f64::max);
FemSolution {
displacements,
von_mises_stress,
max_displacement,
converged: true,
}
}
}
}
pub fn solve_thermal(&self, mesh: &FemMesh, heat_flux: &[ElementHeatFlux]) -> ThermalSolution {
let n_nodes = mesh.node_count();
let mut k = vec![vec![0.0f64; n_nodes]; n_nodes];
for elem in &mesh.elements {
bar1d_thermal_stiffness(n_nodes, elem, &mesh.nodes, &mut k, self.cross_section_area);
}
let mut q_vec = vec![0.0f64; n_nodes];
for hf in heat_flux {
if let Some(elem) = mesh.elements.get(hf.element_id) {
let n = elem.node_ids.len() as f64;
for &nid in &elem.node_ids {
if nid < n_nodes {
q_vec[nid] += hf.q / n;
}
}
}
}
let penalty = 1e30;
for node in &mesh.nodes {
if let Some(ref bc) = node.boundary_condition {
if bc.dof == DofType::Temperature {
let dof = node.id;
if dof < n_nodes {
k[dof][dof] += penalty;
q_vec[dof] += penalty * bc.value;
}
}
}
}
let mut k_copy = k.clone();
let mut q_copy = q_vec.clone();
let solution = gaussian_elimination(&mut k_copy, &mut q_copy);
match solution {
None => ThermalSolution {
temperatures: vec![0.0; n_nodes],
heat_flux: vec![(0.0, 0.0); mesh.element_count()],
max_temperature: 0.0,
converged: false,
},
Some(temps) => {
let element_heat_flux: Vec<(f64, f64)> = mesh
.elements
.iter()
.map(|elem| compute_element_heat_flux(elem, &temps, &mesh.nodes))
.collect();
let max_temperature = temps.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
ThermalSolution {
temperatures: temps,
heat_flux: element_heat_flux,
max_temperature,
converged: true,
}
}
}
}
}
fn compute_von_mises(
u: &[f64],
mesh: &FemMesh,
cross_section_area: f64,
thickness: f64,
) -> Vec<f64> {
mesh.elements
.iter()
.map(|elem| element_von_mises(u, elem, &mesh.nodes, cross_section_area, thickness))
.collect()
}
fn element_von_mises(
u: &[f64],
elem: &FemElement,
nodes: &[FemNode],
cross_section_area: f64,
_thickness: f64,
) -> f64 {
match elem.element_type {
ElementType::Bar1D | ElementType::Beam1D => {
if elem.node_ids.len() < 2 {
return 0.0;
}
let ni = elem.node_ids[0];
let nj = elem.node_ids[1];
let xi = nodes[ni].x;
let xj = nodes[nj].x;
let yi = nodes[ni].y;
let yj = nodes[nj].y;
let dx = xj - xi;
let dy = yj - yi;
let length = (dx * dx + dy * dy).sqrt().max(1e-30);
let c = dx / length;
let s = dy / length;
let u_i = if 2 * ni + 1 < u.len() {
c * u[2 * ni] + s * u[2 * ni + 1]
} else {
0.0
};
let u_j = if 2 * nj + 1 < u.len() {
c * u[2 * nj] + s * u[2 * nj + 1]
} else {
0.0
};
let strain = (u_j - u_i) / length;
(elem.material.youngs_modulus * strain).abs()
}
ElementType::Triangle2D | ElementType::Quad2D => {
if elem.node_ids.is_empty() {
return 0.0;
}
let avg_disp: f64 = elem
.node_ids
.iter()
.map(|&nid| {
let dx = u.get(2 * nid).copied().unwrap_or(0.0);
let dy = u.get(2 * nid + 1).copied().unwrap_or(0.0);
(dx * dx + dy * dy).sqrt()
})
.sum::<f64>()
/ elem.node_ids.len() as f64;
let char_len = cross_section_area.sqrt().max(1e-10);
elem.material.youngs_modulus * avg_disp / char_len
}
}
}
fn compute_element_heat_flux(elem: &FemElement, temps: &[f64], nodes: &[FemNode]) -> (f64, f64) {
if elem.node_ids.len() < 2 {
return (0.0, 0.0);
}
let ni = elem.node_ids[0];
let nj = elem.node_ids[1];
if ni >= temps.len() || nj >= temps.len() {
return (0.0, 0.0);
}
let xi = nodes[ni].x;
let xj = nodes[nj].x;
let yi = nodes[ni].y;
let yj = nodes[nj].y;
let dx = xj - xi;
let dy = yj - yi;
let length = (dx * dx + dy * dy).sqrt().max(1e-30);
let dt_dl = (temps[nj] - temps[ni]) / length;
let k = elem.material.thermal_conductivity;
let qx = -k * dt_dl * (dx / length);
let qy = -k * dt_dl * (dy / length);
(qx, qy)
}
#[cfg(test)]
mod tests {
use super::*;
fn steel() -> FemMaterial {
FemMaterial {
youngs_modulus: 200e9,
poissons_ratio: 0.3,
thermal_conductivity: 50.0,
density: 7850.0,
}
}
fn aluminium() -> FemMaterial {
FemMaterial {
youngs_modulus: 70e9,
poissons_ratio: 0.33,
thermal_conductivity: 205.0,
density: 2700.0,
}
}
#[test]
fn test_mesh_new_is_empty() {
let mesh = FemMesh::new();
assert_eq!(mesh.node_count(), 0);
assert_eq!(mesh.element_count(), 0);
}
#[test]
fn test_mesh_add_nodes() {
let mut mesh = FemMesh::new();
let id0 = mesh.add_node(0.0, 0.0);
let id1 = mesh.add_node(1.0, 0.0);
let id2 = mesh.add_node(0.5, 1.0);
assert_eq!(id0, 0);
assert_eq!(id1, 1);
assert_eq!(id2, 2);
assert_eq!(mesh.node_count(), 3);
}
#[test]
fn test_mesh_add_element_bar() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
let eid = mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
assert_eq!(eid, 0);
assert_eq!(mesh.element_count(), 1);
}
#[test]
fn test_mesh_add_triangle_element() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
let n2 = mesh.add_node(0.5, 1.0);
mesh.add_element(vec![n0, n1, n2], steel(), ElementType::Triangle2D);
assert_eq!(mesh.element_count(), 1);
}
#[test]
fn test_mesh_set_boundary_condition() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
let bc = mesh.nodes[n0]
.boundary_condition
.as_ref()
.expect("BC should be set");
assert_eq!(bc.dof, DofType::Displacement);
assert_eq!(bc.value, 0.0);
}
#[test]
fn test_boundary_condition_temperature() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Temperature, 300.0);
let bc = mesh.nodes[n0]
.boundary_condition
.as_ref()
.expect("BC must be set");
assert_eq!(bc.dof, DofType::Temperature);
assert!((bc.value - 300.0).abs() < 1e-10);
}
#[test]
fn test_boundary_condition_pressure() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Pressure, 101325.0);
let bc = mesh.nodes[n0]
.boundary_condition
.as_ref()
.expect("BC must be set");
assert_eq!(bc.dof, DofType::Pressure);
assert!((bc.value - 101325.0).abs() < 1.0);
}
#[test]
fn test_boundary_condition_invalid_node_is_noop() {
let mut mesh = FemMesh::new();
mesh.set_boundary_condition(99, DofType::Displacement, 0.0);
assert_eq!(mesh.node_count(), 0);
}
#[test]
fn test_mesh_multiple_elements() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
let n2 = mesh.add_node(2.0, 0.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
mesh.add_element(vec![n1, n2], aluminium(), ElementType::Bar1D);
assert_eq!(mesh.element_count(), 2);
}
#[test]
fn test_bar1d_axial_displacement() {
let a = 1e-4; let e = 200e9; let l = 1.0;
let force = 10_000.0;
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(l, 0.0);
mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
mesh.add_element(
vec![n0, n1],
FemMaterial {
youngs_modulus: e,
..FemMaterial::default()
},
ElementType::Bar1D,
);
let solver = FemSolver::with_params(a, 0.01);
let loads = vec![NodalLoad {
node_id: n1,
fx: force,
fy: 0.0,
}];
let sol = solver.solve_static(&mesh, &loads);
assert!(sol.converged, "Solver should converge");
let expected = force * l / (e * a);
let actual = sol.displacements[n1].0;
assert!(
(actual - expected).abs() / expected < 0.01,
"Expected ~{expected:.6e} m, got {actual:.6e} m"
);
}
#[test]
fn test_bar1d_zero_force_zero_displacement() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
let solver = FemSolver::new();
let sol = solver.solve_static(&mesh, &[]);
assert!(sol.converged);
assert!(sol.max_displacement < 1e-20);
}
#[test]
fn test_two_bar_series() {
let a = 1e-4;
let e = 200e9;
let l = 0.5;
let force = 5000.0;
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(l, 0.0);
let n2 = mesh.add_node(2.0 * l, 0.0);
mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
let mat = FemMaterial {
youngs_modulus: e,
..FemMaterial::default()
};
mesh.add_element(vec![n0, n1], mat.clone(), ElementType::Bar1D);
mesh.add_element(vec![n1, n2], mat, ElementType::Bar1D);
let solver = FemSolver::with_params(a, 0.01);
let loads = vec![NodalLoad {
node_id: n2,
fx: force,
fy: 0.0,
}];
let sol = solver.solve_static(&mesh, &loads);
assert!(sol.converged);
let expected = force * (2.0 * l) / (e * a);
let actual = sol.displacements[n2].0;
assert!(
(actual - expected).abs() / expected < 0.01,
"Two-bar series: expected {expected:.6e}, got {actual:.6e}"
);
}
#[test]
fn test_solver_converged_flag() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
let solver = FemSolver::new();
let sol = solver.solve_static(&mesh, &[]);
assert!(sol.converged);
}
#[test]
fn test_max_displacement_positive() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
let solver = FemSolver::new();
let loads = vec![NodalLoad {
node_id: n1,
fx: 10_000.0,
fy: 0.0,
}];
let sol = solver.solve_static(&mesh, &loads);
assert!(sol.max_displacement > 0.0);
}
#[test]
fn test_von_mises_stress_non_negative() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
let solver = FemSolver::new();
let loads = vec![NodalLoad {
node_id: n1,
fx: 10_000.0,
fy: 0.0,
}];
let sol = solver.solve_static(&mesh, &loads);
for &s in &sol.von_mises_stress {
assert!(s >= 0.0, "Von Mises stress must be non-negative");
}
}
#[test]
fn test_triangle2d_mesh_solves() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
let n2 = mesh.add_node(0.5, 1.0);
mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
mesh.set_boundary_condition(n1, DofType::Displacement, 0.0);
mesh.add_element(vec![n0, n1, n2], steel(), ElementType::Triangle2D);
let solver = FemSolver::with_params(1e-4, 0.01);
let loads = vec![NodalLoad {
node_id: n2,
fx: 0.0,
fy: -500.0,
}];
let sol = solver.solve_static(&mesh, &loads);
assert!(sol.converged);
assert_eq!(sol.displacements.len(), 3);
}
#[test]
fn test_quad2d_mesh_solves() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
let n2 = mesh.add_node(1.0, 1.0);
let n3 = mesh.add_node(0.0, 1.0);
mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
mesh.set_boundary_condition(n1, DofType::Displacement, 0.0);
mesh.add_element(vec![n0, n1, n2, n3], steel(), ElementType::Quad2D);
let solver = FemSolver::with_params(1e-4, 0.01);
let loads = vec![
NodalLoad {
node_id: n2,
fx: 0.0,
fy: -1000.0,
},
NodalLoad {
node_id: n3,
fx: 0.0,
fy: -1000.0,
},
];
let sol = solver.solve_static(&mesh, &loads);
assert!(sol.converged);
assert_eq!(sol.displacements.len(), 4);
}
#[test]
fn test_thermal_1d_linear_temperature() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Temperature, 100.0);
mesh.set_boundary_condition(n1, DofType::Temperature, 200.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
let solver = FemSolver::with_params(1e-4, 0.01);
let sol = solver.solve_thermal(&mesh, &[]);
assert!(sol.converged);
assert!((sol.temperatures[0] - 100.0).abs() < 1.0);
assert!((sol.temperatures[1] - 200.0).abs() < 1.0);
}
#[test]
fn test_thermal_max_temperature() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Temperature, 300.0);
mesh.set_boundary_condition(n1, DofType::Temperature, 500.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
let solver = FemSolver::with_params(1e-4, 0.01);
let sol = solver.solve_thermal(&mesh, &[]);
assert!(sol.converged);
assert!((sol.max_temperature - 500.0).abs() < 10.0);
}
#[test]
fn test_thermal_heat_flux_direction() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Temperature, 400.0);
mesh.set_boundary_condition(n1, DofType::Temperature, 300.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
let solver = FemSolver::with_params(1e-4, 0.01);
let sol = solver.solve_thermal(&mesh, &[]);
assert!(sol.converged);
assert!(
sol.heat_flux[0].0 > 0.0,
"Heat should flow from hot to cold (positive qx)"
);
}
#[test]
fn test_thermal_three_node_rod() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(0.5, 0.0);
let n2 = mesh.add_node(1.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Temperature, 100.0);
mesh.set_boundary_condition(n2, DofType::Temperature, 200.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
mesh.add_element(vec![n1, n2], steel(), ElementType::Bar1D);
let solver = FemSolver::with_params(1e-4, 0.01);
let sol = solver.solve_thermal(&mesh, &[]);
assert!(sol.converged);
assert!((sol.temperatures[1] - 150.0).abs() < 5.0);
}
#[test]
fn test_thermal_heat_flux_applied() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Temperature, 300.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Bar1D);
let solver = FemSolver::with_params(1e-4, 0.01);
let hf = vec![ElementHeatFlux {
element_id: 0,
q: 1000.0,
}];
let sol = solver.solve_thermal(&mesh, &hf);
assert!(sol.converged);
assert!(sol.temperatures[1] >= 300.0 - 1.0);
}
#[test]
fn test_element_heat_flux_struct() {
let hf = ElementHeatFlux {
element_id: 3,
q: 500.0,
};
assert_eq!(hf.element_id, 3);
assert!((hf.q - 500.0).abs() < 1e-10);
}
#[test]
fn test_nodal_load_struct() {
let load = NodalLoad {
node_id: 2,
fx: 100.0,
fy: -50.0,
};
assert_eq!(load.node_id, 2);
assert!((load.fx - 100.0).abs() < 1e-10);
assert!((load.fy + 50.0).abs() < 1e-10);
}
#[test]
fn test_fem_material_default() {
let mat = FemMaterial::default();
assert!(mat.youngs_modulus > 0.0);
assert!(mat.poissons_ratio > 0.0 && mat.poissons_ratio < 0.5);
assert!(mat.thermal_conductivity > 0.0);
assert!(mat.density > 0.0);
}
#[test]
fn test_gaussian_elimination_identity() {
let mut a = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let mut b = vec![3.0, 7.0];
let x = gaussian_elimination(&mut a, &mut b).expect("Should converge");
assert!((x[0] - 3.0).abs() < 1e-10);
assert!((x[1] - 7.0).abs() < 1e-10);
}
#[test]
fn test_gaussian_elimination_2x2() {
let mut a = vec![vec![2.0, 1.0], vec![1.0, 3.0]];
let mut b = vec![5.0, 10.0];
let x = gaussian_elimination(&mut a, &mut b).expect("Should converge");
assert!((x[0] - 1.0).abs() < 1e-10);
assert!((x[1] - 3.0).abs() < 1e-10);
}
#[test]
fn test_beam1d_solves_like_bar() {
let mut mesh = FemMesh::new();
let n0 = mesh.add_node(0.0, 0.0);
let n1 = mesh.add_node(1.0, 0.0);
mesh.set_boundary_condition(n0, DofType::Displacement, 0.0);
mesh.add_element(vec![n0, n1], steel(), ElementType::Beam1D);
let solver = FemSolver::new();
let loads = vec![NodalLoad {
node_id: n1,
fx: 5000.0,
fy: 0.0,
}];
let sol = solver.solve_static(&mesh, &loads);
assert!(sol.converged);
assert!(sol.max_displacement > 0.0);
}
}