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)
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

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-20 07:54:01 -05:00
co-authored by Claude Fable 5.1
parent c430510802
commit d25a36d908
6 changed files with 417 additions and 6 deletions
@@ -221,6 +221,8 @@ impl Mask {
wall_exchange_axis: false, wall_exchange_axis: false,
cv_sides_exact: false, cv_sides_exact: false,
wall_order2_centroid: false, wall_order2_centroid: false,
wall_exchange_foot: false,
conv_sides_exact: false,
grad_weights: None, grad_weights: None,
diffusion_centroid: false, diffusion_centroid: false,
face_shifts: None, face_shifts: None,
@@ -435,6 +437,29 @@ impl Mask {
/// crosses a side (the in-plane momentum residual on oblique walls). /// crosses a side (the in-plane momentum residual on oblique walls).
/// `None` at a domain side (the default stays). /// `None` at a domain side (the default stays).
fn exact_cv_sides(&self, cut: &CutGeometry, c: usize, p: [i64; 3]) -> Option<[[f64; 2]; 3]> { fn exact_cv_sides(&self, cut: &CutGeometry, c: usize, p: [i64; 3]) -> Option<[[f64; 2]; 3]> {
let parts = self.exact_cv_side_parts(cut, c, p)?;
let mut ap = [[1.0; 2]; 3];
for d in 0..3 {
for side in 0..2 {
ap[d][side] = if d == c {
parts[d][side].0
} else {
0.5 * (parts[d][side].0 + parts[d][side].1)
};
}
}
Some(ap)
}
/// The parts of [`Self::exact_cv_sides`]: across `c`, the (far half of
/// `cell_minus`'s face, near half of `cell_plus`'s face) apertures per
/// side; along `c`, the centre plane's aperture (twice).
pub(super) fn exact_cv_side_parts(
&self,
cut: &CutGeometry,
c: usize,
p: [i64; 3],
) -> Option<[[(f64, f64); 2]; 3]> {
let lat = self.lattice(); let lat = self.lattice();
let g = self.grid; let g = self.grid;
let mut pm = p; let mut pm = p;
@@ -456,7 +481,7 @@ impl Mask {
corner(cell, o) corner(cell, o)
} }
}; };
let mut ap = [[1.0; 2]; 3]; let mut ap = [[(1.0, 1.0); 2]; 3];
for d in 0..3 { for d in 0..3 {
if d == c { if d == c {
// The two cells' centre planes across `c`: corners at the // The two cells' centre planes across `c`: corners at the
@@ -469,7 +494,8 @@ impl Mask {
o[d2] = o2; o[d2] = o2;
value(*cell, o, true) value(*cell, o, true)
}; };
ap[d][side] = super::cut::quad_fraction(mid(0, 0), mid(1, 0), mid(0, 1), mid(1, 1)); let a = super::cut::quad_fraction(mid(0, 0), mid(1, 0), mid(0, 1), mid(1, 1));
ap[d][side] = (a, a);
} }
} else { } else {
let e = 3 - c - d; let e = 3 - c - d;
@@ -496,7 +522,7 @@ impl Mask {
super::cut::quad_fraction(at(0, 0), at(1, 0), at(0, 1), at(1, 1)) super::cut::quad_fraction(at(0, 0), at(1, 0), at(0, 1), at(1, 1))
} }
}; };
ap[d][side] = 0.5 * (half(cells[0], true) + half(cells[1], false)); ap[d][side] = (half(cells[0], true), half(cells[1], false));
} }
} }
} }
@@ -109,6 +109,12 @@ impl Solver {
let val = |cc: usize, q: [i64; 3]| lat.face(cc, q).map(|f| old[cc][f]); let val = |cc: usize, q: [i64; 3]| lat.face(cc, q).map(|f| old[cc][f]);
let ap = |cc: usize, q: [i64; 3]| mask.aperture(cc, q); let ap = |cc: usize, q: [i64; 3]| mask.aperture(cc, q);
let cv = mask.cv_geometry(c, p); let cv = mask.cv_geometry(c, p);
// S2-7b: the convective sides' own apertures (host prototype).
let conv_parts = if mask.conv_sides_exact {
mask.cut().and_then(|cut| mask.exact_cv_side_parts(cut, c, p))
} else {
None
};
let x = lat.face_position(c, p); let x = lat.face_position(c, p);
let u0 = val(c, p).expect("the face"); let u0 = val(c, p).expect("the face");
let ec = e(c); let ec = e(c);
@@ -133,7 +139,22 @@ impl Solver {
// The control volume's mass fluxes through its plus / minus faces // The control volume's mass fluxes through its plus / minus faces
// along d: the averages of the two adjacent cells' face fluxes // along d: the averages of the two adjacent cells' face fluxes
// (own direction: the face's and its neighbours' fluxes). // (own direction: the face's and its neighbours' fluxes).
let (m_plus, m_minus) = if d == c { let (m_plus, m_minus) = if let Some(parts) = conv_parts {
if d == c {
// The centre planes' apertures times the mean velocity.
(
parts[d][1].0 * 0.5 * (u0 + up1.unwrap_or(u0)) * a_d,
parts[d][0].0 * 0.5 * (dn1.unwrap_or(u0) + u0) * a_d,
)
} else {
// The two half faces' own apertures times their faces' values.
let v = |q: [i64; 3]| val(d, q).unwrap_or(0.0);
(
0.5 * (parts[d][1].0 * v(add(cell_minus, ed, 1)) + parts[d][1].1 * v(add(cell_plus, ed, 1))) * a_d,
0.5 * (parts[d][0].0 * v(cell_minus) + parts[d][0].1 * v(cell_plus)) * a_d,
)
}
} else if d == c {
let f_up = ap(c, add(p, ec, 1)).unwrap_or(cv.alpha) * up1.unwrap_or(u0); let f_up = ap(c, add(p, ec, 1)).unwrap_or(cv.alpha) * up1.unwrap_or(u0);
let f_dn = ap(c, add(p, ec, -1)).unwrap_or(cv.alpha) * dn1.unwrap_or(u0); let f_dn = ap(c, add(p, ec, -1)).unwrap_or(cv.alpha) * dn1.unwrap_or(u0);
let f0 = cv.alpha * u0; let f0 = cv.alpha * u0;
@@ -278,11 +299,22 @@ impl Solver {
diff -= sign * mu * gap * a_d * transverse(q, uq, sign) / spacing(q, sign); diff -= sign * mu * gap * a_d * transverse(q, uq, sign) / spacing(q, sign);
} }
} }
// S2-7b: the wall velocity at the axis foot (distance δ along d)
// instead of the solid face's own value at distance h.
let foot_value = |un: f64, sign: f64| -> f64 {
if !mask.wall_exchange_foot {
return un;
}
let delta = mask.exchange_delta(&cv, d);
let mut xf = x;
xf[d] += sign * delta;
mask.surface_velocity_at(body, xf, c, t_old)
};
diff += match up1 { diff += match up1 {
Some(un) if solid(add(p, ed, 1)) => { Some(un) if solid(add(p, ed, 1)) => {
let k = mu * g_plus * a_d / mask.exchange_delta(&cv, d); let k = mu * g_plus * a_d / mask.exchange_delta(&cv, d);
wall_implicit += k; wall_implicit += k;
wall_rhs += k * un; wall_rhs += k * foot_value(un, 1.0);
0.0 0.0
} }
Some(un) if centroid => { Some(un) if centroid => {
@@ -310,7 +342,7 @@ impl Solver {
Some(ud) if solid(add(p, ed, -1)) => { Some(ud) if solid(add(p, ed, -1)) => {
let k = mu * g_minus * a_d / mask.exchange_delta(&cv, d); let k = mu * g_minus * a_d / mask.exchange_delta(&cv, d);
wall_implicit += k; wall_implicit += k;
wall_rhs += k * ud; wall_rhs += k * foot_value(ud, -1.0);
0.0 0.0
} }
Some(ud) if centroid => { Some(ud) if centroid => {
@@ -177,6 +177,10 @@ impl DeviceStep {
!solver.params.wall_order2_centroid, !solver.params.wall_order2_centroid,
"the order-2 second point at the neighbour's centroid (S2-7) is a host prototype: the device kernels do not carry it" "the order-2 second point at the neighbour's centroid (S2-7) is a host prototype: the device kernels do not carry it"
); );
assert!(
!solver.params.wall_exchange_foot && !solver.params.conv_sides_exact,
"the axis-foot exchange / exact convective sides (S2-7b) are host prototypes: the device kernels do not carry them"
);
assert!( assert!(
!solver.params.cv_sides_exact, !solver.params.cv_sides_exact,
"the exact control-volume sides (S2-7) are a host prototype: the device kernels do not carry it" "the exact control-volume sides (S2-7) are a host prototype: the device kernels do not carry it"
@@ -163,6 +163,17 @@ pub struct Parameters {
/// two differ by ½h(1 α)n_t, O(1) of the step on cut faces). The /// two differ by ½h(1 α)n_t, O(1) of the step on cut faces). The
/// device refuses it. `RTX_E3_WALL_ORDER2=centroid`. /// device refuses it. `RTX_E3_WALL_ORDER2=centroid`.
pub wall_order2_centroid: bool, pub wall_order2_centroid: bool,
/// HOST PROTOTYPE (S2-7b): the axis exchange with a solid neighbour face
/// takes the wall velocity at the AXIS FOOT (distance δ) instead of the
/// solid face's own imposed value (at distance h): consistent for a
/// linear field on a moving or rotating wall (identical on a wall at
/// rest). `RTX_E3_WALL_EXCHANGE=axisfoot`; the device refuses it.
pub wall_exchange_foot: bool,
/// HOST PROTOTYPE (S2-7b): the convective mass fluxes through a cut
/// face's control-volume sides from the sides' own apertures (the half
/// faces across, the cell-centre planes along) instead of the averages
/// of whole-face fluxes. `RTX_E3_CONV_SIDES=exact`; the device refuses it.
pub conv_sides_exact: bool,
/// S2-5: the cross-direction diffusion between two faces over the /// S2-5: the cross-direction diffusion between two faces over the
/// distance between their OPEN-PART CENTROIDS (a cut face's velocity /// distance between their OPEN-PART CENTROIDS (a cut face's velocity
/// is its open part's mean, ½h(1 α) off the face centre along the /// is its open part's mean, ½h(1 α) off the face centre along the
@@ -205,6 +216,8 @@ impl Default for Parameters {
.is_ok_and(|v| v == "off"), .is_ok_and(|v| v == "off"),
// ON by default since S2-7 (`=h` reproduces the records before it). // ON by default since S2-7 (`=h` reproduces the records before it).
wall_exchange_axis: std::env::var("RTX_E3_WALL_EXCHANGE").map_or(true, |v| v != "h"), wall_exchange_axis: std::env::var("RTX_E3_WALL_EXCHANGE").map_or(true, |v| v != "h"),
wall_exchange_foot: std::env::var("RTX_E3_WALL_EXCHANGE").is_ok_and(|v| v == "axisfoot"),
conv_sides_exact: std::env::var("RTX_E3_CONV_SIDES").is_ok_and(|v| v == "exact"),
pressure_centroid: std::env::var("RTX_E3_PRESSURE_CENTROID").is_ok_and(|v| v == "1"), pressure_centroid: std::env::var("RTX_E3_PRESSURE_CENTROID").is_ok_and(|v| v == "1"),
momentum_volume_tiled: std::env::var("RTX_E3_MOMENTUM_VOLUME") momentum_volume_tiled: std::env::var("RTX_E3_MOMENTUM_VOLUME")
.is_ok_and(|v| v == "tiled"), .is_ok_and(|v| v == "tiled"),
@@ -351,6 +364,8 @@ impl Solver {
m.exchange_convection_off = self.params.exchange_convection_off; m.exchange_convection_off = self.params.exchange_convection_off;
m.cv_sides_exact = self.params.cv_sides_exact; m.cv_sides_exact = self.params.cv_sides_exact;
m.wall_order2_centroid = self.params.wall_order2_centroid; m.wall_order2_centroid = self.params.wall_order2_centroid;
m.wall_exchange_foot = self.params.wall_exchange_foot;
m.conv_sides_exact = self.params.conv_sides_exact;
m.diffusion_centroid = self.params.diffusion_centroid; m.diffusion_centroid = self.params.diffusion_centroid;
if self.params.diffusion_centroid { if self.params.diffusion_centroid {
m.compute_face_shifts(); m.compute_face_shifts();
@@ -114,6 +114,10 @@ pub struct Mask {
/// S2-7: the quadratic wall gradient's second point at the neighbour's /// S2-7: the quadratic wall gradient's second point at the neighbour's
/// own centroid distance (host prototype). /// own centroid distance (host prototype).
pub(super) wall_order2_centroid: bool, pub(super) wall_order2_centroid: bool,
/// S2-7b host prototypes: the axis foot's wall velocity in the solid
/// exchange; the convective sides from the sides' own apertures.
pub(super) wall_exchange_foot: bool,
pub(super) conv_sides_exact: bool,
/// The centroid prototype's pressure-gradient weights per u / v / w face. /// The centroid prototype's pressure-gradient weights per u / v / w face.
/// The centroid-distance cross diffusion (S2-5). /// The centroid-distance cross diffusion (S2-5).
pub(super) diffusion_centroid: bool, pub(super) diffusion_centroid: bool,
@@ -542,6 +546,8 @@ impl Mask {
wall_exchange_axis: false, wall_exchange_axis: false,
cv_sides_exact: false, cv_sides_exact: false,
wall_order2_centroid: false, wall_order2_centroid: false,
wall_exchange_foot: false,
conv_sides_exact: false,
grad_weights: None, grad_weights: None,
diffusion_centroid: false, diffusion_centroid: false,
face_shifts: None, face_shifts: None,
@@ -0,0 +1,328 @@
//! 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);
}
}