Skip to content

Commit a3d1490

Browse files
authored
Merge pull request #508 from PyAutoLabs/feature/potential-correction-interferometer
feat: zero-fill extrapolation for input pixelized mass profiles
2 parents a904db9 + 4ff4399 commit a3d1490

4 files changed

Lines changed: 97 additions & 12 deletions

File tree

autogalaxy/profiles/mass/input/input_deflections.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def __init__(
1818
deflections_x: np.ndarray,
1919
image_plane_grid: aa.type.Grid2DLike,
2020
mask: aa.type.Mask2D,
21+
extrapolate: str = "nearest",
2122
Hy: Optional[spmatrix] = None,
2223
Hx: Optional[spmatrix] = None,
2324
):
@@ -56,6 +57,14 @@ def __init__(
5657
The cleaned 2D mask defining the unmasked pixels (see
5758
``aa.util.derivative.cleaned_mask_from``); its ``pixel_scale``
5859
sets the finite-difference step of the derived convergence.
60+
extrapolate
61+
The extrapolation behaviour outside the unmasked pixels' convex
62+
hull: ``"nearest"`` (default; the field continues beyond the
63+
grid) or ``"zero"`` (the field vanishes outside it — required
64+
when the profile represents a localized correction on a
65+
sub-region of a larger grid, e.g. an arc-restricted dpsi mesh,
66+
where nearest extrapolation would produce spurious constant
67+
deflections everywhere else).
5968
Hy
6069
The sparse first-derivative operator along y of the mask; built
6170
from the mask if not input.
@@ -69,6 +78,7 @@ def __init__(
6978
self.deflections_x = np.asarray(deflections_x)
7079
self.image_plane_grid = np.asarray(image_plane_grid)
7180
self.mask = mask
81+
self.extrapolate = extrapolate
7282
self.Hy = Hy
7383
self.Hx = Hx
7484

@@ -85,9 +95,9 @@ def _build_interpolators(self):
8595
) * 0.5
8696

8797
self.tri = Delaunay(np.fliplr(self.image_plane_grid))
88-
self.interp_defl_y = LinearNDInterpolatorExt(self.tri, self.deflections_y)
89-
self.interp_defl_x = LinearNDInterpolatorExt(self.tri, self.deflections_x)
90-
self.interp_kappa = LinearNDInterpolatorExt(self.tri, self.convergence_slim)
98+
self.interp_defl_y = LinearNDInterpolatorExt(self.tri, fill=self.extrapolate, values=self.deflections_y)
99+
self.interp_defl_x = LinearNDInterpolatorExt(self.tri, fill=self.extrapolate, values=self.deflections_x)
100+
self.interp_kappa = LinearNDInterpolatorExt(self.tri, fill=self.extrapolate, values=self.convergence_slim)
91101

92102
@aa.decorators.to_array
93103
def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):

