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
@@ -111,7 +111,8 @@ impl Solver {
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))
mask.cut()
.and_then(|cut| mask.exact_cv_side_parts(cut, c, p))
} else {
None
};
@@ -159,7 +160,9 @@ impl Solver {
// 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][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,
)
}
@@ -260,7 +263,8 @@ impl Solver {
return 0.0;
}
let cvq = mask.cv_geometry(c, q);
let ubq = mask.surface_velocity_at(body, foot_of(q, lat.face_position(c, q)), c, t_old);
let ubq =
mask.surface_velocity_at(body, foot_of(q, lat.face_position(c, q)), c, t_old);
// Explicit, with a coefficient ∝ 1/d_f: take the gradient from
// the faces at least TRANSVERSE_DISTANCE_FLOOR h off the wall —
// a FULL neighbour too, over its own distance along the cut
@@ -426,6 +430,23 @@ impl Solver {
+ wall_rhs)
/ (inertia + shear + wall_implicit);
let v_alpha = fraction * h[c] * area[c];
if let Some(tp) = self.term_probe.borrow_mut().as_mut() {
let m = rho * v_eff;
let terms = [
-conv / m,
diff / m,
(shear * (ub - u_star) - shear_explicit) / m,
pressure / m,
source / m,
(wall_rhs - wall_implicit * u_star) / m,
(u_star - u0) / dt,
];
let v = &mut tp[c];
if v.len() <= idx_f {
v.resize(idx_f + 1, [0.0; 7]);
}
v[idx_f] = terms;
}
(u_star, rho * (v_eff - v_alpha) * (u_star - u0) / dt)
}
}
@@ -230,7 +230,8 @@ impl Default for Parameters {
.is_ok_and(|v| v == "off"),
// 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_foot: std::env::var("RTX_E3_WALL_EXCHANGE").is_ok_and(|v| v == "axisfoot"),
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"),
wall_flux_true_normal: std::env::var("RTX_E3_WALL_FLUX").is_ok_and(|v| v == "true"),
wall_foot_centroid: std::env::var("RTX_E3_WALL_FOOT").is_ok_and(|v| v == "centroid"),
@@ -238,7 +239,8 @@ impl Default for Parameters {
momentum_volume_tiled: std::env::var("RTX_E3_MOMENTUM_VOLUME")
.is_ok_and(|v| v == "tiled"),
cv_sides_exact: std::env::var("RTX_E3_CV_SIDES").is_ok_and(|v| v == "exact"),
wall_order2_centroid: std::env::var("RTX_E3_WALL_ORDER2").is_ok_and(|v| v == "centroid"),
wall_order2_centroid: std::env::var("RTX_E3_WALL_ORDER2")
.is_ok_and(|v| v == "centroid"),
// ON by default since S2-5 (`=0` reproduces the records before it).
diffusion_centroid: std::env::var("RTX_E3_DIFFUSION_CENTROID")
.map_or(true, |v| v != "0"),
@@ -269,6 +271,11 @@ pub struct Solver {
/// cut predictor only): the box route counts it, the operator route
/// does not.
pub(super) floor_source: std::cell::Cell<[f64; 3]>,
/// R5 design-pass instrument (host cut predictor only): per unknown
/// face of each component, the last step's momentum terms as
/// accelerations `[conv, diff, wall shear, pressure, source, solid
/// exchange, total] / (ρ V_eff)`; `None` (the default) records nothing.
pub(super) term_probe: std::cell::RefCell<Option<[Vec<[f64; 7]>; 3]>>,
/// The closure lag of the pressure corrections over the last step
/// (`Mask::closure_lag` summed over the correctors; host path).
pub(super) pressure_lag: std::cell::Cell<[f64; 3]>,
@@ -308,6 +315,7 @@ impl Solver {
params,
momentum_source: None,
floor_source: std::cell::Cell::new([0.0; 3]),
term_probe: std::cell::RefCell::new(None),
pressure_lag: std::cell::Cell::new([0.0; 3]),
vol_old: Vec::new(),
apertures_old: None,
@@ -402,6 +410,16 @@ impl Solver {
.expect("embedded mask")
}
/// R5 instrument: record the cut predictor's momentum terms per face
/// from the next step on (host path only; a no-op on the device path).
pub fn enable_term_probe(&self) {
*self.term_probe.borrow_mut() = Some([Vec::new(), Vec::new(), Vec::new()]);
}
/// The recorded terms per component face index (see `term_probe`).
#[must_use]
pub fn term_probe(&self) -> Option<[Vec<[f64; 7]>; 3]> {
self.term_probe.borrow().clone()
}
/// The small-cell floor's momentum source over the last step (a force
/// on the fluid; host cut predictor only, zero otherwise).
#[must_use]
@@ -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]
@@ -240,6 +240,15 @@ pub struct OversetFluid {
/// The last fluid step's mass defects (patch acceptor ring, background
/// fringe) and the patch's max divergence.
pub last_defects: Cell<(f64, f64, f64)>,
/// R4-i `RTX_FSI2O_REGEN_ONCE`: one patch regeneration per coupled
/// step — the first pass's meshes (one per substep) are reused by every
/// later subiteration of the same step (the wall polygon and velocity
/// still follow the candidate). Active between `begin_coupled_step`
/// calls only; `restore` rewinds the cursor to the step's first substep.
pub regen_once: bool,
pub regen_hold: bool,
pub regen_cache: Vec<rtx_cfd::mesh::PatchMesh>,
pub regen_cursor: usize,
}
impl OversetFluid {
@@ -503,6 +512,10 @@ impl OversetFluid {
correctors_total: Cell::new(0),
last_d: zero_d,
last_defects: Cell::new((0.0, 0.0, 0.0)),
regen_once: std::env::var("RTX_FSI2O_REGEN_ONCE").is_ok(),
regen_hold: false,
regen_cache: Vec::new(),
regen_cursor: 0,
warm_base: None,
warm_sweeps,
})
@@ -918,6 +931,12 @@ impl OversetFluid {
self.last_d = d.to_vec();
return Ok(());
}
if self.regen_once && self.regen_hold && self.regen_cursor < self.regen_cache.len() {
let mesh = self.regen_cache[self.regen_cursor].clone();
self.regen_cursor += 1;
self.last_d = d.to_vec();
return self.solver.set_patch_mesh(mesh);
}
let mesh = match self.patch_for(d) {
Ok(m) => m,
Err(e) => {
@@ -925,10 +944,22 @@ impl OversetFluid {
return Err(e);
}
};
if self.regen_once && self.regen_hold {
self.regen_cache.push(mesh.clone());
self.regen_cursor += 1;
}
self.last_d = d.to_vec();
self.solver.set_patch_mesh(mesh)
}
/// R4-i: a new coupled step begins — the first pass regenerates, the
/// later passes reuse (`RTX_FSI2O_REGEN_ONCE`; a no-op otherwise).
pub fn begin_coupled_step(&mut self) {
self.regen_hold = self.regen_once;
self.regen_cache.clear();
self.regen_cursor = 0;
}
/// The last geometry, for the offline reproduction
/// (`patch_cylinder_flag_deformed.rs`, `RTX_CF_EDGES_FILE`): one edge
/// per block, `x y` per line, when `RTX_FSI2O_DUMP_DIR` is set.
@@ -1198,6 +1229,7 @@ impl OversetFluid {
let t0 = std::time::Instant::now();
self.solver.restore(&saved.0);
self.field = saved.1.clone();
self.regen_cursor = 0;
self.t_restore
.set(self.t_restore.get() + t0.elapsed().as_secs_f64());
}
@@ -137,6 +137,11 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
std::env::var("RTX_FSI2O_FICT_MASS").unwrap_or_else(|_| "0".into()),
std::env::var("RTX_FSI2O_ROBIN_ALPHA").unwrap_or_else(|_| "0".into()),
);
if std::env::var("RTX_FSI2O_REGEN_ONCE").is_ok() {
println!(
" patch regeneration: ONCE per coupled step (R4-i, RTX_FSI2O_REGEN_ONCE): the first pass's mesh is reused by the later subiterations"
);
}
// Phase 1: rigid flag to t_release (`RTX_FSI2O_LOAD=dir` replaces the
// march with the saved state; `RTX_FSI2O_SAVE=dir` saves it).
@@ -399,7 +404,11 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
// cell imbalance.
let mut step_csv = std::env::var("RTX_FSI2O_STEP_CSV").ok().map(|p| {
let mut f = std::fs::File::create(p).expect("step csv");
writeln!(f, "t,ux,uy,drag,lift,defect_patch,defect_bg,patch_div").unwrap();
// R4-i columns: the step's subiterations, the last pass's signed
// y-residual Σ_k (d_new d_candidate)_y and its norm, and the
// nodal load the structure was given (Σ F_x, Σ F_y) — against the
// wall-integral force of the same state (drag, lift).
writeln!(f, "t,ux,uy,drag,lift,defect_patch,defect_bg,patch_div,subit,dres_y,dres_norm,fx_nodal,fy_nodal").unwrap();
f
});
let (t_fluid, t_structure) = (std::cell::Cell::new(0.0_f64), std::cell::Cell::new(0.0_f64));
@@ -433,6 +442,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
if robin_alpha > 0.0 {
robin_datum.replace(fluid.borrow().inner_tractions());
}
fluid.borrow_mut().begin_coupled_step();
let saved = fluid.borrow().snapshot();
type PassResult = (DynamicState, Vec<(NodeId, Vector3<f64>)>, f64, usize);
let latest: RefCell<Option<PassResult>> = RefCell::new(None);
@@ -498,6 +508,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
let tol_step = cfg.tol_floor.max(cfg.rtol * increment);
let retry_at = (5.0 * tol_step).max(0.1 * increment);
let acceptable = (cfg.stall_accept * tol_step).max(0.1 * increment);
let mut step_iterations = 0usize;
let mut outcome = if let Some(iqn) = iqn.as_mut() {
iqn.set_tolerance(tol_step).unwrap();
iqn.solve(&d_predicted, pass)
@@ -533,6 +544,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
Ok(c) => {
total_subiterations += c.iterations;
max_subiterations = max_subiterations.max(c.iterations);
step_iterations = c.iterations;
}
Err(
rtx_fsi::FsiError::CouplingNotConverged {
@@ -549,6 +561,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
worst_stall = worst_stall.max(residual);
total_subiterations += iterations;
max_subiterations = max_subiterations.max(iterations);
step_iterations = iterations;
}
Err(e) => {
println!(
@@ -560,6 +573,25 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
}
}
let (new_state, nodal, conservation, faces) = latest.borrow_mut().take().expect("pass ran");
// R4-i: the last pass's residual (the structure's answer against the
// candidate the fluid's committed state sits at) and the nodal load.
let (dres_y, dres_norm, fx_nodal, fy_nodal) = {
let d_new = extract(&new_state);
let d_cand = &fluid.borrow().last_d;
let mut sy = 0.0_f64;
let mut n2 = 0.0_f64;
for k in 0..d_new.len() / 2 {
sy += d_new[2 * k + 1] - d_cand[2 * k + 1];
n2 += (d_new[2 * k] - d_cand[2 * k]).powi(2)
+ (d_new[2 * k + 1] - d_cand[2 * k + 1]).powi(2);
}
let (mut fx, mut fy) = (0.0_f64, 0.0_f64);
for (_, f) in &nodal {
fx += f.x;
fy += f.y;
}
(sy, n2.sqrt(), fx, fy)
};
flag_state = new_state;
committed_nodal = nodal;
prev_area.set(fluid.borrow().shared.read().unwrap().area());
@@ -590,7 +622,7 @@ pub fn run_march_overset(case: BenchmarkCase, config: &OversetMarchConfig) -> Ov
let (dp, db, pd) = fluid.borrow().last_defects.get();
writeln!(
f,
"{t_now:.6},{ux:.6e},{uy:.6e},{drag_now:.6e},{lift_now:.6e},{dp:.3e},{db:.3e},{pd:.3e}"
"{t_now:.6},{ux:.6e},{uy:.6e},{drag_now:.6e},{lift_now:.6e},{dp:.3e},{db:.3e},{pd:.3e},{step_iterations},{dres_y:.3e},{dres_norm:.3e},{fx_nodal:.6e},{fy_nodal:.6e}"
)
.unwrap();
}