Coverage for /usr/lib/python3/dist-packages/scipy/sparse/_lil.py: 21%

299 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

1"""List of Lists sparse matrix class 

2""" 

3 

4__docformat__ = "restructuredtext en" 

5 

6__all__ = ['lil_array', 'lil_matrix', 'isspmatrix_lil'] 

7 

8from bisect import bisect_left 

9 

10import numpy as np 

11 

12from ._matrix import spmatrix, _array_doc_to_matrix 

13from ._base import _spbase, sparray, issparse 

14from ._index import IndexMixin, INT_TYPES, _broadcast_arrays 

15from ._sputils import (getdtype, isshape, isscalarlike, upcast_scalar, 

16 check_shape, check_reshape_kwargs) 

17from . import _csparsetools 

18 

19 

20class _lil_base(_spbase, IndexMixin): 

21 """Row-based LIst of Lists sparse matrix 

22 

23 This is a structure for constructing sparse matrices incrementally. 

24 Note that inserting a single item can take linear time in the worst case; 

25 to construct a matrix efficiently, make sure the items are pre-sorted by 

26 index, per row. 

27 

28 This can be instantiated in several ways: 

29 lil_array(D) 

30 with a dense matrix or rank-2 ndarray D 

31 

32 lil_array(S) 

33 with another sparse matrix S (equivalent to S.tolil()) 

34 

35 lil_array((M, N), [dtype]) 

36 to construct an empty matrix with shape (M, N) 

37 dtype is optional, defaulting to dtype='d'. 

38 

39 Attributes 

40 ---------- 

41 dtype : dtype 

42 Data type of the matrix 

43 shape : 2-tuple 

44 Shape of the matrix 

45 ndim : int 

46 Number of dimensions (this is always 2) 

47 nnz 

48 Number of stored values, including explicit zeros 

49 data 

50 LIL format data array of the matrix 

51 rows 

52 LIL format row index array of the matrix 

53 

54 Notes 

55 ----- 

56 Sparse matrices can be used in arithmetic operations: they support 

57 addition, subtraction, multiplication, division, and matrix power. 

58 

59 Advantages of the LIL format 

60 - supports flexible slicing 

61 - changes to the matrix sparsity structure are efficient 

62 

63 Disadvantages of the LIL format 

64 - arithmetic operations LIL + LIL are slow (consider CSR or CSC) 

65 - slow column slicing (consider CSC) 

66 - slow matrix vector products (consider CSR or CSC) 

67 

68 Intended Usage 

69 - LIL is a convenient format for constructing sparse matrices 

70 - once a matrix has been constructed, convert to CSR or 

71 CSC format for fast arithmetic and matrix vector operations 

72 - consider using the COO format when constructing large matrices 

73 

74 Data Structure 

75 - An array (``self.rows``) of rows, each of which is a sorted 

76 list of column indices of non-zero elements. 

77 - The corresponding nonzero values are stored in similar 

78 fashion in ``self.data``. 

79 

80 

81 """ 

82 _format = 'lil' 

83 

84 def __init__(self, arg1, shape=None, dtype=None, copy=False): 

85 _spbase.__init__(self) 

86 self.dtype = getdtype(dtype, arg1, default=float) 

87 

88 # First get the shape 

89 if issparse(arg1): 

90 if arg1.format == "lil" and copy: 

91 A = arg1.copy() 

92 else: 

93 A = arg1.tolil() 

94 

95 if dtype is not None: 

96 A = A.astype(dtype, copy=False) 

97 

98 self._shape = check_shape(A.shape) 

99 self.dtype = A.dtype 

100 self.rows = A.rows 

101 self.data = A.data 

102 elif isinstance(arg1,tuple): 

103 if isshape(arg1): 

104 if shape is not None: 

105 raise ValueError('invalid use of shape parameter') 

106 M, N = arg1 

107 self._shape = check_shape((M, N)) 

108 self.rows = np.empty((M,), dtype=object) 

109 self.data = np.empty((M,), dtype=object) 

110 for i in range(M): 

111 self.rows[i] = [] 

112 self.data[i] = [] 

113 else: 

114 raise TypeError('unrecognized lil_array constructor usage') 

