#![allow(missing_docs)]
#[path = "regression_harness.rs"]
mod harness;
use oxiphysics_core::math::Vec3;
use oxiphysics_fem::analysis::LinearStaticAnalysis;
use oxiphysics_fem::boundary::{DirichletBc, NeumannBc};
use oxiphysics_fem::constitutive::LinearElasticMaterial;
use oxiphysics_fem::mesh::TetrahedralMesh;
const BEAM_LENGTH: f64 = 1.0; const BEAM_WIDTH: f64 = 0.1; const BEAM_HEIGHT: f64 = 0.1; const YOUNG_MODULUS: f64 = 210.0e9; const POISSON_RATIO: f64 = 0.3;
const TIP_LOAD: f64 = -1000.0; const COORD_TOL: f64 = 1e-9;
fn analytical_inertia() -> f64 {
BEAM_WIDTH * BEAM_HEIGHT.powi(3) / 12.0
}
fn analytical_tip_deflection() -> f64 {
TIP_LOAD.abs() * BEAM_LENGTH.powi(3) / (3.0 * YOUNG_MODULUS * analytical_inertia())
}
fn solve_cantilever_tip_deflection(nx: usize, ny: usize, nz: usize) -> f64 {
let mesh = TetrahedralMesh::generate_beam(BEAM_LENGTH, BEAM_WIDTH, BEAM_HEIGHT, nx, ny, nz);
let material = LinearElasticMaterial::new(YOUNG_MODULUS, POISSON_RATIO);
let mut dirichlet_bcs: Vec<DirichletBc> = Vec::new();
for (i, node) in mesh.nodes.iter().enumerate() {
if node.x.abs() < COORD_TOL {
dirichlet_bcs.push(DirichletBc::new(i, 0, 0.0));
dirichlet_bcs.push(DirichletBc::new(i, 1, 0.0));
dirichlet_bcs.push(DirichletBc::new(i, 2, 0.0));
}
}
let tip_nodes: Vec<usize> = mesh
.nodes
.iter()
.enumerate()
.filter(|(_, n)| (n.x - BEAM_LENGTH).abs() < COORD_TOL)
.map(|(i, _)| i)
.collect();
assert!(
!tip_nodes.is_empty(),
"no tip nodes found at x = L = {BEAM_LENGTH}"
);
let n_tip = tip_nodes.len() as f64;
let neumann_bcs: Vec<NeumannBc> = tip_nodes
.iter()
.map(|&i| NeumannBc::new(i, [0.0, TIP_LOAD / n_tip, 0.0]))
.collect();
let analysis = LinearStaticAnalysis {
max_iter: 200_000,
tolerance: 1e-12,
};
let result = analysis.solve(
&mesh,
&material,
&dirichlet_bcs,
&neumann_bcs,
&Vec3::new(0.0, 0.0, 0.0),
);
let min_y = tip_nodes
.iter()
.map(|&i| result.displacements[i].y)
.fold(f64::INFINITY, f64::min);
assert!(
min_y.is_finite(),
"solver returned non-finite tip displacement: {min_y}"
);
assert!(
min_y < 0.0,
"tip should deflect downward (negative y), got {min_y:.6e}"
);
min_y.abs()
}
fn relative_error(measured: f64, analytical: f64) -> f64 {
((measured - analytical) / analytical).abs()
}
#[test]
fn test_cantilever_deflection() {
let measured = solve_cantilever_tip_deflection(10, 2, 2);
let analytical = analytical_tip_deflection();
let rel_err = relative_error(measured, analytical);
let baseline = harness::load_baseline("fem_cantilever_deflection")
.expect("fem_cantilever_deflection baseline must exist");
eprintln!(
"[test_cantilever_deflection] mesh = 10x2x2 (200 tets), \
measured = {measured:.9e} m, analytical = {analytical:.9e} m, \
rel_err = {:.3}% (baseline tol_rel = {:.1}%)",
rel_err * 100.0,
baseline.tolerance_rel * 100.0,
);
assert_close!(measured, baseline);
}
#[test]
fn test_cantilever_h_refinement() {
let analytical = analytical_tip_deflection();
let coarse = solve_cantilever_tip_deflection(10, 2, 2);
let fine = solve_cantilever_tip_deflection(20, 4, 4);
let coarse_err = relative_error(coarse, analytical);
let fine_err = relative_error(fine, analytical);
eprintln!(
"[test_cantilever_h_refinement] analytical = {analytical:.9e} m\n \
coarse (10x2x2, 200 tets): measured = {coarse:.9e} m, rel_err = {:.3}%\n \
fine (20x4x4, 1600 tets): measured = {fine:.9e} m, rel_err = {:.3}%",
coarse_err * 100.0,
fine_err * 100.0,
);
assert!(
fine_err < coarse_err,
"h-refinement is not monotone: \
fine_err = {:.3e} (20x4x4) should be strictly less than \
coarse_err = {:.3e} (10x2x2). \
analytical = {analytical:.9e}, coarse = {coarse:.9e}, fine = {fine:.9e}.",
fine_err,
coarse_err,
);
}