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.
- {exprmat-0.1.52 → exprmat-0.1.54}/PKG-INFO +23 -8
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/__init__.py +22 -17
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/ansi.py +7 -4
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/invcdf.pyx +1 -1
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/de.py +3 -3
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/palettes.py +8 -0
- exprmat-0.1.54/exprmat/preprocessing/background/correction.py +503 -0
- exprmat-0.1.54/exprmat/preprocessing/background/estimation.py +457 -0
- exprmat-0.1.54/exprmat/preprocessing/background/soupx.py +179 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/integrate.py +3 -36
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/experiment.py +13 -2
- exprmat-0.1.54/exprmat/transfer/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/pyproject.toml +26 -3
- {exprmat-0.1.52 → exprmat-0.1.54}/Cargo.lock +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/Cargo.toml +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/cnmf.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/dbscan.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/gating.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/kmeans.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/leiden.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/nmf.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/rogue.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/sc3.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/seriation.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/spectral.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/clustering/utils.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/cnv/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/cnv/cnvscore.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/cnv/infercnv.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/configuration.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/cistarget.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/cre.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/finders.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/geneset.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/icnet.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/io.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/lr.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/orthologs.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/presets.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/signature.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/data/tf.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deconv/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deconv/deconv.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deconv/simulation.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/aggregate.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/atac.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/aucell.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/de.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/gse.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/kde.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/recovery.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/summary.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/descriptive/tcr.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/dataset.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/gridsearch.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/inference.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/preprocessing.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/stats.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/deseq/utils.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/generank.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/linreg.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/moments.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/optimization.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/parallel.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/preprocess.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/pseudotime.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/transition.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/utils.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/dynamics/velocity.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/infergrn.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/modules.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/prune.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/regressors.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/grn/utils.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/image/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/image/align.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/aggregate.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/construction.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/importance.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/ligand.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/metrics.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/icnet/pagerank.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/method.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/cellchat.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/cellphonedb.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/connectome.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/geomean.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/logfc.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/natmi.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/rank_aggregate.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/scseqcomm.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/methods/singlecellsignalr.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/pipeline.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/preprocess.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/resources.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/lr/utils.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/metacell/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/metacell/engine.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/metacell/model.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/metacell/utils.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/callpeak.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/common.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/coverage.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/footprint.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/constants.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/idr.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/optimization.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/idr/utils.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/linkage.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/motif.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/peaks/sequence.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/atac.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/cnmf.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/cnv.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/A.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/B.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/C.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/D.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/E.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/F.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/G.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/H.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/I.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/J.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/K.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/L.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/M.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/N.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/O.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/P.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/Q.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/R.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/S.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/T.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/U.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/V.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/W.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/X.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/Y.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/data/Z.pk +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/expression.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/gse.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/icnet.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/lr.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/milestone.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/sankey.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/sequence.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/summary.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/track.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/trajectory.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/utils.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/plotting/velocity.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/__init__.py +0 -0
- {exprmat-0.1.52/exprmat/preprocessing/doublets → exprmat-0.1.54/exprmat/preprocessing/background}/__init__.py +0 -0
- {exprmat-0.1.52/exprmat/preprocessing/impute → exprmat-0.1.54/exprmat/preprocessing/doublets}/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/doublets/composite.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/doublets/scrublet.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/emptydrops.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/filter.py +0 -0
- {exprmat-0.1.52/exprmat/preprocessing/sctransform → exprmat-0.1.54/exprmat/preprocessing/impute}/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/impute/magic.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/linear.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/normal.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/plot.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/qc.py +0 -0
- {exprmat-0.1.52/exprmat/reader → exprmat-0.1.54/exprmat/preprocessing/sctransform}/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/sctransform/estimate.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/sctransform/ksmooth.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/sctransform/residual.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/sctransform/transform.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/sparse.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/preprocessing/uniform.py +0 -0
- {exprmat-0.1.52/exprmat/trajectory → exprmat-0.1.54/exprmat/reader}/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/conversions.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/matcher.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/metadata.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/spatial.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/static.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reader/updater.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/diffmap.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/forceatlas.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/mde.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/nn.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/outliers.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/pca.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/plot.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/scvi.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/spectral.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/tsne.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/reduction/umap.py +0 -0
- {exprmat-0.1.52/exprmat/transfer → exprmat-0.1.54/exprmat/trajectory}/__init__.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/cytotrace.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/elpi.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/pcurve.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/pseudotime.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/root.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/trajectory/trace.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/transfer/atlas.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/exprmat/utils.py +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/license +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/readme.md +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/Cargo.toml +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/license +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/readme +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/call_peaks.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/embedding.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/export.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/knn.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/lib.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/motif.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/network.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/preprocessing.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/utils/anndata.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2/src/utils.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/Cargo.toml +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/embedding.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/export.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/feature_count/counter.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/feature_count/data_iter.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/feature_count/matrix.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/feature_count/mod.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/genome.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/lib.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/motif.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/network.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/bam/flagstat.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/bam/header.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/bam/mark_duplicates.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/bam.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/import.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/mod.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/preprocessing/qc.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/utils/knn.rs +0 -0
- {exprmat-0.1.52 → exprmat-0.1.54}/snapatac2-core/src/utils/mod.rs +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
228
|
+
with os.fdopen(fd, "w") as f: f.write(ver_string)
|
|
225
229
|
license()
|
|
226
230
|
|
|
227
|
-
except FileExistsError:
|
|
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
|
|
30
|
-
|
|
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
|
|
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:
|
|
162
|
-
|
|
163
|
-
|
|
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
|