use oxiphysics_core::math::Vec3;
pub struct LinearTetrahedron;
pub type TetrahedralElement = LinearTetrahedron;
impl LinearTetrahedron {
pub fn volume(nodes: &[Vec3; 4]) -> f64 {
let x10 = nodes[1] - nodes[0];
let x20 = nodes[2] - nodes[0];
let x30 = nodes[3] - nodes[0];
let det = x10.x * (x20.y * x30.z - x20.z * x30.y) - x10.y * (x20.x * x30.z - x20.z * x30.x)
+ x10.z * (x20.x * x30.y - x20.y * x30.x);
det.abs() / 6.0
}
pub fn b_matrix(nodes: &[Vec3; 4]) -> ([f64; 72], f64) {
let x10 = nodes[1] - nodes[0];
let x20 = nodes[2] - nodes[0];
let x30 = nodes[3] - nodes[0];
let det_j = x10.x * (x20.y * x30.z - x20.z * x30.y)
- x10.y * (x20.x * x30.z - x20.z * x30.x)
+ x10.z * (x20.x * x30.y - x20.y * x30.x);
let vol = det_j / 6.0;
let inv_6v = 1.0 / det_j;
let a11 = x20.y * x30.z - x20.z * x30.y;
let a12 = -(x20.x * x30.z - x20.z * x30.x);
let a13 = x20.x * x30.y - x20.y * x30.x;
let a21 = -(x10.y * x30.z - x10.z * x30.y);
let a22 = x10.x * x30.z - x10.z * x30.x;
let a23 = -(x10.x * x30.y - x10.y * x30.x);
let a31 = x10.y * x20.z - x10.z * x20.y;
let a32 = -(x10.x * x20.z - x10.z * x20.x);
let a33 = x10.x * x20.y - x10.y * x20.x;
let dn1dx = a11 * inv_6v;
let dn1dy = a12 * inv_6v;
let dn1dz = a13 * inv_6v;
let dn2dx = a21 * inv_6v;
let dn2dy = a22 * inv_6v;
let dn2dz = a23 * inv_6v;
let dn3dx = a31 * inv_6v;
let dn3dy = a32 * inv_6v;
let dn3dz = a33 * inv_6v;
let dn0dx = -(dn1dx + dn2dx + dn3dx);
let dn0dy = -(dn1dy + dn2dy + dn3dy);
let dn0dz = -(dn1dz + dn2dz + dn3dz);
let dnx = [dn0dx, dn1dx, dn2dx, dn3dx];
let dny = [dn0dy, dn1dy, dn2dy, dn3dy];
let dnz = [dn0dz, dn1dz, dn2dz, dn3dz];
let mut b = [0.0f64; 72];
for i in 0..4 {
let col = i * 3;
b[col] = dnx[i];
b[12 + col + 1] = dny[i];
b[2 * 12 + col + 2] = dnz[i];
b[3 * 12 + col] = dny[i];
b[3 * 12 + col + 1] = dnx[i];
b[4 * 12 + col + 1] = dnz[i];
b[4 * 12 + col + 2] = dny[i];
b[5 * 12 + col] = dnz[i];
b[5 * 12 + col + 2] = dnx[i];
}
(b, vol)
}
pub fn element_stiffness(nodes: &[Vec3; 4], d_matrix: &[[f64; 6]; 6]) -> [[f64; 12]; 12] {
let (b, vol) = Self::b_matrix(nodes);
let vol_abs = vol.abs();
let mut db = [0.0f64; 72];
for i in 0..6 {
for j in 0..12 {
let mut sum = 0.0;
for k in 0..6 {
sum += d_matrix[i][k] * b[k * 12 + j];
}
db[i * 12 + j] = sum;
}
}
let mut ke = [[0.0f64; 12]; 12];
for i in 0..12 {
for j in 0..12 {
let mut sum = 0.0;
for k in 0..6 {
sum += b[k * 12 + i] * db[k * 12 + j];
}
ke[i][j] = vol_abs * sum;
}
}
ke
}
pub fn shape_functions(xi: f64, eta: f64, zeta: f64) -> [f64; 4] {
[1.0 - xi - eta - zeta, xi, eta, zeta]
}
pub fn load_vector(nodes: &[Vec3; 4], body_force: [f64; 3]) -> [f64; 12] {
let vol = Self::volume(nodes);
let f_node = vol / 4.0;
let mut f = [0.0f64; 12];
for i in 0..4 {
f[i * 3] = f_node * body_force[0];
f[i * 3 + 1] = f_node * body_force[1];
f[i * 3 + 2] = f_node * body_force[2];
}
f
}
}
pub struct HexahedralElement;
impl HexahedralElement {
pub fn shape_functions(xi: f64, eta: f64, zeta: f64) -> [f64; 8] {
let nodes = [
(-1.0, -1.0, -1.0), (1.0, -1.0, -1.0), (1.0, 1.0, -1.0), (-1.0, 1.0, -1.0), (-1.0, -1.0, 1.0), (1.0, -1.0, 1.0), (1.0, 1.0, 1.0), (-1.0, 1.0, 1.0), ];
let mut n = [0.0; 8];
for (i, &(xi_i, eta_i, zeta_i)) in nodes.iter().enumerate() {
n[i] = 0.125 * (1.0 + xi_i * xi) * (1.0 + eta_i * eta) * (1.0 + zeta_i * zeta);
}
n
}
pub fn shape_function_derivatives(
xi: f64,
eta: f64,
zeta: f64,
) -> ([f64; 8], [f64; 8], [f64; 8]) {
let nodes = [
(-1.0, -1.0, -1.0),
(1.0, -1.0, -1.0),
(1.0, 1.0, -1.0),
(-1.0, 1.0, -1.0),
(-1.0, -1.0, 1.0),
(1.0, -1.0, 1.0),
(1.0, 1.0, 1.0),
(-1.0, 1.0, 1.0),
];
let mut dn_dxi = [0.0; 8];
let mut dn_deta = [0.0; 8];
let mut dn_dzeta = [0.0; 8];
for (i, &(xi_i, eta_i, zeta_i)) in nodes.iter().enumerate() {
dn_dxi[i] = 0.125 * xi_i * (1.0 + eta_i * eta) * (1.0 + zeta_i * zeta);
dn_deta[i] = 0.125 * (1.0 + xi_i * xi) * eta_i * (1.0 + zeta_i * zeta);
dn_dzeta[i] = 0.125 * (1.0 + xi_i * xi) * (1.0 + eta_i * eta) * zeta_i;
}
(dn_dxi, dn_deta, dn_dzeta)
}
pub fn jacobian(nodes: &[[f64; 3]; 8], xi: f64, eta: f64, zeta: f64) -> [[f64; 3]; 3] {
let (dn_dxi, dn_deta, dn_dzeta) = Self::shape_function_derivatives(xi, eta, zeta);
let mut j = [[0.0; 3]; 3];
for n in 0..8 {
for d in 0..3 {
j[0][d] += dn_dxi[n] * nodes[n][d];
j[1][d] += dn_deta[n] * nodes[n][d];
j[2][d] += dn_dzeta[n] * nodes[n][d];
}
}
j
}
pub fn det3x3(m: &[[f64; 3]; 3]) -> f64 {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
}
pub fn inv3x3(m: &[[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
let det = Self::det3x3(m);
if det.abs() < 1e-30 {
return None;
}
let inv_det = 1.0 / det;
Some([
[
(m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
(m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
(m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
],
[
(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
(m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
(m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
],
[
(m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
(m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
(m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
],
])
}
pub fn volume(nodes: &[[f64; 3]; 8]) -> f64 {
let gp = [-1.0 / 3.0_f64.sqrt(), 1.0 / 3.0_f64.sqrt()];
let gw = [1.0, 1.0];
let mut vol = 0.0;
for (i, &xi) in gp.iter().enumerate() {
for (j, &eta) in gp.iter().enumerate() {
for (k, &zeta) in gp.iter().enumerate() {
let jac = Self::jacobian(nodes, xi, eta, zeta);
let det_j = Self::det3x3(&jac);
vol += gw[i] * gw[j] * gw[k] * det_j.abs();
}
}
}
vol
}
}
pub struct SerendipityElement;
impl SerendipityElement {
pub fn shape_functions(xi: f64, eta: f64, zeta: f64) -> [f64; 20] {
let corners: [(f64, f64, f64); 8] = [
(-1.0, -1.0, -1.0),
(1.0, -1.0, -1.0),
(1.0, 1.0, -1.0),
(-1.0, 1.0, -1.0),
(-1.0, -1.0, 1.0),
(1.0, -1.0, 1.0),
(1.0, 1.0, 1.0),
(-1.0, 1.0, 1.0),
];
let midedges: [(f64, f64, f64); 12] = [
(0.0, -1.0, -1.0), (1.0, 0.0, -1.0), (0.0, 1.0, -1.0), (-1.0, 0.0, -1.0), (0.0, -1.0, 1.0), (1.0, 0.0, 1.0), (0.0, 1.0, 1.0), (-1.0, 0.0, 1.0), (-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0), ];
let mut n = [0.0; 20];
for (i, &(xi_i, eta_i, zeta_i)) in corners.iter().enumerate() {
let f1 = 1.0 + xi_i * xi;
let f2 = 1.0 + eta_i * eta;
let f3 = 1.0 + zeta_i * zeta;
n[i] = 0.125 * f1 * f2 * f3 * (xi_i * xi + eta_i * eta + zeta_i * zeta - 2.0);
}
for (idx, &(xi_m, eta_m, zeta_m)) in midedges.iter().enumerate() {
let i = idx + 8;
if xi_m.abs() < 1e-14 {
n[i] = 0.25 * (1.0 - xi * xi) * (1.0 + eta_m * eta) * (1.0 + zeta_m * zeta);
} else if eta_m.abs() < 1e-14 {
n[i] = 0.25 * (1.0 + xi_m * xi) * (1.0 - eta * eta) * (1.0 + zeta_m * zeta);
} else {
n[i] = 0.25 * (1.0 + xi_m * xi) * (1.0 + eta_m * eta) * (1.0 - zeta * zeta);
}
}
n
}
pub fn verify_partition_of_unity(xi: f64, eta: f64, zeta: f64) -> f64 {
let n = Self::shape_functions(xi, eta, zeta);
let sum: f64 = n.iter().sum();
(sum - 1.0).abs()
}
}
pub fn jacobian_condition_number(j: &[[f64; 3]; 3]) -> f64 {
let mut frob2 = 0.0;
for row in j {
for &val in row {
frob2 += val * val;
}
}
let frob = frob2.sqrt();
let det = HexahedralElement::det3x3(j);
if det.abs() < 1e-30 {
return f64::MAX;
}
let inv = match HexahedralElement::inv3x3(j) {
Some(inv) => inv,
None => return f64::MAX,
};
let mut inv_frob2 = 0.0;
for row in &inv {
for &val in row {
inv_frob2 += val * val;
}
}
let inv_frob = inv_frob2.sqrt();
frob * inv_frob / 3.0 }
pub fn tet_aspect_ratio(nodes: &[[f64; 3]; 4]) -> f64 {
let edges = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
let mut min_len = f64::MAX;
let mut max_len = 0.0_f64;
for &(i, j) in &edges {
let dx = nodes[j][0] - nodes[i][0];
let dy = nodes[j][1] - nodes[i][1];
let dz = nodes[j][2] - nodes[i][2];
let len = (dx * dx + dy * dy + dz * dz).sqrt();
if len < min_len {
min_len = len;
}
if len > max_len {
max_len = len;
}
}
if min_len < 1e-30 {
return f64::MAX;
}
max_len / min_len
}
pub fn tet_skewness(nodes: &[[f64; 3]; 4]) -> f64 {
let edges = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
let mut sum_len = 0.0;
for &(i, j) in &edges {
let dx = nodes[j][0] - nodes[i][0];
let dy = nodes[j][1] - nodes[i][1];
let dz = nodes[j][2] - nodes[i][2];
sum_len += (dx * dx + dy * dy + dz * dz).sqrt();
}
let avg_len = sum_len / 6.0;
let v_ideal = avg_len * avg_len * avg_len / (6.0 * 2.0_f64.sqrt());
let x10 = [
nodes[1][0] - nodes[0][0],
nodes[1][1] - nodes[0][1],
nodes[1][2] - nodes[0][2],
];
let x20 = [
nodes[2][0] - nodes[0][0],
nodes[2][1] - nodes[0][1],
nodes[2][2] - nodes[0][2],
];
let x30 = [
nodes[3][0] - nodes[0][0],
nodes[3][1] - nodes[0][1],
nodes[3][2] - nodes[0][2],
];
let det = x10[0] * (x20[1] * x30[2] - x20[2] * x30[1])
- x10[1] * (x20[0] * x30[2] - x20[2] * x30[0])
+ x10[2] * (x20[0] * x30[1] - x20[1] * x30[0]);
let vol = det.abs() / 6.0;
if v_ideal < 1e-30 {
return 1.0;
}
(1.0 - vol / v_ideal).clamp(0.0, 1.0)
}
pub fn tet_radius_ratio(nodes: &[[f64; 3]; 4]) -> f64 {
let x10 = [
nodes[1][0] - nodes[0][0],
nodes[1][1] - nodes[0][1],
nodes[1][2] - nodes[0][2],
];
let x20 = [
nodes[2][0] - nodes[0][0],
nodes[2][1] - nodes[0][1],
nodes[2][2] - nodes[0][2],
];
let x30 = [
nodes[3][0] - nodes[0][0],
nodes[3][1] - nodes[0][1],
nodes[3][2] - nodes[0][2],
];
let det = x10[0] * (x20[1] * x30[2] - x20[2] * x30[1])
- x10[1] * (x20[0] * x30[2] - x20[2] * x30[0])
+ x10[2] * (x20[0] * x30[1] - x20[1] * x30[0]);
let vol = det.abs() / 6.0;
if vol < 1e-30 {
return 0.0;
}
let face_area = |a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]| -> f64 {
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let cross = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
0.5 * (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt()
};
let a0 = face_area(&nodes[0], &nodes[1], &nodes[2]);
let a1 = face_area(&nodes[0], &nodes[1], &nodes[3]);
let a2 = face_area(&nodes[0], &nodes[2], &nodes[3]);
let a3 = face_area(&nodes[1], &nodes[2], &nodes[3]);
let total_area = a0 + a1 + a2 + a3;
let r_in = 3.0 * vol / total_area;
let edges = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
let mut edge_lens = [0.0; 6];
for (idx, &(i, j)) in edges.iter().enumerate() {
let dx = nodes[j][0] - nodes[i][0];
let dy = nodes[j][1] - nodes[i][1];
let dz = nodes[j][2] - nodes[i][2];
edge_lens[idx] = (dx * dx + dy * dy + dz * dz).sqrt();
}
let max_edge = edge_lens.iter().cloned().fold(0.0_f64, f64::max);
let r_circ_approx = max_edge / 2.0;
if r_circ_approx < 1e-30 {
return 0.0;
}
(3.0 * r_in / r_circ_approx).min(1.0)
}
pub fn nodal_stress_averaging(
num_nodes: usize,
elements: &[[usize; 4]],
element_stresses: &[[f64; 6]],
) -> Vec<[f64; 6]> {
let mut nodal_stress = vec![[0.0_f64; 6]; num_nodes];
let mut nodal_count = vec![0usize; num_nodes];
for (elem, stress) in elements.iter().zip(element_stresses.iter()) {
for &node in elem {
for c in 0..6 {
nodal_stress[node][c] += stress[c];
}
nodal_count[node] += 1;
}
}
for i in 0..num_nodes {
if nodal_count[i] > 0 {
let inv = 1.0 / nodal_count[i] as f64;
for s in &mut nodal_stress[i] {
*s *= inv;
}
}
}
nodal_stress
}
pub fn von_mises_stress(sigma: &[f64; 6]) -> f64 {
let s = sigma;
let vm2 = 0.5 * ((s[0] - s[1]).powi(2) + (s[1] - s[2]).powi(2) + (s[2] - s[0]).powi(2))
+ 3.0 * (s[3] * s[3] + s[4] * s[4] + s[5] * s[5]);
vm2.sqrt()
}
pub fn hydrostatic_stress(sigma: &[f64; 6]) -> f64 {
(sigma[0] + sigma[1] + sigma[2]) / 3.0
}
pub fn deviatoric_stress(sigma: &[f64; 6]) -> [f64; 6] {
let p = hydrostatic_stress(sigma);
[
sigma[0] - p,
sigma[1] - p,
sigma[2] - p,
sigma[3],
sigma[4],
sigma[5],
]
}
pub fn extrapolate_to_nodes_tet(centroid_value: &[f64; 6]) -> [[f64; 6]; 4] {
[*centroid_value; 4]
}
pub fn spr_recovery(target: [f64; 3], centroids: &[[f64; 3]], stresses: &[[f64; 6]]) -> [f64; 6] {
let n = centroids.len();
if n == 0 {
return [0.0; 6];
}
if n == 1 {
return stresses[0];
}
let mut result = [0.0; 6];
for comp in 0..6 {
let mut ata = [[0.0; 4]; 4];
let mut atb = [0.0; 4];
for i in 0..n {
let row = [1.0, centroids[i][0], centroids[i][1], centroids[i][2]];
let b_val = stresses[i][comp];
for r in 0..4 {
for c in 0..4 {
ata[r][c] += row[r] * row[c];
}
atb[r] += row[r] * b_val;
}
}
let coeffs = solve_4x4(&ata, &atb);
result[comp] =
coeffs[0] + coeffs[1] * target[0] + coeffs[2] * target[1] + coeffs[3] * target[2];
}
result
}
fn solve_4x4(a: &[[f64; 4]; 4], b: &[f64; 4]) -> [f64; 4] {
let mut m = [[0.0; 5]; 4]; for i in 0..4 {
for j in 0..4 {
m[i][j] = a[i][j];
}
m[i][4] = b[i];
}
for col in 0..4 {
let mut max_val = m[col][col].abs();
let mut max_row = col;
for (idx, row) in ((col + 1)..4usize).enumerate() {
let _ = idx;
if m[row][col].abs() > max_val {
max_val = m[row][col].abs();
max_row = row;
}
}
if max_val < 1e-30 {
return [0.0; 4]; }
if max_row != col {
m.swap(col, max_row);
}
let pivot = m[col][col];
for row in (col + 1)..4 {
let factor = m[row][col] / pivot;
for (idx, c) in (col..5usize).enumerate() {
let _ = idx;
m[row][c] -= factor * m[col][c];
}
}
}
let mut x = [0.0; 4];
for i in (0..4).rev() {
let mut sum = m[i][4];
for j in (i + 1)..4 {
sum -= m[i][j] * x[j];
}
x[i] = sum / m[i][i];
}
x
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_element_volume() {
let nodes = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
];
let vol = LinearTetrahedron::volume(&nodes);
assert!((vol - 1.0 / 6.0).abs() < 1e-12, "volume = {vol}");
}
#[test]
fn test_element_stiffness_symmetry() {
let nodes = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
];
let e = 200.0e9;
let nu = 0.3;
let d = crate::constitutive::LinearElasticMaterial::new(e, nu).constitutive_matrix();
let ke = LinearTetrahedron::element_stiffness(&nodes, &d);
for (i, row) in ke.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
let diff = (val - ke[j][i]).abs();
let scale = val.abs().max(ke[j][i].abs()).max(1.0);
assert!(
diff / scale < 1e-10,
"Stiffness not symmetric at ({i},{j}): {} vs {}",
val,
ke[j][i]
);
}
}
}
#[test]
fn test_shape_functions_partition_of_unity() {
let test_points = [
(0.0, 0.0, 0.0),
(1.0, 0.0, 0.0),
(0.0, 1.0, 0.0),
(0.0, 0.0, 1.0),
(0.25, 0.25, 0.25),
(0.1, 0.2, 0.3),
];
for (xi, eta, zeta) in test_points {
let n = LinearTetrahedron::shape_functions(xi, eta, zeta);
let sum: f64 = n.iter().sum();
assert!(
(sum - 1.0).abs() < 1e-14,
"Shape functions do not sum to 1 at ({xi},{eta},{zeta}): sum = {sum}"
);
}
}
#[test]
fn test_shape_functions_nodal_values() {
let n0 = LinearTetrahedron::shape_functions(0.0, 0.0, 0.0);
assert!((n0[0] - 1.0).abs() < 1e-14);
assert!(n0[1].abs() < 1e-14);
assert!(n0[2].abs() < 1e-14);
assert!(n0[3].abs() < 1e-14);
let n1 = LinearTetrahedron::shape_functions(1.0, 0.0, 0.0);
assert!(n1[0].abs() < 1e-14);
assert!((n1[1] - 1.0).abs() < 1e-14);
let n3 = LinearTetrahedron::shape_functions(0.0, 0.0, 1.0);
assert!(n3[0].abs() < 1e-14);
assert!((n3[3] - 1.0).abs() < 1e-14);
}
#[test]
fn test_load_vector_body_force() {
let nodes = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
];
let body_force = [0.0, -9.81, 0.0]; let f = LinearTetrahedron::load_vector(&nodes, body_force);
let vol = LinearTetrahedron::volume(&nodes); let total_fy: f64 = f.iter().skip(1).step_by(3).sum();
let expected_fy = body_force[1] * vol;
assert!(
(total_fy - expected_fy).abs() < 1e-12,
"total Fy = {total_fy}, expected {expected_fy}"
);
let total_fx: f64 = f.iter().step_by(3).sum();
assert!(
total_fx.abs() < 1e-12,
"total Fx should be zero, got {total_fx}"
);
}
#[test]
fn test_single_element_patch_test_uniform_strain() {
let eps = 0.001;
let nodes = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
];
let u_e = [
0.0 * eps,
0.0,
0.0,
1.0 * eps,
0.0,
0.0,
0.0 * eps,
0.0,
0.0,
0.0 * eps,
0.0,
0.0,
];
let (b, _vol) = LinearTetrahedron::b_matrix(&nodes);
let mut strain = [0.0f64; 6];
for i in 0..6 {
for j in 0..12 {
strain[i] += b[i * 12 + j] * u_e[j];
}
}
assert!(
(strain[0] - eps).abs() < 1e-12,
"eps_xx = {}, expected {eps}",
strain[0]
);
assert!(
strain[1].abs() < 1e-12,
"eps_yy should be 0, got {}",
strain[1]
);
assert!(
strain[2].abs() < 1e-12,
"eps_zz should be 0, got {}",
strain[2]
);
assert!(
strain[3].abs() < 1e-12,
"gamma_xy should be 0, got {}",
strain[3]
);
assert!(
strain[4].abs() < 1e-12,
"gamma_yz should be 0, got {}",
strain[4]
);
assert!(
strain[5].abs() < 1e-12,
"gamma_xz should be 0, got {}",
strain[5]
);
}
#[test]
fn test_element_stiffness_rigid_body_zero_force() {
let nodes = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(0.0, 2.0, 0.0),
Vec3::new(0.0, 0.0, 2.0),
];
let d = crate::constitutive::LinearElasticMaterial::new(1.0e6, 0.25).constitutive_matrix();
let ke = LinearTetrahedron::element_stiffness(&nodes, &d);
let u_rb = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
for (i, row) in ke.iter().enumerate() {
let fi: f64 = row.iter().zip(u_rb.iter()).map(|(&k, &u)| k * u).sum();
assert!(fi.abs() < 1e-6, "K*u_rb[{i}] = {fi:.3e}, should be ~0");
}
}
#[test]
fn test_tetrahedral_element_alias() {
let nodes = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
];
let vol_alias = TetrahedralElement::volume(&nodes);
let vol_direct = LinearTetrahedron::volume(&nodes);
assert!((vol_alias - vol_direct).abs() < 1e-14);
}
#[test]
fn test_hex_shape_functions_partition_of_unity() {
let test_points = [
(0.0, 0.0, 0.0),
(1.0, -1.0, 0.5),
(-0.5, 0.3, -0.7),
(-1.0, -1.0, -1.0),
(1.0, 1.0, 1.0),
];
for (xi, eta, zeta) in test_points {
let n = HexahedralElement::shape_functions(xi, eta, zeta);
let sum: f64 = n.iter().sum();
assert!(
(sum - 1.0).abs() < 1e-14,
"Hex shape functions do not sum to 1 at ({xi},{eta},{zeta}): sum = {sum}"
);
}
}
#[test]
fn test_hex_shape_functions_nodal_values() {
let corners = [
(-1.0, -1.0, -1.0),
(1.0, -1.0, -1.0),
(1.0, 1.0, -1.0),
(-1.0, 1.0, -1.0),
(-1.0, -1.0, 1.0),
(1.0, -1.0, 1.0),
(1.0, 1.0, 1.0),
(-1.0, 1.0, 1.0),
];
for (node_idx, &(xi, eta, zeta)) in corners.iter().enumerate() {
let n = HexahedralElement::shape_functions(xi, eta, zeta);
for (i, &val) in n.iter().enumerate() {
if i == node_idx {
assert!(
(val - 1.0).abs() < 1e-14,
"N_{i} at node {node_idx} should be 1, got {val}"
);
} else {
assert!(
val.abs() < 1e-14,
"N_{i} at node {node_idx} should be 0, got {val}"
);
}
}
}
}
#[test]
fn test_hex_jacobian_unit_cube() {
let nodes = [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
[0.0, 1.0, 1.0],
];
let jac = HexahedralElement::jacobian(&nodes, 0.0, 0.0, 0.0);
let det = HexahedralElement::det3x3(&jac);
assert!(
(det - 0.125).abs() < 1e-12,
"det(J) = {det}, expected 0.125"
);
}
#[test]
fn test_hex_volume_unit_cube() {
let nodes = [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
[0.0, 1.0, 1.0],
];
let vol = HexahedralElement::volume(&nodes);
assert!(
(vol - 1.0).abs() < 1e-12,
"unit cube volume = {vol}, expected 1.0"
);
}
#[test]
fn test_hex_volume_stretched_box() {
let nodes = [
[0.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[2.0, 3.0, 0.0],
[0.0, 3.0, 0.0],
[0.0, 0.0, 4.0],
[2.0, 0.0, 4.0],
[2.0, 3.0, 4.0],
[0.0, 3.0, 4.0],
];
let vol = HexahedralElement::volume(&nodes);
assert!(
(vol - 24.0).abs() < 1e-10,
"box volume = {vol}, expected 24.0"
);
}
#[test]
fn test_inv3x3_identity() {
let id = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let inv = HexahedralElement::inv3x3(&id).unwrap();
for (i, row) in inv.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(val - expected).abs() < 1e-14,
"inv[{i}][{j}] = {}, expected {expected}",
val
);
}
}
}
#[test]
fn test_serendipity_partition_of_unity() {
let pts = [
(0.0, 0.0, 0.0),
(0.5, -0.3, 0.7),
(-1.0, -1.0, -1.0),
(1.0, 1.0, 1.0),
(0.0, -1.0, -1.0), ];
for (xi, eta, zeta) in pts {
let err = SerendipityElement::verify_partition_of_unity(xi, eta, zeta);
assert!(
err < 1e-12,
"Serendipity partition of unity error = {err} at ({xi},{eta},{zeta})"
);
}
}
#[test]
fn test_serendipity_corner_nodes() {
let corners = [
(-1.0, -1.0, -1.0),
(1.0, -1.0, -1.0),
(1.0, 1.0, -1.0),
(-1.0, 1.0, -1.0),
(-1.0, -1.0, 1.0),
(1.0, -1.0, 1.0),
(1.0, 1.0, 1.0),
(-1.0, 1.0, 1.0),
];
for (node_idx, &(xi, eta, zeta)) in corners.iter().enumerate() {
let n = SerendipityElement::shape_functions(xi, eta, zeta);
assert!(
(n[node_idx] - 1.0).abs() < 1e-12,
"Corner node {node_idx}: N = {}, expected 1.0",
n[node_idx]
);
for (i, &ni) in n.iter().enumerate().skip(8).take(12) {
assert!(
ni.abs() < 1e-12,
"Mid-edge N_{i} at corner {node_idx} = {}, expected 0",
ni
);
}
}
}
#[test]
fn test_tet_aspect_ratio_regular() {
let nodes = [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 3.0_f64.sqrt() / 2.0, 0.0],
[0.5, 3.0_f64.sqrt() / 6.0, (2.0_f64 / 3.0).sqrt()],
];
let ar = tet_aspect_ratio(&nodes);
assert!(
(ar - 1.0).abs() < 0.05,
"regular tet aspect ratio = {ar}, expected ~1.0"
);
}
#[test]
fn test_tet_aspect_ratio_elongated() {
let nodes = [
[0.0, 0.0, 0.0],
[10.0, 0.0, 0.0],
[0.0, 0.1, 0.0],
[0.0, 0.0, 0.1],
];
let ar = tet_aspect_ratio(&nodes);
assert!(
ar > 5.0,
"elongated tet should have high aspect ratio, got {ar}"
);
}
#[test]
fn test_tet_skewness_regular() {
let nodes = [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 3.0_f64.sqrt() / 2.0, 0.0],
[0.5, 3.0_f64.sqrt() / 6.0, (2.0_f64 / 3.0).sqrt()],
];
let sk = tet_skewness(&nodes);
assert!(sk < 0.1, "regular tet skewness should be near 0, got {sk}");
}
#[test]
fn test_tet_radius_ratio_regular() {
let nodes = [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 3.0_f64.sqrt() / 2.0, 0.0],
[0.5, 3.0_f64.sqrt() / 6.0, (2.0_f64 / 3.0).sqrt()],
];
let rr = tet_radius_ratio(&nodes);
assert!(
rr > 0.5,
"regular tet radius ratio should be high, got {rr}"
);
}
#[test]
fn test_jacobian_condition_number_identity() {
let jac = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let cn = jacobian_condition_number(&jac);
assert!(
(cn - 1.0).abs() < 1e-10,
"identity Jacobian condition number = {cn}, expected 1.0"
);
}
#[test]
fn test_jacobian_condition_number_scaled() {
let jac = [[3.0, 0.0, 0.0], [0.0, 3.0, 0.0], [0.0, 0.0, 3.0]];
let cn = jacobian_condition_number(&jac);
assert!(
(cn - 1.0).abs() < 1e-10,
"scaled identity condition number = {cn}, expected 1.0"
);
}
#[test]
fn test_nodal_stress_averaging_single_element() {
let elements = vec![[0, 1, 2, 3]];
let stresses = vec![[100.0, 50.0, 30.0, 10.0, 5.0, 3.0]];
let nodal = nodal_stress_averaging(4, &elements, &stresses);
for (node, row) in nodal.iter().enumerate() {
for (c, &val) in row.iter().enumerate() {
assert!(
(val - stresses[0][c]).abs() < 1e-12,
"node {node} comp {c}: {} vs {}",
val,
stresses[0][c]
);
}
}
}
#[test]
fn test_nodal_stress_averaging_two_elements() {
let elements = vec![[0, 1, 2, 3], [1, 4, 5, 6]];
let stresses = vec![
[100.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[200.0, 0.0, 0.0, 0.0, 0.0, 0.0],
];
let nodal = nodal_stress_averaging(7, &elements, &stresses);
assert!(
(nodal[1][0] - 150.0).abs() < 1e-12,
"node 1 sig_xx = {}, expected 150",
nodal[1][0]
);
assert!(
(nodal[0][0] - 100.0).abs() < 1e-12,
"node 0 sig_xx = {}, expected 100",
nodal[0][0]
);
}
#[test]
fn test_von_mises_uniaxial() {
let sigma = [100.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let vm = von_mises_stress(&sigma);
assert!(
(vm - 100.0).abs() < 1e-10,
"uniaxial von Mises = {vm}, expected 100"
);
}
#[test]
fn test_von_mises_pure_shear() {
let sigma = [0.0, 0.0, 0.0, 50.0, 0.0, 0.0];
let vm = von_mises_stress(&sigma);
let expected = (3.0_f64 * 50.0 * 50.0).sqrt();
assert!(
(vm - expected).abs() < 1e-10,
"pure shear von Mises = {vm}, expected {expected}"
);
}
#[test]
fn test_hydrostatic_stress() {
let sigma = [100.0, 200.0, 300.0, 0.0, 0.0, 0.0];
let p = hydrostatic_stress(&sigma);
assert!((p - 200.0).abs() < 1e-12, "hydrostatic = {p}, expected 200");
}
#[test]
fn test_deviatoric_zero_trace() {
let sigma = [100.0, 200.0, 300.0, 10.0, 20.0, 30.0];
let dev = deviatoric_stress(&sigma);
let trace = dev[0] + dev[1] + dev[2];
assert!(
trace.abs() < 1e-12,
"deviatoric trace = {trace}, should be 0"
);
assert!((dev[3] - 10.0).abs() < 1e-12);
assert!((dev[4] - 20.0).abs() < 1e-12);
assert!((dev[5] - 30.0).abs() < 1e-12);
}
#[test]
fn test_spr_recovery_single() {
let target = [0.0, 0.0, 0.0];
let centroids = vec![[0.25, 0.25, 0.25]];
let stresses = vec![[100.0, 50.0, 30.0, 10.0, 5.0, 3.0]];
let recovered = spr_recovery(target, ¢roids, &stresses);
for (c, &val) in recovered.iter().enumerate() {
assert!(val.is_finite(), "comp {c} is not finite");
}
}
#[test]
fn test_spr_recovery_constant_field() {
let target = [1.0, 2.0, 3.0];
let centroids = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
];
let constant = [42.0, 21.0, 10.0, 5.0, 2.0, 1.0];
let stresses = vec![constant; 5];
let recovered = spr_recovery(target, ¢roids, &stresses);
for c in 0..6 {
assert!(
(recovered[c] - constant[c]).abs() < 1e-8,
"comp {c}: recovered {} vs expected {}",
recovered[c],
constant[c]
);
}
}
#[test]
fn test_extrapolate_to_nodes_tet() {
let val = [100.0, 50.0, 30.0, 10.0, 5.0, 3.0];
let nodal = extrapolate_to_nodes_tet(&val);
for row in &nodal {
for (c, &nv) in row.iter().enumerate() {
assert!((nv - val[c]).abs() < 1e-14);
}
}
}
}