1from numpy import (broadcast, broadcast_to, array, intp, ndarray, bool_,
2                   logical_and, broadcast_arrays)
3
4from .ndindex import NDIndex, ndindex, asshape
5from .subindex_helpers import subindex_slice
6
7class Tuple(NDIndex):
8    """
9    Represents a tuple of single-axis indices.
10
11    Valid single axis indices are
12
13    - :class:`Integer`
14    - :class:`Slice`
15    - :class:`ellipsis`
16    - :class:`Newaxis`
17    - :class:`IntegerArray`
18    - :class:`BooleanArray`
19
20    (some of the above are not yet implemented)
21
22    `Tuple(x1, x2, …, xn)` represents the index `a[x1, x2, …, xn]` or,
23    equivalently, `a[(x1, x2, …, xn)]`. `Tuple()` with no arguments is the
24    empty tuple index, `a[()]`, which returns `a` unchanged.
25
26    >>> from ndindex import Tuple, Slice
27    >>> import numpy as np
28    >>> idx = Tuple(0, Slice(2, 4))
29    >>> a = np.arange(10).reshape((2, 5))
30    >>> a
31    array([[0, 1, 2, 3, 4],
32           [5, 6, 7, 8, 9]])
33    >>> a[0, 2:4]
34    array([2, 3])
35    >>> a[idx.raw]
36    array([2, 3])
37
38    .. note::
39
40       `Tuple` does *not* represent a tuple, but rather an *tuple index*. It
41       does not have most methods that `tuple` has, and should not be used in
42       non-indexing contexts. See the document on :any:`type-confusion` for
43       more details.
44
45    """
46    __slots__ = ()
47
48    def _typecheck(self, *args):
49        newargs = []
50        arrays = []
51        array_block_start = False
52        array_block_stop = False
53        has_array = any(isinstance(i, (ArrayIndex, list, ndarray, bool, bool_)) for i in args)
54        has_boolean_scalar = False
55        for arg in args:
56            newarg = ndindex(arg)
57            if isinstance(newarg, Tuple):
58                if len(args) == 1:
59                    raise ValueError("tuples inside of tuple indices are not supported. Did you mean to call Tuple(*args) instead of Tuple(args)?")
60                raise ValueError("tuples inside of tuple indices are not supported. If you meant to use a fancy index, use a list or array instead.")
61            newargs.append(newarg)
62            if isinstance(newarg, ArrayIndex):
63                array_block_start = True
64                if _is_boolean_scalar(newarg):
65                    has_boolean_scalar = True
66                elif isinstance(newarg, BooleanArray):
67                    arrays.extend(newarg.raw.nonzero())
68                else:
69                    arrays.append(newarg.raw)
70            elif has_array and isinstance(newarg, Integer):
71                array_block_start = True
72            if isinstance(newarg, (Slice, ellipsis, Newaxis)) and array_block_start:
73                array_block_stop = True
74            elif isinstance(newarg, (ArrayIndex, Integer)):
75                if array_block_start and array_block_stop:
76                    # If the arrays in a tuple index are separated by a slice,
77                    # ellipsis, or newaxis, the behavior is that the
78                    # dimensions indexed by the array (and integer) indices
79                    # are added to the front of the final array shape. Travis
80                    # told me that he regrets implementing this behavior in
81                    # NumPy and that he wishes it were in error. So for now,
82                    # that is what we are going to do, unless it turns out
83                    # that we actually need it.
84                    raise NotImplementedError("Array indices separated by slices, ellipses (...), or newaxes (None) are not supported")
85
86        if newargs.count(...) > 1:
87            raise IndexError("an index can only have a single ellipsis ('...')")
88        if len(arrays) > 0:
89            if has_boolean_scalar:
90                raise NotImplementedError("Tuples mixing boolean scalars (True or False) with arrays are not yet supported.")
91
92            try:
93                broadcast(*[i for i in arrays])
94            except ValueError as e:
95                assert e.args == ("shape mismatch: objects cannot be broadcast to a single shape",)
96                raise IndexError("shape mismatch: indexing arrays could not be broadcast together with shapes %s" % ' '.join([str(i.shape) for i in arrays]))
97
98        return tuple(newargs)
99
100    def __eq__(self, other):
101        if isinstance(other, tuple):
102            return self.args == other
103        elif isinstance(other, Tuple):
104            return self.args == other.args
105        return False
106
107    def __hash__(self):
108        # Since self.args is itself a tuple, it will match the hash of
109        # self.raw when it is hashable.
110        return hash(self.args)
111
112    def __repr__(self):
113        # Since tuples are nested, we can print the raw form of the args to
114        # make them a little more readable.
115        def _repr(s):
116            if s == ...:
117                return '...'
118            if isinstance(s, ArrayIndex):
119                if s.shape and 0 not in s.shape:
120                    return repr(s.array.tolist())
121                return repr(s)
122            return repr(s.raw)
123        return f"{self.__class__.__name__}({', '.join(map(_repr, self.args))})"
124
125    def __str__(self):
126        # Since tuples are nested, we can print the raw form of the args to
127        # make them a little more readable.
128        def _str(s):
129            if s == ...:
130                return '...'
131            if isinstance(s, ArrayIndex):
132                return str(s)
133            return str(s.raw)
134        return f"{self.__class__.__name__}({', '.join(map(_str, self.args))})"
135
136    @property
137    def has_ellipsis(self):
138        """
139        Returns True if self has an ellipsis
140        """
141        return ... in self.args
142
143    @property
144    def ellipsis_index(self):
145        """
146        Give the index i of `self.args` where the ellipsis is.
147
148        If `self` doesn't have an ellipsis, it gives `len(self.args)`, since
149        tuple indices without an ellipsis always implicitly end in an
150        ellipsis.
151
152        The resulting value `i` is such that `self.args[:i]` indexes the
153        beginning axes of an array and `self.args[i+1:]` indexes the end axes
154        of an array.
155
156        >>> from ndindex import Tuple
157        >>> idx = Tuple(0, 1, ..., 2, 3)
158        >>> i = idx.ellipsis_index
159        >>> i
160        2
161        >>> idx.args[:i]
162        (Integer(0), Integer(1))
163        >>> idx.args[i+1:]
164        (Integer(2), Integer(3))
165
166        >>> Tuple(0, 1).ellipsis_index
167        2
168
169        """
170        if self.has_ellipsis:
171            return self.args.index(...)
172        return len(self.args)
173
174    @property
175    def raw(self):
176        return tuple(i.raw for i in self.args)
177
178    def reduce(self, shape=None):
179        r"""
180        Reduce a Tuple index on an array of shape `shape`
181
182        A `Tuple` with a single argument is always reduced to that single
183        argument (because `a[idx,]` is the same as `a[idx]`).
184
185        >>> from ndindex import Tuple
186
187        >>> Tuple(slice(2, 4)).reduce()
188        Slice(2, 4, 1)
189
190        If an explicit array shape is given, the result will either be
191        `IndexError` if the index is invalid for the given shape, or an index
192        that is as simple as possible:
193
194        - All the elements of the :any:`Tuple` are recursively :any:`reduced
195          <NDIndex.reduce>`.
196
197        - Any axes that can be merged into an :any:`ellipsis` are removed.
198          This includes the implicit ellipsis at the end of a Tuple that
199          doesn't contain any explicit ellipses.
200
201        - :any:`Ellipses <ellipsis>` that don't match any axes are removed.
202
203        - An :any:`ellipsis` at the end of the :any:`Tuple` is removed.
204
205        - Scalar :any:`BooleanArray` arguments (`True` or `False`) are
206          combined into a single term (the first boolean scalar is replaced
207          with the AND of all the boolean scalars).
208
209        - If the resulting :any:`Tuple` would have a single argument, that
210          argument is returned.
211
212        >>> idx = Tuple(0, ..., slice(0, 3))
213        >>> idx.reduce((5, 4))
214        Tuple(0, slice(0, 3, 1))
215        >>> idx.reduce((5, 3))
216        Integer(0)
217
218        >>> idx = Tuple(slice(0, 10), -3)
219        >>> idx.reduce((5,))
220        Traceback (most recent call last):
221        ...
222        IndexError: too many indices for array: array is 1-dimensional, but 2 were indexed
223        >>> idx.reduce((5, 2))
224        Traceback (most recent call last):
225        ...
226        IndexError: index -3 is out of bounds for axis 1 with size 2
227
228        Note
229        ====
230
231        ndindex presently does not distinguish between scalar objects and
232        rank-0 arrays. It is possible for the original index to produce one
233        and the reduced index to produce the other. In particular, the
234        presence of a redundant ellipsis forces NumPy to return a rank-0 array
235        instead of a scalar.
236
237        >>> import numpy as np
238        >>> a = np.array([0, 1])
239        >>> Tuple(..., 1).reduce(a.shape)
240        Integer(1)
241        >>> a[..., 1]
242        array(1)
243        >>> a[1]
244        1
245
246        See https://github.com/Quansight-Labs/ndindex/issues/22.
247
248        See Also
249        ========
250
251        .Tuple.expand
252        .NDIndex.reduce
253        .Slice.reduce
254        .Integer.reduce
255        .ellipsis.reduce
256        .Newaxis.reduce
257        .IntegerArray.reduce
258        .BooleanArray.reduce
259
260        """
261        args = list(self.args)
262        if ... not in args:
263            return type(self)(*args, ...).reduce(shape)
264
265        boolean_scalars = [i for i in args if _is_boolean_scalar(i)]
266        if len(boolean_scalars) > 1:
267            _args = []
268            seen_boolean_scalar = False
269            for s in args:
270                if _is_boolean_scalar(s):
271                    if seen_boolean_scalar:
272                        continue
273                    _args.append(BooleanArray(all(i == True for i in boolean_scalars)))
274                    seen_boolean_scalar = True
275                else:
276                    _args.append(s)
277            return type(self)(*_args).reduce(shape)
278
279        arrays = []
280        for i in args:
281            if _is_boolean_scalar(i):
282                continue
283            elif isinstance(i, IntegerArray):
284                arrays.append(i.raw)
285            elif isinstance(i, BooleanArray):
286                # TODO: Avoid explicitly calling nonzero
287                arrays.extend(i.raw.nonzero())
288        if not arrays:
289            # Older versions of NumPy do not allow broadcast() with no arguments
290            broadcast_shape = ()
291        else:
292            broadcast_shape = broadcast(*arrays).shape
293        # If the broadcast shape is empty, out of bounds indices in
294        # non-empty arrays are ignored, e.g., ([], [10]) would broadcast to
295        # ([], []), so the bounds for 10 are not checked. Thus, we must do
296        # this before calling reduce() on the arguments. This rule, however,
297        # is *not* followed for scalar integer indices.
298        if 0 in broadcast_shape:
299            for i in range(len(args)):
300                s = args[i]
301                if isinstance(s, IntegerArray):
302                    if s.ndim == 0:
303                        args[i] = Integer(s.raw)
304                    else:
305                        # broadcast_to(x) gives a readonly view on x, which is also
306                        # readonly, so set _copy=False to avoid representing the full
307                        # broadcasted array in memory.
308                        args[i] = type(s)(broadcast_to(s.raw, broadcast_shape),
309                                          _copy=False)
310
311        if shape is not None:
312            # assert self.args.count(...) == 1
313            # assert self.args.count(False) <= 1
314            # assert self.args.count(True) <= 1
315            n_newaxis = self.args.count(None)
316            n_boolean = sum(i.ndim - 1 for i in args if
317                            isinstance(i, BooleanArray) and not _is_boolean_scalar(i))
318            if True in args or False in args:
319                n_boolean -= 1
320            indexed_args = len(args) + n_boolean - n_newaxis - 1 # -1 for the
321
322            shape = asshape(shape, axis=indexed_args - 1)
323
324        ellipsis_i = self.ellipsis_index
325
326        preargs = []
327        removable = shape is not None
328        begin_offset = args[:ellipsis_i].count(None)
329        begin_offset -= sum(j.ndim - 1 for j in args[:ellipsis_i] if
330                            isinstance(j, BooleanArray))
331        for i, s in enumerate(reversed(args[:ellipsis_i]), start=1):
332            if s == None:
333                begin_offset -= 1
334            elif isinstance(s, BooleanArray):
335                begin_offset += s.ndim - 1
336            axis = ellipsis_i - i - begin_offset
337            reduced = s.reduce(shape, axis=axis)
338            if (removable
339                and isinstance(reduced, Slice)
340                and reduced == Slice(0, shape[axis], 1)):
341                continue
342            else:
343                removable = False
344                preargs.insert(0, reduced)
345
346        if shape is None:
347            endargs = [s.reduce() for s in args[ellipsis_i+1:]]
348        else:
349            endargs = []
350            end_offset = 0
351            for i, s in enumerate(reversed(args[ellipsis_i+1:]), start=1):
352                if isinstance(s, BooleanArray):
353                    end_offset -= s.ndim - 1
354                elif s == None:
355                    end_offset += 1
356                axis = len(shape) - i + end_offset
357                if not (isinstance(s, IntegerArray) and (0 in broadcast_shape or
358                                                         False in args)):
359                    # Array bounds are not checked when the broadcast shape is empty
360                    s = s.reduce(shape, axis=axis)
361                endargs.insert(0, s)
362
363        if shape is not None:
364            # Remove redundant slices
365            axis = len(shape) - len(endargs) + end_offset
366            for i, s in enumerate(endargs):
367                axis += i
368                if (isinstance(s, Slice)
369                    and s == Slice(0, shape[axis], 1)):
370                    i += 1
371                    continue
372                else:
373                    break
374            if endargs:
375                endargs = endargs[i:]
376
377        if shape is None or (endargs and len(preargs) + len(endargs)
378                             < len(shape) + args.count(None) - n_boolean):
379            preargs = preargs + [...]
380
381        newargs = preargs + endargs
382
383        if newargs and newargs[-1] == ...:
384            newargs = newargs[:-1]
385
386        if len(newargs) == 1:
387            return newargs[0]
388
389        return type(self)(*newargs)
390
391    def broadcast_arrays(self):
392        args = self.args
393        boolean_scalars = [i for i in args if _is_boolean_scalar(i)]
394        if len(boolean_scalars) > 1:
395            _args = []
396            seen_boolean_scalar = False
397            for s in args:
398                if _is_boolean_scalar(s):
399                    if seen_boolean_scalar:
400                        continue
401                    _args.append(BooleanArray(all(i == True for i in boolean_scalars)))
402                    seen_boolean_scalar = True
403                else:
404                    _args.append(s)
405            return type(self)(*_args).broadcast_arrays()
406
407        # Broadcast all array indices. Note that broadcastability is checked
408        # in the Tuple constructor, so this should not fail.
409        boolean_nonzero = {}
410        arrays = []
411        for s in args:
412            if _is_boolean_scalar(s):
413                continue
414            elif isinstance(s, IntegerArray):
415                arrays.append(s.raw)
416            elif isinstance(s, BooleanArray):
417                nz = s.raw.nonzero()
418                arrays.extend(nz)
419                boolean_nonzero[s] = nz
420        if not arrays:
421            return self
422        broadcast_shape = broadcast(*arrays).shape
423
424        newargs = []
425        for s in args:
426            if isinstance(s, BooleanArray):
427                if not _is_boolean_scalar(s):
428                    newargs.extend([IntegerArray(broadcast_to(i, broadcast_shape))
429                                    for i in boolean_nonzero[s]])
430            elif isinstance(s, Integer):
431                # broadcast_to(x) gives a readonly view on x, which is also
432                # readonly, so set _copy=False to avoid representing the full
433                # broadcasted array in memory.
434                newargs.append(IntegerArray(broadcast_to(array(s.raw, dtype=intp),
435                                              broadcast_shape), _copy=False))
436            elif isinstance(s, IntegerArray):
437                newargs.append(IntegerArray(broadcast_to(s.raw, broadcast_shape),
438                                            _copy=False))
439            else:
440                newargs.append(s)
441        return Tuple(*newargs)
442
443    def expand(self, shape):
444        # The expand() docstring is on NDIndex.expand()
445        args = list(self.args)
446        if ... not in args:
447            return type(self)(*args, ...).expand(shape)
448
449        # TODO: Use broadcast_arrays here. The challenge is that we still need
450        # to do bounds checks on nonscalar integer arrays that get broadcast
451        # away.
452        boolean_scalars = [i for i in args if _is_boolean_scalar(i)]
453        if len(boolean_scalars) > 1:
454            _args = []
455            seen_boolean_scalar = False
456            for s in args:
457                if _is_boolean_scalar(s):
458                    if seen_boolean_scalar:
459                        continue
460                    _args.append(BooleanArray(all(i == True for i in boolean_scalars)))
461                    seen_boolean_scalar = True
462                else:
463                    _args.append(s)
464            return type(self)(*_args).expand(shape)
465
466        # Broadcast all array indices. Note that broadcastability is checked
467        # in the Tuple constructor, so this should not fail.
468        arrays = []
469        for i in args:
470            if _is_boolean_scalar(i):
471                continue
472            elif isinstance(i, IntegerArray):
473                arrays.append(i.raw)
474            elif isinstance(i, BooleanArray):
475                # TODO: Avoid calling nonzero twice
476                arrays.extend(i.raw.nonzero())
477        if not arrays:
478            # Older versions of NumPy do not allow broadcast() with no arguments
479            broadcast_shape = ()
480        else:
481            broadcast_shape = broadcast(*arrays).shape
482        # If the broadcast shape is empty, out of bounds indices in
483        # non-empty arrays are ignored, e.g., ([], [10]) would broadcast to
484        # ([], []), so the bounds for 10 are not checked. Thus, we must do
485        # this before calling reduce() on the arguments. This rule, however,
486        # is *not* followed for scalar integer indices.
487        if arrays:
488            for i in range(len(args)):
489                s = args[i]
490                if isinstance(s, IntegerArray):
491                    if s.ndim == 0:
492                        args[i] = Integer(s.raw)
493                    else:
494                        # broadcast_to(x) gives a readonly view on x, which is also
495                        # readonly, so set _copy=False to avoid representing the full
496                        # broadcasted array in memory.
497                        args[i] = type(s)(broadcast_to(s.raw, broadcast_shape),
498                                          _copy=False)
499
500        # assert args.count(...) == 1
501        # assert args.count(False) <= 1
502        # assert args.count(True) <= 1
503        n_newaxis = args.count(None)
504        n_boolean = sum(i.ndim - 1 for i in args if
505                        isinstance(i, BooleanArray) and not _is_boolean_scalar(i))
506        if True in args or False in args:
507            n_boolean -= 1
508        indexed_args = len(args) + n_boolean - n_newaxis - 1 # -1 for the ellipsis
509        shape = asshape(shape, axis=indexed_args - 1)
510
511        ellipsis_i = self.ellipsis_index
512
513        startargs = []
514        begin_offset = 0
515        for i, s in enumerate(args[:ellipsis_i]):
516            axis = i + begin_offset
517            if not (isinstance(s, IntegerArray) and (0 in broadcast_shape or
518                                                     False in args)):
519                s = s.reduce(shape, axis=axis)
520            if isinstance(s, ArrayIndex):
521                if isinstance(s, BooleanArray):
522                    begin_offset += s.ndim - 1
523                    if not _is_boolean_scalar(s):
524                        s = s.reduce(shape, axis=axis)
525                        startargs.extend([IntegerArray(broadcast_to(i,
526                                                                  broadcast_shape))
527                                        for i in s.array.nonzero()])
528                        continue
529            elif arrays and isinstance(s, Integer):
530                s = IntegerArray(broadcast_to(array(s.raw, dtype=intp),
531                                              broadcast_shape), _copy=False)
532            elif s == None:
533                begin_offset -= 1
534            startargs.append(s)
535
536        # TODO: Merge this with the above loop
537        endargs = []
538        end_offset = 0
539        for i, s in enumerate(reversed(args[ellipsis_i+1:]), start=1):
540            if isinstance(s, ArrayIndex):
541                if isinstance(s, BooleanArray):
542                    end_offset -= s.ndim - 1
543                    if not _is_boolean_scalar(s):
544                        s = s.reduce(shape, axis=len(shape) - i + end_offset)
545                        endargs.extend([IntegerArray(broadcast_to(i,
546                                                                  broadcast_shape))
547                                        for i in reversed(s.array.nonzero())])
548                        continue
549            elif arrays and isinstance(s, Integer):
550                if (0 in broadcast_shape or False in args):
551                    s = s.reduce(shape, axis=len(shape)-i+end_offset)
552                s = IntegerArray(broadcast_to(array(s.raw, dtype=intp),
553                                              broadcast_shape), _copy=False)
554            elif s == None:
555                end_offset += 1
556            axis = len(shape) - i + end_offset
557            assert axis >= 0
558            if not (isinstance(s, IntegerArray) and (0 in broadcast_shape or
559                                                     False in args)):
560                # Array bounds are not checked when the broadcast shape is empty
561                s = s.reduce(shape, axis=axis)
562            endargs.append(s)
563
564        idx_offset = begin_offset - end_offset
565
566        midargs = [Slice(None).reduce(shape, axis=i + ellipsis_i + begin_offset) for
567                        i in range(len(shape) - len(args) + 1 - idx_offset)]
568
569
570        newargs = startargs + midargs + endargs[::-1]
571
572        return type(self)(*newargs)
573
574    def newshape(self, shape):
575        # The docstring for this method is on the NDIndex base class
576        shape = asshape(shape)
577
578        if self == Tuple():
579            return shape
580
581        # This will raise any IndexErrors
582        self = self.expand(shape)
583
584        newshape = []
585        axis = 0
586        arrays = False
587        for i, s in enumerate(self.args):
588            if s == None:
589                newshape.append(1)
590                axis -= 1
591            # After expand(), there will be at most one boolean scalar
592            elif s == True:
593                newshape.append(1)
594                axis -= 1
595            elif s == False:
596                newshape.append(0)
597                axis -= 1
598            elif isinstance(s, ArrayIndex):
599                if not arrays:
600                    # Multiple arrays are all broadcast together (in expand())
601                    # and iterated as one, so we only need to get the shape
602                    # for the first array we see. Note that arrays separated
603                    # by ellipses, slices, or newaxes affect the shape
604                    # differently, but these are currently unsupported (see
605                    # the comments in the Tuple constructor).
606
607                    # expand() should remove all non scalar boolean arrays
608                    assert not isinstance(s, BooleanArray)
609
610                    newshape.extend(list(s.newshape(shape[axis])))
611                    arrays = True
612            else:
613                newshape.extend(list(s.newshape(shape[axis])))
614            axis += 1
615        return tuple(newshape)
616
617    def as_subindex(self, index):
618        index = ndindex(index).reduce().broadcast_arrays()
619
620        self = self.broadcast_arrays()
621
622        if ... in self.args:
623            raise NotImplementedError("Tuple.as_subindex() is not yet implemented for tuples with ellipses")
624
625        if isinstance(index, (Integer, ArrayIndex, Slice)):
626            index = Tuple(index)
627        if isinstance(index, Tuple):
628            new_args = []
629            boolean_arrays = []
630            integer_arrays = []
631            if any(isinstance(i, Slice) and i.step < 0 for i in index.args):
632                    raise NotImplementedError("Tuple.as_subindex() is only implemented on slices with positive steps")
633            if ... in index.args:
634                raise NotImplementedError("Tuple.as_subindex() is not yet implemented for tuples with ellipses")
635            for self_arg, index_arg in zip(self.args, index.args):
636                if (isinstance(self_arg, IntegerArray) and
637                    isinstance(index_arg, Slice)):
638                    if (self_arg.array < 0).any():
639                        raise NotImplementedError("IntegerArray.as_subindex() is only implemented for arrays with all nonnegative entries. Try calling reduce() with a shape first.")
640                    if index_arg.step < 0:
641                        raise NotImplementedError("IntegerArray.as_subindex(Slice) is only implemented for slices with positive steps")
642
643                    # After reducing, start is not None when step > 0
644                    if index_arg.stop is None or index_arg.start < 0 or index_arg.stop < 0:
645                        raise NotImplementedError("IntegerArray.as_subindex(Slice) is only implemented for slices with nonnegative start and stop. Try calling reduce() with a shape first.")
646
647                    s = self_arg.array
648                    start, stop, step = subindex_slice(
649                        s, s+1, 1, index_arg.start, index_arg.stop, index_arg.step)
650                    if (stop <= 0).all():
651                        raise ValueError("Indices do not intersect")
652                    if start.shape == ():
653                        if start >= stop:
654                            raise ValueError("Indices do not intersect")
655
656                    integer_arrays.append((start, stop))
657                    # Placeholder. We need to mask out the stops below.
658                    new_args.append(IntegerArray(start))
659                else:
660                    subindex = self_arg.as_subindex(index_arg)
661                    if isinstance(subindex, Tuple):
662                        assert subindex == ()
663                        subindex # Workaround https://github.com/nedbat/coveragepy/issues/1029
664                        continue
665                    if isinstance(subindex, BooleanArray):
666                        boolean_arrays.append(subindex)
667                    new_args.append(subindex)
668            args_remainder = self.args[min(len(self.args), len(index.args)):]
669            index_remainder = index.args[min(len(self.args), len(index.args)):]
670            if any(isinstance(i, ArrayIndex) and i.isempty() for i in
671                   index_remainder):
672                raise ValueError("Indices do not intersect")
673            for arg in args_remainder:
674                if isinstance(arg, BooleanArray):
675                    boolean_arrays.append(arg)
676                if isinstance(arg, IntegerArray):
677                    integer_arrays.append((arg.array, arg.array+1))
678                new_args.append(arg)
679            # Replace all boolean arrays with the logical AND of them.
680            if any(i.isempty() for i in boolean_arrays):
681                raise ValueError("Indices do not intersect")
682            if boolean_arrays:
683                if len(boolean_arrays) > 1:
684                    new_array = BooleanArray(logical_and.reduce([i.array for i in boolean_arrays]))
685                else:
686                    new_array = boolean_arrays[0]
687                new_args2 = []
688                first = True
689                for arg in new_args:
690                    if arg in boolean_arrays:
691                        if first:
692                            new_args2.append(new_array)
693                            first = False
694                    else:
695                        new_args2.append(arg)
696                new_args = new_args2
697
698            # Mask out integer arrays to only where the start is less than the
699            # stop for all arrays.
700            if integer_arrays:
701                starts, stops = zip(*integer_arrays)
702                starts = array(broadcast_arrays(*starts))
703                stops = array(broadcast_arrays(*stops))
704                mask = logical_and.reduce(starts < stops, axis=0)
705                new_args2 = []
706                i = 0
707                for arg in new_args:
708                    if isinstance(arg, IntegerArray):
709                        if mask.ndim == 0:
710                            # Integer arrays always result in a 1 dimensional
711                            # result, except when we have a scalar, we want to
712                            # have a 0 dimensional result to match Integer().
713                            new_args2.append(IntegerArray(starts[i]))
714                        elif mask.all():
715                            new_args2.append(IntegerArray(starts[i]))
716                        else:
717                            new_args2.append(IntegerArray(starts[i, mask]))
718                        if new_args2[-1].isempty():
719                            raise ValueError("Indices do not intersect")
720                        i += 1
721                    else:
722                        new_args2.append(arg)
723                new_args = new_args2
724            return Tuple(*new_args)
725        raise NotImplementedError(f"Tuple.as_subindex() is not implemented for type '{type(index).__name__}")
726
727    def isempty(self, shape=None):
728        if shape is not None:
729            return 0 in self.newshape(shape)
730
731        return any(i.isempty() for i in self.args)
732
733# Imports at the bottom to avoid circular import issues
734from .array import ArrayIndex
735from .ellipsis import ellipsis
736from .newaxis import Newaxis
737from .slice import Slice
738from .integer import Integer
739from .booleanarray import BooleanArray, _is_boolean_scalar
740from .integerarray import IntegerArray
741