P5-1 instruments and discriminators: RTX_FSI2O_NO_WALL_VEL, _FREEZE_PATCH, _VOLUME_PRESERVE (the imposed wall velocity's net flux removed uniformly along the wall), the pass trace carries the fluid's power on the flag, the wall-pressure roughness, the patch's pressure level, the wall's net volume flux against the FEA polygon's area rate, and the mass defects — the s = 1 runaway is the flag's thickness breathing (ν = 0.4 Quad8) seen by the body-fitted wall as a volume flux equal to the polygon's area rate, answered by the projection's pressure level at ρ/dt
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0116sg1Qz1gMv9hdcKP1XUam
This commit is contained in:
Omar Sobh
2026-09-07 08:37:34 -07:00
co-authored by Claude Fable 5.1
parent 2ed36c3a8c
commit 565215baf9
2 changed files with 158 additions and 3 deletions
@@ -40,6 +40,41 @@ const PATCH_STRETCH: f64 = 4.0;
pub struct WallMotion {
pub polygon: Vec<[f64; 2]>,
pub velocity: Vec<[f64; 2]>,
/// The net normal velocity removed from every segment (the imposed
/// velocity's volume flux over the wall length) when the wall is
/// made volume-preserving; zero otherwise.
pub q: f64,
}
impl WallMotion {
/// The imposed velocity's net volume flux INTO the fluid over the
/// polygon (counter-clockwise around the body: outward normal
/// `(dy, dx)/L`), and the polygon's length.
pub fn net_flux(&self) -> (f64, f64) {
let (mut flux, mut len) = (0.0, 0.0);
for i in 0..self.polygon.len().saturating_sub(1) {
let (a, b) = (self.polygon[i], self.polygon[i + 1]);
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let (va, vb) = (self.velocity[i], self.velocity[i + 1]);
let (vx, vy) = (0.5 * (va[0] + vb[0]), 0.5 * (va[1] + vb[1]));
flux += vx * dy - vy * dx;
len += (dx * dx + dy * dy).sqrt();
}
(flux, len)
}
/// The polygon's enclosed area (shoelace; the walk is closed across
/// the cylinder by its anchors, so this is the flag's area up to a
/// constant).
pub fn area(&self) -> f64 {
let n = self.polygon.len();
let mut a = 0.0;
for i in 0..n {
let (p, q) = (self.polygon[i], self.polygon[(i + 1) % n]);
a += p[0] * q[1] - q[0] * p[1];
}
0.5 * a
}
}
impl WallMotion {
@@ -64,7 +99,11 @@ impl WallMotion {
if d < best_d {
best_d = d;
let (va, vb) = (self.velocity[i], self.velocity[i + 1]);
best = (va[0] + t * (vb[0] - va[0]), va[1] + t * (vb[1] - va[1]));
let l = l2.sqrt().max(1e-300);
best = (
va[0] + t * (vb[0] - va[0]) - self.q * dy / l,
va[1] + t * (vb[1] - va[1]) + self.q * dx / l,
);
}
}
best
@@ -96,6 +135,9 @@ pub struct OversetFluid {
pub correctors_total: Cell<usize>,
/// The interface of the last `set_geometry`.
pub last_d: Vec<f64>,
/// 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)>,
}
impl OversetFluid {
@@ -189,6 +231,7 @@ impl OversetFluid {
.map(|&(x, y)| [x, y])
.collect(),
velocity: vec![[0.0, 0.0]; interface.walk.len()],
q: 0.0,
}));
let wall = shared.clone();
let mut patch = CurvilinearPisoSolver::new(
@@ -201,8 +244,15 @@ impl OversetFluid {
},
patch_mesh,
)?;
// Discriminators (P5-1 matrix): `RTX_FSI2O_NO_WALL_VEL` keeps the
// wall at rest in the fluid while the geometry moves.
let no_wall_vel = std::env::var("RTX_FSI2O_NO_WALL_VEL").is_ok();
patch.set_side_velocity(PatchSide::Inner, move |x, y, _| {
wall.read().unwrap().velocity_at(x, y)
if no_wall_vel {
(0.0, 0.0)
} else {
wall.read().unwrap().velocity_at(x, y)
}
});
let mut patch_field = PatchField::new(patch.mesh());
patch.initialize(&mut patch_field, |_, _| (0.0, 0.0));
@@ -239,6 +289,7 @@ impl OversetFluid {
rounds_total: Cell::new(0),
correctors_total: Cell::new(0),
last_d: zero_d,
last_defects: Cell::new((0.0, 0.0, 0.0)),
})
}
@@ -343,6 +394,21 @@ impl OversetFluid {
.iter()
.map(|&(u, v)| [u, v])
.collect();
// `RTX_FSI2O_VOLUME_PRESERVE`: the fluid sees a volume-preserving
// wall — the imposed velocity's net flux (the flag's thickness
// breathing under the pressure step, a ~300 Hz mode of the
// ν = 0.4 solid) is removed uniformly along the wall.
w.q = 0.0;
if std::env::var("RTX_FSI2O_VOLUME_PRESERVE").is_ok() {
let (flux, len) = w.net_flux();
w.q = flux / len.max(1e-300);
}
}
// `RTX_FSI2O_FREEZE_PATCH`: the undeformed patch throughout (the
// wall velocity still moves) — is the regeneration the driver?
if std::env::var("RTX_FSI2O_FREEZE_PATCH").is_ok() {
self.last_d = d.to_vec();
return Ok(());
}
let mesh = match self.patch_for(d) {
Ok(m) => m,
@@ -389,6 +455,11 @@ impl OversetFluid {
return Err(e);
}
};
self.last_defects.set((
r.patch_mass_defect,
r.background_mass_defect,
r.patch_max_divergence,
));
self.reclassified_total
.set(self.reclassified_total.get() + r.reclassified_cells);
self.fresh_total.set(self.fresh_total.get() + r.fresh_cells);
@@ -498,6 +569,71 @@ impl OversetFluid {
)
}
/// The wall pressure's roughness along the flag: over the flag's
/// wall faces in order, `Σ|t_n(f+1) t_n(f)| / Σ|t_n(f)|` with `t_n`
/// the normal traction, and the count of sign changes of the
/// face-to-face difference — a checkerboard reads ≫ 1 with a sign
/// change at every face.
pub fn wall_roughness(&self) -> (f64, usize, f64, f64) {
let mut tn: Vec<f64> = Vec::new();
for (centre, normal, _, traction) in
self.solver
.patch()
.wall_tractions(&self.field.patch, PatchSide::Inner, self.solver.time())
{
let on_cylinder = ((centre[0] - CYL_CENTRE[0]).powi(2)
+ (centre[1] - CYL_CENTRE[1]).powi(2))
.sqrt()
< CYL_R + 1e-9;
if !on_cylinder {
tn.push(traction[0] * normal[0] + traction[1] * normal[1]);
}
}
let total: f64 = tn.iter().map(|v| v.abs()).sum();
let mut jumps = 0.0;
let mut flips = 0usize;
let mut prev_diff = 0.0;
for w in tn.windows(2) {
let d = w[1] - w[0];
jumps += d.abs();
if prev_diff * d < 0.0 {
flips += 1;
}
prev_diff = d;
}
let max = tn.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
(jumps / total.max(1e-300), flips, max, total / tn.len().max(1) as f64)
}
/// The patch's pressure level (mean over its cells) and the wall's
/// net volume flux from the imposed velocity (`Σ u_wall · S_out` over
/// the Inner faces, positive = fluid leaving the patch through the
/// wall), against the inner ring's enclosed-area rate would be the
/// mesh's own; both must match for a consistent moving wall.
pub fn level_and_wall_flux(&self) -> (f64, f64, f64, f64, f64) {
let mesh = self.solver.patch().mesh();
let p = &self.field.patch.p;
let level = p.iter().sum::<f64>() / p.len().max(1) as f64;
let wall = self.shared.read().unwrap();
let (poly_flux, _) = wall.net_flux();
let poly_area = wall.area();
let (mut flux, mut area) = (0.0, 0.0);
for (f, face) in mesh.faces().iter().enumerate() {
if mesh.side(f) != Some(PatchSide::Inner) {
continue;
}
// Inner side: S points from the body into the fluid (the
// neighbour is the cell), so S is out of the fluid.
let sign = if face.neigh.is_some() { 1.0 } else { -1.0 };
let s_in = [sign * face.s[0], sign * face.s[1]];
let (u, v) = wall.velocity_at(face.centre[0], face.centre[1]);
// Volume flux INTO the fluid = u_wall · S_into_fluid.
flux += u * s_in[0] + v * s_in[1];
area += (s_in[0] * s_in[0] + s_in[1] * s_in[1]).sqrt();
}
(level, flux, area, poly_flux, poly_area)
}
pub fn snapshot(&self) -> (OversetSolverState, OversetField) {
(self.solver.snapshot(), self.field.clone())
}