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
@@ -1,4 +1,4 @@
|
||||
//! Materials: linear elasticity and mass density.
|
||||
//! Materials: linear elasticity, mass density and fluid behaviour.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -60,6 +60,24 @@ impl Elastic {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fluid behaviour.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Fluid {
|
||||
/// A Newtonian fluid of this dynamic viscosity μ (shear stress per shear rate; for example
|
||||
/// 1.0e-9 MPa·s for water in mm, t, s). Its kinematic viscosity is μ over the density.
|
||||
Newtonian { viscosity: f64 },
|
||||
}
|
||||
|
||||
impl Fluid {
|
||||
/// The dynamic viscosity μ.
|
||||
pub fn viscosity(&self) -> f64 {
|
||||
match *self {
|
||||
Fluid::Newtonian { viscosity } => viscosity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A material.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -69,12 +87,21 @@ pub struct Material {
|
||||
/// Mass per volume, needed by frequency steps and gravity.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub density: Option<f64>,
|
||||
/// How it flows, for fluid domains (needs a density).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fluid: Option<Fluid>,
|
||||
}
|
||||
|
||||
impl Material {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.elastic.is_none() && self.density.is_none() {
|
||||
return Err(Error::Invalid("a material needs elastic constants or a density".into()));
|
||||
if self.elastic.is_none() && self.density.is_none() && self.fluid.is_none() {
|
||||
return Err(Error::Invalid("a material needs elastic constants, a density or fluid behaviour".into()));
|
||||
}
|
||||
if let Some(f) = &self.fluid {
|
||||
positive("viscosity", f.viscosity())?;
|
||||
if self.density.is_none() {
|
||||
return Err(Error::Invalid("a fluid needs a density".into()));
|
||||
}
|
||||
}
|
||||
if let Some(e) = &self.elastic {
|
||||
e.validate()?;
|
||||
@@ -102,11 +129,17 @@ impl Model {
|
||||
|
||||
pub fn replace_material(&mut self, name: &str, material: Material) -> Result<()> {
|
||||
material.validate().map_err(|e| context("material", name, e))?;
|
||||
if material.fluid.is_none()
|
||||
&& let Some(d) = self.fluid_domain_users(name).next()
|
||||
{
|
||||
return Err(Error::Invalid(format!("material `{name}`: fluid domain `{d}` needs its fluid behaviour")));
|
||||
}
|
||||
replace_existing(&mut self.materials, "material", name, material)
|
||||
}
|
||||
|
||||
pub fn remove_material(&mut self, name: &str) -> Result<()> {
|
||||
let users = self.sections.iter().filter(|(_, s)| s.material == name).map(|(n, _)| format!("section `{n}`")).collect();
|
||||
let mut users: Vec<String> = self.sections.iter().filter(|(_, s)| s.material == name).map(|(n, _)| format!("section `{n}`")).collect();
|
||||
users.extend(self.fluid_domain_users(name).map(|n| format!("fluid domain `{n}`")));
|
||||
not_in_use("material", name, users)?;
|
||||
remove_existing(&mut self.materials, "material", name).map(|_| ())
|
||||
}
|
||||
@@ -117,7 +150,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn iso(young: f64, poisson: f64) -> Material {
|
||||
Material { elastic: Some(Elastic::Isotropic { young, poisson }), density: None }
|
||||
Material { elastic: Some(Elastic::Isotropic { young, poisson }), ..Material::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -129,12 +162,17 @@ mod tests {
|
||||
let e = m.add_material("Bad", bad).unwrap_err();
|
||||
assert!(e.to_string().contains("material `Bad`"), "{e}");
|
||||
}
|
||||
assert!(m.add_material("Light", Material { elastic: None, density: Some(-1.0) }).is_err());
|
||||
assert!(m.add_material("Light", Material { density: Some(-1.0), ..Material::default() }).is_err());
|
||||
let fluid = |viscosity, density| Material { density, fluid: Some(Fluid::Newtonian { viscosity }), ..Material::default() };
|
||||
m.add_material("Water", fluid(1.0e-9, Some(1.0e-9))).unwrap();
|
||||
for bad in [fluid(0.0, Some(1.0)), fluid(-1.0, Some(1.0)), fluid(f64::NAN, Some(1.0)), fluid(1.0, None)] {
|
||||
assert!(m.add_material("Bad", bad).is_err());
|
||||
}
|
||||
assert!(m.add_material("1st", iso(1.0, 0.3)).is_err(), "names start with a letter");
|
||||
assert!(m.add_material("has space", iso(1.0, 0.3)).is_err());
|
||||
m.replace_material("Steel", iso(2.0e5, 0.29)).unwrap();
|
||||
assert!(m.replace_material("Nope", iso(2.0e5, 0.29)).is_err());
|
||||
assert_eq!(m.materials().len(), 1);
|
||||
assert_eq!(m.materials().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user