Skip to content

Commit 3343559

Browse files
authored
Merge pull request #649 from PyAutoLabs/feature/imaging-sparse-triplets-masking
fix: mask invalid mapper triplets on the imaging sparse path; backend-dispatched scatter-add helpers
2 parents 034a616 + dd26822 commit 3343559

7 files changed

Lines changed: 473 additions & 19 deletions

File tree

‎autoarray/inversion/inversion/imaging/inversion_imaging_util.py‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def data_vector_via_psf_weighted_data_from(
7373
cols: np.ndarray, # (nnz,) int32 source pixel index
7474
vals: np.ndarray, # (nnz,) float64 mapping weights incl sub_fraction
7575
S: int, # number of source pixels
76+
xp=np,
7677
) -> np.ndarray:
7778
r"""
7879
Returns the data vector `D` from the `psf_weighted_data` matrix (see `psf_weighted_data_from`), which encodes the
@@ -96,7 +97,16 @@ def data_vector_via_psf_weighted_data_from(
9697
The values of the sparse mapping matrix triplet representation, which map the image pixels to source pixels.
9798
S
9899
The number of source pixels.
100+
xp
101+
The array module: `np` scatter-adds with `np.bincount` (no JAX import, so this runs without
102+
JAX installed); `jax.numpy` uses `jax.ops.segment_sum`. The triplets must be masked (see
103+
`mapper_util.sparse_triplets_from(pix_sizes_for_sub=...)`) so every column is non-negative.
99104
"""
105+
if xp is np:
106+
w = np.asarray(psf_weighted_data)[np.asarray(rows)] # (nnz,)
107+
contrib = np.asarray(vals) * w # (nnz,)
108+
return np.bincount(np.asarray(cols), weights=contrib, minlength=S)[:S] # (S,)
109+
100110
from jax.ops import segment_sum
101111

102112
w = psf_weighted_data[rows] # (nnz,)
@@ -284,7 +294,32 @@ def mapped_reconstructed_operated_data_via_sparse_operator_from(
284294
vals, # (nnz,)
285295
fft_index_for_masked_pixel,
286296
data_shape: int, # y_shape * x_shape
297+
xp=np,
287298
):
299+
"""
300+
Returns the slim image `A s` of a reconstruction `s` mapped through a mapper's sparse mapping
301+
operator `A`, whose COO triplets `(rows, cols, vals)` index rows on the rectangular FFT grid
302+
(`Mapper.sparse_triplets_curvature`).
303+
304+
The scatter-add is performed on the rectangular grid of `data_shape` and the masked pixels are
305+
then gathered via `fft_index_for_masked_pixel`. With `xp=np` it uses `np.bincount` and imports
306+
no JAX, so it runs without JAX installed; with `xp=jax.numpy` it uses `jax.ops.segment_sum`.
307+
"""
308+
num_segments = data_shape[0] * data_shape[1]
309+
310+
if xp is np:
311+
reconstruction = np.asarray(reconstruction, dtype=np.float64)
312+
rows = np.asarray(rows, dtype=np.int64)
313+
cols = np.asarray(cols, dtype=np.int64)
314+
vals = np.asarray(vals, dtype=np.float64)
315+
316+
contrib = vals * reconstruction[cols] # (nnz,)
317+
image_rect = np.bincount(rows, weights=contrib, minlength=num_segments)[
318+
:num_segments
319+
] # (M_rect,)
320+
321+
return image_rect[np.asarray(fft_index_for_masked_pixel)] # (M_pix,)
322+
288323
import jax.numpy as jnp
289324
from jax.ops import segment_sum
290325

@@ -294,9 +329,7 @@ def mapped_reconstructed_operated_data_via_sparse_operator_from(
294329
vals = jnp.asarray(vals, dtype=jnp.float64)
295330

296331
contrib = vals * reconstruction[cols] # (nnz,)
297-
image_rect = segment_sum(
298-
contrib, rows, num_segments=data_shape[0] * data_shape[1]
299-
) # (M_rect,)
332+
image_rect = segment_sum(contrib, rows, num_segments=num_segments) # (M_rect,)
300333

301334
image_slim = image_rect[fft_index_for_masked_pixel] # (M_pix,)
302335
return image_slim

‎autoarray/inversion/inversion/imaging/sparse.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def _data_vector_mapper(self) -> np.ndarray:
9696
cols=cols,
9797
vals=vals,
9898
S=mapper.params,
99+
xp=self._xp,
99100
)
100101
)
101102
param_range = mapper_param_range[mapper_index]
@@ -149,6 +150,7 @@ def _data_vector_x1_mapper(self) -> np.ndarray:
149150
cols=cols,
150151
vals=vals,
151152
S=linear_obj.params,
153+
xp=self._xp,
152154
)
153155

154156
@property
@@ -174,6 +176,7 @@ def _data_vector_multi_mapper(self) -> np.ndarray:
174176
cols=cols,
175177
vals=vals,
176178
S=mapper.params,
179+
xp=self._xp,
177180
)
178181
)
179182

@@ -532,6 +535,7 @@ def _mapped_reconstructed_data_dict_from(
532535
vals=vals,
533536
fft_index_for_masked_pixel=self.mask.fft_index_for_masked_pixel,
534537
data_shape=self.mask.shape_native,
538+
xp=self._xp,
535539
)
536540

537541
mapped = Array2D(values=mapped, mask=self.mask)

