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
@@ -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");
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -15,7 +15,7 @@ use modelcraft_model::{
|
||||
// Models
|
||||
|
||||
fn steel() -> Material {
|
||||
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9) }
|
||||
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9), fluid: None }
|
||||
}
|
||||
|
||||
fn faces(list: &[(u32, u8)]) -> Set {
|
||||
@@ -91,7 +91,7 @@ fn plate() -> Model {
|
||||
m.add_set("top", faces(&[(1, 1), (2, 1), (3, 1), (4, 1)])).unwrap();
|
||||
m.add_set("edge", Set { nodes: vec![1, 4, 7], ..Set::default() }).unwrap();
|
||||
m.add_set("mid", Set { nodes: vec![2, 5, 8], ..Set::default() }).unwrap();
|
||||
let aluminium = Material { elastic: Some(Elastic::Isotropic { young: 70_000.0, poisson: 0.33 }), density: Some(2.7e-9) };
|
||||
let aluminium = Material { elastic: Some(Elastic::Isotropic { young: 70_000.0, poisson: 0.33 }), density: Some(2.7e-9), fluid: None };
|
||||
m.add_material("Aluminium", aluminium).unwrap();
|
||||
let kind = SectionKind::Shell { thickness: 2.0, offset: 0.5, points: 5 };
|
||||
let formulation = Formulation { integration: Integration::Reduced, ..Formulation::default() };
|
||||
@@ -523,7 +523,8 @@ fn largest_labels_longest_numbers_and_names_fit_a_line() {
|
||||
m.set_element(modelcraft_model::MAX_LABEL, ElementKind::Hex20, (0..20).map(|i| base + i).collect()).unwrap();
|
||||
let long = format!("A{}", "x".repeat(modelcraft_model::MAX_NAME_LEN - 1));
|
||||
m.add_set(&long, Set { elements: vec![modelcraft_model::MAX_LABEL], nodes: (0..20).map(|i| base + i).collect(), ..Set::default() }).unwrap();
|
||||
m.add_material(&long, Material { elastic: Some(Elastic::Isotropic { young: ugly.abs(), poisson: 0.3 }), density: Some(1.0) }).unwrap();
|
||||
m.add_material(&long, Material { elastic: Some(Elastic::Isotropic { young: ugly.abs(), poisson: 0.3 }), density: Some(1.0), fluid: None })
|
||||
.unwrap();
|
||||
m.add_section(&long, Section { set: long.clone(), material: long.clone(), kind: SectionKind::Solid {}, formulation: Formulation::default() })
|
||||
.unwrap();
|
||||
m.add_reference_point(&long, ReferencePoint { xyz: [ugly; 3], label: 0 }).unwrap();
|
||||
@@ -639,7 +640,7 @@ fn what_abaqus_cannot_express_is_named() {
|
||||
|
||||
let mut ortho = plate();
|
||||
let carbon = Elastic::EngineeringConstants { young: [140e3, 10e3, 10e3], poisson: [0.3, 0.3, 0.4], shear: [5e3, 5e3, 3.8e3] };
|
||||
ortho.replace_material("Aluminium", Material { elastic: Some(carbon), density: Some(1.6e-9) }).unwrap();
|
||||
ortho.replace_material("Aluminium", Material { elastic: Some(carbon), density: Some(1.6e-9), fluid: None }).unwrap();
|
||||
assert!(unsupported(&ortho).contains("orthotropic material `Aluminium` in a shell section"));
|
||||
|
||||
let mut loose = frequency();
|
||||
@@ -657,7 +658,7 @@ fn what_abaqus_cannot_express_is_named() {
|
||||
fn orthotropic_solids_beams_and_rods_are_written() {
|
||||
let mut m = cantilever();
|
||||
let carbon = Elastic::EngineeringConstants { young: [140e3, 10e3, 10e3], poisson: [0.3, 0.3, 0.4], shear: [5e3, 5e3, 3.8e3] };
|
||||
m.replace_material("Steel", Material { elastic: Some(carbon), density: None }).unwrap();
|
||||
m.replace_material("Steel", Material { elastic: Some(carbon), density: None, fluid: None }).unwrap();
|
||||
let deck = abaqus::write(&m).unwrap();
|
||||
assert!(deck.contains("*ELASTIC, TYPE=ENGINEERING CONSTANTS\n140000.0, 10000.0, 10000.0, 0.3, 0.3, 0.4, 5000.0, 5000.0\n3800.0\n"), "{deck}");
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use modelcraft_model::*;
|
||||
// Our own models.
|
||||
|
||||
fn steel() -> Material {
|
||||
Material { elastic: Some(Elastic::Isotropic { young: 210000.0, poisson: 0.3 }), density: Some(7.85e-9) }
|
||||
Material { elastic: Some(Elastic::Isotropic { young: 210000.0, poisson: 0.3 }), density: Some(7.85e-9), fluid: None }
|
||||
}
|
||||
|
||||
fn step(name: &str, procedure: Procedure, bcs: &[&str], loads: &[&str]) -> Step {
|
||||
@@ -72,7 +72,7 @@ fn cantilever_modes() -> Model {
|
||||
let mut m = Model::default();
|
||||
cantilever_mesh(&mut m);
|
||||
let ortho = Elastic::EngineeringConstants { young: [140000.0, 10000.0, 10000.0], poisson: [0.3, 0.3, 0.45], shear: [5000.0, 5000.0, 3500.0] };
|
||||
m.add_material("Ply", Material { elastic: Some(ortho), density: Some(1.6e-9) }).unwrap();
|
||||
m.add_material("Ply", Material { elastic: Some(ortho), density: Some(1.6e-9), fluid: None }).unwrap();
|
||||
let f = Formulation { incompatible_modes: true, ..Formulation::default() };
|
||||
m.add_section("beam", Section { set: "all".into(), material: "Ply".into(), kind: SectionKind::Solid {}, formulation: f }).unwrap();
|
||||
m.add_reference_point("RP", ReferencePoint { xyz: [5.0, 0.5, 0.5], label: 0 }).unwrap();
|
||||
@@ -155,7 +155,8 @@ fn solids() -> Model {
|
||||
let h: Vec<u32> = h.iter().map(|p| node(&mut m, *p)).collect();
|
||||
m.set_element(4, ElementKind::Hex20, h).unwrap();
|
||||
|
||||
m.add_material("Aluminium", Material { elastic: Some(Elastic::Isotropic { young: 70e9, poisson: 0.33 }), density: Some(2700.0) }).unwrap();
|
||||
m.add_material("Aluminium", Material { elastic: Some(Elastic::Isotropic { young: 70e9, poisson: 0.33 }), density: Some(2700.0), fluid: None })
|
||||
.unwrap();
|
||||
for (e, name) in [(1, "tet4"), (2, "tet10"), (3, "wedge"), (4, "hex20")] {
|
||||
m.add_set(name, Set { elements: vec![e], ..Set::default() }).unwrap();
|
||||
let f = Formulation { hybrid: e == 1, ..Formulation::default() };
|
||||
@@ -657,7 +658,7 @@ fn hostile_numbers_stay_within_their_fields() {
|
||||
m.set_node(2, [f64::MIN_POSITIVE, -f64::MIN_POSITIVE, 5e-324]).unwrap();
|
||||
m.replace_material(
|
||||
"Steel",
|
||||
Material { elastic: Some(Elastic::Isotropic { young: 1e300, poisson: 0.499_999_999_999_999_9 }), density: Some(1e-300) },
|
||||
Material { elastic: Some(Elastic::Isotropic { young: 1e300, poisson: 0.499_999_999_999_999_9 }), density: Some(1e-300), fluid: None },
|
||||
)
|
||||
.unwrap();
|
||||
m.replace_load(
|
||||
|
||||
@@ -131,7 +131,7 @@ impl Read {
|
||||
// Our own models.
|
||||
|
||||
fn steel() -> Material {
|
||||
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9) }
|
||||
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9), fluid: None }
|
||||
}
|
||||
|
||||
/// A bar of `n` unit hex8 along X; node (i, y, z) = 1 + 4i + 2y + z.
|
||||
@@ -188,7 +188,11 @@ fn plate() -> Model {
|
||||
m.add_set("all", Set { elements: vec![1, 2, 3, 4], ..Set::default() }).unwrap();
|
||||
m.add_set("edge", Set { nodes: vec![1, 4, 7], ..Set::default() }).unwrap();
|
||||
m.add_set("top", Set { faces: (1..=4).map(|element| ElementFace { element, face: 1 }).collect(), ..Set::default() }).unwrap();
|
||||
m.add_material("Aluminium", Material { elastic: Some(Elastic::Isotropic { young: 70_000.0, poisson: 0.33 }), density: Some(2.7e-9) }).unwrap();
|
||||
m.add_material(
|
||||
"Aluminium",
|
||||
Material { elastic: Some(Elastic::Isotropic { young: 70_000.0, poisson: 0.33 }), density: Some(2.7e-9), fluid: None },
|
||||
)
|
||||
.unwrap();
|
||||
let shell = SectionKind::Shell { thickness: 2.0, offset: 0.0, points: 5 };
|
||||
m.add_section("skin", Section { set: "all".into(), material: "Aluminium".into(), kind: shell, formulation: Formulation::default() }).unwrap();
|
||||
m.add_boundary_condition("clamp", BoundaryCondition::Fixed { target: Target::Set("edge".into()) }).unwrap();
|
||||
@@ -517,6 +521,7 @@ fn beams_rods_orthotropy_couplings_and_prescribed_motion() {
|
||||
let ortho = Material {
|
||||
elastic: Some(Elastic::EngineeringConstants { young: [140e3, 10e3, 12e3], poisson: [0.3, 0.28, 0.4], shear: [5e3, 4.5e3, 3.8e3] }),
|
||||
density: Some(1.6e-9),
|
||||
fluid: None,
|
||||
};
|
||||
m.add_material("Carbon", ortho.clone()).unwrap();
|
||||
m.replace_section(
|
||||
@@ -671,7 +676,7 @@ fn what_nastran_cannot_express_is_named() {
|
||||
// Orthotropic beams, tilted orthotropic shells, one-point shells.
|
||||
let mut m = plate();
|
||||
let ortho = Elastic::EngineeringConstants { young: [140e3, 10e3, 10e3], poisson: [0.3, 0.3, 0.4], shear: [5e3, 5e3, 3.8e3] };
|
||||
m.replace_material("Aluminium", Material { elastic: Some(ortho), density: Some(1.6e-9) }).unwrap();
|
||||
m.replace_material("Aluminium", Material { elastic: Some(ortho), density: Some(1.6e-9), fluid: None }).unwrap();
|
||||
m.set_node(9, [10.0, 10.0, 0.5]).unwrap();
|
||||
assert!(unsupported(&m).contains("planes of constant Z"));
|
||||
m.set_node(9, [10.0, 10.0, 0.0]).unwrap();
|
||||
@@ -690,7 +695,7 @@ fn what_nastran_cannot_express_is_named() {
|
||||
let mut m = plate();
|
||||
m.set_element(9, ElementKind::Rod2, vec![1, 9]).unwrap();
|
||||
m.add_set("bars", Set { elements: vec![9], ..Set::default() }).unwrap();
|
||||
m.add_material("Fibre", Material { elastic: Some(ortho), density: Some(1.6e-9) }).unwrap();
|
||||
m.add_material("Fibre", Material { elastic: Some(ortho), density: Some(1.6e-9), fluid: None }).unwrap();
|
||||
m.add_section(
|
||||
"bars",
|
||||
Section { set: "bars".into(), material: "Fibre".into(), kind: SectionKind::Rod { area: 1.0 }, formulation: Formulation::default() },
|
||||
|
||||
Reference in new issue
Block a user