#![allow(missing_docs)]
use oxiphysics::lbm::{LbmGrid3D, lattice::LatticeType, stream_3d};
const GHIA_RE100: [(f64, f64); 17] = [
(1.0000, 1.00000),
(0.9766, 0.84123),
(0.9688, 0.78871),
(0.9609, 0.73722),
(0.9531, 0.68717),
(0.8516, 0.23151),
(0.7344, 0.00332),
(0.6172, -0.13641),
(0.5000, -0.20581),
(0.4531, -0.21090),
(0.2813, -0.15662),
(0.1719, -0.10150),
(0.1016, -0.06434),
(0.0703, -0.04775),
(0.0625, -0.04192),
(0.0547, -0.03717),
(0.0000, 0.00000),
];
const D3Q19_C: [[i32; 3]; 19] = [
[0, 0, 0],
[1, 0, 0],
[-1, 0, 0],
[0, 1, 0],
[0, -1, 0],
[0, 0, 1],
[0, 0, -1],
[1, 1, 0],
[-1, 1, 0],
[1, -1, 0],
[-1, -1, 0],
[1, 0, 1],
[-1, 0, 1],
[1, 0, -1],
[-1, 0, -1],
[0, 1, 1],
[0, -1, 1],
[0, 1, -1],
[0, -1, -1],
];
const D3Q19_W: [f64; 19] = [
1.0 / 3.0,
1.0 / 18.0,
1.0 / 18.0,
1.0 / 18.0,
1.0 / 18.0,
1.0 / 18.0,
1.0 / 18.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
];
const D3Q19_OPP: [usize; 19] = [
0, 2, 1, 4, 3, 6, 5, 10, 9, 8, 7, 14, 13, 12, 11, 18, 17, 16, 15,
];
#[inline]
fn feq_d3q19(w: f64, rho: f64, ux: f64, uy: f64, uz: f64, cx: f64, cy: f64, cz: f64) -> f64 {
let cs2 = 1.0_f64 / 3.0;
let eu = cx * ux + cy * uy + cz * uz;
let u_sq = ux * ux + uy * uy + uz * uz;
w * rho * (1.0 + eu / cs2 + eu * eu / (2.0 * cs2 * cs2) - u_sq / (2.0 * cs2))
}
fn collide_fluid_bgk(grid: &mut LbmGrid3D, omega: f64, is_wall: &[bool]) {
let nx = grid.nx;
let ny = grid.ny;
let nz = grid.nz;
let n = nx * ny * nz;
for (k, &wall) in is_wall.iter().enumerate().take(n) {
if wall {
continue;
}
let rho = grid.rho[k];
let ux = grid.ux[k];
let uy = grid.uy[k];
let uz = grid.uz[k];
for i in 0..19 {
let w = D3Q19_W[i];
let cx = D3Q19_C[i][0] as f64;
let cy = D3Q19_C[i][1] as f64;
let cz = D3Q19_C[i][2] as f64;
let feq = feq_d3q19(w, rho, ux, uy, uz, cx, cy, cz);
grid.f[i][k] -= omega * (grid.f[i][k] - feq);
}
}
}
fn apply_bounce_back(grid: &mut LbmGrid3D, wall_cells: &[usize]) {
for &k in wall_cells {
let mut tmp = [0.0_f64; 19];
for (i, tmp_i) in tmp.iter_mut().enumerate() {
*tmp_i = grid.f[i][k];
}
for (i, f_i) in grid.f.iter_mut().enumerate() {
f_i[k] = tmp[D3Q19_OPP[i]];
}
}
}
fn apply_bounce_back_moving(grid: &mut LbmGrid3D, lid_cells: &[usize], u_wall: [f64; 3]) {
let cs2 = 1.0_f64 / 3.0;
for &k in lid_cells {
let mut tmp = [0.0_f64; 19];
for (i, tmp_i) in tmp.iter_mut().enumerate() {
*tmp_i = grid.f[i][k];
}
let rho = tmp.iter().sum::<f64>();
for (i, f_i) in grid.f.iter_mut().enumerate() {
let opp = D3Q19_OPP[i];
let cx = D3Q19_C[opp][0] as f64;
let cy = D3Q19_C[opp][1] as f64;
let cz = D3Q19_C[opp][2] as f64;
let e_dot_u = cx * u_wall[0] + cy * u_wall[1] + cz * u_wall[2];
let corr = 2.0 * D3Q19_W[opp] * rho * e_dot_u / cs2;
f_i[k] = tmp[opp] + corr;
}
}
}
fn interp_u(ux_profile: &[f64], y_real: f64, u_lid: f64) -> f64 {
if ux_profile.is_empty() || u_lid.abs() < 1e-30 {
return 0.0;
}
let ny = ux_profile.len();
let y_clamped = y_real.clamp(0.0, (ny - 1) as f64);
let y_lo = y_clamped.floor() as usize;
let y_hi = (y_lo + 1).min(ny - 1);
let t = y_clamped - (y_lo as f64);
let u = (1.0 - t) * ux_profile[y_lo] + t * ux_profile[y_hi];
u / u_lid
}
#[test]
#[ignore = "runtime > 60 s: 33^3 × 19 × up to 20 000 LBM steps; run with --ignored"]
fn test_cavity_re100_centerline() {
let nx: usize = 33;
let ny: usize = 33;
let nz: usize = 33;
let l_grid: f64 = (ny - 1) as f64; let u_lid: f64 = 0.1;
let reynolds: f64 = 100.0;
let nu = u_lid * l_grid / reynolds; let omega = 1.0 / (3.0 * nu + 0.5);
debug_assert!(
omega > 1.6 && omega < 1.7,
"ω out of expected band: {omega}"
);
let mut grid = LbmGrid3D::new(nx, ny, nz, LatticeType::D3Q19);
let mut is_wall = vec![false; nx * ny * nz];
let mut static_walls: Vec<usize> = Vec::new();
let mut lid_cells: Vec<usize> = Vec::new();
for z in 0..nz {
for y in 0..ny {
for x in 0..nx {
let k = grid.idx(x, y, z);
let at_bottom = y == 0;
let at_top = y == ny - 1;
let at_xlow = x == 0;
let at_xhigh = x == nx - 1;
let at_zlow = z == 0;
let at_zhigh = z == nz - 1;
let any_bound = at_bottom || at_top || at_xlow || at_xhigh || at_zlow || at_zhigh;
if any_bound {
is_wall[k] = true;
if at_top && !at_xlow && !at_xhigh && !at_zlow && !at_zhigh {
lid_cells.push(k);
} else {
static_walls.push(k);
}
}
}
}
}
for k in 0..nx * ny * nz {
grid.rho[k] = 1.0;
grid.ux[k] = 0.0;
grid.uy[k] = 0.0;
grid.uz[k] = 0.0;
for (i, f_i) in grid.f.iter_mut().enumerate() {
f_i[k] = D3Q19_W[i]; }
}
let u_wall = [u_lid, 0.0, 0.0];
let max_steps = 20_000_usize;
let conv_tol = 1e-5_f64;
let conv_start_step = 500_usize;
let x_mid = nx / 2;
let z_mid = nz / 2;
let mut prev_ux_line: Vec<f64> = (0..ny)
.map(|y| grid.ux[grid.idx(x_mid, y, z_mid)])
.collect();
let mut converged = false;
let mut final_step = max_steps;
let mut final_delta = f64::INFINITY;
for step in 0..max_steps {
collide_fluid_bgk(&mut grid, omega, &is_wall);
stream_3d(&mut grid);
apply_bounce_back(&mut grid, &static_walls);
apply_bounce_back_moving(&mut grid, &lid_cells, u_wall);
grid.compute_macroscopic();
if step >= conv_start_step && step.is_multiple_of(50) {
let curr: Vec<f64> = (0..ny)
.map(|y| grid.ux[grid.idx(x_mid, y, z_mid)])
.collect();
let delta = curr
.iter()
.zip(prev_ux_line.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f64, f64::max)
/ u_lid;
final_delta = delta;
final_step = step + 1;
if delta < conv_tol {
converged = true;
break;
}
prev_ux_line = curr;
}
}
if !converged {
eprintln!(
"[validation_lbm] WARNING: cavity did not fully converge in {max_steps} steps \
(max |Δu|/u_lid = {final_delta:.3e} at step {final_step}); \
proceeding with profile-shape check."
);
}
let ux_line: Vec<f64> = (0..ny)
.map(|y| grid.ux[grid.idx(x_mid, y, z_mid)])
.collect();
let rel_tol = 0.10_f64;
let abs_floor = 0.01_f64;
let mut max_err_rel = 0.0_f64;
let mut worst_point = (0.0, 0.0, 0.0); let mut failed_pairs: Vec<(f64, f64, f64, f64)> = Vec::new();
for &(y_over_l, ref_u) in GHIA_RE100.iter() {
if y_over_l <= 0.0 + 1e-12 || y_over_l >= 1.0 - 1e-12 {
continue;
}
let y_real = y_over_l * l_grid;
let measured = interp_u(&ux_line, y_real, u_lid);
let diff = (measured - ref_u).abs();
let tol = rel_tol * ref_u.abs() + abs_floor;
let rel = if ref_u.abs() > 1e-12 {
diff / ref_u.abs()
} else {
diff
};
if rel > max_err_rel {
max_err_rel = rel;
worst_point = (y_over_l, measured, ref_u);
}
if diff > tol {
failed_pairs.push((y_over_l, measured, ref_u, diff));
}
}
eprintln!(
"[validation_lbm] converged = {converged}, steps = {final_step}, \
final |Δu|/u_lid = {final_delta:.3e}, ω = {omega:.6}, ν = {nu:.6}, \
worst-point y/L = {:.4} measured = {:.5} ref = {:.5} rel_err = {:.3}",
worst_point.0, worst_point.1, worst_point.2, max_err_rel,
);
assert!(
failed_pairs.is_empty(),
"Cavity Re=100 centerline: {} points exceed combined tolerance \
(10% rel + {abs_floor} abs). Failures (y/L, measured, ref, |Δ|): {:?}. \
max rel err = {:.3} at y/L = {:.4} (measured = {:.5}, ref = {:.5}).",
failed_pairs.len(),
failed_pairs,
max_err_rel,
worst_point.0,
worst_point.1,
worst_point.2,
);
}