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 / Format Check (push) Failing after 6s
CI / Build (ubuntu-latest) (push) Failing after 5s
CI / Clippy Check (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 6s
Performance Benchmarks / Run Benchmarks (push) Failing after 27s
CI / Build CPU-Only (Explicit) (push) Failing after 1m24s
Documentation / Build API Documentation (push) Failing after 1m34s
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL
336 lines
13 KiB
Rust
336 lines
13 KiB
Rust
// 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, `|q−z|² ≥ |q−c|² + |c−z|²`, so a ring at Chebyshev
|
||
//! index `r` is at least `√(d_out² + ((r−1)·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);
|
||
}
|
||
}
|