P5-1: FSI2 with the fluid on the overset — fsi2_harness/overset.rs (the background without a body, the cylinder–flag patch regenerated around the deformed flag every pass via set_patch_mesh, the wall velocity from the interface velocities along the wetted polygon, the load from the patch's wall faces into WettedSurface::transfer_load — no probes, no clamp, no smoothing), fsi2_harness/overset_march.rs (the harness's rigid phase / release / subiterated coupling with its acceptance rule, no rescue machinery, death returned not panicked), tests/turek_hron_fsi2_overset.rs (RTX_FSI2O_* knobs); rtx-cfd: CurvilinearPisoSolver::wall_tractions (per-face pressure + full-stress traction, surface_force sums the same terms bit-identically); Interface::edges (bottom/tip/top for the generator); cylinder_flag_mms RTX_CF_BEND (P5-0 gate iv: Stokes orders 2.14 / 2.09 on the flag bent to 80 mm)
Documentation / Build API Documentation (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
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 User Guide (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 / Python Bindings (maturin) (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 07:16:53 -07:00
co-authored by Claude Fable 5.1
parent c2451fbacf
commit f0b2563bf8
6 changed files with 1076 additions and 41 deletions
@@ -651,45 +651,13 @@ impl CurvilinearPisoSolver {
/// enters through the wall point itself). No traction reconstruction
/// through a staircase — the second thing the overset buys (§2.1).
pub fn surface_force(&self, field: &PatchField, side: PatchSide, t: f64) -> PatchLoad {
let mesh = &self.mesh;
let mu = self.config.viscosity;
let (mut fp, mut fv) = ([0.0; 2], [0.0; 2]);
let mut faces = 0usize;
for (f, face) in mesh.faces().iter().enumerate() {
if mesh.side(f) != Some(side) {
continue;
}
let c = mesh.boundary_cell(f);
// Outward from the body = into the fluid: for the Inner side
// (k = 0, the cell is the neighbour) that is +S; for the Outer
// side (the cell is the owner) it is S.
let sign = if face.neigh.is_some() { 1.0 } else { -1.0 };
let s = [sign * face.s[0], sign * face.s[1]];
let xc = mesh.centre(c);
let dxf = [face.centre[0] - xc[0], face.centre[1] - xc[1]];
let gp = self.pressure_gradient(&field.p, c);
let p_f = field.p[c] + gp[0] * dxf[0] + gp[1] * dxf[1];
for (_, (p_f, s, tau)) in self.wall_face_terms(field, side, t) {
fp[0] -= p_f * s[0];
fp[1] -= p_f * s[1];
let wall = |ff: usize| -> Option<(f64, f64)> {
let fc = &mesh.faces()[ff];
let sd = mesh.side(ff)?;
match self.params.boundaries.get(sd) {
SideBc::Velocity => {
Some(self.boundary_velocity(sd, fc.centre[0], fc.centre[1], t))
}
SideBc::Outlet => None,
}
};
let gu = self
.ops
.gradient(mesh, c, &field.u, &|ff| wall(ff).map(|w| w.0));
let gv = self
.ops
.gradient(mesh, c, &field.v, &|ff| wall(ff).map(|w| w.1));
// τ = μ (∇u + ∇uᵀ): τxx = 2 u_x, τxy = u_y + v_x, τyy = 2 v_y.
fv[0] += mu * (2.0 * gu[0] * s[0] + (gu[1] + gv[0]) * s[1]);
fv[1] += mu * ((gu[1] + gv[0]) * s[0] + 2.0 * gv[1] * s[1]);
fv[0] += tau[0];
fv[1] += tau[1];
faces += 1;
}
PatchLoad {
@@ -698,4 +666,79 @@ impl CurvilinearPisoSolver {
faces,
}
}
/// Per wall face of `side`: the face pressure, the area vector INTO the
/// fluid, and the viscous force `μ (∇u + ∇uᵀ) · S` — the terms
/// [`Self::surface_force`] sums. Iterates in face order.
fn wall_face_terms<'a>(
&'a self,
field: &'a PatchField,
side: PatchSide,
t: f64,
) -> impl Iterator<Item = (usize, (f64, [f64; 2], [f64; 2]))> + 'a {
let mesh = &self.mesh;
let mu = self.config.viscosity;
mesh.faces()
.iter()
.enumerate()
.filter(move |(f, _)| mesh.side(*f) == Some(side))
.map(move |(f, face)| {
let c = mesh.boundary_cell(f);
// Outward from the body = into the fluid: for the Inner side
// (k = 0, the cell is the neighbour) that is +S; for the Outer
// side (the cell is the owner) it is S.
let sign = if face.neigh.is_some() { 1.0 } else { -1.0 };
let s = [sign * face.s[0], sign * face.s[1]];
let xc = mesh.centre(c);
let dxf = [face.centre[0] - xc[0], face.centre[1] - xc[1]];
let gp = self.pressure_gradient(&field.p, c);
let p_f = field.p[c] + gp[0] * dxf[0] + gp[1] * dxf[1];
let wall = |ff: usize| -> Option<(f64, f64)> {
let fc = &mesh.faces()[ff];
let sd = mesh.side(ff)?;
match self.params.boundaries.get(sd) {
SideBc::Velocity => {
Some(self.boundary_velocity(sd, fc.centre[0], fc.centre[1], t))
}
SideBc::Outlet => None,
}
};
let gu = self
.ops
.gradient(mesh, c, &field.u, &|ff| wall(ff).map(|w| w.0));
let gv = self
.ops
.gradient(mesh, c, &field.v, &|ff| wall(ff).map(|w| w.1));
// τ = μ (∇u + ∇uᵀ): τxx = 2 u_x, τxy = u_y + v_x, τyy = 2 v_y.
let tau = [
mu * (2.0 * gu[0] * s[0] + (gu[1] + gv[0]) * s[1]),
mu * ((gu[1] + gv[0]) * s[0] + 2.0 * gv[1] * s[1]),
];
(f, (p_f, s, tau))
})
}
/// The traction on every wall face of `side` (P5, the load transfer):
/// `(face centre, unit normal into the fluid, face length, traction
/// per unit length = (p_f S + μ (∇u + ∇uᵀ) · S) / |S|)`, in face
/// order — the same terms as [`Self::surface_force`].
pub fn wall_tractions(
&self,
field: &PatchField,
side: PatchSide,
t: f64,
) -> Vec<([f64; 2], [f64; 2], f64, [f64; 2])> {
let mesh = &self.mesh;
self.wall_face_terms(field, side, t)
.map(|(f, (p_f, s, tau))| {
let len = (s[0] * s[0] + s[1] * s[1]).sqrt().max(1e-300);
(
mesh.faces()[f].centre,
[s[0] / len, s[1] / len],
len,
[(-p_f * s[0] + tau[0]) / len, (-p_f * s[1] + tau[1]) / len],
)
})
.collect()
}
}