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
@@ -95,7 +95,12 @@ pub struct CurvilinearParameters {
/// Projections per step (the first removes the divergence; the rest
/// mop up inner-solver truncation).
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,
/// Convection scheme.
pub convection: PatchConvection,
@@ -111,7 +116,7 @@ impl Default for CurvilinearParameters {
fn default() -> Self {
Self {
corrector_steps: 2,
tolerance: 1e-8,
tolerance: 1e-4,
convection: PatchConvection::Upwind,
normal_diffusion: NormalDiffusion::Explicit,
boundaries: PatchBoundaries::default(),
@@ -304,22 +309,24 @@ impl CurvilinearPisoSolver {
}
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 tolerance = self.params.tolerance * flux_scale;
let floor = 1e-15 * flux_scale;
let mut iterations = 0;
let mut converged = true;
let mut performed = 0;
let mut max_div = self.max_divergence(&field.flux);
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);
iterations += out.iterations;
converged &= out.converged;
self.apply_correction(field, &pc, dt);
performed += 1;
max_div = self.max_divergence(&field.flux);
if max_div <= tolerance {
break;
}
}
self.time = t_new;
Ok(CurvilinearResult {
@@ -64,21 +64,22 @@ impl CurvilinearPisoSolver {
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);
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 (pu, pv) = match (face.owner, face.neigh) {
(Some(p), Some(q)) => {
let other = if p == c { q } else { p };
(
sign * alpha * (field.u[other] - field.u[c]),
sign * alpha * (field.v[other] - field.v[c]),
alpha * (field.u[other] - field.u[c]),
alpha * (field.v[other] - field.v[c]),
)
}
_ => {
let b = bval.expect("dirichlet");
(
sign * alpha * (b.0 - field.u[c]),
sign * alpha * (b.1 - field.v[c]),
)
(alpha * (b.0 - field.u[c]), alpha * (b.1 - field.v[c]))
}
};
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|`.
pub(super) fn max_divergence(&self, flux: &[f64]) -> f64 {
let mesh = &self.mesh;
@@ -38,7 +38,9 @@
use super::ale::{AleBoundaries, SideBoundary};
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::{FlowField, SolverResult};
use crate::{CfdConfig, CfdError, CfdResult};
@@ -61,6 +63,12 @@ pub struct EmbeddedParameters {
/// [`PoissonSolverKind::Sor`]). Both solve the same system to the same
/// true-residual stop; multigrid's cost is mesh-independent.
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
/// [`ConvectionScheme::Upwind`], which is bit-identical to the fixed-grid
/// PISO). The TVD schemes add SIMPLE's limited correction to each
@@ -82,6 +90,7 @@ impl Default for EmbeddedParameters {
tolerance: 1e-6,
boundaries: AleBoundaries::default(),
poisson_solver: PoissonSolverKind::Sor,
poisson_precision: MgPrecision::F64,
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
/// min cell size (default 0, exactly the plain rebuild). With a band,
/// a cell within `band * h_min` of the surface keeps the
@@ -902,7 +917,10 @@ impl EmbeddedPisoSolver {
let solution = solve_multigrid_pcg(
&problem,
&mut p_prime,
&MultigridParameters::default(),
&MultigridParameters {
precision: self.parameters.poisson_precision,
..MultigridParameters::default()
},
inner_stop,
anchor_cell,
);
@@ -57,7 +57,9 @@ pub use flow_field::FlowField;
pub use piso::{PisoParameters, PisoResult, PisoSolver};
#[cfg(feature = "cuda")]
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 simple::{ConvectionScheme, SimpleParameters, SimpleResult, SimpleSolver};
#[cfg(feature = "cuda")]
@@ -37,7 +37,9 @@
//! - 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::poisson::{
MgPrecision, MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg,
};
use super::{BoundaryConditions, FlowField, IncompressibleSolver, SolverResult};
use crate::{CfdConfig, CfdResult};
use async_trait::async_trait;
@@ -59,6 +61,9 @@ pub struct PisoParameters {
/// [`PoissonSolverKind::Sor`]). Both solve the same system to the same
/// true-residual stop; multigrid's cost is mesh-independent.
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 {
@@ -68,6 +73,7 @@ impl Default for PisoParameters {
time_step: 0.001,
tolerance: 1e-6,
poisson_solver: PoissonSolverKind::Sor,
poisson_precision: MgPrecision::F64,
}
}
}
@@ -398,7 +404,10 @@ impl PisoSolver {
let solution = solve_multigrid_pcg(
&problem,
&mut p_prime,
&MultigridParameters::default(),
&MultigridParameters {
precision: self.parameters.poisson_precision,
..MultigridParameters::default()
},
inner_stop,
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.
#[derive(Debug, Clone)]
pub struct MultigridParameters {
/// Precision of the V-cycle (see [`MgPrecision`]).
pub precision: MgPrecision,
/// Symmetric GaussSeidel sweeps before AND after the coarse correction
/// (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
@@ -271,6 +289,7 @@ pub struct MultigridParameters {
impl Default for MultigridParameters {
fn default() -> Self {
Self {
precision: MgPrecision::F64,
smoother_sweeps: 2,
coarsest_cells: 32,
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).
const MAX_LEVELS: usize = 64;
/// One level of the hierarchy: the problem, its diagonal, the list of
/// active cells that carry an equation (row-major), the parent map into the
/// next coarser level, and the V-cycle work vectors.
struct Level {
/// The scalar the V-cycle runs in. `f64` reproduces the historical solver
/// bit for bit (same operations in the same order on the same values);
/// `f32` is the mixed-precision probe.
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.active && ap > 0`: cells with an equation.
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.
cells: Vec<usize>,
/// Fine index → coarse index (empty on the coarsest level).
@@ -350,26 +432,26 @@ struct Level {
}
/// V-cycle work vectors of one level.
struct Work {
struct Work<T: MgScalar> {
/// Right-hand side of the residual equation on this level.
b: Vec<f64>,
b: Vec<T>,
/// Correction on this level.
x: Vec<f64>,
x: Vec<T>,
/// Residual.
r: Vec<f64>,
r: Vec<T>,
}
impl Work {
impl<T: MgScalar> Work<T> {
fn new(n: usize) -> Self {
Self {
b: vec![0.0; n],
x: vec![0.0; n],
r: vec![0.0; n],
b: vec![T::ZERO; n],
x: vec![T::ZERO; n],
r: vec![T::ZERO; n],
}
}
}
impl Level {
impl<T: MgScalar> Level<T> {
fn new(mut problem: PoissonProblem) -> Self {
let (nx, ny) = (problem.nx, problem.ny);
let n = nx * ny;
@@ -405,10 +487,15 @@ impl Level {
}
}
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 {
ae: cast(&problem.ae),
aw: cast(&problem.aw),
an: cast(&problem.an),
as_: cast(&problem.as_),
ap: cast(&ap),
problem,
active,
ap,
cells,
coarse_of: Vec::new(),
}
@@ -418,38 +505,38 @@ impl Level {
/// read (their coefficients are the only non-zero ones after
/// sanitising).
#[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 mut s = 0.0;
let ae = self.problem.ae[idx];
if ae != 0.0 {
let mut s = T::ZERO;
let ae = self.ae[idx];
if ae != T::ZERO {
s += ae * x[idx + 1];
}
let aw = self.problem.aw[idx];
if aw != 0.0 {
let aw = self.aw[idx];
if aw != T::ZERO {
s += aw * x[idx - 1];
}
let an = self.problem.an[idx];
if an != 0.0 {
let an = self.an[idx];
if an != T::ZERO {
s += an * x[idx + nx];
}
let as_ = self.problem.as_[idx];
if as_ != 0.0 {
let as_ = self.as_[idx];
if as_ != T::ZERO {
s += as_ * x[idx - nx];
}
s
}
/// `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 {
y[idx] = self.ap[idx] * x[idx] - self.neighbour_sum(x, idx);
}
}
/// `r = b - A x` on the active cells; returns its L1 norm.
fn residual(&self, b: &[f64], x: &[f64], r: &mut [f64]) -> f64 {
let mut l1 = 0.0;
fn residual(&self, b: &[T], x: &[T], r: &mut [T]) -> T {
let mut l1 = T::ZERO;
for &idx in &self.cells {
let v = b[idx] - (self.ap[idx] * x[idx] - self.neighbour_sum(x, idx));
r[idx] = v;
@@ -459,7 +546,7 @@ impl Level {
}
/// 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 {
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.
pub(crate) struct Hierarchy {
levels: Vec<Level>,
work: Vec<Work>,
pub(crate) struct Hierarchy<T: MgScalar = f64> {
levels: Vec<Level<T>>,
work: Vec<Work<T>>,
sweeps: usize,
}
impl Hierarchy {
impl<T: MgScalar> Hierarchy<T> {
/// Builds the hierarchy down to `coarsest_cells` active cells (or until
/// coarsening stops reducing the count).
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 {
let fine = levels.last().expect("at least one level");
if fine.cells.len() <= params.coarsest_cells.max(1) {
break;
}
let (coarse, coarse_of) = fine.coarsen();
let coarse = Level::new(coarse);
let coarse = Level::<T>::new(coarse);
if coarse.cells.len() >= fine.cells.len() {
break;
}
@@ -570,7 +657,7 @@ impl Hierarchy {
let depth = self.levels.len();
let levels = &self.levels;
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.
for l in 0..depth - 1 {
@@ -578,14 +665,14 @@ impl Hierarchy {
let (head, tail) = self.work.split_at_mut(l + 1);
let (wf, wc) = (&mut head[l], &mut tail[0]);
for &idx in &fine.cells {
wf.x[idx] = 0.0;
wf.x[idx] = T::ZERO;
}
for _ in 0..self.sweeps {
fine.symmetric_gs(&wf.b, &mut wf.x);
}
fine.residual(&wf.b, &wf.x, &mut wf.r);
for &idx in &coarse.cells {
wc.b[idx] = 0.0;
wc.b[idx] = T::ZERO;
}
for &idx in &fine.cells {
let c = fine.coarse_of[idx];
@@ -601,7 +688,7 @@ impl Hierarchy {
let bottom = &levels[depth - 1];
let wb = &mut self.work[depth - 1];
for &idx in &bottom.cells {
wb.x[idx] = 0.0;
wb.x[idx] = T::ZERO;
}
for _ in 0..COARSEST_SWEEPS {
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 (wf, wc) = (&mut head[l], &tail[0]);
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 {
fine.symmetric_gs(&wf.b, &mut wf.x);
}
}
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,
tolerance: f64,
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 {
let n = problem.nx * problem.ny;
assert_eq!(p.len(), n, "p must have nx*ny entries");
@@ -658,8 +762,9 @@ pub fn solve_multigrid_pcg(
problem.validate()
);
let mut hier = Hierarchy::build(problem, params);
let cells: Vec<usize> = hier.levels[0].cells.clone();
let mut hier = Hierarchy::<T>::build(problem, params);
let fine = Level::<f64>::new(problem.clone());
let cells: Vec<usize> = fine.cells.clone();
let active_n = cells.len();
if active_n == 0 {
return PoissonSolution {
@@ -710,9 +815,9 @@ pub fn solve_multigrid_pcg(
let mut q = vec![0.0; n];
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| {
// Level of each singular component: the anchor's component is
// 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 {
return finish(p, 0, res);
}
@@ -759,12 +864,12 @@ pub fn solve_multigrid_pcg(
let mut last_true = res;
while iterations < params.max_iterations {
iterations += 1;
hier.levels[0].apply(&d, &mut q);
fine.apply(&d, &mut q);
let dq = dot(&d, &q);
if !dq.is_finite() || dq <= 0.0 || !rz.is_finite() || rz <= 0.0 {
// Breakdown (r = 0 to rounding, or a non-positive curvature
// 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);
}
let alpha = rz / dq;
@@ -775,7 +880,7 @@ pub fn solve_multigrid_pcg(
if l1(&r) < tolerance {
// The recurrence residual passed: confirm against the TRUE
// 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 {
return finish(p, iterations, res);
}
@@ -792,7 +897,7 @@ pub fn solve_multigrid_pcg(
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)
}
@@ -446,7 +446,7 @@ fn galerkin_coarse_operator_matches_r_a_p() {
}
}
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());
let mut g = Lcg(5);
for l in 0..hier.depth() - 1 {
@@ -570,7 +570,7 @@ fn v_cycle_preconditioner_is_symmetric() {
),
("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 cells = active_indices(&pr);
let mut x = vec![0.0; n * n];