forked from 1b-agent/fieldcraft-upstream
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>
142 lines
3.8 KiB
Rust
142 lines
3.8 KiB
Rust
//! The D3Q19 velocity set (Qian, d'Humières & Lallemand 1992; Krüger et al. 2017, ch. 3).
|
||
//!
|
||
//! Directions come in opposite pairs: 1–2, 3–4, …, 17–18, so the opposite of an odd `q` is
|
||
//! `q + 1` and of an even `q` (not 0) is `q − 1`.
|
||
|
||
/// The number of velocities.
|
||
pub const Q: usize = 19;
|
||
|
||
/// The lattice velocities `c_q` in lattice units.
|
||
pub const C: [[i32; 3]; Q] = [
|
||
[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],
|
||
];
|
||
|
||
/// The weights `w_q`: 1/3 at rest, 1/18 along the axes, 1/36 along the face diagonals.
|
||
pub const W: [f64; Q] = [
|
||
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,
|
||
];
|
||
|
||
/// The squared speed of sound in lattice units, `c_s² = 1/3`.
|
||
pub const CS2: f64 = 1.0 / 3.0;
|
||
|
||
/// The direction opposite `q`.
|
||
#[inline(always)]
|
||
pub const fn opposite(q: usize) -> usize {
|
||
if q == 0 {
|
||
0
|
||
} else if q % 2 == 1 {
|
||
q + 1
|
||
} else {
|
||
q - 1
|
||
}
|
||
}
|
||
|
||
/// `c_q` as floating-point numbers.
|
||
#[inline(always)]
|
||
pub fn c(q: usize) -> [f64; 3] {
|
||
let v = C.get(q).copied().unwrap_or([0; 3]);
|
||
[f64::from(v[0]), f64::from(v[1]), f64::from(v[2])]
|
||
}
|
||
|
||
/// The second-order equilibrium `w_q ρ (1 + 3 c·u + 9/2 (c·u)² − 3/2 u²)`.
|
||
#[inline(always)]
|
||
pub fn equilibrium(q: usize, rho: f64, u: [f64; 3]) -> f64 {
|
||
let c = c(q);
|
||
let cu = c[0] * u[0] + c[1] * u[1] + c[2] * u[2];
|
||
let uu = u[0] * u[0] + u[1] * u[1] + u[2] * u[2];
|
||
W.get(q).copied().unwrap_or(0.0) * rho * (1.0 + 3.0 * cu + 4.5 * cu * cu - 1.5 * uu)
|
||
}
|
||
|
||
/// Ladd's moving-wall term for a population bounced back into direction `q` off a wall moving
|
||
/// with `u` (lattice units, reference density 1): `2 w_q c_q·u / c_s²` (Ladd 1994).
|
||
#[inline(always)]
|
||
pub fn moving_wall(q: usize, u: [f64; 3]) -> f64 {
|
||
let c = c(q);
|
||
2.0 * W.get(q).copied().unwrap_or(0.0) * (c[0] * u[0] + c[1] * u[1] + c[2] * u[2]) / CS2
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn moments_of_the_velocity_set() {
|
||
// Σ w = 1, Σ w c = 0, Σ w c_a c_b = c_s² δ_ab, and opposite directions pair up.
|
||
let mut m0 = 0.0;
|
||
let mut m1 = [0.0; 3];
|
||
let mut m2 = [[0.0; 3]; 3];
|
||
for q in 0..Q {
|
||
let c = c(q);
|
||
m0 += W[q];
|
||
for a in 0..3 {
|
||
m1[a] += W[q] * c[a];
|
||
for b in 0..3 {
|
||
m2[a][b] += W[q] * c[a] * c[b];
|
||
}
|
||
}
|
||
let o = opposite(q);
|
||
assert_eq!(opposite(o), q);
|
||
assert_eq!([C[o][0], C[o][1], C[o][2]], [-C[q][0], -C[q][1], -C[q][2]]);
|
||
assert_eq!(W[o], W[q]);
|
||
}
|
||
assert!((m0 - 1.0f64).abs() < 1e-15);
|
||
for a in 0..3 {
|
||
assert!(m1[a].abs() < 1e-15);
|
||
for b in 0..3 {
|
||
let want = if a == b { CS2 } else { 0.0 };
|
||
assert!((m2[a][b] - want).abs() < 1e-15);
|
||
}
|
||
}
|
||
// The equilibrium carries the density and momentum it is built from.
|
||
let (rho, u) = (1.03, [0.02, -0.01, 0.005]);
|
||
let (mut r, mut j) = (0.0, [0.0; 3]);
|
||
for q in 0..Q {
|
||
let f = equilibrium(q, rho, u);
|
||
r += f;
|
||
for a in 0..3 {
|
||
j[a] += f * c(q)[a];
|
||
}
|
||
}
|
||
assert!((r - rho).abs() < 1e-15);
|
||
for a in 0..3 {
|
||
assert!((j[a] - rho * u[a]).abs() < 1e-15);
|
||
}
|
||
}
|
||
}
|