dask-array 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. dask_array/__init__.py +228 -0
  2. dask_array/_backends.py +76 -0
  3. dask_array/_backends_array.py +99 -0
  4. dask_array/_blockwise.py +1410 -0
  5. dask_array/_broadcast.py +272 -0
  6. dask_array/_chunk.py +445 -0
  7. dask_array/_chunk_types.py +54 -0
  8. dask_array/_collection.py +1644 -0
  9. dask_array/_concatenate.py +331 -0
  10. dask_array/_core_utils.py +1365 -0
  11. dask_array/_dispatch.py +141 -0
  12. dask_array/_einsum.py +277 -0
  13. dask_array/_expr.py +544 -0
  14. dask_array/_expr_flow.py +586 -0
  15. dask_array/_gufunc.py +805 -0
  16. dask_array/_histogram.py +617 -0
  17. dask_array/_map_blocks.py +652 -0
  18. dask_array/_new_collection.py +10 -0
  19. dask_array/_numpy_compat.py +135 -0
  20. dask_array/_overlap.py +1159 -0
  21. dask_array/_rechunk.py +1050 -0
  22. dask_array/_reshape.py +710 -0
  23. dask_array/_routines.py +102 -0
  24. dask_array/_shuffle.py +448 -0
  25. dask_array/_stack.py +264 -0
  26. dask_array/_svg.py +291 -0
  27. dask_array/_templates.py +29 -0
  28. dask_array/_test_utils.py +257 -0
  29. dask_array/_ufunc.py +385 -0
  30. dask_array/_utils.py +349 -0
  31. dask_array/_visualize.py +223 -0
  32. dask_array/_xarray.py +337 -0
  33. dask_array/core/__init__.py +34 -0
  34. dask_array/core/_blockwise_funcs.py +312 -0
  35. dask_array/core/_conversion.py +422 -0
  36. dask_array/core/_from_graph.py +97 -0
  37. dask_array/creation/__init__.py +71 -0
  38. dask_array/creation/_arange.py +121 -0
  39. dask_array/creation/_diag.py +116 -0
  40. dask_array/creation/_diagonal.py +241 -0
  41. dask_array/creation/_eye.py +103 -0
  42. dask_array/creation/_linspace.py +102 -0
  43. dask_array/creation/_mesh.py +134 -0
  44. dask_array/creation/_ones_zeros.py +454 -0
  45. dask_array/creation/_pad.py +270 -0
  46. dask_array/creation/_repeat.py +55 -0
  47. dask_array/creation/_tile.py +36 -0
  48. dask_array/creation/_tri.py +28 -0
  49. dask_array/creation/_utils.py +296 -0
  50. dask_array/fft.py +320 -0
  51. dask_array/io/__init__.py +39 -0
  52. dask_array/io/_base.py +10 -0
  53. dask_array/io/_from_array.py +257 -0
  54. dask_array/io/_from_delayed.py +95 -0
  55. dask_array/io/_from_graph.py +54 -0
  56. dask_array/io/_from_npy_stack.py +67 -0
  57. dask_array/io/_store.py +336 -0
  58. dask_array/io/_tiledb.py +159 -0
  59. dask_array/io/_to_npy_stack.py +65 -0
  60. dask_array/io/_zarr.py +449 -0
  61. dask_array/linalg/__init__.py +39 -0
  62. dask_array/linalg/_cholesky.py +234 -0
  63. dask_array/linalg/_lu.py +300 -0
  64. dask_array/linalg/_norm.py +94 -0
  65. dask_array/linalg/_qr.py +601 -0
  66. dask_array/linalg/_solve.py +349 -0
  67. dask_array/linalg/_svd.py +394 -0
  68. dask_array/linalg/_tensordot.py +334 -0
  69. dask_array/linalg/_utils.py +74 -0
  70. dask_array/manipulation/__init__.py +45 -0
  71. dask_array/manipulation/_expand.py +321 -0
  72. dask_array/manipulation/_flip.py +92 -0
  73. dask_array/manipulation/_roll.py +78 -0
  74. dask_array/manipulation/_transpose.py +309 -0
  75. dask_array/random/__init__.py +125 -0
  76. dask_array/random/_choice.py +181 -0
  77. dask_array/random/_expr.py +256 -0
  78. dask_array/random/_generator.py +441 -0
  79. dask_array/random/_random_state.py +259 -0
  80. dask_array/random/_utils.py +84 -0
  81. dask_array/reductions/__init__.py +84 -0
  82. dask_array/reductions/_arg_reduction.py +130 -0
  83. dask_array/reductions/_common.py +1082 -0
  84. dask_array/reductions/_cumulative.py +522 -0
  85. dask_array/reductions/_percentile.py +261 -0
  86. dask_array/reductions/_reduction.py +725 -0
  87. dask_array/reductions/_trace.py +56 -0
  88. dask_array/routines/__init__.py +133 -0
  89. dask_array/routines/_apply.py +84 -0
  90. dask_array/routines/_bincount.py +112 -0
  91. dask_array/routines/_broadcast.py +111 -0
  92. dask_array/routines/_coarsen.py +115 -0
  93. dask_array/routines/_diff.py +79 -0
  94. dask_array/routines/_gradient.py +158 -0
  95. dask_array/routines/_indexing.py +65 -0
  96. dask_array/routines/_insert_delete.py +132 -0
  97. dask_array/routines/_misc.py +122 -0
  98. dask_array/routines/_nonzero.py +72 -0
  99. dask_array/routines/_search.py +123 -0
  100. dask_array/routines/_select.py +113 -0
  101. dask_array/routines/_statistics.py +171 -0
  102. dask_array/routines/_topk.py +82 -0
  103. dask_array/routines/_triangular.py +74 -0
  104. dask_array/routines/_unique.py +232 -0
  105. dask_array/routines/_where.py +62 -0
  106. dask_array/slicing/__init__.py +67 -0
  107. dask_array/slicing/_basic.py +550 -0
  108. dask_array/slicing/_blocks.py +138 -0
  109. dask_array/slicing/_bool_index.py +145 -0
  110. dask_array/slicing/_setitem.py +329 -0
  111. dask_array/slicing/_squeeze.py +101 -0
  112. dask_array/slicing/_utils.py +1133 -0
  113. dask_array/slicing/_vindex.py +282 -0
  114. dask_array/stacking/__init__.py +15 -0
  115. dask_array/stacking/_block.py +83 -0
  116. dask_array/stacking/_simple.py +58 -0
  117. dask_array/templates/array.html.j2 +48 -0
  118. dask_array/tests/__init__.py +0 -0
  119. dask_array/tests/conftest.py +22 -0
  120. dask_array/tests/test_api.py +40 -0
  121. dask_array/tests/test_binary_op_chunks.py +107 -0
  122. dask_array/tests/test_coarse_slice_through_blockwise.py +362 -0
  123. dask_array/tests/test_collection.py +799 -0
  124. dask_array/tests/test_creation.py +1102 -0
  125. dask_array/tests/test_expr_flow.py +143 -0
  126. dask_array/tests/test_linalg.py +1130 -0
  127. dask_array/tests/test_map_blocks_multi_output.py +104 -0
  128. dask_array/tests/test_rechunk_pushdown.py +214 -0
  129. dask_array/tests/test_reductions.py +1091 -0
  130. dask_array/tests/test_routines.py +2853 -0
  131. dask_array/tests/test_shuffle_chunks.py +67 -0
  132. dask_array/tests/test_slice_pushdown.py +968 -0
  133. dask_array/tests/test_slice_through_blockwise.py +678 -0
  134. dask_array/tests/test_slice_through_overlap.py +366 -0
  135. dask_array/tests/test_slice_through_reshape.py +272 -0
  136. dask_array/tests/test_slicing.py +839 -0
  137. dask_array/tests/test_transpose_slice_pushdown.py +208 -0
  138. dask_array/tests/test_visualize.py +94 -0
  139. dask_array/tests/test_xarray.py +193 -0
  140. dask_array-0.1.0.dist-info/METADATA +48 -0
  141. dask_array-0.1.0.dist-info/RECORD +144 -0
  142. dask_array-0.1.0.dist-info/WHEEL +4 -0
  143. dask_array-0.1.0.dist-info/entry_points.txt +2 -0
  144. dask_array-0.1.0.dist-info/licenses/LICENSE +29 -0
