Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_wall_position_oblique.rs
T
Omar SobhandClaude Fable 5.1 e9b2e7887b
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
CI / Format Check (push) Failing after 5s
CI / Clippy Check (push) Failing after 5s
CI / Build (ubuntu-latest) (push) Failing after 4s
Performance Benchmarks / Run Benchmarks (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
Documentation / Build API Documentation (push) Failing after 17s
CI / Build CPU-Only (Explicit) (push) Failing after 57s
embedded3 S2-6: the oblique-wall instrument (tests/embedded3_wall_position_oblique.rs: z-flow / in-plane Poiseuille + Couette linear exactness; gate oblique_wall_position_is_second_order) and the closures it found, host + device, default OFF — the transverse centroid correction (RTX_E3_DIFFUSION_TRANSVERSE=1, wall_order bit 7), the fine distance floor (RTX_E3_DISTANCE_FLOOR=fine, bit 8); kernel geometry factored into cut_cv; DFG 2D-1 x-shift knob (RTX_E3_DFG_SHIFT_X); FLAG_X0 0.6 -> 0.25 in the three flag tests (the flag of every record was detached)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-18 16:22:38 -05:00

379 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! S2-6 instrument: the cut wall's effective position on an OBLIQUE wall
//! with the flow IN the plane of the cut. Poiseuille flow (body force `F`
//! along the tangent) in a channel between two embedded parallel planes
//! `y = s x + c0` and `y = s x + c0 + w`; the sides carry the exact
//! solution, z is periodic. The exact field is `U(r) t`, `U = F/(2μ)
//! (G²/4 r²)`, constant pressure, and the 5-point Laplacian is exact on
//! it, so on full faces of a central window `u/t_x + F r²/(2μ)` is the
//! constant `F G_eff²/(8μ)`: the effective gap from the u faces and from
//! the v faces separately, each an effective wall offset per wall in
//! units of h. Unlike the z-directed flat-wall test this one exercises the
//! own-direction coupling of cut faces, the convective terms' cut-face
//! values and the projection next to the wall (the spurious pressure is
//! printed).
use rtx_cfd::solvers::incompressible::ConvectionScheme;
use rtx_cfd::solvers::incompressible::embedded3::{
Body, Boundaries, Field, Fluid, Grid, Parameters, Side, Solver, WallScheme,
};
const MU: f64 = 0.1;
const LX: f64 = 2.0;
const LY: f64 = 2.0;
const W: f64 = 0.5;
/// The cut wall's parameters: the environment's (`None`), or the S2-6
/// closures forced on / off for the gate.
fn parameters(s26: Option<bool>) -> Parameters {
let mut p = Parameters {
corrector_steps: 2,
tolerance: 1e-10,
convection_scheme: ConvectionScheme::Upwind,
wall_scheme: WallScheme::CutCell,
boundaries: Boundaries {
z0: Side::Periodic,
z1: Side::Periodic,
..Boundaries::default()
},
..Parameters::default()
};
if let Some(on) = s26 {
p.diffusion_centroid = true;
p.wall_distance_oblique = on;
p.diffusion_transverse = on;
p.distance_floor_fine = on;
}
p
}
struct Reading {
/// Effective wall offset per wall from the fitted profile of the u / v faces.
off_u: f64,
off_v: f64,
/// The driving force the profile's curvature implies, over F.
force_u: f64,
/// 1 (the fitted streamwise pressure slope)/F: must equal `force_u`.
force_p: f64,
/// RMS of the pressure about its linear fit, over F·G: full cells, cut cells.
p_full: f64,
p_cut: f64,
}
/// Least squares of `y = a b x`: returns (a, b).
fn fit(points: &[(f64, f64)]) -> (f64, f64) {
let n = points.len() as f64;
let (sx, sy) = points
.iter()
.fold((0.0, 0.0), |s, p| (s.0 + p.0, s.1 + p.1));
let (mx, my) = (sx / n, sy / n);
let (sxx, sxy) = points.iter().fold((0.0, 0.0), |s, p| {
(s.0 + (p.0 - mx) * (p.0 - mx), s.1 + (p.0 - mx) * (p.1 - my))
});
let slope = sxy / sxx;
(my - slope * mx, -slope)
}
fn reading(n: usize, slope: f64, c0: f64, along_z: bool, s26: Option<bool>) -> Reading {
// The driving force (`RTX_E3_OBLIQUE_F`): the problem is linear in it
// but for the convective terms, so a small value switches them off.
#[allow(non_snake_case)]
let F: f64 = std::env::var("RTX_E3_OBLIQUE_F")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(1.0);
let h = 1.0 / n as f64;
let (nx, ny, nz) = ((LX * n as f64) as usize, (LY * n as f64) as usize, 2);
let norm = (1.0 + slope * slope).sqrt();
let (tx, ty) = (1.0 / norm, slope / norm);
let gap = W / norm;
let r_of = move |x: f64, y: f64| ((y - slope * x - c0) - 0.5 * W) / norm;
let speed = move |x: f64, y: f64| {
let r = r_of(x, y);
if r.abs() < 0.5 * gap {
F / (2.0 * MU) * (0.25 * gap * gap - r * r)
} else {
0.0
}
};
let mut solver = Solver::new(
Fluid {
density: 1.0,
viscosity: MU,
reference_velocity: 1.0,
reference_length: 1.0,
},
parameters(s26),
);
// `along_z`: the same channel with the flow along the periodic z (the
// cross-direction diffusion of w alone: no pressure, no convection).
solver.set_boundary_velocity(move |x, y, _z, _t| {
let s = speed(x, y);
if along_z {
(0.0, 0.0, s)
} else {
(s * tx, s * ty, 0.0)
}
});
solver.set_momentum_source(move |_, _, _, _| {
if along_z {
(0.0, 0.0, F)
} else {
(F * tx, F * ty, 0.0)
}
});
solver.set_body(Body::from_sdf(move |x, y, _z, _t| {
0.5 * gap - r_of(x, y).abs()
}));
let g = Grid::cubic(nx, ny, nz, h);
let mut field = Field::new(g);
for k in 0..=nz {
for j in 0..ny {
for i in 0..nx {
if along_z {
field.w[g.wface(k, j, i)] = speed((i as f64 + 0.5) * h, (j as f64 + 0.5) * h);
}
}
}
}
for k in 0..nz {
if along_z {
break;
}
for j in 0..ny {
for i in 0..=nx {
field.u[g.uface(k, j, i)] = tx * speed(i as f64 * h, (j as f64 + 0.5) * h);
}
}
for j in 0..=ny {
for i in 0..nx {
field.v[g.vface(k, j, i)] = ty * speed((i as f64 + 0.5) * h, j as f64 * h);
}
}
}
solver.initialize(&mut field);
let dt = 0.5 * h * h / (6.0 * MU);
let steps = (2.0 / dt).ceil() as usize;
for _ in 0..steps {
solver.advance(&mut field, dt);
}
let mask = solver.mask().expect("mask");
// The sides pin the flow RATE (the exact profile), so a displaced wall
// appears as a streamwise pressure slope: F_eff = F dp/ds, and on the
// full faces of a central window `u/t_x = F_eff/(2μ) (G_eff²/4 r²)`
// exactly (the 5-point Laplacian is exact on it). Fit both constants.
let window = |x: f64| (x - 0.5 * LX).abs() < 0.3;
let (mut pu, mut pv) = (Vec::new(), Vec::new());
let (mut pf, mut pc) = (Vec::new(), Vec::new());
for j in 0..ny {
for i in 0..nx {
let (xu, yu) = (i as f64 * h, (j as f64 + 0.5) * h);
let fu = g.uface(0, j, i);
let (xw, yw) = ((i as f64 + 0.5) * h, (j as f64 + 0.5) * h);
let fw = g.wface(0, j, i);
if along_z {
if window(xw) && r_of(xw, yw).abs() < 0.4 * gap && mask.a_w(fw) >= 1.0 {
pu.push((r_of(xw, yw).powi(2), field.w[fw]));
}
} else if window(xu) && r_of(xu, yu).abs() < 0.4 * gap && mask.a_u(fu) >= 1.0 {
pu.push((r_of(xu, yu).powi(2), field.u[fu] / tx));
}
let (xv, yv) = ((i as f64 + 0.5) * h, j as f64 * h);
let fv = g.vface(0, j, i);
if !along_z
&& slope > 0.0
&& window(xv)
&& r_of(xv, yv).abs() < 0.4 * gap
&& mask.a_v(fv) >= 1.0
{
pv.push((r_of(xv, yv).powi(2), field.v[fv] / ty));
}
let c = g.cell(0, j, i);
let (xc, yc) = ((i as f64 + 0.5) * h, (j as f64 + 0.5) * h);
if window(xc) && mask.cell_active(c) && r_of(xc, yc).abs() < 0.5 * gap + h {
let s_along = xc * tx + yc * ty;
let full = r_of(xc, yc).abs() < 0.5 * gap - 1.5 * h;
if full {
pf.push((s_along, field.p[c]));
} else {
pc.push((s_along, field.p[c]));
}
}
}
}
let profile = |points: &[(f64, f64)]| {
if points.is_empty() {
return (f64::NAN, f64::NAN);
}
let (a, b) = fit(points);
let gap_eff = 2.0 * (a / b).sqrt();
(0.5 * (gap - gap_eff) / h, 2.0 * MU * b / F)
};
let (off_u, force_u) = profile(&pu);
let (off_v, _) = profile(&pv);
let (p0, minus_slope) = fit(&pf);
let rms = |points: &[(f64, f64)]| {
(points
.iter()
.map(|(s, p)| (p - (p0 - minus_slope * s)).powi(2))
.sum::<f64>()
/ points.len().max(1) as f64)
.sqrt()
/ (F * gap)
};
Reading {
off_u,
off_v,
force_u,
force_p: 1.0 + minus_slope / F,
p_full: rms(&pf),
p_cut: rms(&pc),
}
}
/// The linear-exactness mode: in-plane Couette flow `u = K dist t` over ONE
/// embedded oblique wall (no force, constant pressure, the sides carry the
/// exact field). A scheme exact on linear fields returns the wall position
/// to round-off; the fitted zero of the profile on full faces is the offset.
fn couette(n: usize, slope: f64, c0: f64, s26: Option<bool>) -> (f64, f64) {
const K: f64 = 1.0;
let h = 1.0 / n as f64;
let (nx, ny, nz) = ((LX * n as f64) as usize, (LY * n as f64) as usize, 2);
let norm = (1.0 + slope * slope).sqrt();
let (tx, ty) = (1.0 / norm, slope / norm);
let dist = move |x: f64, y: f64| (y - slope * x - c0) / norm;
let speed = move |x: f64, y: f64| K * dist(x, y).max(0.0);
let mut solver = Solver::new(
Fluid {
density: 1.0,
viscosity: MU,
reference_velocity: 1.0,
reference_length: 1.0,
},
parameters(s26),
);
solver.set_boundary_velocity(move |x, y, _z, _t| {
let s = speed(x, y);
(s * tx, s * ty, 0.0)
});
solver.set_body(Body::from_sdf(move |x, y, _z, _t| dist(x, y)));
let g = Grid::cubic(nx, ny, nz, h);
let mut field = Field::new(g);
for k in 0..nz {
for j in 0..ny {
for i in 0..=nx {
field.u[g.uface(k, j, i)] = tx * speed(i as f64 * h, (j as f64 + 0.5) * h);
}
}
for j in 0..=ny {
for i in 0..nx {
field.v[g.vface(k, j, i)] = ty * speed((i as f64 + 0.5) * h, j as f64 * h);
}
}
}
solver.initialize(&mut field);
let dt = 0.5 * h * h / (6.0 * MU);
let steps = (2.0 / dt).ceil() as usize;
for _ in 0..steps {
solver.advance(&mut field, dt);
}
let mask = solver.mask().expect("mask");
// u/t_x = K (dist δ): fit on full faces of the central window, two to
// six cells off the wall.
let mut points = Vec::new();
let mut worst: f64 = 0.0;
for j in 0..ny {
for i in 0..nx {
let (x, y) = (i as f64 * h, (j as f64 + 0.5) * h);
let f = g.uface(0, j, i);
let dd = dist(x, y);
if (x - 0.5 * LX).abs() < 0.3 && dd > 2.0 * h && dd < 6.0 * h && mask.a_u(f) >= 1.0 {
points.push((dd, field.u[f] / tx));
worst = worst.max((field.u[f] / tx - K * dd).abs() / (K * h));
}
}
}
let (a, minus_b) = fit(&points);
// y = a (b) x with b the slope: the zero sits at dist = a / b.
let b = -minus_b;
(-a / b / h, worst)
}
#[test]
#[ignore = "S2-6 instrument: linear exactness of the cut wall on an oblique wall (in-plane Couette; a minute on the host)"]
fn oblique_wall_linear_exactness() {
for slope in [0.0, 0.25, 0.5, 1.0] {
for n in [16usize, 32] {
let (off, worst) = couette(n, slope, 0.53, None);
println!(
" couette slope {slope:.2} n {n}: wall offset {off:+.4} h (negative = inside the body); worst full-face error {worst:.4} of K·h"
);
}
}
}
#[test]
#[ignore = "S2-6 instrument: the oblique cut wall's effective position with in-plane flow (minutes on the host)"]
fn oblique_wall_effective_position() {
for (slope, c0) in [
(0.0, 0.53),
(0.0, 0.77),
(0.25, 0.53),
(0.5, 0.53),
(1.0, 0.53),
] {
// `RTX_E3_OBLIQUE_N=64` adds a finer rung to the in-plane mode.
let extra: Option<usize> = std::env::var("RTX_E3_OBLIQUE_N")
.ok()
.and_then(|v| v.parse().ok());
let mut runs = vec![(16usize, false), (32, false), (16, true), (32, true)];
if let Some(n) = extra {
runs = vec![(n, false)];
}
for (n, along_z) in runs {
let r = reading(n, slope, c0, along_z, None);
println!(
" slope {slope:.2} c0 {c0} n {n} {}: wall offset {:+.4} h (u faces) {:+.4} h (v faces), positive = inside the fluid; F_eff/F {:.5} (profile) {:.5} (pressure slope); pressure about its fit: {:.2e} full cells, {:.2e} near-wall cells (of F·G)",
if along_z {
"z-flow (w faces)"
} else {
"in-plane"
},
r.off_u,
r.off_v,
r.force_u,
r.force_p,
r.p_full,
r.p_cut
);
}
}
}
/// The gate (S2-6): with the oblique distance, the transverse centroid
/// correction and the fine floor the cut wall is linear-exact to 0.02 h on
/// an oblique wall (without them it sits 0.050.07 h inside the body at
/// every h), and the z-directed Poiseuille offset halves per rung at
/// slope ½ (without them: 0.087 → 0.083 h).
#[test]
fn oblique_wall_position_is_second_order() {
for slope in [0.5, 1.0] {
let (fixed, _) = couette(32, slope, 0.53, Some(true));
let (before, _) = couette(32, slope, 0.53, Some(false));
println!(
" couette slope {slope}: offset {fixed:+.4} h with the S2-6 closures, {before:+.4} h without"
);
assert!(fixed.abs() < 0.02, "slope {slope}: {fixed}");
assert!(
before.abs() > 2.0 * fixed.abs(),
"the instrument lost its contrast"
);
}
let coarse = reading(16, 0.5, 0.53, true, Some(true)).off_u;
let fine = reading(32, 0.5, 0.53, true, Some(true)).off_u;
println!(" z-flow slope 0.5: offset {coarse:+.4} h at n 16, {fine:+.4} h at n 32");
assert!(coarse.abs() < 0.04, "n 16 offset {coarse}");
assert!(
fine.abs() < 0.65 * coarse.abs(),
"the offset does not halve: {coarse} → {fine}"
);
}