@@ -1898,3 +1898,221 @@ def test__interferometer_sparse_operator__data_vector_from_triplets_matches_dens
18981898 rtol = 1.0e-12 ,
18991899 atol = 1.0e-12 * np .abs (reference ).max (),
19001900 )
1901+
1902+
1903+ def _rectangular_mapper_from (mask , shape , over_sample_size ):
1904+ from autoarray .inversion .mesh .mesh .rectangular_rtu_adapt_density import (
1905+ overlay_grid_from ,
1906+ )
1907+
1908+ grid = aa .Grid2D .from_mask (mask = mask , over_sample_size = over_sample_size )
1909+
1910+ interpolator = aa .mesh .RectangularUniform (shape = shape ).interpolator_from (
1911+ source_plane_data_grid = grid ,
1912+ source_plane_mesh_grid = aa .Grid2DIrregular (
1913+ overlay_grid_from (shape_native = shape , grid = grid )
1914+ ),
1915+ adapt_data = None ,
1916+ )
1917+
1918+ return aa .Mapper (
1919+ interpolator = interpolator , regularization = aa .reg .Constant (coefficient = 1.0 )
1920+ )
1921+
1922+
1923+ def _mapped_reconstructed_parity_setup (seed ):
1924+ """
1925+ A 12x12 circular-mask TransformerDFT dataset with its sparse operator, and the linear object lists
1926+ the sparse `mapped_reconstructed_data_dict` is checked on: a Rectangular mesh, a Delaunay mesh,
1927+ over-sampled (sub_size 2) Rectangular and Delaunay meshes, two mappers, and a mapper plus a linear
1928+ function list (in either order).
1929+ """
1930+ mask = aa .Mask2D .circular (shape_native = (12 , 12 ), pixel_scales = 1.0 , radius = 4.0 )
1931+
1932+ rng = np .random .default_rng (seed = seed )
1933+ n_visibilities = 32
1934+
1935+ dataset = aa .Interferometer (
1936+ data = aa .Visibilities (
1937+ visibilities = rng .normal (size = (n_visibilities , 2 )).astype (np .float64 )
1938+ ),
1939+ noise_map = aa .VisibilitiesNoiseMap (
1940+ visibilities = np .ones ((n_visibilities , 2 ), dtype = np .float64 )
1941+ ),
1942+ uv_wavelengths = rng .normal (size = (n_visibilities , 2 )).astype (np .float64 ),
1943+ real_space_mask = mask ,
1944+ transformer_class = aa .TransformerDFT ,
1945+ )
1946+ dataset_sparse = dataset .apply_sparse_operator (use_jax = False )
1947+
1948+ rectangular = _rectangular_mapper_from (mask = mask , shape = (4 , 4 ), over_sample_size = 1 )
1949+ delaunay = _over_sampled_mapper_from (
1950+ mask = mask , pixels = 16 , shape = (4 , 4 ), over_sample_size = 1
1951+ )
1952+ rectangular_sub_2 = _rectangular_mapper_from (
1953+ mask = mask , shape = (5 , 5 ), over_sample_size = 2
1954+ )
1955+ delaunay_sub_2 = _over_sampled_mapper_from (
1956+ mask = mask , pixels = 9 , shape = (3 , 3 ), over_sample_size = 2
1957+ )
1958+ linear_obj = aa .m .MockLinearObjFuncList (
1959+ parameters = 2 ,
1960+ mapping_matrix = rng .normal (size = (mask .pixels_in_mask , 2 )),
1961+ )
1962+
1963+ linear_obj_lists = [
1964+ [rectangular ],
1965+ [delaunay ],
1966+ [rectangular_sub_2 ],
1967+ [delaunay_sub_2 ],
1968+ [delaunay_sub_2 , rectangular ],
1969+ [rectangular , linear_obj ],
1970+ [linear_obj , delaunay_sub_2 ],
1971+ ]
1972+
1973+ return mask , rng , dataset_sparse , linear_obj_lists
1974+
1975+
1976+ def _assert_mapped_reconstructed_data_dict_matches_dense (
1977+ image_dict , linear_obj_list , reconstruction , inversion , rtol
1978+ ):
1979+ """
1980+ Each entry of `image_dict` must equal the dense `mapping_matrix @ reconstruction` of its linear object,
1981+ where `reconstruction` is the (NumPy) reconstruction split per linear object.
1982+ """
1983+ param_range_list = inversion .param_range_list_from (cls = aa .LinearObj )
1984+
1985+ for linear_obj , (start , end ) in zip (linear_obj_list , param_range_list ):
1986+ reference = np .asarray (linear_obj .mapping_matrix ) @ np .asarray (
1987+ reconstruction [start :end ]
1988+ )
1989+
1990+ np .testing .assert_allclose (
1991+ np .asarray (image_dict [linear_obj ]),
1992+ reference ,
1993+ rtol = rtol ,
1994+ atol = rtol * np .abs (reference ).max (),
1995+ )
1996+
1997+
1998+ def test__interferometer_sparse_operator__mapped_reconstructed_data_dict_matches_dense_mapping_matrix ():
1999+ """
2000+ `InversionInterferometerSparse.mapped_reconstructed_data_dict` reconstructs each mapper's image by a
2001+ scatter-add over the mapper's sparse triplets instead of the dense `mapping_matrix @ reconstruction`.
2002+ It must reproduce the dense product for Rectangular and Delaunay meshes, over-sampled mappers, two
2003+ mappers and a mapper mixed with a linear function list, and its values must be wrapped as an
2004+ `Array2D` on the inversion's real-space mask (so the model visibilities are unchanged).
2005+ """
2006+ mask , rng , dataset_sparse , linear_obj_lists = _mapped_reconstructed_parity_setup (
2007+ seed = 7
2008+ )
2009+
2010+ for linear_obj_list in linear_obj_lists :
2011+ inversion = aa .Inversion (
2012+ dataset = dataset_sparse , linear_obj_list = linear_obj_list
2013+ )
2014+
2015+ assert isinstance (inversion , aa .InversionInterferometerSparse )
2016+
2017+ image_dict = inversion .mapped_reconstructed_data_dict
2018+
2019+ for linear_obj in linear_obj_list :
2020+ assert isinstance (image_dict [linear_obj ], aa .Array2D )
2021+ assert (image_dict [linear_obj ].mask == mask ).all ()
2022+
2023+ _assert_mapped_reconstructed_data_dict_matches_dense (
2024+ image_dict = image_dict ,
2025+ linear_obj_list = linear_obj_list ,
2026+ reconstruction = np .asarray (inversion .reconstruction ),
2027+ inversion = inversion ,
2028+ rtol = 1.0e-10 ,
2029+ )
2030+
2031+ # The model visibilities consume the dict and must equal the transform of the dense image.
2032+ visibilities_dict = inversion .mapped_reconstructed_operated_data_dict
2033+
2034+ for linear_obj in linear_obj_list :
2035+ reference = dataset_sparse .transformer .visibilities_from (
2036+ image = aa .Array2D (
2037+ values = np .asarray (linear_obj .mapping_matrix )
2038+ @ np .asarray (inversion .reconstruction_dict [linear_obj ]),
2039+ mask = mask ,
2040+ )
2041+ )
2042+
2043+ np .testing .assert_allclose (
2044+ np .asarray (visibilities_dict [linear_obj ]),
2045+ np .asarray (reference ),
2046+ rtol = 1.0e-10 ,
2047+ atol = 1.0e-10 * np .abs (np .asarray (reference )).max (),
2048+ )
2049+
2050+
2051+ def test__interferometer_sparse_operator__mapped_reconstructed_data_dict_matches_dense_mapping_matrix__jax_jit ():
2052+ """
2053+ The JAX backend of the sparse mapped reconstructed image (a `.at[rows].add` scatter over image pixels)
2054+ must reproduce the dense `mapping_matrix @ reconstruction` under `jax.jit`, with the dirty image traced
2055+ so the reconstruction is computed inside the jitted function.
2056+ """
2057+ jax = pytest .importorskip ("jax" )
2058+
2059+ import jax .numpy as jnp
2060+
2061+ mask , rng , dataset_sparse , linear_obj_lists = _mapped_reconstructed_parity_setup (
2062+ seed = 9
2063+ )
2064+
2065+ dirty_image = np .asarray (dataset_sparse .sparse_operator .dirty_image )
2066+
2067+ def inversion_from (dirty_image , linear_obj_list , xp ):
2068+ return aa .Inversion (
2069+ dataset = aa .DatasetInterface (
2070+ data = dataset_sparse .data ,
2071+ noise_map = dataset_sparse .noise_map ,
2072+ grids = dataset_sparse .grids ,
2073+ transformer = dataset_sparse .transformer ,
2074+ sparse_operator = dataset_sparse .sparse_operator ,
2075+ sparse_dirty_image = dirty_image ,
2076+ ),
2077+ linear_obj_list = linear_obj_list ,
2078+ xp = xp ,
2079+ )
2080+
2081+ for linear_obj_list in linear_obj_lists :
2082+ inversion_np = inversion_from (dirty_image , linear_obj_list , xp = np )
2083+
2084+ assert isinstance (inversion_np , aa .InversionInterferometerSparse )
2085+
2086+ def images_and_reconstruction_from (dirty_image ):
2087+ inversion = inversion_from (dirty_image , linear_obj_list , xp = jnp )
2088+
2089+ image_dict = inversion .mapped_reconstructed_data_dict
2090+
2091+ return (
2092+ [image_dict [linear_obj ].array for linear_obj in linear_obj_list ],
2093+ inversion .reconstruction ,
2094+ )
2095+
2096+ image_list , reconstruction = jax .jit (images_and_reconstruction_from )(
2097+ jnp .asarray (dirty_image )
2098+ )
2099+
2100+ # A sanity check only: the NumPy and JAX linear solves differ by more than the matrices they are
2101+ # handed (see `test__interferometer_sparse_operator__numpy_inversion_matches_jax_inversion`), so the
2102+ # image is compared below against the dense product of the jitted reconstruction itself.
2103+ reference = np .asarray (inversion_np .reconstruction )
2104+
2105+ np .testing .assert_allclose (
2106+ np .asarray (reconstruction ),
2107+ reference ,
2108+ rtol = 0.0 ,
2109+ atol = 1.0e-3 * np .abs (reference ).max (),
2110+ )
2111+
2112+ _assert_mapped_reconstructed_data_dict_matches_dense (
2113+ image_dict = dict (zip (linear_obj_list , image_list )),
2114+ linear_obj_list = linear_obj_list ,
2115+ reconstruction = np .asarray (reconstruction ),
2116+ inversion = inversion_np ,
2117+ rtol = 1.0e-12 ,
2118+ )
0 commit comments