forked from 1b-agent/fieldcraft-upstream
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:
55 files changed
+3800
-64
No files matched your search
@@ -20,6 +20,8 @@ pub enum Kind<'a> {
|
||||
Static(&'a StaticResult),
|
||||
Frequency(&'a FrequencyResult),
|
||||
Explicit(&'a ExplicitResult),
|
||||
/// Per cell, not per node: no nodal fields to display yet.
|
||||
Fluid(&'a modelcraft_lbm::FluidResult),
|
||||
}
|
||||
|
||||
impl StepResult {
|
||||
@@ -28,6 +30,7 @@ impl StepResult {
|
||||
StepResult::Implicit(modelcraft_solver::StepResult::Static(r)) => Kind::Static(r),
|
||||
StepResult::Implicit(modelcraft_solver::StepResult::Frequency(r)) => Kind::Frequency(r),
|
||||
StepResult::Explicit(r) => Kind::Explicit(r),
|
||||
StepResult::Fluid(r) => Kind::Fluid(r),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,8 +149,10 @@ impl Field {
|
||||
Kind::Static(_) => (true, false),
|
||||
Kind::Frequency(_) => (false, false),
|
||||
Kind::Explicit(e) => e.frames.last().map_or((false, false), |f| (!f.stress.is_empty(), !f.velocity.is_empty())),
|
||||
// Cell fields have their own display (M12 streamlines); no nodal field.
|
||||
Kind::Fluid(_) => return None.into_iter().flatten(),
|
||||
};
|
||||
Field::ALL.into_iter().filter(move |f| if *f == Field::Velocity { velocity } else { stress || !f.is_stress() })
|
||||
Some(Field::ALL.into_iter().filter(move |f| if *f == Field::Velocity { velocity } else { stress || !f.is_stress() })).into_iter().flatten()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +227,7 @@ pub fn displacements(r: &StepResult, pick: Pick) -> Result<&[[f64; 3]]> {
|
||||
}
|
||||
Ok(&f.displacement)
|
||||
}
|
||||
Kind::Fluid(f) => Err(EngineError::Params(format!("step `{}` is a fluid step: its fields are per cell (solve.probe)", f.step))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +239,7 @@ pub fn field_values(model: &Model, r: &StepResult, field: Field, pick: Pick) ->
|
||||
let stresses = || -> Result<std::borrow::Cow<'_, [[f64; 6]]>> {
|
||||
match r.kind() {
|
||||
Kind::Static(s) => Ok(std::borrow::Cow::Borrowed(s.stress.as_slice())),
|
||||
Kind::Frequency(_) => Err(unavailable()),
|
||||
Kind::Frequency(_) | Kind::Fluid(_) => Err(unavailable()),
|
||||
Kind::Explicit(e) => {
|
||||
let f = frame(e, pick)?;
|
||||
if f.stress.is_empty() {
|
||||
@@ -255,7 +261,7 @@ pub fn field_values(model: &Model, r: &StepResult, field: Field, pick: Pick) ->
|
||||
let f = frame(e, pick)?;
|
||||
if f.velocity.is_empty() { Err(unavailable()) } else { Ok(f.velocity.iter().map(|v| magnitude(*v)).collect()) }
|
||||
}
|
||||
Kind::Static(_) | Kind::Frequency(_) => Err(unavailable()),
|
||||
Kind::Static(_) | Kind::Frequency(_) | Kind::Fluid(_) => Err(unavailable()),
|
||||
},
|
||||
Field::VonMises => each(&modelcraft_fem::von_mises),
|
||||
Field::MaxPrincipal => principal(0),
|
||||
@@ -338,6 +344,9 @@ impl Session {
|
||||
out["time"] = json!(f.time);
|
||||
}
|
||||
}
|
||||
Kind::Fluid(f) => {
|
||||
out["frames"] = f.frames.iter().map(|f| f.time).collect();
|
||||
}
|
||||
Kind::Static(_) => {}
|
||||
}
|
||||
out
|
||||
|
||||
Reference in new issue
Block a user