use crate::error::{IntegrateError, IntegrateResult};
const Q9: usize = 9;
const CX9: [i32; Q9] = [0, 1, 0, -1, 0, 1, -1, -1, 1];
const CY9: [i32; Q9] = [0, 0, 1, 0, -1, 1, 1, -1, -1];
const W9: [f64; Q9] = [
4.0 / 9.0,
1.0 / 9.0,
1.0 / 9.0,
1.0 / 9.0,
1.0 / 9.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
1.0 / 36.0,
];
const OPP9: [usize; Q9] = [0, 3, 4, 1, 2, 7, 8, 5, 6];
const Q19: usize = 19;
const CX19: [i32; Q19] = [0, 1, -1, 0, 0, 0, 0, 1, -1, 1, -1, 1, -1, 1, -1, 0, 0, 0, 0];
const CY19: [i32; Q19] = [0, 0, 0, 1, -1, 0, 0, 1, 1, -1, -1, 0, 0, 0, 0, 1, -1, 1, -1];
const CZ19: [i32; Q19] = [0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, 1, 1, -1, -1, 1, 1, -1, -1];
const W19: [f64; Q19] = [
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 OPP19: [usize; Q19] = [
0, 2, 1, 4, 3, 6, 5, 10, 9, 8, 7, 14, 13, 12, 11, 18, 17, 16, 15,
];
const CS2: f64 = 1.0 / 3.0;
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum BoundaryType {
Fluid,
Wall,
Inlet {
ux: f64,
uy: f64,
},
Outlet,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum BoundaryType3D {
Fluid,
Wall,
Inlet([f64; 3]),
Outlet,
}
#[inline]
fn feq9(i: usize, rho: f64, ux: f64, uy: f64) -> f64 {
let cu = (CX9[i] as f64) * ux + (CY9[i] as f64) * uy;
let u2 = ux * ux + uy * uy;
W9[i] * rho * (1.0 + cu / CS2 + cu * cu / (2.0 * CS2 * CS2) - u2 / (2.0 * CS2))
}
#[inline]
fn feq19(i: usize, rho: f64, ux: f64, uy: f64, uz: f64) -> f64 {
let cu = (CX19[i] as f64) * ux + (CY19[i] as f64) * uy + (CZ19[i] as f64) * uz;
let u2 = ux * ux + uy * uy + uz * uz;
W19[i] * rho * (1.0 + cu / CS2 + cu * cu / (2.0 * CS2 * CS2) - u2 / (2.0 * CS2))
}
pub struct D2Q9Lbm {
pub nx: usize,
pub ny: usize,
pub omega: f64,
f: Vec<Vec<Vec<f64>>>,
f_buf: Vec<Vec<Vec<f64>>>,
pub density: Vec<Vec<f64>>,
pub velocity_x: Vec<Vec<f64>>,
pub velocity_y: Vec<Vec<f64>>,
boundary: Vec<Vec<BoundaryType>>,
viscosity: f64,
}
impl D2Q9Lbm {
pub fn new(nx: usize, ny: usize, viscosity: f64) -> Self {
let tau = 3.0 * viscosity + 0.5;
let omega = 1.0 / tau;
let rho0 = 1.0_f64;
let ux0 = 0.0_f64;
let uy0 = 0.0_f64;
let mut f = vec![vec![vec![0.0_f64; ny]; nx]; Q9];
for i in 0..Q9 {
for x in 0..nx {
for y in 0..ny {
f[i][x][y] = feq9(i, rho0, ux0, uy0);
}
}
}
Self {
nx,
ny,
omega,
f_buf: f.clone(),
f,
density: vec![vec![rho0; ny]; nx],
velocity_x: vec![vec![ux0; ny]; nx],
velocity_y: vec![vec![uy0; ny]; nx],
boundary: vec![vec![BoundaryType::Fluid; ny]; nx],
viscosity,
}
}
pub fn set_boundary(&mut self, x: usize, y: usize, bc: BoundaryType) {
if x < self.nx && y < self.ny {
self.boundary[x][y] = bc;
}
}
fn update_macroscopic(&mut self) {
for x in 0..self.nx {
for y in 0..self.ny {
let mut rho = 0.0;
let mut ux = 0.0;
let mut uy = 0.0;
for i in 0..Q9 {
let fi = self.f[i][x][y];
rho += fi;
ux += (CX9[i] as f64) * fi;
uy += (CY9[i] as f64) * fi;
}
if rho > 1e-15 {
self.density[x][y] = rho;
self.velocity_x[x][y] = ux / rho;
self.velocity_y[x][y] = uy / rho;
}
}
}
}
fn collide(&mut self) {
for x in 0..self.nx {
for y in 0..self.ny {
match self.boundary[x][y] {
BoundaryType::Wall => continue, _ => {}
}
let rho = self.density[x][y];
let ux = self.velocity_x[x][y];
let uy = self.velocity_y[x][y];
for i in 0..Q9 {
let feqi = feq9(i, rho, ux, uy);
self.f[i][x][y] += self.omega * (feqi - self.f[i][x][y]);
}
}
}
}
fn apply_inlet_bc(&mut self, x: usize, y: usize, ux: f64, uy: f64) {
let rho = 1.0; for i in 0..Q9 {
self.f[i][x][y] = feq9(i, rho, ux, uy);
}
}
fn stream_and_bc(&mut self) {
let nx = self.nx as i64;
let ny = self.ny as i64;
for i in 0..Q9 {
let cx = CX9[i];
let cy = CY9[i];
for x in 0..self.nx {
for y in 0..self.ny {
let xd = ((x as i64 - cx as i64 + nx) % nx) as usize;
let yd = ((y as i64 - cy as i64 + ny) % ny) as usize;
self.f_buf[i][x][y] = self.f[i][xd][yd];
}
}
}
std::mem::swap(&mut self.f, &mut self.f_buf);
for x in 0..self.nx {
for y in 0..self.ny {
match self.boundary[x][y] {
BoundaryType::Wall => {
for i in 0..Q9 {
self.f[i][x][y] = self.f_buf[OPP9[i]][x][y];
}
}
BoundaryType::Inlet { ux, uy } => {
self.apply_inlet_bc(x, y, ux, uy);
}
BoundaryType::Outlet => {
let xn = if x > 0 { x - 1 } else { 1 };
for i in 0..Q9 {
self.f[i][x][y] = self.f[i][xn][y];
}
}
BoundaryType::Fluid => {}
}
}
}
}
pub fn step(&mut self) {
self.update_macroscopic();
self.collide();
self.stream_and_bc();
}
pub fn run(&mut self, n_steps: usize) {
for _ in 0..n_steps {
self.step();
}
self.update_macroscopic();
}
pub fn reynolds_number(&self, l_ref: f64, u_ref: f64) -> f64 {
if self.viscosity.abs() < f64::EPSILON {
return f64::INFINITY;
}
l_ref * u_ref / self.viscosity
}
pub fn total_mass(&self) -> f64 {
let mut mass = 0.0;
for x in 0..self.nx {
for y in 0..self.ny {
mass += self.density[x][y];
}
}
mass
}
pub fn vorticity(&self) -> Vec<Vec<f64>> {
let mut vort = vec![vec![0.0_f64; self.ny]; self.nx];
let nx = self.nx;
let ny = self.ny;
for x in 0..nx {
for y in 0..ny {
let xp = (x + 1) % nx;
let xm = (x + nx - 1) % nx;
let yp = (y + 1) % ny;
let ym = (y + ny - 1) % ny;
let duy_dx = (self.velocity_y[xp][y] - self.velocity_y[xm][y]) * 0.5;
let dux_dy = (self.velocity_x[x][yp] - self.velocity_x[x][ym]) * 0.5;
vort[x][y] = duy_dx - dux_dy;
}
}
vort
}
pub fn pressure(&self) -> Vec<Vec<f64>> {
let mut p = vec![vec![0.0_f64; self.ny]; self.nx];
for x in 0..self.nx {
for y in 0..self.ny {
p[x][y] = self.density[x][y] * CS2;
}
}
p
}
pub fn viscosity(&self) -> f64 {
self.viscosity
}
}
pub struct D3Q19Lbm {
pub nx: usize,
pub ny: usize,
pub nz: usize,
pub omega: f64,
f: Vec<Vec<Vec<Vec<f64>>>>,
f_buf: Vec<Vec<Vec<Vec<f64>>>>,
pub density: Vec<Vec<Vec<f64>>>,
pub velocity: Vec<Vec<Vec<[f64; 3]>>>,
boundary: Vec<Vec<Vec<BoundaryType3D>>>,
viscosity: f64,
}
impl D3Q19Lbm {
pub fn new(nx: usize, ny: usize, nz: usize, viscosity: f64) -> Self {
let tau = 3.0 * viscosity + 0.5;
let omega = 1.0 / tau;
let rho0 = 1.0_f64;
let mut f = vec![vec![vec![vec![0.0_f64; nz]; ny]; nx]; Q19];
for i in 0..Q19 {
for x in 0..nx {
for y in 0..ny {
for z in 0..nz {
f[i][x][y][z] = feq19(i, rho0, 0.0, 0.0, 0.0);
}
}
}
}
Self {
nx,
ny,
nz,
omega,
f_buf: f.clone(),
f,
density: vec![vec![vec![rho0; nz]; ny]; nx],
velocity: vec![vec![vec![[0.0_f64; 3]; nz]; ny]; nx],
boundary: vec![vec![vec![BoundaryType3D::Fluid; nz]; ny]; nx],
viscosity,
}
}
pub fn set_boundary(&mut self, x: usize, y: usize, z: usize, bc: BoundaryType3D) {
if x < self.nx && y < self.ny && z < self.nz {
self.boundary[x][y][z] = bc;
}
}
fn update_macroscopic(&mut self) {
for x in 0..self.nx {
for y in 0..self.ny {
for z in 0..self.nz {
let mut rho = 0.0;
let mut ux = 0.0;
let mut uy = 0.0;
let mut uz = 0.0;
for i in 0..Q19 {
let fi = self.f[i][x][y][z];
rho += fi;
ux += (CX19[i] as f64) * fi;
uy += (CY19[i] as f64) * fi;
uz += (CZ19[i] as f64) * fi;
}
if rho > 1e-15 {
self.density[x][y][z] = rho;
self.velocity[x][y][z] = [ux / rho, uy / rho, uz / rho];
}
}
}
}
}
fn collide(&mut self) {
for x in 0..self.nx {
for y in 0..self.ny {
for z in 0..self.nz {
if self.boundary[x][y][z] == BoundaryType3D::Wall {
continue;
}
let rho = self.density[x][y][z];
let [ux, uy, uz] = self.velocity[x][y][z];
for i in 0..Q19 {
let feqi = feq19(i, rho, ux, uy, uz);
self.f[i][x][y][z] += self.omega * (feqi - self.f[i][x][y][z]);
}
}
}
}
}
fn stream_and_bc(&mut self) {
let nx = self.nx as i64;
let ny = self.ny as i64;
let nz = self.nz as i64;
for i in 0..Q19 {
let cx = CX19[i] as i64;
let cy = CY19[i] as i64;
let cz = CZ19[i] as i64;
for x in 0..self.nx {
for y in 0..self.ny {
for z in 0..self.nz {
let xd = ((x as i64 - cx + nx) % nx) as usize;
let yd = ((y as i64 - cy + ny) % ny) as usize;
let zd = ((z as i64 - cz + nz) % nz) as usize;
self.f_buf[i][x][y][z] = self.f[i][xd][yd][zd];
}
}
}
}
std::mem::swap(&mut self.f, &mut self.f_buf);
for x in 0..self.nx {
for y in 0..self.ny {
for z in 0..self.nz {
match self.boundary[x][y][z] {
BoundaryType3D::Wall => {
for i in 0..Q19 {
self.f[i][x][y][z] = self.f_buf[OPP19[i]][x][y][z];
}
}
BoundaryType3D::Inlet([ux, uy, uz]) => {
let rho = 1.0;
for i in 0..Q19 {
self.f[i][x][y][z] = feq19(i, rho, ux, uy, uz);
}
}
BoundaryType3D::Outlet => {
let xn = if x > 0 { x - 1 } else { 1 };
for i in 0..Q19 {
self.f[i][x][y][z] = self.f[i][xn][y][z];
}
}
BoundaryType3D::Fluid => {}
}
}
}
}
}
pub fn step(&mut self) {
self.update_macroscopic();
self.collide();
self.stream_and_bc();
}
pub fn run(&mut self, n_steps: usize) {
for _ in 0..n_steps {
self.step();
}
self.update_macroscopic();
}
pub fn total_mass(&self) -> f64 {
let mut mass = 0.0;
for x in 0..self.nx {
for y in 0..self.ny {
for z in 0..self.nz {
mass += self.density[x][y][z];
}
}
}
mass
}
pub fn reynolds_number(&self, l_ref: f64, u_ref: f64) -> f64 {
if self.viscosity.abs() < f64::EPSILON {
return f64::INFINITY;
}
l_ref * u_ref / self.viscosity
}
}
pub fn lid_driven_cavity(nx: usize, ny: usize, viscosity: f64, u_lid: f64) -> D2Q9Lbm {
let mut lbm = D2Q9Lbm::new(nx, ny, viscosity);
for y in 0..ny {
lbm.set_boundary(0, y, BoundaryType::Wall);
lbm.set_boundary(nx - 1, y, BoundaryType::Wall);
}
for x in 0..nx {
lbm.set_boundary(x, 0, BoundaryType::Wall);
lbm.set_boundary(x, ny - 1, BoundaryType::Inlet { ux: u_lid, uy: 0.0 });
}
lbm
}
pub fn validate_lbm_parameters(viscosity: f64) -> IntegrateResult<()> {
if viscosity <= 0.0 {
return Err(IntegrateError::ValueError(
"LBM viscosity must be positive".into(),
));
}
let tau = 3.0 * viscosity + 0.5;
let omega = 1.0 / tau;
if omega >= 2.0 {
return Err(IntegrateError::ValueError(format!(
"LBM relaxation frequency omega={:.4} >= 2.0; reduce viscosity for stability",
omega
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_d2q9_mass_conservation_periodic() {
let mut lbm = D2Q9Lbm::new(16, 16, 0.1);
let m0 = lbm.total_mass();
lbm.run(50);
let m1 = lbm.total_mass();
assert!((m1 - m0).abs() < 1e-8, "mass not conserved: Δm={:.2e}", m1 - m0);
}
#[test]
fn test_d2q9_reynolds_number() {
let lbm = D2Q9Lbm::new(64, 64, 0.01);
let re = lbm.reynolds_number(64.0, 0.1);
assert!((re - 640.0).abs() < 1e-10);
}
#[test]
fn test_d2q9_pressure_positive() {
let lbm = D2Q9Lbm::new(8, 8, 0.1);
let p = lbm.pressure();
for x in 0..8 {
for y in 0..8 {
assert!(p[x][y] > 0.0);
}
}
}
#[test]
fn test_d2q9_vorticity_zero_at_rest() {
let lbm = D2Q9Lbm::new(8, 8, 0.1);
let vort = lbm.vorticity();
for x in 0..8 {
for y in 0..8 {
assert!(vort[x][y].abs() < 1e-12);
}
}
}
#[test]
fn test_lid_driven_cavity_runs() {
let mut lbm = lid_driven_cavity(16, 16, 0.1, 0.1);
lbm.run(50);
let ke: f64 = (0..16).flat_map(|x| (0..16).map(move |y| (x, y)))
.map(|(x, y)| {
let ux = lbm.velocity_x[x][y];
let uy = lbm.velocity_y[x][y];
0.5 * lbm.density[x][y] * (ux * ux + uy * uy)
})
.sum();
assert!(ke > 0.0, "kinetic energy should be positive after lid driving");
}
#[test]
fn test_d2q9_wall_boundary() {
let mut lbm = D2Q9Lbm::new(8, 8, 0.1);
for y in 0..8 {
lbm.set_boundary(0, y, BoundaryType::Wall);
}
lbm.run(20);
for y in 0..8 {
assert!(lbm.velocity_x[0][y].abs() < 1e-10);
assert!(lbm.velocity_y[0][y].abs() < 1e-10);
}
}
#[test]
fn test_d3q19_mass_conservation() {
let mut lbm = D3Q19Lbm::new(8, 8, 8, 0.1);
let m0 = lbm.total_mass();
lbm.run(20);
let m1 = lbm.total_mass();
assert!((m1 - m0).abs() < 1e-7, "3D mass not conserved: Δm={:.2e}", m1 - m0);
}
#[test]
fn test_validate_parameters() {
assert!(validate_lbm_parameters(0.1).is_ok());
assert!(validate_lbm_parameters(-0.1).is_err());
}
}