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

+2
View File
@@ -104,6 +104,7 @@ pub fn write(model: &Model) -> Result<String> {
/// [`write`] for a model already resolved.
pub fn write_resolved(model: &Model, r: &Resolved) -> Result<String> {
crate::refuse_fluid(FORMAT, model)?;
let by_type = elements_by_type(model, r)?;
check_expressible(model)?;
let mut d = Deck::default();
@@ -757,6 +758,7 @@ impl Deck {
for step in steps {
self.comment(&format!("Step {}", step.name));
match step.procedure {
Procedure::Fluid { .. } => return Err(crate::fluid_step(FORMAT, &step.name)),
Procedure::Static {} => {
self.line(&format!("*STEP, NAME={}, NLGEOM=NO", step.name));
self.line("*STATIC");
+13
View File
@@ -83,6 +83,19 @@ pub fn with_steps(model: &Model, names: &[String]) -> Result<Model> {
Ok(out)
}
/// A fluid step: solved by ModelCraft's lattice Boltzmann solver, never written to a deck.
pub(crate) fn fluid_step(format: &'static str, step: &str) -> Error {
Error::Unsupported { format, what: format!("fluid step `{step}` (solve it with solve.fluid; write the structural steps only)") }
}
/// Every writer's first check: the model has no fluid step.
pub(crate) fn refuse_fluid(format: &'static str, model: &Model) -> Result<()> {
match model.steps().iter().find(|s| s.procedure.fluid_domain().is_some()) {
Some(step) => Err(fluid_step(format, &step.name)),
None => Ok(()),
}
}
/// Every deck format, one row per writer.
pub const FORMATS: &[Format] = &[
Format { name: "abaqus", extension: "inp", writer: abaqus::write_resolved },
+2
View File
@@ -126,6 +126,7 @@ pub(crate) fn write_resolved(model: &Model, r: &Resolved) -> Result<String> {
}
fn write_with(model: &Model, r: &Resolved) -> Result<Written> {
crate::refuse_fluid(FORMAT, model)?;
let estimate = (model.nodes().len() + model.reference_points().len()) * 130 + model.elements().len() * 260;
if estimate > MAX_DECK_BYTES {
return Err(Error::Limit(format!("the deck would be about {estimate} bytes; at most {MAX_DECK_BYTES} are written")));
@@ -566,6 +567,7 @@ fn write_step(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan, step: &Ste
d.header(&["imflag", "dt0"]);
d.card([Field::from(1), Field::from(1.0)]);
match step.procedure {
Procedure::Fluid { .. } => return Err(crate::fluid_step(FORMAT, &step.name)),
Procedure::Static {} => {
d.line("*CONTROL_IMPLICIT_SOLUTION");
d.header(&["nsolvr"]);
+1
View File
@@ -60,6 +60,7 @@ pub fn write(model: &Model) -> Result<String> {
/// [`write`] for a model already resolved.
pub fn write_resolved(model: &Model, r: &Resolved) -> Result<String> {
crate::refuse_fluid(FORMAT, model)?;
let mut deck = Deck::new(model, r, model.steps().iter().collect());
deck.plan()?;
deck.emit()