autogalaxy/profiles/mass/input/input_potential.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def __init__(
1717
lensing_potential: np.ndarray,
1818
image_plane_grid: aa.type.Grid2DLike,
1919
mask: aa.type.Mask2D,
20+
extrapolate: str = "nearest",
2021
Hy: Optional[spmatrix] = None,
2122
Hx: Optional[spmatrix] = None,
2223
Hyy: Optional[spmatrix] = None,
@@ -54,6 +55,14 @@ def __init__(
5455
``aa.util.derivative.cleaned_mask_from``); its ``pixel_scale``
5556
sets the finite-difference step of the derived deflections and
5657
convergence.
58+
extrapolate
59+
The extrapolation behaviour outside the unmasked pixels' convex
60+
hull: ``"nearest"`` (default; the field continues beyond the
61+
grid) or ``"zero"`` (the field vanishes outside it — required
62+
when the profile represents a localized correction on a
63+
sub-region of a larger grid, e.g. an arc-restricted dpsi mesh,
64+
where nearest extrapolation would produce spurious constant
65+
deflections everywhere else).
5766
Hy
5867
The sparse first-derivative operator along y of the mask; built
5968
from the mask if not input.
@@ -72,6 +81,7 @@ def __init__(
7281
self.lensing_potential = np.asarray(lensing_potential)
7382
self.image_plane_grid = np.asarray(image_plane_grid)
7483
self.mask = mask
84+
self.extrapolate = extrapolate
7585
self.Hy = Hy
7686
self.Hx = Hx
7787
self.Hyy = Hyy
@@ -96,10 +106,10 @@ def _build_interpolators(self):
96106
) * 0.5
97107

98108
self.tri = Delaunay(np.fliplr(self.image_plane_grid))
99-
self.interp_psi = LinearNDInterpolatorExt(self.tri, self.lensing_potential)
100-
self.interp_defl_y = LinearNDInterpolatorExt(self.tri, self.deflections_y)
101-
self.interp_defl_x = LinearNDInterpolatorExt(self.tri, self.deflections_x)
102-
self.interp_kappa = LinearNDInterpolatorExt(self.tri, self.convergence_slim)
109+
self.interp_psi = LinearNDInterpolatorExt(self.tri, fill=self.extrapolate, values=self.lensing_potential)
110+
self.interp_defl_y = LinearNDInterpolatorExt(self.tri, fill=self.extrapolate, values=self.deflections_y)
111+
self.interp_defl_x = LinearNDInterpolatorExt(self.tri, fill=self.extrapolate, values=self.deflections_x)
112+
self.interp_kappa = LinearNDInterpolatorExt(self.tri, fill=self.extrapolate, values=self.convergence_slim)
103113

104114
@aa.decorators.to_array
105115
def convergence_2d_from(self, grid: aa.type.Grid2DLike, xp=np, **kwargs):

autogalaxy/profiles/mass/input/interp.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,20 @@
44

55

66
class LinearNDInterpolatorExt:
7-
def __init__(self, points, values):
7+
def __init__(self, points, values, fill: str = "nearest"):
88
"""
99
Linear interpolation over a Delaunay triangulation of scattered 2D
10-
points, falling back to nearest-neighbour interpolation outside the
11-
convex hull so extrapolated values are never NaN.
10+
points, with a choice of extrapolation behaviour outside the convex
11+
hull so extrapolated values are never NaN:
12+
13+
- ``fill="nearest"`` (default): nearest-neighbour extrapolation —
14+
appropriate when the sampled field genuinely continues beyond the
15+
hull (e.g. a source brightness evaluated slightly off-mesh).
16+
- ``fill="zero"``: zero extrapolation — appropriate when the field is
17+
only defined on the sampled region and must vanish outside it
18+
(e.g. localized potential corrections: nearest extrapolation would
19+
smear constant non-zero values — and for their deflections,
20+
spurious constant deflections — across the whole grid).
1221
1322
Ported from the ``potential_correction`` package of Cao et al. 2025
1423
(https://github.com/caoxiaoyue/lensing_potential_correction; cite via
@@ -22,13 +31,23 @@ def __init__(self, points, values):
2231
triangulation of them.
2332
values
2433
The values interpolated.
34+
fill
35+
The extrapolation behaviour outside the convex hull:
36+
``"nearest"`` or ``"zero"``.
2537
"""
38+
if fill not in ("nearest", "zero"):
39+
raise ValueError(f"fill must be 'nearest' or 'zero', got {fill!r}")
40+
self.fill = fill
2641
self.funcinterp = LinearNDInterpolator(points, values)
27-
self.funcnearest = NearestNDInterpolator(points, values)
42+
self.funcnearest = (
43+
NearestNDInterpolator(points, values) if fill == "nearest" else None
44+
)
2845

2946
def __call__(self, *args):
3047
z = self.funcinterp(*args)
3148
chk = np.isnan(z)
3249
if chk.any():
33-
return np.where(chk, self.funcnearest(*args), z)
50+
if self.fill == "nearest":
51+
return np.where(chk, self.funcnearest(*args), z)
52+
return np.where(chk, 0.0, z)
3453
return z

test_autogalaxy/profiles/mass/input/test_input_potential.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,49 @@ def test__operators_can_be_preloaded():
8080

8181
deflections = np.asarray(profile.deflections_yx_2d_from(grid=grid))
8282
assert deflections[:, 0] == pytest.approx(1.0, abs=1.0e-8)
83+
84+
85+
def test__zero_extrapolation__deflections_vanish_outside_mesh():
86+
mask, grid = masked_setup()
87+
grid_arr = np.asarray(grid)
88+
potential = 2.0 * grid_arr[:, 0] + 3.0 * grid_arr[:, 1]
89+
90+
profile_nearest = ag.mp.InputPotential(
91+
lensing_potential=potential, image_plane_grid=grid_arr, mask=mask
92+
)
93+
profile_zero = ag.mp.InputPotential(
94+
lensing_potential=potential,
95+
image_plane_grid=grid_arr,
96+
mask=mask,
97+
extrapolate="zero",
98+
)
99+
100+
far_outside = aa.Grid2DIrregular(values=[(6.0, 6.0), (-7.0, 5.0)])
101+
102+
deflections_nearest = np.asarray(
103+
profile_nearest.deflections_yx_2d_from(grid=far_outside)
104+
)
105+
deflections_zero = np.asarray(profile_zero.deflections_yx_2d_from(grid=far_outside))
106+
107+
# nearest extrapolation smears constant non-zero deflections outward;
108+
# zero extrapolation vanishes
109+
assert not np.allclose(deflections_nearest, 0.0)
110+
assert deflections_zero == pytest.approx(0.0, abs=1.0e-12)
111+
112+
# inside the mesh the two modes agree exactly
113+
inside = aa.Grid2DIrregular(values=[(0.1, -0.2), (0.7, 0.4)])
114+
assert np.asarray(profile_zero.deflections_yx_2d_from(grid=inside)) == pytest.approx(
115+
np.asarray(profile_nearest.deflections_yx_2d_from(grid=inside)), abs=1.0e-12
116+
)
117+
118+
119+
def test__invalid_extrapolate_raises():
120+
mask, grid = masked_setup()
121+
grid_arr = np.asarray(grid)
122+
with pytest.raises(ValueError):
123+
ag.mp.InputPotential(
124+
lensing_potential=np.ones(grid_arr.shape[0]),
125+
image_plane_grid=grid_arr,
126+
mask=mask,
127+
extrapolate="taper",
128+
)

0 commit comments

Comments
 (0)