brisc 0.1.0__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.
- brisc-0.1.0/LICENSE +21 -0
- brisc-0.1.0/PKG-INFO +107 -0
- brisc-0.1.0/README.md +86 -0
- brisc-0.1.0/brisc/__init__.py +64 -0
- brisc-0.1.0/brisc/colorspacious.py +280 -0
- brisc-0.1.0/brisc/concatenate.py +123 -0
- brisc-0.1.0/brisc/de.py +1571 -0
- brisc-0.1.0/brisc/pseudobulk.py +6669 -0
- brisc-0.1.0/brisc/single_cell.py +19876 -0
- brisc-0.1.0/brisc/sparse.py +1175 -0
- brisc-0.1.0/brisc/type_aliases.py +32 -0
- brisc-0.1.0/brisc/utils.py +1330 -0
- brisc-0.1.0/brisc/validated_dict.py +549 -0
- brisc-0.1.0/brisc/x86_64_v2/__init__.py +0 -0
- brisc-0.1.0/brisc/x86_64_v2/cysparse.pyx +1336 -0
- brisc-0.1.0/brisc/x86_64_v2/cyutils.pyx +1276 -0
- brisc-0.1.0/brisc/x86_64_v2/doublets.pyx +581 -0
- brisc-0.1.0/brisc/x86_64_v2/embed.pyx +1743 -0
- brisc-0.1.0/brisc/x86_64_v2/harmonize.pyx +1298 -0
- brisc-0.1.0/brisc/x86_64_v2/hdf5.pyx +633 -0
- brisc-0.1.0/brisc/x86_64_v2/hvg.pyx +531 -0
- brisc-0.1.0/brisc/x86_64_v2/kmeans.pyx +1433 -0
- brisc-0.1.0/brisc/x86_64_v2/knn.pyx +973 -0
- brisc-0.1.0/brisc/x86_64_v2/labels.pyx +671 -0
- brisc-0.1.0/brisc/x86_64_v2/leiden.pyx +1674 -0
- brisc-0.1.0/brisc/x86_64_v2/norm_factors.pyx +322 -0
- brisc-0.1.0/brisc/x86_64_v2/normalize.pyx +451 -0
- brisc-0.1.0/brisc/x86_64_v2/pca.pyx +1257 -0
- brisc-0.1.0/brisc/x86_64_v2/pseudobulk_and_markers.pyx +759 -0
- brisc-0.1.0/brisc/x86_64_v2/qc.pyx +378 -0
- brisc-0.1.0/brisc/x86_64_v2/snn.pyx +489 -0
- brisc-0.1.0/brisc/x86_64_v3/__init__.py +0 -0
- brisc-0.1.0/brisc/x86_64_v3/cysparse.pyx +1336 -0
- brisc-0.1.0/brisc/x86_64_v3/cyutils.pyx +1276 -0
- brisc-0.1.0/brisc/x86_64_v3/doublets.pyx +581 -0
- brisc-0.1.0/brisc/x86_64_v3/embed.pyx +1743 -0
- brisc-0.1.0/brisc/x86_64_v3/harmonize.pyx +1298 -0
- brisc-0.1.0/brisc/x86_64_v3/hdf5.pyx +633 -0
- brisc-0.1.0/brisc/x86_64_v3/hvg.pyx +531 -0
- brisc-0.1.0/brisc/x86_64_v3/kmeans.pyx +1433 -0
- brisc-0.1.0/brisc/x86_64_v3/knn.pyx +973 -0
- brisc-0.1.0/brisc/x86_64_v3/labels.pyx +671 -0
- brisc-0.1.0/brisc/x86_64_v3/leiden.pyx +1674 -0
- brisc-0.1.0/brisc/x86_64_v3/norm_factors.pyx +322 -0
- brisc-0.1.0/brisc/x86_64_v3/normalize.pyx +451 -0
- brisc-0.1.0/brisc/x86_64_v3/pca.pyx +1257 -0
- brisc-0.1.0/brisc/x86_64_v3/pseudobulk_and_markers.pyx +759 -0
- brisc-0.1.0/brisc/x86_64_v3/qc.pyx +378 -0
- brisc-0.1.0/brisc/x86_64_v3/snn.pyx +489 -0
- brisc-0.1.0/brisc/x86_64_v4/__init__.py +0 -0
- brisc-0.1.0/brisc/x86_64_v4/cysparse.pyx +1336 -0
- brisc-0.1.0/brisc/x86_64_v4/cyutils.pyx +1276 -0
- brisc-0.1.0/brisc/x86_64_v4/doublets.pyx +581 -0
- brisc-0.1.0/brisc/x86_64_v4/embed.pyx +1743 -0
- brisc-0.1.0/brisc/x86_64_v4/harmonize.pyx +1298 -0
- brisc-0.1.0/brisc/x86_64_v4/hdf5.pyx +633 -0
- brisc-0.1.0/brisc/x86_64_v4/hvg.pyx +531 -0
- brisc-0.1.0/brisc/x86_64_v4/kmeans.pyx +1433 -0
- brisc-0.1.0/brisc/x86_64_v4/knn.pyx +973 -0
- brisc-0.1.0/brisc/x86_64_v4/labels.pyx +671 -0
- brisc-0.1.0/brisc/x86_64_v4/leiden.pyx +1674 -0
- brisc-0.1.0/brisc/x86_64_v4/norm_factors.pyx +322 -0
- brisc-0.1.0/brisc/x86_64_v4/normalize.pyx +451 -0
- brisc-0.1.0/brisc/x86_64_v4/pca.pyx +1257 -0
- brisc-0.1.0/brisc/x86_64_v4/pseudobulk_and_markers.pyx +759 -0
- brisc-0.1.0/brisc/x86_64_v4/qc.pyx +378 -0
- brisc-0.1.0/brisc/x86_64_v4/snn.pyx +489 -0
- brisc-0.1.0/brisc.egg-info/PKG-INFO +107 -0
- brisc-0.1.0/brisc.egg-info/SOURCES.txt +84 -0
- brisc-0.1.0/brisc.egg-info/dependency_links.txt +1 -0
- brisc-0.1.0/brisc.egg-info/requires.txt +11 -0
- brisc-0.1.0/brisc.egg-info/top_level.txt +1 -0
- brisc-0.1.0/pyproject.toml +58 -0
- brisc-0.1.0/setup.cfg +4 -0
- brisc-0.1.0/setup.py +164 -0
- brisc-0.1.0/tests/test_cluster.py +24 -0
- brisc-0.1.0/tests/test_de.py +19 -0
- brisc-0.1.0/tests/test_embedding.py +51 -0
- brisc-0.1.0/tests/test_hvg.py +23 -0
- brisc-0.1.0/tests/test_integration.py +58 -0
- brisc-0.1.0/tests/test_markers.py +32 -0
- brisc-0.1.0/tests/test_neighbors.py +49 -0
- brisc-0.1.0/tests/test_normalize.py +33 -0
- brisc-0.1.0/tests/test_pca.py +25 -0
- brisc-0.1.0/tests/test_pseudobulk.py +20 -0
- brisc-0.1.0/tests/test_qc.py +108 -0
brisc-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Wainberg and Keon Arbabi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
brisc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brisc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fast single-cell toolkit
|
|
5
|
+
Author-email: Michael Wainberg <m.wainberg@utoronto.ca>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: anndata
|
|
10
|
+
Requires-Dist: h5py
|
|
11
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
12
|
+
Requires-Dist: numpy>=1.17.0
|
|
13
|
+
Requires-Dist: pandas
|
|
14
|
+
Requires-Dist: polars>=1.41.0
|
|
15
|
+
Requires-Dist: pyarrow>=0.17.0
|
|
16
|
+
Requires-Dist: ryp
|
|
17
|
+
Requires-Dist: scipy>=0.16.0
|
|
18
|
+
Requires-Dist: scikit-misc
|
|
19
|
+
Requires-Dist: threadpoolctl
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<picture>
|
|
24
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://brisc.run/_static/images/runner_title_wide_dark.svg">
|
|
25
|
+
<source media="(prefers-color-scheme: light)" srcset="https://brisc.run/_static/images/runner_title_wide_light.svg">
|
|
26
|
+
<img alt="brisc" src="https://brisc.run/_static/images/runner_title_wide_light.svg" width="500">
|
|
27
|
+
</picture>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
brisc is a high-performance library for analyzing single-cell data at scale. It prioritizes running as fast as possible on multi-core CPU systems, strict reproducibility, and a clean, user-friendly interface. On datasets of 1 to 20 million cells, it cuts the runtime of common workflows from hours to minutes.
|
|
31
|
+
|
|
32
|
+
Full documentation is available at **[brisc.run](https://brisc.run)**.
|
|
33
|
+
|
|
34
|
+
## Why brisc?
|
|
35
|
+
|
|
36
|
+
- **Blazing fast** — ground-up optimization of core algorithms and effective parallelism.
|
|
37
|
+
- **Deterministic** — every step gives floating-point identical results between runs, regardless of the number of threads used.
|
|
38
|
+
- **Complete toolkit** — preprocessing, dimensionality reduction, harmonization, label transfer, clustering, embedding, pseudobulk differential expression, and plotting.
|
|
39
|
+
- **Interoperable** — reads and writes `.h5ad`, `.rds`, `.h5Seurat`, and 10x files, and supports interleaving Python and R analyses via [ryp](https://github.com/Wainberg/ryp) without intermediate writes to disk.
|
|
40
|
+
- **Memory-efficient** — ~2× lower peak memory than Scanpy by tabulating which cells pass QC, instead of subsetting to them.
|
|
41
|
+
- **User-friendly** — sensible defaults, strict type-checking, and solution-focused error messages.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
brisc supports Linux, macOS, and Windows on Python 3.9+.
|
|
46
|
+
|
|
47
|
+
**conda (recommended)**
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
conda install -c conda-forge brisc
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**pip**
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install brisc
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
conda is recommended because it sets up the fast MKL BLAS and some of the R packages brisc uses. With pip, you'll need to handle those yourself: see the [installation guide](https://brisc.run/installation.html) for details, including optional R integration (for differential expression, Seurat, and SingleCellExperiment support) via [ryp](https://github.com/Wainberg/ryp).
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
### Basic workflow
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from brisc import SingleCell
|
|
67
|
+
|
|
68
|
+
sc = SingleCell('data.h5ad')\
|
|
69
|
+
.qc()\
|
|
70
|
+
.hvg(batch_column='donor')\
|
|
71
|
+
.normalize()\
|
|
72
|
+
.pca()\
|
|
73
|
+
.neighbors()\
|
|
74
|
+
.shared_neighbors()\
|
|
75
|
+
.cluster(resolution=[0.25, 0.5, 1, 1.5, 2])\
|
|
76
|
+
.pacmap()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Label transfer
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from brisc import SingleCell
|
|
83
|
+
|
|
84
|
+
sc_ref = SingleCell('data_ref.h5ad').qc()
|
|
85
|
+
sc_query = SingleCell('data_query.h5ad').qc()
|
|
86
|
+
sc_ref, sc_query = sc_ref.hvg(sc_query)
|
|
87
|
+
sc_ref = sc_ref.normalize()
|
|
88
|
+
sc_query = sc_query.normalize()
|
|
89
|
+
sc_ref, sc_query = sc_ref.pca(sc_query)
|
|
90
|
+
sc_ref, sc_query = sc_ref.harmonize(sc_query)
|
|
91
|
+
sc_query = sc_query.label_transfer_from(
|
|
92
|
+
sc_ref, 'cell_type')
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Pseudobulk differential expression
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from brisc import SingleCell
|
|
99
|
+
|
|
100
|
+
pb = SingleCell('data.h5ad')\
|
|
101
|
+
.qc()\
|
|
102
|
+
.pseudobulk('sample', 'cell_type')
|
|
103
|
+
de = pb\
|
|
104
|
+
.qc('condition')\
|
|
105
|
+
.library_size()\
|
|
106
|
+
.de('~ condition + sex + pmi')
|
|
107
|
+
```
|
brisc-0.1.0/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://brisc.run/_static/images/runner_title_wide_dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://brisc.run/_static/images/runner_title_wide_light.svg">
|
|
5
|
+
<img alt="brisc" src="https://brisc.run/_static/images/runner_title_wide_light.svg" width="500">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
brisc is a high-performance library for analyzing single-cell data at scale. It prioritizes running as fast as possible on multi-core CPU systems, strict reproducibility, and a clean, user-friendly interface. On datasets of 1 to 20 million cells, it cuts the runtime of common workflows from hours to minutes.
|
|
10
|
+
|
|
11
|
+
Full documentation is available at **[brisc.run](https://brisc.run)**.
|
|
12
|
+
|
|
13
|
+
## Why brisc?
|
|
14
|
+
|
|
15
|
+
- **Blazing fast** — ground-up optimization of core algorithms and effective parallelism.
|
|
16
|
+
- **Deterministic** — every step gives floating-point identical results between runs, regardless of the number of threads used.
|
|
17
|
+
- **Complete toolkit** — preprocessing, dimensionality reduction, harmonization, label transfer, clustering, embedding, pseudobulk differential expression, and plotting.
|
|
18
|
+
- **Interoperable** — reads and writes `.h5ad`, `.rds`, `.h5Seurat`, and 10x files, and supports interleaving Python and R analyses via [ryp](https://github.com/Wainberg/ryp) without intermediate writes to disk.
|
|
19
|
+
- **Memory-efficient** — ~2× lower peak memory than Scanpy by tabulating which cells pass QC, instead of subsetting to them.
|
|
20
|
+
- **User-friendly** — sensible defaults, strict type-checking, and solution-focused error messages.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
brisc supports Linux, macOS, and Windows on Python 3.9+.
|
|
25
|
+
|
|
26
|
+
**conda (recommended)**
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
conda install -c conda-forge brisc
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**pip**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install brisc
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
conda is recommended because it sets up the fast MKL BLAS and some of the R packages brisc uses. With pip, you'll need to handle those yourself: see the [installation guide](https://brisc.run/installation.html) for details, including optional R integration (for differential expression, Seurat, and SingleCellExperiment support) via [ryp](https://github.com/Wainberg/ryp).
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
### Basic workflow
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from brisc import SingleCell
|
|
46
|
+
|
|
47
|
+
sc = SingleCell('data.h5ad')\
|
|
48
|
+
.qc()\
|
|
49
|
+
.hvg(batch_column='donor')\
|
|
50
|
+
.normalize()\
|
|
51
|
+
.pca()\
|
|
52
|
+
.neighbors()\
|
|
53
|
+
.shared_neighbors()\
|
|
54
|
+
.cluster(resolution=[0.25, 0.5, 1, 1.5, 2])\
|
|
55
|
+
.pacmap()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Label transfer
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from brisc import SingleCell
|
|
62
|
+
|
|
63
|
+
sc_ref = SingleCell('data_ref.h5ad').qc()
|
|
64
|
+
sc_query = SingleCell('data_query.h5ad').qc()
|
|
65
|
+
sc_ref, sc_query = sc_ref.hvg(sc_query)
|
|
66
|
+
sc_ref = sc_ref.normalize()
|
|
67
|
+
sc_query = sc_query.normalize()
|
|
68
|
+
sc_ref, sc_query = sc_ref.pca(sc_query)
|
|
69
|
+
sc_ref, sc_query = sc_ref.harmonize(sc_query)
|
|
70
|
+
sc_query = sc_query.label_transfer_from(
|
|
71
|
+
sc_ref, 'cell_type')
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Pseudobulk differential expression
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from brisc import SingleCell
|
|
78
|
+
|
|
79
|
+
pb = SingleCell('data.h5ad')\
|
|
80
|
+
.qc()\
|
|
81
|
+
.pseudobulk('sample', 'cell_type')
|
|
82
|
+
de = pb\
|
|
83
|
+
.qc('condition')\
|
|
84
|
+
.library_size()\
|
|
85
|
+
.de('~ condition + sex + pmi')
|
|
86
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import signal
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Disable HDF5 file locking, which can cause issues when loading
|
|
6
|
+
os.environ['HDF5_USE_FILE_LOCKING'] = 'FALSE'
|
|
7
|
+
|
|
8
|
+
# Python's `logging` module calls `os.register_at_fork()` to handle aspects of
|
|
9
|
+
# its internal locking. Unfortunately, this can cause KeyboardInterrupts to be
|
|
10
|
+
# repeatedly ignored in multiprocessing-based HDF5 loading with errors like:
|
|
11
|
+
# Exception ignored in: <function _releaseLock at 0x7fb85797c5e0>
|
|
12
|
+
# Traceback (most recent call last):
|
|
13
|
+
# File ".../logging/__init__.py", line 243, in _releaseLock
|
|
14
|
+
# def _releaseLock():
|
|
15
|
+
# To get around this bug, temporarily monkeypatch `os.register_at_fork()` to be
|
|
16
|
+
# a null-op, then `import logging`. Unfortunately, there's no way to unregister
|
|
17
|
+
# a callback created with `os.register_at_fork()`, so this "fix" only works
|
|
18
|
+
# when the `logging` module has not been imported yet.
|
|
19
|
+
if 'logging' not in sys.modules and hasattr(os, 'register_at_fork'):
|
|
20
|
+
original_register_at_fork = os.register_at_fork
|
|
21
|
+
os.register_at_fork = lambda *args, **kwargs: None
|
|
22
|
+
try:
|
|
23
|
+
import logging
|
|
24
|
+
finally:
|
|
25
|
+
os.register_at_fork = original_register_at_fork
|
|
26
|
+
|
|
27
|
+
# Ignore Ctrl + C when importing certain modules, to avoid errors due to
|
|
28
|
+
# incomplete imports
|
|
29
|
+
original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
30
|
+
try:
|
|
31
|
+
import h5py
|
|
32
|
+
import numpy as np
|
|
33
|
+
import polars as pl
|
|
34
|
+
import pyarrow as pa
|
|
35
|
+
from scipy import sparse
|
|
36
|
+
from scipy.sparse._compressed import _cs_matrix
|
|
37
|
+
from scipy.special import stdtrit
|
|
38
|
+
finally:
|
|
39
|
+
signal.signal(signal.SIGINT, original_sigint_handler)
|
|
40
|
+
|
|
41
|
+
# On Linux with thread pinning enabled, MKL BLAS narrows the thread affinity
|
|
42
|
+
# mask to one core when first executed, which would make all of brisc run
|
|
43
|
+
# single-threaded if not fixed. Fix this now by running a BLAS function, then
|
|
44
|
+
# re-expanding the mask to all available cores. (This works even if cgroups
|
|
45
|
+
# limit the available cores, e.g. on Slurm: the OS will take the intersection
|
|
46
|
+
# of what we asked for and what the cgroup allows.)
|
|
47
|
+
if hasattr(os, 'sched_setaffinity'):
|
|
48
|
+
np.linalg.svd([[0]])
|
|
49
|
+
os.sched_setaffinity(0, range(os.cpu_count()))
|
|
50
|
+
|
|
51
|
+
# Ignore harmless warnings about libiomp/libomp mismatch on Windows, which come
|
|
52
|
+
# from compiling with /openmp:llvm but calling BLAS functions compiled with
|
|
53
|
+
# Intel OpenMP
|
|
54
|
+
if sys.platform == 'win32':
|
|
55
|
+
import warnings
|
|
56
|
+
warnings.filterwarnings(action='ignore', module='threadpoolctl',
|
|
57
|
+
category=RuntimeWarning)
|
|
58
|
+
|
|
59
|
+
# Expose the public API
|
|
60
|
+
from .single_cell import SingleCell
|
|
61
|
+
from .pseudobulk import Pseudobulk
|
|
62
|
+
from .de import DE
|
|
63
|
+
from .concatenate import concat_obs, concat_var
|
|
64
|
+
__all__ = 'SingleCell', 'Pseudobulk', 'DE', 'concat_obs', 'concat_var'
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A standalone replacement for `colorspacious.cspace_convert()`, covering only
|
|
3
|
+
the conversions used by `generate_palette()`:
|
|
4
|
+
|
|
5
|
+
* ('sRGB1', 'CAM02-UCS')
|
|
6
|
+
* ('sRGB255', 'CAM02-UCS')
|
|
7
|
+
* ('CAM02-UCS', 'JCh')
|
|
8
|
+
* ('CAM02-UCS', 'sRGB1')
|
|
9
|
+
|
|
10
|
+
It is a faithful port of the relevant pieces of colorspacious
|
|
11
|
+
(github.com/njsmith/colorspacious, MIT license, (C) Nathaniel J. Smith),
|
|
12
|
+
using the fixed sRGB viewing conditions and the CAM02-UCS parameters that
|
|
13
|
+
colorspacious uses for these named color spaces. It reproduces colorspacious's
|
|
14
|
+
numeric output for these routes, but does not support any other conversion.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
__all__ = ['cspace_convert']
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# sRGB <-> linear-sRGB <-> XYZ100 (from colorspacious/basics.py)
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
# Exact matrix from IEC 61966-2-1:1999
|
|
26
|
+
_XYZ100_to_sRGB1_matrix = np.array([
|
|
27
|
+
[ 3.2406, -1.5372, -0.4986],
|
|
28
|
+
[-0.9689, 1.8758, 0.0415],
|
|
29
|
+
[ 0.0557, -0.2040, 1.0570]])
|
|
30
|
+
_sRGB1_to_XYZ100_matrix = np.linalg.inv(_XYZ100_to_sRGB1_matrix)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _srgb_to_linear(c):
|
|
34
|
+
c = np.asarray(c, dtype=float)
|
|
35
|
+
out = np.empty(c.shape, dtype=float)
|
|
36
|
+
lin = c < 0.04045
|
|
37
|
+
a = 0.055
|
|
38
|
+
out[lin] = c[lin] / 12.92
|
|
39
|
+
out[~lin] = ((c[~lin] + a) / (a + 1)) ** 2.4
|
|
40
|
+
return out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _linear_to_srgb(c):
|
|
44
|
+
c = np.asarray(c, dtype=float)
|
|
45
|
+
out = np.empty(c.shape, dtype=float)
|
|
46
|
+
lin = c <= 0.0031308
|
|
47
|
+
a = 0.055
|
|
48
|
+
out[lin] = c[lin] * 12.92
|
|
49
|
+
out[~lin] = (1 + a) * c[~lin] ** (1 / 2.4) - a
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _matvec(mat, vecs):
|
|
54
|
+
# mat @ each trailing-3 vector of `vecs`
|
|
55
|
+
return np.einsum('...ij,...j->...i', mat, vecs)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _sRGB1_to_XYZ100(sRGB1):
|
|
59
|
+
return _matvec(_sRGB1_to_XYZ100_matrix, _srgb_to_linear(sRGB1)) * 100.0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _XYZ100_to_sRGB1(XYZ100):
|
|
63
|
+
lin = _matvec(_XYZ100_to_sRGB1_matrix, np.asarray(XYZ100, float) / 100.0)
|
|
64
|
+
return _linear_to_srgb(lin)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# CIECAM02 viewing conditions (colorspacious's CIECAM02Space.sRGB)
|
|
69
|
+
# XYZ100_w = D65, Y_b = 20, L_A = (64/pi)/5, average surround
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
_M_CAT02 = np.array([[ 0.7328, 0.4296, -0.1624],
|
|
73
|
+
[-0.7036, 1.6975, 0.0061],
|
|
74
|
+
[ 0.0030, 0.0136, 0.9834]])
|
|
75
|
+
_M_HPE = np.array([[ 0.38971, 0.68898, -0.07868],
|
|
76
|
+
[-0.22981, 1.18340, 0.04641],
|
|
77
|
+
[ 0.00000, 0.00000, 1.00000]])
|
|
78
|
+
_M_CAT02_inv = np.linalg.inv(_M_CAT02)
|
|
79
|
+
_M_HPE_M_CAT02_inv = _M_HPE @ _M_CAT02_inv
|
|
80
|
+
_M_CAT02_M_HPE_inv = _M_CAT02 @ np.linalg.inv(_M_HPE)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _VC:
|
|
84
|
+
"""Precomputed CIECAM02 viewing-condition constants (sRGB)."""
|
|
85
|
+
def __init__(self):
|
|
86
|
+
XYZ_w = np.array([95.047, 100.0, 108.883]) # D65
|
|
87
|
+
Y_b = 20.0
|
|
88
|
+
L_A = (64.0 / np.pi) / 5.0
|
|
89
|
+
F, c, N_c = 1.0, 0.69, 1.0 # average surround
|
|
90
|
+
|
|
91
|
+
self.c = c
|
|
92
|
+
self.N_c = N_c
|
|
93
|
+
|
|
94
|
+
RGB_w = _M_CAT02 @ XYZ_w
|
|
95
|
+
D = F * (1 - (1 / 3.6) * np.exp((-L_A - 42) / 92))
|
|
96
|
+
D = np.clip(D, 0, 1)
|
|
97
|
+
self.D_RGB = D * XYZ_w[1] / RGB_w + 1 - D
|
|
98
|
+
|
|
99
|
+
k = 1 / (5 * L_A + 1)
|
|
100
|
+
self.F_L = (0.2 * k ** 4 * (5 * L_A)
|
|
101
|
+
+ 0.1 * (1 - k ** 4) ** 2 * (5 * L_A) ** (1 / 3))
|
|
102
|
+
self.n = Y_b / XYZ_w[1]
|
|
103
|
+
self.z = 1.48 + np.sqrt(self.n)
|
|
104
|
+
self.N_bb = 0.725 * (1 / self.n) ** 0.2
|
|
105
|
+
self.N_cb = self.N_bb
|
|
106
|
+
|
|
107
|
+
RGB_wc = self.D_RGB * RGB_w
|
|
108
|
+
RGBp_w = _M_HPE_M_CAT02_inv @ RGB_wc
|
|
109
|
+
tmp = ((self.F_L * RGBp_w) / 100) ** 0.42
|
|
110
|
+
RGBp_aw = 400 * (tmp / (tmp + 27.13)) + 0.1
|
|
111
|
+
self.A_w = (np.dot([2, 1, 1 / 20], RGBp_aw) - 0.305) * self.N_bb
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_VC = _VC()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _XYZ100_to_JCh_M(XYZ100):
|
|
118
|
+
"""Forward CIECAM02. Returns (J, C, h, M)."""
|
|
119
|
+
vc = _VC
|
|
120
|
+
XYZ100 = np.asarray(XYZ100, dtype=float)
|
|
121
|
+
|
|
122
|
+
RGB = np.inner(XYZ100, _M_CAT02)
|
|
123
|
+
RGB_C = vc.D_RGB * RGB
|
|
124
|
+
RGBp = np.inner(RGB_C, _M_HPE_M_CAT02_inv)
|
|
125
|
+
|
|
126
|
+
signs = np.sign(RGBp)
|
|
127
|
+
tmp = (vc.F_L * signs * RGBp / 100) ** 0.42
|
|
128
|
+
RGBp_a = signs * 400 * (tmp / (tmp + 27.13)) + 0.1
|
|
129
|
+
|
|
130
|
+
a = np.inner(RGBp_a, [1, -12 / 11, 1 / 11])
|
|
131
|
+
b = np.inner(RGBp_a, [1 / 9, 1 / 9, -2 / 9])
|
|
132
|
+
h_rad = np.arctan2(b, a)
|
|
133
|
+
h = np.rad2deg(h_rad) % 360
|
|
134
|
+
|
|
135
|
+
A = (np.inner(RGBp_a, [2, 1, 1 / 20]) - 0.305) * vc.N_bb
|
|
136
|
+
if np.any(A < 0):
|
|
137
|
+
error_message = 'achromatic signal A was negative'
|
|
138
|
+
raise ValueError(error_message)
|
|
139
|
+
|
|
140
|
+
J = 100 * (A / vc.A_w) ** (vc.c * vc.z)
|
|
141
|
+
e = (12500 / 13) * vc.N_c * vc.N_cb * (np.cos(h_rad + 2) + 3.8)
|
|
142
|
+
t = (e * np.sqrt(a ** 2 + b ** 2)) / np.inner(RGBp_a, [1, 1, 21 / 20])
|
|
143
|
+
C = t ** 0.9 * (J / 100) ** 0.5 * (1.64 - 0.29 ** vc.n) ** 0.73
|
|
144
|
+
M = C * vc.F_L ** 0.25
|
|
145
|
+
return J, C, h, M
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _JMh_to_XYZ100(J, M, h):
|
|
149
|
+
"""Inverse CIECAM02 from (J, M, h). Port of the J/M/h path only."""
|
|
150
|
+
vc = _VC
|
|
151
|
+
J = np.asarray(J, dtype=float)
|
|
152
|
+
M = np.asarray(M, dtype=float)
|
|
153
|
+
h = np.asarray(h, dtype=float)
|
|
154
|
+
|
|
155
|
+
C = M / vc.F_L ** 0.25
|
|
156
|
+
|
|
157
|
+
J, C, h = np.broadcast_arrays(J, C, h)
|
|
158
|
+
target_shape = J.shape
|
|
159
|
+
if J.ndim == 0:
|
|
160
|
+
J = np.atleast_1d(J)
|
|
161
|
+
C = np.atleast_1d(C)
|
|
162
|
+
h = np.atleast_1d(h)
|
|
163
|
+
|
|
164
|
+
t = (C / (np.sqrt(J / 100)
|
|
165
|
+
* (1.64 - 0.29 ** vc.n) ** 0.73)) ** (1 / 0.9)
|
|
166
|
+
e_t = 0.25 * (np.cos(np.deg2rad(h) + 2) + 3.8)
|
|
167
|
+
A = vc.A_w * (J / 100) ** (1 / (vc.c * vc.z))
|
|
168
|
+
|
|
169
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
170
|
+
one_over_t = 1 / t
|
|
171
|
+
one_over_t = np.select([np.isnan(one_over_t), True],
|
|
172
|
+
[np.inf, one_over_t])
|
|
173
|
+
|
|
174
|
+
p_1 = (50000 / 13) * vc.N_c * vc.N_cb * e_t * one_over_t
|
|
175
|
+
p_2 = A / vc.N_bb + 0.305
|
|
176
|
+
p_3 = 21 / 20
|
|
177
|
+
|
|
178
|
+
sin_h = np.sin(np.deg2rad(h))
|
|
179
|
+
cos_h = np.cos(np.deg2rad(h))
|
|
180
|
+
|
|
181
|
+
num = p_2 * (2 + p_3) * (460 / 1403)
|
|
182
|
+
denom_part2 = (2 + p_3) * (220 / 1403)
|
|
183
|
+
denom_part3 = (-27 / 1403) + p_3 * (6300 / 1403)
|
|
184
|
+
|
|
185
|
+
a = np.empty_like(h)
|
|
186
|
+
b = np.empty_like(h)
|
|
187
|
+
small_cos = np.abs(sin_h) >= np.abs(cos_h)
|
|
188
|
+
|
|
189
|
+
b[small_cos] = (num[small_cos]
|
|
190
|
+
/ (p_1[small_cos] / sin_h[small_cos]
|
|
191
|
+
+ denom_part2 * cos_h[small_cos] / sin_h[small_cos]
|
|
192
|
+
+ denom_part3))
|
|
193
|
+
a[small_cos] = b[small_cos] * cos_h[small_cos] / sin_h[small_cos]
|
|
194
|
+
|
|
195
|
+
a[~small_cos] = (num[~small_cos]
|
|
196
|
+
/ (p_1[~small_cos] / cos_h[~small_cos]
|
|
197
|
+
+ denom_part2
|
|
198
|
+
+ denom_part3 * sin_h[~small_cos] / cos_h[~small_cos]))
|
|
199
|
+
b[~small_cos] = a[~small_cos] * sin_h[~small_cos] / cos_h[~small_cos]
|
|
200
|
+
|
|
201
|
+
p2ab = np.stack([p_2, a, b], axis=-1)
|
|
202
|
+
RGBp_a_matrix = (1 / 1403) * np.array([[460, 451, 288],
|
|
203
|
+
[460, -891, -261],
|
|
204
|
+
[460, -220, -6300]], dtype=float)
|
|
205
|
+
RGBp_a = np.inner(p2ab, RGBp_a_matrix)
|
|
206
|
+
|
|
207
|
+
RGBp = (np.sign(RGBp_a - 0.1)
|
|
208
|
+
* (100 / vc.F_L)
|
|
209
|
+
* ((27.13 * np.abs(RGBp_a - 0.1))
|
|
210
|
+
/ (400 - np.abs(RGBp_a - 0.1))) ** (1 / 0.42))
|
|
211
|
+
|
|
212
|
+
RGB_C = np.inner(RGBp, _M_CAT02_M_HPE_inv)
|
|
213
|
+
RGB = RGB_C / vc.D_RGB
|
|
214
|
+
XYZ100 = np.inner(RGB, _M_CAT02_inv)
|
|
215
|
+
return XYZ100.reshape(target_shape + (3,))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
# CAM02-UCS (Luo et al. 2006) J'a'b' <-> JMh (KL=1.00, c1=0.007, c2=0.0228)
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
_UCS_KL, _UCS_C1, _UCS_C2 = 1.00, 0.007, 0.0228
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _JMh_to_Jpapbp(JMh):
|
|
226
|
+
JMh = np.asarray(JMh, dtype=float)
|
|
227
|
+
J = JMh[..., 0]
|
|
228
|
+
M = JMh[..., 1]
|
|
229
|
+
h = JMh[..., 2]
|
|
230
|
+
Jp = (1 + 100 * _UCS_C1) * J / (1 + _UCS_C1 * J) / _UCS_KL
|
|
231
|
+
Mp = (1 / _UCS_C2) * np.log(1 + _UCS_C2 * M)
|
|
232
|
+
h_rad = np.deg2rad(h)
|
|
233
|
+
ap = Mp * np.cos(h_rad)
|
|
234
|
+
bp = Mp * np.sin(h_rad)
|
|
235
|
+
return np.stack([Jp, ap, bp], axis=-1)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _Jpapbp_to_JMh(Jpapbp):
|
|
239
|
+
Jpapbp = np.asarray(Jpapbp, dtype=float)
|
|
240
|
+
Jp = Jpapbp[..., 0] * _UCS_KL
|
|
241
|
+
ap = Jpapbp[..., 1]
|
|
242
|
+
bp = Jpapbp[..., 2]
|
|
243
|
+
J = -Jp / (_UCS_C1 * Jp - 100 * _UCS_C1 - 1)
|
|
244
|
+
Mp = np.hypot(ap, bp)
|
|
245
|
+
h = np.rad2deg(np.arctan2(bp, ap)) % 360
|
|
246
|
+
M = (np.exp(_UCS_C2 * Mp) - 1) / _UCS_C2
|
|
247
|
+
return np.stack([J, M, h], axis=-1)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Public entry point
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
def cspace_convert(arr, start, end):
|
|
255
|
+
"""Drop-in replacement for `colorspacious.cspace_convert()`, restricted to
|
|
256
|
+
the start-end pairs used by `generate_palette()`. Raises for anything
|
|
257
|
+
else."""
|
|
258
|
+
route = (start, end)
|
|
259
|
+
|
|
260
|
+
if route == ('sRGB1', 'CAM02-UCS'):
|
|
261
|
+
J, C, h, M = _XYZ100_to_JCh_M(_sRGB1_to_XYZ100(arr))
|
|
262
|
+
return _JMh_to_Jpapbp(np.stack([J, M, h], axis=-1))
|
|
263
|
+
|
|
264
|
+
if route == ('sRGB255', 'CAM02-UCS'):
|
|
265
|
+
sRGB1 = np.asarray(arr, dtype=float) / 255.0
|
|
266
|
+
J, C, h, M = _XYZ100_to_JCh_M(_sRGB1_to_XYZ100(sRGB1))
|
|
267
|
+
return _JMh_to_Jpapbp(np.stack([J, M, h], axis=-1))
|
|
268
|
+
|
|
269
|
+
if route == ('CAM02-UCS', 'JCh'):
|
|
270
|
+
JMh = _Jpapbp_to_JMh(arr)
|
|
271
|
+
XYZ100 = _JMh_to_XYZ100(JMh[..., 0], JMh[..., 1], JMh[..., 2])
|
|
272
|
+
J, C, h, M = _XYZ100_to_JCh_M(XYZ100)
|
|
273
|
+
return np.stack([J, C, h], axis=-1)
|
|
274
|
+
|
|
275
|
+
if route == ('CAM02-UCS', 'sRGB1'):
|
|
276
|
+
JMh = _Jpapbp_to_JMh(arr)
|
|
277
|
+
XYZ100 = _JMh_to_XYZ100(JMh[..., 0], JMh[..., 1], JMh[..., 2])
|
|
278
|
+
return _XYZ100_to_sRGB1(XYZ100)
|
|
279
|
+
|
|
280
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
from .single_cell import SingleCell
|
|
4
|
+
from .pseudobulk import Pseudobulk
|
|
5
|
+
from .utils import check_type, to_tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def concat_obs(datasets: SingleCell | Iterable[SingleCell] |
|
|
9
|
+
Pseudobulk | Iterable[Pseudobulk],
|
|
10
|
+
*more_datasets: SingleCell | Pseudobulk,
|
|
11
|
+
dataset_column: str | None = None,
|
|
12
|
+
dataset_labels: Iterable[str] | None = None,
|
|
13
|
+
flexible: bool = False,
|
|
14
|
+
num_threads: int | np.integer | None = None) -> \
|
|
15
|
+
SingleCell | Pseudobulk:
|
|
16
|
+
"""
|
|
17
|
+
Concatenate multiple SingleCell datasets cell-wise, or multiple Pseudobulk
|
|
18
|
+
datasets sample-wise.
|
|
19
|
+
|
|
20
|
+
Delegates to `SingleCell.concat_obs()` or `Pseudobulk.concat_obs()`,
|
|
21
|
+
depending on whether the datasets are SingleCell or Pseudobulk.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
datasets: one or more SingleCell or Pseudobulk datasets to concatenate
|
|
25
|
+
*more_datasets: additional SingleCell or Pseudobulk datasets to
|
|
26
|
+
concatenate with this one, specified as positional
|
|
27
|
+
arguments
|
|
28
|
+
dataset_column: the name of an Enum column to be added to the
|
|
29
|
+
concatenated dataset's `obs` labeling which dataset
|
|
30
|
+
each cell came from. The labels themselves are
|
|
31
|
+
determined by the `dataset_labels` argument.
|
|
32
|
+
dataset_labels: a sequence of labels for each dataset, used to populate
|
|
33
|
+
`dataset_column`. There must be one label per dataset
|
|
34
|
+
being concatenated. If `dataset_labels` is not
|
|
35
|
+
specified, the labels default to `{dataset_column}_0`,
|
|
36
|
+
`{dataset_column}_1`, ..., `{dataset_column}_{N - 1}`.
|
|
37
|
+
Can only be specified when `dataset_column` is not
|
|
38
|
+
`None`.
|
|
39
|
+
flexible: whether to subset to genes, columns of `obs` and `var`, and
|
|
40
|
+
(for SingleCell datasets) keys of `obsm`, `varm` and `uns`
|
|
41
|
+
common to all datasets before concatenating, rather than
|
|
42
|
+
raising an error on any mismatches
|
|
43
|
+
num_threads: the number of threads to use when concatenating. Does not
|
|
44
|
+
affect the concatenated SingleCell or Pseudobulk dataset's
|
|
45
|
+
`num_threads`; this will always be the same as the first
|
|
46
|
+
dataset's `num_threads`.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The concatenated SingleCell or Pseudobulk dataset.
|
|
50
|
+
"""
|
|
51
|
+
if isinstance(datasets, (SingleCell, Pseudobulk)):
|
|
52
|
+
datasets = (datasets,) + more_datasets
|
|
53
|
+
else:
|
|
54
|
+
datasets = tuple(datasets) + more_datasets
|
|
55
|
+
check_type(datasets[0], 'the first dataset', (SingleCell, Pseudobulk),
|
|
56
|
+
'a SingleCell or Pseudobulk dataset')
|
|
57
|
+
if isinstance(datasets[0], SingleCell):
|
|
58
|
+
return datasets[0].concat_obs(
|
|
59
|
+
datasets[1:], flexible=flexible, dataset_column=dataset_column,
|
|
60
|
+
dataset_labels=dataset_labels, num_threads=num_threads)
|
|
61
|
+
else:
|
|
62
|
+
return datasets[0].concat_obs(
|
|
63
|
+
datasets[1:], flexible=flexible, dataset_column=dataset_column,
|
|
64
|
+
dataset_labels=dataset_labels)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def concat_var(datasets: SingleCell | Iterable[SingleCell] |
|
|
68
|
+
Pseudobulk | Iterable[Pseudobulk],
|
|
69
|
+
*more_datasets: SingleCell | Pseudobulk,
|
|
70
|
+
dataset_column: str | None = None,
|
|
71
|
+
dataset_labels: Iterable[str] | None = None,
|
|
72
|
+
flexible: bool = False,
|
|
73
|
+
num_threads: int | np.integer | None = None) -> \
|
|
74
|
+
SingleCell | Pseudobulk:
|
|
75
|
+
"""
|
|
76
|
+
Concatenate multiple SingleCell datasets or multiple Pseudobulk datasets,
|
|
77
|
+
gene-wise. This is much less common than the cell- or sample-wise
|
|
78
|
+
concatenation provided by `concat_obs()`.
|
|
79
|
+
|
|
80
|
+
Delegates to `SingleCell.concat_var()` or `Pseudobulk.concat_var()`,
|
|
81
|
+
depending on whether the datasets are SingleCell or Pseudobulk.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
datasets: one or more SingleCell or Pseudobulk datasets to concatenate
|
|
85
|
+
*more_datasets: additional Pseudobulk datasets to concatenate with this
|
|
86
|
+
one, specified as positional arguments
|
|
87
|
+
dataset_column: the name of an Enum column to be added to the
|
|
88
|
+
concatenated dataset's `var` labeling which dataset
|
|
89
|
+
each cell came from. The labels themselves are
|
|
90
|
+
determined by the `dataset_labels` argument.
|
|
91
|
+
dataset_labels: a sequence of labels for each dataset, used to populate
|
|
92
|
+
`dataset_column`. There must be one label per dataset
|
|
93
|
+
being concatenated. If `dataset_labels` is not
|
|
94
|
+
specified, the labels default to `{dataset_column}_0`,
|
|
95
|
+
`{dataset_column}_1`, ..., `{dataset_column}_{N - 1}`.
|
|
96
|
+
Can only be specified when `dataset_column` is not
|
|
97
|
+
`None`.
|
|
98
|
+
flexible: whether to subset to cells/samples, columns of `obs` and
|
|
99
|
+
`var`, and (for SingleCell datasets) keys of `obsm`, `varm`
|
|
100
|
+
and `uns` common to all datasets before concatenating, rather
|
|
101
|
+
than raising an error on any mismatches
|
|
102
|
+
num_threads: the number of threads to use when concatenating. Does not
|
|
103
|
+
affect the concatenated SingleCell/Pseudobulk dataset's
|
|
104
|
+
`num_threads`; this will always be the same as the first
|
|
105
|
+
dataset's `num_threads`.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The concatenated SingleCell or Pseudobulk dataset.
|
|
109
|
+
"""
|
|
110
|
+
if isinstance(datasets, (SingleCell, Pseudobulk)):
|
|
111
|
+
datasets = (datasets,) + more_datasets
|
|
112
|
+
else:
|
|
113
|
+
datasets = tuple(datasets) + more_datasets
|
|
114
|
+
check_type(datasets[0], 'the first dataset', (SingleCell, Pseudobulk),
|
|
115
|
+
'a SingleCell or Pseudobulk dataset')
|
|
116
|
+
if isinstance(datasets[0], SingleCell):
|
|
117
|
+
return datasets[0].concat_var(
|
|
118
|
+
datasets[1:], flexible=flexible, dataset_column=dataset_column,
|
|
119
|
+
dataset_labels=dataset_labels, num_threads=num_threads)
|
|
120
|
+
else:
|
|
121
|
+
return datasets[0].concat_var(
|
|
122
|
+
datasets[1:], flexible=flexible, dataset_column=dataset_column,
|
|
123
|
+
dataset_labels=dataset_labels)
|