forked from 1b-agent/fieldcraft-upstream
Lattice Boltzmann flow on the CPU: model, solver, engine (M12) (#36)
Model: Newtonian fluid materials; fluid domains (box, spacing, conditions on the box faces, obstacle bodies with conditions on their named faces, initial velocity, body acceleration); fluid steps with caps. Deck writers refuse fluid steps; a fluid-only model needs no mesh in model.check; one step selector (Model::select_step) for every solver. New L2 crate modelcraft-lbm: D3Q19 two-relaxation-time collision with Guo forcing; halfway bounce-back walls (moving walls after Ladd), slip faces, periodic pairs; Zou–He velocity and pressure faces with the D3Q19 transverse correction; obstacles voxelised from the face-tagged boundary surface with exact predicates and symbolic perturbation; momentum-exchange forces per box face and per B-rep face; unit conversion, automatic time step and Mach/tau limits; bounded runs with frames and force histories. Engine: model.fluid_domain.create/edit/delete, model.fluid_domain.boundary, solve.fluid (obstacles from the open design or imported surfaces, lowerable caps), solve.probe for fluid fields and wall force histories. Fluid Domain, Inlet & Outlet and Lattice Boltzmann toolbar entries are ready. Verification (analytic): plane Poiseuille exact with Λ = 3/16 and second order (2.000) with Λ = 1/4; Couette exact with the wall shear; Taylor–Green decay second order (2.01, 2.00); staircase pipe; Stokes drag of a sphere array within 1.7% of Hasimoto at R = 6; Zou–He channel; mass conserved to round-off; bit-for-bit reproducibility. Benchmark: Schäfer–Turek 2D-1. Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
55 files changed
+3800
-64
No files matched your search
@@ -0,0 +1,598 @@
|
||||
//! The lattice Boltzmann state and one time step: collide, stream, open boundaries.
|
||||
//!
|
||||
//! # Scheme
|
||||
//! - **Lattice and equilibrium:** D3Q19 with the second-order equilibrium
|
||||
//! (Qian, d'Humières & Lallemand 1992; Krüger et al. 2017, ch. 3).
|
||||
//! - **Collision:** two relaxation times (Ginzburg, Verhaeghe & d'Humières 2008): the symmetric
|
||||
//! part of each opposite pair relaxes with `1/τ⁺` (which sets the viscosity), the antisymmetric
|
||||
//! part with `1/τ⁻`, related by the parameter `Λ = (τ⁺ − ½)(τ⁻ − ½)`. With `Λ = 3/16`
|
||||
//! halfway bounce-back puts the wall exactly halfway between nodes and steady solutions do not
|
||||
//! depend on the viscosity.
|
||||
//! - **Body force:** Guo, Zheng & Shi 2002 (PRE 65:046308): the velocity is
|
||||
//! `(Σ f c + F/2) / ρ` and the forcing term is split into its symmetric part, relaxed with
|
||||
//! `1 − 1/(2τ⁺)`, and its antisymmetric part, with `1 − 1/(2τ⁻)` (the TRT form of Guo's
|
||||
//! term, Krüger et al. 2017, ch. 10).
|
||||
//! - **Walls:** halfway bounce-back at every link from a fluid cell to a solid cell or across a
|
||||
//! wall face of the box; moving walls add Ladd's term `6 w_q ρ₀ c_q·u_w` (Ladd 1994, JFM
|
||||
//! 271:285). Slip faces reflect populations specularly.
|
||||
//! - **Inlets and outlets:** Zou & He 1997 (Phys. Fluids 9:1591) on the first layer of cells: the
|
||||
//! unknown incoming populations are the opposite ones plus the equilibrium difference
|
||||
//! (bounce-back of the non-equilibrium part), the density (velocity inlet) or the normal velocity
|
||||
//! (pressure outlet) follow from mass and normal momentum, and the transverse momentum is
|
||||
//! corrected on the diagonal unknowns as M. Hecht and J. Harting do for D3Q19 (*Implementation
|
||||
//! of on-site velocity boundary conditions for D3Q19 lattice Boltzmann simulations*, J. Stat.
|
||||
//! Mech. 2010, P01018). The prescribed velocity is the one with the half force of Guo's scheme.
|
||||
//! - **Forces:** momentum exchange on every wall link (Ladd 1994; R. Mei, D. Yu, W. Shyy, L.-S.
|
||||
//! Luo, *Force evaluation in the lattice Boltzmann method involving curved geometry*, PRE
|
||||
//! 65:041203, 2002): a population `f*` leaving a fluid cell toward the wall and the one `f'`
|
||||
//! coming back transfer `c_out f* − c_in f'` to the wall.
|
||||
//!
|
||||
//! # Layout and determinism
|
||||
//! Two arrays of populations, structure of arrays (`f[q n + cell]`), A–B pattern. Streaming
|
||||
//! pulls: every population of a cell comes from one source (a neighbour, a periodic image, the
|
||||
//! cell itself through a wall, or an open boundary). Cells whose 18 neighbours are all fluid
|
||||
//! cells inside the box stream by one shifted copy per direction; the others follow per-link
|
||||
//! rules. Every loop runs in a fixed order with no parallel reductions, so the result is the
|
||||
//! same bit for bit on every platform.
|
||||
|
||||
use modelcraft_model::{BoxFace, FaceCondition, Profile};
|
||||
|
||||
use crate::lattice::{C, Q, W, c, equilibrium, moving_wall, opposite};
|
||||
use crate::setup::{MAX_LATTICE_SPEED, Obstacle, Setup};
|
||||
use crate::voxel::{Grid, TriangleTree, voxelise};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Where a population of a cell next to a wall or box face comes from.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum Rule {
|
||||
/// The same direction from another cell (a neighbour or a periodic image).
|
||||
Pull { from: u32 },
|
||||
/// Bounce-back at a wall of force group `group`; `du` is the moving-wall term.
|
||||
Bounce { group: u32, du: f64 },
|
||||
/// Specular reflection at a slip face: direction `q` from cell `from`.
|
||||
Reflect { from: u32, q: u8, group: u32 },
|
||||
/// From outside an inlet or outlet: set by the open boundary after streaming.
|
||||
Open,
|
||||
}
|
||||
|
||||
/// A population, direction `q` of `cell`, and where it comes from.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Link {
|
||||
cell: u32,
|
||||
q: u8,
|
||||
rule: Rule,
|
||||
}
|
||||
|
||||
/// An inlet or outlet cell.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct OpenCell {
|
||||
cell: u32,
|
||||
axis: usize,
|
||||
/// +1 at a min face (the inward normal points up the axis), −1 at a max face.
|
||||
inward: f64,
|
||||
target: Target,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Target {
|
||||
/// The velocity (lattice units).
|
||||
Velocity([f64; 3]),
|
||||
/// The density.
|
||||
Density(f64),
|
||||
}
|
||||
|
||||
/// A force group: a box face or a face of an obstacle.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Group {
|
||||
/// `x_min` … for box faces; the face name for obstacle faces.
|
||||
pub name: String,
|
||||
/// The obstacle body, for obstacle faces.
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
/// The flow state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Flow {
|
||||
grid: Grid,
|
||||
n: usize,
|
||||
/// The distance between two directions' arrays in `f` and `g` (at least `n`).
|
||||
stride: usize,
|
||||
fluid: Vec<bool>,
|
||||
f: Vec<f64>,
|
||||
g: Vec<f64>,
|
||||
/// The links that do not stream by the shifted copies, in cell order.
|
||||
links: Vec<Link>,
|
||||
open: Vec<OpenCell>,
|
||||
omega_plus: f64,
|
||||
omega_minus: f64,
|
||||
acceleration: [f64; 3],
|
||||
/// The momentum transferred to each group in the last step (lattice units).
|
||||
forces: Vec<[f64; 3]>,
|
||||
groups: Vec<Group>,
|
||||
/// Per direction, the index offset of the neighbour a population streams from.
|
||||
offset: [isize; Q],
|
||||
fluid_cells: usize,
|
||||
steps_done: u64,
|
||||
/// Obstacles that cover no cell centre (smaller than the spacing, or outside the box).
|
||||
empty_obstacles: Vec<String>,
|
||||
}
|
||||
|
||||
const NONE: u32 = u32::MAX;
|
||||
|
||||
/// A vector allocated without aborting when memory runs out.
|
||||
fn filled<T: Clone>(len: usize, value: T, what: &str) -> Result<Vec<T>> {
|
||||
let mut v = Vec::new();
|
||||
v.try_reserve_exact(len).map_err(|_| Error::Limit(format!("not enough memory for the {what} ({len} entries)")))?;
|
||||
v.resize(len, value);
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
impl Flow {
|
||||
/// Build the grid (voxelising the obstacles), the streaming rules and the initial state (at
|
||||
/// rest or the domain's initial velocity, density 1).
|
||||
pub fn new(setup: &Setup, obstacles: &[Obstacle]) -> Result<Flow> {
|
||||
let grid = setup.grid;
|
||||
let [nx, ny, nz] = grid.cells;
|
||||
let n = grid.len();
|
||||
if n == 0 || n >= NONE as usize {
|
||||
return Err(Error::Limit(format!("{n} cells cannot be addressed")));
|
||||
}
|
||||
// Each direction's array starts an odd number of cache lines (8 doubles) after the
|
||||
// previous one, so the 19 arrays never fall into the same cache sets (a power-of-two grid
|
||||
// would otherwise put them all in one).
|
||||
let lines = n.div_ceil(8);
|
||||
let stride = 8 * (lines + 1 - lines % 2);
|
||||
let q_n = stride.checked_mul(Q).ok_or_else(|| Error::Limit("the grid is too large".into()))?;
|
||||
|
||||
// Solid cells, obstacle by obstacle (the first obstacle that covers a cell owns it).
|
||||
let mut fluid = filled(n, true, "cell flags")?;
|
||||
let mut owner = filled(n, NONE, "cell owners")?;
|
||||
let mut trees = Vec::with_capacity(setup.obstacles.len());
|
||||
let mut empty_obstacles = Vec::new();
|
||||
// Force groups: the six box faces, then every face of every obstacle.
|
||||
let mut groups: Vec<Group> = BoxFace::ALL.iter().map(|f| Group { name: f.name().to_string(), body: None }).collect();
|
||||
let mut base = Vec::with_capacity(setup.obstacles.len());
|
||||
for (k, faces) in setup.obstacles.iter().enumerate() {
|
||||
let o = obstacles.get(faces.index).ok_or_else(|| Error::NotFound(format!("obstacle {}", faces.index)))?;
|
||||
let mut inside = filled(n, false, "obstacle cells")?;
|
||||
if voxelise(&o.surface, &grid, &mut inside)? == 0 {
|
||||
empty_obstacles.push(o.body.clone());
|
||||
}
|
||||
for (c, &solid) in inside.iter().enumerate() {
|
||||
if solid && owner[c] == NONE {
|
||||
owner[c] = k as u32;
|
||||
fluid[c] = false;
|
||||
}
|
||||
}
|
||||
trees.push((TriangleTree::new(&o.surface), o.surface.tri_face()));
|
||||
base.push(groups.len() as u32);
|
||||
groups.extend(faces.names.iter().map(|name| Group { name: name.clone(), body: Some(o.body.clone()) }));
|
||||
}
|
||||
|
||||
let domain = &setup.domain;
|
||||
let periodic = [domain.faces.periodic(0), domain.faces.periodic(1), domain.faces.periodic(2)];
|
||||
let wall_velocity = |face: BoxFace| match *domain.faces.get(face) {
|
||||
FaceCondition::MovingWall { velocity } => Some(setup.to_lattice(velocity)),
|
||||
FaceCondition::Wall {} => Some([0.0; 3]),
|
||||
_ => None,
|
||||
};
|
||||
let is_slip = |face: BoxFace| matches!(domain.faces.get(face), FaceCondition::Slip {});
|
||||
let dims = [nx as i64, ny as i64, nz as i64];
|
||||
let index = |p: [i64; 3]| grid.index([p[0] as usize, p[1] as usize, p[2] as usize]) as u32;
|
||||
let mut offset = [0isize; Q];
|
||||
for (q, o) in offset.iter_mut().enumerate() {
|
||||
let cq = C[q];
|
||||
*o = cq[0] as isize + nx as isize * (cq[1] as isize + ny as isize * cq[2] as isize);
|
||||
}
|
||||
|
||||
// The group and moving-wall term of a link into fluid cell `cell` (direction q) from the
|
||||
// obstacle cell `solid`: the face of the triangle nearest the link's midpoint.
|
||||
let obstacle_link = |cell: usize, solid: u32, q: usize| -> Rule {
|
||||
let k = owner.get(solid as usize).copied().unwrap_or(NONE) as usize;
|
||||
let (Some((tree, tags)), Some(faces), Some(&first)) = (trees.get(k), setup.obstacles.get(k), base.get(k)) else {
|
||||
return Rule::Bounce { group: 0, du: 0.0 };
|
||||
};
|
||||
let (centre, cq) = (grid.centre(cell), c(q));
|
||||
let mid = [0, 1, 2].map(|a| centre[a] - 0.5 * cq[a] * grid.spacing);
|
||||
let tag = tree.nearest(mid).and_then(|t| tags.get(t as usize).copied()).unwrap_or(0);
|
||||
let u = faces.velocities.get(tag as usize).copied().unwrap_or([0.0; 3]);
|
||||
Rule::Bounce { group: first + tag, du: moving_wall(q, u) }
|
||||
};
|
||||
|
||||
// The links that do not stream from the neighbour the shifted copies read.
|
||||
let mut links: Vec<Link> = Vec::new();
|
||||
let mut fluid_cells = 0;
|
||||
for cell in 0..n {
|
||||
if !fluid[cell] {
|
||||
continue;
|
||||
}
|
||||
fluid_cells += 1;
|
||||
let ijk = grid.coords(cell);
|
||||
let p = [ijk[0] as i64, ijk[1] as i64, ijk[2] as i64];
|
||||
for q in 1..Q {
|
||||
let cq = C[q];
|
||||
let mut s = [p[0] - i64::from(cq[0]), p[1] - i64::from(cq[1]), p[2] - i64::from(cq[2])];
|
||||
let mut wrapped = false;
|
||||
let mut crossed: [Option<BoxFace>; 3] = [None; 3];
|
||||
for a in 0..3 {
|
||||
if s[a] < 0 || s[a] >= dims[a] {
|
||||
if periodic[a] {
|
||||
s[a] = s[a].rem_euclid(dims[a]);
|
||||
wrapped = true;
|
||||
} else {
|
||||
crossed[a] = Some(BoxFace::of(a, s[a] >= dims[a]));
|
||||
}
|
||||
}
|
||||
}
|
||||
let crossed_faces = || crossed.iter().flatten().copied();
|
||||
let rule = match crossed_faces().next() {
|
||||
None => {
|
||||
let from = index(s);
|
||||
match (fluid.get(from as usize).copied().unwrap_or(false), wrapped) {
|
||||
(true, false) => continue,
|
||||
(true, true) => Rule::Pull { from },
|
||||
(false, _) => obstacle_link(cell, from, q),
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
if let Some(face) = crossed_faces().find(|f| wall_velocity(*f).is_some()) {
|
||||
Rule::Bounce { group: face.index() as u32, du: moving_wall(q, wall_velocity(face).unwrap_or([0.0; 3])) }
|
||||
} else if let Some(face) = crossed_faces().find(|f| is_slip(*f)) {
|
||||
// The mirror source: the same cell layer, the tangential components shifted.
|
||||
let a = face.axis();
|
||||
let mut m = s;
|
||||
m[a] = p[a];
|
||||
let mut mirrored = C[q];
|
||||
mirrored[a] = -mirrored[a];
|
||||
let q2 = C.iter().position(|v| *v == mirrored).unwrap_or(opposite(q));
|
||||
let single = crossed_faces().count() == 1;
|
||||
let inside = (0..3).all(|b| m[b] >= 0 && m[b] < dims[b]);
|
||||
match inside.then(|| index(m)) {
|
||||
Some(from) if single && fluid.get(from as usize).copied().unwrap_or(false) => {
|
||||
Rule::Reflect { from, q: q2 as u8, group: face.index() as u32 }
|
||||
}
|
||||
_ => Rule::Bounce { group: face.index() as u32, du: 0.0 },
|
||||
}
|
||||
} else {
|
||||
Rule::Open
|
||||
}
|
||||
}
|
||||
};
|
||||
links.push(Link { cell: cell as u32, q: q as u8, rule });
|
||||
}
|
||||
}
|
||||
|
||||
// Inlets and outlets: the fluid cells of each open face's layer.
|
||||
let mut open = Vec::new();
|
||||
for face in BoxFace::ALL {
|
||||
let a = face.axis();
|
||||
let layer = if face.is_max() { grid.cells[a] - 1 } else { 0 };
|
||||
let (t1, t2) = ((a + 1) % 3, (a + 2) % 3);
|
||||
for j in 0..grid.cells[t2] {
|
||||
for i in 0..grid.cells[t1] {
|
||||
let mut ijk = [0usize; 3];
|
||||
ijk[a] = layer;
|
||||
ijk[t1] = i;
|
||||
ijk[t2] = j;
|
||||
let cell = grid.index(ijk);
|
||||
if !fluid[cell] {
|
||||
continue;
|
||||
}
|
||||
let target = match *domain.faces.get(face) {
|
||||
FaceCondition::Velocity { velocity, profile } => {
|
||||
let mut scale = 1.0;
|
||||
if profile == Profile::Parabolic {
|
||||
for t in [t1, t2].into_iter().filter(|t| !periodic[*t]) {
|
||||
let s = (ijk[t] as f64 + 0.5) / grid.cells[t] as f64;
|
||||
scale *= 4.0 * s * (1.0 - s);
|
||||
}
|
||||
}
|
||||
Target::Velocity(setup.to_lattice(velocity).map(|v| v * scale))
|
||||
}
|
||||
FaceCondition::Pressure { pressure } => Target::Density(setup.lattice_density(pressure)),
|
||||
_ => break,
|
||||
};
|
||||
open.push(OpenCell { cell: cell as u32, axis: a, inward: if face.is_max() { -1.0 } else { 1.0 }, target });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The initial state: equilibrium at density 1 and the initial velocity.
|
||||
let mut f = filled(q_n, 0.0, "populations")?;
|
||||
let g = filled(q_n, 0.0, "populations")?;
|
||||
for cell in 0..n {
|
||||
for q in 0..Q {
|
||||
f[q * stride + cell] = if fluid[cell] { equilibrium(q, 1.0, setup.initial_velocity) } else { 0.0 };
|
||||
}
|
||||
}
|
||||
Ok(Flow {
|
||||
grid,
|
||||
n,
|
||||
stride,
|
||||
fluid,
|
||||
f,
|
||||
g,
|
||||
links,
|
||||
open,
|
||||
omega_plus: 1.0 / setup.tau_plus,
|
||||
omega_minus: 1.0 / setup.tau_minus,
|
||||
acceleration: setup.acceleration,
|
||||
forces: vec![[0.0; 3]; groups.len()],
|
||||
groups,
|
||||
offset,
|
||||
fluid_cells,
|
||||
steps_done: 0,
|
||||
empty_obstacles,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn grid(&self) -> &Grid {
|
||||
&self.grid
|
||||
}
|
||||
|
||||
pub fn fluid_cells(&self) -> usize {
|
||||
self.fluid_cells
|
||||
}
|
||||
|
||||
pub fn is_fluid(&self, cell: usize) -> bool {
|
||||
self.fluid.get(cell).copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn groups(&self) -> &[Group] {
|
||||
&self.groups
|
||||
}
|
||||
|
||||
/// The momentum each group received in the last step (lattice units: force per step).
|
||||
pub fn forces(&self) -> &[[f64; 3]] {
|
||||
&self.forces
|
||||
}
|
||||
|
||||
/// Obstacles that cover no cell centre.
|
||||
pub fn empty_obstacles(&self) -> &[String] {
|
||||
&self.empty_obstacles
|
||||
}
|
||||
|
||||
pub fn steps_done(&self) -> u64 {
|
||||
self.steps_done
|
||||
}
|
||||
|
||||
/// Set every fluid cell to the equilibrium of `state(cell) = (density, velocity)` in lattice
|
||||
/// units (velocity as defined with the half force).
|
||||
pub fn set_equilibrium(&mut self, state: impl Fn(usize) -> (f64, [f64; 3])) {
|
||||
let n = self.n;
|
||||
let s = self.stride;
|
||||
for cell in 0..n {
|
||||
if !self.fluid[cell] {
|
||||
continue;
|
||||
}
|
||||
let (rho, u) = state(cell);
|
||||
let bare = [0, 1, 2].map(|a| u[a] - 0.5 * self.acceleration[a]);
|
||||
for q in 0..Q {
|
||||
self.f[q * s + cell] = equilibrium(q, rho, bare);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The density and velocity of a cell (lattice units), or `None` for a solid cell.
|
||||
pub fn moments(&self, cell: usize) -> Option<(f64, [f64; 3])> {
|
||||
if !self.is_fluid(cell) {
|
||||
return None;
|
||||
}
|
||||
let s = self.stride;
|
||||
let (mut rho, mut j) = (0.0, [0.0; 3]);
|
||||
for q in 0..Q {
|
||||
let fq = self.f.get(q * s + cell).copied().unwrap_or(0.0);
|
||||
let cq = c(q);
|
||||
rho += fq;
|
||||
j[0] += fq * cq[0];
|
||||
j[1] += fq * cq[1];
|
||||
j[2] += fq * cq[2];
|
||||
}
|
||||
let u = [0, 1, 2].map(|a| (j[a] + 0.5 * rho * self.acceleration[a]) / rho);
|
||||
Some((rho, u))
|
||||
}
|
||||
|
||||
/// The total mass of the fluid cells (lattice units: the sum of every population).
|
||||
pub fn mass(&self) -> f64 {
|
||||
let n = self.n;
|
||||
let s = self.stride;
|
||||
let mut m = 0.0;
|
||||
for cell in (0..n).filter(|c| self.fluid[*c]) {
|
||||
for q in 0..Q {
|
||||
m += self.f[q * s + cell];
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// One time step. Fails with [`Error::Unstable`] when a density stops being a positive
|
||||
/// number or a lattice speed exceeds [`MAX_LATTICE_SPEED`].
|
||||
pub fn step(&mut self) -> Result<()> {
|
||||
let max_u2 = self.collide();
|
||||
if !(max_u2 <= MAX_LATTICE_SPEED * MAX_LATTICE_SPEED) {
|
||||
return Err(Error::Unstable(format!(
|
||||
"the flow went unstable at time step {}: the lattice speed reached {:.3} (limit {MAX_LATTICE_SPEED}) or a density stopped being a positive number; lower the time step, refine the spacing or check the boundary conditions",
|
||||
self.steps_done + 1,
|
||||
max_u2.sqrt()
|
||||
)));
|
||||
}
|
||||
self.stream();
|
||||
self.open_boundaries();
|
||||
std::mem::swap(&mut self.f, &mut self.g);
|
||||
self.steps_done += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Two-relaxation-time collision with Guo forcing, in place. Returns the largest squared
|
||||
/// lattice speed, or NaN when a density is not positive.
|
||||
fn collide(&mut self) -> f64 {
|
||||
let n = self.n;
|
||||
let s = self.stride;
|
||||
let (wp, wm) = (self.omega_plus, self.omega_minus);
|
||||
let (sp, sm) = (1.0 - 0.5 * wp, 1.0 - 0.5 * wm);
|
||||
let a = self.acceleration;
|
||||
let forced = a != [0.0; 3];
|
||||
let mut max_u2: f64 = 0.0;
|
||||
let mut fq = [0.0; Q];
|
||||
for cell in 0..n {
|
||||
if !self.fluid[cell] {
|
||||
continue;
|
||||
}
|
||||
let (mut rho, mut j) = (0.0, [0.0; 3]);
|
||||
for q in 0..Q {
|
||||
let v = self.f[q * s + cell];
|
||||
fq[q] = v;
|
||||
let cq = C[q];
|
||||
rho += v;
|
||||
j[0] += v * f64::from(cq[0]);
|
||||
j[1] += v * f64::from(cq[1]);
|
||||
j[2] += v * f64::from(cq[2]);
|
||||
}
|
||||
if !(rho > 0.0 && rho.is_finite()) {
|
||||
return f64::NAN;
|
||||
}
|
||||
let force = [rho * a[0], rho * a[1], rho * a[2]];
|
||||
let u = [(j[0] + 0.5 * force[0]) / rho, (j[1] + 0.5 * force[1]) / rho, (j[2] + 0.5 * force[2]) / rho];
|
||||
let uu = u[0] * u[0] + u[1] * u[1] + u[2] * u[2];
|
||||
max_u2 = max_u2.max(uu);
|
||||
let uf = u[0] * force[0] + u[1] * force[1] + u[2] * force[2];
|
||||
// Rest population: symmetric only.
|
||||
let eq0 = W[0] * rho * (1.0 - 1.5 * uu);
|
||||
let mut out0 = fq[0] - wp * (fq[0] - eq0);
|
||||
if forced {
|
||||
out0 += sp * W[0] * (-3.0 * uf);
|
||||
}
|
||||
self.f[cell] = out0;
|
||||
for q in (1..Q).step_by(2) {
|
||||
let o = q + 1;
|
||||
let cq = C[q];
|
||||
let cv = [f64::from(cq[0]), f64::from(cq[1]), f64::from(cq[2])];
|
||||
let cu = cv[0] * u[0] + cv[1] * u[1] + cv[2] * u[2];
|
||||
let w = W[q];
|
||||
let eq_plus = w * rho * (1.0 + 4.5 * cu * cu - 1.5 * uu);
|
||||
let eq_minus = w * rho * 3.0 * cu;
|
||||
let plus = 0.5 * (fq[q] + fq[o]);
|
||||
let minus = 0.5 * (fq[q] - fq[o]);
|
||||
let relax_plus = wp * (plus - eq_plus);
|
||||
let relax_minus = wm * (minus - eq_minus);
|
||||
let (mut s_plus, mut s_minus) = (0.0, 0.0);
|
||||
if forced {
|
||||
let cf = cv[0] * force[0] + cv[1] * force[1] + cv[2] * force[2];
|
||||
s_plus = sp * w * (9.0 * cu * cf - 3.0 * uf);
|
||||
s_minus = sm * w * 3.0 * cf;
|
||||
}
|
||||
self.f[q * s + cell] = fq[q] - relax_plus - relax_minus + s_plus + s_minus;
|
||||
self.f[o * s + cell] = fq[o] - relax_plus + relax_minus + s_plus - s_minus;
|
||||
}
|
||||
}
|
||||
max_u2
|
||||
}
|
||||
|
||||
/// Pull streaming into `g`, with the wall rules and the momentum exchange.
|
||||
fn stream(&mut self) {
|
||||
let n = self.n;
|
||||
let s = self.stride;
|
||||
// Every direction as one shifted copy: right for cells whose neighbours are all fluid
|
||||
// cells in the box; the others are overwritten by their rules below.
|
||||
for q in 0..Q {
|
||||
let off = self.offset[q];
|
||||
let (src, dst) = (&self.f[q * s..q * s + n], &mut self.g[q * s..q * s + n]);
|
||||
if off >= 0 {
|
||||
let o = off as usize;
|
||||
if o < n {
|
||||
dst[o..].copy_from_slice(&src[..n - o]);
|
||||
}
|
||||
} else {
|
||||
let o = off.unsigned_abs();
|
||||
if o < n {
|
||||
dst[..n - o].copy_from_slice(&src[o..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.forces.fill([0.0; 3]);
|
||||
for link in &self.links {
|
||||
let (cell, q) = (link.cell as usize, link.q as usize);
|
||||
let cq = c(q);
|
||||
match link.rule {
|
||||
Rule::Pull { from } => self.g[q * s + cell] = self.f[q * s + from as usize],
|
||||
Rule::Bounce { group, du } => {
|
||||
let out = self.f[opposite(q) * s + cell];
|
||||
let back = out + du;
|
||||
self.g[q * s + cell] = back;
|
||||
if let Some(force) = self.forces.get_mut(group as usize) {
|
||||
// c_out f* − c_in f' with c_out = −c_q.
|
||||
for a in 0..3 {
|
||||
force[a] -= cq[a] * (out + back);
|
||||
}
|
||||
}
|
||||
}
|
||||
Rule::Reflect { from, q: q2, group } => {
|
||||
let q2 = q2 as usize;
|
||||
let out = self.f[q2 * s + from as usize];
|
||||
self.g[q * s + cell] = out;
|
||||
let c2 = c(q2);
|
||||
if let Some(force) = self.forces.get_mut(group as usize) {
|
||||
for a in 0..3 {
|
||||
force[a] += c2[a] * out - cq[a] * out;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A placeholder the open boundary replaces.
|
||||
Rule::Open => self.g[q * s + cell] = self.f[opposite(q) * s + cell],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Zou–He velocity and pressure boundaries on the streamed populations.
|
||||
fn open_boundaries(&mut self) {
|
||||
let s = self.stride;
|
||||
let a_half = self.acceleration.map(|v| 0.5 * v);
|
||||
for oc in &self.open {
|
||||
let cell = oc.cell as usize;
|
||||
let axis = oc.axis;
|
||||
let sign = oc.inward;
|
||||
let ci = |q: usize| f64::from(C[q][axis]);
|
||||
// Tangential (c_n = 0) and outgoing (c_n = −s, known) sums.
|
||||
let (mut tangential, mut outgoing) = (0.0, 0.0);
|
||||
for q in 0..Q {
|
||||
let v = self.g[q * s + cell];
|
||||
let cn = ci(q) * sign;
|
||||
if cn == 0.0 {
|
||||
tangential += v;
|
||||
} else if cn < 0.0 {
|
||||
outgoing += v;
|
||||
}
|
||||
}
|
||||
// The bare velocity (without the half force) the populations must carry.
|
||||
let (rho, bare) = match oc.target {
|
||||
Target::Velocity(u) => {
|
||||
let bare = [u[0] - a_half[0], u[1] - a_half[1], u[2] - a_half[2]];
|
||||
let un = sign * bare[axis];
|
||||
((tangential + 2.0 * outgoing) / (1.0 - un), bare)
|
||||
}
|
||||
Target::Density(rho) => {
|
||||
let un = 1.0 - (tangential + 2.0 * outgoing) / rho;
|
||||
let mut bare = [-a_half[0], -a_half[1], -a_half[2]];
|
||||
bare[axis] = sign * un;
|
||||
(rho, bare)
|
||||
}
|
||||
};
|
||||
// Unknowns: bounce-back of the non-equilibrium part.
|
||||
for q in (1..Q).filter(|q| ci(*q) * sign > 0.0) {
|
||||
let cq = c(q);
|
||||
let cu = cq[0] * bare[0] + cq[1] * bare[1] + cq[2] * bare[2];
|
||||
self.g[q * s + cell] = self.g[opposite(q) * s + cell] + 6.0 * W[q] * rho * cu;
|
||||
}
|
||||
// Transverse momentum: correct the two diagonal unknowns along each tangential axis.
|
||||
for t in (0..3).filter(|t| *t != axis) {
|
||||
let mut m = -rho * bare[t];
|
||||
for q in 0..Q {
|
||||
m += self.g[q * s + cell] * f64::from(C[q][t]);
|
||||
}
|
||||
for q in (1..Q).filter(|q| ci(*q) * sign > 0.0 && C[*q][t] != 0) {
|
||||
self.g[q * s + cell] -= 0.5 * m * f64::from(C[q][t]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user