@@ -68,12 +68,30 @@ def test__data_vector_via_transformed_mapping_matrix_from():
6868 assert (data_vector_complex_via_blurred == data_vector_via_transformed ).all ()
6969
7070
71- def _sparse_operator_and_mask ( ):
71+ def _dataset_from ( mask , n_visibilities , seed ):
7272 """
73- Returns a real `InterferometerSparseOperator` (and the mask it is defined on) built from a
74- small 7x7 `TransformerDFT` interferometer dataset .
73+ Returns a small `TransformerDFT` interferometer dataset on the input mask, with `n_visibilities`
74+ seeded random visibilities and unit noise, alongside the random generator used to build it .
7575 """
76- mask = aa .Mask2D (
76+ rng = np .random .default_rng (seed = seed )
77+
78+ dataset = aa .Interferometer (
79+ data = aa .Visibilities (
80+ visibilities = rng .normal (size = (n_visibilities , 2 )).astype (np .float64 )
81+ ),
82+ noise_map = aa .VisibilitiesNoiseMap (
83+ visibilities = np .ones ((n_visibilities , 2 ), dtype = np .float64 )
84+ ),
85+ uv_wavelengths = rng .normal (size = (n_visibilities , 2 )).astype (np .float64 ),
86+ real_space_mask = mask ,
87+ transformer_class = aa .TransformerDFT ,
88+ )
89+
90+ return dataset , rng
91+
92+
93+ def _mask_7x7 ():
94+ return aa .Mask2D (
7795 mask = [
7896 [True , True , True , True , True , True , True ],
7997 [True , True , True , True , True , True , True ],
@@ -86,20 +104,15 @@ def _sparse_operator_and_mask():
86104 pixel_scales = 2.0 ,
87105 )
88106
89- n_visibilities = 5
90- rng = np .random .default_rng (seed = 3 )
91107
92- dataset = aa .Interferometer (
93- data = aa .Visibilities (
94- visibilities = rng .normal (size = (n_visibilities , 2 )).astype (np .float64 )
95- ),
96- noise_map = aa .VisibilitiesNoiseMap (
97- visibilities = np .ones ((n_visibilities , 2 ), dtype = np .float64 )
98- ),
99- uv_wavelengths = rng .normal (size = (n_visibilities , 2 )).astype (np .float64 ),
100- real_space_mask = mask ,
101- transformer_class = aa .TransformerDFT ,
102- )
108+ def _sparse_operator_and_mask ():
109+ """
110+ Returns a real `InterferometerSparseOperator` (and the mask it is defined on) built from a
111+ small 7x7 `TransformerDFT` interferometer dataset.
112+ """
113+ mask = _mask_7x7 ()
114+
115+ dataset , rng = _dataset_from (mask = mask , n_visibilities = 5 , seed = 3 )
103116
104117 return dataset .apply_sparse_operator (use_jax = False ).sparse_operator , mask , rng
105118
@@ -253,3 +266,65 @@ def test__interferometer_sparse_operator__operated_matrix_slim_from():
253266
254267 assert operated .shape == (mask .pixels_in_mask , 2 )
255268 assert operated == pytest .approx (operated_dense , 1.0e-8 )
269+
270+
271+ def _apply_operator_via_complex_fft2 (preload , operator ):
272+ """
273+ Returns `W~ @ I` computed with the complex `fft2` / `ifft2` pair, written out in NumPy so that
274+ it is an independent reference for the `rfft2` / `irfft2` implementation in
275+ `InterferometerSparseOperator.apply_operator`.
276+ """
277+ y_shape , x_shape = operator .y_shape , operator .x_shape
278+ M = operator .M
279+
280+ Fbatch_flat = np .eye (M )
281+ B = Fbatch_flat .shape [1 ]
282+
283+ F_img = Fbatch_flat .T .reshape ((B , y_shape , x_shape ))
284+ F_pad = np .pad (F_img , ((0 , 0 ), (0 , y_shape ), (0 , x_shape )))
285+
286+ Khat = np .fft .fft2 (preload )
287+ Ghat = np .fft .fft2 (F_pad ) * Khat [None , :, :]
288+ G_pad = np .fft .ifft2 (Ghat )
289+ G = np .real (G_pad [:, :y_shape , :x_shape ])
290+
291+ return G .reshape ((B , M )).T
292+
293+
294+ def test__interferometer_sparse_operator__apply_operator__rfft2_matches_complex_fft2_reference ():
295+ pytest .importorskip ("jax" )
296+
297+ cases = [
298+ (_mask_7x7 (), 5 , 3 ),
299+ (
300+ aa .Mask2D .circular (shape_native = (12 , 12 ), pixel_scales = 1.0 , radius = 4.0 ),
301+ 64 ,
302+ 11 ,
303+ ),
304+ ]
305+
306+ for mask , n_visibilities , seed in cases :
307+ dataset , _ = _dataset_from (mask = mask , n_visibilities = n_visibilities , seed = seed )
308+
309+ preload = dataset .psf_precision_operator_from (use_jax = False )
310+ operator = dataset .apply_sparse_operator (
311+ nufft_precision_operator = preload
312+ ).sparse_operator
313+
314+ # The preload and the batch are both real, so `rfft2` stores only the non-redundant half
315+ # of the spectrum: (2y, x + 1) rather than (2y, 2x).
316+ assert operator .Khat .shape == (2 * operator .y_shape , operator .x_shape + 1 )
317+
318+ operated = np .array (operator .apply_operator (np .eye (operator .M )))
319+ operated_via_complex_fft2 = _apply_operator_via_complex_fft2 (
320+ preload = preload , operator = operator
321+ )
322+
323+ # The real transform pair is exact for a real preload and a real batch, so this pin is at
324+ # round-off, not at an algorithmic tolerance.
325+ np .testing .assert_allclose (
326+ operated ,
327+ operated_via_complex_fft2 ,
328+ rtol = 1.0e-10 ,
329+ atol = 1.0e-10 * np .abs (operated_via_complex_fft2 ).max (),
330+ )
0 commit comments