Skip to content

Commit 650c8a0

Browse files
Jammy2211Jammy2211
authored andcommitted
merge
2 parents 5c41ba1 + 8fca9ba commit 650c8a0

File tree

11 files changed

+111
-212
lines changed

11 files changed

+111
-212
lines changed

autoarray/geometry/geometry_util.py

Lines changed: 2 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
import numpy as np
33
from typing import Tuple, Union
44

5-
6-
from autoarray import numba_util
75
from autoarray import type as ty
86

97

@@ -178,70 +176,6 @@ def convert_pixel_scales_2d(pixel_scales: ty.PixelScales) -> Tuple[float, float]
178176
return pixel_scales
179177

180178

181-
@numba_util.jit()
182-
def central_pixel_coordinates_2d_numba_from(
183-
shape_native: Tuple[int, int],
184-
) -> Tuple[float, float]:
185-
"""
186-
Returns the central pixel coordinates of a 2D geometry (and therefore a 2D data structure like an ``Array2D``)
187-
from the shape of that data structure.
188-
189-
Examples of the central pixels are as follows:
190-
191-
- For a 3x3 image, the central pixel is pixel [1, 1].
192-
- For a 4x4 image, the central pixel is [1.5, 1.5].
193-
194-
Parameters
195-
----------
196-
shape_native
197-
The dimensions of the data structure, which can be in 1D, 2D or higher dimensions.
198-
199-
Returns
200-
-------
201-
The central pixel coordinates of the data structure.
202-
"""
203-
return (float(shape_native[0] - 1) / 2, float(shape_native[1] - 1) / 2)
204-
205-
206-
@numba_util.jit()
207-
def central_scaled_coordinate_2d_numba_from(
208-
shape_native: Tuple[int, int],
209-
pixel_scales: ty.PixelScales,
210-
origin: Tuple[float, float] = (0.0, 0.0),
211-
) -> Tuple[float, float]:
212-
"""
213-
Returns the central scaled coordinates of a 2D geometry (and therefore a 2D data structure like an ``Array2D``)
214-
from the shape of that data structure.
215-
216-
This is computed by using the data structure's shape and converting it to scaled units using an input
217-
pixel-coordinates to scaled-coordinate conversion factor `pixel_scales`.
218-
219-
The origin of the scaled grid can also be input and moved from (0.0, 0.0).
220-
221-
Parameters
222-
----------
223-
shape_native
224-
The 2D shape of the data structure whose central scaled coordinates are computed.
225-
pixel_scales
226-
The (y,x) scaled units to pixel units conversion factor of the 2D data structure.
227-
origin
228-
The (y,x) scaled units origin of the coordinate system the central scaled coordinate is computed on.
229-
230-
Returns
231-
-------
232-
The central coordinates of the 2D data structure in scaled units.
233-
"""
234-
235-
central_pixel_coordinates = central_pixel_coordinates_2d_numba_from(
236-
shape_native=shape_native
237-
)
238-
239-
y_pixel = central_pixel_coordinates[0] + (origin[0] / pixel_scales[0])
240-
x_pixel = central_pixel_coordinates[1] - (origin[1] / pixel_scales[1])
241-
242-
return (y_pixel, x_pixel)
243-
244-
245179
def central_pixel_coordinates_2d_from(
246180
shape_native: Tuple[int, int],
247181
) -> Tuple[float, float]:
@@ -294,7 +228,7 @@ def central_scaled_coordinate_2d_from(
294228
The central coordinates of the 2D data structure in scaled units.
295229
"""
296230

297-
central_pixel_coordinates = central_pixel_coordinates_2d_numba_from(
231+
central_pixel_coordinates = central_pixel_coordinates_2d_from(
298232
shape_native=shape_native
299233
)
300234

@@ -367,7 +301,6 @@ def pixel_coordinates_2d_from(
367301
return (y_pixel, x_pixel)
368302

369303

370-
@numba_util.jit()
371304
def scaled_coordinates_2d_from(
372305
pixel_coordinates_2d: Tuple[float, float],
373306
shape_native: Tuple[int, int],
@@ -411,7 +344,7 @@ def scaled_coordinates_2d_from(
411344
origin=(0.0, 0.0)
412345
)
413346
"""
414-
central_scaled_coordinates = central_scaled_coordinate_2d_numba_from(
347+
central_scaled_coordinates = central_scaled_coordinate_2d_from(
415348
shape_native=shape_native, pixel_scales=pixel_scales, origin=origins
416349
)
417350

autoarray/inversion/inversion/abstract.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ def __init__(
7474
A dictionary which contains timing of certain functions calls which is used for profiling.
7575
"""
7676

77+
try:
78+
import numba
79+
except ModuleNotFoundError:
80+
raise exc.InversionException(
81+
"Inversion functionality (linear light profiles, pixelized reconstructions) is "
82+
"disabled if numba is not installed.\n\n"
83+
"This is because the run-times without numba are too slow.\n\n"
84+
"Please install numba, which is described at the following web page:\n\n"
85+
"https://pyautolens.readthedocs.io/en/latest/installation/overview.html"
86+
)
87+
88+
7789
self.dataset = dataset
7890

7991
self.linear_obj_list = linear_obj_list

autoarray/operators/over_sampling/over_sample_util.py

Lines changed: 55 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import annotations
2+
from collections import defaultdict
23
import numpy as np
34
from typing import TYPE_CHECKING, Union
45
from typing import List, Tuple
@@ -8,7 +9,6 @@
89
if TYPE_CHECKING:
910
from autoarray.structures.grids.uniform_2d import Grid2D
1011

11-
from autoarray.geometry import geometry_util
1212
from autoarray.mask.mask_2d import Mask2D
1313

1414
from autoarray import numba_util
@@ -49,7 +49,6 @@ def over_sample_size_convert_to_array_2d_from(
4949
return Array2D(values=np.array(over_sample_size).astype("int"), mask=mask)
5050

5151

52-
@numba_util.jit()
5352
def total_sub_pixels_2d_from(sub_size: np.ndarray) -> int:
5453
"""
5554
Returns the total number of sub-pixels in unmasked pixels in a mask.
@@ -115,14 +114,15 @@ def slim_index_for_sub_slim_index_via_mask_2d_from(
115114
n_unmasked = unmasked_indices.shape[0]
116115

117116
# Step 2: Compute total number of sub-pixels
118-
sub_pixels_per_pixel = sub_size ** 2
117+
sub_pixels_per_pixel = sub_size**2
119118

120119
# Step 3: Repeat slim indices for each sub-pixel
121120
slim_indices = np.arange(n_unmasked)
122121
slim_index_for_sub_slim_index = np.repeat(slim_indices, sub_pixels_per_pixel)
123122

124123
return slim_index_for_sub_slim_index
125124

125+
126126
def sub_size_radial_bins_from(
127127
radial_grid: np.ndarray,
128128
sub_size_list: np.ndarray,
@@ -169,7 +169,6 @@ def sub_size_radial_bins_from(
169169
return sub_size_list[bin_indices]
170170

171171

172-
# @numba_util.jit()
173172
def grid_2d_slim_over_sampled_via_mask_from(
174173
mask_2d: np.ndarray,
175174
pixel_scales: ty.PixelScales,
@@ -215,120 +214,58 @@ def grid_2d_slim_over_sampled_via_mask_from(
215214
grid_slim = grid_2d_slim_over_sampled_via_mask_from(mask=mask, pixel_scales=(0.5, 0.5), sub_size=1, origin=(0.0, 0.0))
216215
"""
217216

218-
total_sub_pixels = np.sum(sub_size**2)
219-
220-
grid_slim = np.zeros(shape=(total_sub_pixels, 2))
221-
222-
centres_scaled = geometry_util.central_scaled_coordinate_2d_numba_from(
223-
shape_native=mask_2d.shape, pixel_scales=pixel_scales, origin=origin
224-
)
225-
226-
index = 0
227-
sub_index = 0
228-
229-
for y in range(mask_2d.shape[0]):
230-
for x in range(mask_2d.shape[1]):
231-
if not mask_2d[y, x]:
232-
sub = sub_size[index]
233-
234-
y_sub_half = pixel_scales[0] / 2
235-
y_sub_step = pixel_scales[0] / (sub)
236-
237-
x_sub_half = pixel_scales[1] / 2
238-
x_sub_step = pixel_scales[1] / (sub)
239-
240-
y_scaled = (y - centres_scaled[0]) * pixel_scales[0]
241-
x_scaled = (x - centres_scaled[1]) * pixel_scales[1]
242-
243-
for y1 in range(sub):
244-
for x1 in range(sub):
245-
grid_slim[sub_index, 0] = -(
246-
y_scaled - y_sub_half + y1 * y_sub_step + (y_sub_step / 2.0)
247-
)
248-
grid_slim[sub_index, 1] = (
249-
x_scaled - x_sub_half + x1 * x_sub_step + (x_sub_step / 2.0)
250-
)
251-
sub_index += 1
252-
253-
index += 1
254-
255-
return grid_slim
256-
257-
258-
@numba_util.jit()
259-
def binned_array_2d_from(
260-
array_2d: np.ndarray,
261-
mask_2d: np.ndarray,
262-
sub_size: np.ndarray,
263-
) -> np.ndarray:
264-
"""
265-
For a sub-grid, every unmasked pixel of its 2D mask with shape (total_y_pixels, total_x_pixels) is divided into
266-
a finer uniform grid of shape (total_y_pixels*sub_size, total_x_pixels*sub_size). This routine computes the (y,x)
267-
scaled coordinates a the centre of every sub-pixel defined by this 2D mask array.
268-
269-
The sub-grid is returned on an array of shape (total_unmasked_pixels*sub_size**2, 2). y coordinates are
270-
stored in the 0 index of the second dimension, x coordinates in the 1 index. Masked coordinates are therefore
271-
removed and not included in the slimmed grid.
272-
273-
Grid2D are defined from the top-left corner, where the first unmasked sub-pixel corresponds to index 0.
274-
Sub-pixels that are part of the same mask array pixel are indexed next to one another, such that the second
275-
sub-pixel in the first pixel has index 1, its next sub-pixel has index 2, and so forth.
276-
277-
Parameters
278-
----------
279-
mask_2d
280-
A 2D array of bools, where `False` values are unmasked and therefore included as part of the calculated
281-
sub-grid.
282-
pixel_scales
283-
The (y,x) scaled units to pixel units conversion factor of the 2D mask array.
284-
sub_size
285-
The size of the sub-grid that each pixel of the 2D mask array is divided into.
286-
origin
287-
The (y,x) origin of the 2D array, which the sub-grid is shifted around.
288-
289-
Returns
290-
-------
291-
ndarray
292-
A slimmed sub grid of (y,x) scaled coordinates at the centre of every pixel unmasked pixel on the 2D mask
293-
array. The sub grid array has dimensions (total_unmasked_pixels*sub_size**2, 2).
294-
295-
Examples
296-
--------
297-
mask = np.array([[True, False, True],
298-
[False, False, False]
299-
[True, False, True]])
300-
grid_slim = grid_2d_slim_over_sampled_via_mask_from(mask=mask, pixel_scales=(0.5, 0.5), sub_size=1, origin=(0.0, 0.0))
301-
"""
302-
303-
total_pixels = np.sum(~mask_2d)
304-
305-
sub_fraction = 1.0 / sub_size**2
306-
307-
binned_array_2d_slim = np.zeros(shape=total_pixels)
308-
309-
index = 0
310-
sub_index = 0
311-
312-
for y in range(mask_2d.shape[0]):
313-
for x in range(mask_2d.shape[1]):
314-
if not mask_2d[y, x]:
315-
sub = sub_size[index]
316-
317-
for y1 in range(sub):
318-
for x1 in range(sub):
319-
# if use_jax:
320-
# binned_array_2d_slim = binned_array_2d_slim.at[index].add(
321-
# array_2d[sub_index] * sub_fraction[index]
322-
# )
323-
# else:
324-
binned_array_2d_slim[index] += (
325-
array_2d[sub_index] * sub_fraction[index]
326-
)
327-
sub_index += 1
328-
329-
index += 1
330-
331-
return binned_array_2d_slim
217+
H, W = mask_2d.shape
218+
sy, sx = pixel_scales
219+
oy, ox = origin
220+
221+
# 1) Find unmasked pixels in row-major order
222+
rows, cols = np.nonzero(~mask_2d)
223+
Npix = rows.size
224+
225+
# 2) Normalize sub_size input
226+
sub_arr = np.asarray(sub_size)
227+
sub_arr = np.full(Npix, sub_arr, dtype=int) if sub_arr.size == 1 else sub_arr
228+
229+
# 3) Pixel centers in physical coords, y↑up
230+
cy = (H - 1) / 2.0
231+
cx = (W - 1) / 2.0
232+
y_pix = (cy - rows) * sy + oy
233+
x_pix = (cols - cx) * sx + ox
234+
235+
# Pre‐group pixel indices by sub_size
236+
groups = defaultdict(list)
237+
for i, s in enumerate(sub_arr):
238+
groups[s].append(i)
239+
240+
# Prepare output
241+
total = np.sum(sub_arr * sub_arr)
242+
coords = np.empty((total, 2), float)
243+
idx = 0
244+
245+
for s, pix_indices in groups.items():
246+
# Compute offsets once for this sub_size
247+
dy, dx = sy / s, sx / s
248+
y_off = np.linspace(+sy / 2 - dy / 2, -sy / 2 + dy / 2, s)
249+
x_off = np.linspace(-sx / 2 + dx / 2, +sx / 2 - dx / 2, s)
250+
y_sub, x_sub = np.meshgrid(y_off, x_off, indexing="ij")
251+
y_sub = y_sub.ravel()
252+
x_sub = x_sub.ravel()
253+
n_sub = s * s
254+
255+
# Now vectorize over all pixels in this group
256+
pix_idx = np.array(pix_indices)
257+
y_centers = y_pix[pix_idx]
258+
x_centers = x_pix[pix_idx]
259+
260+
# Repeat‐tile to shape (len(pix_idx)*n_sub,)
261+
all_y = np.repeat(y_centers, n_sub) + np.tile(y_sub, len(pix_idx))
262+
all_x = np.repeat(x_centers, n_sub) + np.tile(x_sub, len(pix_idx))
263+
264+
coords[idx : idx + all_y.size, 0] = all_y
265+
coords[idx : idx + all_x.size, 1] = all_x
266+
idx += all_y.size
267+
268+
return coords
332269

333270

334271
def over_sample_size_via_radial_bins_from(

autoarray/operators/over_sampling/over_sampler.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,20 @@ def binned_array_2d_from(self, array: Array2D) -> "Array2D":
230230
In **PyAutoCTI** all `Array2D` objects are used in their `native` representation without sub-gridding.
231231
Significant memory can be saved by only store this format, thus the `native_binned_only` config override
232232
can force this behaviour. It is recommended users do not use this option to avoid unexpected behaviour.
233+
234+
Old docstring:
235+
236+
For a sub-grid, every unmasked pixel of its 2D mask with shape (total_y_pixels, total_x_pixels) is divided into
237+
a finer uniform grid of shape (total_y_pixels*sub_size, total_x_pixels*sub_size). This routine computes the (y,x)
238+
scaled coordinates a the centre of every sub-pixel defined by this 2D mask array.
239+
240+
The sub-grid is returned on an array of shape (total_unmasked_pixels*sub_size**2, 2). y coordinates are
241+
stored in the 0 index of the second dimension, x coordinates in the 1 index. Masked coordinates are therefore
242+
removed and not included in the slimmed grid.
243+
244+
Grid2D are defined from the top-left corner, where the first unmasked sub-pixel corresponds to index 0.
245+
Sub-pixels that are part of the same mask array pixel are indexed next to one another, such that the second
246+
sub-pixel in the first pixel has index 1, its next sub-pixel has index 2, and so forth.
233247
"""
234248
if conf.instance["general"]["structures"]["native_binned_only"]:
235249
return self
@@ -258,7 +272,6 @@ def binned_array_2d_from(self, array: Array2D) -> "Array2D":
258272
mask=self.mask,
259273
)
260274

261-
262275
@cached_property
263276
def slim_for_sub_slim(self) -> np.ndarray:
264277
"""

0 commit comments

Comments
 (0)