Files
OneCAD-Simulation/crates/lbm/tests/benchmark.rs
T
a17752a49f 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>
2026-10-13 20:03:12 -04:00

68 lines
3.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Benchmark suite: comparisons with published numerical reference values (not analytic; the
//! analytic suite in `verification.rs` is the gate). Each case names its source. The cases are
//! long, so they are ignored by default:
//! `cargo test --release -p modelcraft-lbm --test benchmark -- --ignored --nocapture`.
mod common;
use common::*;
use modelcraft_lbm::{Limits, solve_fluid};
use modelcraft_model::{FaceCondition, Procedure, Profile};
/// Benchmark "2D-1" (steady flow around a cylinder at Re = 20): M. Schäfer, S. Turek, *Benchmark
/// computations of laminar flow around a cylinder*, in: Flow Simulation with High-Performance
/// Computers II, NNFM 52:547–566, Vieweg 1996, DOI 10.1007/978-3-322-89849-4_39.
/// Channel 2.2 × 0.41, cylinder of diameter 0.1 centred at (0.2, 0.2), parabolic inlet with peak
/// 0.3 (mean 0.2), ν = 10⁻³, ρ = 1. Reference intervals: drag coefficient 5.57–5.59, lift
/// coefficient 0.0104–0.0110, pressure difference between the cylinder's front and back
/// 0.1172–0.1176. Run quasi-2D: one periodic cell across z.
/// Returns (c_D, c_L, Δp).
fn schafer_turek_2d1(dx: f64, time_step: Option<f64>) -> (f64, f64, f64) {
let (h, l, d, um, nu) = (0.41, 2.2, 0.1, 0.3, 1e-3);
let mut dom = domain([0.0; 3], [l, h, dx], dx);
dom.faces.x_min = FaceCondition::Velocity { velocity: [um, 0.0, 0.0], profile: Profile::Parabolic };
dom.faces.x_max = FaceCondition::Pressure { pressure: 0.0 };
dom.faces.z_min = FaceCondition::Periodic {};
dom.faces.z_max = FaceCondition::Periodic {};
dom.obstacles = vec!["Cylinder".into()];
let cyl = obstacle("Cylinder", cylinder([0.2, 0.2], d / 2.0, -0.1, 0.1, 1024), &["side", "bottom", "top"]);
let procedure = Procedure::Fluid { domain: "Flow".into(), end_time: Some(10.0), steps: None, time_step, frames: 1, magic: None };
let m = model(1.0, nu, dom, procedure);
let r = solve_fluid(&m, None, std::slice::from_ref(&cyl), &Limits::default()).unwrap();
let side = r.forces.iter().find(|f| f.name == "side").unwrap();
// The mean over the last tenth of the run, per unit depth.
let n = side.force.len();
let tail = &side.force[n - n / 10..];
let mean = |a: usize| tail.iter().map(|v| v[a]).sum::<f64>() / tail.len() as f64 / dx;
let mean_u = 2.0 / 3.0 * um;
let coefficient = |f: f64| 2.0 * f / (mean_u * mean_u * d);
// Pressure in the fluid cells just in front of and behind the cylinder on its axis
// (the average of the two cell rows about y = 0.2).
let p = |x: f64| {
let at = |y: f64| f64::from(r.probe(1, [x, y, 0.5 * dx]).unwrap().2);
0.5 * (at(0.2 - 0.5 * dx) + at(0.2 + 0.5 * dx))
};
let dp = p(0.15 - 0.5 * dx) - p(0.25 + 0.5 * dx);
println!("Schäfer–Turek 2D-1, Δx = {dx}: {:?}; warnings {:?}", r.stats, r.warnings);
(coefficient(mean(0)), coefficient(mean(1)), dp)
}
#[test]
#[ignore]
fn schafer_turek_2d1_cylinder_drag_and_lift() {
// Lattice velocity 0.025 (Mach 0.043): the inlet's pressure, about 0.3, changes the density
// by Δp / (ρ c_s²) in lattice units, so the time step shrinks with the spacing squared.
for dx in [0.01, 0.005] {
let dt = 0.025 * dx / 0.3;
let (cd, cl, dp) = schafer_turek_2d1(dx, Some(dt * (dx / 0.01)));
println!("Schäfer–Turek 2D-1, D/Δx = {}: c_D = {cd:.4} (5.57–5.59), c_L = {cl:.4} (0.0104–0.0110), Δp = {dp:.4} (0.1172–0.1176)", 0.1 / dx);
if dx <= 0.005 {
// A staircase cylinder of 20 cells (measured: c_D 5.773, +3.4%; c_L 0.0107, inside
// the interval). Δp is read in the cells half a spacing off the staircase, where the
// pressure changes fast, so it is reported, not asserted (measured 0.108).
assert!((cd / 5.58 - 1.0).abs() < 0.05, "{cd}");
assert!((cl / 0.0107 - 1.0).abs() < 0.2, "{cl}");
}
}
}