rtx-cfd: curvilinear collocated PISO on a structured patch (overset A-P0, WIP) — PatchMesh (right-handed s,n; periodic seam with shift; face metrics), patch generators (TFI, skewed annulus, sheared/varying-skew channels), CSR + Jacobi-BiCGSTAB, the Zang–Street–Koseff incremental step with the node-based 9-point L_f, LSQ gradients, explicit and line-implicit-n predictors, adjustPhi; tests: mesh metrics (5 green), operators exact on linear fields incl. the seam (green), sparse (2 green), MMS ladder (Cartesian 16/32: 1.37–1.39x the staggered error, order 0.83; n=64 stalls at a |du/dt| floor 2e-4 — open, tolerance-scaling hypothesis), annulus/Poiseuille not yet run
CI / Distributed Training Tests (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
1347bc6772
commit
52da75a3a9
@@ -0,0 +1,184 @@
|
||||
//! P0 gate 3 (`docs/overset_metal_campaign.md` §5.3): plane Poiseuille
|
||||
//! flow on the patch. On the Cartesian AND the affinely sheared periodic
|
||||
//! channel the discrete fixed point is exactly the `poiseuille.rs`
|
||||
//! profile (congruent parallelogram cells: the non-orthogonal corrections
|
||||
//! cancel by translation invariance) — recovered to 1e-8 with `v` and the
|
||||
//! pressure spread at the same level. On a channel of smoothly varying
|
||||
//! skew and stretch the error against the parabola falls at second
|
||||
//! order. Cell mass is conserved to 1e-12 everywhere.
|
||||
|
||||
use rtx_cfd::mesh::PatchMesh;
|
||||
use rtx_cfd::mesh::patch_gen::{cartesian, channel_sheared, channel_varying_skew};
|
||||
use rtx_cfd::solvers::incompressible::{
|
||||
CurvilinearParameters, CurvilinearPisoSolver, NormalDiffusion, PatchField,
|
||||
};
|
||||
use rtx_cfd::{CfdConfig, CfdResult};
|
||||
|
||||
const MU: f64 = 0.1;
|
||||
const G: f64 = 0.8;
|
||||
|
||||
fn u_exact(y: f64) -> f64 {
|
||||
G / (2.0 * MU) * y * (1.0 - y)
|
||||
}
|
||||
|
||||
/// The 1-D discrete channel profile with half-cell wall closures
|
||||
/// (`tests/poiseuille.rs`).
|
||||
fn discrete_profile(n: usize) -> Vec<f64> {
|
||||
let h = 1.0 / n as f64;
|
||||
let rhs_value = -G * h * h / MU;
|
||||
let mut diag = vec![-2.0; n];
|
||||
diag[0] = -3.0;
|
||||
diag[n - 1] = -3.0;
|
||||
let mut rhs = vec![rhs_value; n];
|
||||
let upper = vec![1.0; n];
|
||||
for j in 1..n {
|
||||
let factor = 1.0 / diag[j - 1];
|
||||
diag[j] -= factor * upper[j - 1];
|
||||
rhs[j] -= factor * rhs[j - 1];
|
||||
}
|
||||
let mut u = vec![0.0; n];
|
||||
u[n - 1] = rhs[n - 1] / diag[n - 1];
|
||||
for j in (0..n - 1).rev() {
|
||||
u[j] = (rhs[j] - upper[j] * u[j + 1]) / diag[j];
|
||||
}
|
||||
u
|
||||
}
|
||||
|
||||
struct Steady {
|
||||
field: PatchField,
|
||||
mesh: PatchMesh,
|
||||
worst_mass: f64,
|
||||
}
|
||||
|
||||
async fn steady(mesh: PatchMesh, diffusion: NormalDiffusion, dt_factor: f64) -> CfdResult<Steady> {
|
||||
let mut h = f64::INFINITY;
|
||||
for c in 0..mesh.cell_count() {
|
||||
for (f, _) in mesh.cell_faces(c) {
|
||||
let d = mesh.faces()[f].d;
|
||||
h = h.min((d[0] * d[0] + d[1] * d[1]).sqrt());
|
||||
}
|
||||
}
|
||||
let dt = dt_factor * 0.4 * h * h / (4.0 * MU);
|
||||
let config = CfdConfig::new().with_density(1.0).with_viscosity(MU);
|
||||
let params = CurvilinearParameters {
|
||||
tolerance: 1e-11,
|
||||
normal_diffusion: diffusion,
|
||||
..CurvilinearParameters::default()
|
||||
};
|
||||
let mut solver = CurvilinearPisoSolver::new(config, params, mesh)?;
|
||||
solver.set_boundary_velocity(|_, _, _| (0.0, 0.0));
|
||||
solver.set_momentum_source(|_, _, _| (G, 0.0));
|
||||
let mut field = PatchField::new(solver.mesh());
|
||||
solver.initialize(&mut field, |_, _| (0.0, 0.0));
|
||||
// Mass defect at the steady state, relative to the largest face flux
|
||||
// (during the transient it is the pressure solver's residual).
|
||||
let mut worst_mass = 0.0_f64;
|
||||
let mut converged = false;
|
||||
for _ in 0..2_000_000 {
|
||||
let before = field.u.clone();
|
||||
let r = solver.advance(&mut field, dt).await?;
|
||||
assert!(r.poisson_converged, "{r:?}");
|
||||
let scale: f64 = field.flux.iter().map(|f| f.abs()).fold(0.0, f64::max);
|
||||
worst_mass = r.max_divergence / scale.max(1e-300);
|
||||
let change = field
|
||||
.u
|
||||
.iter()
|
||||
.zip(&before)
|
||||
.map(|(a, b)| (a - b).abs())
|
||||
.fold(0.0, f64::max);
|
||||
if change / dt < 1e-11 {
|
||||
converged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(converged, "no steady state");
|
||||
let mesh = solver.mesh().clone();
|
||||
Ok(Steady {
|
||||
field,
|
||||
mesh,
|
||||
worst_mass,
|
||||
})
|
||||
}
|
||||
|
||||
fn max_vs_discrete(s: &Steady, n: usize) -> (f64, f64, f64) {
|
||||
let u_hat = discrete_profile(n);
|
||||
let (mut du, mut v, mut pmin, mut pmax) = (0.0_f64, 0.0_f64, f64::INFINITY, f64::NEG_INFINITY);
|
||||
for c in 0..s.mesh.cell_count() {
|
||||
let (k, _) = s.mesh.cell_ki(c);
|
||||
du = du.max((s.field.u[c] - u_hat[k]).abs());
|
||||
v = v.max(s.field.v[c].abs());
|
||||
pmin = pmin.min(s.field.p[c]);
|
||||
pmax = pmax.max(s.field.p[c]);
|
||||
}
|
||||
(du, v, pmax - pmin)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cartesian_and_sheared_channels_hit_the_discrete_profile_exactly() -> CfdResult<()> {
|
||||
let n = 16;
|
||||
let shapes: [(&str, Box<dyn Fn() -> CfdResult<PatchMesh>>); 3] = [
|
||||
(
|
||||
"cartesian",
|
||||
Box::new(move || cartesian(n, n, 1.0, 1.0, true)),
|
||||
),
|
||||
(
|
||||
"sheared 0.4",
|
||||
Box::new(move || channel_sheared(1.0, 1.0, n, n, 0.4, true)),
|
||||
),
|
||||
(
|
||||
"sheared -0.7",
|
||||
Box::new(move || channel_sheared(1.0, 1.0, n, n, -0.7, true)),
|
||||
),
|
||||
];
|
||||
for (name, mesh) in &shapes {
|
||||
for diffusion in [NormalDiffusion::Explicit, NormalDiffusion::LineImplicit] {
|
||||
let s = steady(mesh()?, diffusion, 1.0).await?;
|
||||
let (du, v, dp) = max_vs_discrete(&s, n);
|
||||
println!(
|
||||
"{name} {diffusion:?}: |u - u_hat| {du:.3e}, |v| {v:.3e}, p spread {dp:.3e}, mass {:.3e}",
|
||||
s.worst_mass
|
||||
);
|
||||
assert!(du < 1e-8, "{name} {diffusion:?}: |u - u_hat| = {du:.3e}");
|
||||
assert!(v < 1e-8, "{name} {diffusion:?}: |v| = {v:.3e}");
|
||||
assert!(dp < 1e-8, "{name} {diffusion:?}: p spread {dp:.3e}");
|
||||
assert!(
|
||||
s.worst_mass < 1e-12,
|
||||
"{name} {diffusion:?}: mass defect {:.3e}",
|
||||
s.worst_mass
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn varying_skew_channel_converges_to_the_parabola_at_second_order() -> CfdResult<()> {
|
||||
let mut errs = Vec::new();
|
||||
let mut vs = Vec::new();
|
||||
for n in [16usize, 32, 64] {
|
||||
let s = steady(
|
||||
channel_varying_skew(1.0, 1.0, n, n, 0.3, 2.0, true)?,
|
||||
NormalDiffusion::Explicit,
|
||||
1.0,
|
||||
)
|
||||
.await?;
|
||||
let (mut e, mut v) = (0.0_f64, 0.0_f64);
|
||||
for c in 0..s.mesh.cell_count() {
|
||||
let y = s.mesh.centre(c)[1];
|
||||
e = e.max((s.field.u[c] - u_exact(y)).abs());
|
||||
v = v.max(s.field.v[c].abs());
|
||||
}
|
||||
println!(
|
||||
"varying skew n={n}: |u - parabola| {e:.3e}, |v| {v:.3e}, mass {:.3e}",
|
||||
s.worst_mass
|
||||
);
|
||||
assert!(s.worst_mass < 1e-12);
|
||||
errs.push(e);
|
||||
vs.push(v);
|
||||
}
|
||||
let o: Vec<f64> = errs.windows(2).map(|p| (p[0] / p[1]).log2()).collect();
|
||||
let ov: Vec<f64> = vs.windows(2).map(|p| (p[0] / p[1]).log2()).collect();
|
||||
println!("varying skew orders u {o:?}, v {ov:?}");
|
||||
assert!(o.iter().all(|&x| x >= 1.8), "orders {o:?}");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user