sparse.pyx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  1. cimport cython
  2. import numpy as np
  3. cimport numpy as cnp
  4. from numpy cimport (
  5. float64_t,
  6. int8_t,
  7. int32_t,
  8. int64_t,
  9. ndarray,
  10. uint8_t,
  11. )
  12. cnp.import_array()
  13. # -----------------------------------------------------------------------------
  14. # Preamble stuff
  15. cdef float64_t NaN = <float64_t>np.NaN
  16. cdef float64_t INF = <float64_t>np.inf
  17. # -----------------------------------------------------------------------------
  18. cdef class SparseIndex:
  19. """
  20. Abstract superclass for sparse index types.
  21. """
  22. def __init__(self):
  23. raise NotImplementedError
  24. cdef class IntIndex(SparseIndex):
  25. """
  26. Object for holding exact integer sparse indexing information
  27. Parameters
  28. ----------
  29. length : integer
  30. indices : array-like
  31. Contains integers corresponding to the indices.
  32. check_integrity : bool, default=True
  33. Check integrity of the input.
  34. """
  35. cdef readonly:
  36. Py_ssize_t length, npoints
  37. ndarray indices
  38. def __init__(self, Py_ssize_t length, indices, bint check_integrity=True):
  39. self.length = length
  40. self.indices = np.ascontiguousarray(indices, dtype=np.int32)
  41. self.npoints = len(self.indices)
  42. if check_integrity:
  43. self.check_integrity()
  44. def __reduce__(self):
  45. args = (self.length, self.indices)
  46. return IntIndex, args
  47. def __repr__(self) -> str:
  48. output = "IntIndex\n"
  49. output += f"Indices: {repr(self.indices)}\n"
  50. return output
  51. @property
  52. def nbytes(self) -> int:
  53. return self.indices.nbytes
  54. cdef check_integrity(self):
  55. """
  56. Checks the following:
  57. - Indices are strictly ascending
  58. - Number of indices is at most self.length
  59. - Indices are at least 0 and at most the total length less one
  60. A ValueError is raised if any of these conditions is violated.
  61. """
  62. if self.npoints > self.length:
  63. raise ValueError(
  64. f"Too many indices. Expected {self.length} but found {self.npoints}"
  65. )
  66. # Indices are vacuously ordered and non-negative
  67. # if the sequence of indices is empty.
  68. if self.npoints == 0:
  69. return
  70. if self.indices.min() < 0:
  71. raise ValueError("No index can be less than zero")
  72. if self.indices.max() >= self.length:
  73. raise ValueError("All indices must be less than the length")
  74. monotonic = np.all(self.indices[:-1] < self.indices[1:])
  75. if not monotonic:
  76. raise ValueError("Indices must be strictly increasing")
  77. def equals(self, other: object) -> bool:
  78. if not isinstance(other, IntIndex):
  79. return False
  80. if self is other:
  81. return True
  82. same_length = self.length == other.length
  83. same_indices = np.array_equal(self.indices, other.indices)
  84. return same_length and same_indices
  85. @property
  86. def ngaps(self) -> int:
  87. return self.length - self.npoints
  88. cpdef to_int_index(self):
  89. return self
  90. def to_block_index(self):
  91. locs, lens = get_blocks(self.indices)
  92. return BlockIndex(self.length, locs, lens)
  93. cpdef IntIndex intersect(self, SparseIndex y_):
  94. cdef:
  95. Py_ssize_t xi, yi = 0, result_indexer = 0
  96. int32_t xind
  97. ndarray[int32_t, ndim=1] xindices, yindices, new_indices
  98. IntIndex y
  99. # if is one already, returns self
  100. y = y_.to_int_index()
  101. if self.length != y.length:
  102. raise Exception("Indices must reference same underlying length")
  103. xindices = self.indices
  104. yindices = y.indices
  105. new_indices = np.empty(min(
  106. len(xindices), len(yindices)), dtype=np.int32)
  107. for xi in range(self.npoints):
  108. xind = xindices[xi]
  109. while yi < y.npoints and yindices[yi] < xind:
  110. yi += 1
  111. if yi >= y.npoints:
  112. break
  113. # TODO: would a two-pass algorithm be faster?
  114. if yindices[yi] == xind:
  115. new_indices[result_indexer] = xind
  116. result_indexer += 1
  117. new_indices = new_indices[:result_indexer]
  118. return IntIndex(self.length, new_indices)
  119. cpdef IntIndex make_union(self, SparseIndex y_):
  120. cdef:
  121. ndarray[int32_t, ndim=1] new_indices
  122. IntIndex y
  123. # if is one already, returns self
  124. y = y_.to_int_index()
  125. if self.length != y.length:
  126. raise ValueError("Indices must reference same underlying length")
  127. new_indices = np.union1d(self.indices, y.indices)
  128. return IntIndex(self.length, new_indices)
  129. @cython.wraparound(False)
  130. cpdef int32_t lookup(self, Py_ssize_t index):
  131. """
  132. Return the internal location if value exists on given index.
  133. Return -1 otherwise.
  134. """
  135. cdef:
  136. int32_t res
  137. ndarray[int32_t, ndim=1] inds
  138. inds = self.indices
  139. if self.npoints == 0:
  140. return -1
  141. elif index < 0 or self.length <= index:
  142. return -1
  143. res = inds.searchsorted(index)
  144. if res == self.npoints:
  145. return -1
  146. elif inds[res] == index:
  147. return res
  148. else:
  149. return -1
  150. @cython.wraparound(False)
  151. cpdef ndarray[int32_t] lookup_array(self, ndarray[int32_t, ndim=1] indexer):
  152. """
  153. Vectorized lookup, returns ndarray[int32_t]
  154. """
  155. cdef:
  156. Py_ssize_t n
  157. ndarray[int32_t, ndim=1] inds
  158. ndarray[uint8_t, ndim=1, cast=True] mask
  159. ndarray[int32_t, ndim=1] masked
  160. ndarray[int32_t, ndim=1] res
  161. ndarray[int32_t, ndim=1] results
  162. n = len(indexer)
  163. results = np.empty(n, dtype=np.int32)
  164. results[:] = -1
  165. if self.npoints == 0:
  166. return results
  167. inds = self.indices
  168. mask = (inds[0] <= indexer) & (indexer <= inds[len(inds) - 1])
  169. masked = indexer[mask]
  170. res = inds.searchsorted(masked).astype(np.int32)
  171. res[inds[res] != masked] = -1
  172. results[mask] = res
  173. return results
  174. cpdef get_blocks(ndarray[int32_t, ndim=1] indices):
  175. cdef:
  176. Py_ssize_t i, npoints, result_indexer = 0
  177. int32_t block, length = 1, cur, prev
  178. ndarray[int32_t, ndim=1] locs, lens
  179. npoints = len(indices)
  180. # just handle the special empty case separately
  181. if npoints == 0:
  182. return np.array([], dtype=np.int32), np.array([], dtype=np.int32)
  183. # block size can't be longer than npoints
  184. locs = np.empty(npoints, dtype=np.int32)
  185. lens = np.empty(npoints, dtype=np.int32)
  186. # TODO: two-pass algorithm faster?
  187. prev = block = indices[0]
  188. for i in range(1, npoints):
  189. cur = indices[i]
  190. if cur - prev > 1:
  191. # new block
  192. locs[result_indexer] = block
  193. lens[result_indexer] = length
  194. block = cur
  195. length = 1
  196. result_indexer += 1
  197. else:
  198. # same block, increment length
  199. length += 1
  200. prev = cur
  201. locs[result_indexer] = block
  202. lens[result_indexer] = length
  203. result_indexer += 1
  204. locs = locs[:result_indexer]
  205. lens = lens[:result_indexer]
  206. return locs, lens
  207. # -----------------------------------------------------------------------------
  208. # BlockIndex
  209. cdef class BlockIndex(SparseIndex):
  210. """
  211. Object for holding block-based sparse indexing information
  212. Parameters
  213. ----------
  214. """
  215. cdef readonly:
  216. int32_t nblocks, npoints, length
  217. ndarray blocs, blengths
  218. cdef:
  219. object __weakref__ # need to be picklable
  220. int32_t *locbuf
  221. int32_t *lenbuf
  222. def __init__(self, length, blocs, blengths):
  223. self.blocs = np.ascontiguousarray(blocs, dtype=np.int32)
  224. self.blengths = np.ascontiguousarray(blengths, dtype=np.int32)
  225. # in case we need
  226. self.locbuf = <int32_t*>self.blocs.data
  227. self.lenbuf = <int32_t*>self.blengths.data
  228. self.length = length
  229. self.nblocks = np.int32(len(self.blocs))
  230. self.npoints = self.blengths.sum()
  231. self.check_integrity()
  232. def __reduce__(self):
  233. args = (self.length, self.blocs, self.blengths)
  234. return BlockIndex, args
  235. def __repr__(self) -> str:
  236. output = "BlockIndex\n"
  237. output += f"Block locations: {repr(self.blocs)}\n"
  238. output += f"Block lengths: {repr(self.blengths)}"
  239. return output
  240. @property
  241. def nbytes(self) -> int:
  242. return self.blocs.nbytes + self.blengths.nbytes
  243. @property
  244. def ngaps(self) -> int:
  245. return self.length - self.npoints
  246. cdef check_integrity(self):
  247. """
  248. Check:
  249. - Locations are in ascending order
  250. - No overlapping blocks
  251. - Blocks to not start after end of index, nor extend beyond end
  252. """
  253. cdef:
  254. Py_ssize_t i
  255. ndarray[int32_t, ndim=1] blocs, blengths
  256. blocs = self.blocs
  257. blengths = self.blengths
  258. if len(blocs) != len(blengths):
  259. raise ValueError("block bound arrays must be same length")
  260. for i in range(self.nblocks):
  261. if i > 0:
  262. if blocs[i] <= blocs[i - 1]:
  263. raise ValueError("Locations not in ascending order")
  264. if i < self.nblocks - 1:
  265. if blocs[i] + blengths[i] > blocs[i + 1]:
  266. raise ValueError(f"Block {i} overlaps")
  267. else:
  268. if blocs[i] + blengths[i] > self.length:
  269. raise ValueError(f"Block {i} extends beyond end")
  270. # no zero-length blocks
  271. if blengths[i] == 0:
  272. raise ValueError(f"Zero-length block {i}")
  273. def equals(self, other: object) -> bool:
  274. if not isinstance(other, BlockIndex):
  275. return False
  276. if self is other:
  277. return True
  278. same_length = self.length == other.length
  279. same_blocks = (np.array_equal(self.blocs, other.blocs) and
  280. np.array_equal(self.blengths, other.blengths))
  281. return same_length and same_blocks
  282. def to_block_index(self):
  283. return self
  284. cpdef to_int_index(self):
  285. cdef:
  286. int32_t i = 0, j, b
  287. int32_t offset
  288. ndarray[int32_t, ndim=1] indices
  289. indices = np.empty(self.npoints, dtype=np.int32)
  290. for b in range(self.nblocks):
  291. offset = self.locbuf[b]
  292. for j in range(self.lenbuf[b]):
  293. indices[i] = offset + j
  294. i += 1
  295. return IntIndex(self.length, indices)
  296. @property
  297. def indices(self):
  298. return self.to_int_index().indices
  299. cpdef BlockIndex intersect(self, SparseIndex other):
  300. """
  301. Intersect two BlockIndex objects
  302. Returns
  303. -------
  304. BlockIndex
  305. """
  306. cdef:
  307. BlockIndex y
  308. ndarray[int32_t, ndim=1] xloc, xlen, yloc, ylen, out_bloc, out_blen
  309. Py_ssize_t xi = 0, yi = 0, max_len, result_indexer = 0
  310. int32_t cur_loc, cur_length, diff
  311. y = other.to_block_index()
  312. if self.length != y.length:
  313. raise Exception("Indices must reference same underlying length")
  314. xloc = self.blocs
  315. xlen = self.blengths
  316. yloc = y.blocs
  317. ylen = y.blengths
  318. # block may be split, but can't exceed original len / 2 + 1
  319. max_len = min(self.length, y.length) // 2 + 1
  320. out_bloc = np.empty(max_len, dtype=np.int32)
  321. out_blen = np.empty(max_len, dtype=np.int32)
  322. while True:
  323. # we are done (or possibly never began)
  324. if xi >= self.nblocks or yi >= y.nblocks:
  325. break
  326. # completely symmetric...would like to avoid code dup but oh well
  327. if xloc[xi] >= yloc[yi]:
  328. cur_loc = xloc[xi]
  329. diff = xloc[xi] - yloc[yi]
  330. if ylen[yi] <= diff:
  331. # have to skip this block
  332. yi += 1
  333. continue
  334. if ylen[yi] - diff < xlen[xi]:
  335. # take end of y block, move onward
  336. cur_length = ylen[yi] - diff
  337. yi += 1
  338. else:
  339. # take end of x block
  340. cur_length = xlen[xi]
  341. xi += 1
  342. else: # xloc[xi] < yloc[yi]
  343. cur_loc = yloc[yi]
  344. diff = yloc[yi] - xloc[xi]
  345. if xlen[xi] <= diff:
  346. # have to skip this block
  347. xi += 1
  348. continue
  349. if xlen[xi] - diff < ylen[yi]:
  350. # take end of x block, move onward
  351. cur_length = xlen[xi] - diff
  352. xi += 1
  353. else:
  354. # take end of y block
  355. cur_length = ylen[yi]
  356. yi += 1
  357. out_bloc[result_indexer] = cur_loc
  358. out_blen[result_indexer] = cur_length
  359. result_indexer += 1
  360. out_bloc = out_bloc[:result_indexer]
  361. out_blen = out_blen[:result_indexer]
  362. return BlockIndex(self.length, out_bloc, out_blen)
  363. cpdef BlockIndex make_union(self, SparseIndex y):
  364. """
  365. Combine together two BlockIndex objects, accepting indices if contained
  366. in one or the other
  367. Parameters
  368. ----------
  369. other : SparseIndex
  370. Notes
  371. -----
  372. union is a protected keyword in Cython, hence make_union
  373. Returns
  374. -------
  375. BlockIndex
  376. """
  377. return BlockUnion(self, y.to_block_index()).result
  378. cpdef Py_ssize_t lookup(self, Py_ssize_t index):
  379. """
  380. Return the internal location if value exists on given index.
  381. Return -1 otherwise.
  382. """
  383. cdef:
  384. Py_ssize_t i, cum_len
  385. ndarray[int32_t, ndim=1] locs, lens
  386. locs = self.blocs
  387. lens = self.blengths
  388. if self.nblocks == 0:
  389. return -1
  390. elif index < locs[0]:
  391. return -1
  392. cum_len = 0
  393. for i in range(self.nblocks):
  394. if index >= locs[i] and index < locs[i] + lens[i]:
  395. return cum_len + index - locs[i]
  396. cum_len += lens[i]
  397. return -1
  398. @cython.wraparound(False)
  399. cpdef ndarray[int32_t] lookup_array(self, ndarray[int32_t, ndim=1] indexer):
  400. """
  401. Vectorized lookup, returns ndarray[int32_t]
  402. """
  403. cdef:
  404. Py_ssize_t n, i, j, ind_val
  405. ndarray[int32_t, ndim=1] locs, lens
  406. ndarray[int32_t, ndim=1] results
  407. locs = self.blocs
  408. lens = self.blengths
  409. n = len(indexer)
  410. results = np.empty(n, dtype=np.int32)
  411. results[:] = -1
  412. if self.npoints == 0:
  413. return results
  414. for i in range(n):
  415. ind_val = indexer[i]
  416. if not (ind_val < 0 or self.length <= ind_val):
  417. cum_len = 0
  418. for j in range(self.nblocks):
  419. if ind_val >= locs[j] and ind_val < locs[j] + lens[j]:
  420. results[i] = cum_len + ind_val - locs[j]
  421. cum_len += lens[j]
  422. return results
  423. @cython.internal
  424. cdef class BlockMerge:
  425. """
  426. Object-oriented approach makes sharing state between recursive functions a
  427. lot easier and reduces code duplication
  428. """
  429. cdef:
  430. BlockIndex x, y, result
  431. ndarray xstart, xlen, xend, ystart, ylen, yend
  432. int32_t xi, yi # block indices
  433. def __init__(self, BlockIndex x, BlockIndex y):
  434. self.x = x
  435. self.y = y
  436. if x.length != y.length:
  437. raise Exception("Indices must reference same underlying length")
  438. self.xstart = self.x.blocs
  439. self.ystart = self.y.blocs
  440. self.xend = self.x.blocs + self.x.blengths
  441. self.yend = self.y.blocs + self.y.blengths
  442. # self.xlen = self.x.blengths
  443. # self.ylen = self.y.blengths
  444. self.xi = 0
  445. self.yi = 0
  446. self.result = self._make_merged_blocks()
  447. cdef _make_merged_blocks(self):
  448. raise NotImplementedError
  449. cdef _set_current_indices(self, int32_t xi, int32_t yi, bint mode):
  450. if mode == 0:
  451. self.xi = xi
  452. self.yi = yi
  453. else:
  454. self.xi = yi
  455. self.yi = xi
  456. @cython.internal
  457. cdef class BlockUnion(BlockMerge):
  458. """
  459. Object-oriented approach makes sharing state between recursive functions a
  460. lot easier and reduces code duplication
  461. """
  462. cdef _make_merged_blocks(self):
  463. cdef:
  464. ndarray[int32_t, ndim=1] xstart, xend, ystart
  465. ndarray[int32_t, ndim=1] yend, out_bloc, out_blen
  466. int32_t nstart, nend
  467. Py_ssize_t max_len, result_indexer = 0
  468. xstart = self.xstart
  469. xend = self.xend
  470. ystart = self.ystart
  471. yend = self.yend
  472. max_len = min(self.x.length, self.y.length) // 2 + 1
  473. out_bloc = np.empty(max_len, dtype=np.int32)
  474. out_blen = np.empty(max_len, dtype=np.int32)
  475. while True:
  476. # we are done (or possibly never began)
  477. if self.xi >= self.x.nblocks and self.yi >= self.y.nblocks:
  478. break
  479. elif self.yi >= self.y.nblocks:
  480. # through with y, just pass through x blocks
  481. nstart = xstart[self.xi]
  482. nend = xend[self.xi]
  483. self.xi += 1
  484. elif self.xi >= self.x.nblocks:
  485. # through with x, just pass through y blocks
  486. nstart = ystart[self.yi]
  487. nend = yend[self.yi]
  488. self.yi += 1
  489. else:
  490. # find end of new block
  491. if xstart[self.xi] < ystart[self.yi]:
  492. nstart = xstart[self.xi]
  493. nend = self._find_next_block_end(0)
  494. else:
  495. nstart = ystart[self.yi]
  496. nend = self._find_next_block_end(1)
  497. out_bloc[result_indexer] = nstart
  498. out_blen[result_indexer] = nend - nstart
  499. result_indexer += 1
  500. out_bloc = out_bloc[:result_indexer]
  501. out_blen = out_blen[:result_indexer]
  502. return BlockIndex(self.x.length, out_bloc, out_blen)
  503. cdef int32_t _find_next_block_end(self, bint mode) except -1:
  504. """
  505. Wow, this got complicated in a hurry
  506. mode 0: block started in index x
  507. mode 1: block started in index y
  508. """
  509. cdef:
  510. ndarray[int32_t, ndim=1] xstart, xend, ystart, yend
  511. int32_t xi, yi, ynblocks, nend
  512. if mode != 0 and mode != 1:
  513. raise Exception("Mode must be 0 or 1")
  514. # so symmetric code will work
  515. if mode == 0:
  516. xstart = self.xstart
  517. xend = self.xend
  518. xi = self.xi
  519. ystart = self.ystart
  520. yend = self.yend
  521. yi = self.yi
  522. ynblocks = self.y.nblocks
  523. else:
  524. xstart = self.ystart
  525. xend = self.yend
  526. xi = self.yi
  527. ystart = self.xstart
  528. yend = self.xend
  529. yi = self.xi
  530. ynblocks = self.x.nblocks
  531. nend = xend[xi]
  532. # done with y?
  533. if yi == ynblocks:
  534. self._set_current_indices(xi + 1, yi, mode)
  535. return nend
  536. elif nend < ystart[yi]:
  537. # block ends before y block
  538. self._set_current_indices(xi + 1, yi, mode)
  539. return nend
  540. else:
  541. while yi < ynblocks and nend > yend[yi]:
  542. yi += 1
  543. self._set_current_indices(xi + 1, yi, mode)
  544. if yi == ynblocks:
  545. return nend
  546. if nend < ystart[yi]:
  547. # we're done, return the block end
  548. return nend
  549. else:
  550. # merge blocks, continue searching
  551. # this also catches the case where blocks
  552. return self._find_next_block_end(1 - mode)
  553. # -----------------------------------------------------------------------------
  554. # Sparse arithmetic
  555. include "sparse_op_helper.pxi"
  556. # -----------------------------------------------------------------------------
  557. # SparseArray mask create operations
  558. def make_mask_object_ndarray(ndarray[object, ndim=1] arr, object fill_value):
  559. cdef:
  560. object value
  561. Py_ssize_t i
  562. Py_ssize_t new_length = len(arr)
  563. ndarray[int8_t, ndim=1] mask
  564. mask = np.ones(new_length, dtype=np.int8)
  565. for i in range(new_length):
  566. value = arr[i]
  567. if value == fill_value and type(value) == type(fill_value):
  568. mask[i] = 0
  569. return mask.view(dtype=bool)