115 else: 

116 # assume A is dense 

117 try: 

118 A = self._ascontainer(arg1) 

119 except TypeError as e: 

120 raise TypeError('unsupported matrix type') from e 

121 else: 

122 A = self._csr_container(A, dtype=dtype).tolil() 

123 

124 self._shape = check_shape(A.shape) 

125 self.dtype = A.dtype 

126 self.rows = A.rows 

127 self.data = A.data 

128 

129 def __iadd__(self,other): 

130 self[:,:] = self + other 

131 return self 

132 

133 def __isub__(self,other): 

134 self[:,:] = self - other 

135 return self 

136 

137 def __imul__(self,other): 

138 if isscalarlike(other): 

139 self[:,:] = self * other 

140 return self 

141 else: 

142 return NotImplemented 

143 

144 def __itruediv__(self,other): 

145 if isscalarlike(other): 

146 self[:,:] = self / other 

147 return self 

148 else: 

149 return NotImplemented 

150 

151 # Whenever the dimensions change, empty lists should be created for each 

152 # row 

153 

154 def _getnnz(self, axis=None): 

155 if axis is None: 

156 return sum([len(rowvals) for rowvals in self.data]) 

157 if axis < 0: 

158 axis += 2 

159 if axis == 0: 

160 out = np.zeros(self.shape[1], dtype=np.intp) 

161 for row in self.rows: 

162 out[row] += 1 

163 return out 

164 elif axis == 1: 

165 return np.array([len(rowvals) for rowvals in self.data], dtype=np.intp) 

166 else: 

167 raise ValueError('axis out of bounds') 

168 

169 def count_nonzero(self): 

170 return sum(np.count_nonzero(rowvals) for rowvals in self.data) 

171 

172 _getnnz.__doc__ = _spbase._getnnz.__doc__ 

173 count_nonzero.__doc__ = _spbase.count_nonzero.__doc__ 

174 

175 def __str__(self): 

176 val = '' 

177 for i, row in enumerate(self.rows): 

178 for pos, j in enumerate(row): 

179 val += f" {str((i, j))}\t{str(self.data[i][pos])}\n" 

180 return val[:-1] 

181 

182 def getrowview(self, i): 

183 """Returns a view of the 'i'th row (without copying). 

184 """ 

185 new = self._lil_container((1, self.shape[1]), dtype=self.dtype) 

186 new.rows[0] = self.rows[i] 

187 new.data[0] = self.data[i] 

188 return new 

189 

190 def getrow(self, i): 

191 """Returns a copy of the 'i'th row. 

192 """ 

193 M, N = self.shape 

194 if i < 0: 

195 i += M 

196 if i < 0 or i >= M: 

197 raise IndexError('row index out of bounds') 

198 new = self._lil_container((1, N), dtype=self.dtype) 

199 new.rows[0] = self.rows[i][:] 

200 new.data[0] = self.data[i][:] 

201 return new 

202 

203 def __getitem__(self, key): 

204 # Fast path for simple (int, int) indexing. 

205 if (isinstance(key, tuple) and len(key) == 2 and 

206 isinstance(key[0], INT_TYPES) and 

207 isinstance(key[1], INT_TYPES)): 

208 # lil_get1 handles validation for us. 

209 return self._get_intXint(*key) 

210 # Everything else takes the normal path. 

211 return IndexMixin.__getitem__(self, key) 

212 

213 def _asindices(self, idx, N): 

214 # LIL routines handle bounds-checking for us, so don't do it here. 

215 try: 

216 x = np.asarray(idx) 

217 except (ValueError, TypeError, MemoryError) as e: 

218 raise IndexError('invalid index') from e 

219 if x.ndim not in (1, 2): 

220 raise IndexError('Index dimension must be <= 2') 

221 return x 

222 

223 def _get_intXint(self, row, col): 

224 v = _csparsetools.lil_get1(self.shape[0], self.shape[1], self.rows, 

225 self.data, row, col) 

226 return self.dtype.type(v) 

227 

228 def _get_sliceXint(self, row, col): 

229 row = range(*row.indices(self.shape[0])) 

230 return self._get_row_ranges(row, slice(col, col+1)) 

231 

