R4-i + R5 design pass: RTX_FSI2O_REGEN_ONCE (one patch regeneration per coupled step; dies at step 0 — the registered test is void), the step CSV's subit/dres_y/dres_norm/fx_nodal/fy_nodal columns; the cut predictor's per-face term probe (enable_term_probe) and the curved instrument's exact side/wall viscous integrals — the static convex wall's flat residual is the viscous closure's first-order relative accuracy on O(1/h) fluxes
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 (macos-latest) (push) Waiting to run
CI / Build CPU-Only (Explicit) (push) Failing after 4s
CI / Clippy Check (push) Failing after 4s
Documentation / Build User Guide (push) Successful in 4s
CI / Format Check (push) Failing after 12s
CI / Build (ubuntu-latest) (push) Failing after 1m26s
Documentation / Build API Documentation (push) Failing after 1m28s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m7s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-22 07:39:54 -05:00
co-authored by Claude Fable 5.1
parent f5735fdeaa
commit 501b45f9d0
5 changed files with 316 additions and 31 deletions
@@ -58,9 +58,17 @@ impl Exact {
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 }
if rigid || !outer_drives() {
OMEGA * r
} else {
0.0
}
} else if r > R2 {
if rigid || outer_drives() { OMEGA * r } else { 0.0 }
if rigid || outer_drives() {
OMEGA * r
} else {
0.0
}
} else {
self.u_theta(r)
};
@@ -145,12 +153,14 @@ fn reading(n: usize, rigid: bool) {
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;
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;
field.v[g.vface(k, j, i)] =
ex.velocity((i as f64 + 0.5) * h, j as f64 * h, rigid).1;
}
}
}
@@ -287,7 +297,8 @@ fn reading(n: usize, rigid: bool) {
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);
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());
@@ -303,7 +314,13 @@ fn reading(n: usize, rigid: bool) {
);
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" },
if rigid {
"rigid"
} else if outer_drives() {
"outer-driven"
} else {
"couette"
},
ex.a,
ex.b,
pts.len(),
@@ -374,20 +391,29 @@ fn probe(n: usize, rigid: bool) {
let g = Grid::cubic(nx, ny, nz, h);
let mut field = Field::new(g);
solver.initialize(&mut field);
let tables = solver.mask().expect("mask").face_shift_tables().expect("shift tables").clone();
let tables = solver
.mask()
.expect("mask")
.face_shift_tables()
.expect("shift tables")
.clone();
for k in 0..nz {
for j in 0..ny {
for i in 0..=nx {
let f = g.uface(k, j, i);
let t = &tables[0][3 * f..3 * f + 3];
field.u[f] = ex.velocity(i as f64 * h + t[0], (j as f64 + 0.5) * h + t[1], rigid).0;
field.u[f] = ex
.velocity(i as f64 * h + t[0], (j as f64 + 0.5) * h + t[1], rigid)
.0;
}
}
for j in 0..=ny {
for i in 0..nx {
let f = g.vface(k, j, i);
let t = &tables[1][3 * f..3 * f + 3];
field.v[f] = ex.velocity((i as f64 + 0.5) * h + t[0], j as f64 * h + t[1], rigid).1;
field.v[f] = ex
.velocity((i as f64 + 0.5) * h + t[0], j as f64 * h + t[1], rigid)
.1;
}
}
}
@@ -399,25 +425,119 @@ fn probe(n: usize, rigid: bool) {
}
{
let (body, mask) = (solver.body().expect("body"), solver.mask().expect("mask"));
mask.impose(body, &mut field.u, &mut field.v, &mut field.w, solver.time());
mask.impose(
body,
&mut field.u,
&mut field.v,
&mut field.w,
solver.time(),
);
}
let dt = 0.5 * h * h / (6.0 * MU);
// R5 design pass: the predictor's terms per face (viscous group =
// diffusion + wall shear + solid exchange, zero on the exact field;
// inertial group = convection + pressure + source, zero on the exact
// steady field): which group carries the cut layer's residual.
solver.enable_term_probe();
solver.advance(&mut field, dt);
let terms = solver.term_probe().expect("term probe");
let mask = solver.mask().expect("mask");
let pp = &field.p_prime;
let a_max = OMEGA * OMEGA * R2;
let is_cut = |c: usize| mask.vol(c) < 1.0 - 1e-9;
// [wall][band]: bands α<¼, ¼–½, ½–¾, ¾–1, full next to cut.
let mut acc: [[Vec<f64>; 5]; 2] = Default::default();
// [wall][band]: bands α<¼, ¼–½, ½–¾, ¾–1, full next to cut, interior;
// per face [total, viscous group, inertial group] / a_max.
let mut acc: [[Vec<[f64; 9]>; 6]; 2] = Default::default();
// R5 design pass: the EXACT viscous line integrals over the momentum
// control volume of a face — the open parts of its four sides (the
// side-diffusion group: diffusion + solid exchange) and the wall arc
// inside it (the wall-shear group) — from the analytic gradient of the
// TaylorCouette field, as accelerations on the same `V_eff` the
// predictor uses. Their sum is the quadrature error (∇²u = 0).
let grad = |c: usize, x: f64, y: f64| -> [f64; 2] {
let (xp, yp) = (x - CENTRE.0, y - CENTRE.1);
let r = (xp * xp + yp * yp).sqrt().max(1e-12);
let (f, fp) = if rigid {
(OMEGA, 0.0)
} else {
(ex.a + ex.b / (r * r), -2.0 * ex.b / (r * r * r))
};
if c == 0 {
[-fp * (xp / r) * yp, -f - fp * (yp / r) * yp]
} else {
[f + fp * (xp / r) * xp, fp * (yp / r) * xp]
}
};
let in_gap = |x: f64, y: f64| {
let r = r_of(x, y);
(R1..=R2).contains(&r)
};
let side = |c: usize, x0: f64, y0: f64, x1: f64, y1: f64, n: [f64; 2]| -> f64 {
let m = 4096;
let len = ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt();
let mut s = 0.0;
for q in 0..m {
let t = (q as f64 + 0.5) / m as f64;
let (x, y) = (x0 + t * (x1 - x0), y0 + t * (y1 - y0));
if in_gap(x, y) {
let g = grad(c, x, y);
s += (g[0] * n[0] + g[1] * n[1]) * len / m as f64;
}
}
MU * s
};
let arc = |c: usize, xa: f64, xb: f64, ya: f64, yb: f64| -> f64 {
let mut s = 0.0;
let m = 1 << 20;
for (r, sign) in [(R1, -1.0), (R2, 1.0)] {
for q in 0..m {
let th = std::f64::consts::TAU * (q as f64 + 0.5) / m as f64;
let (x, y) = (CENTRE.0 + r * th.cos(), CENTRE.1 + r * th.sin());
if x >= xa && x < xb && y >= ya && y < yb {
let g = grad(c, x, y);
let n = [sign * th.cos(), sign * th.sin()];
s += (g[0] * n[0] + g[1] * n[1]) * r * std::f64::consts::TAU / m as f64;
}
}
}
MU * s
};
// (side-diffusion, wall-shear) exact accelerations for the face of
// component `c` whose CV is the box [xa, xb] × [ya, yb], aperture `a`.
let exact_terms = |c: usize, xa: f64, xb: f64, ya: f64, yb: f64, a: f64| -> (f64, f64) {
let d = side(c, xa, ya, xa, yb, [-1.0, 0.0])
+ side(c, xb, ya, xb, yb, [1.0, 0.0])
+ side(c, xa, ya, xb, ya, [0.0, -1.0])
+ side(c, xa, yb, xb, yb, [0.0, 1.0]);
let w = arc(c, xa, xb, ya, yb);
let v_eff = a.max(0.1) * h * h * h;
(d * h / (RHO * v_eff), w * h / (RHO * v_eff))
};
let bin_of = |a: f64, near: bool| -> Option<usize> {
if a < 1.0 {
Some(((a * 4.0).floor() as usize).min(3))
} else if near {
Some(4)
} else {
None
Some(5)
}
};
// [total, viscous group, inertial group, diffusion, wall shear, solid exchange] / a_max.
// …, then the side-diffusion error (diff + exch exact), the wall-shear
// error (shear exact) and the exact sum (the quadrature's own error).
let groups = |t: &[f64; 7], total: f64, exact: (f64, f64)| -> [f64; 9] {
[
total / a_max,
(t[1] + t[2] + t[5]) / a_max,
(t[0] + t[3] + t[4]) / a_max,
t[1] / a_max,
t[2] / a_max,
t[5] / a_max,
(t[1] + t[5] - exact.0) / a_max,
(t[2] - exact.1) / a_max,
(exact.0 + exact.1) / a_max,
]
};
let wall_of = |x: f64, y: f64| usize::from(r_of(x, y) >= 0.5 * (R1 + R2));
for j in 0..ny {
for i in 1..nx {
@@ -426,9 +546,24 @@ fn probe(n: usize, rigid: bool) {
continue;
}
let (cm, cp) = (g.cell(0, j, i - 1), g.cell(0, j, i));
let Some(b) = bin_of(mask.a_u(f), is_cut(cm) || is_cut(cp)) else { continue };
let Some(b) = bin_of(mask.a_u(f), is_cut(cm) || is_cut(cp)) else {
continue;
};
let star = field.u[f] + (dt / RHO) * mask.grad_weight(0, f) * (pp[cp] - pp[cm]) / h;
acc[wall_of(i as f64 * h, (j as f64 + 0.5) * h)][b].push((star - field.u_old[f]) / dt / a_max);
let t = terms[0].get(f).copied().unwrap_or([0.0; 7]);
let exact = exact_terms(
0,
(i as f64 - 0.5) * h,
(i as f64 + 0.5) * h,
j as f64 * h,
(j as f64 + 1.0) * h,
mask.a_u(f),
);
acc[wall_of(i as f64 * h, (j as f64 + 0.5) * h)][b].push(groups(
&t,
(star - field.u_old[f]) / dt,
exact,
));
}
}
for j in 1..ny {
@@ -438,9 +573,24 @@ fn probe(n: usize, rigid: bool) {
continue;
}
let (cm, cp) = (g.cell(0, j - 1, i), g.cell(0, j, i));
let Some(b) = bin_of(mask.a_v(f), is_cut(cm) || is_cut(cp)) else { continue };
let Some(b) = bin_of(mask.a_v(f), is_cut(cm) || is_cut(cp)) else {
continue;
};
let star = field.v[f] + (dt / RHO) * mask.grad_weight(1, f) * (pp[cp] - pp[cm]) / h;
acc[wall_of((i as f64 + 0.5) * h, j as f64 * h)][b].push((star - field.v_old[f]) / dt / a_max);
let t = terms[1].get(f).copied().unwrap_or([0.0; 7]);
let exact = exact_terms(
1,
i as f64 * h,
(i as f64 + 1.0) * h,
(j as f64 - 0.5) * h,
(j as f64 + 0.5) * h,
mask.a_v(f),
);
acc[wall_of((i as f64 + 0.5) * h, j as f64 * h)][b].push(groups(
&t,
(star - field.v_old[f]) / dt,
exact,
));
}
}
let scale = RHO * (OMEGA * R1).powi(2);
@@ -464,23 +614,55 @@ fn probe(n: usize, rigid: bool) {
continue;
}
let e = (pp[c] - lvl) / scale;
let w = if is_cut(c) { wall_of((i as f64 + 0.5) * h, (j as f64 + 0.5) * h) } else { 2 };
let w = if is_cut(c) {
wall_of((i as f64 + 0.5) * h, (j as f64 + 0.5) * h)
} else {
2
};
psq[w] += e * e;
pn[w] += 1;
}
}
let rms = |v: &[f64]| (v.iter().map(|x| x * x).sum::<f64>() / v.len().max(1) as f64).sqrt();
let names = ["α", "¼–½", "½–¾", "¾–1", "full next to cut"];
let mode = if rigid { "rigid" } else if outer_drives() { "outer-driven" } else { "couette" };
let rms = |v: &[[f64; 9]], k: usize| {
(v.iter().map(|x| x[k] * x[k]).sum::<f64>() / v.len().max(1) as f64).sqrt()
};
let names = ["α", "¼–½", "½–¾", "¾–1", "full next to cut", "interior"];
let mode = if rigid {
"rigid"
} else if outer_drives() {
"outer-driven"
} else {
"couette"
};
for (w, wname) in ["inner wall", "outer wall"].iter().enumerate() {
let mut line = format!(" probe {mode} n {n} {wname}:");
for (b, name) in names.iter().enumerate() {
line += &format!(" {name} {} rms {:.3e};", acc[w][b].len(), rms(&acc[w][b]));
line += &format!(
" {name} {} rms {:.3e} (visc {:.3e} inert {:.3e}; diff {:.3e} shear {:.3e} exch {:.3e}; ERR sides {:.3e} wall {:.3e} quad {:.1e});",
acc[w][b].len(),
rms(&acc[w][b], 0),
rms(&acc[w][b], 1),
rms(&acc[w][b], 2),
rms(&acc[w][b], 3),
rms(&acc[w][b], 4),
rms(&acc[w][b], 5),
rms(&acc[w][b], 6),
rms(&acc[w][b], 7),
rms(&acc[w][b], 8)
);
}
line += &format!(" p' at its cut cells {:.3e} ({})", (psq[w] / pn[w].max(1) as f64).sqrt(), pn[w]);
line += &format!(
" p' at its cut cells {:.3e} ({})",
(psq[w] / pn[w].max(1) as f64).sqrt(),
pn[w]
);
println!("{line}");
}
println!(" probe {mode} n {n} interior p' {:.3e} ({})", (psq[2] / pn[2].max(1) as f64).sqrt(), pn[2]);
println!(
" probe {mode} n {n} interior p' {:.3e} ({})",
(psq[2] / pn[2].max(1) as f64).sqrt(),
pn[2]
);
}
#[test]