rtx-cfd: indexed polygon SDF — bit-identical queries, the fluid's measured hot function cut
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
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s

The 2026-08-30 fluid profile (symbolized samples, rigid AND coupled
phases of the FSI3 default) attributed the fluid step to the function:
polygon_signed_distance 51% rigid / 35% coupled — the embedded mask
rebuild and its ghost reconstruction walk every edge of the ~150-vertex
interface polygon for every cell-centre and face query, every step.
(Also measured, refuting the parked consolidation: Level::new — the MG
hierarchy build — is 0.5-0.7% in BOTH phases; caching it would buy
nothing. The MG smoother at 34-40% is the honest remaining fluid cost.)

PolygonSdf (solvers/incompressible/polygon_sdf.rs): a binned edge
index whose query is BIT-IDENTICAL to polygon_signed_distance by
construction — per-edge distances use the same float ops, the ring
search provably visits a superset of the argmin (convex-projection
lower bound sqrt(d_out^2 + ((r-1)b)^2)), and parity XORs the same ray
tests over exactly the straddling edges (y-binned). Equality is
ASSERTED, not assumed: tests compare to_bits against the brute force
over ~40k adversarial points (flag-like walks, random polygons with
degenerate zero-length edges, horizontal-edge/vertex-y rays). Wired
into EmbeddedBody::polygon and the FSI harness's shared geometry
(rebuilt per set_geometry, ~microseconds for 150 edges).

Verification — the bar for a bit-exact change is digit identity, and
it holds: FSI2 and FSI3 committed defaults reproduce EVERY printed
digit of the banded-LU baseline logs (uy 3.7732±3.7920 / 6.0229±
25.2190 mm, conservation 8.26e-12 / 1.49e-12, rigid drags 121.4 /
426.9); rtx-cfd full suite 0 failures; rtx-fsi lib/piston/transfer/
FSI1 green. The study pins need no re-run: the trajectories are
unchanged by construction and confirmed by measurement.

