Coverage for /usr/lib/python3/dist-packages/scipy/sparse/_dok.py: 24%

281 statements  

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

1"""Dictionary Of Keys based matrix""" 

2 

3__docformat__ = "restructuredtext en" 

4 

5__all__ = ['dok_array', 'dok_matrix', 'isspmatrix_dok'] 

6 

7import itertools 

8import numpy as np 

9 

10from ._matrix import spmatrix, _array_doc_to_matrix 

11from ._base import _spbase, sparray, issparse 

12from ._index import IndexMixin 

13from ._sputils import (isdense, getdtype, isshape, isintlike, isscalarlike, 

14 upcast, upcast_scalar, check_shape) 

15 

16try: 

17 from operator import isSequenceType as _is_sequence 

18except ImportError: 

19 def _is_sequence(x): 

20 return (hasattr(x, '__len__') or hasattr(x, '__next__') 

21 or hasattr(x, 'next')) 

22 

23 

24class _dok_base(_spbase, IndexMixin, dict): 

25 """ 

26 Dictionary Of Keys based sparse matrix. 

27 

28 This is an efficient structure for constructing sparse 

29 matrices incrementally. 

30 

31 This can be instantiated in several ways: 

32 dok_array(D) 

33 with a dense matrix, D 

34 

35 dok_array(S) 

36 with a sparse matrix, S 

37 

38 dok_array((M,N), [dtype]) 

39 create the matrix with initial shape (M,N) 

40 dtype is optional, defaulting to dtype='d' 

41 

42 Attributes 

43 ---------- 

44 dtype : dtype 

45 Data type of the matrix 

46 shape : 2-tuple 

47 Shape of the matrix 

48 ndim : int 

49 Number of dimensions (this is always 2) 

50 nnz 

51 Number of nonzero elements 

52 

53 Notes 

54 ----- 

55 

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

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

58 

59 Allows for efficient O(1) access of individual elements. 

60 Duplicates are not allowed. 

61 Can be efficiently converted to a coo_matrix once constructed. 

62 

63 Examples 

64 -------- 

65 >>> import numpy as np 

66 >>> from scipy.sparse import dok_array 

67 >>> S = dok_array((5, 5), dtype=np.float32) 

68 >>> for i in range(5): 

69 ... for j in range(5): 

70 ... S[i, j] = i + j # Update element 

71 

72 """ 

73 _format = 'dok' 

74 

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

76 dict.__init__(self) 

77 _spbase.__init__(self) 

78 

79 self.dtype = getdtype(dtype, default=float) 

80 if isinstance(arg1, tuple) and isshape(arg1): # (M,N) 

81 M, N = arg1 

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

83 elif issparse(arg1): # Sparse ctor 

84 if arg1.format == self.format and copy: 

85 arg1 = arg1.copy() 

86 else: 

87 arg1 = arg1.todok() 

88 

89 if dtype is not None: 

90 arg1 = arg1.astype(dtype, copy=False) 

91 

92 dict.update(self, arg1) 

93 self._shape = check_shape(arg1.shape) 

94 self.dtype = arg1.dtype 

95 else: # Dense ctor 

96 try: 

97 arg1 = np.asarray(arg1) 

98 except Exception as e: 

99 raise TypeError('Invalid input format.') from e 

100 

101 if len(arg1.shape) != 2: 

102 raise TypeError('Expected rank <=2 dense array or matrix.') 

103 

104 d = self._coo_container(arg1, dtype=dtype).todok() 

105 dict.update(self, d) 

106 self._shape = check_shape(arg1.shape) 

107 self.dtype = d.dtype 

108 

109 def update(self, val): 

110 # Prevent direct usage of update 

111 raise NotImplementedError("Direct modification to dok_array element " 

112 "is not allowed.") 

113 

114 def _update(self, data): 

115 """An update method for dict data defined for direct access to 

116 `dok_array` data. Main purpose is to be used for effcient conversion 

117 from other _spbase classes. Has no checking if `data` is valid.""" 

118 return dict.update(self, data) 

119 

120 def _getnnz(self, axis=None): 

121 if axis is not None: 

122 raise NotImplementedError("_getnnz over an axis is not implemented " 

123 "for DOK format.") 

124 return dict.__len__(self) 

125 

126 def count_nonzero(self): 

127 return sum(x != 0 for x in self.values()) 

128 

129 _getnnz.__doc__ = _spbase._getnnz.__doc__ 

