Skip to content

Commit 326dbf5

Browse files
authored
Merge pull request #208 from Jammy2211/feature/border_relocator_via_ellipse
Feature/border relocator via ellipse
2 parents 7f921f1 + 6a0fd8d commit 326dbf5

File tree

8 files changed

+118
-179
lines changed

8 files changed

+118
-179
lines changed

autoarray/dataset/grids.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ def __init__(
1717
over_sample_size_lp: Union[int, Array2D],
1818
over_sample_size_pixelization: Union[int, Array2D],
1919
psf: Optional[Kernel2D] = None,
20-
use_sparse_operator: bool = False,
2120
):
2221
"""
2322
Contains grids of (y,x) Cartesian coordinates at the centre of every pixel in the dataset's image and
@@ -66,8 +65,6 @@ def __init__(
6665
self._blurring = None
6766
self._border_relocator = None
6867

69-
self.use_sparse_operator = use_sparse_operator
70-
7168
@property
7269
def lp(self):
7370

@@ -120,7 +117,6 @@ def border_relocator(self) -> BorderRelocator:
120117
self._border_relocator = BorderRelocator(
121118
mask=self.mask,
122119
sub_size=self.over_sample_size_pixelization,
123-
use_sparse_operator=self.use_sparse_operator,
124120
)
125121

126122
return self._border_relocator

autoarray/dataset/imaging/dataset.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,11 @@ def __init__(
192192
if psf.mask.shape[0] % 2 == 0 or psf.mask.shape[1] % 2 == 0:
193193
raise exc.KernelException("Kernel2D Kernel2D must be odd")
194194

195-
use_sparse_operator = True if sparse_operator is not None else False
196-
197195
self.grids = GridsDataset(
198196
mask=self.data.mask,
199197
over_sample_size_lp=self.over_sample_size_lp,
200198
over_sample_size_pixelization=self.over_sample_size_pixelization,
201199
psf=self.psf,
202-
use_sparse_operator=use_sparse_operator,
203200
)
204201

205202
self.sparse_operator = sparse_operator

autoarray/dataset/interferometer/dataset.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,10 @@ def __init__(
9696
real_space_mask=real_space_mask,
9797
)
9898

99-
use_sparse_operator = True if sparse_operator is not None else False
100-
10199
self.grids = GridsDataset(
102100
mask=self.real_space_mask,
103101
over_sample_size_lp=self.over_sample_size_lp,
104102
over_sample_size_pixelization=self.over_sample_size_pixelization,
105-
use_sparse_operator=use_sparse_operator,
106103
)
107104

108105
self.sparse_operator = sparse_operator

autoarray/inversion/inversion/imaging_numba/inversion_imaging_numba_util.py

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -803,74 +803,6 @@ def mapped_reconstructed_data_via_image_to_pix_unique_from(
803803
return mapped_reconstructed_data
804804

805805

806-
@numba_util.jit()
807-
def relocated_grid_via_jit_from(grid, border_grid):
808-
"""
809-
Relocate the coordinates of a grid to its border if they are outside the border, where the border is
810-
defined as all pixels at the edge of the grid's mask (see *mask._border_1d_indexes*).
811-
812-
This is performed as follows:
813-
814-
1: Use the mean value of the grid's y and x coordinates to determine the origin of the grid.
815-
2: Compute the radial distance of every grid coordinate from the origin.
816-
3: For every coordinate, find its nearest pixel in the border.
817-
4: Determine if it is outside the border, by comparing its radial distance from the origin to its paired
818-
border pixel's radial distance.
819-
5: If its radial distance is larger, use the ratio of radial distances to move the coordinate to the
820-
border (if its inside the border, do nothing).
821-
822-
The method can be used on uniform or irregular grids, however for irregular grids the border of the
823-
'image-plane' mask is used to define border pixels.
824-
825-
Parameters
826-
----------
827-
grid
828-
The grid (uniform or irregular) whose pixels are to be relocated to the border edge if outside it.
829-
border_grid : Grid2D
830-
The grid of border (y,x) coordinates.
831-
"""
832-
833-
grid_relocated = np.zeros(grid.shape)
834-
grid_relocated[:, :] = grid[:, :]
835-
836-
border_origin = np.zeros(2)
837-
border_origin[0] = np.mean(border_grid[:, 0])
838-
border_origin[1] = np.mean(border_grid[:, 1])
839-
border_grid_radii = np.sqrt(
840-
np.add(
841-
np.square(np.subtract(border_grid[:, 0], border_origin[0])),
842-
np.square(np.subtract(border_grid[:, 1], border_origin[1])),
843-
)
844-
)
845-
border_min_radii = np.min(border_grid_radii)
846-
847-
grid_radii = np.sqrt(
848-
np.add(
849-
np.square(np.subtract(grid[:, 0], border_origin[0])),
850-
np.square(np.subtract(grid[:, 1], border_origin[1])),
851-
)
852-
)
853-
854-
for pixel_index in range(grid.shape[0]):
855-
if grid_radii[pixel_index] > border_min_radii:
856-
closest_pixel_index = np.argmin(
857-
np.square(grid[pixel_index, 0] - border_grid[:, 0])
858-
+ np.square(grid[pixel_index, 1] - border_grid[:, 1])
859-
)
860-
861-
move_factor = (
862-
border_grid_radii[closest_pixel_index] / grid_radii[pixel_index]
863-
)
864-
865-
if move_factor < 1.0:
866-
grid_relocated[pixel_index, :] = (
867-
move_factor * (grid[pixel_index, :] - border_origin[:])
868-
+ border_origin[:]
869-
)
870-
871-
return grid_relocated
872-
873-
874806
class SparseLinAlgImagingNumba:
875807
def __init__(
876808
self,

autoarray/inversion/pixelization/border_relocator.py

Lines changed: 107 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -203,76 +203,122 @@ def sub_border_slim_from(mask, sub_size):
203203
).astype("int")
204204

205205

206-
def relocated_grid_from(grid, border_grid, xp=np):
206+
def ellipse_params_via_border_pca_from(border_grid, xp=np, eps=1e-12):
207207
"""
208-
Relocate the coordinates of a grid to its border if they are outside the border, where the border is
209-
defined as all pixels at the edge of the grid's mask (see *mask._border_1d_indexes*).
208+
Estimate origin, semi-axes (a,b), and rotation phi from border points using PCA.
209+
Works well for circle/ellipse-like borders.
210210
211-
This is performed as follows:
211+
Parameters
212+
----------
213+
border_grid : (M,2)
214+
Border coordinates in (y, x) order.
215+
xp : module
216+
numpy-like module (np, jnp, cupy, etc.).
217+
eps : float
218+
Numerical safety epsilon.
219+
220+
Returns
221+
-------
222+
origin : (2,)
223+
Estimated center (y0, x0).
224+
a : float
225+
Semi-major axis.
226+
b : float
227+
Semi-minor axis.
228+
phi : float
229+
Rotation angle in radians.
230+
"""
231+
232+
origin = xp.mean(border_grid, axis=0)
233+
234+
dy = border_grid[:, 0] - origin[0]
235+
dx = border_grid[:, 1] - origin[1]
236+
237+
# Build data matrix in (x, y) order for PCA
238+
X = xp.stack([dx, dy], axis=1) # (M,2)
239+
240+
# Covariance matrix
241+
denom = xp.maximum(X.shape[0] - 1, 1)
242+
C = (X.T @ X) / denom # (2,2)
243+
244+
# Eigen-decomposition (ascending eigenvalues)
245+
evals, evecs = xp.linalg.eigh(C)
246+
247+
# Major axis = eigenvector with largest eigenvalue
248+
v_major = evecs[:, -1] # (2,) in (x,y)
249+
250+
phi = xp.arctan2(v_major[1], v_major[0])
251+
252+
# Rotate border points into ellipse-aligned frame
253+
c = xp.cos(phi)
254+
s = xp.sin(phi)
255+
256+
xprime = c * dx + s * dy
257+
yprime = -s * dx + c * dy
258+
259+
# Semi-axes from maximal extent
260+
a = xp.max(xp.abs(xprime)) + eps
261+
b = xp.max(xp.abs(yprime)) + eps
212262

213-
1: Use the mean value of the grid's y and x coordinates to determine the origin of the grid.
214-
2: Compute the radial distance of every grid coordinate from the origin.
215-
3: For every coordinate, find its nearest pixel in the border.
216-
4: Determine if it is outside the border, by comparing its radial distance from the origin to its paired
217-
border pixel's radial distance.
218-
5: If its radial distance is larger, use the ratio of radial distances to move the coordinate to the
219-
border (if its inside the border, do nothing).
263+
return origin, a, b, phi
220264

221-
The method can be used on uniform or irregular grids, however for irregular grids the border of the
222-
'image-plane' mask is used to define border pixels.
265+
266+
def relocated_grid_via_ellipse_border_from(grid, origin, a, b, phi, xp=np, eps=1e-12):
267+
"""
268+
Rotated ellipse centered at origin with semi-axes a (major, x'), b (minor, y'),
269+
rotated by phi radians (counterclockwise).
223270
224271
Parameters
225272
----------
226-
grid
227-
The grid (uniform or irregular) whose pixels are to be relocated to the border edge if outside it.
228-
border_grid : Grid2D
229-
The grid of border (y,x) coordinates.
273+
grid : (N,2)
274+
Coordinates in (y, x) order.
275+
origin : (2,)
276+
Ellipse center (y0, x0).
277+
a, b : float
278+
Semi-major and semi-minor axes.
279+
phi : float
280+
Rotation angle in radians.
281+
xp : module
282+
numpy-like module (np, jnp, cupy, etc.).
283+
eps : float
284+
Numerical safety epsilon.
230285
"""
231286

232-
# Compute origin (center) of the border grid
233-
border_origin = xp.mean(border_grid, axis=0)
287+
# shift to origin
288+
dy = grid[:, 0] - origin[0]
289+
dx = grid[:, 1] - origin[1]
234290

235-
# Radii from origin
236-
grid_radii = xp.linalg.norm(grid - border_origin, axis=1) # (N,)
237-
border_radii = xp.linalg.norm(border_grid - border_origin, axis=1) # (M,)
238-
border_min_radius = xp.min(border_radii)
291+
c = xp.cos(phi)
292+
s = xp.sin(phi)
239293

240-
# Determine which points are outside
241-
outside_mask = grid_radii > border_min_radius # (N,)
294+
# rotate into ellipse-aligned frame
295+
xprime = c * dx + s * dy
296+
yprime = -s * dx + c * dy
242297

243-
# To compute nearest border point for each grid point, we must do it for all and then mask later
244-
# Compute all distances: (N, M)
245-
diffs = grid[:, None, :] - border_grid[None, :, :] # (N, M, 2)
246-
dists_squared = xp.sum(diffs**2, axis=2) # (N, M)
247-
closest_indices = xp.argmin(dists_squared, axis=1) # (N,)
298+
# ellipse radius in normalized coords
299+
q = (xprime / a) ** 2 + (yprime / b) ** 2
248300

249-
# Get border radius for closest border point to each grid point
250-
matched_border_radii = border_radii[closest_indices] # (N,)
301+
outside = q > 1.0
302+
scale = 1.0 / xp.sqrt(xp.maximum(q, 1.0 + eps))
251303

252-
# Ratio of border to grid radius
253-
move_factors = matched_border_radii / grid_radii # (N,)
304+
# scale back to boundary
305+
xprime2 = xprime * scale
306+
yprime2 = yprime * scale
254307

255-
# Only move if:
256-
# - the point is outside the border
257-
# - the matched border point is closer to the origin (i.e. move_factor < 1)
258-
apply_move = xp.logical_and(outside_mask, move_factors < 1.0) # (N,)
308+
# rotate back to original frame
309+
dx2 = c * xprime2 - s * yprime2
310+
dy2 = s * xprime2 + c * yprime2
259311

260-
# Compute moved positions (for all points, but will select with mask)
261-
direction_vectors = grid - border_origin # (N, 2)
262-
moved_grid = move_factors[:, None] * direction_vectors + border_origin # (N, 2)
312+
moved = xp.stack([origin[0] + dy2, origin[1] + dx2], axis=1)
263313

264-
# Select which grid points to move
265-
relocated_grid = xp.where(apply_move[:, None], moved_grid, grid) # (N, 2)
266-
267-
return relocated_grid
314+
return xp.where(outside[:, None], moved, grid)
268315

269316

270317
class BorderRelocator:
271318
def __init__(
272319
self,
273320
mask: Mask2D,
274321
sub_size: Union[int, Array2D],
275-
use_sparse_operator: bool = False,
276322
):
277323
"""
278324
Relocates source plane coordinates that trace outside the mask’s border in the source-plane back onto the
@@ -330,8 +376,6 @@ def __init__(
330376

331377
self.sub_border_grid = sub_grid[self.sub_border_slim]
332378

333-
self.use_sparse_operator = use_sparse_operator
334-
335379
def relocated_grid_from(self, grid: Grid2D, xp=np) -> Grid2D:
336380
"""
337381
Relocate the coordinates of a grid to the border of this grid if they are outside the border, where the
@@ -359,32 +403,17 @@ def relocated_grid_from(self, grid: Grid2D, xp=np) -> Grid2D:
359403
if len(self.sub_border_grid) == 0:
360404
return grid
361405

362-
if self.use_sparse_operator is False or xp.__name__.startswith("jax"):
363-
364-
values = relocated_grid_from(
365-
grid=grid.array, border_grid=grid.array[self.border_slim], xp=xp
366-
)
367-
368-
over_sampled = relocated_grid_from(
369-
grid=grid.over_sampled.array,
370-
border_grid=grid.over_sampled.array[self.sub_border_slim],
371-
xp=xp,
372-
)
373-
374-
else:
375-
376-
from autoarray.inversion.inversion.imaging_numba import (
377-
inversion_imaging_numba_util,
378-
)
406+
origin, a, b, phi = ellipse_params_via_border_pca_from(
407+
grid.array[self.border_slim], xp=xp
408+
)
379409

380-
values = inversion_imaging_numba_util.relocated_grid_via_jit_from(
381-
grid=grid.array, border_grid=grid[self.border_slim]
382-
)
410+
values = relocated_grid_via_ellipse_border_from(
411+
grid=grid.array, origin=origin, a=a, b=b, phi=phi, xp=xp
412+
)
383413

384-
over_sampled = inversion_imaging_numba_util.relocated_grid_via_jit_from(
385-
grid=grid.over_sampled.array,
386-
border_grid=grid.over_sampled[self.sub_border_slim],
387-
)
414+
over_sampled = relocated_grid_via_ellipse_border_from(
415+
grid=grid.over_sampled.array, origin=origin, a=a, b=b, phi=phi, xp=xp
416+
)
388417

389418
return Grid2D(
390419
values=values,
@@ -411,24 +440,13 @@ def relocated_mesh_grid_from(
411440
if len(self.sub_border_grid) == 0:
412441
return mesh_grid
413442

414-
if self.use_sparse_operator is False or xp.__name__.startswith("jax"):
415-
416-
relocated_grid = relocated_grid_from(
417-
grid=mesh_grid.array,
418-
border_grid=grid[self.sub_border_slim],
419-
xp=xp,
420-
)
421-
422-
else:
423-
424-
from autoarray.inversion.inversion.imaging import (
425-
inversion_imaging_numba_util,
426-
)
443+
origin, a, b, phi = ellipse_params_via_border_pca_from(
444+
grid.array[self.border_slim], xp=xp
445+
)
427446

428-
relocated_grid = inversion_imaging_numba_util.relocated_grid_via_jit_from(
429-
grid=mesh_grid.array,
430-
border_grid=grid[self.sub_border_slim],
431-
)
447+
relocated_grid = relocated_grid_via_ellipse_border_from(
448+
grid=mesh_grid.array, origin=origin, a=a, b=b, phi=phi, xp=xp
449+
)
432450

433451
return Grid2DIrregular(
434452
values=relocated_grid,

0 commit comments

Comments
 (0)