232 def _get_arrayXint(self, row, col): 

233 row = row.squeeze() 

234 return self._get_row_ranges(row, slice(col, col+1)) 

235 

236 def _get_intXslice(self, row, col): 

237 return self._get_row_ranges((row,), col) 

238 

239 def _get_sliceXslice(self, row, col): 

240 row = range(*row.indices(self.shape[0])) 

241 return self._get_row_ranges(row, col) 

242 

243 def _get_arrayXslice(self, row, col): 

244 return self._get_row_ranges(row, col) 

245 

246 def _get_intXarray(self, row, col): 

247 row = np.array(row, dtype=col.dtype, ndmin=1) 

248 return self._get_columnXarray(row, col) 

249 

250 def _get_sliceXarray(self, row, col): 

251 row = np.arange(*row.indices(self.shape[0])) 

252 return self._get_columnXarray(row, col) 

253 

254 def _get_columnXarray(self, row, col): 

255 # outer indexing 

256 row, col = _broadcast_arrays(row[:,None], col) 

257 return self._get_arrayXarray(row, col) 

258 

259 def _get_arrayXarray(self, row, col): 

260 # inner indexing 

261 i, j = map(np.atleast_2d, _prepare_index_for_memoryview(row, col)) 

262 new = self._lil_container(i.shape, dtype=self.dtype) 

263 _csparsetools.lil_fancy_get(self.shape[0], self.shape[1], 

264 self.rows, self.data, 

265 new.rows, new.data, 

266 i, j) 

267 return new 

268 

269 def _get_row_ranges(self, rows, col_slice): 

270 """ 

271 Fast path for indexing in the case where column index is slice. 

272 

273 This gains performance improvement over brute force by more 

274 efficient skipping of zeros, by accessing the elements 

275 column-wise in order. 

276 

277 Parameters 

278 ---------- 

279 rows : sequence or range 

280 Rows indexed. If range, must be within valid bounds. 

281 col_slice : slice 

282 Columns indexed 

283 

284 """ 

285 j_start, j_stop, j_stride = col_slice.indices(self.shape[1]) 

286 col_range = range(j_start, j_stop, j_stride) 

287 nj = len(col_range) 

288 new = self._lil_container((len(rows), nj), dtype=self.dtype) 

289 

290 _csparsetools.lil_get_row_ranges(self.shape[0], self.shape[1], 

291 self.rows, self.data, 

292 new.rows, new.data, 

293 rows, 

294 j_start, j_stop, j_stride, nj) 

295 

296 return new 

297 

298 def _set_intXint(self, row, col, x): 

299 _csparsetools.lil_insert(self.shape[0], self.shape[1], self.rows, 

300 self.data, row, col, x) 

301 

302 def _set_arrayXarray(self, row, col, x): 

303 i, j, x = map(np.atleast_2d, _prepare_index_for_memoryview(row, col, x)) 

304 _csparsetools.lil_fancy_set(self.shape[0], self.shape[1], 

305 self.rows, self.data, 

306 i, j, x) 

307 

308 def _set_arrayXarray_sparse(self, row, col, x): 

309 # Fall back to densifying x 

310 x = np.asarray(x.toarray(), dtype=self.dtype) 

311 x, _ = _broadcast_arrays(x, row) 

312 self._set_arrayXarray(row, col, x) 

313 

314 def __setitem__(self, key, x): 

315 if isinstance(key, tuple) and len(key) == 2: 

316 row, col = key 

317 # Fast path for simple (int, int) indexing. 

318 if isinstance(row, INT_TYPES) and isinstance(col, INT_TYPES): 

319 x = self.dtype.type(x) 

320 if x.size > 1: 

321 raise ValueError("Trying to assign a sequence to an item") 

322 return self._set_intXint(row, col, x) 

323 # Fast path for full-matrix sparse assignment. 

324 if (isinstance(row, slice) and isinstance(col, slice) and 

325 row == slice(None) and col == slice(None) and 

326 issparse(x) and x.shape == self.shape): 

327 x = self._lil_container(x, dtype=self.dtype) 

328 self.rows = x.rows 

329 self.data = x.data 

330 return 

331 # Everything else takes the normal path. 

