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
The first coupled fluid-structure computation in the workspace, verified against a closed form, and the first time rtx-fsi's added-mass claims run against a real discretised fluid rather than a linear model map. ALE extensions: per-side boundaries (Velocity / SlipWall / PressureOutlet) and moving boundary lines. A moving Velocity side is a material wall whose prescribed normal velocity must equal the line's own motion; a pressure outlet takes Dirichlet p' = 0 in the projection (replacing the Neumann anchor) with a zero-gradient predictor on its faces. Fluid half verified alone (tests/ale_piston_channel.rs): prescribed piston motion, slip walls, outlet. The incompressible rigid column is exact DISCRETELY - continuity forces every u to the wall's discrete velocity (8e-12) and the projected pressure is exactly linear with gradient rho times the wall's backward-difference acceleration (2.5e-9). Coupled benchmark (rtx-fsi/tests/piston_added_mass.rs): elastic piston (Newmark average acceleration) against added mass rho*L*H at mass ratio 6.25, rtx-fsi's Subiterated driving a real fluid/structure pass per step: - plain staggered diverges in 7 subiterations (Causin-Gerbeau-Nobile on a real solver); - Aitken converges at 3.0 subiterations/step onto T = 1.07009 vs the closed form 1.06999 - 9.8e-5 relative, halving with dt; - outlet flux matches the piston sweep to ~1e-9 every step. Discrete-analysis finding: Newmark beta scales the staggered added-mass threshold - the iteration gain is beta*m_a/(M + K*beta*dt^2), so the continuous ratio 2.5 CONVERGES at beta = 1/4 (gain 0.625, measured ~17 passes/step) and the benchmark needs ratio 6.25 (gain 1.56). Two real defects found and fixed, twelfth and thirteenth of the campaign: 1. rtx-cfd ale::advance re-stamped boundary faces at t_old from the current boundary function, which in a coupling loop carries the NEW interval's wall velocity - the predictor's old state had interior u = w0 but wall face u = w1, leaving an O(dt) pressure artifact confined to the wall-adjacent cells (p exact to 6e-11 everywhere except the wall cell at 4.7e-5). The start-of-step boundary faces are whatever the previous step's end-of-step application left there. 2. rtx-fsi aitken_factor guarded its denominator - a SQUARED residual- difference norm - against a bare f64::EPSILON, silently disabling Aitken below residual ~1e-8 and degrading to unit relaxation exactly in the well-converged regime; the repulsive fixed point then amplified 1e-9 residuals back up and the coupling diverged. Third instance of the absolute-threshold species (NNLS, ECSW). The guard is relative now; aitken_is_scale_invariant pins it at initial residual 1e-9. rtx-cfd 293 green (+1), rtx-fsi 29 green (+3). rtx-fsi's lib gains only the relative guard; the coupling layer still depends on no solver (rtx-cfd is a dev-dependency of its tests). Co-Authored-By: Claude Fable 5 <[email protected]>
151 lines
6.2 KiB
Rust
151 lines
6.2 KiB
Rust
//! The discrete geometric conservation law (DGCL) test for the ALE solver.
|
||
//!
|
||
//! **Uniform flow must stay exactly uniform on an arbitrarily moving mesh.**
|
||
//! A constant velocity field with a constant pressure is an exact solution of
|
||
//! the incompressible Navier–Stokes equations regardless of how the mesh
|
||
//! moves underneath it; an ALE discretisation preserves it if and only if its
|
||
//! discrete volume increments equal the sums of its discrete face-swept
|
||
//! volumes — the DGCL (Thomas & Lombard 1979; Farhat, Geuzaine & Grandmont
|
||
//! 2001). Nothing about the physics is exercised, so any deviation is pure
|
||
//! geometric inconsistency, and it shows up at machine precision rather than
|
||
//! at truncation order.
|
||
//!
|
||
//! For tensor-product mesh motion the trapezoidal face rule is exactly
|
||
//! conservative:
|
||
//!
|
||
//! ```text
|
||
//! dx1*dy1 - dx0*dy0 = (dx1-dx0)*(dy0+dy1)/2 + (dy1-dy0)*(dx0+dx1)/2
|
||
//! ```
|
||
//!
|
||
//! an algebraic identity, so with time-averaged face areas in both the fluid
|
||
//! fluxes and the swept volumes the uniform state is a fixed point of the
|
||
//! update to rounding error. The negative control replaces the averaged areas
|
||
//! with end-of-step areas — the "obvious" choice that looks consistent and is
|
||
//! first-order accurate — and per step each cell then picks up a relative
|
||
//! error of exactly `dw*dh/V` (the cross term the identity above absorbs).
|
||
//! That control failing loudly is what proves this test can fail.
|
||
|
||
use rtx_cfd::solvers::incompressible::ale::{
|
||
AleField, AleParameters, AlePisoSolver, SweptFaceRule,
|
||
};
|
||
use rtx_cfd::{CfdConfig, CfdResult};
|
||
use std::f64::consts::PI;
|
||
|
||
const U_UNIFORM: f64 = 0.7;
|
||
const V_UNIFORM: f64 = -0.4;
|
||
const LX: f64 = 1.0;
|
||
const LY: f64 = 0.75;
|
||
const NX: usize = 16;
|
||
const NY: usize = 12;
|
||
|
||
/// Interior mesh-line motion: smooth, boundary-fixed, with different lines
|
||
/// moving out of phase (the `phase * xi` term) and incommensurate frequencies
|
||
/// in the two directions, so no symmetry can hide a conservation defect.
|
||
/// The displacement gradient stays below 1 (`amp * (pi + phase) < 1` in the
|
||
/// normalised coordinate), so mesh lines never cross.
|
||
fn moved_x(xi: f64, t: f64) -> f64 {
|
||
let s = xi / LX;
|
||
xi + 0.06 * LX * (PI * s).sin() * (2.9 * t + 3.0 * s).sin()
|
||
}
|
||
|
||
fn moved_y(yj: f64, t: f64) -> f64 {
|
||
let s = yj / LY;
|
||
yj + 0.06 * LY * (PI * s).sin() * (4.3 * t + 2.0 * s).sin()
|
||
}
|
||
|
||
fn reference_x() -> Vec<f64> {
|
||
(0..=NX).map(|i| LX * i as f64 / NX as f64).collect()
|
||
}
|
||
|
||
fn reference_y() -> Vec<f64> {
|
||
(0..=NY).map(|j| LY * j as f64 / NY as f64).collect()
|
||
}
|
||
|
||
/// March uniform flow on the wiggling mesh and return the largest deviation
|
||
/// from uniformity, over every velocity unknown and every step, plus the
|
||
/// largest final pressure magnitude.
|
||
async fn max_deviation(rule: SweptFaceRule, steps: usize, dt: f64) -> CfdResult<(f64, f64)> {
|
||
let config = CfdConfig::new()
|
||
.with_density(1.0)
|
||
.with_viscosity(0.05)
|
||
.with_reference_velocity(1.0)
|
||
.with_reference_length(1.0);
|
||
// The tolerance must sit at the rounding floor, not at an engineering
|
||
// level: the projection's inner stop is floored at
|
||
// `0.1 * tolerance * reference_flux`, and with a 1e-9 tolerance the SOR
|
||
// quits after one sweep on the eps-level sources this test produces,
|
||
// leaving a partial p' that accumulates into p (~3e-9 over 400 steps)
|
||
// and whose gradient re-perturbs the velocities at ~1e-11 — five orders
|
||
// above rounding, with the geometry entirely blameless. Measured before
|
||
// and after: 3.6e-11 at tolerance 1e-9, rounding-level at 1e-13.
|
||
let params = AleParameters {
|
||
corrector_steps: 2,
|
||
tolerance: 1e-13,
|
||
swept_face_rule: rule,
|
||
..AleParameters::default()
|
||
};
|
||
let mut solver = AlePisoSolver::new(config, params)?;
|
||
solver.set_boundary_velocity(|_x, _y, _t| (U_UNIFORM, V_UNIFORM));
|
||
|
||
let mut field = AleField::new(reference_x(), reference_y())?;
|
||
field.u.fill(U_UNIFORM);
|
||
field.v.fill(V_UNIFORM);
|
||
field.p.fill(0.0);
|
||
|
||
let (rx, ry) = (reference_x(), reference_y());
|
||
let mut worst: f64 = 0.0;
|
||
for step in 0..steps {
|
||
let t_new = (step + 1) as f64 * dt;
|
||
let new_x: Vec<f64> = rx.iter().map(|&xi| moved_x(xi, t_new)).collect();
|
||
let new_y: Vec<f64> = ry.iter().map(|&yj| moved_y(yj, t_new)).collect();
|
||
solver.advance(&mut field, &new_x, &new_y, dt).await?;
|
||
|
||
for value in field.u.iter() {
|
||
worst = worst.max((value - U_UNIFORM).abs());
|
||
}
|
||
for value in field.v.iter() {
|
||
worst = worst.max((value - V_UNIFORM).abs());
|
||
}
|
||
}
|
||
|
||
let max_p = field.p.iter().fold(0.0f64, |m, &p| m.max(p.abs()));
|
||
Ok((worst, max_p))
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn uniform_flow_stays_exactly_uniform_on_an_arbitrarily_moving_mesh() -> CfdResult<()> {
|
||
let (worst, max_p) = max_deviation(SweptFaceRule::Trapezoidal, 400, 1e-3).await?;
|
||
println!(" DGCL: max |u - U| over 400 steps = {worst:.3e}, final max |p| = {max_p:.3e}");
|
||
|
||
// Machine precision relative to the velocity scale — not truncation
|
||
// order. 400 steps of rounding accumulate to ~1e-13 at worst.
|
||
let scale = U_UNIFORM.abs().max(V_UNIFORM.abs());
|
||
assert!(
|
||
worst < 1e-11 * scale,
|
||
"DGCL violated: uniform flow deviated by {worst:.3e} on the moving mesh"
|
||
);
|
||
// With a uniform field the projection source is exactly zero, so the
|
||
// pressure must never move off its initial constant.
|
||
assert!(max_p < 1e-11, "pressure moved off constant: {max_p:.3e}");
|
||
Ok(())
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn end_of_step_face_areas_violate_the_gcl_visibly() -> CfdResult<()> {
|
||
// The control: identical run, but fluxes and swept volumes use the
|
||
// end-of-step face areas. Each cell's update then multiplies the uniform
|
||
// state by (1 + dw*dh/V) per step — a defect this test must see and the
|
||
// conservative rule must not have.
|
||
let (worst, _) = max_deviation(SweptFaceRule::EndOfStep, 400, 1e-3).await?;
|
||
println!(" GCL-violating control: max |u - U| over 400 steps = {worst:.3e}");
|
||
|
||
let scale = U_UNIFORM.abs().max(V_UNIFORM.abs());
|
||
assert!(
|
||
worst > 1e-6 * scale,
|
||
"the GCL-violating rule deviated only {worst:.3e} — the DGCL test \
|
||
has lost its teeth (motion too tame, or the rule is not actually \
|
||
reaching the fluxes)"
|
||
);
|
||
Ok(())
|
||
}
|