From 12d7ee74064b85266005b1b743f970fbda7ff8f3 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 11 Feb 2026 17:06:30 +0000 Subject: [PATCH 1/7] prevent negative areas, due to delaunay triangles at edge next to one another --- autoarray/structures/mesh/delaunay_2d.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/autoarray/structures/mesh/delaunay_2d.py b/autoarray/structures/mesh/delaunay_2d.py index 3bd8cf466..2095b8d74 100644 --- a/autoarray/structures/mesh/delaunay_2d.py +++ b/autoarray/structures/mesh/delaunay_2d.py @@ -59,7 +59,9 @@ def scipy_delaunay(points_np, query_points_np, use_voronoi_areas, areas_factor): xp=np, ) + areas = np.maximum(areas, 0.0) split_point_areas = areas_factor * np.sqrt(areas) + split_point_areas = np.where(np.isfinite(split_point_areas), split_point_areas, 0.0) # ---------- Compute split cross points for Split regularization ---------- split_points = split_points_from( From 952b12da16da72d2419603d38a07de123e72aafa Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 11 Feb 2026 17:16:19 +0000 Subject: [PATCH 2/7] add small buffer to mask movement --- .../inversion/pixelization/border_relocator.py | 18 +++++++++++------- autoarray/structures/mesh/delaunay_2d.py | 2 -- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/autoarray/inversion/pixelization/border_relocator.py b/autoarray/inversion/pixelization/border_relocator.py index 4ec44def0..4e5941fa3 100644 --- a/autoarray/inversion/pixelization/border_relocator.py +++ b/autoarray/inversion/pixelization/border_relocator.py @@ -263,7 +263,7 @@ def ellipse_params_via_border_pca_from(border_grid, xp=np, eps=1e-12): return origin, a, b, phi -def relocated_grid_via_ellipse_border_from(grid, origin, a, b, phi, xp=np, eps=1e-12): +def relocated_grid_via_ellipse_border_from(grid, origin, a, b, phi, xp=np, border_frac=1e-3): """ Rotated ellipse centered at origin with semi-axes a (major, x'), b (minor, y'), rotated by phi radians (counterclockwise). @@ -284,28 +284,32 @@ def relocated_grid_via_ellipse_border_from(grid, origin, a, b, phi, xp=np, eps=1 Numerical safety epsilon. """ - # shift to origin dy = grid[:, 0] - origin[0] dx = grid[:, 1] - origin[1] c = xp.cos(phi) s = xp.sin(phi) - # rotate into ellipse-aligned frame xprime = c * dx + s * dy yprime = -s * dx + c * dy - # ellipse radius in normalized coords q = (xprime / a) ** 2 + (yprime / b) ** 2 outside = q > 1.0 - scale = 1.0 / xp.sqrt(xp.maximum(q, 1.0 + eps)) - # scale back to boundary + # Target radius in normalized coords (slightly inside 1.0) + # Using squared target so it matches q's definition. + shrink = xp.asarray(1.0 - border_frac, dtype=q.dtype) + q_target = shrink * shrink # (1 - border_frac)^2 + + # Project outside points to q = q_target + # scale^2 * q = q_target -> scale = sqrt(q_target / q) + safe_q = xp.maximum(q, xp.asarray(1.0, dtype=q.dtype)) # outside => q>=1; inside => 1 + scale = xp.sqrt(q_target / safe_q) + xprime2 = xprime * scale yprime2 = yprime * scale - # rotate back to original frame dx2 = c * xprime2 - s * yprime2 dy2 = s * xprime2 + c * yprime2 diff --git a/autoarray/structures/mesh/delaunay_2d.py b/autoarray/structures/mesh/delaunay_2d.py index 2095b8d74..3bd8cf466 100644 --- a/autoarray/structures/mesh/delaunay_2d.py +++ b/autoarray/structures/mesh/delaunay_2d.py @@ -59,9 +59,7 @@ def scipy_delaunay(points_np, query_points_np, use_voronoi_areas, areas_factor): xp=np, ) - areas = np.maximum(areas, 0.0) split_point_areas = areas_factor * np.sqrt(areas) - split_point_areas = np.where(np.isfinite(split_point_areas), split_point_areas, 0.0) # ---------- Compute split cross points for Split regularization ---------- split_points = split_points_from( From a2e3c05bc8e2cb0d2fa2977241c03a54ef4066e3 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 11 Feb 2026 17:19:27 +0000 Subject: [PATCH 3/7] change frac --- autoarray/inversion/pixelization/border_relocator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoarray/inversion/pixelization/border_relocator.py b/autoarray/inversion/pixelization/border_relocator.py index 4e5941fa3..9c9047135 100644 --- a/autoarray/inversion/pixelization/border_relocator.py +++ b/autoarray/inversion/pixelization/border_relocator.py @@ -263,7 +263,7 @@ def ellipse_params_via_border_pca_from(border_grid, xp=np, eps=1e-12): return origin, a, b, phi -def relocated_grid_via_ellipse_border_from(grid, origin, a, b, phi, xp=np, border_frac=1e-3): +def relocated_grid_via_ellipse_border_from(grid, origin, a, b, phi, xp=np, border_frac=1e-2): """ Rotated ellipse centered at origin with semi-axes a (major, x'), b (minor, y'), rotated by phi radians (counterclockwise). From 117b7540a59c3188e2fa77372976d644dafe858e Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 11 Feb 2026 17:21:14 +0000 Subject: [PATCH 4/7] move outside border should be safer --- .../pixelization/border_relocator.py | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/autoarray/inversion/pixelization/border_relocator.py b/autoarray/inversion/pixelization/border_relocator.py index 9c9047135..94ff70d29 100644 --- a/autoarray/inversion/pixelization/border_relocator.py +++ b/autoarray/inversion/pixelization/border_relocator.py @@ -263,25 +263,15 @@ def ellipse_params_via_border_pca_from(border_grid, xp=np, eps=1e-12): return origin, a, b, phi -def relocated_grid_via_ellipse_border_from(grid, origin, a, b, phi, xp=np, border_frac=1e-2): +def relocated_grid_via_ellipse_border_from( + grid, origin, a, b, phi, xp=np, eps=1e-12, border_frac=1e-3 +): """ - Rotated ellipse centered at origin with semi-axes a (major, x'), b (minor, y'), - rotated by phi radians (counterclockwise). + Move points outside the ellipse to a contour slightly *outside* the border + to avoid geometric degeneracy in Voronoi/Delaunay. - Parameters - ---------- - grid : (N,2) - Coordinates in (y, x) order. - origin : (2,) - Ellipse center (y0, x0). - a, b : float - Semi-major and semi-minor axes. - phi : float - Rotation angle in radians. - xp : module - numpy-like module (np, jnp, cupy, etc.). - eps : float - Numerical safety epsilon. + border_frac: fractional expansion of ellipse radius + (e.g. 1e-3 = +0.1% outside). """ dy = grid[:, 0] - origin[0] @@ -290,26 +280,27 @@ def relocated_grid_via_ellipse_border_from(grid, origin, a, b, phi, xp=np, borde c = xp.cos(phi) s = xp.sin(phi) + # Rotate into ellipse-aligned frame xprime = c * dx + s * dy yprime = -s * dx + c * dy + # Normalized ellipse radius q = (xprime / a) ** 2 + (yprime / b) ** 2 outside = q > 1.0 - # Target radius in normalized coords (slightly inside 1.0) - # Using squared target so it matches q's definition. - shrink = xp.asarray(1.0 - border_frac, dtype=q.dtype) - q_target = shrink * shrink # (1 - border_frac)^2 + # Target radius slightly OUTSIDE ellipse + expand = xp.asarray(1.0 + border_frac, dtype=q.dtype) + q_target = expand * expand # (1 + border_frac)^2 - # Project outside points to q = q_target - # scale^2 * q = q_target -> scale = sqrt(q_target / q) - safe_q = xp.maximum(q, xp.asarray(1.0, dtype=q.dtype)) # outside => q>=1; inside => 1 + # Project only outside points + safe_q = xp.maximum(q, xp.asarray(1.0, dtype=q.dtype)) scale = xp.sqrt(q_target / safe_q) xprime2 = xprime * scale yprime2 = yprime * scale + # Rotate back dx2 = c * xprime2 - s * yprime2 dy2 = s * xprime2 + c * yprime2 @@ -318,6 +309,7 @@ def relocated_grid_via_ellipse_border_from(grid, origin, a, b, phi, xp=np, borde return xp.where(outside[:, None], moved, grid) + class BorderRelocator: def __init__( self, From 30cd355d4556822146d7ea62da54fcf6ecf36820 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 11 Feb 2026 17:24:13 +0000 Subject: [PATCH 5/7] one more frac --- autoarray/inversion/pixelization/border_relocator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoarray/inversion/pixelization/border_relocator.py b/autoarray/inversion/pixelization/border_relocator.py index 94ff70d29..162689917 100644 --- a/autoarray/inversion/pixelization/border_relocator.py +++ b/autoarray/inversion/pixelization/border_relocator.py @@ -264,7 +264,7 @@ def ellipse_params_via_border_pca_from(border_grid, xp=np, eps=1e-12): def relocated_grid_via_ellipse_border_from( - grid, origin, a, b, phi, xp=np, eps=1e-12, border_frac=1e-3 + grid, origin, a, b, phi, xp=np, eps=1e-12, border_frac=1e-2 ): """ Move points outside the ellipse to a contour slightly *outside* the border From ea5e0b201b6aa4005afc8d89fd3a4ef346bff088 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 11 Feb 2026 17:27:06 +0000 Subject: [PATCH 6/7] use barycentric areas --- .../pixelization/border_relocator.py | 42 ++++++++++--------- autoarray/structures/mesh/delaunay_2d.py | 34 +++++++-------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/autoarray/inversion/pixelization/border_relocator.py b/autoarray/inversion/pixelization/border_relocator.py index 162689917..4ec44def0 100644 --- a/autoarray/inversion/pixelization/border_relocator.py +++ b/autoarray/inversion/pixelization/border_relocator.py @@ -263,44 +263,49 @@ def ellipse_params_via_border_pca_from(border_grid, xp=np, eps=1e-12): return origin, a, b, phi -def relocated_grid_via_ellipse_border_from( - grid, origin, a, b, phi, xp=np, eps=1e-12, border_frac=1e-2 -): +def relocated_grid_via_ellipse_border_from(grid, origin, a, b, phi, xp=np, eps=1e-12): """ - Move points outside the ellipse to a contour slightly *outside* the border - to avoid geometric degeneracy in Voronoi/Delaunay. + Rotated ellipse centered at origin with semi-axes a (major, x'), b (minor, y'), + rotated by phi radians (counterclockwise). - border_frac: fractional expansion of ellipse radius - (e.g. 1e-3 = +0.1% outside). + Parameters + ---------- + grid : (N,2) + Coordinates in (y, x) order. + origin : (2,) + Ellipse center (y0, x0). + a, b : float + Semi-major and semi-minor axes. + phi : float + Rotation angle in radians. + xp : module + numpy-like module (np, jnp, cupy, etc.). + eps : float + Numerical safety epsilon. """ + # shift to origin dy = grid[:, 0] - origin[0] dx = grid[:, 1] - origin[1] c = xp.cos(phi) s = xp.sin(phi) - # Rotate into ellipse-aligned frame + # rotate into ellipse-aligned frame xprime = c * dx + s * dy yprime = -s * dx + c * dy - # Normalized ellipse radius + # ellipse radius in normalized coords q = (xprime / a) ** 2 + (yprime / b) ** 2 outside = q > 1.0 + scale = 1.0 / xp.sqrt(xp.maximum(q, 1.0 + eps)) - # Target radius slightly OUTSIDE ellipse - expand = xp.asarray(1.0 + border_frac, dtype=q.dtype) - q_target = expand * expand # (1 + border_frac)^2 - - # Project only outside points - safe_q = xp.maximum(q, xp.asarray(1.0, dtype=q.dtype)) - scale = xp.sqrt(q_target / safe_q) - + # scale back to boundary xprime2 = xprime * scale yprime2 = yprime * scale - # Rotate back + # rotate back to original frame dx2 = c * xprime2 - s * yprime2 dy2 = s * xprime2 + c * yprime2 @@ -309,7 +314,6 @@ def relocated_grid_via_ellipse_border_from( return xp.where(outside[:, None], moved, grid) - class BorderRelocator: def __init__( self, diff --git a/autoarray/structures/mesh/delaunay_2d.py b/autoarray/structures/mesh/delaunay_2d.py index 3bd8cf466..89d05717f 100644 --- a/autoarray/structures/mesh/delaunay_2d.py +++ b/autoarray/structures/mesh/delaunay_2d.py @@ -40,24 +40,24 @@ def scipy_delaunay(points_np, query_points_np, use_voronoi_areas, areas_factor): # ---------- Voronoi or Barycentric Areas used to weight split points ---------- - if use_voronoi_areas: - - areas = voronoi_areas_numpy( - points, - ) - - max_area = np.percentile(areas, 90.0) - - areas[areas == -1] = max_area - areas[areas > max_area] = max_area - - else: + # if use_voronoi_areas: + # + # areas = voronoi_areas_numpy( + # points, + # ) + # + # max_area = np.percentile(areas, 90.0) + # + # areas[areas == -1] = max_area + # areas[areas > max_area] = max_area + # + # else: - areas = barycentric_dual_area_from( - points, - simplices, - xp=np, - ) + areas = barycentric_dual_area_from( + points, + simplices, + xp=np, + ) split_point_areas = areas_factor * np.sqrt(areas) From e57c1f942c4858396f6b063fc3a837037fd68df7 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 11 Feb 2026 17:31:44 +0000 Subject: [PATCH 7/7] revert to bary --- autoarray/structures/mesh/delaunay_2d.py | 37 +++++++++++++----------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/autoarray/structures/mesh/delaunay_2d.py b/autoarray/structures/mesh/delaunay_2d.py index 89d05717f..a4086bab1 100644 --- a/autoarray/structures/mesh/delaunay_2d.py +++ b/autoarray/structures/mesh/delaunay_2d.py @@ -40,24 +40,27 @@ def scipy_delaunay(points_np, query_points_np, use_voronoi_areas, areas_factor): # ---------- Voronoi or Barycentric Areas used to weight split points ---------- - # if use_voronoi_areas: - # - # areas = voronoi_areas_numpy( - # points, - # ) - # - # max_area = np.percentile(areas, 90.0) - # - # areas[areas == -1] = max_area - # areas[areas > max_area] = max_area - # - # else: + if use_voronoi_areas: - areas = barycentric_dual_area_from( - points, - simplices, - xp=np, - ) + try: + areas = voronoi_areas_numpy(points) + except Exception as e: + # Qhull precision problems -> fallback + print(f"[pure_callback] Voronoi failed ({type(e).__name__}); falling back to barycentric areas.") + areas = barycentric_dual_area_from(points, simplices, xp=np) + + max_area = np.percentile(areas, 90.0) + + areas[areas == -1] = max_area + areas[areas > max_area] = max_area + + else: + + areas = barycentric_dual_area_from( + points, + simplices, + xp=np, + ) split_point_areas = areas_factor * np.sqrt(areas)