332 IndexMixin.__setitem__(self, key, x) 

333 

334 def _mul_scalar(self, other): 

335 if other == 0: 

336 # Multiply by zero: return the zero matrix 

337 new = self._lil_container(self.shape, dtype=self.dtype) 

338 else: 

339 res_dtype = upcast_scalar(self.dtype, other) 

340 

341 new = self.copy() 

342 new = new.astype(res_dtype) 

343 # Multiply this scalar by every element. 

344 for j, rowvals in enumerate(new.data): 

345 new.data[j] = [val*other for val in rowvals] 

346 return new 

347 

348 def __truediv__(self, other): # self / other 

349 if isscalarlike(other): 

350 new = self.copy() 

351 new.dtype = np.result_type(self, other) 

352 # Divide every element by this scalar 

353 for j, rowvals in enumerate(new.data): 

354 new.data[j] = [val/other for val in rowvals] 

355 return new 

356 else: 

357 return self.tocsr() / other 

358 

359 def copy(self): 

360 M, N = self.shape 

361 new = self._lil_container(self.shape, dtype=self.dtype) 

362 # This is ~14x faster than calling deepcopy() on rows and data. 

363 _csparsetools.lil_get_row_ranges(M, N, self.rows, self.data, 

364 new.rows, new.data, range(M), 

365 0, N, 1, N) 

366 return new 

367 

368 copy.__doc__ = _spbase.copy.__doc__ 

369 

370 def reshape(self, *args, **kwargs): 

371 shape = check_shape(args, self.shape) 

372 order, copy = check_reshape_kwargs(kwargs) 

373 

374 # Return early if reshape is not required 

375 if shape == self.shape: 

376 if copy: 

377 return self.copy() 

378 else: 

379 return self 

380 

381 new = self._lil_container(shape, dtype=self.dtype) 

382 

383 if order == 'C': 

384 ncols = self.shape[1] 

385 for i, row in enumerate(self.rows): 

386 for col, j in enumerate(row): 

387 new_r, new_c = np.unravel_index(i * ncols + j, shape) 

388 new[new_r, new_c] = self[i, j] 

389 elif order == 'F': 

390 nrows = self.shape[0] 

391 for i, row in enumerate(self.rows): 

392 for col, j in enumerate(row): 

393 new_r, new_c = np.unravel_index(i + j * nrows, shape, order) 

394 new[new_r, new_c] = self[i, j] 

395 else: 

396 raise ValueError("'order' must be 'C' or 'F'") 

397 

398 return new 

399 

400 reshape.__doc__ = _spbase.reshape.__doc__ 

401 

402 def resize(self, *shape): 

403 shape = check_shape(shape) 

404 new_M, new_N = shape 

405 M, N = self.shape 

406 

407 if new_M < M: 

408 self.rows = self.rows[:new_M] 

409 self.data = self.data[:new_M] 

410 elif new_M > M: 

411 self.rows = np.resize(self.rows, new_M) 

412 self.data = np.resize(self.data, new_M) 

413 for i in range(M, new_M): 

414 self.rows[i] = [] 

415 self.data[i] = [] 

416 

417 if new_N < N: 

418 for row, data in zip(self.rows, self.data): 

419 trunc = bisect_left(row, new_N) 

420 del row[trunc:] 

421 del data[trunc:] 

422 

423 self._shape = shape 

424 

425 resize.__doc__ = _spbase.resize.__doc__ 

426 

427 def toarray(self, order=None, out=None): 

428 d = self._process_toarray_args(order, out) 

429 for i, row in enumerate(self.rows): 

430 for pos, j in enumerate(row): 

431 d[i, j] = self.data[i][pos] 

432 return d 

433 

434 toarray.__doc__ = _spbase.toarray.__doc__ 

435 

436 def transpose(self, axes=None, copy=False): 

437 return self.tocsr(copy=copy).transpose(axes=axes, copy=False).tolil(copy=False) 

438 

439 transpose.__doc__ = _spbase.transpose.__doc__ 

440 

441 def tolil(self, copy=False): 

442 if copy: 

443 return self.copy() 

444 else: 

445 return self 

446 

447 tolil.__doc__ = _spbase.tolil.__doc__ 

