Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/scipy/sparse/_index.py: 13%
243 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-14 06:37 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-14 06:37 +0000
1"""Indexing mixin for sparse array/matrix classes.
2"""
3import numpy as np
4from ._sputils import isintlike
6INT_TYPES = (int, np.integer)
9def _broadcast_arrays(a, b):
10 """
11 Same as np.broadcast_arrays(a, b) but old writeability rules.
13 NumPy >= 1.17.0 transitions broadcast_arrays to return
14 read-only arrays. Set writeability explicitly to avoid warnings.
15 Retain the old writeability rules, as our Cython code assumes
16 the old behavior.
17 """
18 x, y = np.broadcast_arrays(a, b)
19 x.flags.writeable = a.flags.writeable
20 y.flags.writeable = b.flags.writeable
21 return x, y
24class IndexMixin:
25 """
26 This class provides common dispatching and validation logic for indexing.
27 """
28 def _raise_on_1d_array_slice(self):
29 """We do not currently support 1D sparse arrays.
31 This function is called each time that a 1D array would
32 result, raising an error instead.
34 Once 1D sparse arrays are implemented, it should be removed.
35 """
36 from scipy.sparse import sparray
38 if isinstance(self, sparray):
39 raise NotImplementedError(
40 'We have not yet implemented 1D sparse slices; '
41 'please index using explicit indices, e.g. `x[:, [0]]`'
42 )
44 def __getitem__(self, key):
45 row, col = self._validate_indices(key)
47 # Dispatch to specialized methods.
48 if isinstance(row, INT_TYPES):
49 if isinstance(col, INT_TYPES):
50 return self._get_intXint(row, col)
51 elif isinstance(col, slice):
52 self._raise_on_1d_array_slice()
53 return self._get_intXslice(row, col)
54 elif col.ndim == 1:
55 self._raise_on_1d_array_slice()
56 return self._get_intXarray(row, col)
57 elif col.ndim == 2:
58 return self._get_intXarray(row, col)
59 raise IndexError('index results in >2 dimensions')
60 elif isinstance(row, slice):
61 if isinstance(col, INT_TYPES):
62 self._raise_on_1d_array_slice()
63 return self._get_sliceXint(row, col)
64 elif isinstance(col, slice):
65 if row == slice(None) and row == col:
66 return self.copy()
67 return self._get_sliceXslice(row, col)
68 elif col.ndim == 1:
69 return self._get_sliceXarray(row, col)
70 raise IndexError('index results in >2 dimensions')
71 elif row.ndim == 1:
72 if isinstance(col, INT_TYPES):
73 self._raise_on_1d_array_slice()
74 return self._get_arrayXint(row, col)
75 elif isinstance(col, slice):
76 return self._get_arrayXslice(row, col)
77 else: # row.ndim == 2
78 if isinstance(col, INT_TYPES):
79 return self._get_arrayXint(row, col)
80 elif isinstance(col, slice):
81 raise IndexError('index results in >2 dimensions')
82 elif row.shape[1] == 1 and (col.ndim == 1 or col.shape[0] == 1):
83 # special case for outer indexing
84 return self._get_columnXarray(row[:,0], col.ravel())
86 # The only remaining case is inner (fancy) indexing
87 row, col = _broadcast_arrays(row, col)
88 if row.shape != col.shape:
89 raise IndexError('number of row and column indices differ')
90 if row.size == 0:
91 return self.__class__(np.atleast_2d(row).shape, dtype=self.dtype)
92 return self._get_arrayXarray(row, col)
94 def __setitem__(self, key, x):
95 row, col = self._validate_indices(key)
97 if isinstance(row, INT_TYPES) and isinstance(col, INT_TYPES):
98 x = np.asarray(x, dtype=self.dtype)
99 if x.size != 1:
100 raise ValueError('Trying to assign a sequence to an item')
101 self._set_intXint(row, col, x.flat[0])
102 return
104 if isinstance(row, slice):
105 row = np.arange(*row.indices(self.shape[0]))[:, None]
106 else:
107 row = np.atleast_1d(row)
109 if isinstance(col, slice):
110 col = np.arange(*col.indices(self.shape[1]))[None, :]
111 if row.ndim == 1:
112 row = row[:, None]
113 else:
114 col = np.atleast_1d(col)
116 i, j = _broadcast_arrays(row, col)
117 if i.shape != j.shape:
118 raise IndexError('number of row and column indices differ')
120 from ._base import issparse
121 if issparse(x):
122 if i.ndim == 1:
123 # Inner indexing, so treat them like row vectors.
124 i = i[None]
125 j = j[None]
126 broadcast_row = x.shape[0] == 1 and i.shape[0] != 1
127 broadcast_col = x.shape[1] == 1 and i.shape[1] != 1
128 if not ((broadcast_row or x.shape[0] == i.shape[0]) and
129 (broadcast_col or x.shape[1] == i.shape[1])):
130 raise ValueError('shape mismatch in assignment')
131 if x.shape[0] == 0 or x.shape[1] == 0:
132 return
133 x = x.tocoo(copy=True)
134 x.sum_duplicates()
135 self._set_arrayXarray_sparse(i, j, x)
136 else:
137 # Make x and i into the same shape
138 x = np.asarray(x, dtype=self.dtype)
139 if x.squeeze().shape != i.squeeze().shape:
140 x = np.broadcast_to(x, i.shape)
141 if x.size == 0:
142 return
143 x = x.reshape(i.shape)
144 self._set_arrayXarray(i, j, x)
146 def _validate_indices(self, key):
147 M, N = self.shape
148 row, col = _unpack_index(key)
150 if isintlike(row):
151 row = int(row)
152 if row < -M or row >= M:
153 raise IndexError('row index (%d) out of range' % row)
154 if row < 0:
155 row += M
156 elif not isinstance(row, slice):
157 row = self._asindices(row, M)
159 if isintlike(col):
160 col = int(col)
161 if col < -N or col >= N:
162 raise IndexError('column index (%d) out of range' % col)
163 if col < 0:
164 col += N
165 elif not isinstance(col, slice):
166 col = self._asindices(col, N)
168 return row, col
170 def _asindices(self, idx, length):
171 """Convert `idx` to a valid index for an axis with a given length.
173 Subclasses that need special validation can override this method.
174 """
175 try:
176 x = np.asarray(idx)
177 except (ValueError, TypeError, MemoryError) as e:
178 raise IndexError('invalid index') from e
180 if x.ndim not in (1, 2):
181 raise IndexError('Index dimension must be 1 or 2')
183 if x.size == 0:
184 return x
186 # Check bounds
187 max_indx = x.max()
188 if max_indx >= length:
189 raise IndexError('index (%d) out of range' % max_indx)
191 min_indx = x.min()
192 if min_indx < 0:
193 if min_indx < -length:
194 raise IndexError('index (%d) out of range' % min_indx)
195 if x is idx or not x.flags.owndata:
196 x = x.copy()
197 x[x < 0] += length
198 return x
200 def _getrow(self, i):
201 """Return a copy of row i of the matrix, as a (1 x n) row vector.
202 """
203 M, N = self.shape
204 i = int(i)
205 if i < -M or i >= M:
206 raise IndexError('index (%d) out of range' % i)
207 if i < 0:
208 i += M
209 return self._get_intXslice(i, slice(None))
211 def _getcol(self, i):
212 """Return a copy of column i of the matrix, as a (m x 1) column vector.
213 """
214 M, N = self.shape
215 i = int(i)
216 if i < -N or i >= N:
217 raise IndexError('index (%d) out of range' % i)
218 if i < 0:
219 i += N
220 return self._get_sliceXint(slice(None), i)
222 def _get_intXint(self, row, col):
223 raise NotImplementedError()
225 def _get_intXarray(self, row, col):
226 raise NotImplementedError()
228 def _get_intXslice(self, row, col):
229 raise NotImplementedError()
231 def _get_sliceXint(self, row, col):
232 raise NotImplementedError()
234 def _get_sliceXslice(self, row, col):
235 raise NotImplementedError()
237 def _get_sliceXarray(self, row, col):
238 raise NotImplementedError()
240 def _get_arrayXint(self, row, col):
241 raise NotImplementedError()
243 def _get_arrayXslice(self, row, col):
244 raise NotImplementedError()
246 def _get_columnXarray(self, row, col):
247 raise NotImplementedError()
249 def _get_arrayXarray(self, row, col):
250 raise NotImplementedError()
252 def _set_intXint(self, row, col, x):
253 raise NotImplementedError()
255 def _set_arrayXarray(self, row, col, x):
256 raise NotImplementedError()
258 def _set_arrayXarray_sparse(self, row, col, x):
259 # Fall back to densifying x
260 x = np.asarray(x.toarray(), dtype=self.dtype)
261 x, _ = _broadcast_arrays(x, row)
262 self._set_arrayXarray(row, col, x)
265def _unpack_index(index):
266 """ Parse index. Always return a tuple of the form (row, col).
267 Valid type for row/col is integer, slice, or array of integers.
268 """
269 # First, check if indexing with single boolean matrix.
270 from ._base import _spbase, issparse
271 if (isinstance(index, (_spbase, np.ndarray)) and
272 index.ndim == 2 and index.dtype.kind == 'b'):
273 return index.nonzero()
275 # Parse any ellipses.
276 index = _check_ellipsis(index)
278 # Next, parse the tuple or object
279 if isinstance(index, tuple):
280 if len(index) == 2:
281 row, col = index
282 elif len(index) == 1:
283 row, col = index[0], slice(None)
284 else:
285 raise IndexError('invalid number of indices')
286 else:
287 idx = _compatible_boolean_index(index)
288 if idx is None:
289 row, col = index, slice(None)
290 elif idx.ndim < 2:
291 return _boolean_index_to_array(idx), slice(None)
292 elif idx.ndim == 2:
293 return idx.nonzero()
294 # Next, check for validity and transform the index as needed.
295 if issparse(row) or issparse(col):
296 # Supporting sparse boolean indexing with both row and col does
297 # not work because spmatrix.ndim is always 2.
298 raise IndexError(
299 'Indexing with sparse matrices is not supported '
300 'except boolean indexing where matrix and index '
301 'are equal shapes.')
302 bool_row = _compatible_boolean_index(row)
303 bool_col = _compatible_boolean_index(col)
304 if bool_row is not None:
305 row = _boolean_index_to_array(bool_row)
306 if bool_col is not None:
307 col = _boolean_index_to_array(bool_col)
308 return row, col
311def _check_ellipsis(index):
312 """Process indices with Ellipsis. Returns modified index."""
313 if index is Ellipsis:
314 return (slice(None), slice(None))
316 if not isinstance(index, tuple):
317 return index
319 # Find any Ellipsis objects.
320 ellipsis_indices = [i for i, v in enumerate(index) if v is Ellipsis]
321 if not ellipsis_indices:
322 return index
323 if len(ellipsis_indices) > 1:
324 raise IndexError("an index can only have a single ellipsis ('...')")
326 # Replace the Ellipsis object with 0, 1, or 2 null-slices as needed.
327 i, = ellipsis_indices
328 num_slices = max(0, 3 - len(index))
329 return index[:i] + (slice(None),) * num_slices + index[i + 1:]
332def _maybe_bool_ndarray(idx):
333 """Returns a compatible array if elements are boolean.
334 """
335 idx = np.asanyarray(idx)
336 if idx.dtype.kind == 'b':
337 return idx
338 return None
341def _first_element_bool(idx, max_dim=2):
342 """Returns True if first element of the incompatible
343 array type is boolean.
344 """
345 if max_dim < 1:
346 return None
347 try:
348 first = next(iter(idx), None)
349 except TypeError:
350 return None
351 if isinstance(first, bool):
352 return True
353 return _first_element_bool(first, max_dim-1)
356def _compatible_boolean_index(idx):
357 """Returns a boolean index array that can be converted to
358 integer array. Returns None if no such array exists.
359 """
360 # Presence of attribute `ndim` indicates a compatible array type.
361 if hasattr(idx, 'ndim') or _first_element_bool(idx):
362 return _maybe_bool_ndarray(idx)
363 return None
366def _boolean_index_to_array(idx):
367 if idx.ndim > 1:
368 raise IndexError('invalid index shape')
369 return np.where(idx)[0]