@@ -211,6 +211,78 @@ def test_indexing_into_sparse(self):
211211 sf .data [1 :- 1 , 0 ] = np .arange (8 )
212212 assert np .all (sf .data [1 :- 1 , 0 ] == np .arange (8 ))
213213
214+ def test_slice_after_transpose (self ):
215+ """
216+ Slicing a ``Data`` view that has been transposed (via ``.T``,
217+ ``transpose`` or ``swapaxes``) must use the new axis ordering for
218+ per-axis metadata. Previously the metadata was copied verbatim from
219+ the un-transposed array, so a subsequent slice was computed against
220+ the wrong decomposition and silently returned a wrong-shaped result
221+ (see issue #2187).
222+ """
223+ grid = Grid (shape = (4 , 6 ))
224+ f = Function (name = 'f' , grid = grid )
225+ f .data [:] = np .arange (24 ).reshape ((4 , 6 )).astype (np .float32 )
226+ ref = np .array (f .data )
227+
228+ # ``.T`` (C-level shortcut) then slice
229+ assert np .array_equal (f .data .T [::2 , ::2 ], ref .T [::2 , ::2 ])
230+
231+ # Equivalent: slice then ``.T``
232+ assert np .array_equal (f .data [::2 , ::2 ].T , ref [::2 , ::2 ].T )
233+
234+ # Explicit ``transpose`` call -- same behavior as ``.T``
235+ assert np .array_equal (f .data .transpose ()[::2 , ::2 ],
236+ ref .transpose ()[::2 , ::2 ])
237+
238+ # ``swapaxes`` between non-conforming dims
239+ assert np .array_equal (f .data .swapaxes (0 , 1 )[::2 , ::2 ],
240+ ref .swapaxes (0 , 1 )[::2 , ::2 ])
241+
242+ # 3D transpose with an explicit axis order, then per-axis slice
243+ grid3 = Grid (shape = (2 , 4 , 6 ))
244+ g = Function (name = 'g3' , grid = grid3 )
245+ g .data [:] = np .arange (48 ).reshape ((2 , 4 , 6 )).astype (np .float32 )
246+ ref3 = np .array (g .data )
247+
248+ assert np .array_equal (g .data .T [::2 , ::2 , ::2 ], ref3 .T [::2 , ::2 , ::2 ])
249+ assert np .array_equal (g .data .transpose ((1 , 0 , 2 ))[::2 , ::1 , ::3 ],
250+ ref3 .transpose ((1 , 0 , 2 ))[::2 , ::1 , ::3 ])
251+
252+ def test_transpose_permutes_data_metadata (self ):
253+ """
254+ After a transpose-like operation, ``_decomposition`` and ``_modulo``
255+ must be permuted to match the new axis order so that subsequent
256+ ``__getitem__`` translations use the right per-axis ranges.
257+ """
258+ grid = Grid (shape = (4 , 6 ))
259+ f = Function (name = 'f' , grid = grid )
260+
261+ original_decomp = f .data ._decomposition
262+ assert len (original_decomp ) == 2
263+
264+ # ``.T`` reverses everything
265+ tdata = f .data .T
266+ assert tdata ._decomposition == original_decomp [::- 1 ]
267+ assert tdata ._modulo == f .data ._modulo [::- 1 ]
268+
269+ # ``transpose()`` with no args == ``.T``
270+ tdata2 = f .data .transpose ()
271+ assert tdata2 ._decomposition == original_decomp [::- 1 ]
272+
273+ # ``swapaxes`` swaps the two named axes
274+ sdata = f .data .swapaxes (0 , 1 )
275+ assert sdata ._decomposition == (original_decomp [1 ], original_decomp [0 ])
276+
277+ # Explicit axis-order
278+ grid3 = Grid (shape = (2 , 4 , 6 ))
279+ g = Function (name = 'g3' , grid = grid3 )
280+ gdec = g .data ._decomposition
281+ perm = g .data .transpose ((1 , 2 , 0 ))
282+ assert perm ._decomposition == (gdec [1 ], gdec [2 ], gdec [0 ])
283+ assert perm ._modulo == (g .data ._modulo [1 ], g .data ._modulo [2 ],
284+ g .data ._modulo [0 ])
285+
214286 @pytest .mark .parallel (mode = 1 )
215287 def test_indexing_into_sparse_subfunc_singlempi (self , mode ):
216288 grid = Grid (shape = (4 , 4 ))
0 commit comments