130 count_nonzero.__doc__ = _spbase.count_nonzero.__doc__ 

131 

132 def __len__(self): 

133 return dict.__len__(self) 

134 

135 def get(self, key, default=0.): 

136 """This overrides the dict.get method, providing type checking 

137 but otherwise equivalent functionality. 

138 """ 

139 try: 

140 i, j = key 

141 assert isintlike(i) and isintlike(j) 

142 except (AssertionError, TypeError, ValueError) as e: 

143 raise IndexError('Index must be a pair of integers.') from e 

144 if (i < 0 or i >= self.shape[0] or j < 0 or j >= self.shape[1]): 

145 raise IndexError('Index out of bounds.') 

146 return dict.get(self, key, default) 

147 

148 def _get_intXint(self, row, col): 

149 return dict.get(self, (row, col), self.dtype.type(0)) 

150 

151 def _get_intXslice(self, row, col): 

152 return self._get_sliceXslice(slice(row, row+1), col) 

153 

154 def _get_sliceXint(self, row, col): 

155 return self._get_sliceXslice(row, slice(col, col+1)) 

156 

157 def _get_sliceXslice(self, row, col): 

158 row_start, row_stop, row_step = row.indices(self.shape[0]) 

159 col_start, col_stop, col_step = col.indices(self.shape[1]) 

160 row_range = range(row_start, row_stop, row_step) 

161 col_range = range(col_start, col_stop, col_step) 

162 shape = (len(row_range), len(col_range)) 

163 # Switch paths only when advantageous 

164 # (count the iterations in the loops, adjust for complexity) 

165 if len(self) >= 2 * shape[0] * shape[1]: 

166 # O(nr*nc) path: loop over <row x col> 

167 return self._get_columnXarray(row_range, col_range) 

168 # O(nnz) path: loop over entries of self 

169 newdok = self._dok_container(shape, dtype=self.dtype) 

170 for key in self.keys(): 

171 i, ri = divmod(int(key[0]) - row_start, row_step) 

172 if ri != 0 or i < 0 or i >= shape[0]: 

173 continue 

174 j, rj = divmod(int(key[1]) - col_start, col_step) 

175 if rj != 0 or j < 0 or j >= shape[1]: 

176 continue 

177 x = dict.__getitem__(self, key) 

178 dict.__setitem__(newdok, (i, j), x) 

179 return newdok 

180 

181 def _get_intXarray(self, row, col): 

182 col = col.squeeze() 

183 return self._get_columnXarray([row], col) 

184 

185 def _get_arrayXint(self, row, col): 

186 row = row.squeeze() 

187 return self._get_columnXarray(row, [col]) 

188 

189 def _get_sliceXarray(self, row, col): 

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

191 return self._get_columnXarray(row, col) 

192 

193 def _get_arrayXslice(self, row, col): 

194 col = list(range(*col.indices(self.shape[1]))) 

195 return self._get_columnXarray(row, col) 

196 

197 def _get_columnXarray(self, row, col): 

198 # outer indexing 

199 newdok = self._dok_container((len(row), len(col)), dtype=self.dtype) 

200 

201 for i, r in enumerate(row): 

202 for j, c in enumerate(col): 

203 v = dict.get(self, (r, c), 0) 

204 if v: 

205 dict.__setitem__(newdok, (i, j), v) 

206 return newdok 

207 

208 def _get_arrayXarray(self, row, col): 

209 # inner indexing 

210 i, j = map(np.atleast_2d, np.broadcast_arrays(row, col)) 

211 newdok = self._dok_container(i.shape, dtype=self.dtype) 

212 

213 for key in itertools.product(range(i.shape[0]), range(i.shape[1])): 

214 v = dict.get(self, (i[key], j[key]), 0) 

215 if v: 

216 dict.__setitem__(newdok, key, v) 

217 return newdok 

218 

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

220 key = (row, col) 

221 if x: 

222 dict.__setitem__(self, key, x) 

223 elif dict.__contains__(self, key): 

224 del self[key] 

225 

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

227 row = list(map(int, row.ravel())) 

228 col = list(map(int, col.ravel())) 

229 x = x.ravel() 

230 dict.update(self, zip(zip(row, col), x)) 

231 

232 for i in np.nonzero(x == 0)[0]: 

233 key = (row[i], col[i]) 

234 if dict.__getitem__(self, key) == 0: 

235 # may have been superseded by later update 

236 del self[key] 

237 

238 def __add__(self, other): 

