Files
rustytorch/crates/specialized/rtx-cfd/tests/embedded3_wall_position_curved.rs
T
Omar SobhandClaude Fable 5.1 d25a36d908
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 / Build CPU-Only (Explicit) (push) Failing after 4s
Documentation / Build API Documentation (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 4s
CI / Format Check (push) Failing after 10s
CI / Build (ubuntu-latest) (push) Failing after 1m25s
CI / Clippy Check (push) Failing after 1m47s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m19s
embedded3 S2-7b: the curved-wall instrument (tests/embedded3_wall_position_curved.rs: Taylor–Couette between embedded cylinders, rigid-rotation linear mode, outer-driven mode; walls' effective radii, pressure error by cell class against the exact p(r), wall-flux and ghost diagnostics); the sphere operator probe's sub-bands and force units; two host prototypes, default off, device refuses them: RTX_E3_WALL_EXCHANGE=axisfoot (the axis exchange takes the wall velocity at the axis foot — the S2-7 default read the solid face's own value, exact only for a uniform wall velocity: rigid rotation A 1.012 → 1.003) and RTX_E3_CONV_SIDES=exact (convective mass fluxes from the sides' own apertures; moves nothing)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-20 07:54:01 -05:00

329 lines
12 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-7b instrument: the cut wall on a CURVED wall with an exact velocity
//! and an exact pressure that has a wall-normal gradient. TaylorCouette
//! flow between two embedded concentric cylinders (inner `R1` rotating at
//! `OMEGA`, outer `R2` at rest), z periodic; the domain sides lie in the
//! solid. Exact: `u_θ = A r + B/r`, `p = ρ (A² r²/2 + 2AB ln r B²/(2r²))`.
//! Reads: both walls' effective radii from the profile fit on full faces
//! (the S2-7 form), and the pressure error by cell class against the
//! exact p(r) (the S2-7b form). `RTX_E3_CURVED_MODE=rigid` is the
//! linear-exactness mode: solid-body rotation of both cylinders
//! (`u = Ω r e_θ`, `p = ρ Ω² r²/2`).
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 RHO: f64 = 1.0;
const LX: f64 = 2.0;
const R1: f64 = 0.3;
const R2: f64 = 0.8;
const OMEGA: f64 = 1.0;
const CENTRE: (f64, f64) = (1.013, 1.017);
struct Exact {
a: f64,
b: f64,
}
impl Exact {
fn new(rigid: bool) -> Self {
let d = R2 * R2 - R1 * R1;
if rigid {
Exact { a: OMEGA, b: 0.0 }
} else if outer_drives() {
// The OUTER cylinder rotates at Ω, the inner is at rest: the
// static wall is the convex one (the DFG's kind).
Exact {
a: OMEGA * R2 * R2 / d,
b: -OMEGA * R1 * R1 * R2 * R2 / d,
}
} else {
Exact {
a: -OMEGA * R1 * R1 / d,
b: OMEGA * R1 * R1 * R2 * R2 / d,
}
}
}
fn u_theta(&self, r: f64) -> f64 {
self.a * r + self.b / r
}
fn p(&self, r: f64) -> f64 {
RHO * (0.5 * self.a * self.a * r * r + 2.0 * self.a * self.b * r.ln()
- 0.5 * self.b * self.b / (r * r))
}
/// The velocity at (x, y): the exact profile in the gap, the walls' own
/// motion outside it (the inner body rotates, the outer is at rest).
fn velocity(&self, x: f64, y: f64, rigid: bool) -> (f64, f64) {
let (dx, dy) = (x - CENTRE.0, y - CENTRE.1);
let r = (dx * dx + dy * dy).sqrt().max(1e-12);
let ut = if r < R1 {
if rigid || !outer_drives() { OMEGA * r } else { 0.0 }
} else if r > R2 {
if rigid || outer_drives() { OMEGA * r } else { 0.0 }
} else {
self.u_theta(r)
};
(-ut * dy / r, ut * dx / r)
}
}
/// `RTX_E3_CURVED_MODE=outer`: the outer cylinder drives, the inner is at rest.
fn outer_drives() -> bool {
std::env::var("RTX_E3_CURVED_MODE").is_ok_and(|v| v == "outer")
}
fn parameters() -> Parameters {
Parameters {
corrector_steps: 2,
tolerance: 1e-10,
convection_scheme: ConvectionScheme::TvdVanAlbada,
wall_scheme: WallScheme::CutCell,
boundaries: Boundaries {
z0: Side::Periodic,
z1: Side::Periodic,
..Boundaries::default()
},
..Parameters::default()
}
}
/// Least squares of `u_θ = a r + b / r` through `(r, u_θ)` points.
fn fit_ab(points: &[(f64, f64)]) -> (f64, f64) {
let (mut s11, mut s12, mut s22, mut t1, mut t2) = (0.0, 0.0, 0.0, 0.0, 0.0);
for &(r, u) in points {
let (f1, f2) = (r, 1.0 / r);
s11 += f1 * f1;
s12 += f1 * f2;
s22 += f2 * f2;
t1 += f1 * u;
t2 += f2 * u;
}
let det = s11 * s22 - s12 * s12;
((t1 * s22 - t2 * s12) / det, (s11 * t2 - s12 * t1) / det)
}
fn reading(n: usize, rigid: bool) {
let h = 1.0 / n as f64;
let (nx, ny, nz) = ((LX * n as f64) as usize, (LX * n as f64) as usize, 2);
let ex = Exact::new(rigid);
let mut solver = Solver::new(
Fluid {
density: RHO,
viscosity: MU,
reference_velocity: OMEGA * R1,
reference_length: R2 - R1,
},
parameters(),
);
let exb = Exact::new(rigid);
solver.set_boundary_velocity(move |x, y, _z, _t| {
let (u, v) = exb.velocity(x, y, rigid);
(u, v, 0.0)
});
// The fluid is the gap: φ > 0 there.
solver.set_body(
Body::from_sdf(move |x, y, _z, _t| {
let r = ((x - CENTRE.0).powi(2) + (y - CENTRE.1).powi(2)).sqrt();
(r - R1).min(R2 - r)
})
.with_surface_velocity(move |x, y, _z, _t| {
let (dx, dy) = (x - CENTRE.0, y - CENTRE.1);
let r = (dx * dx + dy * dy).sqrt().max(1e-12);
let inner_side = r < 0.5 * (R1 + R2);
let moving = rigid || (inner_side != outer_drives());
let ut = if moving { OMEGA * r } else { 0.0 };
(-ut * dy / r, ut * dx / r, 0.0)
}),
);
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)] = ex.velocity(i as f64 * h, (j as f64 + 0.5) * h, rigid).0;
}
}
for j in 0..=ny {
for i in 0..nx {
field.v[g.vface(k, j, i)] = ex.velocity((i as f64 + 0.5) * h, j as f64 * h, rigid).1;
}
}
}
solver.initialize(&mut field);
let dt = 0.5 * h * h / (6.0 * MU);
let t_end: f64 = std::env::var("RTX_E3_CURVED_T")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(4.0);
let steps = (t_end / dt).ceil() as usize;
let mut last_res = 0.0;
for _ in 0..steps {
last_res = solver.advance(&mut field, dt).final_residual;
}
let mask = solver.mask().expect("mask");
let r_of = |x: f64, y: f64| ((x - CENTRE.0).powi(2) + (y - CENTRE.1).powi(2)).sqrt();
// The profile on full faces two to N cells off both walls.
let mut pts = Vec::new();
let inner = |r: f64| r > R1 + 2.0 * h && r < R2 - 2.0 * h;
for j in 0..ny {
for i in 0..=nx {
let f = g.uface(0, j, i);
let (x, y) = (i as f64 * h, (j as f64 + 0.5) * h);
let r = r_of(x, y);
if inner(r) && mask.a_u(f) >= 1.0 {
// u = u_θ dy/r
let dy = y - CENTRE.1;
if dy.abs() > 0.3 * r {
pts.push((r, -field.u[f] * r / dy));
}
}
}
}
for j in 0..=ny {
for i in 0..nx {
let f = g.vface(0, j, i);
let (x, y) = ((i as f64 + 0.5) * h, j as f64 * h);
let r = r_of(x, y);
if inner(r) && mask.a_v(f) >= 1.0 {
let dx = x - CENTRE.0;
if dx.abs() > 0.3 * r {
pts.push((r, field.v[f] * r / dx));
}
}
}
}
let (a, b) = fit_ab(&pts);
// The walls: outer where u_θ = 0 (Couette) or the fit's own (rigid: the
// inner/outer are not separable, report A and B); inner where u_θ = Ω r.
let (off_in, off_out) = if rigid {
(f64::NAN, f64::NAN)
} else if outer_drives() {
// inner: u_θ = 0 → r² = b/a; outer: u_θ = Ω r → r² = b/(Ω a)
let r_in = (-b / a).sqrt();
let r_out = (b / (OMEGA - a)).sqrt();
((r_in - R1) / h, (R2 - r_out) / h)
} else {
let r_out = (-b / a).sqrt();
let r_in = (b / (OMEGA - a)).sqrt();
((r_in - R1) / h, (R2 - r_out) / h)
};
// The pressure against the exact p(r): mean-free over full cells in the gap.
let scale = RHO * (OMEGA * R1).powi(2);
let is_cut = |c: usize| mask.vol(c) < 1.0 - 1e-9;
let (mut sum, mut cnt) = (0.0, 0usize);
let cell_r = |c: usize| {
let (_, j, i) = g.kji(c);
r_of((i as f64 + 0.5) * h, (j as f64 + 0.5) * h)
};
for j in 0..ny {
for i in 0..nx {
let c = g.cell(0, j, i);
if mask.cell_active(c) && !is_cut(c) && mask.master(c).is_none() {
sum += field.p[c] - ex.p(cell_r(c));
cnt += 1;
}
}
}
let level = sum / cnt.max(1) as f64;
let (mut sq_full, mut sq_cut, mut n_cut, mut sum_cut) = (0.0, 0.0, 0usize, 0.0);
let (mut sq_small, mut n_small, mut sq_large, mut n_large) = (0.0, 0usize, 0.0, 0usize);
let (mut sq_in, mut n_in, mut sq_out, mut n_out) = (0.0, 0usize, 0.0, 0usize);
for j in 0..ny {
for i in 0..nx {
let c = g.cell(0, j, i);
if !mask.cell_active(c) || mask.master(c).is_some() {
continue;
}
let e = (field.p[c] - level - ex.p(cell_r(c))) / scale;
if is_cut(c) {
sq_cut += e * e;
sum_cut += e;
n_cut += 1;
if mask.vol(c) < 0.5 {
sq_small += e * e;
n_small += 1;
} else {
sq_large += e * e;
n_large += 1;
}
if cell_r(c) < 0.5 * (R1 + R2) {
sq_in += e * e;
n_in += 1;
} else {
sq_out += e * e;
n_out += 1;
}
} else {
sq_full += e * e;
}
}
}
let rms = |sq: f64, n: usize| (sq / n.max(1) as f64).sqrt();
// S2-7b diagnostics: the wall's mass flux per cut cell (the body's
// velocity is tangential: any flux is the facet normal's), in units of
// Ω R1 h², and the ghost faces' error against the exact field (u faces
// of kind Ghost), in units of Ω R1.
let body = solver.body().expect("body");
let (wf, _) = mask.wall_flux_table(body, solver.time());
let (mut wsum, mut wmax, mut wn) = (0.0f64, 0.0f64, 0usize);
for j in 0..ny {
for i in 0..nx {
let c = g.cell(0, j, i);
if mask.cell_active(c) && is_cut(c) {
let q = wf[c].abs() / (OMEGA * R1 * h * h);
wsum += q;
wmax = wmax.max(q);
wn += 1;
}
}
}
let (mut gsq, mut gn, mut gmax) = (0.0f64, 0usize, 0.0f64);
for j in 0..ny {
for i in 0..=nx {
let f = g.uface(0, j, i);
if mask.u_kind(f) == rtx_cfd::solvers::incompressible::embedded3::FaceKind::Ghost {
let e = (field.u[f] - ex.velocity(i as f64 * h, (j as f64 + 0.5) * h, rigid).0) / (OMEGA * R1);
gsq += e * e;
gn += 1;
gmax = gmax.max(e.abs());
}
}
}
println!(
" wall flux per cut cell (of ΩR1 h²): mean {:.3e} max {:.3e} ({wn}); ghost u faces vs exact (of ΩR1): rms {:.3e} max {:.3e} ({gn})",
wsum / wn.max(1) as f64,
wmax,
(gsq / gn.max(1) as f64).sqrt(),
gmax
);
println!(
" {} n {n}: walls' offsets {off_in:+.4} h (inner) {off_out:+.4} h (outer), positive = inside the fluid; fit A {a:.5} B {b:.5} (exact {:.5} {:.5}, {} points); pressure error of ρ(ΩR1)²: full cells {:.3e} ({cnt}), cut cells {:.3e} mean {:+.3e} ({n_cut}), fraction < 0.5 {:.3e} ({n_small}), ≥ 0.5 {:.3e} ({n_large}), inner wall {:.3e} ({n_in}), outer wall {:.3e} ({n_out}); merged {}; residual {last_res:.1e}",
if rigid { "rigid" } else if outer_drives() { "outer-driven" } else { "couette" },
ex.a,
ex.b,
pts.len(),
rms(sq_full, cnt),
rms(sq_cut, n_cut),
sum_cut / n_cut.max(1) as f64,
rms(sq_small, n_small),
rms(sq_large, n_large),
rms(sq_in, n_in),
rms(sq_out, n_out),
mask.merged_cells()
);
}
#[test]
#[ignore = "S2-7b instrument: TaylorCouette between embedded cylinders (minutes per rung on the host)"]
fn curved_wall_effective_position_and_pressure() {
let ns: Vec<usize> = std::env::var("RTX_E3_CURVED_NS")
.ok()
.map(|v| v.split(',').filter_map(|t| t.trim().parse().ok()).collect())
.unwrap_or_else(|| vec![16, 32]);
let rigid = std::env::var("RTX_E3_CURVED_MODE").is_ok_and(|v| v == "rigid");
for n in ns {
reading(n, rigid);
}
}