Skip to content

Commit d802559

Browse files
Jammy2211Jammy2211
authored andcommitted
fix: isolate invalid Delaunay callback lanes
1 parent 73cdf06 commit d802559

2 files changed

Lines changed: 86 additions & 10 deletions

File tree

autoarray/inversion/mesh/interpolator/delaunay.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,27 @@ def scipy_delaunay_tri_only(points_np):
9696
coplanar/duplicate (the walk then starts from simplex 0 instead,
9797
which is equally valid — the walk converges from any start).
9898
"""
99+
N = points_np.shape[0]
100+
simplices_padded = -np.ones((2 * N, 3), dtype=np.int32)
101+
simplex_neighbors = -np.ones((2 * N, 3), dtype=np.int32)
102+
vertex_simplex = -np.ones(N, dtype=np.int32)
103+
104+
# This function is the body of a sequentially-vmapped host callback. A
105+
# qhull exception in one lane aborts the entire batch, so keep non-finite
106+
# input lane-local and encode it with the callback's existing padding
107+
# convention. The JAX-side walk and weights preserve the lane as NaN.
108+
if not np.isfinite(points_np).all():
109+
return simplices_padded, simplex_neighbors, vertex_simplex
110+
99111
from scipy.spatial import Delaunay
100112

101-
N = points_np.shape[0]
102113
tri = Delaunay(points_np)
103114
simplices = tri.simplices.astype(np.int32) # (T, 3)
104115
T = simplices.shape[0]
105116

106-
simplices_padded = -np.ones((2 * N, 3), dtype=np.int32)
107117
simplices_padded[:T] = simplices
108-
109-
simplex_neighbors = -np.ones((2 * N, 3), dtype=np.int32)
110118
simplex_neighbors[:T] = tri.neighbors.astype(np.int32)
111119

112-
vertex_simplex = -np.ones(N, dtype=np.int32)
113120
simplex_ids = np.arange(T, dtype=np.int32)
114121
for k in range(3):
115122
vertex_simplex[simplices[:, k]] = simplex_ids
@@ -185,9 +192,11 @@ def cross(u, v):
185192

186193
def weights_of(cur, q_chunk):
187194
verts = simplices_padded[cur] # (chunk, 3)
188-
a = points[verts[:, 0]]
189-
b = points[verts[:, 1]]
190-
c = points[verts[:, 2]]
195+
valid_simplex = (verts >= 0).all(axis=1)
196+
safe_verts = verts.clip(min=0)
197+
a = points[safe_verts[:, 0]]
198+
b = points[safe_verts[:, 1]]
199+
c = points[safe_verts[:, 2]]
191200
den = cross(b - a, c - a)
192201
den = xp.where(den != 0.0, den, 1.0)
193202
w = (
@@ -201,13 +210,14 @@ def weights_of(cur, q_chunk):
201210
)
202211
/ den[:, None]
203212
)
204-
return verts, w
213+
return verts, w, valid_simplex
205214

206215
def walk_step(carry, q_chunk):
207216
cur, done, outside = carry
208-
_, w = weights_of(cur, q_chunk)
217+
_, w, valid_simplex = weights_of(cur, q_chunk)
209218
minw = w.min(axis=1)
210219
opposite = w.argmin(axis=1)
220+
outside = outside | (~done & ~valid_simplex)
211221
done = done | (~outside & (minw >= -1.0e-12))
212222
nxt = simplex_neighbors[cur, opposite]
213223
outside = outside | (~done & (nxt < 0))
@@ -551,6 +561,10 @@ def pixel_weights_delaunay_from(
551561
# -----------------------------
552562
pixel_weights = xp.where(has_simplex[:, None], weights_bary, weights_nn)
553563

564+
if xp is not np:
565+
mesh_is_finite = xp.isfinite(mesh_grid).all()
566+
pixel_weights = xp.where(mesh_is_finite, pixel_weights, xp.nan)
567+
554568
return pixel_weights
555569

556570

test_autoarray/inversion/pixelization/interpolator/test_delaunay_walk.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,68 @@ def test__tri_only_callback__tables_match_triangulation():
5252
assert v in tri.simplices[vertex_simplex[v]]
5353

5454

55+
def test__tri_only_callback__non_finite_points_return_sentinel_tables(monkeypatch):
56+
def qhull_must_not_run(_):
57+
raise AssertionError("Delaunay called for non-finite points")
58+
59+
monkeypatch.setattr("scipy.spatial.Delaunay", qhull_must_not_run)
60+
61+
rng = np.random.default_rng(5)
62+
finite_points = rng.uniform(size=(40, 2))
63+
poisoned_points = []
64+
65+
for poison_index in (7, 39):
66+
points = finite_points.copy()
67+
points[poison_index, 0] = np.nan
68+
poisoned_points.append(points)
69+
70+
poisoned_points.append(np.full_like(finite_points, np.nan))
71+
72+
for points in poisoned_points:
73+
simplices, neighbors, vertex_simplex = scipy_delaunay_tri_only(points)
74+
75+
assert simplices.shape == (80, 3)
76+
assert neighbors.shape == (80, 3)
77+
assert vertex_simplex.shape == (40,)
78+
assert simplices.dtype == np.int32
79+
assert neighbors.dtype == np.int32
80+
assert vertex_simplex.dtype == np.int32
81+
assert (simplices == -1).all()
82+
assert (neighbors == -1).all()
83+
assert (vertex_simplex == -1).all()
84+
85+
86+
def test__walk_locator__sentinel_tables_keep_nearest_vertex_fallback_live():
87+
rng = np.random.default_rng(6)
88+
finite_points = rng.uniform(size=(40, 2))
89+
query = rng.uniform(size=(8, 2))
90+
91+
poisoned_points = []
92+
for poison_index in (7, 39):
93+
points = finite_points.copy()
94+
points[poison_index, 0] = np.nan
95+
poisoned_points.append(points)
96+
poisoned_points.append(np.full_like(finite_points, np.nan))
97+
98+
simplices = -np.ones((80, 3), dtype=np.int32)
99+
neighbors = -np.ones((80, 3), dtype=np.int32)
100+
vertex_simplex = -np.ones(40, dtype=np.int32)
101+
102+
for points in poisoned_points:
103+
mappings = pix_indexes_delaunay_walk_from(
104+
query_points=query,
105+
points=points,
106+
simplices_padded=simplices,
107+
simplex_neighbors=neighbors,
108+
vertex_simplex=vertex_simplex,
109+
xp=np,
110+
)
111+
112+
assert (mappings[:, 0] >= 0).all()
113+
assert (mappings[:, 0] < points.shape[0]).all()
114+
assert (mappings[:, 1:] == -1).all()
115+
116+
55117
def _assert_matches_find_simplex(points, query):
56118
"""The walk must reproduce find_simplex + KDTree-fallback semantics: rows
57119
identical, except at most fp edge ties where the returned simplex still

0 commit comments

Comments
 (0)