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:
authored and GitHub committed 2026-10-13 20:03:12 -04:00
1 parent bd55abea7f
commit a17752a49f
55 files changed
+3800 -64

No files matched your search

+6 -2
View File
@@ -136,7 +136,9 @@ impl Model {
let mut resolved = Resolved::default();
let units = self.units;
if self.elements.is_empty() {
// A model of fluid steps alone needs no mesh: its domains carry their own grid.
let fluid_only = !self.steps.is_empty() && self.steps.iter().all(|s| s.procedure.fluid_domain().is_some());
if self.elements.is_empty() && !fluid_only {
d.error("model", "the mesh has no elements");
}
if units.length().is_none() {
@@ -248,7 +250,7 @@ impl Model {
let why = match s.procedure {
Procedure::Frequency { .. } => "extracts frequencies",
Procedure::ExplicitDynamic(_) => "is dynamic",
Procedure::Static {} => return None,
Procedure::Static {} | Procedure::Fluid { .. } => return None,
};
Some(format!("step `{}` {why}", s.name))
})
@@ -373,6 +375,8 @@ impl Model {
d.warning(&subject, "nothing loads or moves the model in this step");
}
}
// The fluid domain holds its own conditions; nothing structural applies.
Procedure::Fluid { .. } => continue,
}
// A free body is a natural case of transient dynamics (flight, impact).
if s.boundary_conditions.is_empty() && !s.procedure.is_dynamic() {