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