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
@@ -30,6 +30,16 @@ pub const MAX_HISTORY_SAMPLES: f64 = 1_000_000.0;
|
||||
/// Most initial velocities or rigid walls an explicit dynamic step lists.
|
||||
pub const MAX_STEP_ITEMS: usize = 10_000;
|
||||
|
||||
/// Most time steps a fluid step asks for (the solver caps a run lower by default).
|
||||
pub const MAX_FLUID_STEPS: u64 = 1_000_000_000;
|
||||
|
||||
/// Most output frames a fluid step writes.
|
||||
pub const MAX_FLUID_FRAMES: u32 = 1000;
|
||||
|
||||
fn default_frames() -> u32 {
|
||||
10
|
||||
}
|
||||
|
||||
/// What a step solves.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
@@ -47,6 +57,25 @@ pub enum Procedure {
|
||||
},
|
||||
/// Transient dynamics by explicit time integration up to `end_time`.
|
||||
ExplicitDynamic(ExplicitDynamic),
|
||||
/// Time-dependent incompressible flow in a fluid domain, from rest (or the domain's initial
|
||||
/// velocity) to `end_time`, or for `steps` time steps of `time_step` (exactly one of the two
|
||||
/// lengths). The solver picks the time step when `time_step` is left out. `frames` velocity
|
||||
/// and pressure fields are written, evenly spaced, after the initial one; forces on every
|
||||
/// wall are recorded throughout. `magic` is the lattice Boltzmann two-relaxation-time
|
||||
/// parameter Λ (default 3/16, which puts bounce-back walls exactly halfway between cells).
|
||||
Fluid {
|
||||
domain: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
end_time: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
steps: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
time_step: Option<f64>,
|
||||
#[serde(default = "default_frames")]
|
||||
frames: u32,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
magic: Option<f64>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The settings of an explicit dynamic step.
|
||||
@@ -281,6 +310,15 @@ impl Procedure {
|
||||
Procedure::Static {} => "static",
|
||||
Procedure::Frequency { .. } => "frequency",
|
||||
Procedure::ExplicitDynamic(_) => "explicit_dynamic",
|
||||
Procedure::Fluid { .. } => "fluid",
|
||||
}
|
||||
}
|
||||
|
||||
/// The fluid domain a fluid step solves in.
|
||||
pub fn fluid_domain(&self) -> Option<&str> {
|
||||
match self {
|
||||
Procedure::Fluid { domain, .. } => Some(domain),
|
||||
Procedure::Static {} | Procedure::Frequency { .. } | Procedure::ExplicitDynamic(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +331,7 @@ impl Procedure {
|
||||
pub fn sets(&self) -> impl Iterator<Item = &str> {
|
||||
let explicit = match self {
|
||||
Procedure::ExplicitDynamic(e) => Some(e),
|
||||
Procedure::Static {} | Procedure::Frequency { .. } => None,
|
||||
Procedure::Static {} | Procedure::Frequency { .. } | Procedure::Fluid { .. } => None,
|
||||
};
|
||||
explicit.into_iter().flat_map(ExplicitDynamic::sets)
|
||||
}
|
||||
@@ -302,6 +340,27 @@ impl Procedure {
|
||||
if let Procedure::ExplicitDynamic(e) = self {
|
||||
return e.validate();
|
||||
}
|
||||
if let Procedure::Fluid { end_time, steps, time_step, frames, magic, .. } = *self {
|
||||
match (end_time, steps) {
|
||||
(Some(t), None) => {
|
||||
crate::positive("end time", t)?;
|
||||
}
|
||||
(None, Some(n)) if (1..=MAX_FLUID_STEPS).contains(&n) => {}
|
||||
(None, Some(n)) => return Err(Error::Invalid(format!("a fluid step runs 1 to {MAX_FLUID_STEPS} time steps, got {n}"))),
|
||||
_ => return Err(Error::Invalid("a fluid step needs exactly one of `end_time` and `steps`".into())),
|
||||
}
|
||||
if let Some(dt) = time_step {
|
||||
crate::positive("time step", dt)?;
|
||||
}
|
||||
if !(1..=MAX_FLUID_FRAMES).contains(&frames) {
|
||||
return Err(Error::Invalid(format!("a fluid step writes 1 to {MAX_FLUID_FRAMES} frames, got {frames}")));
|
||||
}
|
||||
if let Some(m) = magic
|
||||
&& !(m > 0.0 && m <= 1.0)
|
||||
{
|
||||
return Err(Error::Invalid(format!("the two-relaxation-time parameter Λ must be above 0 and at most 1, got {m}")));
|
||||
}
|
||||
}
|
||||
if let Procedure::Frequency { modes, min_hz, max_hz } = *self {
|
||||
if !(1..=MAX_MODES).contains(&modes) {
|
||||
return Err(Error::Invalid(format!("a frequency step extracts 1 to {MAX_MODES} modes, got {modes}")));
|
||||
@@ -326,6 +385,8 @@ impl Procedure {
|
||||
Procedure::Static {} => OutputVariable::ALL.into_iter().collect(),
|
||||
Procedure::Frequency { .. } => [OutputVariable::Displacement].into_iter().collect(),
|
||||
Procedure::ExplicitDynamic(_) => [OutputVariable::Displacement, OutputVariable::Stress].into_iter().collect(),
|
||||
// Velocity, pressure and wall forces, always.
|
||||
Procedure::Fluid { .. } => BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,6 +428,20 @@ impl Step {
|
||||
pub fn new(name: impl Into<String>, procedure: Procedure) -> Step {
|
||||
Step { name: name.into(), output: procedure.default_output(), procedure, boundary_conditions: BTreeSet::new(), loads: BTreeSet::new() }
|
||||
}
|
||||
|
||||
/// Change the procedure. Moving between a structural and a fluid procedure starts from the
|
||||
/// new procedure's default output and, for a fluid step, nothing active (a fluid step takes
|
||||
/// its conditions from its domain); between structural procedures the output and table stay.
|
||||
pub fn set_procedure(&mut self, procedure: Procedure) {
|
||||
if procedure.fluid_domain().is_some() != self.procedure.fluid_domain().is_some() {
|
||||
self.output = procedure.default_output();
|
||||
if procedure.fluid_domain().is_some() {
|
||||
self.boundary_conditions.clear();
|
||||
self.loads.clear();
|
||||
}
|
||||
}
|
||||
self.procedure = procedure;
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@@ -415,6 +490,16 @@ impl Model {
|
||||
for set in step.procedure.sets() {
|
||||
require(&self.sets, "set", set).map_err(|e| context("step", &step.name, e))?;
|
||||
}
|
||||
if let Some(domain) = step.procedure.fluid_domain() {
|
||||
require(&self.fluid_domains, "fluid domain", domain).map_err(|e| context("step", &step.name, e))?;
|
||||
if !(step.boundary_conditions.is_empty() && step.loads.is_empty() && step.output.is_empty()) {
|
||||
return Err(context(
|
||||
"step",
|
||||
&step.name,
|
||||
Error::Invalid("a fluid step takes its conditions from its fluid domain and always writes velocity, pressure and wall forces: list no boundary conditions, loads or output".into()),
|
||||
));
|
||||
}
|
||||
}
|
||||
for b in &step.boundary_conditions {
|
||||
require(&self.boundary_conditions, "boundary condition", b).map_err(|e| context("step", &step.name, e))?;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user