Wall clock: FSI2 rigid 323 -> 167 s (1.93x), whole default 400 -> 225 s;
FSI3 rigid 420 -> 250 s (1.68x), whole default 539 -> 343 s. Cumulative
with the banded LU this session: FSI3 default 944 -> 343 s (2.75x),
FSI2 524 -> 225 s (2.33x).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
This commit is contained in:
Omar Sobh
2026-08-30 07:47:42 -05:00
co-authored by Claude Fable 5
parent 0b4f306ed1
commit 6c48e53998
4 changed files with 353 additions and 10 deletions
@@ -173,8 +173,11 @@ impl EmbeddedBody {
.sum::<f64>()
* 0.5;
let ccw = signed_area > 0.0;
let sdf_vertices = vertices.clone();
let mut body = Self::from_sdf(move |x, y, _| polygon_signed_distance(&sdf_vertices, x, y));
// Indexed query, bit-identical to `polygon_signed_distance`
// (asserted in polygon_sdf's tests) — the SDF is the measured
// hot function of the embedded mask rebuild.
let sdf = super::polygon_sdf::PolygonSdf::new(vertices.clone());
let mut body = Self::from_sdf(move |x, y, _| sdf.signed_distance(x, y));
let sampler_vertices = vertices;
body.sampler = Some(Box::new(move |ds| {
let n = sampler_vertices.len();
@@ -26,6 +26,7 @@ pub mod piso;
pub mod piso_gpu;
/// Five-point Poisson problems and the multigrid-preconditioned CG solver
pub mod poisson;
pub mod polygon_sdf;
/// SIMPLE algorithm implementation
pub mod simple;
/// GPU-accelerated SIMPLE algorithm implementation
@@ -49,6 +50,7 @@ pub use piso::{PisoParameters, PisoResult, PisoSolver};
#[cfg(feature = "cuda")]
pub use piso_gpu::PisoGpuSolver;
pub use poisson::{MultigridParameters, PoissonProblem, PoissonSolution, PoissonSolverKind};
pub use polygon_sdf::PolygonSdf;
pub use simple::{ConvectionScheme, SimpleParameters, SimpleResult, SimpleSolver};
#[cfg(feature = "cuda")]
pub use simple_gpu::SimpleGpuSolver;
@@ -0,0 +1,335 @@
// Copyright (c) 2024 RustyTorch++ Team
// Licensed under the Apache License, Version 2.0
//! An indexed polygon signed-distance query, bit-identical to
//! [`polygon_signed_distance`].
//!
//! Measured motivation (2026-08-30 FSI3 profile): 51% of the fluid step
//! is `polygon_signed_distance` — the embedded mask rebuild and its
//! ghost reconstruction evaluate the SDF tens of thousands of times per
//! step, each walking every edge of a ~150-vertex interface polygon.
//! This index cuts each query to the handful of edges that can matter,
//! while returning EXACTLY the brute-force f64:
//!
//! - **Distance**: per-edge squared distances are computed by the same
//! float ops as the brute force; the ring search visits a candidate
//! set that provably contains the minimizing edge, and `min` over a
//! superset containing the argmin equals `min` over all edges bit for
//! bit. The ring lower bound uses the convex-projection inequality:
//! for a query `q` clamped to `c` on the grid box and any point `z`
//! inside it, `|qz|² ≥ |qc|² + |cz|²`, so a ring at Chebyshev
//! index `r` is at least `√(d_out² + ((r1)·b)²)` away (`b` the
//! smaller bin dimension).
//! - **Sign**: the even-odd ray test flips parity only for edges whose
//! y-interval straddles the query, so edges are binned by y-interval
//! and the query XORs over exactly the straddling candidates — the
//! same tests, the same parity.
//!
//! The equality is asserted, not assumed: the tests compare against
//! [`polygon_signed_distance`] with `to_bits` over adversarial points.
use super::embedded_body::polygon_signed_distance;
/// Indexed signed distance to a closed polygon (negative inside, either
/// winding). Build once per geometry; queries are `O(edges near the
/// point)` instead of `O(all edges)`.
pub struct PolygonSdf {
vertices: Vec<(f64, f64)>,
/// Grid over the polygon's padded bounding box.
x0: f64,
y0: f64,
bin_w: f64,
bin_h: f64,
nx: usize,
ny: usize,
/// Edge indices per bin (an edge appears in every bin its padded
/// bounding box overlaps).
bins: Vec<Vec<u32>>,
/// Edge indices per y-row of the SAME grid, for the ray-crossing
/// parity: an edge appears in every row its y-interval overlaps.
rows: Vec<Vec<u32>>,
}
impl PolygonSdf {
/// Index `vertices` (at least 3). Bin count scales with the edge
/// count so construction stays `O(edges)`.
#[must_use]
pub fn new(vertices: Vec<(f64, f64)>) -> Self {
assert!(vertices.len() >= 3, "a polygon needs at least 3 vertices");
let n = vertices.len();
let (mut min_x, mut min_y) = (f64::MAX, f64::MAX);
let (mut max_x, mut max_y) = (f64::MIN, f64::MIN);
for &(x, y) in &vertices {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
// Degenerate extents still get a positive bin size.
let width = (max_x - min_x).max(1e-12);
let height = (max_y - min_y).max(1e-12);
// ~2 edges per bin on a perimeter polygon: bins ~ n along the
// longer side, aspect-scaled on the shorter.
let nx = ((n as f64).sqrt() * (width / height).sqrt().max(0.25))
.ceil()
.clamp(1.0, 256.0) as usize;
let ny = ((n as f64).sqrt() * (height / width).sqrt().max(0.25))
.ceil()
.clamp(1.0, 256.0) as usize;
let bin_w = width / nx as f64;
let bin_h = height / ny as f64;
let mut bins = vec![Vec::new(); nx * ny];
let mut rows = vec![Vec::new(); ny];
let clamp_i = |x: f64| (((x - min_x) / bin_w) as isize).clamp(0, nx as isize - 1) as usize;
let clamp_j = |y: f64| (((y - min_y) / bin_h) as isize).clamp(0, ny as isize - 1) as usize;
for k in 0..n {
let (ax, ay) = vertices[k];
let (bx, by) = vertices[(k + 1) % n];
let (i0, i1) = (clamp_i(ax.min(bx)), clamp_i(ax.max(bx)));
let (j0, j1) = (clamp_j(ay.min(by)), clamp_j(ay.max(by)));
for j in j0..=j1 {
for i in i0..=i1 {
bins[j * nx + i].push(k as u32);
}
rows[j].push(k as u32);
}
}
Self {
vertices,
x0: min_x,
y0: min_y,
bin_w,
bin_h,
nx,
ny,
bins,
rows,
}
}
/// The indexed vertices.
#[must_use]
pub fn vertices(&self) -> &[(f64, f64)] {
&self.vertices
}
/// Squared distance from `(x, y)` to edge `k` — float-op for
/// float-op the brute force's per-edge computation.
#[inline]
fn edge_dist2(&self, k: u32, x: f64, y: f64) -> f64 {
let n = self.vertices.len();
let (ax, ay) = self.vertices[k as usize];
let (bx, by) = self.vertices[(k as usize + 1) % n];
let (ex, ey) = (bx - ax, by - ay);
let len2 = ex * ex + ey * ey;
let s = if len2 > 0.0 {
(((x - ax) * ex + (y - ay) * ey) / len2).clamp(0.0, 1.0)
} else {
0.0
};
let (qx, qy) = (ax + s * ex - x, ay + s * ey - y);
qx * qx + qy * qy
}
/// Signed distance, bit-identical to
/// `polygon_signed_distance(self.vertices(), x, y)`.
#[must_use]
pub fn signed_distance(&self, x: f64, y: f64) -> f64 {
// Sign: XOR the ray test over the y-row candidates. Any edge
// that straddles y lies in this row's list (its y-interval
// overlaps the row), so the parity is over exactly the edges
// the brute force flips on.
let j_row = (((y - self.y0) / self.bin_h) as isize).clamp(0, self.ny as isize - 1) as usize;
let mut inside = false;
// Edges whose y-interval leaves the grid entirely are impossible
// (the grid spans the polygon's bbox), but a query y outside the
// bbox straddles nothing — the clamped row still contains every
// straddling edge because there are none.
for &k in &self.rows[j_row] {
let n = self.vertices.len();
let (ax, ay) = self.vertices[k as usize];
let (bx, by) = self.vertices[(k as usize + 1) % n];
if (ay > y) != (by > y) {
let x_cross = ax + (y - ay) / (by - ay) * (bx - ax);
if x < x_cross {
inside = !inside;
}
}
}
// Distance: ring search from the clamped bin.
let ci = (((x - self.x0) / self.bin_w) as isize).clamp(0, self.nx as isize - 1);
let cj = (((y - self.y0) / self.bin_h) as isize).clamp(0, self.ny as isize - 1);
// Distance from the query to the grid box (0 inside).
let cx = x.clamp(self.x0, self.x0 + self.bin_w * self.nx as f64);
let cy = y.clamp(self.y0, self.y0 + self.bin_h * self.ny as f64);
let d_out2 = (x - cx) * (x - cx) + (y - cy) * (y - cy);
let b = self.bin_w.min(self.bin_h);
let mut dist2 = f64::MAX;
let max_ring = self.nx.max(self.ny) as isize;
for r in 0..=max_ring {
// Every point of a ring-r bin is at least this far away
// (convex-projection inequality; see the module docs).
if r >= 2 {
let lb = (r - 1) as f64 * b;
if d_out2 + lb * lb > dist2 {
break;
}
}
let (i_lo, i_hi) = (ci - r, ci + r);
let (j_lo, j_hi) = (cj - r, cj + r);
let mut visit = |i: isize, j: isize, dist2: &mut f64| {
if i < 0 || j < 0 || i >= self.nx as isize || j >= self.ny as isize {
return;
}
for &k in &self.bins[j as usize * self.nx + i as usize] {
let d2 = self.edge_dist2(k, x, y);
if d2 < *dist2 {
*dist2 = d2;
}
}
};
if r == 0 {
visit(ci, cj, &mut dist2);
} else {
for i in i_lo..=i_hi {
visit(i, j_lo, &mut dist2);
visit(i, j_hi, &mut dist2);
}
for j in (j_lo + 1)..j_hi {
visit(i_lo, j, &mut dist2);
visit(i_hi, j, &mut dist2);
}
}
}
let dist = dist2.sqrt();
if inside { -dist } else { dist }
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A deterministic pseudo-random stream (no rand dependency).
struct Lcg(u64);
impl Lcg {
fn next_f64(&mut self, lo: f64, hi: f64) -> f64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let u = (self.0 >> 11) as f64 / (1u64 << 53) as f64;
lo + u * (hi - lo)
}
}
fn assert_bit_identical(vertices: &[(f64, f64)], points: &[(f64, f64)]) {
let sdf = PolygonSdf::new(vertices.to_vec());
for &(x, y) in points {
let brute = polygon_signed_distance(vertices, x, y);
let indexed = sdf.signed_distance(x, y);
assert_eq!(
brute.to_bits(),
indexed.to_bits(),
"indexed {indexed:.17e} != brute {brute:.17e} at ({x}, {y})"
);
}
}
/// A flag-like polygon: a long thin rectangle sampled densely (the
/// FSI interface walk's shape), mildly deformed.
fn flag_polygon(n_per_side: usize, deflect: f64) -> Vec<(f64, f64)> {
let (x0, x1, y0, y1) = (0.25, 0.6, 0.19, 0.21);
let mut v = Vec::new();
for k in 0..n_per_side {
let s = k as f64 / n_per_side as f64;
let x = x0 + s * (x1 - x0);
v.push((x, y0 + deflect * s * s));
}
for k in 0..3 {
let s = k as f64 / 3.0;
v.push((x1, y0 + deflect + s * (y1 - y0)));
}
for k in 0..n_per_side {
let s = k as f64 / n_per_side as f64;
let x = x1 - s * (x1 - x0);
v.push((x, y1 + deflect * (1.0 - s) * (1.0 - s)));
}
v.push((x0, y1));
v
}
#[test]
fn bit_identical_on_flag_polygon() {
for &deflect in &[0.0, 0.05, -0.08] {
let vertices = flag_polygon(72, deflect);
let mut rng = Lcg(42);
let mut points = Vec::new();
// The whole domain, the near field, and exactly-on-feature
// points (vertices, edge midpoints, the bbox corners).
for _ in 0..2000 {
points.push((rng.next_f64(0.0, 2.5), rng.next_f64(0.0, 0.41)));
}
for _ in 0..2000 {
points.push((rng.next_f64(0.24, 0.62), rng.next_f64(0.15, 0.28)));
}
for k in 0..vertices.len() {
let (ax, ay) = vertices[k];
let (bx, by) = vertices[(k + 1) % vertices.len()];
points.push((ax, ay));
points.push((0.5 * (ax + bx), 0.5 * (ay + by)));
}
points.push((-3.0, -1.0));
points.push((10.0, 5.0));
assert_bit_identical(&vertices, &points);
}
}
#[test]
fn bit_identical_on_random_polygons() {
let mut rng = Lcg(7);
for poly in 0..20 {
let n = 3 + (poly % 9);
let mut vertices: Vec<(f64, f64)> = (0..n)
.map(|_| (rng.next_f64(-1.0, 1.0), rng.next_f64(-1.0, 1.0)))
.collect();
// Exercise degenerate zero-length edges too.
if poly % 4 == 0 {
let first = vertices[0];
vertices.insert(1, first);
}
let points: Vec<(f64, f64)> = (0..1500)
.map(|_| (rng.next_f64(-3.0, 3.0), rng.next_f64(-3.0, 3.0)))
.collect();
assert_bit_identical(&vertices, &points);
}
}
#[test]
fn bit_identical_on_horizontal_edge_rays() {
// Horizontal edges never straddle their own y (the strict/loose
// comparison pair `(ay > y) != (by > y)` is false when ay == by),
// and queries exactly AT a vertex y exercise the boundary of the
// straddle test. The parity must match the brute force on all of
// them.
let vertices = vec![
(0.0, 0.0),
(2.0, 0.0),
(2.0, 1.0),
(1.0, 1.0),
(1.0, 0.5),
(0.0, 0.5),
];
let mut points = Vec::new();
for &y in &[0.0, 0.25, 0.5, 0.75, 1.0] {
for k in 0..40 {
points.push((-0.5 + 3.0 * k as f64 / 39.0, y));
}
}
assert_bit_identical(&vertices, &points);
}
}