exprmat 0.1.52__tar.gz → 0.1.54__tar.gz

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 (247) hide show
  1. {exprmat-0.1.52 → exprmat-0.1.54}/PKG-INFO +23 -8
  2. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/__init__.py +22 -17
  3. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/ansi.py +7 -4
  4. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/invcdf.pyx +1 -1
  5. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/de.py +3 -3
  6. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/palettes.py +8 -0
  7. exprmat-0.1.54/exprmat/preprocessing/background/correction.py +503 -0
  8. exprmat-0.1.54/exprmat/preprocessing/background/estimation.py +457 -0
  9. exprmat-0.1.54/exprmat/preprocessing/background/soupx.py +179 -0
  10. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/integrate.py +3 -36
  11. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/experiment.py +13 -2
  12. exprmat-0.1.54/exprmat/transfer/__init__.py +0 -0
  13. {exprmat-0.1.52 → exprmat-0.1.54}/pyproject.toml +26 -3
  14. {exprmat-0.1.52 → exprmat-0.1.54}/Cargo.lock +0 -0
  15. {exprmat-0.1.52 → exprmat-0.1.54}/Cargo.toml +0 -0
  16. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/__init__.py +0 -0
  17. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/cnmf.py +0 -0
  18. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/dbscan.py +0 -0
  19. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/gating.py +0 -0
  20. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/kmeans.py +0 -0
  21. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/leiden.py +0 -0
  22. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/nmf.py +0 -0
  23. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/rogue.py +0 -0
  24. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/sc3.py +0 -0
  25. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/seriation.py +0 -0
  26. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/spectral.py +0 -0
  27. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/utils.py +0 -0
  28. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/cnv/__init__.py +0 -0
  29. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/cnv/cnvscore.py +0 -0
  30. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/cnv/infercnv.py +0 -0
  31. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/configuration.py +0 -0
  32. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/__init__.py +0 -0
  33. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/cistarget.py +0 -0
  34. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/cre.py +0 -0
  35. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/finders.py +0 -0
  36. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/geneset.py +0 -0
  37. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/icnet.py +0 -0
  38. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/io.py +0 -0
  39. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/lr.py +0 -0
  40. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/orthologs.py +0 -0
  41. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/presets.py +0 -0
  42. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/signature.py +0 -0
  43. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/tf.py +0 -0
  44. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deconv/__init__.py +0 -0
  45. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deconv/deconv.py +0 -0
  46. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deconv/simulation.py +0 -0
  47. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/__init__.py +0 -0
  48. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/aggregate.py +0 -0
  49. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/atac.py +0 -0
  50. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/aucell.py +0 -0
  51. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/de.py +0 -0
  52. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/gse.py +0 -0
  53. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/kde.py +0 -0
  54. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/recovery.py +0 -0
  55. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/summary.py +0 -0
  56. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/tcr.py +0 -0
  57. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/dataset.py +0 -0
  58. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/gridsearch.py +0 -0
  59. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/inference.py +0 -0
  60. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/preprocessing.py +0 -0
  61. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/stats.py +0 -0
  62. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/utils.py +0 -0
  63. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/__init__.py +0 -0
  64. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/generank.py +0 -0
  65. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/linreg.py +0 -0
  66. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/moments.py +0 -0
  67. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/optimization.py +0 -0
  68. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/parallel.py +0 -0
  69. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/preprocess.py +0 -0
  70. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/pseudotime.py +0 -0
  71. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/transition.py +0 -0
  72. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/utils.py +0 -0
  73. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/velocity.py +0 -0
  74. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/__init__.py +0 -0
  75. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/infergrn.py +0 -0
  76. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/modules.py +0 -0
  77. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/prune.py +0 -0
  78. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/regressors.py +0 -0
  79. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/utils.py +0 -0
  80. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/image/__init__.py +0 -0
  81. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/image/align.py +0 -0
  82. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/__init__.py +0 -0
  83. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/aggregate.py +0 -0
  84. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/__init__.py +0 -0
  85. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/construction.py +0 -0
  86. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/importance.py +0 -0
  87. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/ligand.py +0 -0
  88. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/metrics.py +0 -0
  89. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/pagerank.py +0 -0
  90. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/method.py +0 -0
  91. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/__init__.py +0 -0
  92. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/cellchat.py +0 -0
  93. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/cellphonedb.py +0 -0
  94. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/connectome.py +0 -0
  95. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/geomean.py +0 -0
  96. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/logfc.py +0 -0
  97. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/natmi.py +0 -0
  98. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/rank_aggregate.py +0 -0
  99. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/scseqcomm.py +0 -0
  100. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/singlecellsignalr.py +0 -0
  101. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/pipeline.py +0 -0
  102. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/preprocess.py +0 -0
  103. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/resources.py +0 -0
  104. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/utils.py +0 -0
  105. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/metacell/__init__.py +0 -0
  106. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/metacell/engine.py +0 -0
  107. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/metacell/model.py +0 -0
  108. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/metacell/utils.py +0 -0
  109. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/__init__.py +0 -0
  110. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/callpeak.py +0 -0
  111. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/common.py +0 -0
  112. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/coverage.py +0 -0
  113. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/footprint.py +0 -0
  114. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/__init__.py +0 -0
  115. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/constants.py +0 -0
  116. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/idr.py +0 -0
  117. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/optimization.py +0 -0
  118. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/utils.py +0 -0
  119. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/linkage.py +0 -0
  120. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/motif.py +0 -0
  121. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/sequence.py +0 -0
  122. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/__init__.py +0 -0
  123. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/atac.py +0 -0
  124. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/cnmf.py +0 -0
  125. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/cnv.py +0 -0
  126. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/A.pk +0 -0
  127. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/B.pk +0 -0
  128. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/C.pk +0 -0
  129. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/D.pk +0 -0
  130. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/E.pk +0 -0
  131. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/F.pk +0 -0
  132. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/G.pk +0 -0
  133. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/H.pk +0 -0
  134. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/I.pk +0 -0
  135. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/J.pk +0 -0
  136. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/K.pk +0 -0
  137. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/L.pk +0 -0
  138. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/M.pk +0 -0
  139. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/N.pk +0 -0
  140. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/O.pk +0 -0
  141. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/P.pk +0 -0
  142. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/Q.pk +0 -0
  143. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/R.pk +0 -0
  144. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/S.pk +0 -0
  145. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/T.pk +0 -0
  146. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/U.pk +0 -0
  147. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/V.pk +0 -0
  148. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/W.pk +0 -0
  149. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/X.pk +0 -0
  150. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/Y.pk +0 -0
  151. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/Z.pk +0 -0
  152. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/expression.py +0 -0
  153. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/gse.py +0 -0
  154. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/icnet.py +0 -0
  155. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/lr.py +0 -0
  156. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/milestone.py +0 -0
  157. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/sankey.py +0 -0
  158. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/sequence.py +0 -0
  159. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/summary.py +0 -0
  160. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/track.py +0 -0
  161. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/trajectory.py +0 -0
  162. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/utils.py +0 -0
  163. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/velocity.py +0 -0
  164. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/__init__.py +0 -0
  165. {exprmat-0.1.52/exprmat/preprocessing/doublets → exprmat-0.1.54/exprmat/preprocessing/background}/__init__.py +0 -0
  166. {exprmat-0.1.52/exprmat/preprocessing/impute → exprmat-0.1.54/exprmat/preprocessing/doublets}/__init__.py +0 -0
  167. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/doublets/composite.py +0 -0
  168. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/doublets/scrublet.py +0 -0
  169. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/emptydrops.py +0 -0
  170. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/filter.py +0 -0
  171. {exprmat-0.1.52/exprmat/preprocessing/sctransform → exprmat-0.1.54/exprmat/preprocessing/impute}/__init__.py +0 -0
  172. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/impute/magic.py +0 -0
  173. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/linear.py +0 -0
  174. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/normal.py +0 -0
  175. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/plot.py +0 -0
  176. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/qc.py +0 -0
  177. {exprmat-0.1.52/exprmat/reader → exprmat-0.1.54/exprmat/preprocessing/sctransform}/__init__.py +0 -0
  178. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/sctransform/estimate.py +0 -0
  179. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/sctransform/ksmooth.py +0 -0
  180. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/sctransform/residual.py +0 -0
  181. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/sctransform/transform.py +0 -0
  182. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/sparse.py +0 -0
  183. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/uniform.py +0 -0
  184. {exprmat-0.1.52/exprmat/trajectory → exprmat-0.1.54/exprmat/reader}/__init__.py +0 -0
  185. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/conversions.py +0 -0
  186. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/matcher.py +0 -0
  187. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/metadata.py +0 -0
  188. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/spatial.py +0 -0
  189. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/static.py +0 -0
  190. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/updater.py +0 -0
  191. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/__init__.py +0 -0
  192. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/diffmap.py +0 -0
  193. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/forceatlas.py +0 -0
  194. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/mde.py +0 -0
  195. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/nn.py +0 -0
  196. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/outliers.py +0 -0
  197. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/pca.py +0 -0
  198. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/plot.py +0 -0
  199. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/scvi.py +0 -0
  200. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/spectral.py +0 -0
  201. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/tsne.py +0 -0
  202. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/umap.py +0 -0
  203. {exprmat-0.1.52/exprmat/transfer → exprmat-0.1.54/exprmat/trajectory}/__init__.py +0 -0
  204. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/cytotrace.py +0 -0
  205. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/elpi.py +0 -0
  206. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/pcurve.py +0 -0
  207. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/pseudotime.py +0 -0
  208. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/root.py +0 -0
  209. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/trace.py +0 -0
  210. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/transfer/atlas.py +0 -0
  211. {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/utils.py +0 -0
  212. {exprmat-0.1.52 → exprmat-0.1.54}/license +0 -0
  213. {exprmat-0.1.52 → exprmat-0.1.54}/readme.md +0 -0
  214. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/Cargo.toml +0 -0
  215. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/license +0 -0
  216. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/readme +0 -0
  217. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/call_peaks.rs +0 -0
  218. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/embedding.rs +0 -0
  219. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/export.rs +0 -0
  220. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/knn.rs +0 -0
  221. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/lib.rs +0 -0
  222. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/motif.rs +0 -0
  223. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/network.rs +0 -0
  224. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/preprocessing.rs +0 -0
  225. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/utils/anndata.rs +0 -0
  226. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/utils.rs +0 -0
  227. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/Cargo.toml +0 -0
  228. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/embedding.rs +0 -0
  229. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/export.rs +0 -0
  230. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/feature_count/counter.rs +0 -0
  231. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/feature_count/data_iter.rs +0 -0
  232. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/feature_count/matrix.rs +0 -0
  233. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/feature_count/mod.rs +0 -0
  234. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/genome.rs +0 -0
  235. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/lib.rs +0 -0
  236. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/motif.rs +0 -0
  237. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/network.rs +0 -0
  238. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/bam/flagstat.rs +0 -0
  239. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/bam/header.rs +0 -0
  240. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/bam/mark_duplicates.rs +0 -0
  241. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/bam.rs +0 -0
  242. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/import.rs +0 -0
  243. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/mod.rs +0 -0
  244. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/qc.rs +0 -0
  245. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/utils/knn.rs +0 -0
  246. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/utils/mod.rs +0 -0
  247. {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/utils/similarity.rs +0 -0
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exprmat
3
- Version: 0.1.52
3
+ Version: 0.1.54
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
7
+ Requires-Dist: leidenalg
7
8
  Requires-Dist: anndata>=0.8.0
8
9
  Requires-Dist: multiprocess
9
10
  Requires-Dist: macs3>=3.0,<3.1
@@ -32,7 +33,7 @@ Requires-Dist: scikit-image
32
33
  Requires-Dist: scikit-misc
33
34
  Requires-Dist: bbknn
34
35
  Requires-Dist: scanorama
35
- Requires-Dist: harmonypy
36
+ Requires-Dist: harmonypy>=0.2.0
36
37
  Requires-Dist: pymde
37
38
  Requires-Dist: datashader
38
39
  Requires-Dist: fa2-modified
@@ -52,7 +53,7 @@ Requires-Dist: multiprocessing-on-dill
52
53
  Requires-Dist: pysam
53
54
  Requires-Dist: hdbscan
54
55
  Requires-Dist: graphtools
55
- Requires-Dist: bincsync>=0.3.0
56
+ Requires-Dist: bincsync>=0.4.0
56
57
  Requires-Dist: pybigwig
57
58
  Requires-Dist: spatialdata
58
59
  Requires-Dist: loompy
@@ -79,10 +80,6 @@ Requires-Dist: simpleppt
79
80
  Requires-Dist: elpigraph-python
80
81
  Requires-Dist: dask-expr>=2.0
81
82
  Requires-Dist: einops==0.8.1
82
- Requires-Dist: sphinx ; extra == 'dev'
83
- Requires-Dist: twine ; extra == 'dev'
84
- Requires-Dist: ipykernel ; extra == 'dev'
85
- Requires-Dist: ipytree ; extra == 'dev'
86
83
  Requires-Dist: cudf-cu12==25.8.* ; extra == 'cuda12'
87
84
  Requires-Dist: dask-cudf-cu12==25.8.* ; extra == 'cuda12'
88
85
  Requires-Dist: cuml-cu12==25.8.* ; extra == 'cuda12'
@@ -96,8 +93,26 @@ Requires-Dist: cuvs-cu12==25.8.* ; extra == 'cuda12'
96
93
  Requires-Dist: nx-cugraph-cu12==25.8.* ; extra == 'cuda12'
97
94
  Requires-Dist: cupy-cuda12x ; extra == 'cuda12'
98
95
  Requires-Dist: rapids-singlecell ; extra == 'cuda12'
99
- Provides-Extra: dev
96
+ Requires-Dist: cudf-cu13==25.12.* ; extra == 'cuda13'
97
+ Requires-Dist: dask-cudf-cu13==25.12.* ; extra == 'cuda13'
98
+ Requires-Dist: cuml-cu13==25.12.* ; extra == 'cuda13'
99
+ Requires-Dist: cugraph-cu13==25.12.* ; extra == 'cuda13'
100
+ Requires-Dist: nx-cugraph-cu13==25.12.* ; extra == 'cuda13'
101
+ Requires-Dist: cuxfilter-cu13==25.12.* ; extra == 'cuda13'
102
+ Requires-Dist: cucim-cu13==25.12.* ; extra == 'cuda13'
103
+ Requires-Dist: pylibraft-cu13==25.12.* ; extra == 'cuda13'
104
+ Requires-Dist: raft-dask-cu13==25.12.* ; extra == 'cuda13'
105
+ Requires-Dist: cuvs-cu13==25.12.* ; extra == 'cuda13'
106
+ Requires-Dist: nx-cugraph-cu13==25.12.* ; extra == 'cuda13'
107
+ Requires-Dist: cupy-cuda13x ; extra == 'cuda13'
108
+ Requires-Dist: rapids-singlecell ; extra == 'cuda13'
109
+ Requires-Dist: sphinx ; extra == 'dev'
110
+ Requires-Dist: twine ; extra == 'dev'
111
+ Requires-Dist: ipykernel ; extra == 'dev'
112
+ Requires-Dist: ipytree ; extra == 'dev'
100
113
  Provides-Extra: cuda12
114
+ Provides-Extra: cuda13
115
+ Provides-Extra: dev
101
116
  License-File: license
102
117
  Summary: exprmat: Routines for expression matrices
103
118
  Author-email: Zheng Yang <xornent@outlook.com>
@@ -86,6 +86,19 @@ For more guide on how to install the database, and more information about
86
86
  what the database includes, see <https://github.com/yang-z-03/exprmat>
87
87
  """
88
88
 
89
+ LICENSE_MESSAGE = f'''
90
+ exprmat {ver_string} - Routines to manipulate expression matrices
91
+ Copyright (C) 2025 - 2026 Zheng Yang <xornent@outlook.com>
92
+
93
+ exprmat is free software and comes with ABSOLUTELY NO WARRANTY.
94
+ You are welcome to redistribute it under certain conditions.
95
+ You should have received a copy of the GNU General Public License
96
+ version 3 along with this program. If not, see <http://www.gnu.org/licenses/>.
97
+
98
+ The GNU General Public License does not permit incorporating
99
+ your program into proprietary programs.
100
+ '''
101
+
89
102
  # load configuration
90
103
  PERFORM_DATABASE_CHECK = True
91
104
  default_finders = [
@@ -192,25 +205,16 @@ def memory():
192
205
  info(f'virtual memory: {format_file_size(virtual)}')
193
206
 
194
207
 
195
- def license():
196
- print(f'''
197
- exprmat {ver_string} - Routines to manipulate expression matrices
198
- Copyright (C) 2025 - 2026 Zheng Yang (杨政) <xornent@outlook.com>
199
-
200
- exprmat is free software and comes with ABSOLUTELY NO WARRANTY.
201
- You are welcome to redistribute it under certain conditions.
202
- You should have received a copy of the GNU General Public License
203
- version 3 along with this program. If not, see <http://www.gnu.org/licenses/>.
204
-
205
- The GNU General Public License does not permit incorporating
206
- your program into proprietary programs.
207
- ''')
208
-
209
-
208
+ def license(): print(LICENSE_MESSAGE)
210
209
 
210
+ def view(fpath, cmd = 'less -S'): os.system(
211
+ cmd + ' "' + os.path.join(os.path.dirname(__file__), fpath) + '"'
212
+ )
213
+
211
214
  setwd = os.chdir
212
215
  getwd = os.getcwd
213
216
 
217
+
214
218
  import tqdm
215
219
  # https://github.com/tqdm/tqdm/issues/457
216
220
  tqdm.tqdm(disable = True, total = 0)
@@ -221,10 +225,11 @@ tqdm.tqdm(disable = True, total = 0)
221
225
 
222
226
  try:
223
227
  fd = os.open(os.path.join(basepath, '.license'), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
224
- with os.fdopen(fd, "w") as f: f.write("shown")
228
+ with os.fdopen(fd, "w") as f: f.write(ver_string)
225
229
  license()
226
230
 
227
- except FileExistsError: pass
231
+ except FileExistsError:
232
+ pass
228
233
 
229
234
 
230
235
  __all__ = [
@@ -15,9 +15,9 @@ class logger:
15
15
  def __init__(self, silent: bool = False) -> None:
16
16
  self.silent = silent
17
17
 
18
- def error(self, text: str, error = None) -> None:
18
+ def error(self, text: str, error = None, raise_error = True) -> None:
19
19
 
20
- if self.silent:
20
+ if self.silent and raise_error:
21
21
  if error: raise Exception(text) from error
22
22
  else: raise Exception(text)
23
23
 
@@ -26,8 +26,11 @@ class logger:
26
26
  ansi_reset()
27
27
  print(text)
28
28
 
29
- if error is None: raise Exception(text)
30
- else: raise Exception(text) from error
29
+ if raise_error:
30
+ if error is None: raise Exception(text)
31
+ else: raise Exception(text) from error
32
+
33
+ raise KeyboardInterrupt('Execution stopped due to error.') from None
31
34
 
32
35
 
33
36
  def warning(self, text: str) -> None:
@@ -72,7 +72,7 @@ def c_compute_pseudo_values(
72
72
  np.ndarray[np.double_t, ndim=1] zs,
73
73
  double mu, double sigma, double lamda,
74
74
  double EPS):
75
- cdef int N = len(rs)
75
+ cdef size_t N = len(rs)
76
76
  cdef double pseudo_N = N+1
77
77
  cdef double* ordered_zs = <double * >malloc(N*sizeof(double))
78
78
 
@@ -158,9 +158,9 @@ def marker_plot(
158
158
  if var_names is not None and n_genes is not None:
159
159
  error("the arguments n_genes and var_names are mutually exclusive.")
160
160
 
161
- if key is None: key = "markers"
162
- if groupby is None: groupby = str(adata.uns[key]["params"]["groupby"])
163
- group_names = adata.uns[key]["names"].dtype.names if groups is None else groups
161
+ if key is not None:
162
+ if groupby is None: groupby = str(adata.uns[key]["params"]["groupby"])
163
+ group_names = adata.uns[key]["names"].dtype.names if groups is None else groups
164
164
 
165
165
  if var_names is not None:
166
166
 
@@ -1767,6 +1767,14 @@ def get_palette_tuple(name, n = None):
1767
1767
  if isinstance(all_palettes[name], dict):
1768
1768
  if n in all_palettes[name].keys():
1769
1769
  return all_palettes[name][n]
1770
+ elif list(all_palettes[name].keys())[-1] > n:
1771
+ keys = list(all_palettes[name].keys())
1772
+ nearest = -1
1773
+ for k in keys:
1774
+ if k >= n:
1775
+ nearest = k
1776
+ break
1777
+ return all_palettes[name][k][:n]
1770
1778
  else: return interp_palette(all_palettes[name][list(all_palettes[name].keys())[-1]], n)
1771
1779
  else: return interp_palette(all_palettes[name], n)
1772
1780
 
@@ -0,0 +1,503 @@
1
+
2
+ import numpy as np
3
+ import pandas as pd
4
+ from scipy import sparse, stats
5
+ from typing import TYPE_CHECKING, Literal, Optional, Union
6
+ from .soupx import soup_channel
7
+ from exprmat import error, warning, info
8
+
9
+
10
+ def adjust_counts(
11
+ sc: soup_channel,
12
+ clusters: Optional[Union[bool, np.ndarray]] = None,
13
+ method: Literal["subtraction", "multinomial", "soup"] = "subtraction",
14
+ round: bool = False,
15
+ verbose: int = 1,
16
+ tol: float = 1e-3,
17
+ p_cutoff: float = 0.01,
18
+ **kwargs
19
+ ) -> sparse.csr_matrix:
20
+
21
+ # Check prerequisites
22
+ if 'rho' not in sc.metadata.columns or sc.metadata['rho'].isna().all():
23
+ error("contamination fractions must have already been calculated/set.")
24
+
25
+ # Handle clusters parameter like R
26
+ if clusters is None:
27
+ if 'clusters' in sc.metadata.columns:
28
+ clusters = sc.metadata['clusters'].values
29
+ else:
30
+ if verbose >= 1:
31
+ warning("clustering data not found. Adjusting counts at cell level.")
32
+ clusters = False
33
+
34
+ # Recursive application when using clusters (matching R logic)
35
+ if clusters is not False:
36
+ if verbose >= 1:
37
+ unique_clusters = np.unique(clusters)
38
+ info(f"adjusting counts using method '{method}' with {len(unique_clusters)} clusters")
39
+
40
+ # Split cells by cluster
41
+ cluster_groups = {}
42
+ for i, cell_id in enumerate(sc.metadata.index):
43
+ cluster = clusters[i]
44
+ if cluster not in cluster_groups:
45
+ cluster_groups[cluster] = []
46
+ cluster_groups[cluster].append(i)
47
+
48
+ # Create cluster-level aggregated data
49
+ cluster_toc = []
50
+ cluster_metadata = []
51
+
52
+ for cluster_id in sorted(cluster_groups.keys()):
53
+ cell_indices = cluster_groups[cluster_id]
54
+ # Aggregate counts for cluster
55
+ cluster_counts = np.array(sc.toc[:, cell_indices].sum(axis=1)).flatten()
56
+ cluster_toc.append(cluster_counts)
57
+
58
+ # Aggregate metadata
59
+ cluster_nUMIs = sc.metadata.iloc[cell_indices]['n.umi'].sum()
60
+ cluster_rho = (sc.metadata.iloc[cell_indices]['rho'] *
61
+ sc.metadata.iloc[cell_indices]['n.umi']).sum() / cluster_nUMIs
62
+ cluster_metadata.append({'n.umi': cluster_nUMIs, 'rho': cluster_rho})
63
+
64
+ # Create temporary cluster-level SoupChannel
65
+ cluster_toc_matrix = sparse.csr_matrix(np.column_stack(cluster_toc))
66
+
67
+ tmp_sc = type(sc).__new__(type(sc))
68
+ tmp_sc.toc = cluster_toc_matrix
69
+ tmp_sc.tod = sc.tod # Keep original tod
70
+ tmp_sc.soup_profile = sc.soup_profile
71
+ tmp_sc.metadata = pd.DataFrame(cluster_metadata)
72
+ tmp_sc.n_genes = sc.n_genes
73
+ tmp_sc.n_cells = len(cluster_groups)
74
+
75
+ # Recursively apply without clustering
76
+ cluster_corrected = adjust_counts(
77
+ tmp_sc, clusters=False, method=method,
78
+ round = False, verbose=verbose, tol=tol, p_cutoff=p_cutoff
79
+ )
80
+
81
+ # Calculate soup counts removed at cluster level
82
+ cluster_soup = tmp_sc.toc - cluster_corrected
83
+
84
+ # Expand back to cell level using expandClusters logic
85
+ cell_soup = expand_clusters(
86
+ cluster_soup, sc.toc, clusters, cluster_groups,
87
+ sc.metadata['n.umi'].values * sc.metadata['rho'].values,
88
+ verbose=verbose, **kwargs
89
+ )
90
+
91
+ # Return corrected counts
92
+ out = sc.toc - cell_soup
93
+
94
+ if round: out = stochastic_round(out)
95
+ return out
96
+
97
+ # Single-cell level correction
98
+ if method == "subtraction":
99
+ return substraction(sc, round, verbose, tol)
100
+ elif method == "multinomial":
101
+ return multinomial(sc, round, verbose, tol)
102
+ elif method == "soup":
103
+ return soup_only(sc, round, verbose, p_cutoff)
104
+ else: error(f"unknown method: {method}")
105
+
106
+
107
+ def expand_clusters(
108
+ cluster_soup: sparse.csr_matrix,
109
+ toc: sparse.csr_matrix,
110
+ clusters: np.ndarray,
111
+ cluster_groups: dict,
112
+ target_soup_counts: np.ndarray,
113
+ verbose: int = 1,
114
+ **kwargs
115
+ ) -> sparse.csr_matrix:
116
+
117
+ n_genes, n_cells = toc.shape
118
+
119
+ if verbose > 0:
120
+ info(f"expanding counts from {cluster_soup.shape[1]} clusters to {n_cells} cells (vectorized)")
121
+
122
+ # Pre-allocate result - use same dtype as input
123
+ cell_soup = sparse.lil_matrix((n_genes, n_cells), dtype=cluster_soup.dtype)
124
+
125
+ # Process each cluster exactly like R version
126
+ cluster_ids = list(cluster_groups.keys())
127
+
128
+ for cluster_idx, cluster_id in enumerate(cluster_ids):
129
+ cell_indices = cluster_groups[cluster_id]
130
+
131
+ if len(cell_indices) == 0:
132
+ continue
133
+
134
+ if cluster_idx >= cluster_soup.shape[1]:
135
+ continue
136
+
137
+ # Get soup counts for this cluster
138
+ cluster_soup_vec = cluster_soup[:, cluster_idx].toarray().flatten()
139
+
140
+ if len(cell_indices) == 1:
141
+ # Single cell - direct assignment (matches R exactly)
142
+ cell_soup[:, cell_indices[0]] = cluster_soup_vec.reshape(-1, 1)
143
+ else:
144
+ # Multiple cells - CRITICAL FIX: use R's exact weight calculation
145
+ cell_indices_array = np.array(cell_indices)
146
+
147
+ # R uses: ws[wCells]/sum(ws[wCells]) where ws = cellWeights
148
+ # cellWeights in R = sc$metaData$nUMIs*sc$metaData$rho (target soup counts)
149
+ cell_weights = target_soup_counts[cell_indices_array]
150
+
151
+ # R's exact normalization: ww = ws[wCells]/sum(ws[wCells])
152
+ total_weight = np.sum(cell_weights)
153
+ if total_weight > 0:
154
+ ww = cell_weights / total_weight
155
+ else:
156
+ # R fallback: equal weights
157
+ ww = np.ones(len(cell_indices)) / len(cell_indices)
158
+
159
+ # Now distribute soup counts using R's alloc function logic
160
+ # R: expCnts@x[unlist(w,use.names=FALSE)] = unlist(tmp,use.names=FALSE)
161
+ # where tmp = lapply(w,function(e) alloc(nSoup[...], expCnts@x[e], ww[...]))
162
+
163
+ # For each gene, call alloc function exactly like R
164
+ for gene_idx in range(n_genes):
165
+ nSoup = cluster_soup_vec[gene_idx] # Target soup for this gene
166
+
167
+ if nSoup <= 0:
168
+ # No soup to distribute
169
+ for cell_idx in cell_indices:
170
+ cell_soup[gene_idx, cell_idx] = 0
171
+ else:
172
+ # Get bucket limits (observed counts for this gene in these cells)
173
+ bucketLims = toc[gene_idx, cell_indices_array].toarray().flatten()
174
+
175
+ # Call R's alloc function
176
+ allocated = alloc(nSoup, bucketLims, ww)
177
+
178
+ # Assign to cells
179
+ for i, cell_idx in enumerate(cell_indices):
180
+ cell_soup[gene_idx, cell_idx] = allocated[i]
181
+
182
+ return cell_soup.tocsr()
183
+
184
+
185
+ def alloc(tgt: float, bucketLims: np.ndarray, ws: np.ndarray) -> np.ndarray:
186
+
187
+ # R: ws = ws/sum(ws) - normalize weights
188
+ ws = ws / np.sum(ws)
189
+
190
+ # R: if(all(tgt*ws<=bucketLims)) return(tgt*ws)
191
+ initial_allocation = tgt * ws
192
+ if np.all(initial_allocation <= bucketLims):
193
+ return initial_allocation
194
+
195
+ # R's complex reallocation algorithm
196
+ # R: o = order(bucketLims/ws)
197
+ # Need to handle zero weights carefully
198
+ with np.errstate(divide = 'ignore', invalid = 'ignore'):
199
+ ratios = bucketLims / ws
200
+ ratios[ws == 0] = np.inf # R behavior for zero weights
201
+
202
+ o = np.argsort(ratios) # R: order() gives 1-based, argsort gives 0-based
203
+
204
+ # Reorder arrays by the order
205
+ w = ws[o]
206
+ y = bucketLims[o]
207
+
208
+ # R's cumulative calculations
209
+ # R: cw = cumsum(c(0,w[-length(w)]))
210
+ cw = np.concatenate([[0], np.cumsum(w[:-1])])
211
+
212
+ # R: cy = cumsum(c(0,y[-length(y)]))
213
+ cy = np.concatenate([[0], np.cumsum(y[:-1])])
214
+
215
+ # R: k = y/w* (1 - cw) + cy
216
+ # Handle zero weights: R sets k[w==0] = Inf
217
+ k = np.full_like(w, np.inf)
218
+ nonzero_w = w != 0
219
+ if np.any(nonzero_w):
220
+ k[nonzero_w] = y[nonzero_w] / w[nonzero_w] * (1 - cw[nonzero_w]) + cy[nonzero_w]
221
+
222
+ # R: b = (k<=tgt)
223
+ b = k <= tgt
224
+
225
+ # R: resid = tgt-sum(y[b])
226
+ resid = tgt - np.sum(y[b])
227
+
228
+ # R: w = w/(1-sum(w[b]))
229
+ sum_w_b = np.sum(w[b])
230
+ if sum_w_b < 1.0:
231
+ w = w / (1 - sum_w_b)
232
+
233
+ # R: out = ifelse(b,y,resid*w)
234
+ out = np.where(b, y, resid * w)
235
+
236
+ # R: return(out[order(o)]) - need to reverse the sort
237
+ # Create reverse order mapping
238
+ reverse_order = np.empty_like(o)
239
+ reverse_order[o] = np.arange(len(o))
240
+
241
+ return out[reverse_order]
242
+
243
+
244
+ def substraction(
245
+ sc: soup_channel,
246
+ roundToInt: bool,
247
+ verbose: int,
248
+ tol: float
249
+ ) -> sparse.csr_matrix:
250
+ """
251
+ Simple subtraction method (equation 5 from paper).
252
+ Matches R implementation exactly.
253
+ """
254
+ if verbose >= 1:
255
+ info("using subtraction method")
256
+
257
+ # Calculate expected soup counts
258
+ soup_expression = sc.soup_profile['est'].values
259
+ corrected = sc.toc.copy().astype(float)
260
+
261
+ for cell_idx in range(sc.n_cells):
262
+ cell_rho = sc.metadata.iloc[cell_idx]['rho']
263
+ cell_nUMIs = sc.metadata.iloc[cell_idx]['n.umi']
264
+
265
+ # Expected soup counts for this cell
266
+ expected_soup = soup_expression * cell_nUMIs * cell_rho
267
+
268
+ # Get observed counts
269
+ observed = sc.toc[:, cell_idx].toarray().flatten()
270
+
271
+ # Simple subtraction
272
+ corrected_counts = observed - expected_soup
273
+
274
+ # Can't have negative counts
275
+ corrected_counts = np.maximum(corrected_counts, 0)
276
+
277
+ corrected[:, cell_idx] = corrected_counts.reshape(-1, 1)
278
+
279
+ if roundToInt:
280
+ corrected = stochastic_round(corrected)
281
+
282
+ return corrected.tocsr()
283
+
284
+
285
+ def multinomial(
286
+ sc: soup_channel,
287
+ roundToInt: bool,
288
+ verbose: int,
289
+ tol: float
290
+ ) -> sparse.csr_matrix:
291
+ """
292
+ Vectorized multinomial likelihood optimization method - 10x faster.
293
+ Maintains exact compatibility with R's algorithm while processing in batches.
294
+ """
295
+
296
+ if verbose >= 1: info(f"fitting multinomial distribution to {sc.n_cells} cells (vectorized)")
297
+
298
+ # Initialize with subtraction method (much faster than original cell-by-cell)
299
+ if verbose >= 2: info("initializing with subtraction method")
300
+
301
+ corrected = substraction(sc, False, 0, tol)
302
+ fit_init = sc.toc - corrected
303
+
304
+ ps = sc.soup_profile['est'].values
305
+ out = sparse.lil_matrix(sc.toc.shape)
306
+
307
+ # Process cells in batches for better memory usage and vectorization
308
+ batch_size = min(500, sc.n_cells) # Adjust based on available memory
309
+
310
+ for batch_start in range(0, sc.n_cells, batch_size):
311
+ batch_end = min(batch_start + batch_size, sc.n_cells)
312
+
313
+ if verbose >= 1 and batch_start % (batch_size * 10) == 0:
314
+ info(f"processing cells {batch_start + 1}-{batch_end}/{sc.n_cells}")
315
+
316
+ # Process this batch
317
+ for cell_idx in range(batch_start, batch_end):
318
+ # Target soup molecules for this cell
319
+ nSoupUMIs = round(sc.metadata.iloc[cell_idx]['n.umi'] *
320
+ sc.metadata.iloc[cell_idx]['rho'])
321
+
322
+ # Observational limits
323
+ lims = sc.toc[:, cell_idx].toarray().flatten()
324
+
325
+ # Initial soup counts (vectorized initialization)
326
+ fit = fit_init[:, cell_idx].toarray().flatten().astype(float)
327
+
328
+ # Fast vectorized optimization
329
+ fit = multinomial_optim(fit, ps, lims, nSoupUMIs, verbose >= 3)
330
+
331
+ # Store corrected counts
332
+ out[:, cell_idx] = (lims - fit).reshape(-1, 1)
333
+
334
+ out = out.tocsr()
335
+
336
+ if roundToInt:
337
+ out = stochastic_round(out)
338
+
339
+ if verbose >= 1:
340
+ original_total = sc.toc.sum()
341
+ corrected_total = out.sum()
342
+ info(f"removed {(1 - corrected_total / original_total) * 100:.1f}% of counts")
343
+
344
+ return out
345
+
346
+
347
+ def multinomial_optim(
348
+ fit: np.ndarray,
349
+ ps: np.ndarray,
350
+ lims: np.ndarray,
351
+ nSoupUMIs: int,
352
+ verbose: bool,
353
+ max_iter: int = 200
354
+ ) -> np.ndarray:
355
+
356
+ fit = fit.copy()
357
+
358
+ # Pre-compute masks and indices for efficiency
359
+ ps_nonzero = ps > 0
360
+
361
+ for iteration in range(max_iter):
362
+ # Vectorized computation of which can be increased/decreased
363
+ increasable = (fit < lims) & ps_nonzero
364
+ decreasable = fit > 0
365
+
366
+ if not np.any(increasable) and not np.any(decreasable):
367
+ break
368
+
369
+ # Vectorized likelihood change calculations
370
+ delInc = np.full(len(fit), -np.inf)
371
+ delDec = np.full(len(fit), -np.inf)
372
+
373
+ if np.any(increasable):
374
+ mask = increasable
375
+ delInc[mask] = np.log(ps[mask]) - np.log(fit[mask] + 1)
376
+
377
+ if np.any(decreasable):
378
+ mask = decreasable
379
+ delDec[mask] = -np.log(ps[mask]) + np.log(fit[mask])
380
+
381
+ # Find best moves (vectorized)
382
+ finite_inc = np.isfinite(delInc)
383
+ finite_dec = np.isfinite(delDec)
384
+
385
+ max_delInc = np.max(delInc[finite_inc]) if np.any(finite_inc) else -np.inf
386
+ max_delDec = np.max(delDec[finite_dec]) if np.any(finite_dec) else -np.inf
387
+
388
+ # Get all indices of best moves
389
+ wInc_all = np.where((delInc == max_delInc) & finite_inc)[0]
390
+ wDec_all = np.where((delDec == max_delDec) & finite_dec)[0]
391
+
392
+ if len(wInc_all) == 0 and len(wDec_all) == 0:
393
+ break
394
+
395
+ # Randomly select from ties (matching R behavior)
396
+ wInc = np.random.choice(wInc_all) if len(wInc_all) > 0 else None
397
+ wDec = np.random.choice(wDec_all) if len(wDec_all) > 0 else None
398
+
399
+ # Current soup count
400
+ current_soup = int(np.sum(fit))
401
+
402
+ # Make moves based on current state
403
+ if current_soup < nSoupUMIs:
404
+ # Need more soup
405
+ if wInc is not None and max_delInc > -np.inf:
406
+ fit[wInc] += 1
407
+ elif current_soup > nSoupUMIs:
408
+ # Too much soup
409
+ if wDec is not None and max_delDec > -np.inf:
410
+ fit[wDec] -= 1
411
+ else:
412
+ # At target, check for improvements
413
+ if max_delInc + max_delDec > 0 and wInc is not None and wDec is not None:
414
+ fit[wInc] += 1
415
+ fit[wDec] -= 1
416
+ elif max_delInc + max_delDec == 0:
417
+ # Handle ambiguous case with vectorized redistribution
418
+ zeroBucket = np.unique(np.concatenate([wInc_all, wDec_all]))
419
+ if len(wDec_all) > 0 and len(zeroBucket) > 0:
420
+ fit[wDec_all] -= 1
421
+ redistribution = len(wDec_all) / len(zeroBucket)
422
+ fit[zeroBucket] += redistribution
423
+ break
424
+ else:
425
+ break
426
+
427
+ # Early termination check
428
+ if abs(current_soup - nSoupUMIs) <= 0.5:
429
+ break
430
+
431
+ if verbose and iteration == max_iter - 1:
432
+ warning(f"max iterations reached.")
433
+
434
+ return fit
435
+
436
+
437
+ def soup_only(
438
+ sc: soup_channel,
439
+ round: bool,
440
+ verbose: int,
441
+ p_cutoff: float
442
+ ) -> sparse.csr_matrix:
443
+
444
+ if verbose >= 1: info("identifying genes likely to be pure contamination")
445
+ corrected = sc.toc.copy().astype(float)
446
+
447
+ for cell_idx in range(sc.n_cells):
448
+ cell_rho = sc.metadata.iloc[cell_idx]['rho']
449
+ cell_nUMIs = sc.metadata.iloc[cell_idx]['n.umi']
450
+ observed = sc.toc[:, cell_idx].toarray().flatten()
451
+
452
+ # Expected soup counts
453
+ expected_soup = sc.soup_profile['est'].values * cell_nUMIs * cell_rho
454
+
455
+ # Calculate p-values for each gene
456
+ p_vals = []
457
+ for gene_idx in range(sc.n_genes):
458
+ if expected_soup[gene_idx] <= 0:
459
+ p_val = 0.0 if observed[gene_idx] > 0 else 1.0
460
+ else:
461
+ # Poisson test - is observed significantly > expected?
462
+ p_val = 1 - stats.poisson.cdf(observed[gene_idx] - 1, expected_soup[gene_idx])
463
+ p_vals.append(p_val)
464
+
465
+ # Sort genes by p-value
466
+ gene_order = np.argsort(p_vals)
467
+
468
+ # Remove genes until we've removed ~rho fraction
469
+ soup_removed = 0
470
+ target_soup = cell_nUMIs * cell_rho
471
+
472
+ for gene_idx in gene_order:
473
+ if p_vals[gene_idx] > p_cutoff:
474
+ # This gene shows no evidence of endogenous expression
475
+ soup_removed += observed[gene_idx]
476
+ corrected[gene_idx, cell_idx] = 0
477
+
478
+ if soup_removed >= target_soup:
479
+ break
480
+
481
+ if round:
482
+ corrected = stochastic_round(corrected)
483
+
484
+ return corrected.tocsr()
485
+
486
+
487
+ def stochastic_round(matrix: sparse.spmatrix) -> sparse.csr_matrix:
488
+
489
+ matrix = matrix.tocsr()
490
+ data = matrix.data.copy()
491
+
492
+ # Get integer and fractional parts
493
+ int_part = np.floor(data)
494
+ frac_part = data - int_part
495
+
496
+ # Stochastically round up based on fractional part
497
+ round_up = np.random.random(len(data)) < frac_part
498
+ data = int_part + round_up
499
+
500
+ # Create new matrix with integer values
501
+ result = sparse.csr_matrix((data, matrix.indices, matrix.indptr),
502
+ shape=matrix.shape, dtype=int)
503
+ return result