rtx-cfd: Robin wall sign pin — a still closed annulus under a uniform pressure above a zero datum: the explicit offset recedes into the body at exactly −p₀/(α(1+g)) (3e-16), the implicit compliant term answers with a pressure DROP (mean 0.17 of p₀, max 0.36) that absorbs the recession (net wall flux 6e-19 of 0.51); the steady MMS pin is blind to the sign of both parts
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
Performance Benchmarks / Run Benchmarks (push) Failing after 6s
CI / Format Check (push) Failing after 6s
CI / Clippy Check (push) Failing after 7s
CI / Build (ubuntu-latest) (push) Failing after 7s
Documentation / Build User Guide (push) Successful in 5s
CI / Build CPU-Only (Explicit) (push) Failing after 1m2s
Documentation / Build API Documentation (push) Failing after 1m4s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL
This commit is contained in:
Omar Sobh
2026-09-15 11:17:38 -05:00
co-authored by Claude Fable 5.1
parent 1c98156653
commit 96a7f1c700
@@ -6,9 +6,9 @@
//! and ≈ 1 with upwind; every step is divergence-free to the solver's
//! tolerance.
use rtx_cfd::mesh::patch_gen::{annulus_skewed, cartesian};
use rtx_cfd::mesh::PatchMesh;
use rtx_cfd::mesh::PatchSide;
use rtx_cfd::mesh::patch_gen::{annulus_skewed, cartesian};
use rtx_cfd::solvers::incompressible::{
CurvilinearParameters, CurvilinearPisoSolver, NormalDiffusion, PatchConvection, PatchField,
RobinWall,
@@ -433,3 +433,95 @@ async fn skewed_annulus_stokes_with_a_robin_inner_wall_keeps_second_order() -> C
);
Ok(())
}
/// The Robin wall's SIGN — the steady MMS pin above is blind to it (both
/// parts vanish at its fixed point). A still CLOSED annulus under a
/// uniform pressure `p₀` above a zero datum: the explicit part must move
/// the wall INTO the body (offset · n < 0 with n = S/|S| into the fluid,
/// the magnitude `p₀/(α (1 + g))`, `g = μ/(α d)` the viscous damping);
/// the fluid cannot follow (the outer wall is fixed), so the implicit
/// compliant term `|S| p'/α` must answer with a pressure DROP that stops
/// the recession — `p' ≈ p₀/(1 + g)`, the net wall flux absorbed. With
/// the wrong sign the pressure would rise by the same amount.
#[tokio::test]
async fn robin_wall_recedes_under_pressure_on_both_parts() -> CfdResult<()> {
let mesh = annulus_skewed([0.0, 0.0], 0.5, 1.5, 48, 12, 0.3, 3.0)?;
let config = CfdConfig::new()
.with_density(RHO)
.with_viscosity(MU)
.with_reference_velocity(1.0)
.with_reference_length(1.0);
let params = CurvilinearParameters {
tolerance: 1e-12,
convection: PatchConvection::None,
normal_diffusion: NormalDiffusion::Explicit,
..CurvilinearParameters::default()
};
let mut solver = CurvilinearPisoSolver::new(config, params, mesh)?;
solver.set_boundary_velocity(|_, _, _| (0.0, 0.0));
let n_inner = (0..solver.mesh().faces().len())
.filter(|&f| solver.mesh().side(f) == Some(PatchSide::Inner))
.count();
let alpha = 4.0;
solver.set_robin_wall(Some(RobinWall {
alpha,
datum: vec![[0.0, 0.0]; n_inner],
}));
let mut field = PatchField::new(solver.mesh());
solver.initialize(&mut field, |_, _| (0.0, 0.0));
let p0 = 1.0;
field.p.iter_mut().for_each(|p| *p = p0);
let h = min_spacing(solver.mesh());
let dt = 0.1 * (h * h / (4.0 * MU / RHO)).min(h);
solver.advance(&mut field, dt).await?;
let mesh = solver.mesh();
let inner: Vec<usize> = (0..mesh.faces().len())
.filter(|&f| mesh.side(f) == Some(PatchSide::Inner))
.collect();
let offsets = solver.robin_offsets();
assert_eq!(offsets.len(), inner.len());
let (mut worst_rel, mut net_flux, mut recession, mut p_expect) = (0.0_f64, 0.0, 0.0, 0.0);
for (j, &f) in inner.iter().enumerate() {
let face = &mesh.faces()[f];
let s = face.s; // owner None, neigh Some: S points into the fluid
let len = (s[0] * s[0] + s[1] * s[1]).sqrt();
let n = [s[0] / len, s[1] / len];
let c = mesh.boundary_cell(f);
let xc = mesh.centre(c);
let d = ((face.centre[0] - xc[0]).powi(2) + (face.centre[1] - xc[1]).powi(2)).sqrt();
let g = MU / (alpha * d);
let expect = -p0 / (alpha * (1.0 + g));
let got = offsets[j][0] * n[0] + offsets[j][1] * n[1];
assert!(
got < 0.0,
"face {f}: explicit offset · n = {got:.3e} (must recede)"
);
worst_rel = worst_rel.max(((got - expect) / expect).abs());
net_flux += field.flux[f];
recession += got.abs() * len;
p_expect += (p0 - p0 / (1.0 + g)) / inner.len() as f64;
}
let p_mean = field.p.iter().sum::<f64>() / field.p.len() as f64;
let p_max = field.p.iter().cloned().fold(f64::MIN, f64::max);
println!(
" Robin sign pin: {} Inner faces, explicit offset p₀/(α(1+g)) to {worst_rel:.2e}; net wall flux {net_flux:.2e} vs the recession's {recession:.2e}; pressure after the step mean {p_mean:.4} max {p_max:.4} (p₀ {p0}, compliant answer ≈ {p_expect:.4})",
inner.len()
);
assert!(
worst_rel < 1e-9,
"explicit offset magnitude off by {worst_rel:.2e}"
);
assert!(
net_flux.abs() < 1e-6 * recession,
"the compliant wall did not absorb the recession: net {net_flux:.3e} of {recession:.3e}"
);
assert!(
p_max < p0,
"the pressure ROSE (max {p_max:.4} ≥ p₀): the implicit term advances the wall"
);
assert!(
(p_mean - p_expect).abs() < 0.3 * p0,
"pressure drop {p_mean:.4} far from the compliant answer {p_expect:.4}"
);
Ok(())
}