‎autoarray/inversion/inversion/interferometer/sparse.py‎

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -180,31 +180,18 @@ def _sparse_triplets_slim_from(self, mapper: Mapper, apply_primary_beam: bool =
180180
beamed operator `P A` (used by the data vector); `False` gives those of `A` (used for the
181181
true-sky reconstructed image).
182182
"""
183-
xp = self._xp
184-
185-
rows, cols, vals = mapper_util.sparse_triplets_from(
183+
return mapper_util.sparse_triplets_from(
186184
pix_indexes_for_sub=mapper.pix_indexes_for_sub_slim_index,
187185
pix_weights_for_sub=mapper.pix_weights_for_sub_slim_index,
188186
slim_index_for_sub=mapper.slim_index_for_sub_slim_index,
189187
fft_index_for_masked_pixel=self.mask.extent_index_for_masked_pixel,
190188
sub_fraction_slim=mapper.over_sampler.sub_fraction.array,
191189
return_rows_slim=True,
192190
slim_weights=self._primary_beam_slim if apply_primary_beam else None,
193-
xp=xp,
191+
pix_sizes_for_sub=mapper.pix_sizes_for_sub_slim_index,
192+
xp=self._xp,
194193
)
195194

196-
pix_indexes = xp.asarray(mapper.pix_indexes_for_sub_slim_index)
197-
M_sub, P = pix_indexes.shape
198-
199-
pix_sizes = xp.asarray(mapper.pix_sizes_for_sub_slim_index, dtype=xp.int32)
200-
slot = xp.tile(xp.arange(P, dtype=xp.int32), M_sub)
201-
valid = (slot < xp.repeat(pix_sizes, P)) & (cols >= 0)
202-
203-
vals = xp.where(valid, vals, 0.0)
204-
cols = xp.where(valid, cols, 0)
205-
206-
return rows, cols, vals
207-
208195
def _data_vector_mapper_from(self, mapper: Mapper, dirty_image) -> np.ndarray:
209196
"""
210197
Returns a mapper's block of the `data_vector`, `Aᵀ d~`, computed from the sparse triplets

‎autoarray/inversion/mappers/abstract.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ def sparse_triplets_data(self):
407407
slim_index_for_sub=self.slim_index_for_sub_slim_index,
408408
fft_index_for_masked_pixel=self.mask.fft_index_for_masked_pixel,
409409
sub_fraction_slim=self.over_sampler.sub_fraction.array,
410+
pix_sizes_for_sub=self.pix_sizes_for_sub_slim_index,
410411
xp=self._xp,
411412
)
412413

@@ -463,6 +464,7 @@ def sparse_triplets_curvature(self):
463464
slim_index_for_sub=self.slim_index_for_sub_slim_index,
464465
fft_index_for_masked_pixel=self.mask.fft_index_for_masked_pixel,
465466
sub_fraction_slim=self.over_sampler.sub_fraction.array,
467+
pix_sizes_for_sub=self.pix_sizes_for_sub_slim_index,
466468
xp=self._xp,
467469
return_rows_slim=False,
468470
)

‎autoarray/inversion/mappers/mapper_util.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def sparse_triplets_from(
276276
*,
277277
return_rows_slim: bool = True,
278278
slim_weights=None,
279+
pix_sizes_for_sub=None,
279280
xp=np,
280281
):
281282
"""
@@ -303,6 +304,13 @@ def sparse_triplets_from(
303304
of `diag(w) A` rather than `A`. The interferometer sparse inversion passes the primary
304305
beam here so the curvature and data vector use the beamed response `P A`. `None`
305306
(default) applies no weighting.
307+
pix_sizes_for_sub
308+
Optional number of used stencil slots per sub-pixel (M_sub,), i.e. the mapper's
309+
`pix_sizes_for_sub_slim_index`. When given, every entry that `mapping_matrix_from` drops
310+
(slot index `>= pix_sizes_for_sub`, or a negative source pixel index such as the `-1`
311+
padding Delaunay meshes write into unused slots) is given value 0 and column 0, so it
312+
contributes nothing to any scatter-add and never indexes out of range. `None` (default)
313+
returns the raw stencil entries unmasked.
306314
xp
307315
Backend module (np or jnp)
308316
@@ -340,6 +348,13 @@ def sparse_triplets_from(
340348
if slim_weights is not None:
341349
vals = vals * np.asarray(slim_weights, dtype=np.float64)[slim_rows]
342350

351+
if pix_sizes_for_sub is not None:
352+
slot = np.tile(np.arange(P, dtype=np.int32), M_sub)
353+
pix_sizes = np.asarray(pix_sizes_for_sub, dtype=np.int32)
354+
valid = (slot < np.repeat(pix_sizes, P)) & (cols >= 0)
355+
vals = np.where(valid, vals, 0.0)
356+
cols = np.where(valid, cols, 0).astype(np.int32)
357+
343358
if return_rows_slim:
344359
return slim_rows, cols, vals
345360

@@ -370,6 +385,13 @@ def sparse_triplets_from(
370385
if slim_weights is not None:
371386
vals = vals * xp.asarray(slim_weights, dtype=xp.float64)[slim_rows]
372387

388+
if pix_sizes_for_sub is not None:
389+
slot = xp.tile(xp.arange(P, dtype=xp.int32), M_sub)
390+
pix_sizes = xp.asarray(pix_sizes_for_sub, dtype=xp.int32)
391+
valid = (slot < xp.repeat(pix_sizes, P)) & (cols >= 0)
392+
vals = xp.where(valid, vals, 0.0)
393+
cols = xp.where(valid, cols, 0)
394+
373395
if return_rows_slim:
374396
return slim_rows, cols, vals
375397

0 commit comments

Comments
 (0)