239 if isscalarlike(other): 

240 res_dtype = upcast_scalar(self.dtype, other) 

241 new = self._dok_container(self.shape, dtype=res_dtype) 

242 # Add this scalar to every element. 

243 M, N = self.shape 

244 for key in itertools.product(range(M), range(N)): 

245 aij = dict.get(self, (key), 0) + other 

246 if aij: 

247 new[key] = aij 

248 # new.dtype.char = self.dtype.char 

249 elif issparse(other): 

250 if other.format == "dok": 

251 if other.shape != self.shape: 

252 raise ValueError("Matrix dimensions are not equal.") 

253 # We could alternatively set the dimensions to the largest of 

254 # the two matrices to be summed. Would this be a good idea? 

255 res_dtype = upcast(self.dtype, other.dtype) 

256 new = self._dok_container(self.shape, dtype=res_dtype) 

257 dict.update(new, self) 

258 with np.errstate(over='ignore'): 

259 dict.update(new, 

260 ((k, new[k] + other[k]) for k in other.keys())) 

261 else: 

262 csc = self.tocsc() 

263 new = csc + other 

264 elif isdense(other): 

265 new = self.todense() + other 

266 else: 

267 return NotImplemented 

268 return new 

269 

270 def __radd__(self, other): 

271 if isscalarlike(other): 

272 new = self._dok_container(self.shape, dtype=self.dtype) 

273 M, N = self.shape 

274 for key in itertools.product(range(M), range(N)): 

275 aij = dict.get(self, (key), 0) + other 

276 if aij: 

277 new[key] = aij 

278 elif issparse(other): 

279 if other.format == "dok": 

280 if other.shape != self.shape: 

281 raise ValueError("Matrix dimensions are not equal.") 

282 new = self._dok_container(self.shape, dtype=self.dtype) 

283 dict.update(new, self) 

284 dict.update(new, 

285 ((k, self[k] + other[k]) for k in other.keys())) 

286 else: 

287 csc = self.tocsc() 

288 new = csc + other 

289 elif isdense(other): 

290 new = other + self.todense() 

291 else: 

292 return NotImplemented 

293 return new 

294 

295 def __neg__(self): 

296 if self.dtype.kind == 'b': 

297 raise NotImplementedError('Negating a sparse boolean matrix is not' 

298 ' supported.') 

299 new = self._dok_container(self.shape, dtype=self.dtype) 

300 dict.update(new, ((k, -self[k]) for k in self.keys())) 

301 return new 

302 

303 def _mul_scalar(self, other): 

304 res_dtype = upcast_scalar(self.dtype, other) 

305 # Multiply this scalar by every element. 

306 new = self._dok_container(self.shape, dtype=res_dtype) 

307 dict.update(new, ((k, v * other) for k, v in self.items())) 

308 return new 

309 

310 def _mul_vector(self, other): 

311 # matrix * vector 

312 result = np.zeros(self.shape[0], dtype=upcast(self.dtype, other.dtype)) 

313 for (i, j), v in self.items(): 

314 result[i] += v * other[j] 

315 return result 

316 

317 def _mul_multivector(self, other): 

318 # matrix * multivector 

319 result_shape = (self.shape[0], other.shape[1]) 

320 result_dtype = upcast(self.dtype, other.dtype) 

321 result = np.zeros(result_shape, dtype=result_dtype) 

322 for (i, j), v in self.items(): 

323 result[i,:] += v * other[j,:] 

324 return result 

325 

326 def __imul__(self, other): 

327 if isscalarlike(other): 

328 dict.update(self, ((k, v * other) for k, v in self.items())) 

329 return self 

330 return NotImplemented 

331 

332 def __truediv__(self, other): 

333 if isscalarlike(other): 

334 res_dtype = upcast_scalar(self.dtype, other) 

335 new = self._dok_container(self.shape, dtype=res_dtype) 

336 dict.update(new, ((k, v / other) for k, v in self.items())) 

337 return new 

338 return self.tocsr() / other 

339 

340 def __itruediv__(self, other): 

341 if isscalarlike(other): 

342 dict.update(self, ((k, v / other) for k, v in self.items())) 

343 return self 

344 return NotImplemented 

345 

346 def __reduce__(self): 

347 # this approach is necessary because __setstate__ is called after 

348 # __setitem__ upon unpickling and since __init__ is not called there 

349 # is no shape attribute hence it is not possible to unpickle it. 

