rtx-cfd/rtx-fsi: overset A-P0 GATED + M1 precision probe — curvilinear collocated PISO: relative-reduction pressure stop (the absolute stop floored |du/dt| at 2e-4 on 64²), line-implicit-n sign fix, adjustPhi; gates: Cartesian reduction 1.37–1.40x the staggered error at orders 0.83/0.90; skewed stretched periodic annulus Stokes orders 2.30/2.06 (explicit and line-implicit), upwind 1.08/0.80; Poiseuille exact to 1e-9 on Cartesian and affine-sheared periodic channels (both diffusion variants), varying-skew channel order 2.02 (v 1.9), cell mass 1e-14; divergence ≤ 1e-11 relative every step; snapshot/restore bit-identical. M1: poisson.rs multigrid hierarchy generic over MgScalar (f32/f64), f64 CG keeps its own fine level; MgPrecision on MultigridParameters/EmbeddedParameters/PisoParameters, set_poisson_precision, harness RTX_FSI2_POISSON_F32 (march + noise probe, printed marker); f64 arm bit-identical in vivo (FSI2 default line-for-line with 08-31), f32 arm holds the noise floor and stall pins and the FSI2 band; poisson_equivalence f32 arm
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (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 / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (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:
Omar Sobh
2026-09-04 12:40:43 -07:00
co-authored by Claude Fable 5.1
parent 52da75a3a9
commit c63d79c300
22 changed files with 341 additions and 77 deletions
@@ -124,6 +124,7 @@ pub fn channel_sheared(
/// A channel whose skew varies smoothly along x: `x' = x + alpha · y · /// A channel whose skew varies smoothly along x: `x' = x + alpha · y ·
/// sin(2π x / lx)` (zero at both ends, so it can be periodic), with the /// sin(2π x / lx)` (zero at both ends, so it can be periodic), with the
/// across-channel spacing stretched by `stretch` toward the top wall. /// across-channel spacing stretched by `stretch` toward the top wall.
/// Folds when `2π alpha ly / lx > 1`; keep `alpha` around 0.1.
pub fn channel_varying_skew( pub fn channel_varying_skew(
lx: f64, lx: f64,
ly: f64, ly: f64,
@@ -95,7 +95,12 @@ pub struct CurvilinearParameters {
/// Projections per step (the first removes the divergence; the rest /// Projections per step (the first removes the divergence; the rest
/// mop up inner-solver truncation). /// mop up inner-solver truncation).
pub corrector_steps: usize, pub corrector_steps: usize,
/// Pressure-correction stop, relative to the step's flux scale. /// Pressure-correction stop: each projection reduces the L1 cell mass
/// imbalance by this factor (plus a rounding floor of 1e-15 × Σ|F|).
/// Relative to the incoming divergence, not to the flux scale, so the
/// stop tightens as the flow settles — an absolute stop leaves a
/// velocity-noise floor that grows with the grid (measured: |du/dt|
/// floored at 2e-4 on the 64² Cartesian MMS with `1e-10 × Σ|F|`).
pub tolerance: f64, pub tolerance: f64,
/// Convection scheme. /// Convection scheme.
pub convection: PatchConvection, pub convection: PatchConvection,
@@ -111,7 +116,7 @@ impl Default for CurvilinearParameters {
fn default() -> Self { fn default() -> Self {
Self { Self {
corrector_steps: 2, corrector_steps: 2,
tolerance: 1e-8, tolerance: 1e-4,
convection: PatchConvection::Upwind, convection: PatchConvection::Upwind,
normal_diffusion: NormalDiffusion::Explicit, normal_diffusion: NormalDiffusion::Explicit,
boundaries: PatchBoundaries::default(), boundaries: PatchBoundaries::default(),
@@ -304,22 +309,24 @@ impl CurvilinearPisoSolver {
} }
let (_, matrix, anchor) = self.matrix.as_ref().expect("assembled"); let (_, matrix, anchor) = self.matrix.as_ref().expect("assembled");
let flux_scale: f64 = field.flux.iter().map(|f| f.abs()).sum::<f64>().max(1e-300); let flux_scale: f64 = field.flux.iter().map(|f| f.abs()).sum::<f64>().max(1e-300);
let tolerance = self.params.tolerance * flux_scale; let floor = 1e-15 * flux_scale;
let mut iterations = 0; let mut iterations = 0;
let mut converged = true; let mut converged = true;
let mut performed = 0; let mut performed = 0;
let mut max_div = self.max_divergence(&field.flux); let mut max_div = self.max_divergence(&field.flux);
for _ in 0..self.params.corrector_steps { for _ in 0..self.params.corrector_steps {
let incoming = self.divergence_l1(&field.flux);
if incoming <= floor {
break;
}
let tolerance = self.params.tolerance * incoming + floor;
let (pc, out) = self.solve_pressure_correction(matrix, *anchor, &field.flux, tolerance); let (pc, out) = self.solve_pressure_correction(matrix, *anchor, &field.flux, tolerance);
iterations += out.iterations; iterations += out.iterations;
converged &= out.converged; converged &= out.converged;
self.apply_correction(field, &pc, dt); self.apply_correction(field, &pc, dt);
performed += 1; performed += 1;
max_div = self.max_divergence(&field.flux); max_div = self.max_divergence(&field.flux);
if max_div <= tolerance {
break;
}
} }
self.time = t_new; self.time = t_new;
Ok(CurvilinearResult { Ok(CurvilinearResult {
@@ -64,21 +64,22 @@ impl CurvilinearPisoSolver {
let mut lu = ops.face_gradient_flux(mesh, f, &field.u, &un, bu); let mut lu = ops.face_gradient_flux(mesh, f, &field.u, &un, bu);
let mut lv = ops.face_gradient_flux(mesh, f, &field.v, &vn, bv); let mut lv = ops.face_gradient_flux(mesh, f, &field.v, &vn, bv);
if implicit_n && is_n && (side.is_none() || bval.is_some()) { if implicit_n && is_n && (side.is_none() || bval.is_some()) {
// The orthogonal part, `alpha (u_other u_c)` into the
// cell whichever side owns the face, goes implicit: take
// it out of the explicit flux here, put it back in
// `solve_lines`.
let alpha = ops.face(f).alpha; let alpha = ops.face(f).alpha;
let (pu, pv) = match (face.owner, face.neigh) { let (pu, pv) = match (face.owner, face.neigh) {
(Some(p), Some(q)) => { (Some(p), Some(q)) => {
let other = if p == c { q } else { p }; let other = if p == c { q } else { p };
( (
sign * alpha * (field.u[other] - field.u[c]), alpha * (field.u[other] - field.u[c]),
sign * alpha * (field.v[other] - field.v[c]), alpha * (field.v[other] - field.v[c]),
) )
} }
_ => { _ => {
let b = bval.expect("dirichlet"); let b = bval.expect("dirichlet");
( (alpha * (b.0 - field.u[c]), alpha * (b.1 - field.v[c]))
sign * alpha * (b.0 - field.u[c]),
sign * alpha * (b.1 - field.v[c]),
)
} }
}; };
lu = sign * lu - pu; lu = sign * lu - pu;
@@ -207,6 +207,20 @@ impl CurvilinearPisoSolver {
} }
} }
/// Total cell mass imbalance `Σ_c |Σ_f sign F_f|`.
pub(super) fn divergence_l1(&self, flux: &[f64]) -> f64 {
let mesh = &self.mesh;
(0..mesh.cell_count())
.map(|c| {
mesh.cell_faces(c)
.iter()
.map(|&(f, sign)| sign * flux[f])
.sum::<f64>()
.abs()
})
.sum()
}
/// Largest cell mass imbalance `|Σ sign F_f|`. /// Largest cell mass imbalance `|Σ sign F_f|`.
pub(super) fn max_divergence(&self, flux: &[f64]) -> f64 { pub(super) fn max_divergence(&self, flux: &[f64]) -> f64 {
let mesh = &self.mesh; let mesh = &self.mesh;
@@ -38,7 +38,9 @@
use super::ale::{AleBoundaries, SideBoundary}; use super::ale::{AleBoundaries, SideBoundary};
use super::embedded_body::{EmbeddedBody, EmbeddedMask, FaceKind}; use super::embedded_body::{EmbeddedBody, EmbeddedMask, FaceKind};
use super::poisson::{MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg}; use super::poisson::{
MgPrecision, MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg,
};
use super::simple::ConvectionScheme; use super::simple::ConvectionScheme;
use super::{FlowField, SolverResult}; use super::{FlowField, SolverResult};
use crate::{CfdConfig, CfdError, CfdResult}; use crate::{CfdConfig, CfdError, CfdResult};
@@ -61,6 +63,12 @@ pub struct EmbeddedParameters {
/// [`PoissonSolverKind::Sor`]). Both solve the same system to the same /// [`PoissonSolverKind::Sor`]). Both solve the same system to the same
/// true-residual stop; multigrid's cost is mesh-independent. /// true-residual stop; multigrid's cost is mesh-independent.
pub poisson_solver: PoissonSolverKind, pub poisson_solver: PoissonSolverKind,
/// Precision of the multigrid V-cycle (default [`MgPrecision::F64`] =
/// bit-identical). `F32` is the M1 precision probe of
/// `overset_metal_campaign.md` §3.2 / §5.2: the CG, its true residual
/// and the stop stay f64; only the preconditioner runs in single
/// precision. No effect with [`PoissonSolverKind::Sor`].
pub poisson_precision: MgPrecision,
/// Convective face values in the explicit predictor (default /// Convective face values in the explicit predictor (default
/// [`ConvectionScheme::Upwind`], which is bit-identical to the fixed-grid /// [`ConvectionScheme::Upwind`], which is bit-identical to the fixed-grid
/// PISO). The TVD schemes add SIMPLE's limited correction to each /// PISO). The TVD schemes add SIMPLE's limited correction to each
@@ -82,6 +90,7 @@ impl Default for EmbeddedParameters {
tolerance: 1e-6, tolerance: 1e-6,
boundaries: AleBoundaries::default(), boundaries: AleBoundaries::default(),
poisson_solver: PoissonSolverKind::Sor, poisson_solver: PoissonSolverKind::Sor,
poisson_precision: MgPrecision::F64,
convection_scheme: ConvectionScheme::Upwind, convection_scheme: ConvectionScheme::Upwind,
} }
} }
@@ -174,6 +183,12 @@ impl EmbeddedPisoSolver {
}) })
} }
/// Precision of the multigrid V-cycle (see
/// [`EmbeddedParameters::poisson_precision`]); the M1 probe knob.
pub fn set_poisson_precision(&mut self, precision: MgPrecision) {
self.parameters.poisson_precision = precision;
}
/// Mask hysteresis for the moving-body rebuild, as a fraction of the /// Mask hysteresis for the moving-body rebuild, as a fraction of the
/// min cell size (default 0, exactly the plain rebuild). With a band, /// min cell size (default 0, exactly the plain rebuild). With a band,
/// a cell within `band * h_min` of the surface keeps the /// a cell within `band * h_min` of the surface keeps the
@@ -902,7 +917,10 @@ impl EmbeddedPisoSolver {
let solution = solve_multigrid_pcg( let solution = solve_multigrid_pcg(
&problem, &problem,
&mut p_prime, &mut p_prime,
&MultigridParameters::default(), &MultigridParameters {
precision: self.parameters.poisson_precision,
..MultigridParameters::default()
},
inner_stop, inner_stop,
anchor_cell, anchor_cell,
); );
@@ -57,7 +57,9 @@ pub use flow_field::FlowField;
pub use piso::{PisoParameters, PisoResult, PisoSolver}; pub use piso::{PisoParameters, PisoResult, PisoSolver};
#[cfg(feature = "cuda")] #[cfg(feature = "cuda")]
pub use piso_gpu::PisoGpuSolver; pub use piso_gpu::PisoGpuSolver;
pub use poisson::{MultigridParameters, PoissonProblem, PoissonSolution, PoissonSolverKind}; pub use poisson::{
MgPrecision, MultigridParameters, PoissonProblem, PoissonSolution, PoissonSolverKind,
};
pub use polygon_sdf::PolygonSdf; pub use polygon_sdf::PolygonSdf;
pub use simple::{ConvectionScheme, SimpleParameters, SimpleResult, SimpleSolver}; pub use simple::{ConvectionScheme, SimpleParameters, SimpleResult, SimpleSolver};
#[cfg(feature = "cuda")] #[cfg(feature = "cuda")]
@@ -37,7 +37,9 @@
//! - Convective face fluxes fell back to the centre value at the sweep edges //! - Convective face fluxes fell back to the centre value at the sweep edges
//! instead of using the prescribed boundary faces that exist there. //! instead of using the prescribed boundary faces that exist there.
use super::poisson::{MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg}; use super::poisson::{
MgPrecision, MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg,
};
use super::{BoundaryConditions, FlowField, IncompressibleSolver, SolverResult}; use super::{BoundaryConditions, FlowField, IncompressibleSolver, SolverResult};
use crate::{CfdConfig, CfdResult}; use crate::{CfdConfig, CfdResult};
use async_trait::async_trait; use async_trait::async_trait;
@@ -59,6 +61,9 @@ pub struct PisoParameters {
/// [`PoissonSolverKind::Sor`]). Both solve the same system to the same /// [`PoissonSolverKind::Sor`]). Both solve the same system to the same
/// true-residual stop; multigrid's cost is mesh-independent. /// true-residual stop; multigrid's cost is mesh-independent.
pub poisson_solver: PoissonSolverKind, pub poisson_solver: PoissonSolverKind,
/// Precision of the multigrid V-cycle (default [`MgPrecision::F64`] =
/// bit-identical; `F32` = the M1 precision probe, no effect with SOR).
pub poisson_precision: MgPrecision,
} }
impl Default for PisoParameters { impl Default for PisoParameters {
@@ -68,6 +73,7 @@ impl Default for PisoParameters {
time_step: 0.001, time_step: 0.001,
tolerance: 1e-6, tolerance: 1e-6,
poisson_solver: PoissonSolverKind::Sor, poisson_solver: PoissonSolverKind::Sor,
poisson_precision: MgPrecision::F64,
} }
} }
} }
@@ -398,7 +404,10 @@ impl PisoSolver {
let solution = solve_multigrid_pcg( let solution = solve_multigrid_pcg(
&problem, &problem,
&mut p_prime, &mut p_prime,
&MultigridParameters::default(), &MultigridParameters {
precision: self.parameters.poisson_precision,
..MultigridParameters::default()
},
inner_stop, inner_stop,
Some(nx + 1), Some(nx + 1),
); );
@@ -252,9 +252,27 @@ impl PoissonProblem {
} }
} }
/// Arithmetic precision of the multigrid PRECONDITIONER (the conjugate
/// gradient around it, its true residual, the mean projections and the
/// stopping rule stay `f64`). `F32` is the registered mixed-precision
/// design of `overset_metal_campaign.md` §3.2 M1 / §5.2: it decides
/// whether an fp32-only device (Apple Metal) can carry the pressure solve
/// of a coupled march. Default `F64` = bit-identical to the historical
/// solver.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MgPrecision {
/// Double precision throughout (the default; bit-identical).
#[default]
F64,
/// Single-precision V-cycle inside the double-precision CG.
F32,
}
/// Multigrid preconditioner parameters. /// Multigrid preconditioner parameters.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MultigridParameters { pub struct MultigridParameters {
/// Precision of the V-cycle (see [`MgPrecision`]).
pub precision: MgPrecision,
/// Symmetric GaussSeidel sweeps before AND after the coarse correction /// Symmetric GaussSeidel sweeps before AND after the coarse correction
/// (default 2; a value of 0 is treated as 1). One count for both on /// (default 2; a value of 0 is treated as 1). One count for both on
/// purpose: unequal pre/post counts make the V-cycle non-symmetric and /// purpose: unequal pre/post counts make the V-cycle non-symmetric and
@@ -271,6 +289,7 @@ pub struct MultigridParameters {
impl Default for MultigridParameters { impl Default for MultigridParameters {
fn default() -> Self { fn default() -> Self {
Self { Self {
precision: MgPrecision::F64,
smoother_sweeps: 2, smoother_sweeps: 2,
coarsest_cells: 32, coarsest_cells: 32,
max_iterations: 500, max_iterations: 500,
@@ -335,14 +354,77 @@ const COARSE_CORRECTION: f64 = 2.0;
/// Hard cap on the number of levels (a 1×1 coarsest is reached long before). /// Hard cap on the number of levels (a 1×1 coarsest is reached long before).
const MAX_LEVELS: usize = 64; const MAX_LEVELS: usize = 64;
/// One level of the hierarchy: the problem, its diagonal, the list of /// The scalar the V-cycle runs in. `f64` reproduces the historical solver
/// active cells that carry an equation (row-major), the parent map into the /// bit for bit (same operations in the same order on the same values);
/// next coarser level, and the V-cycle work vectors. /// `f32` is the mixed-precision probe.
struct Level { pub trait MgScalar:
Copy
+ PartialEq
+ PartialOrd
+ std::ops::Add<Output = Self>
+ std::ops::Sub<Output = Self>
+ std::ops::Mul<Output = Self>
+ std::ops::Div<Output = Self>
+ std::ops::AddAssign
+ Send
+ Sync
+ 'static
{
/// Zero.
const ZERO: Self;
/// From double.
fn from_f64(v: f64) -> Self;
/// To double.
fn to_f64(self) -> f64;
/// Absolute value.
fn abs(self) -> Self;
}
impl MgScalar for f64 {
const ZERO: Self = 0.0;
#[inline]
fn from_f64(v: f64) -> Self {
v
}
#[inline]
fn to_f64(self) -> f64 {
self
}
#[inline]
fn abs(self) -> Self {
f64::abs(self)
}
}
impl MgScalar for f32 {
const ZERO: Self = 0.0;
#[inline]
fn from_f64(v: f64) -> Self {
v as f32
}
#[inline]
fn to_f64(self) -> f64 {
f64::from(self)
}
#[inline]
fn abs(self) -> Self {
f32::abs(self)
}
}
/// One level of the hierarchy: the problem (kept in `f64` for coarsening
/// and inspection), its coefficients and diagonal in the V-cycle scalar,
/// the list of active cells that carry an equation (row-major), and the
/// parent map into the next coarser level.
struct Level<T: MgScalar> {
problem: PoissonProblem, problem: PoissonProblem,
/// `problem.active && ap > 0`: cells with an equation. /// `problem.active && ap > 0`: cells with an equation.
active: Vec<bool>, active: Vec<bool>,
ap: Vec<f64>, ae: Vec<T>,
aw: Vec<T>,
an: Vec<T>,
as_: Vec<T>,
ap: Vec<T>,
/// Row-major indices of the active cells. /// Row-major indices of the active cells.
cells: Vec<usize>, cells: Vec<usize>,
/// Fine index → coarse index (empty on the coarsest level). /// Fine index → coarse index (empty on the coarsest level).
@@ -350,26 +432,26 @@ struct Level {
} }
/// V-cycle work vectors of one level. /// V-cycle work vectors of one level.
struct Work { struct Work<T: MgScalar> {
/// Right-hand side of the residual equation on this level. /// Right-hand side of the residual equation on this level.
b: Vec<f64>, b: Vec<T>,
/// Correction on this level. /// Correction on this level.
x: Vec<f64>, x: Vec<T>,
/// Residual. /// Residual.
r: Vec<f64>, r: Vec<T>,
} }
impl Work { impl<T: MgScalar> Work<T> {
fn new(n: usize) -> Self { fn new(n: usize) -> Self {
Self { Self {
b: vec![0.0; n], b: vec![T::ZERO; n],
x: vec![0.0; n], x: vec![T::ZERO; n],
r: vec![0.0; n], r: vec![T::ZERO; n],
} }
} }
} }
impl Level { impl<T: MgScalar> Level<T> {
fn new(mut problem: PoissonProblem) -> Self { fn new(mut problem: PoissonProblem) -> Self {
let (nx, ny) = (problem.nx, problem.ny); let (nx, ny) = (problem.nx, problem.ny);
let n = nx * ny; let n = nx * ny;
@@ -405,10 +487,15 @@ impl Level {
} }
} }
let cells: Vec<usize> = (0..n).filter(|&idx| active[idx]).collect(); let cells: Vec<usize> = (0..n).filter(|&idx| active[idx]).collect();
let cast = |v: &[f64]| v.iter().map(|&x| T::from_f64(x)).collect::<Vec<T>>();
Self { Self {
ae: cast(&problem.ae),
aw: cast(&problem.aw),
an: cast(&problem.an),
as_: cast(&problem.as_),
ap: cast(&ap),
problem, problem,
active, active,
ap,
cells, cells,
coarse_of: Vec::new(), coarse_of: Vec::new(),
} }
@@ -418,38 +505,38 @@ impl Level {
/// read (their coefficients are the only non-zero ones after /// read (their coefficients are the only non-zero ones after
/// sanitising). /// sanitising).
#[inline] #[inline]
fn neighbour_sum(&self, x: &[f64], idx: usize) -> f64 { fn neighbour_sum(&self, x: &[T], idx: usize) -> T {
let nx = self.problem.nx; let nx = self.problem.nx;
let mut s = 0.0; let mut s = T::ZERO;
let ae = self.problem.ae[idx]; let ae = self.ae[idx];
if ae != 0.0 { if ae != T::ZERO {
s += ae * x[idx + 1]; s += ae * x[idx + 1];
} }
let aw = self.problem.aw[idx]; let aw = self.aw[idx];
if aw != 0.0 { if aw != T::ZERO {
s += aw * x[idx - 1]; s += aw * x[idx - 1];
} }
let an = self.problem.an[idx]; let an = self.an[idx];
if an != 0.0 { if an != T::ZERO {
s += an * x[idx + nx]; s += an * x[idx + nx];
} }
let as_ = self.problem.as_[idx]; let as_ = self.as_[idx];
if as_ != 0.0 { if as_ != T::ZERO {
s += as_ * x[idx - nx]; s += as_ * x[idx - nx];
} }
s s
} }
/// `y = A x` on the active cells. /// `y = A x` on the active cells.
fn apply(&self, x: &[f64], y: &mut [f64]) { fn apply(&self, x: &[T], y: &mut [T]) {
for &idx in &self.cells { for &idx in &self.cells {
y[idx] = self.ap[idx] * x[idx] - self.neighbour_sum(x, idx); y[idx] = self.ap[idx] * x[idx] - self.neighbour_sum(x, idx);
} }
} }
/// `r = b - A x` on the active cells; returns its L1 norm. /// `r = b - A x` on the active cells; returns its L1 norm.
fn residual(&self, b: &[f64], x: &[f64], r: &mut [f64]) -> f64 { fn residual(&self, b: &[T], x: &[T], r: &mut [T]) -> T {
let mut l1 = 0.0; let mut l1 = T::ZERO;
for &idx in &self.cells { for &idx in &self.cells {
let v = b[idx] - (self.ap[idx] * x[idx] - self.neighbour_sum(x, idx)); let v = b[idx] - (self.ap[idx] * x[idx] - self.neighbour_sum(x, idx));
r[idx] = v; r[idx] = v;
@@ -459,7 +546,7 @@ impl Level {
} }
/// One symmetric GaussSeidel sweep (forward then backward) on `A x = b`. /// One symmetric GaussSeidel sweep (forward then backward) on `A x = b`.
fn symmetric_gs(&self, b: &[f64], x: &mut [f64]) { fn symmetric_gs(&self, b: &[T], x: &mut [T]) {
for &idx in &self.cells { for &idx in &self.cells {
x[idx] = (b[idx] + self.neighbour_sum(x, idx)) / self.ap[idx]; x[idx] = (b[idx] + self.neighbour_sum(x, idx)) / self.ap[idx];
} }
@@ -506,24 +593,24 @@ impl Level {
} }
/// The multigrid hierarchy: level 0 is the fine problem. /// The multigrid hierarchy: level 0 is the fine problem.
pub(crate) struct Hierarchy { pub(crate) struct Hierarchy<T: MgScalar = f64> {
levels: Vec<Level>, levels: Vec<Level<T>>,
work: Vec<Work>, work: Vec<Work<T>>,
sweeps: usize, sweeps: usize,
} }
impl Hierarchy { impl<T: MgScalar> Hierarchy<T> {
/// Builds the hierarchy down to `coarsest_cells` active cells (or until /// Builds the hierarchy down to `coarsest_cells` active cells (or until
/// coarsening stops reducing the count). /// coarsening stops reducing the count).
pub(crate) fn build(problem: &PoissonProblem, params: &MultigridParameters) -> Self { pub(crate) fn build(problem: &PoissonProblem, params: &MultigridParameters) -> Self {
let mut levels = vec![Level::new(problem.clone())]; let mut levels = vec![Level::<T>::new(problem.clone())];
while levels.len() < MAX_LEVELS { while levels.len() < MAX_LEVELS {
let fine = levels.last().expect("at least one level"); let fine = levels.last().expect("at least one level");
if fine.cells.len() <= params.coarsest_cells.max(1) { if fine.cells.len() <= params.coarsest_cells.max(1) {
break; break;
} }
let (coarse, coarse_of) = fine.coarsen(); let (coarse, coarse_of) = fine.coarsen();
let coarse = Level::new(coarse); let coarse = Level::<T>::new(coarse);
if coarse.cells.len() >= fine.cells.len() { if coarse.cells.len() >= fine.cells.len() {
break; break;
} }
@@ -570,7 +657,7 @@ impl Hierarchy {
let depth = self.levels.len(); let depth = self.levels.len();
let levels = &self.levels; let levels = &self.levels;
for &idx in &levels[0].cells { for &idx in &levels[0].cells {
self.work[0].b[idx] = r[idx]; self.work[0].b[idx] = T::from_f64(r[idx]);
} }
// Down: smooth from zero, restrict the residual. // Down: smooth from zero, restrict the residual.
for l in 0..depth - 1 { for l in 0..depth - 1 {
@@ -578,14 +665,14 @@ impl Hierarchy {
let (head, tail) = self.work.split_at_mut(l + 1); let (head, tail) = self.work.split_at_mut(l + 1);
let (wf, wc) = (&mut head[l], &mut tail[0]); let (wf, wc) = (&mut head[l], &mut tail[0]);
for &idx in &fine.cells { for &idx in &fine.cells {
wf.x[idx] = 0.0; wf.x[idx] = T::ZERO;
} }
for _ in 0..self.sweeps { for _ in 0..self.sweeps {
fine.symmetric_gs(&wf.b, &mut wf.x); fine.symmetric_gs(&wf.b, &mut wf.x);
} }
fine.residual(&wf.b, &wf.x, &mut wf.r); fine.residual(&wf.b, &wf.x, &mut wf.r);
for &idx in &coarse.cells { for &idx in &coarse.cells {
wc.b[idx] = 0.0; wc.b[idx] = T::ZERO;
} }
for &idx in &fine.cells { for &idx in &fine.cells {
let c = fine.coarse_of[idx]; let c = fine.coarse_of[idx];
@@ -601,7 +688,7 @@ impl Hierarchy {
let bottom = &levels[depth - 1]; let bottom = &levels[depth - 1];
let wb = &mut self.work[depth - 1]; let wb = &mut self.work[depth - 1];
for &idx in &bottom.cells { for &idx in &bottom.cells {
wb.x[idx] = 0.0; wb.x[idx] = T::ZERO;
} }
for _ in 0..COARSEST_SWEEPS { for _ in 0..COARSEST_SWEEPS {
bottom.symmetric_gs(&wb.b, &mut wb.x); bottom.symmetric_gs(&wb.b, &mut wb.x);
@@ -613,14 +700,14 @@ impl Hierarchy {
let (head, tail) = self.work.split_at_mut(l + 1); let (head, tail) = self.work.split_at_mut(l + 1);
let (wf, wc) = (&mut head[l], &tail[0]); let (wf, wc) = (&mut head[l], &tail[0]);
for &idx in &fine.cells { for &idx in &fine.cells {
wf.x[idx] += COARSE_CORRECTION * wc.x[fine.coarse_of[idx]]; wf.x[idx] += T::from_f64(COARSE_CORRECTION) * wc.x[fine.coarse_of[idx]];
} }
for _ in 0..self.sweeps { for _ in 0..self.sweeps {
fine.symmetric_gs(&wf.b, &mut wf.x); fine.symmetric_gs(&wf.b, &mut wf.x);
} }
} }
for &idx in &levels[0].cells { for &idx in &levels[0].cells {
z[idx] = self.work[0].x[idx]; z[idx] = self.work[0].x[idx].to_f64();
} }
} }
} }
@@ -649,6 +736,23 @@ pub fn solve_multigrid_pcg(
params: &MultigridParameters, params: &MultigridParameters,
tolerance: f64, tolerance: f64,
anchor: Option<usize>, anchor: Option<usize>,
) -> PoissonSolution {
match params.precision {
MgPrecision::F64 => solve_pcg_with::<f64>(problem, p, params, tolerance, anchor),
MgPrecision::F32 => solve_pcg_with::<f32>(problem, p, params, tolerance, anchor),
}
}
/// The CG driver: `f64` throughout, with the V-cycle in `T`. The fine
/// level used for the CG's own products and true residual is a separate
/// `f64` level, so the precision of the preconditioner never enters the
/// stopping rule or the reported residual.
fn solve_pcg_with<T: MgScalar>(
problem: &PoissonProblem,
p: &mut [f64],
params: &MultigridParameters,
tolerance: f64,
anchor: Option<usize>,
) -> PoissonSolution { ) -> PoissonSolution {
let n = problem.nx * problem.ny; let n = problem.nx * problem.ny;
assert_eq!(p.len(), n, "p must have nx*ny entries"); assert_eq!(p.len(), n, "p must have nx*ny entries");
@@ -658,8 +762,9 @@ pub fn solve_multigrid_pcg(
problem.validate() problem.validate()
); );
let mut hier = Hierarchy::build(problem, params); let mut hier = Hierarchy::<T>::build(problem, params);
let cells: Vec<usize> = hier.levels[0].cells.clone(); let fine = Level::<f64>::new(problem.clone());
let cells: Vec<usize> = fine.cells.clone();
let active_n = cells.len(); let active_n = cells.len();
if active_n == 0 { if active_n == 0 {
return PoissonSolution { return PoissonSolution {
@@ -710,9 +815,9 @@ pub fn solve_multigrid_pcg(
let mut q = vec![0.0; n]; let mut q = vec![0.0; n];
let true_residual = let true_residual =
|p: &[f64], r: &mut [f64], hier: &Hierarchy| -> f64 { hier.levels[0].residual(&b, p, r) }; |p: &[f64], r: &mut [f64], fine: &Level<f64>| -> f64 { fine.residual(&b, p, r) };
let anchor = anchor.filter(|&a| a < n && hier.levels[0].active[a]); let anchor = anchor.filter(|&a| a < n && fine.active[a]);
let finish = |p: &mut [f64], iterations: usize, residual: f64| { let finish = |p: &mut [f64], iterations: usize, residual: f64| {
// Level of each singular component: the anchor's component is // Level of each singular component: the anchor's component is
// shifted so p[anchor] == 0, every other singular component to mean // shifted so p[anchor] == 0, every other singular component to mean
@@ -737,7 +842,7 @@ pub fn solve_multigrid_pcg(
} }
}; };
let mut res = true_residual(p, &mut r, &hier); let mut res = true_residual(p, &mut r, &fine);
if res < tolerance { if res < tolerance {
return finish(p, 0, res); return finish(p, 0, res);
} }
@@ -759,12 +864,12 @@ pub fn solve_multigrid_pcg(
let mut last_true = res; let mut last_true = res;
while iterations < params.max_iterations { while iterations < params.max_iterations {
iterations += 1; iterations += 1;
hier.levels[0].apply(&d, &mut q); fine.apply(&d, &mut q);
let dq = dot(&d, &q); let dq = dot(&d, &q);
if !dq.is_finite() || dq <= 0.0 || !rz.is_finite() || rz <= 0.0 { if !dq.is_finite() || dq <= 0.0 || !rz.is_finite() || rz <= 0.0 {
// Breakdown (r = 0 to rounding, or a non-positive curvature // Breakdown (r = 0 to rounding, or a non-positive curvature
// from rounding in the null space): stop on the true residual. // from rounding in the null space): stop on the true residual.
res = true_residual(p, &mut r, &hier); res = true_residual(p, &mut r, &fine);
return finish(p, iterations, res); return finish(p, iterations, res);
} }
let alpha = rz / dq; let alpha = rz / dq;
@@ -775,7 +880,7 @@ pub fn solve_multigrid_pcg(
if l1(&r) < tolerance { if l1(&r) < tolerance {
// The recurrence residual passed: confirm against the TRUE // The recurrence residual passed: confirm against the TRUE
// residual, and resynchronise if rounding has let them drift. // residual, and resynchronise if rounding has let them drift.
res = true_residual(p, &mut r, &hier); res = true_residual(p, &mut r, &fine);
if res < tolerance || res > 0.9 * last_true { if res < tolerance || res > 0.9 * last_true {
return finish(p, iterations, res); return finish(p, iterations, res);
} }
@@ -792,7 +897,7 @@ pub fn solve_multigrid_pcg(
d[idx] = z[idx] + beta * d[idx]; d[idx] = z[idx] + beta * d[idx];
} }
} }
res = true_residual(p, &mut r, &hier); res = true_residual(p, &mut r, &fine);
finish(p, iterations, res) finish(p, iterations, res)
} }
@@ -446,7 +446,7 @@ fn galerkin_coarse_operator_matches_r_a_p() {
} }
} }
pr.validate().expect("weighted problem is valid"); pr.validate().expect("weighted problem is valid");
let hier = Hierarchy::build(&pr, &MultigridParameters::default()); let hier = Hierarchy::<f64>::build(&pr, &MultigridParameters::default());
assert!(hier.depth() >= 3, "depth {}", hier.depth()); assert!(hier.depth() >= 3, "depth {}", hier.depth());
let mut g = Lcg(5); let mut g = Lcg(5);
for l in 0..hier.depth() - 1 { for l in 0..hier.depth() - 1 {
@@ -570,7 +570,7 @@ fn v_cycle_preconditioner_is_symmetric() {
), ),
("dirichlet", assemble(n, n, |_, _| true, [true; 4])), ("dirichlet", assemble(n, n, |_, _| true, [true; 4])),
] { ] {
let mut hier = Hierarchy::build(&pr, &params); let mut hier = Hierarchy::<f64>::build(&pr, &params);
let mut g = Lcg(11); let mut g = Lcg(11);
let cells = active_indices(&pr); let cells = active_indices(&pr);
let mut x = vec![0.0; n * n]; let mut x = vec![0.0; n * n];
@@ -69,7 +69,7 @@ async fn march(
.unwrap_or(d) .unwrap_or(d)
}; };
let dt = env("RTX_CURV_DTFRAC", 0.4) * (h * h / (4.0 * nu)).min(h); let dt = env("RTX_CURV_DTFRAC", 0.4) * (h * h / (4.0 * nu)).min(h);
let tol = env("RTX_CURV_TOL", 1e-10); let tol = env("RTX_CURV_TOL", 1e-5);
let config = CfdConfig::new() let config = CfdConfig::new()
.with_density(RHO) .with_density(RHO)
.with_viscosity(MU) .with_viscosity(MU)
@@ -61,7 +61,7 @@ async fn steady(mesh: PatchMesh, diffusion: NormalDiffusion, dt_factor: f64) ->
let dt = dt_factor * 0.4 * h * h / (4.0 * MU); let dt = dt_factor * 0.4 * h * h / (4.0 * MU);
let config = CfdConfig::new().with_density(1.0).with_viscosity(MU); let config = CfdConfig::new().with_density(1.0).with_viscosity(MU);
let params = CurvilinearParameters { let params = CurvilinearParameters {
tolerance: 1e-11, tolerance: 1e-6,
normal_diffusion: diffusion, normal_diffusion: diffusion,
..CurvilinearParameters::default() ..CurvilinearParameters::default()
}; };
@@ -72,9 +72,20 @@ async fn steady(mesh: PatchMesh, diffusion: NormalDiffusion, dt_factor: f64) ->
solver.initialize(&mut field, |_, _| (0.0, 0.0)); solver.initialize(&mut field, |_, _| (0.0, 0.0));
// Mass defect at the steady state, relative to the largest face flux // Mass defect at the steady state, relative to the largest face flux
// (during the transient it is the pressure solver's residual). // (during the transient it is the pressure solver's residual).
let env = |k: &str, d: f64| {
std::env::var(k)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(d)
};
// 1e-9: the rounding floor of |du/dt| sits at ~2e-11 on the 32² channels
// (measured: 0 pressure iterations, divergence 1e-16) and the gates are
// at 1e-8.
let steady_tol = env("RTX_CURV_STEADY", 1e-9);
let trace = std::env::var("RTX_CURV_TRACE").is_ok();
let mut worst_mass = 0.0_f64; let mut worst_mass = 0.0_f64;
let mut converged = false; let mut converged = false;
for _ in 0..2_000_000 { for step in 0..2_000_000 {
let before = field.u.clone(); let before = field.u.clone();
let r = solver.advance(&mut field, dt).await?; let r = solver.advance(&mut field, dt).await?;
assert!(r.poisson_converged, "{r:?}"); assert!(r.poisson_converged, "{r:?}");
@@ -86,7 +97,16 @@ async fn steady(mesh: PatchMesh, diffusion: NormalDiffusion, dt_factor: f64) ->
.zip(&before) .zip(&before)
.map(|(a, b)| (a - b).abs()) .map(|(a, b)| (a - b).abs())
.fold(0.0, f64::max); .fold(0.0, f64::max);
if change / dt < 1e-11 { if trace && step % 50_000 == 0 {
println!(
" step {step} t={:.2} |du/dt| {:.3e} iters {} div {:.2e}",
solver.time(),
change / dt,
r.poisson_iterations,
r.max_divergence
);
}
if change / dt < steady_tol {
converged = true; converged = true;
break; break;
} }
@@ -155,9 +175,15 @@ async fn cartesian_and_sheared_channels_hit_the_discrete_profile_exactly() -> Cf
async fn varying_skew_channel_converges_to_the_parabola_at_second_order() -> CfdResult<()> { async fn varying_skew_channel_converges_to_the_parabola_at_second_order() -> CfdResult<()> {
let mut errs = Vec::new(); let mut errs = Vec::new();
let mut vs = Vec::new(); let mut vs = Vec::new();
let only: Option<usize> = std::env::var("RTX_CURV_N")
.ok()
.and_then(|v| v.parse().ok());
for n in [16usize, 32, 64] { for n in [16usize, 32, 64] {
if only.is_some_and(|o| o != n) {
continue;
}
let s = steady( let s = steady(
channel_varying_skew(1.0, 1.0, n, n, 0.3, 2.0, true)?, channel_varying_skew(1.0, 1.0, n, n, 0.1, 2.0, true)?,
NormalDiffusion::Explicit, NormalDiffusion::Explicit,
1.0, 1.0,
) )
@@ -179,6 +205,8 @@ async fn varying_skew_channel_converges_to_the_parabola_at_second_order() -> Cfd
let o: Vec<f64> = errs.windows(2).map(|p| (p[0] / p[1]).log2()).collect(); 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(); let ov: Vec<f64> = vs.windows(2).map(|p| (p[0] / p[1]).log2()).collect();
println!("varying skew orders u {o:?}, v {ov:?}"); println!("varying skew orders u {o:?}, v {ov:?}");
assert!(o.iter().all(|&x| x >= 1.8), "orders {o:?}"); if only.is_none() {
assert!(o.iter().all(|&x| x >= 1.8), "orders {o:?}");
}
Ok(()) Ok(())
} }
@@ -140,6 +140,7 @@ async fn run(moving: bool, dt: f64, t_end: f64) -> CfdResult<Vec<Record>> {
corrector_steps: 2, corrector_steps: 2,
tolerance: 1e-8, tolerance: 1e-8,
poisson_solver: PoissonSolverKind::Multigrid, poisson_solver: PoissonSolverKind::Multigrid,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
..EmbeddedParameters::default() ..EmbeddedParameters::default()
}, },
)?; )?;
@@ -80,6 +80,7 @@ fn solver(n: usize) -> CfdResult<EmbeddedPisoSolver> {
corrector_steps: 2, corrector_steps: 2,
tolerance: 1e-8, tolerance: 1e-8,
poisson_solver: PoissonSolverKind::Multigrid, poisson_solver: PoissonSolverKind::Multigrid,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
..EmbeddedParameters::default() ..EmbeddedParameters::default()
}, },
)?; )?;
@@ -30,8 +30,8 @@
use rtx_cfd::solvers::incompressible::{ use rtx_cfd::solvers::incompressible::{
AleBoundaries, BoundaryConditions, EmbeddedBody, EmbeddedParameters, EmbeddedPisoSolver, AleBoundaries, BoundaryConditions, EmbeddedBody, EmbeddedParameters, EmbeddedPisoSolver,
FaceKind, FlowField, IncompressibleSolver, PisoParameters, PisoSolver, PoissonSolverKind, FaceKind, FlowField, IncompressibleSolver, MgPrecision, PisoParameters, PisoSolver,
SideBoundary, PoissonSolverKind, SideBoundary,
}; };
use rtx_cfd::{CfdConfig, CfdResult}; use rtx_cfd::{CfdConfig, CfdResult};
use std::f64::consts::PI; use std::f64::consts::PI;
@@ -128,6 +128,7 @@ async fn piso_mms(n: usize, kind: PoissonSolverKind) -> CfdResult<SteadyMeasurem
time_step: dt, time_step: dt,
tolerance: 1e-8, tolerance: 1e-8,
poisson_solver: kind, poisson_solver: kind,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
}, },
)?; )?;
solver.set_momentum_source(source); solver.set_momentum_source(source);
@@ -291,6 +292,7 @@ async fn taylor_green(n: usize, kind: PoissonSolverKind) -> CfdResult<TaylorGree
time_step: dt, time_step: dt,
tolerance: 1e-9, tolerance: 1e-9,
poisson_solver: kind, poisson_solver: kind,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
}, },
)?; )?;
@@ -418,6 +420,16 @@ fn mms_initial_field(n: usize) -> CfdResult<FlowField> {
/// `tests/embedded_mms.rs::measure`, velocity error and divergence only, /// `tests/embedded_mms.rs::measure`, velocity error and divergence only,
/// with the inner solver selectable. /// with the inner solver selectable.
async fn embedded_mms(n: usize, kind: PoissonSolverKind) -> CfdResult<SteadyMeasurement> { async fn embedded_mms(n: usize, kind: PoissonSolverKind) -> CfdResult<SteadyMeasurement> {
embedded_mms_with(n, kind, MgPrecision::F64).await
}
/// `embedded_mms` with the multigrid V-cycle precision selectable (the M1
/// precision probe of `overset_metal_campaign.md` §3.2).
async fn embedded_mms_with(
n: usize,
kind: PoissonSolverKind,
precision: MgPrecision,
) -> CfdResult<SteadyMeasurement> {
let dx = 1.0 / n as f64; let dx = 1.0 / n as f64;
let dt = mms_time_step(n); let dt = mms_time_step(n);
let mut solver = EmbeddedPisoSolver::new( let mut solver = EmbeddedPisoSolver::new(
@@ -426,6 +438,7 @@ async fn embedded_mms(n: usize, kind: PoissonSolverKind) -> CfdResult<SteadyMeas
corrector_steps: 2, corrector_steps: 2,
tolerance: 1e-8, tolerance: 1e-8,
poisson_solver: kind, poisson_solver: kind,
poisson_precision: precision,
..EmbeddedParameters::default() ..EmbeddedParameters::default()
}, },
)?; )?;
@@ -526,6 +539,44 @@ async fn embedded_circle_steady_state_is_solver_independent() -> CfdResult<()> {
/// solver assembles exactly the fixed-grid PISO's `PoissonProblem` (all /// solver assembles exactly the fixed-grid PISO's `PoissonProblem` (all
/// cells active, anchor `(1, 1)`, no outlet), so the fields must still agree /// cells active, anchor `(1, 1)`, no outlet), so the fields must still agree
/// to the bit. /// to the bit.
/// M1 precision probe (`overset_metal_campaign.md` §3.2 M1, §5.2): with
/// the V-cycle in single precision inside the f64 conjugate gradient, the
/// embedded-circle steady state must be the same field to the projection
/// stop — the f64 CG owns the true residual, so the preconditioner's
/// precision may cost iterations, never accuracy. The measured gap is
/// printed; the assert is at the tolerance scale, not at f32 eps.
#[tokio::test]
async fn embedded_circle_steady_state_survives_an_f32_vcycle() -> CfdResult<()> {
let n = 32;
let f64_arm = embedded_mms_with(n, PoissonSolverKind::Multigrid, MgPrecision::F64).await?;
let f32_arm = embedded_mms_with(n, PoissonSolverKind::Multigrid, MgPrecision::F32).await?;
println!(
"f32 V-cycle vs f64: L2 error {:.6e} vs {:.6e} (rel {:.2e}), max div {:.2e} vs {:.2e}, \
steps {} vs {}, wall {:.2} s vs {:.2} s",
f32_arm.l2_velocity,
f64_arm.l2_velocity,
rel(f32_arm.l2_velocity, f64_arm.l2_velocity),
f32_arm.max_div,
f64_arm.max_div,
f32_arm.steps,
f64_arm.steps,
f32_arm.seconds,
f64_arm.seconds
);
assert!(
rel(f32_arm.l2_velocity, f64_arm.l2_velocity) < 1e-5,
"f32 V-cycle changed the steady error: {:.6e} vs {:.6e}",
f32_arm.l2_velocity,
f64_arm.l2_velocity
);
assert!(
f32_arm.max_div < 1e-5,
"divergence with the f32 V-cycle {:.3e}",
f32_arm.max_div
);
Ok(())
}
#[tokio::test] #[tokio::test]
async fn without_a_body_the_embedded_solver_is_piso_to_the_bit_with_multigrid() -> CfdResult<()> { async fn without_a_body_the_embedded_solver_is_piso_to_the_bit_with_multigrid() -> CfdResult<()> {
let n = 16; let n = 16;
@@ -537,6 +588,7 @@ async fn without_a_body_the_embedded_solver_is_piso_to_the_bit_with_multigrid()
time_step: dt, time_step: dt,
tolerance: 1e-8, tolerance: 1e-8,
poisson_solver: PoissonSolverKind::Multigrid, poisson_solver: PoissonSolverKind::Multigrid,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
}, },
)?; )?;
piso.set_momentum_source(source); piso.set_momentum_source(source);
@@ -547,6 +599,7 @@ async fn without_a_body_the_embedded_solver_is_piso_to_the_bit_with_multigrid()
corrector_steps: 2, corrector_steps: 2,
tolerance: 1e-8, tolerance: 1e-8,
poisson_solver: PoissonSolverKind::Multigrid, poisson_solver: PoissonSolverKind::Multigrid,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
..EmbeddedParameters::default() ..EmbeddedParameters::default()
}, },
)?; )?;
@@ -611,6 +664,7 @@ async fn channel_with_circle(kind: PoissonSolverKind) -> CfdResult<(FlowField, u
top: SideBoundary::Velocity, top: SideBoundary::Velocity,
}, },
poisson_solver: kind, poisson_solver: kind,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
..EmbeddedParameters::default() ..EmbeddedParameters::default()
}, },
)?; )?;
@@ -119,6 +119,7 @@ async fn run_cfd1(ny: usize) -> CfdResult<Cfd1> {
top: SideBoundary::Velocity, top: SideBoundary::Velocity,
}, },
poisson_solver: PoissonSolverKind::Multigrid, poisson_solver: PoissonSolverKind::Multigrid,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
..EmbeddedParameters::default() ..EmbeddedParameters::default()
}; };
let mut solver = EmbeddedPisoSolver::new(config, params)?; let mut solver = EmbeddedPisoSolver::new(config, params)?;
@@ -126,6 +126,7 @@ impl Runner {
top: SideBoundary::Velocity, top: SideBoundary::Velocity,
}, },
poisson_solver: PoissonSolverKind::Multigrid, poisson_solver: PoissonSolverKind::Multigrid,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
// Upwind's numerical viscosity (|u| h / 2 ~ 10x the physical nu // Upwind's numerical viscosity (|u| h / 2 ~ 10x the physical nu
// on these grids) suppressed CFD3's vortex shedding entirely: // on these grids) suppressed CFD3's vortex shedding entirely:
// the ny = 41 upwind run produced ONE lift zero-crossing in // the ny = 41 upwind run produced ONE lift zero-crossing in
@@ -122,6 +122,10 @@ pub struct MarchConfig {
/// is repeated as 2, 4, 8, 16, 32 coupled substeps of dt/n — see /// is repeated as 2, 4, 8, 16, 32 coupled substeps of dt/n — see
/// `rescue.rs` and omni-cortex `docs/coupling_rescue_campaign.md`. /// `rescue.rs` and omni-cortex `docs/coupling_rescue_campaign.md`.
pub coupling_rescue: bool, pub coupling_rescue: bool,
/// M1 precision probe (`RTX_{prefix}_POISSON_F32`, default off =
/// bit-identical): the pressure multigrid's V-cycle in single
/// precision inside the f64 CG (`overset_metal_campaign.md` §3.2 M1).
pub poisson_f32: bool,
/// Rung C (`RTX_{prefix}_CRESCUE_COARSE`, default 0 = off; needs /// Rung C (`RTX_{prefix}_CRESCUE_COARSE`, default 0 = off; needs
/// `coupling_rescue`): on a trigger, instead of the substep ladder, /// `coupling_rescue`): on a trigger, instead of the substep ladder,
/// reject the step and enter a coarse EPISODE of this many coupled /// reject the step and enter a coarse EPISODE of this many coupled
@@ -208,6 +212,7 @@ impl MarchConfig {
.and_then(|v| v.parse::<usize>().ok()) .and_then(|v| v.parse::<usize>().ok())
.unwrap_or(defaults.trace_from), .unwrap_or(defaults.trace_from),
coupling_rescue: num("CRESCUE", f64::from(u8::from(defaults.coupling_rescue))) != 0.0, coupling_rescue: num("CRESCUE", f64::from(u8::from(defaults.coupling_rescue))) != 0.0,
poisson_f32: num("POISSON_F32", f64::from(u8::from(defaults.poisson_f32))) != 0.0,
coarse_episode: num("CRESCUE_COARSE", defaults.coarse_episode as f64) as usize, coarse_episode: num("CRESCUE_COARSE", defaults.coarse_episode as f64) as usize,
inc_trace: std::env::var(key("INCTRACE")).ok().or(defaults.inc_trace), inc_trace: std::env::var(key("INCTRACE")).ok().or(defaults.inc_trace),
increment_factor: num("CRESCUE_INC", defaults.increment_factor), increment_factor: num("CRESCUE_INC", defaults.increment_factor),
@@ -363,6 +368,7 @@ pub fn run_march(case: BenchmarkCase, config: &MarchConfig) -> MarchResult {
trace_steps, trace_steps,
trace_from, trace_from,
coupling_rescue, coupling_rescue,
poisson_f32,
coarse_episode, coarse_episode,
ref inc_trace, ref inc_trace,
increment_factor, increment_factor,
@@ -374,6 +380,10 @@ pub fn run_march(case: BenchmarkCase, config: &MarchConfig) -> MarchResult {
let (harness, mut solver, mut field) = Fsi2Harness::build_case(case, ny, flag_nx, smooth_in_h); let (harness, mut solver, mut field) = Fsi2Harness::build_case(case, ny, flag_nx, smooth_in_h);
solver.set_mask_hysteresis(mask_hysteresis); solver.set_mask_hysteresis(mask_hysteresis);
if poisson_f32 {
solver.set_poisson_precision(rtx_cfd::solvers::incompressible::MgPrecision::F32);
println!(" poisson V-cycle precision: F32 (M1 probe)");
}
let dt_fluid = harness.dt_fluid; let dt_fluid = harness.dt_fluid;
let dt = dt_fluid * subcycle as f64; let dt = dt_fluid * subcycle as f64;
let interface = &harness.interface; let interface = &harness.interface;
@@ -438,6 +438,7 @@ impl Fsi2Harness {
top: SideBoundary::Velocity, top: SideBoundary::Velocity,
}, },
poisson_solver: PoissonSolverKind::Multigrid, poisson_solver: PoissonSolverKind::Multigrid,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
// TVD, deliberately: FSI2 marches in time and needs the // TVD, deliberately: FSI2 marches in time and needs the
// shedding physics upwind's numerical viscosity killed on // shedding physics upwind's numerical viscosity killed on
// these grids (CFD3's finding). The limiter chatter that // these grids (CFD3's finding). The limiter chatter that
@@ -99,6 +99,13 @@ fn fsi2_interface_noise_floor() {
_ => fsi2_harness::FSI2, _ => fsi2_harness::FSI2,
}; };
let (mut harness, mut solver, mut field) = Fsi2Harness::build_case(case, ny, flag_nx, 0.0); let (mut harness, mut solver, mut field) = Fsi2Harness::build_case(case, ny, flag_nx, 0.0);
// M1 precision probe (`RTX_FSI2_POISSON_F32=1`): the pressure
// multigrid's V-cycle in f32 inside the f64 CG. Printed so the arm can
// never pass vacuously.
if env_or("RTX_FSI2_POISSON_F32", 0.0) != 0.0 {
solver.set_poisson_precision(rtx_cfd::solvers::incompressible::MgPrecision::F32);
println!(" poisson V-cycle precision: F32 (M1 probe)");
}
let dt_fluid = harness.dt_fluid; let dt_fluid = harness.dt_fluid;
// Rigid march to operating loads (the floor rides with the loads — // Rigid march to operating loads (the floor rides with the loads —
@@ -323,6 +323,7 @@ fn fsi1_coupled_cylinder_and_flag() {
top: SideBoundary::Velocity, top: SideBoundary::Velocity,
}, },
poisson_solver: PoissonSolverKind::Multigrid, poisson_solver: PoissonSolverKind::Multigrid,
poisson_precision: rtx_cfd::solvers::incompressible::MgPrecision::F64,
// Upwind, deliberately: FSI1 is a steady FIXED-POINT problem, and // Upwind, deliberately: FSI1 is a steady FIXED-POINT problem, and
// the TVD limiter's switching keeps the steady load chattering by // the TVD limiter's switching keeps the steady load chattering by
// ~0.5% (a known property of limited schemes — they stall short of // ~0.5% (a known property of limited schemes — they stall short of
@@ -162,6 +162,7 @@ fn fsi2_flapping_flag() {
trace_steps: 0, trace_steps: 0,
trace_from: usize::MAX, trace_from: usize::MAX,
coupling_rescue: false, coupling_rescue: false,
poisson_f32: false,
coarse_episode: 0, coarse_episode: 0,
inc_trace: None, inc_trace: None,
increment_factor: 0.0, increment_factor: 0.0,
@@ -134,6 +134,7 @@ fn fsi3_added_mass_flag() {
trace_steps: 0, trace_steps: 0,
trace_from: usize::MAX, trace_from: usize::MAX,
coupling_rescue: false, coupling_rescue: false,
poisson_f32: false,
coarse_episode: 0, coarse_episode: 0,
inc_trace: None, inc_trace: None,
increment_factor: 0.0, increment_factor: 0.0,