|
1 | 1 | from __future__ import annotations |
| 2 | +from collections import defaultdict |
2 | 3 | import numpy as np |
3 | 4 | from typing import TYPE_CHECKING, Union |
4 | 5 | from typing import List, Tuple |
|
8 | 9 | if TYPE_CHECKING: |
9 | 10 | from autoarray.structures.grids.uniform_2d import Grid2D |
10 | 11 |
|
11 | | -from autoarray.geometry import geometry_util |
12 | 12 | from autoarray.mask.mask_2d import Mask2D |
13 | 13 |
|
14 | 14 | from autoarray import numba_util |
@@ -49,7 +49,6 @@ def over_sample_size_convert_to_array_2d_from( |
49 | 49 | return Array2D(values=np.array(over_sample_size).astype("int"), mask=mask) |
50 | 50 |
|
51 | 51 |
|
52 | | -@numba_util.jit() |
53 | 52 | def total_sub_pixels_2d_from(sub_size: np.ndarray) -> int: |
54 | 53 | """ |
55 | 54 | 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( |
115 | 114 | n_unmasked = unmasked_indices.shape[0] |
116 | 115 |
|
117 | 116 | # Step 2: Compute total number of sub-pixels |
118 | | - sub_pixels_per_pixel = sub_size ** 2 |
| 117 | + sub_pixels_per_pixel = sub_size**2 |
119 | 118 |
|
120 | 119 | # Step 3: Repeat slim indices for each sub-pixel |
121 | 120 | slim_indices = np.arange(n_unmasked) |
122 | 121 | slim_index_for_sub_slim_index = np.repeat(slim_indices, sub_pixels_per_pixel) |
123 | 122 |
|
124 | 123 | return slim_index_for_sub_slim_index |
125 | 124 |
|
| 125 | + |
126 | 126 | def sub_size_radial_bins_from( |
127 | 127 | radial_grid: np.ndarray, |
128 | 128 | sub_size_list: np.ndarray, |
@@ -169,7 +169,6 @@ def sub_size_radial_bins_from( |
169 | 169 | return sub_size_list[bin_indices] |
170 | 170 |
|
171 | 171 |
|
172 | | -# @numba_util.jit() |
173 | 172 | def grid_2d_slim_over_sampled_via_mask_from( |
174 | 173 | mask_2d: np.ndarray, |
175 | 174 | pixel_scales: ty.PixelScales, |
@@ -215,120 +214,58 @@ def grid_2d_slim_over_sampled_via_mask_from( |
215 | 214 | 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)) |
216 | 215 | """ |
217 | 216 |
|
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 |
332 | 269 |
|
333 | 270 |
|
334 | 271 | def over_sample_size_via_radial_bins_from( |
|
0 commit comments