P6-b design 2: the Robin wall on the patch's Inner side — RobinWall { alpha, datum } with the wall velocity u_s + (t_f − datum)/α (explicit, damped by the wall's own viscous gain μ/(α d)) and the compliant-wall pressure term |S| p'/α implicit in the projection (the added-mass operator in the fluid's own response); Dirichlet bit for bit when off; MMS pin on the skewed annulus (orders 2.46/2.13 at α = 10 and 100 μ/h, the Dirichlet values; a 1e12 wall reproduces Dirichlet to 2e-6); OversetPisoSolver::patch_mut; harness knob RTX_FSI2O_ROBIN_ALPHA with the previous pass's tractions as the datum, printed in the header
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 (ubuntu-latest) (push) Failing after 7s
CI / Format Check (push) Failing after 17s
Documentation / Build User Guide (push) Successful in 19s
Documentation / Build API Documentation (push) Failing after 1m51s
CI / Build CPU-Only (Explicit) (push) Failing after 1m58s
CI / Clippy Check (push) Failing after 2m13s
Performance Benchmarks / Run Benchmarks (push) Successful in 2m54s

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL
This commit is contained in:
Omar Sobh
2026-09-15 10:46:35 -05:00
co-authored by Claude Fable 5.1
parent c144a5f733
commit c3dbd5040a
12 changed files with 333 additions and 20 deletions
@@ -232,6 +232,33 @@ pub struct CurvilinearPisoSolver {
acceptors: Option<AcceptorRing>,
time: f64,
matrix: Option<PressureSystem>,
/// The Robin wall on the Inner side, if any.
robin: Option<RobinWall>,
/// Global face index of every Inner face (Inner-face order).
robin_faces: Vec<usize>,
/// `(t_f datum) / alpha` per Inner face for the current step.
robin_offset: Vec<[f64; 2]>,
}
/// A Robin wall on the `Inner` side (P6-b, the coupler with the added
/// mass built in — `docs/overset_metal_campaign.md` §5.19 in omni-cortex):
/// the wall velocity is the prescribed one plus `(t_f datum) / alpha`,
/// with `t_f` the fluid's traction on the body (the [`Self::wall_tractions`]
/// convention) and `datum` the traction the structure was loaded with —
/// a wall of impedance `alpha` (Pa·s/m) that recedes when the fluid
/// pushes harder than the structure expects. Explicit in the predictor
/// (the start-of-step traction); IMPLICIT in the pressure: the wall flux
/// answers the pressure correction with `|S| p' / alpha` (a compliant
/// wall), which is the term that carries the added-mass operator into
/// the fluid's own response per subiterate. At the coupled fixed point
/// `t_f = datum` and the wall is the Dirichlet one.
#[derive(Debug, Clone)]
pub struct RobinWall {
/// Impedance, Pa·s/m (`ρ_s h_s / Δt` for a plate of thickness `h_s`).
pub alpha: f64,
/// The structure's traction per Inner face, in Inner-face order (the
/// order [`CurvilinearPisoSolver::wall_tractions`] returns).
pub datum: Vec<[f64; 2]>,
}
/// The assembled pressure-correction system for one `dt` and geometry.
@@ -277,9 +304,80 @@ impl CurvilinearPisoSolver {
acceptors: None,
time: 0.0,
matrix: None,
robin: None,
robin_faces: Vec::new(),
robin_offset: Vec::new(),
})
}
/// Put a [`RobinWall`] on the Inner side (`datum` per Inner face, in
/// [`Self::wall_tractions`] order) or replace its datum; the pressure
/// matrix is rebuilt. `None` restores the Dirichlet wall bit for bit.
pub fn set_robin_wall(&mut self, wall: Option<RobinWall>) {
let faces: Vec<usize> = (0..self.mesh.faces().len())
.filter(|&f| self.mesh.side(f) == Some(PatchSide::Inner))
.collect();
if let Some(w) = &wall {
assert_eq!(
w.datum.len(),
faces.len(),
"Robin datum must have one entry per Inner face"
);
}
if self.robin_offset.len() != faces.len() {
self.robin_offset = vec![[0.0, 0.0]; faces.len()];
}
self.robin_faces = faces;
self.robin = wall;
self.matrix = None;
}
/// The Robin wall's current velocity offsets per Inner face.
pub fn robin_offsets(&self) -> &[[f64; 2]] {
&self.robin_offset
}
/// `(t_f datum) / alpha` on every Inner face from the field's current
/// tractions (the explicit part of the Robin wall), called at the start
/// of a step; with no Robin wall the offsets stay zero.
fn refresh_robin_offsets(&mut self, field: &PatchField, t: f64) {
let Some(w) = &self.robin else { return };
let alpha = w.alpha;
let datum = w.datum.clone();
let mu = self.config.viscosity;
let tractions = self.wall_tractions(field, PatchSide::Inner, t);
let faces: Vec<usize> = self.robin_faces.clone();
for (j, (_, _, _, tf)) in tractions.iter().enumerate() {
// The wall's own viscous stress answers the wall velocity as
// μ/d; taken implicitly in the update (a plain explicit
// (t_f datum)/α has gain (μ/d)/α and blew up at α = μ/h), the
// fixed point unchanged: offset = (t_f datum)/α.
let f = faces[j];
let c = self.mesh.boundary_cell(f);
let xc = self.mesh.centre(c);
let xf = self.mesh.faces()[f].centre;
let d = ((xf[0] - xc[0]).powi(2) + (xf[1] - xc[1]).powi(2))
.sqrt()
.max(1e-300);
let g = mu / (alpha * d);
let old = self.robin_offset[j];
self.robin_offset[j] = [
((tf[0] - datum[j][0]) / alpha + g * old[0]) / (1.0 + g),
((tf[1] - datum[j][1]) / alpha + g * old[1]) / (1.0 + g),
];
}
}
/// The Robin offset at a point of the Inner side (the nearest face).
fn robin_offset_at(&self, x: f64, y: f64) -> (f64, f64) {
let mut best = (f64::INFINITY, [0.0, 0.0]);
for (j, &f) in self.robin_faces.iter().enumerate() {
let c = self.mesh.faces()[f].centre;
let d = (c[0] - x).powi(2) + (c[1] - y).powi(2);
if d < best.0 {
best = (d, self.robin_offset[j]);
}
}
(best.1[0], best.1[1])
}
/// Velocity on every `Velocity` side, `(x, y, t) -> (u, v)`.
pub fn set_boundary_velocity<F>(&mut self, f: F)
where
@@ -475,10 +573,16 @@ impl CurvilinearPisoSolver {
}
pub(crate) fn boundary_velocity(&self, side: PatchSide, x: f64, y: f64, t: f64) -> (f64, f64) {
self.side_velocity[side_index(side)]
let (u, v) = self.side_velocity[side_index(side)]
.as_ref()
.or(self.boundary_velocity.as_ref())
.map_or((0.0, 0.0), |f| f(x, y, t))
.map_or((0.0, 0.0), |f| f(x, y, t));
if side == PatchSide::Inner && self.robin.is_some() {
let (ou, ov) = self.robin_offset_at(x, y);
(u + ou, v + ov)
} else {
(u, v)
}
}
/// Dirichlet `p'` of acceptor cell `c`, if it is one.
pub(crate) fn acceptor_correction(&self, c: usize) -> Option<f64> {
@@ -568,6 +672,9 @@ impl CurvilinearPisoSolver {
Some(o) => StepGeometry::new(o, &self.mesh, self.params.swept_face_rule),
None => StepGeometry::stationary(&self.mesh),
};
if self.robin.is_some() {
self.refresh_robin_offsets(field, t_old);
}
let old_mesh = old.as_ref().unwrap_or(&self.mesh);
let mesh = &self.mesh;
@@ -6,7 +6,7 @@
use super::{CurvilinearPisoSolver, PatchField, PressureSystem, SideBc, StepGeometry};
use crate::mesh::PatchSide;
use crate::solvers::incompressible::sparse_bicgstab::{
BicgstabResult, CsrMatrix, bicgstab_jacobi, project_mean,
bicgstab_jacobi, project_mean, BicgstabResult, CsrMatrix,
};
impl CurvilinearPisoSolver {
@@ -116,7 +116,9 @@ impl CurvilinearPisoSolver {
]
.iter()
.any(|&s| self.params.boundaries.get(s) == SideBc::Outlet);
if has_outlet || self.acceptors.is_some() {
// A Robin wall absorbs the net flux through its compliance (the
// pressure system is then not pure Neumann).
if has_outlet || self.acceptors.is_some() || self.robin.is_some() {
return 0.0;
}
let (mut net, mut total_len) = (0.0, 0.0);
@@ -158,6 +160,15 @@ impl CurvilinearPisoSolver {
continue;
}
for (f, sign) in mesh.cell_faces(c) {
if let Some(w) = &self.robin {
if mesh.side(f) == Some(PatchSide::Inner) {
// The compliant wall: outward flux `+|S| p'_c / alpha`.
let sv = mesh.faces()[f].s;
let len = (sv[0] * sv[0] + sv[1] * sv[1]).sqrt();
tri.push((c, c, len / w.alpha));
any_dirichlet = true;
}
}
self.ops
.face_gradient_coeffs(mesh, &self.params.boundaries, f, &mut coefs);
if mesh.side(f).is_some() && !coefs.is_empty() {
@@ -257,6 +268,16 @@ impl CurvilinearPisoSolver {
for f in 0..mesh.faces().len() {
field.flux[f] -= dt / rho * lp[f];
}
if let Some(w) = &self.robin {
// The compliant wall's flux answer: `δu_b = p' S / (alpha |S|)`,
// `δF = δu_b · S = p' |S| / alpha` in the face's own orientation.
for &f in &self.robin_faces {
let c = mesh.boundary_cell(f);
let sv = mesh.faces()[f].s;
let len = (sv[0] * sv[0] + sv[1] * sv[1]).sqrt();
field.flux[f] -= pc[c] * len / w.alpha;
}
}
for c in 0..mesh.cell_count() {
if self.is_acceptor(c) {
continue;
@@ -41,7 +41,7 @@ mod projection;
use super::ale::{AleBoundaries, SideBoundary};
use super::embedded_body::{EmbeddedBody, EmbeddedMask, FaceKind};
use super::poisson::{
MgPrecision, MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg,
solve_multigrid_pcg, MgPrecision, MultigridParameters, PoissonProblem, PoissonSolverKind,
};
use super::simple::ConvectionScheme;
use super::{FlowField, SolverResult};
@@ -4,12 +4,12 @@
//! meshes before applying the correction once.
use super::EmbeddedPisoSolver;
use crate::CfdResult;
use crate::solvers::incompressible::ale::SideBoundary;
use crate::solvers::incompressible::poisson::{
MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg,
solve_multigrid_pcg, MultigridParameters, PoissonProblem, PoissonSolverKind,
};
use crate::solvers::incompressible::{EmbeddedMask, FlowField};
use crate::CfdResult;
impl EmbeddedPisoSolver {
/// The pressure-correction system of one projection as a
@@ -337,28 +337,44 @@ impl EmbeddedPisoSolver {
// prescribed: a domain side with velocity data, or a
// non-fluid interior face.
let ae = if i + 1 == nx {
if b.right == outlet { ae_outlet } else { 0.0 }
if b.right == outlet {
ae_outlet
} else {
0.0
}
} else if self.u_is_fluid(j, i + 1) {
ae_interior
} else {
0.0
};
let aw = if i == 0 {
if b.left == outlet { ae_outlet } else { 0.0 }
if b.left == outlet {
ae_outlet
} else {
0.0
}
} else if self.u_is_fluid(j, i) {
ae_interior
} else {
0.0
};
let an = if j + 1 == ny {
if b.top == outlet { an_outlet } else { 0.0 }
if b.top == outlet {
an_outlet
} else {
0.0
}
} else if self.v_is_fluid(j + 1, i) {
an_interior
} else {
0.0
};
let as_ = if j == 0 {
if b.bottom == outlet { an_outlet } else { 0.0 }
if b.bottom == outlet {
an_outlet
} else {
0.0
}
} else if self.v_is_fluid(j, i) {
an_interior
} else {
@@ -322,7 +322,11 @@ pub fn polygon_signed_distance(vertices: &[(f64, f64)], x: f64, y: f64) -> f64 {
}
}
let dist = dist2.sqrt();
if inside { -dist } else { dist }
if inside {
-dist
} else {
dist
}
}
/// Velocity of the point on a closed polygon nearest to `(x, y)`, where
@@ -49,12 +49,12 @@ pub use boundary_conditions::{
pub use curvilinear::{
CurvilinearParameters, CurvilinearPisoSolver, CurvilinearResult, CurvilinearSolverState,
NormalDiffusion, Operators, PatchBalance, PatchBoundaries, PatchConvection, PatchField,
PatchLoad, SideBc, StepGeometry,
PatchLoad, RobinWall, SideBc, StepGeometry,
};
pub use embedded::{EmbeddedParameters, EmbeddedPisoSolver, EmbeddedResult, EmbeddedSolverState};
pub use embedded_body::{
EmbeddedBody, EmbeddedMask, FaceKind, SurfaceForce, SurfaceSample, polygon_interface_velocity,
polygon_signed_distance,
polygon_interface_velocity, polygon_signed_distance, EmbeddedBody, EmbeddedMask, FaceKind,
SurfaceForce, SurfaceSample,
};
pub use flow_field::FlowField;
pub use overset::{
@@ -305,6 +305,10 @@ impl OversetPisoSolver {
pub fn patch(&self) -> &CurvilinearPisoSolver {
&self.patch
}
/// The patch solver, mutably (the Robin wall's datum per pass).
pub fn patch_mut(&mut self) -> &mut CurvilinearPisoSolver {
&mut self.patch
}
/// The current overlap map.
pub fn overlap(&self) -> &OverlapMap {
&self.overlap
@@ -38,7 +38,7 @@
//! instead of using the prescribed boundary faces that exist there.
use super::poisson::{
MgPrecision, MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg,
solve_multigrid_pcg, MgPrecision, MultigridParameters, PoissonProblem, PoissonSolverKind,
};
use super::{BoundaryConditions, FlowField, IncompressibleSolver, SolverResult};
use crate::{CfdConfig, CfdResult};
@@ -206,7 +206,11 @@ impl PolygonSdf {
}
let dist = dist2.sqrt();
if inside { -dist } else { dist }
if inside {
-dist
} else {
dist
}
}
}