rtx-cfd: fix the cell-centre velocity interpolation, which was half a cell out
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 / 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
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
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s

`get_velocity_at` averaged u-faces `i - 1` and `i` to report the velocity
at cell `i`. On this staggered layout `u` is `(ny, nx + 1)` and cell `i`
is bounded by faces `i` and `i + 1` -- which is the convention
`compute_mass_source` uses to form the divergence, and therefore the one
that defines the grid. The two disagreed by one index.

Consequences: every profile read through this function was shifted half a
cell west of the field the solver actually computed, the first and last
cells were special-cased to a single face, and the outermost face was
never read at all.

It is a diagnostic path rather than a solve path -- the residuals are
byte-identical before and after -- but the cavity comparison against Ghia
is taken through it, so the reported vortex position was affected. The
corrected grid study, unchanged in the solve:

    n      u_min      y
    17^2   -0.1257    0.4375
    33^2   -0.1550    0.4688
    65^2   -0.1743    0.4844
    97^2   -0.1825    0.5000
    Ghia   -0.2109    0.4531

The shift matters most where the grid is coarse and washes out under
refinement, which is what a half-cell offset should do.

Found while establishing where each staggered variable physically sits, a
prerequisite for applying the method of manufactured solutions to this
solver.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-19 12:10:41 -07:00
co-authored by Claude Opus 5
parent 8615fc5783
commit 10e5f9cb90
@@ -110,22 +110,20 @@ impl FlowField {
return Err(CfdError::invalid_parameter("Grid indices out of bounds"));
}
// Interpolate velocities to cell center
let u_center = if i == 0 {
self.u[(j, 0)]
} else if i == self.nx - 1 {
self.u[(j, self.nx - 1)]
} else {
0.5 * (self.u[(j, i - 1)] + self.u[(j, i)])
};
let v_center = if j == 0 {
self.v[(0, i)]
} else if j == self.ny - 1 {
self.v[(self.ny - 1, i)]
} else {
0.5 * (self.v[(j - 1, i)] + self.v[(j, i)])
};
// Interpolate to the cell centre from the cell's own two faces.
//
// On this staggered layout `u` is `(ny, nx + 1)` and `v` is
// `(ny + 1, nx)`, and cell `i` is bounded by u-faces `i` and `i + 1` —
// which is the convention `compute_mass_source` uses to form the
// divergence, and therefore the one that defines the grid.
//
// This previously averaged faces `i - 1` and `i`, half a cell to the
// west of the cell it claimed to be reporting, with the first and last
// cells special-cased to a single face and the outermost face never
// read at all. Every profile taken through this function was shifted
// by half a cell against the field the solver actually computed.
let u_center = 0.5 * (self.u[(j, i)] + self.u[(j, i + 1)]);
let v_center = 0.5 * (self.v[(j, i)] + self.v[(j + 1, i)]);
Ok((u_center, v_center))
}