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

+20 -1
View File
@@ -50,6 +50,8 @@ pub enum EngineError {
Solver(#[from] modelcraft_solver::Error),
#[error(transparent)]
Explicit(#[from] modelcraft_explicit::Error),
#[error(transparent)]
Fluid(#[from] modelcraft_lbm::Error),
/// A command panicked; the model is unchanged.
#[error("internal error in {command}: {message}")]
Internal { command: String, message: String },
@@ -57,11 +59,12 @@ pub enum EngineError {
pub type Result<T> = std::result::Result<T, EngineError>;
/// The result of a solved step, from the implicit or the explicit solver.
/// The result of a solved step, from the implicit, the explicit or the lattice Boltzmann solver.
#[derive(Debug, Clone, PartialEq)]
pub enum StepResult {
Implicit(modelcraft_solver::StepResult),
Explicit(ExplicitResult),
Fluid(modelcraft_lbm::FluidResult),
}
impl StepResult {
@@ -69,6 +72,7 @@ impl StepResult {
match self {
StepResult::Implicit(r) => r.step(),
StepResult::Explicit(r) => &r.step,
StepResult::Fluid(r) => &r.step,
}
}
@@ -77,6 +81,8 @@ impl StepResult {
match self {
StepResult::Implicit(r) => r.nodes(),
StepResult::Explicit(r) => &r.nodes,
// A fluid result is per cell, not per node.
StepResult::Fluid(_) => &[],
}
}
}
@@ -357,6 +363,19 @@ mod tests {
json!({"step": "Bend", "loads": ["nope"], "active": "yes"}),
json!({"step": "Bend", "boundary_conditions": [], "loads": []}),
json!({"system": "furlongs"}),
// Fluid domains, boundaries and steps.
json!({"name": "A", "material": "Steel", "min": [0, 0, 0], "max": [1e308, 1, 1], "spacing": 1e-300}),
json!({"name": "A", "material": "Steel", "min": [0, 0, 0], "max": [1, 1, 1], "spacing": 0.5, "faces": {"x_min": {"type": "periodic"}}}),
json!({"name": "A", "material": "nope", "min": [0, 0, 0], "max": [1, 1, 1], "spacing": 0.5, "obstacles": ["", ""]}),
json!({"name": "A", "density": 1, "fluid": {"type": "newtonian", "viscosity": -1}}),
json!({"name": "A", "fluid": {"type": "newtonian", "viscosity": 1}}),
json!({"domain": "A", "at": "x_mid", "condition": {"type": "wall"}}),
json!({"domain": "A", "at": {"body": "B", "face": "f"}, "condition": {"type": "pressure", "pressure": 1e308}}),
json!({"domain": null, "at": null, "condition": null}),
json!({"name": "A", "procedure": {"type": "fluid", "domain": "A", "steps": 1e300}}),
json!({"name": "A", "procedure": {"type": "fluid", "domain": "A", "end_time": -1, "steps": 3}}),
json!({"name": "Bend", "procedure": {"type": "fluid", "domain": "nope", "end_time": 1}}),
json!({"step": "Bend", "max_cells": 1e300, "max_steps": -3}),
json!({"geometry": {"body": "Bar"}, "elements": [1, 99], "faces": [{"element": 1, "face": 200}]}),
json!({"geometry": {"body": "Bar", "vertex": "v"}, "nodes": [1], "edges": [{"element": 1, "edge": 0}]}),
];