@@ -563,6 +563,133 @@ def test__interferometer_sparse_operator__func_list_and_mapper__identical_to_map
563563 )
564564
565565
566+ def test__interferometer_sparse_operator__func_list_only__identical_to_mapping ():
567+ """
568+ A linear function list with no mapper (e.g. an MGE with no pixelization) is routed to the sparse
569+ path whenever the dataset has a sparse operator, where its curvature matrix is the func-func block
570+ `Bᵀ W~ B` alone and its data vector is `Bᵀ d~`, reproducing the dense (mapping formalism) inversion.
571+ """
572+ mask , rng , dataset , dataset_sparse = _sparse_parity_setup ()
573+
574+ linear_obj = aa .m .MockLinearObjFuncList (
575+ parameters = 3 ,
576+ mapping_matrix = rng .normal (size = (mask .pixels_in_mask , 3 )),
577+ )
578+
579+ inversion_sparse = aa .Inversion (
580+ dataset = dataset_sparse , linear_obj_list = [linear_obj ]
581+ )
582+ inversion_mapping = aa .Inversion (dataset = dataset , linear_obj_list = [linear_obj ])
583+
584+ assert type (inversion_sparse ) is aa .InversionInterferometerSparse
585+ assert isinstance (inversion_mapping , aa .InversionInterferometerMapping )
586+
587+ # The reconstruction comes out of a linear solve of this small, poorly conditioned system and is
588+ # compared (to its looser tolerance) by `_assert_sparse_matches_mapping` below.
589+ for name in ("curvature_matrix" , "data_vector" ):
590+ reference = np .asarray (getattr (inversion_mapping , name ))
591+
592+ np .testing .assert_allclose (
593+ np .asarray (getattr (inversion_sparse , name )),
594+ reference ,
595+ rtol = 1.0e-10 ,
596+ atol = 1.0e-10 * np .abs (reference ).max (),
597+ err_msg = name ,
598+ )
599+
600+ _assert_sparse_matches_mapping (
601+ dataset = dataset ,
602+ dataset_sparse = dataset_sparse ,
603+ linear_obj_list = [linear_obj ],
604+ )
605+
606+
607+ def test__interferometer_sparse_operator__sparse_dirty_image_override__used_by_data_vector ():
608+ """
609+ The sparse operator caches the dirty image of the visibilities it was built from. When an inversion
610+ fits different visibilities (e.g. with the visibilities of ordinary light profiles subtracted), the
611+ `DatasetInterface` supplies their dirty image via `sparse_dirty_image`, which the data vector must use
612+ so that the sparse inversion reproduces the dense inversion of the subtracted visibilities.
613+ """
614+ mask , rng , dataset , dataset_sparse = _sparse_parity_setup ()
615+
616+ linear_obj = aa .m .MockLinearObjFuncList (
617+ parameters = 2 ,
618+ mapping_matrix = rng .normal (size = (mask .pixels_in_mask , 2 )),
619+ )
620+
621+ mapper = _mapper_from (
622+ mask = mask ,
623+ pixels = 9 ,
624+ shape = (3 , 3 ),
625+ regularization = aa .reg .Constant (coefficient = 1.0 ),
626+ )
627+
628+ subtracted = aa .Visibilities (
629+ visibilities = dataset .data .array
630+ - rng .normal (size = dataset .data .shape )
631+ - 1j * rng .normal (size = dataset .data .shape )
632+ )
633+
634+ sparse_dirty_image = dataset .transformer .image_from (
635+ visibilities = aa .Visibilities (
636+ visibilities = subtracted .array .real * dataset .noise_map .array .real ** - 2.0
637+ + 1j * subtracted .array .imag * dataset .noise_map .array .imag ** - 2.0
638+ )
639+ ).array
640+
641+ dataset_interface_mapping = aa .DatasetInterface (
642+ data = subtracted ,
643+ noise_map = dataset .noise_map ,
644+ grids = dataset .grids ,
645+ transformer = dataset .transformer ,
646+ )
647+
648+ for linear_obj_list in ([linear_obj ], [linear_obj , mapper ]):
649+ inversion_mapping = aa .Inversion (
650+ dataset = dataset_interface_mapping , linear_obj_list = linear_obj_list
651+ )
652+
653+ inversion_sparse = aa .Inversion (
654+ dataset = aa .DatasetInterface (
655+ data = subtracted ,
656+ noise_map = dataset .noise_map ,
657+ grids = dataset .grids ,
658+ transformer = dataset .transformer ,
659+ sparse_operator = dataset_sparse .sparse_operator ,
660+ sparse_dirty_image = sparse_dirty_image ,
661+ ),
662+ linear_obj_list = linear_obj_list ,
663+ )
664+
665+ assert isinstance (inversion_sparse , aa .InversionInterferometerSparse )
666+
667+ reference = np .asarray (inversion_mapping .data_vector )
668+
669+ np .testing .assert_allclose (
670+ np .asarray (inversion_sparse .data_vector ),
671+ reference ,
672+ rtol = 1.0e-10 ,
673+ atol = 1.0e-10 * np .abs (reference ).max (),
674+ )
675+
676+ # The control: without the override the data vector is that of the unsubtracted visibilities.
677+ inversion_sparse_cached = aa .Inversion (
678+ dataset = aa .DatasetInterface (
679+ data = subtracted ,
680+ noise_map = dataset .noise_map ,
681+ grids = dataset .grids ,
682+ transformer = dataset .transformer ,
683+ sparse_operator = dataset_sparse .sparse_operator ,
684+ ),
685+ linear_obj_list = linear_obj_list ,
686+ )
687+
688+ assert np .abs (
689+ np .asarray (inversion_sparse_cached .data_vector ) - reference
690+ ).max () > 1.0e-4 * np .abs (reference ).max ()
691+
692+
566693def test__interferometer_sparse_operator__x2_mappers__identical_to_mapping ():
567694 """
568695 Two `Mapper` objects fitted simultaneously require the mapper-mapper off-diagonal block
@@ -866,3 +993,54 @@ def test__interferometer_sparse_operator__numpy_inversion_matches_jax_inversion(
866993 ).max ()
867994
868995 assert difference_sparse <= max (10.0 * difference_dense , 1.0e-14 )
996+
997+ # A linear function list with no mapper (e.g. an MGE with no pixelization) takes the same
998+ # sparse path, with only the func-func curvature block and `Bᵀ d~` data vector.
999+ linear_obj = aa .m .MockLinearObjFuncList (
1000+ parameters = 3 ,
1001+ mapping_matrix = np .random .default_rng (seed = seed ).normal (
1002+ size = (mask .pixels_in_mask , 3 )
1003+ ),
1004+ )
1005+
1006+ inversion_np = aa .Inversion (
1007+ dataset = dataset_sparse , linear_obj_list = [linear_obj ], xp = np
1008+ )
1009+ inversion_jax = aa .Inversion (
1010+ dataset = dataset_sparse , linear_obj_list = [linear_obj ], xp = jnp
1011+ )
1012+
1013+ assert type (inversion_np ) is aa .InversionInterferometerSparse
1014+ assert type (inversion_jax ) is aa .InversionInterferometerSparse
1015+
1016+ for name in ("curvature_matrix" , "data_vector" ):
1017+ reference = np .asarray (getattr (inversion_jax , name ))
1018+
1019+ np .testing .assert_allclose (
1020+ np .asarray (getattr (inversion_np , name )),
1021+ reference ,
1022+ rtol = 1.0e-10 ,
1023+ atol = 1.0e-10 * np .abs (reference ).max (),
1024+ err_msg = name ,
1025+ )
1026+
1027+ # The unregularized func-list system is poorly conditioned, so the reconstruction is held to
1028+ # the same control as above: no worse than the dense inversion's NumPy / JAX spread.
1029+ difference_dense = np .abs (
1030+ np .asarray (
1031+ aa .Inversion (
1032+ dataset = dataset , linear_obj_list = [linear_obj ], xp = np
1033+ ).reconstruction
1034+ )
1035+ - np .asarray (
1036+ aa .Inversion (
1037+ dataset = dataset , linear_obj_list = [linear_obj ], xp = jnp
1038+ ).reconstruction
1039+ )
1040+ ).max ()
1041+ difference_sparse = np .abs (
1042+ np .asarray (inversion_np .reconstruction )
1043+ - np .asarray (inversion_jax .reconstruction )
1044+ ).max ()
1045+
1046+ assert difference_sparse <= max (10.0 * difference_dense , 1.0e-14 )
0 commit comments