use alice_physics::linear_elastic_fem::{
error_indicators_squared, mark_bulk, solve, solve_adaptive, AdaptiveConfig, AdaptiveSolution,
Axis, BoundaryConditions, ElasticMaterial, FemSolution, SolverConfig, StressTensor,
};
use alice_physics::math::Fix128;
use alice_physics::sdf_fem_mesh::{SdfTetMesh, Tetrahedron};
const E_MPA: f64 = 204_800.0;
const NU: f64 = 0.25;
const TRACTION: f64 = 64.0;
const CELLS: (usize, usize, usize) = (3, 2, 2);
fn fx(v: f64) -> Fix128 {
Fix128::from_f64(v)
}
fn half() -> Fix128 {
Fix128::from_raw(0, 1 << 63)
}
fn node_index(nx: usize, ny: usize, i: usize, j: usize, k: usize) -> u32 {
u32::try_from(i + j * (nx + 1) + k * (nx + 1) * (ny + 1)).expect("lattice fits u32")
}
fn kuhn_box(nx: usize, ny: usize, nz: usize) -> SdfTetMesh {
let mut mesh = SdfTetMesh::default();
for k in 0..=nz {
for j in 0..=ny {
for i in 0..=nx {
mesh.vertices.push([i as f32, j as f32, k as f32]);
}
}
}
const PATHS: [[usize; 3]; 6] = [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
];
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
for path in PATHS {
let mut step = [0usize; 3];
let mut corners = [0u32; 4];
corners[0] = node_index(nx, ny, i, j, k);
for (n, axis) in path.into_iter().enumerate() {
step[axis] = 1;
corners[n + 1] = node_index(nx, ny, i + step[0], j + step[1], k + step[2]);
}
mesh.tets.push(Tetrahedron { vertices: corners });
}
}
}
}
mesh
}
fn patch_nodes() -> [(u32, f64); 4] {
let (nx, ny, _) = CELLS;
[
(node_index(nx, ny, nx, 0, 0), 1.0 / 3.0),
(node_index(nx, ny, nx, 1, 1), 1.0 / 3.0),
(node_index(nx, ny, nx, 1, 0), 1.0 / 6.0),
(node_index(nx, ny, nx, 0, 1), 1.0 / 6.0),
]
}
fn boundary_for(mesh: &SdfTetMesh) -> BoundaryConditions {
let mut bc = BoundaryConditions::new();
for (v, p) in mesh.vertices.iter().enumerate() {
if p[0].abs() < 0.5 {
let node = u32::try_from(v).expect("fits u32");
bc.prescribe(node, Axis::X, Fix128::ZERO);
bc.prescribe(node, Axis::Y, Fix128::ZERO);
bc.prescribe(node, Axis::Z, Fix128::ZERO);
}
}
for (node, share) in patch_nodes() {
bc.add_load(node, Axis::X, fx(TRACTION * share));
}
bc
}
fn strain_energy(sol: &FemSolution) -> Fix128 {
let mut work = Fix128::ZERO;
for (node, share) in patch_nodes() {
work = work + fx(TRACTION * share) * sol.displacements[node as usize][0];
}
half() * work
}
fn main() {
let (nx, ny, nz) = CELLS;
let base = kuhn_box(nx, ny, nz);
let material = ElasticMaterial::new(fx(E_MPA), fx(NU)).expect("valid steel");
let linear = SolverConfig::default();
println!(
"block {nx}x{ny}x{nz} cells, {} tets, {} nodes; traction {TRACTION} MPa on one face cell\n",
base.tets.len(),
base.vertices.len()
);
let mut reference_mesh = base.clone();
reference_mesh
.try_refine_conforming(0.6, 24)
.expect("reference refinement finishes");
let reference = solve(
&reference_mesh,
&material,
&boundary_for(&reference_mesh),
&linear,
)
.expect("reference solve succeeds");
let reference_energy = strain_energy(&reference);
let mut uniform_mesh = base.clone();
uniform_mesh
.try_refine_conforming(0.95, 24)
.expect("uniform refinement finishes");
let uniform = solve(
&uniform_mesh,
&material,
&boundary_for(&uniform_mesh),
&linear,
)
.expect("uniform solve succeeds");
let config = AdaptiveConfig::try_new(linear, half(), 6, 24).expect("valid adaptive config");
let adaptive: AdaptiveSolution =
solve_adaptive(&base, &material, boundary_for, &config).expect("adaptive solve succeeds");
println!(
"config: θ = {}, at most {} rounds, {} refinement passes each, CG tolerance {:e}",
config.bulk_fraction().to_f64(),
config.max_rounds(),
config.max_refine_passes(),
config.linear().relative_tolerance().to_f64()
);
println!();
println!(
"{:<12} {:>7} {:>7} {:>16} {:>12}",
"run", "tets", "nodes", "energy", "error"
);
let row = |name: &str, mesh: &SdfTetMesh, sol: &FemSolution| {
println!(
"{name:<12} {:>7} {:>7} {:>16.9} {:>12.3e}",
mesh.tets.len(),
mesh.vertices.len(),
strain_energy(sol).to_f64(),
(reference_energy - strain_energy(sol)).abs().to_f64()
);
};
row("reference", &reference_mesh, &reference);
row("uniform", &uniform_mesh, &uniform);
row("adaptive", &adaptive.mesh, &adaptive.field);
println!(
"\nadaptive used {} rounds; total indicator per round: {:?}",
adaptive.rounds,
adaptive
.total_indicator_history
.iter()
.map(|v| v.to_f64())
.collect::<Vec<_>>()
);
println!("\none adaptive step, piece by piece:");
let first = solve(&base, &material, &boundary_for(&base), &linear).expect("solve succeeds");
let indicators =
error_indicators_squared(&base, &material, &first).expect("indicators computed");
let total = indicators.iter().fold(Fix128::ZERO, |a, &b| a + b);
let worst = indicators
.iter()
.enumerate()
.max_by(|a, b| a.1.cmp(b.1))
.expect("non-empty");
println!(
" Σ η² = {:.6}; worst element {} carries {:.1}% of it",
total.to_f64(),
worst.0,
100.0 * worst.1.to_f64() / total.to_f64()
);
let marked = mark_bulk(&indicators, half()).expect("valid marking");
let marked_count = marked.iter().filter(|&&m| m).count();
println!(
" bulk marking at θ = 1/2 chose {marked_count} of {} elements",
marked.len()
);
let mut refined = base.clone();
let passes = refined
.try_refine_marked(&marked, 24)
.expect("refinement finishes");
println!(
" refining them took {passes} passes: {} -> {} tets, {} -> {} nodes",
base.tets.len(),
refined.tets.len(),
base.vertices.len(),
refined.vertices.len()
);
println!(
" ⚠️ more elements split than were marked ({} vs {marked_count}): the extra ones are \
propagation keeping the mesh conforming",
refined.tets.len() - base.tets.len()
);
println!("\nthe energy norm σ:C⁻¹:σ, on three states with closed forms:");
let s = Fix128::from_int(8);
for (name, stress, closed) in [
(
"uniaxial",
StressTensor {
xx: s,
..Default::default()
},
"s²/E",
),
(
"pure shear",
StressTensor {
xy: s,
..Default::default()
},
"2(1+ν)s²/E",
),
(
"hydrostatic",
StressTensor {
xx: s,
yy: s,
zz: s,
..Default::default()
},
"3(1−2ν)p²/E",
),
] {
println!(
" {name:<12} {:.9} ({closed})",
stress.complementary_energy_density(&material).to_f64()
);
}
}