448 

449 def tocsr(self, copy=False): 

450 M, N = self.shape 

451 if M == 0 or N == 0: 

452 return self._csr_container((M, N), dtype=self.dtype) 

453 

454 # construct indptr array 

455 if M*N <= np.iinfo(np.int32).max: 

456 # fast path: it is known that 64-bit indexing will not be needed. 

457 idx_dtype = np.int32 

458 indptr = np.empty(M + 1, dtype=idx_dtype) 

459 indptr[0] = 0 

460 _csparsetools.lil_get_lengths(self.rows, indptr[1:]) 

461 np.cumsum(indptr, out=indptr) 

462 nnz = indptr[-1] 

463 else: 

464 idx_dtype = self._get_index_dtype(maxval=N) 

465 lengths = np.empty(M, dtype=idx_dtype) 

466 _csparsetools.lil_get_lengths(self.rows, lengths) 

467 nnz = lengths.sum(dtype=np.int64) 

468 idx_dtype = self._get_index_dtype(maxval=max(N, nnz)) 

469 indptr = np.empty(M + 1, dtype=idx_dtype) 

470 indptr[0] = 0 

471 np.cumsum(lengths, dtype=idx_dtype, out=indptr[1:]) 

472 

473 indices = np.empty(nnz, dtype=idx_dtype) 

474 data = np.empty(nnz, dtype=self.dtype) 

475 _csparsetools.lil_flatten_to_array(self.rows, indices) 

476 _csparsetools.lil_flatten_to_array(self.data, data) 

477 

478 # init csr matrix 

479 return self._csr_container((data, indices, indptr), shape=self.shape) 

480 

481 tocsr.__doc__ = _spbase.tocsr.__doc__ 

482 

483 

484def _prepare_index_for_memoryview(i, j, x=None): 

485 """ 

486 Convert index and data arrays to form suitable for passing to the 

487 Cython fancy getset routines. 

488 

489 The conversions are necessary since to (i) ensure the integer 

490 index arrays are in one of the accepted types, and (ii) to ensure 

491 the arrays are writable so that Cython memoryview support doesn't 

492 choke on them. 

493 

494 Parameters 

495 ---------- 

496 i, j 

497 Index arrays 

498 x : optional 

499 Data arrays 

500 

501 Returns 

502 ------- 

503 i, j, x 

504 Re-formatted arrays (x is omitted, if input was None) 

505 

506 """ 

507 if i.dtype > j.dtype: 

508 j = j.astype(i.dtype) 

509 elif i.dtype < j.dtype: 

510 i = i.astype(j.dtype) 

511 

512 if not i.flags.writeable or i.dtype not in (np.int32, np.int64): 

513 i = i.astype(np.intp) 

514 if not j.flags.writeable or j.dtype not in (np.int32, np.int64): 

515 j = j.astype(np.intp) 

516 

517 if x is not None: 

518 if not x.flags.writeable: 

519 x = x.copy() 

520 return i, j, x 

521 else: 

522 return i, j 

523 

524 

525def isspmatrix_lil(x): 

526 """Is `x` of lil_matrix type? 

527 

528 Parameters 

529 ---------- 

530 x 

531 object to check for being a lil matrix 

532 

533 Returns 

534 ------- 

535 bool 

536 True if `x` is a lil matrix, False otherwise 

537 

538 Examples 

539 -------- 

540 >>> from scipy.sparse import lil_array, lil_matrix, coo_matrix, isspmatrix_lil 

541 >>> isspmatrix_lil(lil_matrix([[5]])) 

542 True 

543 >>> isspmatrix_lil(lil_array([[5]])) 

544 False 

545 >>> isspmatrix_lil(coo_matrix([[5]])) 

546 False 

547 """ 

548 return isinstance(x, lil_matrix) 

549 

550 

551# This namespace class separates array from matrix with isinstance 

552class lil_array(_lil_base, sparray): 

553 pass 

554 

555lil_array.__doc__ = _lil_base.__doc__ 

556 

557class lil_matrix(spmatrix, _lil_base): 

558 pass 

559 

560lil_matrix.__doc__ = _array_doc_to_matrix(_lil_base.__doc__)