forked from 1b-agent/fieldcraft-upstream
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family, docs and licences. No compatibility shim: no files of the earlier name exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
645 lines
29 KiB
Rust
645 lines
29 KiB
Rust
//! 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. Collision and streaming run by fixed blocks of cells in parallel, each cell writing
|
||
//! only its own populations; the wall forces are summed in link order. There are no parallel
|
||
//! reductions, so the result is the same bit for bit on any number of threads and platform.
|
||
|
||
use fieldcraft_model::{BoxFace, FaceCondition, Profile};
|
||
|
||
use fieldcraft_parallel as parallel;
|
||
|
||
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;
|
||
|
||
/// Cells per parallel block of the collision and the streaming. The blocks are fixed, every
|
||
/// cell's update reads and writes only its own populations (collision) or writes only its own
|
||
/// (streaming), so the result does not depend on the number of threads.
|
||
const CELLS: usize = 4096;
|
||
|
||
/// The first `n` populations of each direction of an array with direction stride `stride`, cut
|
||
/// into blocks of [`CELLS`] cells: block b holds the Q directions' slices of cells
|
||
/// b·CELLS.. .
|
||
fn cell_blocks(data: &mut [f64], stride: usize, n: usize) -> Vec<Vec<&mut [f64]>> {
|
||
let mut blocks: Vec<Vec<&mut [f64]>> = (0..n.div_ceil(CELLS)).map(|_| Vec::with_capacity(Q)).collect();
|
||
for direction in data.chunks_mut(stride.max(1)).take(Q) {
|
||
for (block, cells) in blocks.iter_mut().zip(direction.get_mut(..n).unwrap_or_default().chunks_mut(CELLS)) {
|
||
block.push(cells);
|
||
}
|
||
}
|
||
blocks
|
||
}
|
||
|
||
/// 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, by blocks of [`CELLS`] cells
|
||
/// in parallel. Returns the largest squared lattice speed, or NaN when a density is not
|
||
/// positive.
|
||
fn collide(&mut self) -> f64 {
|
||
let n = self.n;
|
||
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 fluid = &self.fluid;
|
||
let mut blocks = cell_blocks(&mut self.f, self.stride, n);
|
||
let mut maxima = vec![0.0f64; blocks.len()];
|
||
let mut work: Vec<(&mut Vec<&mut [f64]>, &mut f64)> = blocks.iter_mut().zip(maxima.iter_mut()).collect();
|
||
parallel::for_each_mut(&mut work, |b, (f, max_u2)| {
|
||
let mut fq = [0.0; Q];
|
||
let len = f.first().map_or(0, |d| d.len());
|
||
for i in 0..len {
|
||
if !fluid.get(b * CELLS + i).copied().unwrap_or(false) {
|
||
continue;
|
||
}
|
||
let (mut rho, mut j) = (0.0, [0.0; 3]);
|
||
for q in 0..Q {
|
||
let v = f.get(q).and_then(|d| d.get(i)).copied().unwrap_or(0.0);
|
||
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()) {
|
||
**max_u2 = f64::NAN;
|
||
return;
|
||
}
|
||
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];
|
||
let mut put = |q: usize, v: f64| {
|
||
if let Some(x) = f.get_mut(q).and_then(|d| d.get_mut(i)) {
|
||
*x = v;
|
||
}
|
||
};
|
||
// 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);
|
||
}
|
||
put(0, 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;
|
||
}
|
||
put(q, fq[q] - relax_plus - relax_minus + s_plus + s_minus);
|
||
put(o, fq[o] - relax_plus + relax_minus + s_plus - s_minus);
|
||
}
|
||
}
|
||
});
|
||
// The largest speed of all blocks (a maximum does not depend on the order), or NaN.
|
||
if maxima.iter().any(|m| m.is_nan()) { f64::NAN } else { maxima.iter().fold(0.0, |m, x| m.max(*x)) }
|
||
}
|
||
|
||
/// Pull streaming into `g`, with the wall rules (blocks of [`CELLS`] destination cells in
|
||
/// parallel), then the momentum exchange (in link order).
|
||
fn stream(&mut self) {
|
||
let n = self.n;
|
||
let s = self.stride;
|
||
let (f, links, offset) = (&self.f, &self.links, &self.offset);
|
||
let mut blocks = cell_blocks(&mut self.g, s, n);
|
||
parallel::for_each_mut(&mut blocks, |b, g| {
|
||
let start = b * CELLS;
|
||
// 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, dst) in g.iter_mut().enumerate() {
|
||
let off = offset.get(q).copied().unwrap_or(0);
|
||
let src = f.get(q * s..q * s + n).unwrap_or_default();
|
||
for (i, x) in dst.iter_mut().enumerate() {
|
||
if let Some(from) = (start + i).checked_add_signed(-off)
|
||
&& let Some(v) = src.get(from)
|
||
{
|
||
*x = *v;
|
||
}
|
||
}
|
||
}
|
||
let first = links.partition_point(|l| (l.cell as usize) < start);
|
||
for link in links.get(first..).unwrap_or_default().iter().take_while(|l| (l.cell as usize) < start + CELLS) {
|
||
let (cell, q) = (link.cell as usize, link.q as usize);
|
||
let v = match link.rule {
|
||
Rule::Pull { from } => f.get(q * s + from as usize).copied(),
|
||
Rule::Bounce { du, .. } => f.get(opposite(q) * s + cell).map(|out| out + du),
|
||
Rule::Reflect { from, q: q2, .. } => f.get(q2 as usize * s + from as usize).copied(),
|
||
// A placeholder the open boundary replaces.
|
||
Rule::Open => f.get(opposite(q) * s + cell).copied(),
|
||
};
|
||
if let (Some(v), Some(x)) = (v, g.get_mut(q).and_then(|d| d.get_mut(cell - start))) {
|
||
*x = v;
|
||
}
|
||
}
|
||
});
|
||
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::Bounce { group, du } => {
|
||
let out = self.f[opposite(q) * s + cell];
|
||
let back = out + du;
|
||
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];
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
Rule::Pull { .. } | Rule::Open => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|