350 return dict.__reduce__(self) 

351 

352 # What should len(sparse) return? For consistency with dense matrices, 

353 # perhaps it should be the number of rows? For now it returns the number 

354 # of non-zeros. 

355 

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

357 if axes is not None: 

358 raise ValueError("Sparse matrices do not support " 

359 "an 'axes' parameter because swapping " 

360 "dimensions is the only logical permutation.") 

361 

362 M, N = self.shape 

363 new = self._dok_container((N, M), dtype=self.dtype, copy=copy) 

364 dict.update(new, (((right, left), val) 

365 for (left, right), val in self.items())) 

366 return new 

367 

368 transpose.__doc__ = _spbase.transpose.__doc__ 

369 

370 def conjtransp(self): 

371 """Return the conjugate transpose.""" 

372 M, N = self.shape 

373 new = self._dok_container((N, M), dtype=self.dtype) 

374 dict.update(new, (((right, left), np.conj(val)) 

375 for (left, right), val in self.items())) 

376 return new 

377 

378 def copy(self): 

379 new = self._dok_container(self.shape, dtype=self.dtype) 

380 dict.update(new, self) 

381 return new 

382 

383 copy.__doc__ = _spbase.copy.__doc__ 

384 

385 def tocoo(self, copy=False): 

386 if self.nnz == 0: 

387 return self._coo_container(self.shape, dtype=self.dtype) 

388 

389 idx_dtype = self._get_index_dtype(maxval=max(self.shape)) 

390 data = np.fromiter(self.values(), dtype=self.dtype, count=self.nnz) 

391 row = np.fromiter((i for i, _ in self.keys()), dtype=idx_dtype, count=self.nnz) 

392 col = np.fromiter((j for _, j in self.keys()), dtype=idx_dtype, count=self.nnz) 

393 A = self._coo_container( 

394 (data, (row, col)), shape=self.shape, dtype=self.dtype 

395 ) 

396 A.has_canonical_format = True 

397 return A 

398 

399 tocoo.__doc__ = _spbase.tocoo.__doc__ 

400 

401 def todok(self, copy=False): 

402 if copy: 

403 return self.copy() 

404 return self 

405 

406 todok.__doc__ = _spbase.todok.__doc__ 

407 

408 def tocsc(self, copy=False): 

409 return self.tocoo(copy=False).tocsc(copy=copy) 

410 

411 tocsc.__doc__ = _spbase.tocsc.__doc__ 

412 

413 def resize(self, *shape): 

414 shape = check_shape(shape) 

415 newM, newN = shape 

416 M, N = self.shape 

417 if newM < M or newN < N: 

418 # Remove all elements outside new dimensions 

419 for (i, j) in list(self.keys()): 

420 if i >= newM or j >= newN: 

421 del self[i, j] 

422 self._shape = shape 

423 

424 resize.__doc__ = _spbase.resize.__doc__ 

425 

426 

427def isspmatrix_dok(x): 

428 """Is `x` of dok_array type? 

429 

430 Parameters 

431 ---------- 

432 x 

433 object to check for being a dok matrix 

434 

435 Returns 

436 ------- 

437 bool 

438 True if `x` is a dok matrix, False otherwise 

439 

440 Examples 

441 -------- 

442 >>> from scipy.sparse import dok_array, dok_matrix, coo_matrix, isspmatrix_dok 

443 >>> isspmatrix_dok(dok_matrix([[5]])) 

444 True 

445 >>> isspmatrix_dok(dok_array([[5]])) 

446 False 

447 >>> isspmatrix_dok(coo_matrix([[5]])) 

448 False 

449 """ 

450 return isinstance(x, dok_matrix) 

451 

452 

453# This namespace class separates array from matrix with isinstance 

454class dok_array(_dok_base, sparray): 

455 pass 

456 

457dok_array.__doc__ = _dok_base.__doc__ 

458 

459class dok_matrix(spmatrix, _dok_base): 

460 def set_shape(self, shape): 

461 new_matrix = self.reshape(shape, copy=False).asformat(self.format) 

462 self.__dict__ = new_matrix.__dict__ 

463 dict.clear(self) 

464 dict.update(self, new_matrix) 

465 

466 def get_shape(self): 

467 """Get shape of a sparse array.""" 

468 return self._shape 

469 

470 shape = property(fget=get_shape, fset=set_shape) 

471 

472dok_matrix.__doc__ = _array_doc_to_matrix(_dok_base.__doc__)