@@ -0,0 +1,1133 @@
1
+ """Slicing utility functions copied from dask.array.slicing.
2
+
3
+ These are local copies to reduce dependency on dask.array.slicing.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import bisect
9
+ import math
10
+ import warnings
11
+ from numbers import Integral, Number
12
+
13
+ import numpy as np
14
+ from tlz import memoize
15
+
16
+ from dask.base import is_dask_collection
17
+ from dask.utils import cached_cumsum, is_arraylike
18
+
19
+ colon = slice(None, None, None)
20
+
21
+
22
+ class SlicingNoop(Exception):
23
+ """This indicates that a slicing operation is a no-op. The caller has to handle this"""
24
+
25
+ pass
26
+
27
+
28
+ # ============================================================================
29
+ # Helper functions
30
+ # ============================================================================
31
+
32
+
33
+ def _array_like_safe(np_func, da_func, a, like, **kwargs):
34
+ """Helper for asanyarray_safe."""
35
+ from dask_array._collection import Array
36
+
37
+ if like is a and hasattr(a, "__array_function__"):
38
+ return a
39
+
40
+ if isinstance(like, Array):
41
+ return da_func(a, **kwargs)
42
+
43
+ # Handle dask arrays backed by cupy
44
+ if isinstance(a, Array):
45
+ try:
46
+ from dask.utils import is_cupy_type
47
+
48
+ if is_cupy_type(a._meta):
49
+ a = a.compute(scheduler="sync")
50
+ except ImportError:
51
+ pass
52
+
53
+ if hasattr(like, "__array_function__"):
54
+ return np_func(a, like=like, **kwargs)
55
+
56
+ if type(like).__module__.startswith("scipy.sparse"):
57
+ kwargs.pop("order", None)
58
+ if np.isscalar(a):
59
+ a = np.array([[a]])
60
+ return type(like)(a, **kwargs)
61
+
62
+ return np_func(a, **kwargs)
63
+
64
+
65
+ def asanyarray_safe(a, like, **kwargs):
66
+ """Convert to array using np.asanyarray with proper dispatching.
67
+
68
+ If a is dask.array, return dask.array.asanyarray(a, **kwargs),
69
+ otherwise return np.asanyarray(a, like=like, **kwargs), dispatching
70
+ the call to the library that implements the like array.
71
+ """
72
+ from dask_array.core._conversion import asanyarray
73
+
74
+ return _array_like_safe(np.asanyarray, asanyarray, a, like, **kwargs)
75
+
76
+
77
+ # ============================================================================
78
+ # sanitize_index and helpers
79
+ # ============================================================================
80
+
81
+
82
+ def _sanitize_index_element(ind):
83
+ """Sanitize a one-element index."""
84
+ if isinstance(ind, Number):
85
+ ind2 = int(ind)
86
+ if ind2 != ind:
87
+ raise IndexError(f"Bad index. Must be integer-like: {ind}")
88
+ else:
89
+ return ind2
90
+ elif ind is None:
91
+ return None
92
+ elif is_dask_collection(ind):
93
+ if ind.dtype.kind != "i" or ind.size != 1:
94
+ raise IndexError(f"Bad index. Must be integer-like: {ind}")
95
+ return ind
96
+ else:
97
+ raise TypeError("Invalid index type", type(ind), ind)
98
+
99
+
100
+ def sanitize_index(ind):
101
+ """Sanitize the elements for indexing along one axis.
102
+
103
+ Examples
104
+ --------
105
+ >>> sanitize_index([2, 3, 5])
106
+ array([2, 3, 5])
107
+ >>> sanitize_index([True, False, True, False])
108
+ array([0, 2])
109
+ >>> sanitize_index(np.array([1, 2, 3]))
110
+ array([1, 2, 3])
111
+ >>> sanitize_index(np.array([False, True, True]))
112
+ array([1, 2])
113
+ >>> type(sanitize_index(np.int32(0)))
114
+ <class 'int'>
115
+ >>> sanitize_index(1.0)
116
+ 1
117
+ >>> sanitize_index(0.5)
118
+ Traceback (most recent call last):
119
+ ...
120
+ IndexError: Bad index. Must be integer-like: 0.5
121
+ """
122
+ if ind is None:
123
+ return None
124
+ elif isinstance(ind, slice):
125
+ return slice(
126
+ _sanitize_index_element(ind.start),
127
+ _sanitize_index_element(ind.stop),
128
+ _sanitize_index_element(ind.step),
129
+ )
130
+ elif isinstance(ind, Number):
131
+ return _sanitize_index_element(ind)
132
+ elif is_dask_collection(ind):
133
+ return ind
134
+ index_array = asanyarray_safe(ind, like=ind)
135
+ if index_array.dtype == bool:
136
+ nonzero = np.nonzero(index_array)
137
+ if len(nonzero) == 1:
138
+ nonzero = nonzero[0]
139
+ if is_arraylike(nonzero):
140
+ return nonzero
141
+ else:
142
+ return np.asanyarray(nonzero)
143
+ elif np.issubdtype(index_array.dtype, np.integer):
144
+ return index_array
145
+ elif np.issubdtype(index_array.dtype, np.floating):
146
+ int_index = index_array.astype(np.intp)
147
+ if np.allclose(index_array, int_index):
148
+ return int_index
149
+ else:
150
+ check_int = np.isclose(index_array, int_index)
151
+ first_err = index_array.ravel()[np.flatnonzero(~check_int)[0]]
152
+ raise IndexError(f"Bad index. Must be integer-like: {first_err}")
153
+ else:
154
+ raise TypeError("Invalid index type", type(ind), ind)
155
+
156
+
157
+ # ============================================================================
158
+ # replace_ellipsis
159
+ # ============================================================================
160
+
161
+
162
+ def replace_ellipsis(n, index):
163
+ """Replace ... with slices, :, :, :
164
+
165
+ Examples
166
+ --------
167
+ >>> replace_ellipsis(4, (3, Ellipsis, 2))
168
+ (3, slice(None, None, None), slice(None, None, None), 2)
169
+
170
+ >>> replace_ellipsis(2, (Ellipsis, None))
171
+ (slice(None, None, None), slice(None, None, None), None)
172
+ """
173
+ # Careful about using in or index because index may contain arrays
174
+ isellipsis = [i for i, ind in enumerate(index) if ind is Ellipsis]
175
+ if not isellipsis:
176
+ return index
177
+ else:
178
+ loc = isellipsis[0]
179
+ extra_dimensions = n - (len(index) - sum(i is None for i in index) - 1)
180
+ return index[:loc] + (slice(None, None, None),) * extra_dimensions + index[loc + 1 :]
181
+
182
+
183
+ # ============================================================================
184
+ # normalize_slice and helpers
185
+ # ============================================================================
186
+
187
+
188
+ def normalize_slice(idx, dim):
189
+ """Normalize slices to canonical form.
190
+
191
+ Parameters
192
+ ----------
193
+ idx: slice or other index
194
+ dim: dimension length
195
+
196
+ Examples
197
+ --------
198
+ >>> normalize_slice(slice(0, 10, 1), 10)
199
+ slice(None, None, None)
200
+ """
201
+ if isinstance(idx, slice):
202
+ if math.isnan(dim):
203
+ return idx
204
+ start, stop, step = idx.indices(dim)
205
+ if step > 0:
206
+ if start == 0:
207
+ start = None
208
+ if stop >= dim:
209
+ stop = None
210
+ if step == 1:
211
+ step = None
212
+ if stop is not None and start is not None and stop < start:
213
+ stop = start
214
+ elif step < 0:
215
+ if start >= dim - 1:
216
+ start = None
217
+ if stop < 0:
218
+ stop = None
219
+ return slice(start, stop, step)
220
+ return idx
221
+
222
+
223
+ # ============================================================================
224
+ # posify_index
225
+ # ============================================================================
226
+
227
+
228
+ def posify_index(shape, ind):
229
+ """Flip negative indices around to positive ones.
230
+
231
+ Examples
232
+ --------
233
+ >>> posify_index(10, 3)
234
+ 3
235
+ >>> posify_index(10, -3)
236
+ 7
237
+ >>> posify_index(10, [3, -3])
238
+ array([3, 7])
239
+
240
+ >>> posify_index((10, 20), (3, -3))
241
+ (3, 17)
242
+ >>> posify_index((10, 20), (3, [3, 4, -3])) # doctest: +NORMALIZE_WHITESPACE
243
+ (3, array([ 3, 4, 17]))
244
+ """
245
+ if isinstance(ind, tuple):
246
+ return tuple(map(posify_index, shape, ind))
247
+ if isinstance(ind, Integral):
248
+ if ind < 0 and not math.isnan(shape):
249
+ return ind + shape
250
+ else:
251
+ return ind
252
+ if isinstance(ind, (np.ndarray, list)) and not math.isnan(shape):
253
+ ind = np.asanyarray(ind)
254
+ return np.where(ind < 0, ind + shape, ind)
255
+ return ind
256
+
257
+
258
+ # ============================================================================
259
+ # check_index
260
+ # ============================================================================
261
+
262
+
263
+ def check_index(axis, ind, dimension):
264
+ """Check validity of index for a given dimension.
265
+
266
+ Examples
267
+ --------
268
+ >>> check_index(0, 3, 5)
269
+ >>> check_index(0, 5, 5)
270
+ Traceback (most recent call last):
271
+ ...
272
+ IndexError: Index 5 is out of bounds for axis 0 with size 5
273
+
274
+ >>> check_index(1, 6, 5)
275
+ Traceback (most recent call last):
276
+ ...
277
+ IndexError: Index 6 is out of bounds for axis 1 with size 5
278
+
279
+ >>> check_index(1, -1, 5)
280
+ >>> check_index(1, -6, 5)
281
+ Traceback (most recent call last):
282
+ ...
283
+ IndexError: Index -6 is out of bounds for axis 1 with size 5
284
+
285
+ >>> check_index(0, [1, 2], 5)
286
+ >>> check_index(0, [6, 3], 5)
287
+ Traceback (most recent call last):
288
+ ...
289
+ IndexError: Index is out of bounds for axis 0 with size 5
290
+
291
+ >>> check_index(1, slice(0, 3), 5)
292
+
293
+ >>> check_index(0, [True], 1)
294
+ >>> check_index(0, [True, True], 3)
295
+ Traceback (most recent call last):
296
+ ...
297
+ IndexError: Boolean array with size 2 is not long enough for axis 0 with size 3
298
+ >>> check_index(0, [True, True, True], 1)
299
+ Traceback (most recent call last):
300
+ ...
301
+ IndexError: Boolean array with size 3 is not long enough for axis 0 with size 1
302
+ """
303
+ if isinstance(ind, list):
304
+ ind = np.asanyarray(ind)
305
+
306
+ # unknown dimension, assumed to be in bounds
307
+ if np.isnan(dimension):
308
+ return
309
+ elif is_dask_collection(ind):
310
+ return
311
+ elif is_arraylike(ind):
312
+ if ind.dtype == bool:
313
+ if ind.size != dimension:
314
+ raise IndexError(
315
+ f"Boolean array with size {ind.size} is not long enough for axis {axis} with size {dimension}"
316
+ )
317
+ elif (ind >= dimension).any() or (ind < -dimension).any():
318
+ raise IndexError(f"Index is out of bounds for axis {axis} with size {dimension}")
319
+ elif isinstance(ind, slice):
320
+ return
321
+ elif ind is None:
322
+ return
323
+
324
+ elif ind >= dimension or ind < -dimension:
325
+ raise IndexError(f"Index {ind} is out of bounds for axis {axis} with size {dimension}")
326
+
327
+
328
+ # ============================================================================
329
+ # _slice_1d
330
+ # ============================================================================
331
+
332
+
333
+ def _slice_1d(dim_shape, lengths, index):
334
+ """Returns a dict of {blocknum: slice}.
335
+
336
+ This function figures out where each slice should start in each
337
+ block for a single dimension. If the slice won't return any elements
338
+ in the block, that block will not be in the output.
339
+
340
+ Parameters
341
+ ----------
342
+ dim_shape - the number of elements in this dimension.
343
+ This should be a positive, non-zero integer
344
+ lengths - the number of elements per block in this dimension
345
+ This should be a positive, non-zero integer
346
+ index - a description of the elements in this dimension that we want
347
+ This might be an integer, a slice(), or an Ellipsis
348
+
349
+ Returns
350
+ -------
351
+ dictionary where the keys are the integer index of the blocks that
352
+ should be sliced and the values are the slices
353
+
354
+ Examples
355
+ --------
356
+ Trivial slicing
357
+
358
+ >>> _slice_1d(100, [60, 40], slice(None, None, None))
359
+ {0: slice(None, None, None), 1: slice(None, None, None)}
360
+
361
+ 100 length array cut into length 20 pieces, slice 0:35
362
+
363
+ >>> _slice_1d(100, [20, 20, 20, 20, 20], slice(0, 35))
364
+ {0: slice(None, None, None), 1: slice(0, 15, 1)}
365
+
366
+ Support irregular blocks and various slices
367
+
368
+ >>> _slice_1d(100, [20, 10, 10, 10, 25, 25], slice(10, 35))
369
+ {0: slice(10, 20, 1), 1: slice(None, None, None), 2: slice(0, 5, 1)}
370
+
371
+ Support step sizes
372
+
373
+ >>> _slice_1d(100, [15, 14, 13], slice(10, 41, 3))
374
+ {0: slice(10, 15, 3), 1: slice(1, 14, 3), 2: slice(2, 12, 3)}
375
+
376
+ >>> _slice_1d(100, [20, 20, 20, 20, 20], slice(0, 100, 40)) # step > blocksize
377
+ {0: slice(0, 20, 40), 2: slice(0, 20, 40), 4: slice(0, 20, 40)}
378
+
379
+ Also support indexing single elements
380
+
381
+ >>> _slice_1d(100, [20, 20, 20, 20, 20], 25)
382
+ {1: 5}
383
+
384
+ And negative slicing
385
+
386
+ >>> _slice_1d(100, [20, 20, 20, 20, 20], slice(100, 0, -3)) # doctest: +NORMALIZE_WHITESPACE
387
+ {4: slice(-1, -21, -3),
388
+ 3: slice(-2, -21, -3),
389
+ 2: slice(-3, -21, -3),
390
+ 1: slice(-1, -21, -3),
391
+ 0: slice(-2, -20, -3)}
392
+
393
+ >>> _slice_1d(100, [20, 20, 20, 20, 20], slice(100, 12, -3)) # doctest: +NORMALIZE_WHITESPACE
394
+ {4: slice(-1, -21, -3),
395
+ 3: slice(-2, -21, -3),
396
+ 2: slice(-3, -21, -3),
397
+ 1: slice(-1, -21, -3),
398
+ 0: slice(-2, -8, -3)}
399
+
400
+ >>> _slice_1d(100, [20, 20, 20, 20, 20], slice(100, -12, -3))
401
+ {4: slice(-1, -12, -3)}
402
+ """
403
+ chunk_boundaries = cached_cumsum(lengths)
404
+
405
+ if isinstance(index, Integral):
406
+ # use right-side search to be consistent with previous result
407
+ i = bisect.bisect_right(chunk_boundaries, index)
408
+ if i > 0:
409
+ # the very first chunk has no relative shift
410
+ ind = index - chunk_boundaries[i - 1]
411
+ else:
412
+ ind = index
413
+ return {int(i): int(ind)}
414
+
415
+ assert isinstance(index, slice)
416
+
417
+ if index == colon:
418
+ return dict.fromkeys(range(len(lengths)), colon)
419
+
420
+ step = index.step or 1
421
+ if step > 0:
422
+ start = index.start or 0
423
+ stop = index.stop if index.stop is not None else dim_shape
424
+ else:
425
+ start = index.start if index.start is not None else dim_shape - 1
426
+ start = dim_shape - 1 if start >= dim_shape else start
427
+ stop = -(dim_shape + 1) if index.stop is None else index.stop
428
+
429
+ # posify start and stop
430
+ if start < 0:
431
+ start += dim_shape
432
+ if stop < 0:
433
+ stop += dim_shape
434
+
435
+ d = dict()
436
+ if step > 0:
437
+ istart = bisect.bisect_right(chunk_boundaries, start)
438
+ istop = bisect.bisect_left(chunk_boundaries, stop)
439
+
440
+ # the bound is not exactly tight; make it tighter?
441
+ istop = min(istop + 1, len(lengths))
442
+
443
+ # jump directly to istart
444
+ if istart > 0:
445
+ start = start - chunk_boundaries[istart - 1]
446
+ stop = stop - chunk_boundaries[istart - 1]
447
+
448
+ for i in range(istart, istop):
449
+ length = lengths[i]
450
+ if start < length and stop > 0:
451
+ d[i] = slice(start, min(stop, length), step)
452
+ start = (start - length) % step
453
+ else:
454
+ start = start - length
455
+ stop -= length
456
+ else:
457
+ rstart = start # running start
458
+
459
+ istart = bisect.bisect_left(chunk_boundaries, start)
460
+ istop = bisect.bisect_right(chunk_boundaries, stop)
461
+
462
+ # the bound is not exactly tight; make it tighter?
463
+ istart = min(istart + 1, len(chunk_boundaries) - 1)
464
+ istop = max(istop - 1, -1)
465
+
466
+ for i in range(istart, istop, -1):
467
+ chunk_stop = chunk_boundaries[i]
468
+ # create a chunk start and stop
469
+ if i == 0:
470
+ chunk_start = 0
471
+ else:
472
+ chunk_start = chunk_boundaries[i - 1]
473
+
474
+ # if our slice is in this chunk
475
+ if (chunk_start <= rstart < chunk_stop) and (rstart > stop):
476
+ d[i] = slice(
477
+ rstart - chunk_stop,
478
+ max(chunk_start - chunk_stop - 1, stop - chunk_stop),
479
+ step,
480
+ )
481
+
482
+ # compute the next running start point,
483
+ offset = (rstart - (chunk_start - 1)) % step
484
+ rstart = chunk_start + offset - 1
485
+
486
+ # replace 0:20:1 with : if appropriate
487
+ for k, v in d.items():
488
+ if v == slice(0, lengths[k], 1):
489
+ d[k] = slice(None, None, None)
490
+
491
+ if not d: # special case x[:0]
492
+ d[0] = slice(0, 0, 1)
493
+
494
+ return d
495
+
496
+
497
+ # ============================================================================
498
+ # new_blockdim
499
+ # ============================================================================
500
+
501
+
502
+ def new_blockdim(dim_shape, lengths, index):
503
+ """Compute new block dimension sizes after slicing.
504
+
505
+ Examples
506
+ --------
507
+ >>> new_blockdim(100, [20, 10, 20, 10, 40], slice(0, 90, 2))
508
+ [10, 5, 10, 5, 15]
509
+
510
+ >>> new_blockdim(100, [20, 10, 20, 10, 40], [5, 1, 30, 22])
511
+ [4]
512
+
513
+ >>> new_blockdim(100, [20, 10, 20, 10, 40], slice(90, 10, -2))
514
+ [16, 5, 10, 5, 4]
515
+ """
516
+ from operator import itemgetter
517
+
518
+ if index == slice(None, None, None):
519
+ return lengths
520
+ if isinstance(index, list):
521
+ return [len(index)]
522
+ assert not isinstance(index, Integral)
523
+ pairs = sorted(_slice_1d(dim_shape, lengths, index).items(), key=itemgetter(0))
524
+ slices = [slice(0, lengths[i], 1) if slc == slice(None, None, None) else slc for i, slc in pairs]
525
+ if isinstance(index, slice) and index.step and index.step < 0:
526
+ slices.reverse()
527
+ return [int(math.ceil((1.0 * slc.stop - slc.start) / slc.step)) for slc in slices]
528
+
529
+
530
+ # ============================================================================
531
+ # normalize_index (uses many helpers above)
532
+ # ============================================================================
533
+
534
+
535
+ def normalize_index(idx, shape):
536
+ """Normalize slicing indexes.
537
+
538
+ 1. Replaces ellipses with many full slices
539
+ 2. Adds full slices to end of index
540
+ 3. Checks bounding conditions
541
+ 4. Replace multidimensional numpy arrays with dask arrays
542
+ 5. Replaces numpy arrays with lists
543
+ 6. Posify's integers and lists
544
+ 7. Normalizes slices to canonical form
545
+
546
+ Examples
547
+ --------
548
+ >>> normalize_index(1, (10,))
549
+ (1,)
550
+ >>> normalize_index(-1, (10,))
551
+ (9,)
552
+ >>> normalize_index([-1], (10,))
553
+ (array([9]),)
554
+ >>> normalize_index(slice(-3, 10, 1), (10,))
555
+ (slice(7, None, None),)
556
+ >>> normalize_index((Ellipsis, None), (10,))
557
+ (slice(None, None, None), None)
558
+ """
559
+ from dask_array._collection import Array
560
+ from dask_array.core import from_array
561
+
562
+ if not isinstance(idx, tuple):
563
+ idx = (idx,)
564
+
565
+ # if a > 1D numpy.array is provided, cast it to a dask array
566
+ if len(idx) > 0 and len(shape) > 1:
567
+ i = idx[0]
568
+ if is_arraylike(i) and not isinstance(i, Array) and i.shape == shape:
569
+ idx = (from_array(i), *idx[1:])
570
+
571
+ idx = replace_ellipsis(len(shape), idx)
572
+ n_sliced_dims = 0
573
+ for i in idx:
574
+ if hasattr(i, "ndim") and i.ndim >= 1:
575
+ n_sliced_dims += i.ndim
576
+ elif i is None:
577
+ continue
578
+ else:
579
+ n_sliced_dims += 1
580
+
581
+ idx = idx + (slice(None),) * (len(shape) - n_sliced_dims)
582
+ if len([i for i in idx if i is not None]) > len(shape):
583
+ raise IndexError("Too many indices for array")
584
+
585
+ none_shape = []
586
+ i = 0
587
+ for ind in idx:
588
+ if ind is not None:
589
+ none_shape.append(shape[i])
590
+ i += 1
591
+ else:
592
+ none_shape.append(None)
593
+
594
+ for axis, (i, d) in enumerate(zip(idx, none_shape)):
595
+ if d is not None:
596
+ check_index(axis, i, d)
597
+ idx = tuple(map(sanitize_index, idx))
598
+ idx = tuple(map(normalize_slice, idx, none_shape))
599
+ idx = posify_index(none_shape, idx)
600
+ return idx
601
+
602
+
603
+ # ============================================================================
604
+ # parse_assignment_indices
605
+ # ============================================================================
606
+
607
+
608
+ def parse_assignment_indices(indices, shape):
609
+ """Reformat the indices for assignment.
610
+
611
+ The aim of this is to convert the indices to a standardised form
612
+ so that it is easier to ascertain which chunks are touched by the
613
+ indices.
614
+
615
+ This function is intended to be called by `setitem_array`.
616
+
617
+ A slice object that is decreasing (i.e. with a negative step), is
618
+ recast as an increasing slice (i.e. with a positive step. For
619
+ example ``slice(7,3,-1)`` would be cast as ``slice(4,8,1)``. This
620
+ is to facilitate finding which blocks are touched by the
621
+ index. The dimensions for which this has occurred are returned by
622
+ the function.
623
+
624
+ Parameters
625
+ ----------
626
+ indices : numpy-style indices
627
+ Indices to array defining the elements to be assigned.
628
+ shape : sequence of `int`
629
+ The shape of the array.
630
+
631
+ Returns
632
+ -------
633
+ parsed_indices : `list`
634
+ The reformatted indices that are equivalent to the input
635
+ indices.
636
+ implied_shape : `list`
637
+ The shape implied by the parsed indices. For instance, indices
638
+ of ``(slice(0,2), 5, [4,1,-1])`` will have implied shape
639
+ ``[2,3]``.
640
+ reverse : `list`
641
+ The positions of the dimensions whose indices in the
642
+ parsed_indices output are reversed slices.
643
+ implied_shape_positions: `list`
644
+ The positions of the dimensions whose indices contribute to
645
+ the implied_shape. For instance, indices of ``(slice(0,2), 5,
646
+ [4,1,-1])`` will have implied_shape ``[2,3]`` and
647
+ implied_shape_positions ``[0,2]``.
648
+
649
+ Examples
650
+ --------
651
+ >>> parse_assignment_indices((slice(1, -1),), (8,))
652
+ ([slice(1, 7, 1)], [6], [], [0])
653
+
654
+ >>> parse_assignment_indices(([1, 2, 6, 5],), (8,))
655
+ ([array([1, 2, 6, 5])], [4], [], [0])
656
+
657
+ >>> parse_assignment_indices((3, slice(-1, 2, -1)), (7, 8))
658
+ ([3, slice(3, 8, 1)], [5], [1], [1])
659
+
660
+ >>> parse_assignment_indices((slice(-1, 2, -1), 3, [1, 2]), (7, 8, 9))
661
+ ([slice(3, 7, 1), 3, array([1, 2])], [4, 2], [0], [0, 2])
662
+
663
+ >>> parse_assignment_indices((slice(0, 5), slice(3, None, 2)), (5, 4))
664
+ ([slice(0, 5, 1), slice(3, 4, 2)], [5, 1], [], [0, 1])
665
+
666
+ >>> parse_assignment_indices((slice(0, 5), slice(3, 3, 2)), (5, 4))
667
+ ([slice(0, 5, 1), slice(3, 3, 2)], [5, 0], [], [0])
668
+
669
+ """
670
+ if not isinstance(indices, tuple):
671
+ indices = (indices,)
672
+
673
+ # Disallow scalar boolean indexing, and also indexing by scalar
674
+ # numpy or dask array.
675
+ for index in indices:
676
+ if index is True or index is False:
677
+ raise NotImplementedError(f"dask does not yet implement assignment to a scalar boolean index: {index!r}")
678
+
679
+ if (is_arraylike(index) or is_dask_collection(index)) and not index.ndim:
680
+ raise NotImplementedError(
681
+ f"dask does not yet implement assignment to a scalar numpy or dask array index: {index!r}"
682
+ )
683
+
684
+ # Initialize output variables
685
+ implied_shape = []
686
+ implied_shape_positions = []
687
+ reverse = []
688
+ parsed_indices = list(normalize_index(indices, shape))
689
+
690
+ n_lists = 0
691
+
692
+ for i, (index, size) in enumerate(zip(parsed_indices, shape)):
693
+ is_slice = isinstance(index, slice)
694
+ if is_slice:
695
+ # Index is a slice
696
+ start, stop, step = index.indices(size)
697
+ if step < 0 and stop == -1:
698
+ stop = None
699
+
700
+ index = slice(start, stop, step)
701
+
702
+ if step < 0:
703
+ # When the slice step is negative, transform the
704
+ # original slice to a new slice with a positive step
705
+ # such that the result of the new slice is the reverse
706
+ # of the result of the original slice.
707
+ start, stop, step = index.indices(size)
708
+ step *= -1
709
+ div, mod = divmod(start - stop - 1, step)
710
+ div_step = div * step
711
+ start -= div_step
712
+ stop = start + div_step + 1
713
+
714
+ index = slice(start, stop, step)
715
+ reverse.append(i)
716
+
717
+ start, stop, step = index.indices(size)
718
+
719
+ # Note: We now have stop >= start and step >= 0
720
+
721
+ div, mod = divmod(stop - start, step)
722
+ if not div and not mod:
723
+ # stop equals start => zero-sized slice for this
724
+ # dimension
725
+ implied_shape.append(0)
726
+ else:
727
+ if mod != 0:
728
+ div += 1
729
+
730
+ implied_shape.append(div)
731
+ implied_shape_positions.append(i)
732
+
733
+ elif isinstance(index, (int, np.integer)):
734
+ # Index is an integer
735
+ index = int(index)
736
+
737
+ elif is_arraylike(index) or is_dask_collection(index):
738
+ # Index is 1-d array
739
+ n_lists += 1
740
+ if n_lists > 1:
741
+ raise NotImplementedError(
742
+ "dask is currently limited to at most one "
743
+ "dimension's assignment index being a "
744
+ "1-d array of integers or booleans. "
745
+ f"Got: {indices}"
746
+ )
747
+
748
+ if index.ndim != 1:
749
+ raise IndexError(f"Incorrect shape ({index.shape}) of integer indices for dimension with size {size}")
750
+
751
+ index_size = index.size
752
+ if index.dtype == bool and not math.isnan(index_size) and index_size != size:
753
+ raise IndexError(
754
+ "boolean index did not match indexed array along "
755
+ f"dimension {i}; dimension is {size} but "
756
+ f"corresponding boolean dimension is {index_size}"
757
+ )
758
+
759
+ # Posify an integer dask array (integer numpy arrays were
760
+ # posified in `normalize_index`)
761
+ if is_dask_collection(index):
762
+ if index.dtype == bool:
763
+ index_size = np.nan
764
+ else:
765
+ index = np.where(index < 0, index + size, index)
766
+
767
+ implied_shape.append(index_size)
768
+ implied_shape_positions.append(i)
769
+
770
+ parsed_indices[i] = index
771
+
772
+ return parsed_indices, implied_shape, reverse, implied_shape_positions
773
+
774
+
775
+ # ============================================================================
776
+ # setitem (chunk function)
777
+ # ============================================================================
778
+
779
+
780
+ def setitem(x, v, indices):
781
+ """Chunk function for array assignment.
782
+
783
+ Assign v to indices of x.
784
+
785
+ Parameters
786
+ ----------
787
+ x : numpy/cupy/etc. array
788
+ The array to be assigned to.
789
+ v : numpy/cupy/etc. array
790
+ The values which will be assigned.
791
+ indices : list of `slice`, `int`, or numpy array
792
+ The indices describing the elements of x to be assigned from
793
+ v. One index per axis.
794
+
795
+ Note that an individual index can not be a `list`, use a 1-d
796
+ numpy array instead.
797
+
798
+ If a 1-d numpy array index contains the non-valid value of the
799
+ size of the corresponding dimension of x, then those index
800
+ elements will be removed prior to the assignment (see
801
+ `block_index_from_1d_index` function).
802
+
803
+ Returns
804
+ -------
805
+ numpy/cupy/etc. array
806
+ A new independent array with assigned elements, unless v is
807
+ empty (i.e. has zero size) in which case then the input array
808
+ is returned and the indices are ignored.
809
+
810
+ Examples
811
+ --------
812
+ >>> x = np.arange(8).reshape(2, 4)
813
+ >>> setitem(x, np.array(-99), [np.array([False, True])])
814
+ array([[ 0, 1, 2, 3],
815
+ [-99, -99, -99, -99]])
816
+ >>> x
817
+ array([[0, 1, 2, 3],
818
+ [4, 5, 6, 7]])
819
+ >>> setitem(x, np.array([-88, -99]), [slice(None), np.array([1, 3])])
820
+ array([[ 0, -88, 2, -99],
821
+ [ 4, -88, 6, -99]])
822
+ >>> setitem(x, -x, [slice(None)])
823
+ array([[ 0, -1, -2, -3],
824
+ [-4, -5, -6, -7]])
825
+ >>> x
826
+ array([[0, 1, 2, 3],
827
+ [4, 5, 6, 7]])
828
+ >>> setitem(x, np.array([-88, -99]), [slice(None), np.array([4, 4, 3, 4, 1, 4])])
829
+ array([[ 0, -99, 2, -88],
830
+ [ 4, -99, 6, -88]])
831
+ >>> value = np.where(x < 0)[0]
832
+ >>> value.size
833
+ 0
834
+ >>> y = setitem(x, value, [Ellipsis])
835
+ >>> y is x
836
+ True
837
+ """
838
+ if not math.prod(v.shape):
839
+ return x
840
+
841
+ # Normalize integer array indices
842
+ for i, (index, block_size) in enumerate(zip(indices, x.shape)):
843
+ if isinstance(index, np.ndarray) and index.dtype != bool:
844
+ # Strip out any non-valid place-holder values
845
+ index = index[np.where(index < block_size)[0]]
846
+ indices[i] = index
847
+
848
+ # If x is not masked but v is, then turn the x into a masked
849
+ # array.
850
+ if not np.ma.isMA(x) and np.ma.isMA(v):
851
+ x = x.view(np.ma.MaskedArray)
852
+
853
+ # Copy the array to guarantee no other objects are corrupted.
854
+ # When x is the output of a scalar __getitem__ call, it is a
855
+ # np.generic, which is read-only. Convert it to a (writeable)
856
+ # 0-d array. x could also be a cupy array etc.
857
+ x = np.asarray(x) if isinstance(x, np.generic) else x.copy()
858
+
859
+ # Do the assignment
860
+ try:
861
+ x[tuple(indices)] = v
862
+ except ValueError as e:
863
+ raise ValueError("shape mismatch: value array could not be broadcast to indexing result") from e
864
+
865
+ return x
866
+
867
+
868
+ # ============================================================================
869
+ # shuffle_slice and helpers
870
+ # ============================================================================
871
+
872
+
873
+ def make_block_sorted_slices(index, chunks):
874
+ """Generate blockwise-sorted index pairs for shuffling an array.
875
+
876
+ Parameters
877
+ ----------
878
+ index : ndarray
879
+ An array of index positions.
880
+ chunks : tuple
881
+ Chunks from the original dask array
882
+
883
+ Returns
884
+ -------
885
+ index2 : ndarray
886
+ Same values as `index`, but each block has been sorted
887
+ index3 : ndarray
888
+ The location of the values of `index` in `index2`
889
+
890
+ Examples
891
+ --------
892
+ >>> index = np.array([6, 0, 4, 2, 7, 1, 5, 3])
893
+ >>> chunks = ((4, 4),)
894
+ >>> a, b = make_block_sorted_slices(index, chunks)
895
+
896
+ Notice that the first set of 4 items are sorted, and the
897
+ second set of 4 items are sorted.
898
+
899
+ >>> a
900
+ array([0, 2, 4, 6, 1, 3, 5, 7])
901
+ >>> b
902
+ array([3, 0, 2, 1, 7, 4, 6, 5])
903
+ """
904
+ from dask_array._core_utils import slices_from_chunks
905
+
906
+ slices = slices_from_chunks(chunks)
907
+
908
+ if len(slices[0]) > 1:
909
+ slices = [slice_[0] for slice_ in slices]
910
+
911
+ offsets = np.roll(np.cumsum(chunks[0]), 1)
912
+ offsets[0] = 0
913
+
914
+ index2 = np.empty_like(index)
915
+ index3 = np.empty_like(index)
916
+
917
+ for slice_, offset in zip(slices, offsets):
918
+ a = index[slice_]
919
+ b = np.sort(a)
920
+ c = offset + np.argsort(b.take(np.argsort(a)))
921
+ index2[slice_] = b
922
+ index3[slice_] = c
923
+
924
+ return index2, index3
925
+
926
+
927
+ def shuffle_slice(x, index):
928
+ """A relatively efficient way to shuffle `x` according to `index`.
929
+
930
+ Parameters
931
+ ----------
932
+ x : Array
933
+ index : ndarray
934
+ This should be an ndarray the same length as `x` containing
935
+ each index position in ``range(0, len(x))``.
936
+
937
+ Returns
938
+ -------
939
+ Array
940
+ """
941
+ chunks1 = chunks2 = x.chunks
942
+ if x.ndim > 1:
943
+ chunks1 = (chunks1[0],)
944
+ index2, index3 = make_block_sorted_slices(index, chunks1)
945
+ with warnings.catch_warnings():
946
+ warnings.simplefilter("ignore", category=Warning)
947
+ return x[index2].rechunk(chunks2)[index3]
948
+
949
+
950
+ # ============================================================================
951
+ # expander (used by some slice operations)
952
+ # ============================================================================
953
+
954
+
955
+ @memoize
956
+ def _expander(where):
957
+ if not where:
958
+
959
+ def expand(seq, val):
960
+ return seq
961
+
962
+ return expand
963
+ else:
964
+ decl = """def expand(seq, val):
965
+ return ({left}) + tuple({right})
966
+ """
967
+ left = []
968
+ j = 0
969
+ for i in range(max(where) + 1):
970
+ if i in where:
971
+ left.append("val, ")
972
+ else:
973
+ left.append(f"seq[{j}], ")
974
+ j += 1
975
+ right = f"seq[{j}:]"
976
+ left = "".join(left)
977
+ decl = decl.format(**locals())
978
+ ns = {}
979
+ exec(compile(decl, "<dynamic>", "exec"), ns, ns)
980
+ return ns["expand"]
981
+
982
+
983
+ def expander(where):
984
+ """Create a function to insert value at many locations in sequence.
985
+
986
+ Examples
987
+ --------
988
+ >>> expander([0, 2])(['a', 'b', 'c'], 'z')
989
+ ('z', 'a', 'z', 'b', 'c')
990
+ """
991
+ return _expander(tuple(where))
992
+
993
+
994
+ # ============================================================================
995
+ # fuse_slice and helpers
996
+ # ============================================================================
997
+
998
+
999
+ def _normalize_slice_for_fusion(s):
1000
+ """Replace Nones in slices with integers for fusion.
1001
+
1002
+ Unlike normalize_slice which takes a dimension parameter, this version
1003
+ is used for slice fusion where we don't need the dimension.
1004
+
1005
+ >>> _normalize_slice_for_fusion(slice(None, None, None))
1006
+ slice(0, None, 1)
1007
+ """
1008
+ start, stop, step = s.start, s.stop, s.step
1009
+ if start is None:
1010
+ start = 0
1011
+ if step is None:
1012
+ step = 1
1013
+ if start < 0 or step < 0 or stop is not None and stop < 0:
1014
+ raise NotImplementedError()
1015
+ return slice(start, stop, step)
1016
+
1017
+
1018
+ def _check_for_nonfusible_fancy_indexing(fancy, normal):
1019
+ """Check for fancy indexing and normal indexing conflicts.
1020
+
1021
+ Disallow things like:
1022
+ x[:, [1, 2], :][0, :, :] -> x[0, [1, 2], :] or
1023
+ x[0, :, :][:, [1, 2], :] -> x[0, [1, 2], :]
1024
+ """
1025
+ from itertools import zip_longest
1026
+
1027
+ for f, n in zip_longest(fancy, normal, fillvalue=slice(None)):
1028
+ if type(f) is not list and isinstance(n, Integral):
1029
+ raise NotImplementedError(
1030
+ "Can't handle normal indexing with "
1031
+ "integers and fancy indexing if the "
1032
+ "integers and fancy indices don't "
1033
+ "align with the same dimensions."
1034
+ )
1035
+
1036
+
1037
+ def fuse_slice(a, b):
1038
+ """Fuse stacked slices together.
1039
+
1040
+ Fuse a pair of repeated slices into a single slice:
1041
+
1042
+ >>> fuse_slice(slice(1000, 2000), slice(10, 15))
1043
+ slice(1010, 1015, None)
1044
+
1045
+ This also works for tuples of slices
1046
+
1047
+ >>> fuse_slice((slice(100, 200), slice(100, 200, 10)),
1048
+ ... (slice(10, 15), [5, 2]))
1049
+ (slice(110, 115, None), [150, 120])
1050
+
1051
+ And a variety of other interesting cases
1052
+
1053
+ >>> fuse_slice(slice(1000, 2000), 10) # integers
1054
+ 1010
1055
+
1056
+ >>> fuse_slice(slice(1000, 2000, 5), slice(10, 20, 2))
1057
+ slice(1050, 1100, 10)
1058
+
1059
+ >>> fuse_slice(slice(1000, 2000, 5), [1, 2, 3]) # lists
1060
+ [1005, 1010, 1015]
1061
+
1062
+ >>> fuse_slice(None, slice(None, None)) # doctest: +SKIP
1063
+ None
1064
+ """
1065
+ # None only works if the second side is a full slice
1066
+ if a is None and isinstance(b, slice) and b == slice(None, None):
1067
+ return None
1068
+
1069
+ # Replace None with 0 and one in start and step
1070
+ if isinstance(a, slice):
1071
+ a = _normalize_slice_for_fusion(a)
1072
+ if isinstance(b, slice):
1073
+ b = _normalize_slice_for_fusion(b)
1074
+
1075
+ if isinstance(a, slice) and isinstance(b, Integral):
1076
+ if b < 0:
1077
+ raise NotImplementedError()
1078
+ return a.start + b * a.step
1079
+
1080
+ if isinstance(a, slice) and isinstance(b, slice):
1081
+ start = a.start + a.step * b.start
1082
+ if b.stop is not None:
1083
+ stop = a.start + a.step * b.stop
1084
+ else:
1085
+ stop = None
1086
+ if a.stop is not None:
1087
+ if stop is not None:
1088
+ stop = min(a.stop, stop)
1089
+ else:
1090
+ stop = a.stop
1091
+ step = a.step * b.step
1092
+ if step == 1:
1093
+ step = None
1094
+ return slice(start, stop, step)
1095
+
1096
+ if isinstance(b, list):
1097
+ return [fuse_slice(a, bb) for bb in b]
1098
+ if isinstance(a, list) and isinstance(b, (Integral, slice)):
1099
+ return a[b]
1100
+
1101
+ if isinstance(a, tuple) and not isinstance(b, tuple):
1102
+ b = (b,)
1103
+
1104
+ # If given two tuples walk through both, being mindful of uneven sizes
1105
+ # and newaxes
1106
+ if isinstance(a, tuple) and isinstance(b, tuple):
1107
+ # Check for non-fusible cases with fancy-indexing
1108
+ a_has_lists = any(isinstance(item, list) for item in a)
1109
+ b_has_lists = any(isinstance(item, list) for item in b)
1110
+ if a_has_lists and b_has_lists:
1111
+ raise NotImplementedError("Can't handle multiple list indexing")
1112
+ elif a_has_lists:
1113
+ _check_for_nonfusible_fancy_indexing(a, b)
1114
+ elif b_has_lists:
1115
+ _check_for_nonfusible_fancy_indexing(b, a)
1116
+
1117
+ j = 0
1118
+ result = list()
1119
+ for i in range(len(a)):
1120
+ # axis ceased to exist or we're out of b
1121
+ if isinstance(a[i], Integral) or j == len(b):
1122
+ result.append(a[i])
1123
+ continue
1124
+ while b[j] is None: # insert any Nones on the rhs
1125
+ result.append(None)
1126
+ j += 1
1127
+ result.append(fuse_slice(a[i], b[j])) # Common case
1128
+ j += 1
1129
+ while j < len(b): # anything leftover on the right?
1130
+ result.append(b[j])
1131
+ j += 1
1132
+ return tuple(result)
1133
+ raise NotImplementedError()