@@ -622,3 +622,123 @@ def test_coord_offsets_values_match_output_size_and_scale(self, test_zarr_image)
622622 expected_hi = image .output_size [axis ] / 2 - image .scale [axis ] / 2
623623 assert abs (arr [0 ] - expected_lo ) < 1e-9
624624 assert abs (arr [- 1 ] - expected_hi ) < 1e-9
625+
626+
627+ # ---------------------------------------------------------------------------
628+ # full_coords memory fix: property (not cached) + _array_shape
629+ # ---------------------------------------------------------------------------
630+
631+
632+ class TestFullCoordsMemoryFix :
633+ """Verify the full_coords / _array_shape memory-reduction change.
634+
635+ full_coords was changed from @cached_property to @property so that the
636+ large per-axis coordinate arrays are not held in memory between
637+ __getitem__ calls. _array_shape replaces the per-call zarr shape read
638+ with a compact cached tuple.
639+ """
640+
641+ @pytest .fixture
642+ def image (self , tmp_path ):
643+ data = create_test_image_data ((32 , 32 , 32 ), pattern = "gradient" )
644+ path = tmp_path / "test_image.zarr"
645+ create_test_zarr_array (path , data , scale = (4.0 , 4.0 , 4.0 ))
646+ return CellMapImage (
647+ path = str (path ),
648+ target_class = "test_class" ,
649+ target_scale = (4.0 , 4.0 , 4.0 ),
650+ target_voxel_shape = (8 , 8 , 8 ),
651+ )
652+
653+ # ------------------------------------------------------------------
654+ # _array_shape: cached, compact
655+ # ------------------------------------------------------------------
656+
657+ def test_array_shape_is_cached (self , image ):
658+ """_array_shape must be a @cached_property (stored in __dict__)."""
659+ _ = image ._array_shape
660+ assert "_array_shape" in image .__dict__
661+
662+ def test_array_shape_is_tuple_of_ints (self , image ):
663+ """_array_shape must be a tuple of plain Python ints."""
664+ shape = image ._array_shape
665+ assert isinstance (shape , tuple )
666+ assert all (isinstance (s , int ) for s in shape )
667+
668+ def test_array_shape_matches_source_array (self , image ):
669+ """_array_shape must match the underlying zarr array dimensions."""
670+ shape = image ._array_shape
671+ assert shape == (32 , 32 , 32 )
672+
673+ def test_array_shape_same_object_on_repeated_access (self , image ):
674+ """_array_shape returns the same tuple object (cached, not recomputed)."""
675+ s1 = image ._array_shape
676+ s2 = image ._array_shape
677+ assert s1 is s2
678+
679+ # ------------------------------------------------------------------
680+ # full_coords: NOT cached between calls
681+ # ------------------------------------------------------------------
682+
683+ def test_full_coords_not_in_dict_after_bounding_box (self , image ):
684+ """After bounding_box initialises, full_coords must NOT be in __dict__.
685+
686+ bounding_box is the primary consumer of full_coords during setup;
687+ the fix requires that the large coord arrays are freed immediately
688+ after bounding_box is cached.
689+ """
690+ _ = image .bounding_box # triggers full_coords access internally
691+ assert "full_coords" not in image .__dict__
692+
693+ def test_full_coords_not_cached_after_getitem (self , image ):
694+ """After a __getitem__ call, full_coords must not be in __dict__."""
695+ center = {"z" : 64.0 , "y" : 64.0 , "x" : 64.0 }
696+ _ = image [center ]
697+ assert "full_coords" not in image .__dict__
698+
699+ def test_full_coords_returns_new_object_each_access (self , image ):
700+ """full_coords must produce a new tuple on every call (not cached)."""
701+ fc1 = image .full_coords
702+ fc2 = image .full_coords
703+ assert fc1 is not fc2
704+
705+ def test_full_coords_values_consistent (self , image ):
706+ """Repeated calls to full_coords must return equivalent coordinate values."""
707+ import numpy as np
708+
709+ fc1 = image .full_coords
710+ fc2 = image .full_coords
711+ assert len (fc1 ) == len (fc2 )
712+ for da1 , da2 in zip (fc1 , fc2 ):
713+ np .testing .assert_array_equal (da1 .values , da2 .values )
714+ assert da1 .dims == da2 .dims
715+
716+ # ------------------------------------------------------------------
717+ # shape property still correct (now delegates to _array_shape)
718+ # ------------------------------------------------------------------
719+
720+ def test_shape_property_correct (self , image ):
721+ """shape must still return the correct axis→size mapping."""
722+ shape = image .shape
723+ assert isinstance (shape , dict )
724+ for axis in image .axes :
725+ assert axis in shape
726+ assert shape [axis ] == 32
727+
728+ def test_shape_uses_array_shape (self , image ):
729+ """shape values must match _array_shape elements."""
730+ arr_shape = image ._array_shape
731+ for s , axis in zip (arr_shape , image .axes ):
732+ assert image .shape [axis ] == s
733+
734+ # ------------------------------------------------------------------
735+ # bounding_box correctness preserved after the refactor
736+ # ------------------------------------------------------------------
737+
738+ def test_bounding_box_still_correct_after_refactor (self , image ):
739+ """bounding_box must still return valid min/max per axis."""
740+ bbox = image .bounding_box
741+ assert set (bbox .keys ()) == set (image .axes )
742+ for axis in image .axes :
743+ lo , hi = bbox [axis ]
744+ assert lo <= hi
0 commit comments