rtx-cfd: F2 — the moving embedded body, and falsifier 3 measured

EmbeddedPisoSolver::set_moving_body: the mask is rebuilt at the
end-of-step geometry every step, and the new mask's ghost values are
reconstructed FROM THE PREVIOUS CORRECTED FIELD (EmbeddedMask::
impose_from — the boundary-history principle extended to a moving wall),
so a stationary body run through the moving path is bit-identical to the
static path, which is the first test. A velocity face that flips
solid -> fluid enters the new interval holding exactly the ghost
reconstruction the previous step left on it — a consistent near-wall
value, not garbage; a fresh pressure cell is refilled from its fluid
neighbours before the predictor's gradient can read the value it kept
while inside the body. The body must move under a cell per step (the
convective dt limit already enforces this for bodies slower than the
local peak velocity). EmbeddedResult reports fresh_cells.

tests/embedded_moving.rs:
- a stationary body through the moving path: 0.0 difference over 100
  steps (and zero fresh cells, identical ghost corrections);
- a circle (r = 0.2) translating through the steady manufactured field
  with the exact field as its surface velocity — the solution must hold
  still while the mask sweeps 84 cells fresh over 300 steps at n = 32:
  max L2 velocity error 9.85e-3 = 1.16x the static steady level
  (8.489e-3), max L2 pressure error 4.67e-2 = 2.11x the static level
  (2.22e-2), bulk |div u| 1.6e-7, projection residual 5.9e-9 every step.

That pressure ratio is the geometry decision's falsifier 3 (omni-cortex
docs/turek_hron_geometry_decision.md): fresh-cell transients sit at ~2x
the static discretisation error, not orders above it — the falsifier
does not fire and no cut cells are needed. Measurement note, recorded in
the test: the divergence of body-adjacent cells read after the
end-of-step ghost re-imposition is a one-step lag by design (the next
projection honours the re-imposed prescribed fluxes — the same lag the
static path has); the continuity claims are the projection residual and
the bulk divergence over all-fluid-faced cells.

Deferred: an oscillating-cylinder benchmark against published force
histories (Duetsch et al. 1998) when the FSI rungs need it.

rtx-cfd 321 -> 323 green.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-20 13:07:28 -07:00
co-authored by Claude Fable 5
parent 38645c7c74
commit 0ad31abb6b
3 changed files with 424 additions and 7 deletions
@@ -525,6 +525,26 @@ impl EmbeddedMask {
u: &mut DMatrix<f64>,
v: &mut DMatrix<f64>,
t: f64,
) -> f64 {
let (u_source, v_source) = (u.clone(), v.clone());
self.impose_from(body, &u_source, &v_source, u, v, t)
}
/// [`Self::impose`] with the fluid values read from a *different* field
/// than the one written: the moving-body step reconstructs the new
/// mask's ghost values from the previous step's corrected field (the
/// boundary-history principle — ghost data, like domain-boundary data,
/// is carried by what the previous step left, not by the uncorrected
/// predictor state). With `source == target` values this is `impose`.
#[allow(clippy::too_many_arguments)]
pub fn impose_from(
&self,
body: &EmbeddedBody,
u_source: &DMatrix<f64>,
v_source: &DMatrix<f64>,
u: &mut DMatrix<f64>,
v: &mut DMatrix<f64>,
t: f64,
) -> f64 {
let (nx, ny, dx, dy) = (self.nx, self.ny, self.dx, self.dy);
@@ -549,10 +569,18 @@ impl EmbeddedMask {
}
}
// Ghost values from the fluid field as it stands (reads only fluid
// faces and fallbacks, so order does not matter).
let u_vals: Vec<f64> = self.u_ghosts.iter().map(|g| g.reconstruct(u)).collect();
let v_vals: Vec<f64> = self.v_ghosts.iter().map(|g| g.reconstruct(v)).collect();
// Ghost values from the source fluid field (reads only fluid faces
// and fallbacks, so order does not matter).
let u_vals: Vec<f64> = self
.u_ghosts
.iter()
.map(|g| g.reconstruct(u_source))
.collect();
let v_vals: Vec<f64> = self
.v_ghosts
.iter()
.map(|g| g.reconstruct(v_source))
.collect();
// Net outward (from fluid) flux through flux-carrying ghost faces.
let mut net = 0.0;