rtx-cfd: overset A-P2 — the patch overlaps the background (OversetPisoSolver), gated S1–S5
CI / Test (macos-latest) (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 (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
Documentation / Build User Guide (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
CI / Test (macos-latest) (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 (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
Documentation / Build User Guide (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
Background = the embedded solver with a mask from the overlap classification
(embedded/{mod,projection}.rs: module split, projection's solve/apply halves,
set_overlap, fringe p' Dirichlet by elimination into extra_diag/rhs, anchor
dropped, set_inner_stop_factor, phase API begin_step/solve_correction/
apply_correction/end_step; advance rebuilt on the phases — every suite digit-
identical, FSI2 default line-for-line). Patch = the curvilinear solver with an
acceptor ring (set_side_velocity; set_acceptor_ring/stamp_acceptors/
set_acceptor_correction; acceptor Dirichlet by elimination into
PressureSystem.links so the BiCGSTAB stop stays in flux units — identity rows
measured unconverged at 2431 iterations; same phase API). overset/overlap.rs:
OverlapMap — hole/fringe/active from the patch's own indices (hole = body or
k <= nn-1-overlap_rows, DEFAULT_OVERLAP_ROWS = 4 from the 2.9 h depth budget),
dual-quad inverse-bilinear donors patch→fringe, lattice donors →acceptors,
both invariants asserted, mass-defect measures. overset/mod.rs:
OversetPisoSolver — advance (exchange rebuilt BEFORE the predictors from the
previous corrected field), alternating Schwarz on the acceptor p' vector with
Anderson(3) (plain Schwarz measured 0.82/round: floating patch, Neumann wall)
and the previous step's vector as warm start (1 round/corrector at steady
state), stop relative to the STEP's p' scale (the MG absolute stop is
1e-9/dt² in pressure — the whole second correction), set_patch_mesh,
snapshot/restore carrying the warm-start vector.
Gates: overlap linear-exact 1e-13, quadratic orders 1.96/1.99 (acceptors),
1.40/1.91 (fringe); half-couplings: patch with exact acceptors Stokes 2.07/1.98
+ 2.08/1.98, upwind 0.84/0.84, background with exact fringe 7.86e-3/2.90e-3/
1.09e-3 (1.44/1.41); two-mesh MMS n=32/64: background 8.717e-3/4.207e-3 (1.03x/
0.97x the embedded circle), patch 1.322e-2/6.904e-3 (1.5-1.6x), orders 1.05/
0.94, patch div <= 5e-13, overlap mass defect 3.6e-3 -> 8.2e-4 of the overlap
flux (under the registered 1e-3 from n=64; disclosed at 32); motion: stationary
patch through set_patch_mesh bit-identical, snapshot/restore with a pending mesh
bit-identical, translating phantom circle 1.22x/1.19x the static level over
4.5 cells. Inherited, disclosed: poisson_equivalence's no-body multigrid pin
fails by 3.9e-9 at d46fb0b (M1's commit; verified in a clean worktree).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d46fb0b7a7
commit
afd1bff6ee
@@ -0,0 +1,240 @@
|
||||
//! A-P2, step S5 (`docs/overset_metal_campaign.md` §5.9): the overset with a
|
||||
//! MOVING patch.
|
||||
//!
|
||||
//! 1. A stationary patch pushed through `set_patch_mesh` every step is the
|
||||
//! static overset path to the bit (the overlap rebuild and the fringe
|
||||
//! re-stamping reproduce the same numbers).
|
||||
//! 2. The phantom circle and its patch translating through the steady
|
||||
//! manufactured field (the `embedded_moving.rs` pattern: the wall carries
|
||||
//! the exact velocity, so the exact solution is unchanged while the
|
||||
//! hole, fringe and acceptors sweep the background) keep the time-max
|
||||
//! L2 on both meshes within 1.5× the static level; reclassification
|
||||
//! counts are printed.
|
||||
//! 3. Snapshot/restore with a pending patch mesh re-runs bit-identically.
|
||||
|
||||
mod overset_common;
|
||||
|
||||
use overset_common::{build, errors, p2_patch};
|
||||
use rtx_cfd::CfdResult;
|
||||
use rtx_cfd::mesh::PatchMesh;
|
||||
use rtx_cfd::mesh::patch_gen::annulus_skewed;
|
||||
use rtx_cfd::solvers::incompressible::OversetField;
|
||||
|
||||
fn fields_identical(a: &OversetField, b: &OversetField) -> bool {
|
||||
let bits = |v: &[f64], w: &[f64]| v.iter().zip(w).all(|(x, y)| x.to_bits() == y.to_bits());
|
||||
bits(a.background.u.as_slice(), b.background.u.as_slice())
|
||||
&& bits(a.background.v.as_slice(), b.background.v.as_slice())
|
||||
&& bits(a.background.p.as_slice(), b.background.p.as_slice())
|
||||
&& bits(&a.patch.u, &b.patch.u)
|
||||
&& bits(&a.patch.v, &b.patch.v)
|
||||
&& bits(&a.patch.p, &b.patch.p)
|
||||
&& bits(&a.patch.flux, &b.patch.flux)
|
||||
}
|
||||
|
||||
fn max_diff(a: &OversetField, b: &OversetField) -> f64 {
|
||||
let d = |v: &[f64], w: &[f64]| {
|
||||
v.iter()
|
||||
.zip(w)
|
||||
.map(|(x, y)| (x - y).abs())
|
||||
.fold(0.0, f64::max)
|
||||
};
|
||||
d(a.background.u.as_slice(), b.background.u.as_slice())
|
||||
.max(d(a.background.v.as_slice(), b.background.v.as_slice()))
|
||||
.max(d(a.background.p.as_slice(), b.background.p.as_slice()))
|
||||
.max(d(&a.patch.u, &b.patch.u))
|
||||
.max(d(&a.patch.v, &b.patch.v))
|
||||
.max(d(&a.patch.p, &b.patch.p))
|
||||
.max(d(&a.patch.flux, &b.patch.flux))
|
||||
}
|
||||
|
||||
/// The P2 patch translated to centre `(cx, cy)`.
|
||||
fn translated_patch(n: usize, cx: f64, cy: f64) -> CfdResult<PatchMesh> {
|
||||
annulus_skewed([cx, cy], 0.2, 0.354, 9 * n / 4, n / 4, 0.3, 3.0)
|
||||
}
|
||||
|
||||
fn time_step(solver: &rtx_cfd::solvers::incompressible::OversetPisoSolver, n: usize) -> f64 {
|
||||
let h = 1.0 / n as f64;
|
||||
let mut hp = f64::INFINITY;
|
||||
for c in 0..solver.patch().mesh().cell_count() {
|
||||
for (f, _) in solver.patch().mesh().cell_faces(c) {
|
||||
let d = solver.patch().mesh().faces()[f].d;
|
||||
hp = hp.min((d[0] * d[0] + d[1] * d[1]).sqrt());
|
||||
}
|
||||
}
|
||||
0.4 * (hp * hp / (4.0 * 0.05)).min(h)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stationary_patch_through_the_moving_path_is_bit_identical() -> CfdResult<()> {
|
||||
let n = 32;
|
||||
let (mut plain, mut f_plain) = build(n, p2_patch(n)?, 1e-3)?;
|
||||
let (mut moving, mut f_moving) = build(n, p2_patch(n)?, 1e-3)?;
|
||||
let dt = time_step(&plain, n);
|
||||
for _ in 0..20 {
|
||||
plain.advance(&mut f_plain, dt).await?;
|
||||
let same = moving.patch().mesh().clone();
|
||||
moving.set_patch_mesh(same)?;
|
||||
let r = moving.advance(&mut f_moving, dt).await?;
|
||||
assert_eq!(r.reclassified_cells, 0);
|
||||
}
|
||||
assert_eq!(plain.time().to_bits(), moving.time().to_bits());
|
||||
assert!(
|
||||
fields_identical(&f_plain, &f_moving),
|
||||
"moving path with a stationary patch differs from the static path by {:.3e}",
|
||||
max_diff(&f_plain, &f_moving)
|
||||
);
|
||||
println!(" stationary patch through the moving overset path — bit-identical over 20 steps");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn translating_phantom_circle_keeps_the_static_error_level() -> CfdResult<()> {
|
||||
let n: usize = std::env::var("RTX_OVERSET_N")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(32);
|
||||
let h = 1.0 / n as f64;
|
||||
let (mut solver, mut field) = build(n, p2_patch(n)?, 1e-3)?;
|
||||
let dt = time_step(&solver, n);
|
||||
|
||||
// Static phase to the steady state.
|
||||
let mut steady = f64::INFINITY;
|
||||
for _ in 0..400_000 {
|
||||
let before = (field.background.u.clone(), field.patch.u.clone());
|
||||
solver.advance(&mut field, dt).await?;
|
||||
let change = (&field.background.u - &before.0).abs().max().max(
|
||||
field
|
||||
.patch
|
||||
.u
|
||||
.iter()
|
||||
.zip(&before.1)
|
||||
.map(|(a, b)| (a - b).abs())
|
||||
.fold(0.0, f64::max),
|
||||
);
|
||||
steady = change / dt;
|
||||
if steady < 1e-6 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(steady < 1e-6, "no static steady state: {steady:.3e}");
|
||||
let (static_bg, static_patch) = errors(&solver, &field, n);
|
||||
println!(" static level n = {n}: L2 background {static_bg:.4e}, patch {static_patch:.4e}");
|
||||
|
||||
// Moving phase: the circle and its patch translate at speed 0.3 to the
|
||||
// left through ~4.5 background cells (per step ≤ 0.01 h).
|
||||
let speed = 0.3;
|
||||
let t0 = solver.time();
|
||||
let distance = 4.5 * h;
|
||||
let steps = (distance / (speed * dt)).ceil() as usize;
|
||||
let (mut worst_bg, mut worst_patch) = (0.0_f64, 0.0_f64);
|
||||
let (mut reclassified, mut fresh) = (0usize, 0usize);
|
||||
let mut max_rounds = 0usize;
|
||||
for step in 0..steps {
|
||||
let t_new = solver.time() + dt;
|
||||
let cx = 0.6 - speed * (t_new - t0);
|
||||
solver.set_patch_mesh(translated_patch(n, cx, 0.45)?)?;
|
||||
let r = solver.advance(&mut field, dt).await?;
|
||||
reclassified += r.reclassified_cells;
|
||||
fresh += r.fresh_cells;
|
||||
max_rounds = max_rounds.max(r.rounds.iter().copied().max().unwrap_or(0));
|
||||
let (eb, ep) = errors(&solver, &field, n);
|
||||
worst_bg = worst_bg.max(eb);
|
||||
worst_patch = worst_patch.max(ep);
|
||||
if std::env::var("RTX_OVERSET_TRACE").is_ok() && step % 200 == 0 {
|
||||
println!(
|
||||
" moving step {step}: cx {cx:.4} L2 bg {eb:.4e} patch {ep:.4e} reclassified {} fresh {} rounds {:?} defect bg {:.2e}",
|
||||
r.reclassified_cells,
|
||||
r.fresh_cells,
|
||||
r.rounds,
|
||||
r.background_mass_defect / r.overlap_flux_scale.max(1e-300)
|
||||
);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
" moving n = {n}: {steps} steps, {distance:.3} travelled; time-max L2 background {worst_bg:.4e} ({:.2}x static), \
|
||||
patch {worst_patch:.4e} ({:.2}x static); reclassified {reclassified} cells, fresh {fresh}; max Schwarz rounds {max_rounds}",
|
||||
worst_bg / static_bg,
|
||||
worst_patch / static_patch
|
||||
);
|
||||
assert!(
|
||||
worst_bg < 1.5 * static_bg,
|
||||
"background error rose to {worst_bg:.3e} (static {static_bg:.3e})"
|
||||
);
|
||||
assert!(
|
||||
worst_patch < 1.5 * static_patch,
|
||||
"patch error rose to {worst_patch:.3e} (static {static_patch:.3e})"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn snapshot_restore_with_a_pending_mesh_is_bit_identical() -> CfdResult<()> {
|
||||
let n = 32;
|
||||
let (mut solver, mut field) = build(n, p2_patch(n)?, 1e-3)?;
|
||||
let dt = time_step(&solver, n);
|
||||
let speed = 0.3;
|
||||
let mut step = 0usize;
|
||||
let cx = |s: usize| 0.6 - speed * s as f64 * dt;
|
||||
for _ in 0..5 {
|
||||
step += 1;
|
||||
solver.set_patch_mesh(translated_patch(n, cx(step), 0.45)?)?;
|
||||
solver.advance(&mut field, dt).await?;
|
||||
}
|
||||
solver.set_patch_mesh(translated_patch(n, cx(step + 1), 0.45)?)?;
|
||||
let saved = solver.snapshot();
|
||||
let field_saved = field.clone();
|
||||
let step_saved = step;
|
||||
for _ in 0..10 {
|
||||
step += 1;
|
||||
if step > step_saved + 1 {
|
||||
solver.set_patch_mesh(translated_patch(n, cx(step), 0.45)?)?;
|
||||
}
|
||||
solver.advance(&mut field, dt).await?;
|
||||
}
|
||||
let reference = field.clone();
|
||||
let t_ref = solver.time();
|
||||
|
||||
solver.restore(&saved);
|
||||
field = field_saved;
|
||||
step = step_saved;
|
||||
for _ in 0..10 {
|
||||
step += 1;
|
||||
if step > step_saved + 1 {
|
||||
solver.set_patch_mesh(translated_patch(n, cx(step), 0.45)?)?;
|
||||
}
|
||||
solver.advance(&mut field, dt).await?;
|
||||
}
|
||||
assert_eq!(solver.time().to_bits(), t_ref.to_bits());
|
||||
assert!(
|
||||
fields_identical(&field, &reference),
|
||||
"re-run on the moving overset differs by {:.3e}",
|
||||
max_diff(&field, &reference)
|
||||
);
|
||||
println!(" snapshot/restore with a pending patch mesh — bit-identical re-run over 10 steps");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One step with the patch jumped by two background cells must reclassify
|
||||
/// background cells (the overlap follows the pending mesh).
|
||||
#[tokio::test]
|
||||
async fn a_translated_patch_reclassifies_the_background() -> CfdResult<()> {
|
||||
let n = 32;
|
||||
let (mut solver, mut field) = build(n, p2_patch(n)?, 1e-3)?;
|
||||
let dt = time_step(&solver, n);
|
||||
solver.advance(&mut field, dt).await?;
|
||||
let before = solver.overlap().hole_cells();
|
||||
solver.set_patch_mesh(translated_patch(n, 0.6 - 2.0 / n as f64, 0.45)?)?;
|
||||
let r = solver.advance(&mut field, dt).await?;
|
||||
println!(
|
||||
" 2 h jump: reclassified {}, fresh {}, holes {} -> {}",
|
||||
r.reclassified_cells,
|
||||
r.fresh_cells,
|
||||
before,
|
||||
solver.overlap().hole_cells()
|
||||
);
|
||||
assert!(
|
||||
r.reclassified_cells > 0,
|
||||
"the overlap did not follow the patch"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user