rtx-cfd: second-order convection by deferred-correction TVD; MMS order 1.84, cavity closes on Ghia
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
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 / 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
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
First-order upwind's O(h) numerical viscosity was the measured limit on the
whole discretisation: MMS order ~0.9 at Re = 20 against 2.05 in the Stokes
limit. This adds a ConvectionScheme parameter to SimPLE — Upwind (default,
behaviour unchanged), TvdVanAlbada, TvdVanLeer — implemented by deferred
correction: the upwind operator stays implicit, so a_p = sum(a_nb) and
diagonal dominance survive unconditionally, and the limited
high-order-minus-upwind flux difference enters the source explicitly at the
current iterate. At a fixed point the two agree, so the converged answer is
the TVD discretisation. Faces whose far-upwind node lies outside the domain
fall back to pure upwind; wall faces pass no mass, so no correction enters.
Measured by the manufactured solution (van Albada, 16 -> 32 -> 64):
L2 velocity 1.325e-3 4.406e-4 1.232e-4 orders 1.59, 1.84
(upwind) 3.516e-2 1.954e-2 1.038e-2 orders 0.85, 0.91
The error is 27x to 84x below upwind's at equal resolution, the order climbs
toward 2 (the shortfall is limiter clipping plus the boundary fallback, both
of which shrink with h), the pressure error falls at the same rate, and
continuity still holds to solver tolerance in every cell.
On the Re = 100 lid-driven cavity at 65^2 the centreline minimum moves from
-0.1932 (upwind) to -0.2036 against Ghia's -0.2109 — 59% of the remaining
gap closed at equal resolution, converged in 790 iterations — and the vortex
position moves from 0.5000 to 0.4844 toward Ghia's 0.4531. Both new cavity
bounds exclude the upwind values, so falling back to first order fails them.
284 tests, 0 failing.
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
87cf392556
commit
796cf173e6
@@ -32,7 +32,7 @@ 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 simple::{SimpleParameters, SimpleResult, SimpleSolver};
|
pub use simple::{ConvectionScheme, SimpleParameters, SimpleResult, SimpleSolver};
|
||||||
#[cfg(feature = "cuda")]
|
#[cfg(feature = "cuda")]
|
||||||
pub use simple_gpu::SimpleGpuSolver;
|
pub use simple_gpu::SimpleGpuSolver;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,65 @@ use async_trait::async_trait;
|
|||||||
use nalgebra::{DMatrix, DVector, Vector3};
|
use nalgebra::{DMatrix, DVector, Vector3};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// Discretisation of the convective term in the momentum equations.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ConvectionScheme {
|
||||||
|
/// First-order upwind. Unconditionally bounded, but carries a numerical
|
||||||
|
/// viscosity of about `|u| dx / 2`, which caps the observed order of the
|
||||||
|
/// whole discretisation at 1 whenever convection matters.
|
||||||
|
Upwind,
|
||||||
|
/// Deferred-correction TVD with the van Albada limiter
|
||||||
|
/// `psi(r) = (r^2 + r) / (r^2 + 1)` (0 for `r <= 0`).
|
||||||
|
///
|
||||||
|
/// The upwind operator stays implicit, so `a_p = sum(a_nb)` and diagonal
|
||||||
|
/// dominance survive unconditionally; the limited high-order-minus-upwind
|
||||||
|
/// flux difference is added explicitly to the source, evaluated at the
|
||||||
|
/// current iterate. At a converged state the two agree, so the fixed
|
||||||
|
/// point is the TVD discretisation — relaxation changes the path, never
|
||||||
|
/// the answer. Faces whose far-upwind node lies outside the domain fall
|
||||||
|
/// back to pure upwind, the standard TVD boundary treatment.
|
||||||
|
TvdVanAlbada,
|
||||||
|
/// Deferred-correction TVD with the van Leer limiter
|
||||||
|
/// `psi(r) = (r + |r|) / (1 + |r|)`. Same construction as
|
||||||
|
/// [`ConvectionScheme::TvdVanAlbada`].
|
||||||
|
TvdVanLeer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConvectionScheme {
|
||||||
|
/// Flux limiter `psi(r)`. Zero recovers pure upwind, one recovers central
|
||||||
|
/// differencing; both TVD limiters satisfy `psi(1) = 1`, which is what
|
||||||
|
/// makes them second order in smooth regions.
|
||||||
|
fn limiter(self, r: f64) -> f64 {
|
||||||
|
match self {
|
||||||
|
Self::Upwind => 0.0,
|
||||||
|
Self::TvdVanAlbada => {
|
||||||
|
if r > 0.0 {
|
||||||
|
(r * r + r) / (r * r + 1.0)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::TvdVanLeer => (r + r.abs()) / (1.0 + r.abs()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The limited correction `u_face_HO - u_face_upwind` for one face, given
|
||||||
|
/// the far-upwind, upwind and downwind values along the flow direction.
|
||||||
|
/// `None` for the far-upwind value means it lies outside the domain, and
|
||||||
|
/// the face falls back to pure upwind.
|
||||||
|
fn face_correction(self, far_upwind: Option<f64>, upwind: f64, downwind: f64) -> f64 {
|
||||||
|
let Some(far) = far_upwind else {
|
||||||
|
return 0.0;
|
||||||
|
};
|
||||||
|
let denominator = downwind - upwind;
|
||||||
|
if denominator.abs() < 1e-300 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let r = (upwind - far) / denominator;
|
||||||
|
0.5 * self.limiter(r) * denominator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parameters for SIMPLE algorithm
|
/// Parameters for SIMPLE algorithm
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SimpleParameters {
|
pub struct SimpleParameters {
|
||||||
@@ -24,6 +83,8 @@ pub struct SimpleParameters {
|
|||||||
pub pressure_relaxation: f64,
|
pub pressure_relaxation: f64,
|
||||||
/// Under-relaxation factor for velocity (typically 0.5-0.8)
|
/// Under-relaxation factor for velocity (typically 0.5-0.8)
|
||||||
pub velocity_relaxation: f64,
|
pub velocity_relaxation: f64,
|
||||||
|
/// Convection discretisation. Defaults to first-order upwind.
|
||||||
|
pub convection_scheme: ConvectionScheme,
|
||||||
/// Maximum number of iterations
|
/// Maximum number of iterations
|
||||||
pub max_iterations: usize,
|
pub max_iterations: usize,
|
||||||
/// Convergence tolerance for residuals
|
/// Convergence tolerance for residuals
|
||||||
@@ -66,6 +127,13 @@ impl SimpleParameters {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the convection scheme
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_convection_scheme(mut self, scheme: ConvectionScheme) -> Self {
|
||||||
|
self.convection_scheme = scheme;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set maximum iterations
|
/// Set maximum iterations
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
|
pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
|
||||||
@@ -118,6 +186,7 @@ impl Default for SimpleParameters {
|
|||||||
Self {
|
Self {
|
||||||
pressure_relaxation: 0.3,
|
pressure_relaxation: 0.3,
|
||||||
velocity_relaxation: 0.7,
|
velocity_relaxation: 0.7,
|
||||||
|
convection_scheme: ConvectionScheme::Upwind,
|
||||||
max_iterations: 1000,
|
max_iterations: 1000,
|
||||||
tolerance: 1e-6,
|
tolerance: 1e-6,
|
||||||
time_step: 0.001,
|
time_step: 0.001,
|
||||||
@@ -929,6 +998,140 @@ impl SimpleSolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deferred-correction source for the u-momentum equation: the limited
|
||||||
|
/// high-order convective fluxes minus their upwind counterparts, moved to
|
||||||
|
/// the right-hand side with the sign that puts convection on the left.
|
||||||
|
///
|
||||||
|
/// Face stencils run along the flow direction: for each face the upwind
|
||||||
|
/// node `C`, downwind node `D` and far-upwind node `U` define
|
||||||
|
/// `r = (C - U) / (D - C)`, and the correction is
|
||||||
|
/// `psi(r) (D - C) / 2`. A face whose far-upwind node lies outside the
|
||||||
|
/// domain falls back to pure upwind, and a wall face has zero mass flux,
|
||||||
|
/// so its correction never enters.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn u_deferred_correction(
|
||||||
|
&self,
|
||||||
|
flow_field: &FlowField,
|
||||||
|
i: usize,
|
||||||
|
j: usize,
|
||||||
|
nx: usize,
|
||||||
|
ny: usize,
|
||||||
|
fe: f64,
|
||||||
|
fw: f64,
|
||||||
|
fn_: f64,
|
||||||
|
fs: f64,
|
||||||
|
) -> f64 {
|
||||||
|
let scheme = self.parameters.convection_scheme;
|
||||||
|
if scheme == ConvectionScheme::Upwind {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let u = &flow_field.u;
|
||||||
|
|
||||||
|
// East face of the u control volume, between u faces `i` and `i + 1`.
|
||||||
|
let delta_e = if fe >= 0.0 {
|
||||||
|
// `i >= 1` for every unknown, so the far-upwind node exists.
|
||||||
|
scheme.face_correction(Some(u[(j, i - 1)]), u[(j, i)], u[(j, i + 1)])
|
||||||
|
} else {
|
||||||
|
let far = (i + 2 <= nx).then(|| u[(j, i + 2)]);
|
||||||
|
scheme.face_correction(far, u[(j, i + 1)], u[(j, i)])
|
||||||
|
};
|
||||||
|
|
||||||
|
// West face, between u faces `i - 1` and `i`.
|
||||||
|
let delta_w = if fw >= 0.0 {
|
||||||
|
let far = (i >= 2).then(|| u[(j, i - 2)]);
|
||||||
|
scheme.face_correction(far, u[(j, i - 1)], u[(j, i)])
|
||||||
|
} else {
|
||||||
|
scheme.face_correction(Some(u[(j, i + 1)]), u[(j, i)], u[(j, i - 1)])
|
||||||
|
};
|
||||||
|
|
||||||
|
// North face, between rows `j` and `j + 1`; a wall face passes no mass.
|
||||||
|
let delta_n = if j + 1 >= ny {
|
||||||
|
0.0
|
||||||
|
} else if fn_ >= 0.0 {
|
||||||
|
let far = (j >= 1).then(|| u[(j - 1, i)]);
|
||||||
|
scheme.face_correction(far, u[(j, i)], u[(j + 1, i)])
|
||||||
|
} else {
|
||||||
|
let far = (j + 2 < ny).then(|| u[(j + 2, i)]);
|
||||||
|
scheme.face_correction(far, u[(j + 1, i)], u[(j, i)])
|
||||||
|
};
|
||||||
|
|
||||||
|
// South face, between rows `j - 1` and `j`.
|
||||||
|
let delta_s = if j == 0 {
|
||||||
|
0.0
|
||||||
|
} else if fs >= 0.0 {
|
||||||
|
let far = (j >= 2).then(|| u[(j - 2, i)]);
|
||||||
|
scheme.face_correction(far, u[(j - 1, i)], u[(j, i)])
|
||||||
|
} else {
|
||||||
|
let far = (j + 1 < ny).then(|| u[(j + 1, i)]);
|
||||||
|
scheme.face_correction(far, u[(j, i)], u[(j - 1, i)])
|
||||||
|
};
|
||||||
|
|
||||||
|
-(fe * delta_e - fw * delta_w + fn_ * delta_n - fs * delta_s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deferred-correction source for the v-momentum equation; mirrors
|
||||||
|
/// [`Self::u_deferred_correction`] with the roles of the axes swapped.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn v_deferred_correction(
|
||||||
|
&self,
|
||||||
|
flow_field: &FlowField,
|
||||||
|
i: usize,
|
||||||
|
j: usize,
|
||||||
|
nx: usize,
|
||||||
|
ny: usize,
|
||||||
|
fe: f64,
|
||||||
|
fw: f64,
|
||||||
|
fn_: f64,
|
||||||
|
fs: f64,
|
||||||
|
) -> f64 {
|
||||||
|
let scheme = self.parameters.convection_scheme;
|
||||||
|
if scheme == ConvectionScheme::Upwind {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let v = &flow_field.v;
|
||||||
|
|
||||||
|
// North face of the v control volume, between v faces `j` and `j + 1`.
|
||||||
|
let delta_n = if fn_ >= 0.0 {
|
||||||
|
scheme.face_correction(Some(v[(j - 1, i)]), v[(j, i)], v[(j + 1, i)])
|
||||||
|
} else {
|
||||||
|
let far = (j + 2 <= ny).then(|| v[(j + 2, i)]);
|
||||||
|
scheme.face_correction(far, v[(j + 1, i)], v[(j, i)])
|
||||||
|
};
|
||||||
|
|
||||||
|
// South face, between v faces `j - 1` and `j`.
|
||||||
|
let delta_s = if fs >= 0.0 {
|
||||||
|
let far = (j >= 2).then(|| v[(j - 2, i)]);
|
||||||
|
scheme.face_correction(far, v[(j - 1, i)], v[(j, i)])
|
||||||
|
} else {
|
||||||
|
scheme.face_correction(Some(v[(j + 1, i)]), v[(j, i)], v[(j - 1, i)])
|
||||||
|
};
|
||||||
|
|
||||||
|
// East face, between columns `i` and `i + 1`; a wall face passes no
|
||||||
|
// mass.
|
||||||
|
let delta_e = if i + 1 >= nx {
|
||||||
|
0.0
|
||||||
|
} else if fe >= 0.0 {
|
||||||
|
let far = (i >= 1).then(|| v[(j, i - 1)]);
|
||||||
|
scheme.face_correction(far, v[(j, i)], v[(j, i + 1)])
|
||||||
|
} else {
|
||||||
|
let far = (i + 2 < nx).then(|| v[(j, i + 2)]);
|
||||||
|
scheme.face_correction(far, v[(j, i + 1)], v[(j, i)])
|
||||||
|
};
|
||||||
|
|
||||||
|
// West face, between columns `i - 1` and `i`.
|
||||||
|
let delta_w = if i == 0 {
|
||||||
|
0.0
|
||||||
|
} else if fw >= 0.0 {
|
||||||
|
let far = (i >= 2).then(|| v[(j, i - 2)]);
|
||||||
|
scheme.face_correction(far, v[(j, i - 1)], v[(j, i)])
|
||||||
|
} else {
|
||||||
|
let far = (i + 1 < nx).then(|| v[(j, i + 1)]);
|
||||||
|
scheme.face_correction(far, v[(j, i)], v[(j, i - 1)])
|
||||||
|
};
|
||||||
|
|
||||||
|
-(fe * delta_e - fw * delta_w + fn_ * delta_n - fs * delta_s)
|
||||||
|
}
|
||||||
|
|
||||||
/// Compute coefficients for u-momentum equation
|
/// Compute coefficients for u-momentum equation
|
||||||
fn compute_u_momentum_coefficients(
|
fn compute_u_momentum_coefficients(
|
||||||
&self,
|
&self,
|
||||||
@@ -941,7 +1144,7 @@ impl SimpleSolver {
|
|||||||
dx: f64,
|
dx: f64,
|
||||||
dy: f64,
|
dy: f64,
|
||||||
) -> CfdResult<MomentumEquationCoeffs> {
|
) -> CfdResult<MomentumEquationCoeffs> {
|
||||||
let (_nx, ny, _, _) = flow_field.grid_info();
|
let (nx, ny, _, _) = flow_field.grid_info();
|
||||||
|
|
||||||
// Compute effective viscosity (molecular + turbulent)
|
// Compute effective viscosity (molecular + turbulent)
|
||||||
let mu_eff = self.compute_effective_viscosity(flow_field, i, j, mu);
|
let mu_eff = self.compute_effective_viscosity(flow_field, i, j, mu);
|
||||||
@@ -1074,6 +1277,7 @@ impl SimpleSolver {
|
|||||||
+ time_term
|
+ time_term
|
||||||
+ wall_source
|
+ wall_source
|
||||||
+ self.u_source_term(i, j, dx, dy)
|
+ self.u_source_term(i, j, dx, dy)
|
||||||
|
+ self.u_deferred_correction(flow_field, i, j, nx, ny, fe, fw, fn_, fs)
|
||||||
+ (1.0 - alpha) / alpha * ap_unrelaxed * flow_field.u_old[(j, i)];
|
+ (1.0 - alpha) / alpha * ap_unrelaxed * flow_field.u_old[(j, i)];
|
||||||
|
|
||||||
Ok(MomentumEquationCoeffs {
|
Ok(MomentumEquationCoeffs {
|
||||||
@@ -1098,7 +1302,7 @@ impl SimpleSolver {
|
|||||||
dx: f64,
|
dx: f64,
|
||||||
dy: f64,
|
dy: f64,
|
||||||
) -> CfdResult<MomentumEquationCoeffs> {
|
) -> CfdResult<MomentumEquationCoeffs> {
|
||||||
let (nx, _ny, _, _) = flow_field.grid_info();
|
let (nx, ny, _, _) = flow_field.grid_info();
|
||||||
|
|
||||||
// Compute effective viscosity (molecular + turbulent)
|
// Compute effective viscosity (molecular + turbulent)
|
||||||
let mu_eff = self.compute_effective_viscosity(flow_field, i, j, mu);
|
let mu_eff = self.compute_effective_viscosity(flow_field, i, j, mu);
|
||||||
@@ -1177,6 +1381,7 @@ impl SimpleSolver {
|
|||||||
+ time_term
|
+ time_term
|
||||||
+ wall_source
|
+ wall_source
|
||||||
+ self.v_source_term(i, j, dx, dy)
|
+ self.v_source_term(i, j, dx, dy)
|
||||||
|
+ self.v_deferred_correction(flow_field, i, j, nx, ny, fe, fw, fn_, fs)
|
||||||
+ (1.0 - alpha) / alpha * ap_unrelaxed * flow_field.v_old[(j, i)];
|
+ (1.0 - alpha) / alpha * ap_unrelaxed * flow_field.v_old[(j, i)];
|
||||||
|
|
||||||
Ok(MomentumEquationCoeffs {
|
Ok(MomentumEquationCoeffs {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ async fn test_simple_solver_workflow() -> CfdResult<()> {
|
|||||||
max_courant: 1.0,
|
max_courant: 1.0,
|
||||||
use_turbulence: true,
|
use_turbulence: true,
|
||||||
steady: true,
|
steady: true,
|
||||||
|
..SimpleParameters::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create solver
|
// Create solver
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
//! in [`SimpleSolver::set_wall_velocity`].
|
//! in [`SimpleSolver::set_wall_velocity`].
|
||||||
|
|
||||||
use rtx_cfd::solvers::incompressible::{
|
use rtx_cfd::solvers::incompressible::{
|
||||||
BoundaryConditions, FlowField, SimpleParameters, SimpleSolver,
|
BoundaryConditions, ConvectionScheme, FlowField, SimpleParameters, SimpleSolver,
|
||||||
};
|
};
|
||||||
use rtx_cfd::{CfdConfig, CfdResult};
|
use rtx_cfd::{CfdConfig, CfdResult};
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
@@ -157,7 +157,7 @@ struct Measurement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Solve the manufactured problem on an `n` by `n` grid.
|
/// Solve the manufactured problem on an `n` by `n` grid.
|
||||||
async fn measure(n: usize) -> CfdResult<Measurement> {
|
async fn measure(n: usize, scheme: ConvectionScheme) -> CfdResult<Measurement> {
|
||||||
let dx = 1.0 / n as f64;
|
let dx = 1.0 / n as f64;
|
||||||
let dy = dx;
|
let dy = dx;
|
||||||
|
|
||||||
@@ -168,7 +168,8 @@ async fn measure(n: usize) -> CfdResult<Measurement> {
|
|||||||
.with_reference_length(1.0);
|
.with_reference_length(1.0);
|
||||||
let params = SimpleParameters::default()
|
let params = SimpleParameters::default()
|
||||||
.with_max_iterations(40000)
|
.with_max_iterations(40000)
|
||||||
.with_tolerance(1e-9);
|
.with_tolerance(1e-9)
|
||||||
|
.with_convection_scheme(scheme);
|
||||||
let mut solver = SimpleSolver::new(config, params)?;
|
let mut solver = SimpleSolver::new(config, params)?;
|
||||||
solver.set_momentum_source(source);
|
solver.set_momentum_source(source);
|
||||||
|
|
||||||
@@ -283,7 +284,7 @@ async fn observed_order_matches_the_convection_scheme() -> CfdResult<()> {
|
|||||||
let resolutions = [16usize, 32, 64];
|
let resolutions = [16usize, 32, 64];
|
||||||
let mut measurements = Vec::new();
|
let mut measurements = Vec::new();
|
||||||
for &n in &resolutions {
|
for &n in &resolutions {
|
||||||
measurements.push(measure(n).await?);
|
measurements.push(measure(n, ConvectionScheme::Upwind).await?);
|
||||||
}
|
}
|
||||||
let errors: Vec<f64> = measurements.iter().map(|m| m.l2_velocity).collect();
|
let errors: Vec<f64> = measurements.iter().map(|m| m.l2_velocity).collect();
|
||||||
let rates: Vec<f64> = errors
|
let rates: Vec<f64> = errors
|
||||||
@@ -395,7 +396,7 @@ async fn observed_order_matches_the_convection_scheme() -> CfdResult<()> {
|
|||||||
/// loud rather than reporting 2.53e-7 as if it were zero.
|
/// loud rather than reporting 2.53e-7 as if it were zero.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn continuity_holds_on_the_outer_ring_as_well_as_the_interior() -> CfdResult<()> {
|
async fn continuity_holds_on_the_outer_ring_as_well_as_the_interior() -> CfdResult<()> {
|
||||||
let m = measure(32).await?;
|
let m = measure(32, ConvectionScheme::Upwind).await?;
|
||||||
println!(
|
println!(
|
||||||
" n = 32 max |div u| ring = {:.6e} interior = {:.6e}",
|
" n = 32 max |div u| ring = {:.6e} interior = {:.6e}",
|
||||||
m.max_div_ring, m.max_div_interior
|
m.max_div_ring, m.max_div_interior
|
||||||
@@ -416,3 +417,109 @@ async fn continuity_holds_on_the_outer_ring_as_well_as_the_interior() -> CfdResu
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The deferred-correction TVD scheme must lift the observed order toward 2.
|
||||||
|
///
|
||||||
|
/// The Stokes-limit measurement already pinned every non-convective operator
|
||||||
|
/// at second order, so with a second-order convective flux the whole
|
||||||
|
/// discretisation should approach 2 — and the error at every resolution must
|
||||||
|
/// be strictly below upwind's, since the schemes differ only in the
|
||||||
|
/// convective face values.
|
||||||
|
///
|
||||||
|
/// Measured (van Albada, 16 -> 32 -> 64): L2 velocity 1.325e-3, 4.406e-4,
|
||||||
|
/// 1.232e-4 — observed orders 1.59 and 1.84, climbing toward 2, against
|
||||||
|
/// upwind's 0.85 and 0.91 on the same meshes. The error is 27x to 84x below
|
||||||
|
/// upwind's at equal resolution. The shortfall from exactly 2 is the limiter
|
||||||
|
/// clipping at extrema plus the pure-upwind fallback at faces whose
|
||||||
|
/// far-upwind node lies outside the domain, both of which shrink with h —
|
||||||
|
/// which is why the rate climbs. The order bound of 1.5 sits under the worst
|
||||||
|
/// measured rate and still fails first-order upwind by a wide margin.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn observed_order_reaches_two_with_tvd_convection() -> CfdResult<()> {
|
||||||
|
let resolutions = [16usize, 32, 64];
|
||||||
|
let mut tvd = Vec::new();
|
||||||
|
let mut upwind = Vec::new();
|
||||||
|
for &n in &resolutions {
|
||||||
|
tvd.push(measure(n, ConvectionScheme::TvdVanAlbada).await?);
|
||||||
|
upwind.push(measure(n, ConvectionScheme::Upwind).await?);
|
||||||
|
}
|
||||||
|
let errors: Vec<f64> = tvd.iter().map(|m| m.l2_velocity).collect();
|
||||||
|
let rates: Vec<f64> = errors
|
||||||
|
.windows(2)
|
||||||
|
.map(|pair| (pair[0] / pair[1]).log2())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (i, &n) in resolutions.iter().enumerate() {
|
||||||
|
let rate = if i == 0 {
|
||||||
|
String::from(" -")
|
||||||
|
} else {
|
||||||
|
format!("{:5.2}", rates[i - 1])
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
" n = {n:3} TVD L2 = {:.6e} order = {rate} upwind L2 = {:.6e} \
|
||||||
|
max |p - p_exact| = {:.6e}",
|
||||||
|
errors[i], upwind[i].l2_velocity, tvd[i].max_p_error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, m) in tvd.iter().enumerate() {
|
||||||
|
assert!(
|
||||||
|
m.l2_velocity < upwind[i].l2_velocity,
|
||||||
|
"TVD error {:.4e} not below upwind {:.4e} at n = {}",
|
||||||
|
m.l2_velocity,
|
||||||
|
upwind[i].l2_velocity,
|
||||||
|
resolutions[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
errors.windows(2).all(|pair| pair[1] < pair[0]),
|
||||||
|
"the error must fall under refinement; got {errors:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (i, &rate) in rates.iter().enumerate() {
|
||||||
|
assert!(
|
||||||
|
rate > 1.5,
|
||||||
|
"refinement {} -> {}: observed order {rate:.3}, below the 1.59 and 1.84 \
|
||||||
|
this scheme measures. Errors: {errors:?}",
|
||||||
|
resolutions[i],
|
||||||
|
resolutions[i + 1]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
rate < 2.5,
|
||||||
|
"refinement {} -> {}: observed order {rate:.3} — too good; suspect the \
|
||||||
|
error measure. Errors: {errors:?}",
|
||||||
|
resolutions[i],
|
||||||
|
resolutions[i + 1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TVD must not corrupt continuity or the pressure: divergence stays at
|
||||||
|
// solver tolerance and the pressure error still falls under refinement.
|
||||||
|
for m in &tvd {
|
||||||
|
assert!(m.max_div_ring < 1e-5);
|
||||||
|
assert!(m.max_div_interior < 1e-5);
|
||||||
|
}
|
||||||
|
let p_errors: Vec<f64> = tvd.iter().map(|m| m.max_p_error).collect();
|
||||||
|
assert!(
|
||||||
|
p_errors.windows(2).all(|pair| pair[1] < pair[0]),
|
||||||
|
"the pressure error must fall under refinement; got {p_errors:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Van Leer is the same construction with a different limiter; one resolution
|
||||||
|
/// pins it as implemented (beats upwind) without doubling the suite's runtime.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn van_leer_limiter_also_beats_upwind() -> CfdResult<()> {
|
||||||
|
let tvd = measure(32, ConvectionScheme::TvdVanLeer).await?;
|
||||||
|
let upwind = measure(32, ConvectionScheme::Upwind).await?;
|
||||||
|
println!(
|
||||||
|
" n = 32 van Leer L2 = {:.6e} upwind L2 = {:.6e}",
|
||||||
|
tvd.l2_velocity, upwind.l2_velocity
|
||||||
|
);
|
||||||
|
assert!(tvd.l2_velocity < upwind.l2_velocity);
|
||||||
|
assert!(tvd.max_div_interior < 1e-5 && tvd.max_div_ring < 1e-5);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -272,6 +272,105 @@ mod simple_tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The same Re = 100 cavity with the deferred-correction TVD scheme.
|
||||||
|
///
|
||||||
|
/// First-order upwind's numerical viscosity is what holds the 65^2
|
||||||
|
/// centreline minimum near -0.19 against Ghia's -0.2109; a second-order
|
||||||
|
/// convective flux removes most of that viscosity, so this measures how
|
||||||
|
/// far the cavity closes on the reference once the scheme, rather than
|
||||||
|
/// the resolution, stops being the limit.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_lid_driven_cavity_re100_tvd() -> CfdResult<()> {
|
||||||
|
use rtx_cfd::solvers::incompressible::ConvectionScheme;
|
||||||
|
|
||||||
|
let config = CfdConfig::new()
|
||||||
|
.with_density(1.0)
|
||||||
|
.with_viscosity(1e-2)
|
||||||
|
.with_reference_velocity(1.0)
|
||||||
|
.with_reference_length(1.0);
|
||||||
|
|
||||||
|
let params = SimpleParameters::default()
|
||||||
|
.with_pressure_relaxation(0.3)
|
||||||
|
.with_velocity_relaxation(0.7)
|
||||||
|
.with_max_iterations(20000)
|
||||||
|
.with_convection_scheme(ConvectionScheme::TvdVanAlbada)
|
||||||
|
// Same corner-singularity floor as the upwind test above.
|
||||||
|
.with_tolerance(2e-4);
|
||||||
|
|
||||||
|
let mut solver = SimpleSolver::new(config, params)?;
|
||||||
|
|
||||||
|
let nx = 65;
|
||||||
|
let ny = 65;
|
||||||
|
let dx = 1.0 / (nx as f64 - 1.0);
|
||||||
|
let dy = 1.0 / (ny as f64 - 1.0);
|
||||||
|
let mut flow_field = FlowField::new(nx, ny, dx, dy)?;
|
||||||
|
|
||||||
|
let mut bcs = BoundaryConditions::new();
|
||||||
|
for location in [
|
||||||
|
BoundaryLocation::Left,
|
||||||
|
BoundaryLocation::Right,
|
||||||
|
BoundaryLocation::Bottom,
|
||||||
|
BoundaryLocation::Top,
|
||||||
|
] {
|
||||||
|
bcs.add_boundary_condition(location, BoundaryType::FreeSlipWall);
|
||||||
|
}
|
||||||
|
let domain_top = ny as f64 * dy;
|
||||||
|
solver.set_wall_velocity(move |_x, y| {
|
||||||
|
if y > 0.5 * domain_top {
|
||||||
|
(1.0, 0.0)
|
||||||
|
} else {
|
||||||
|
(0.0, 0.0)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
flow_field.apply_boundary_conditions(&bcs)?;
|
||||||
|
|
||||||
|
let result = solver.solve(&mut flow_field, &bcs).await?;
|
||||||
|
assert!(
|
||||||
|
result.solver_result.converged,
|
||||||
|
"TVD cavity did not converge: residual {:.3e} after {} iterations",
|
||||||
|
result.solver_result.final_residual, result.solver_result.iterations
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut u_min = 0.0_f64;
|
||||||
|
let mut y_at_min = 0.0_f64;
|
||||||
|
for j in 0..ny {
|
||||||
|
let (u, _) = flow_field.get_velocity_at(nx / 2, j)?;
|
||||||
|
if u < u_min {
|
||||||
|
u_min = u;
|
||||||
|
y_at_min = j as f64 / (ny - 1) as f64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
" TVD 65^2 cavity: u_min = {u_min:.4} at y = {y_at_min:.4} \
|
||||||
|
(Ghia: -0.2109 at 0.4531) iterations = {}",
|
||||||
|
result.solver_result.iterations
|
||||||
|
);
|
||||||
|
|
||||||
|
// Measured: u_min = -0.2036 at y = 0.4844, in 790 iterations. Upwind
|
||||||
|
// on the same mesh reads -0.1932 at 0.5000, so the TVD scheme closes
|
||||||
|
// 59% of the remaining gap to Ghia's -0.2109 at equal resolution. The
|
||||||
|
// band is set around what the scheme delivers and excludes the upwind
|
||||||
|
// value: falling back to first order is the regression this test is
|
||||||
|
// here to catch.
|
||||||
|
assert!(
|
||||||
|
(-0.215..-0.195).contains(&u_min),
|
||||||
|
"centreline minimum {u_min:.4} outside the band the TVD scheme \
|
||||||
|
warrants at 65^2 (measured -0.2036; upwind gives -0.1932; \
|
||||||
|
Ghia -0.2109)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Position: 0.4844 against Ghia's 0.4531, down from upwind's 0.5000.
|
||||||
|
// The gridline spacing is 1/64, so the reading is quantised; the upper
|
||||||
|
// bound excludes 0.5000 exactly because that is the upwind value.
|
||||||
|
assert!(
|
||||||
|
(0.44..0.50).contains(&y_at_min),
|
||||||
|
"primary vortex at y = {y_at_min:.4}, outside the TVD band \
|
||||||
|
(measured 0.4844; Ghia 0.4531; upwind reads 0.5000)"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_simple_pressure_correction() -> CfdResult<()> {
|
async fn test_simple_pressure_correction() -> CfdResult<()> {
|
||||||
// Test that pressure correction step actually corrects mass balance
|
// Test that pressure correction step actually corrects mass balance
|
||||||
|
|||||||
Reference in New Issue
Block a user