@@ -296,7 +296,14 @@ def index_glb_to_loc(self, *args, rel=True):
296296 loc_min = loc_abs_min - base \
297297 + np .mod (glb_idx .step - np .mod (base , glb_idx .step ),
298298 glb_idx .step )
299- else : # glb start is given explicitly
299+ elif glb_idx_min >= loc_abs_min :
300+ # The slice starts within this subdomain, so its first
301+ # selected index is the start itself (the stride phase
302+ # begins here, not at loc_abs_min).
303+ loc_min = glb_idx_min - base
304+ else : # the slice started in an earlier subdomain
305+ # Advance to the first in-stride index at or after this
306+ # subdomain's start.
300307 loc_min = loc_abs_min - base \
301308 + np .mod (glb_idx .step - np .mod (base - glb_idx .start ,
302309 glb_idx .step ), glb_idx .step )
@@ -319,7 +326,14 @@ def index_glb_to_loc(self, *args, rel=True):
319326 loc_max = top - base \
320327 + np .mod (glb_idx .step - np .mod (top - glb_max ,
321328 glb_idx .step ), glb_idx .step )
322- else :
329+ elif glb_idx_max <= loc_abs_max :
330+ # The slice's high end (its start) is within this
331+ # subdomain, so the first selected index is the start
332+ # itself (the stride phase begins here, not at loc_abs_max).
333+ loc_max = glb_idx_max - base
334+ else : # the slice's high end is in a later subdomain
335+ # Step back to the first in-stride index at or below this
336+ # subdomain's end.
323337 loc_max = top - base \
324338 + np .mod (glb_idx .step - np .mod (top - glb_idx .start ,
325339 glb_idx .step ), glb_idx .step )
@@ -555,3 +569,50 @@ def reshape(self, *args):
555569 items = [i + nleft for i in items ]
556570
557571 return Decomposition (items , self .local )
572+
573+ def index_decomposition (self , idx ):
574+ """
575+ The decomposition induced by NumPy-indexing this axis with a slice.
576+
577+ Each subdomain keeps the result positions, in index order, of the global
578+ indices it owns. A strided or reversed slice therefore just relabels
579+ which rank owns which result index -- indexing never moves data across
580+ ranks. Unlike `reshape` (which adjusts subdomain *boundaries* and
581+ is used for halos), this follows exact NumPy slicing semantics.
582+
583+ Examples
584+ --------
585+ >>> d = Decomposition([[0, 1, 2, 3], [4, 5, 6, 7]], 0)
586+ >>> d.index_decomposition(slice(None, None, -1))
587+ Decomposition(<<[4,7]>>, [0,3])
588+ >>> d.index_decomposition(slice(None, None, -3))
589+ Decomposition(<<[2,2]>>, [0,1])
590+ """
591+ if self .glb_max is None :
592+ return Decomposition ([np .array ([], dtype = np .int64 )]* len (self ),
593+ self .local )
594+ size = self .glb_max - self .glb_min + 1
595+
596+ # The global indices selected by `idx`, in result order. For a slice
597+ # (the only caller) this is built directly, without materialising the
598+ # whole axis.
599+ if isinstance (idx , slice ):
600+ start , stop , step = idx .indices (size )
601+ selected = self .glb_min + np .arange (start , stop , step )
602+ else :
603+ selected = np .arange (self .glb_min , self .glb_min + size )[idx ]
604+
605+ # Bucket the selected indices by owning subdomain. A subdomain is a
606+ # contiguous range, so membership is a bounds check in O(len(selected))
607+ # -- avoiding an O(axis) `isin` on every slice. A non-contiguous
608+ # subdomain (not produced by gridding/slicing) falls back to `isin`.
609+ items = []
610+ for sd in self :
611+ if sd .size == 0 :
612+ items .append (np .array ([], dtype = np .int64 ))
613+ elif sd [- 1 ] - sd [0 ] + 1 == sd .size :
614+ owned = (selected >= sd [0 ]) & (selected <= sd [- 1 ])
615+ items .append (np .nonzero (owned )[0 ])
616+ else :
617+ items .append (np .nonzero (np .isin (selected , sd ))[0 ])
618+ return Decomposition (items , self .local )
0 commit comments