rtx-cfd: multigrid-PCG projection — 30x faster, same answers — and the CFD1 refinement study
Performance Benchmarks / Run Benchmarks (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
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (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
Performance Benchmarks / Run Benchmarks (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
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (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
Falsifier 4 of the Turek–Hron geometry decision fired (the SOR projection
cost 0.09 s/step at 250x41 and an hour per run at 5 mm); this answers it.
solvers::incompressible::poisson: PoissonProblem (cell-centred five-point
SPD operator as per-cell face coefficients + Dirichlet diagonal extra +
active mask) and solve_multigrid_pcg — conjugate gradient preconditioned
by one V-cycle of geometric multigrid: aggregation by 2 per direction (odd
sizes absorbed, coarse cell active iff any child is), the Galerkin coarse
operator for piecewise-constant prolongation / summation restriction,
symmetric Gauss–Seidel smoothing, coarse correction scaled by 2 (Braess's
under-correction of unsmoothed aggregation; scalar, so the preconditioner
stays symmetric and positive on range(A)), L1 TRUE-residual stop with a
stagnation guard. Singular systems are handled per connected component of
the active cells (mean projection and level per pure-Neumann component;
the anchor's component to p[anchor] = 0). PoissonSolverKind::{Sor,
Multigrid} on PisoParameters / EmbeddedParameters; Sor is the default and
its code is byte-for-byte untouched; an unconverged multigrid solve falls
back to the SOR sweeps for that projection.
Verified (poisson/tests.rs, tests/poisson_equivalence.rs):
- PCG iterations to cut the residual 1e-8 on the closed Neumann box at
32^2..256^2: 4, 4, 4, 4; ragged masked domains 8/8/8;
- manufactured recoveries to ~1e-14; Galerkin identity A_c v = R A P v to
7e-15 on every level (masked, outlet column, non-uniform conductances);
V-cycle symmetric to 1e-14; NaN-poisoned inactive cells untouched;
- two Neumann components with opposite imbalances, and a Dirichlet
component beside an imbalanced Neumann one (review scenarios): converge,
each component right up to its own constant;
- speed vs plain SOR at the same stop: 22.7x (128^2), 41x (256^2);
- same answers as SOR: PISO MMS 4.6e-8 relative, Taylor–Green divergence
1.4e-9 every step, embedded-circle MMS 7e-8, no-body bit-identity with MG
on both solvers, channel+outlet+circle 1.4e-10; CFD1 loads identical to
four digits at 0.003 s/step vs 0.094 (30x).
CFD1 refinement study (tests/turek_hron_cfd.rs, three grids, 257 s):
h = 10 / 6.6 / 5 mm -> control-volume drag 15.6156 / 15.2829 / 15.0988 vs
14.2929 (+9.25 / +6.93 / +5.64%), apparent order 0.71, Richardson
extrapolate 14.04; surface route and lift not monotone (flag 2/3/4 cells
thick) — the test asserts the measured band at the finest grid.
Built with a 4-agent workflow (core, integration, refinement study,
adversarial review); the review found no defects and four risks, three
fixed here (per-component projection, one symmetric smoother-sweep
parameter, acting on `converged` with an SOR fallback) and one recorded
(isotropic aggregation loses grid-independence on anisotropic cells).
rtx-cfd 301 -> 318 green.
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c25f15b3c4
commit
327da7ff47
@@ -37,6 +37,7 @@
|
||||
//! - Convective face fluxes fell back to the centre value at the sweep edges
|
||||
//! instead of using the prescribed boundary faces that exist there.
|
||||
|
||||
use super::poisson::{MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg};
|
||||
use super::{BoundaryConditions, FlowField, IncompressibleSolver, SolverResult};
|
||||
use crate::{CfdConfig, CfdResult};
|
||||
use async_trait::async_trait;
|
||||
@@ -54,6 +55,10 @@ pub struct PisoParameters {
|
||||
/// Convergence tolerance on the normalised mass imbalance after
|
||||
/// correction.
|
||||
pub tolerance: f64,
|
||||
/// Inner solver of the pressure-correction system (default
|
||||
/// [`PoissonSolverKind::Sor`]). Both solve the same system to the same
|
||||
/// true-residual stop; multigrid's cost is mesh-independent.
|
||||
pub poisson_solver: PoissonSolverKind,
|
||||
}
|
||||
|
||||
impl Default for PisoParameters {
|
||||
@@ -62,6 +67,7 @@ impl Default for PisoParameters {
|
||||
corrector_steps: 2,
|
||||
time_step: 0.001,
|
||||
tolerance: 1e-6,
|
||||
poisson_solver: PoissonSolverKind::Sor,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -368,51 +374,94 @@ impl PisoSolver {
|
||||
let reference_flux = rho * self.config.reference_velocity * self.config.reference_length;
|
||||
let inner_stop =
|
||||
(1e-2 * source_scale).max(0.1 * self.parameters.tolerance * reference_flux) + 1e-14;
|
||||
let omega = 2.0 / (1.0 + (std::f64::consts::PI / nx.max(ny) as f64).sin());
|
||||
for _sweep in 0..2000 {
|
||||
let mut residual = 0.0;
|
||||
let mut multigrid_converged = false;
|
||||
if self.parameters.poisson_solver == PoissonSolverKind::Multigrid {
|
||||
// The same five-point system the SOR loop below sweeps — the
|
||||
// same coefficients, right-hand side, anchor cell and stop —
|
||||
// handed to the multigrid-preconditioned CG solver. The SOR
|
||||
// loop pins `p'(1, 1) = 0` and solves the remaining equations;
|
||||
// on the compatible (closed-box) source that is the singular
|
||||
// system's solution shifted to `p'(1, 1) = 0`, which is what
|
||||
// `anchor` requests.
|
||||
let mut problem = PoissonProblem::new(nx, ny);
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if i == 1 && j == 1 {
|
||||
flow_field.p_prime[(j, i)] = 0.0;
|
||||
continue;
|
||||
}
|
||||
|
||||
let ae = if i + 1 == nx { 0.0 } else { ae_interior };
|
||||
let aw = if i == 0 { 0.0 } else { ae_interior };
|
||||
let an = if j + 1 == ny { 0.0 } else { an_interior };
|
||||
let as_ = if j == 0 { 0.0 } else { an_interior };
|
||||
let ap = ae + aw + an + as_;
|
||||
|
||||
let east = if i + 1 < nx {
|
||||
ae * flow_field.p_prime[(j, i + 1)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let west = if i > 0 {
|
||||
aw * flow_field.p_prime[(j, i - 1)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let north = if j + 1 < ny {
|
||||
an * flow_field.p_prime[(j + 1, i)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let south = if j > 0 {
|
||||
as_ * flow_field.p_prime[(j - 1, i)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let rhs = flow_field.sp[(j, i)] + east + west + north + south;
|
||||
let p_old = flow_field.p_prime[(j, i)];
|
||||
residual += (rhs - ap * p_old).abs();
|
||||
flow_field.p_prime[(j, i)] = (1.0 - omega) * p_old + omega * rhs / ap;
|
||||
let idx = j * nx + i;
|
||||
problem.ae[idx] = if i + 1 == nx { 0.0 } else { ae_interior };
|
||||
problem.aw[idx] = if i == 0 { 0.0 } else { ae_interior };
|
||||
problem.an[idx] = if j + 1 == ny { 0.0 } else { an_interior };
|
||||
problem.as_[idx] = if j == 0 { 0.0 } else { an_interior };
|
||||
problem.rhs[idx] = flow_field.sp[(j, i)];
|
||||
}
|
||||
}
|
||||
if residual < inner_stop {
|
||||
break;
|
||||
let mut p_prime = vec![0.0; nx * ny];
|
||||
let solution = solve_multigrid_pcg(
|
||||
&problem,
|
||||
&mut p_prime,
|
||||
&MultigridParameters::default(),
|
||||
inner_stop,
|
||||
Some(nx + 1),
|
||||
);
|
||||
// An unconverged multigrid solve (iteration cap, rounding floor,
|
||||
// inconsistent system) is not applied: the SOR sweeps below take
|
||||
// over for this projection, so the worst case is the old cost,
|
||||
// never a silently wrong correction.
|
||||
multigrid_converged = solution.converged;
|
||||
if multigrid_converged {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
flow_field.p_prime[(j, i)] = p_prime[j * nx + i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !multigrid_converged {
|
||||
let omega = 2.0 / (1.0 + (std::f64::consts::PI / nx.max(ny) as f64).sin());
|
||||
for _sweep in 0..2000 {
|
||||
let mut residual = 0.0;
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
if i == 1 && j == 1 {
|
||||
flow_field.p_prime[(j, i)] = 0.0;
|
||||
continue;
|
||||
}
|
||||
|
||||
let ae = if i + 1 == nx { 0.0 } else { ae_interior };
|
||||
let aw = if i == 0 { 0.0 } else { ae_interior };
|
||||
let an = if j + 1 == ny { 0.0 } else { an_interior };
|
||||
let as_ = if j == 0 { 0.0 } else { an_interior };
|
||||
let ap = ae + aw + an + as_;
|
||||
|
||||
let east = if i + 1 < nx {
|
||||
ae * flow_field.p_prime[(j, i + 1)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let west = if i > 0 {
|
||||
aw * flow_field.p_prime[(j, i - 1)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let north = if j + 1 < ny {
|
||||
an * flow_field.p_prime[(j + 1, i)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let south = if j > 0 {
|
||||
as_ * flow_field.p_prime[(j - 1, i)]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let rhs = flow_field.sp[(j, i)] + east + west + north + south;
|
||||
let p_old = flow_field.p_prime[(j, i)];
|
||||
residual += (rhs - ap * p_old).abs();
|
||||
flow_field.p_prime[(j, i)] = (1.0 - omega) * p_old + omega * rhs / ap;
|
||||
}
|
||||
}
|
||||
if residual < inner_stop {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user