sclab 0.1.8__tar.gz → 0.3.4__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.
- sclab-0.3.4/LICENSE +29 -0
- {sclab-0.1.8 → sclab-0.3.4}/PKG-INFO +20 -9
- {sclab-0.1.8 → sclab-0.3.4}/README.md +4 -4
- {sclab-0.1.8 → sclab-0.3.4}/pyproject.toml +17 -4
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/__init__.py +3 -1
- sclab-0.3.4/src/sclab/_io.py +103 -0
- sclab-0.3.4/src/sclab/_methods_registry.py +65 -0
- sclab-0.3.4/src/sclab/_sclab.py +300 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/_dataset.py +3 -5
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/processor/_processor.py +41 -19
- sclab-0.3.4/src/sclab/dataset/processor/_results_panel.py +94 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/processor/step/_processor_step_base.py +12 -6
- sclab-0.3.4/src/sclab/examples/processor_steps/__init__.py +23 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/examples/processor_steps/_cluster.py +2 -2
- sclab-0.3.4/src/sclab/examples/processor_steps/_differential_expression.py +329 -0
- sclab-0.3.4/src/sclab/examples/processor_steps/_doublet_detection.py +68 -0
- sclab-0.3.4/src/sclab/examples/processor_steps/_gene_expression.py +125 -0
- sclab-0.3.4/src/sclab/examples/processor_steps/_integration.py +116 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/examples/processor_steps/_neighbors.py +26 -6
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/examples/processor_steps/_pca.py +13 -8
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/examples/processor_steps/_preprocess.py +52 -25
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/examples/processor_steps/_qc.py +24 -8
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/examples/processor_steps/_umap.py +2 -2
- sclab-0.3.4/src/sclab/gui/__init__.py +0 -0
- sclab-0.3.4/src/sclab/gui/components/__init__.py +7 -0
- sclab-0.3.4/src/sclab/gui/components/_guided_pseudotime.py +482 -0
- sclab-0.3.4/src/sclab/gui/components/_transfer_metadata.py +186 -0
- sclab-0.3.4/src/sclab/methods/__init__.py +50 -0
- sclab-0.3.4/src/sclab/preprocess/__init__.py +26 -0
- sclab-0.3.4/src/sclab/preprocess/_cca.py +176 -0
- sclab-0.3.4/src/sclab/preprocess/_cca_integrate.py +109 -0
- sclab-0.3.4/src/sclab/preprocess/_filter_obs.py +42 -0
- sclab-0.3.4/src/sclab/preprocess/_harmony.py +421 -0
- sclab-0.3.4/src/sclab/preprocess/_harmony_integrate.py +53 -0
- sclab-0.3.4/src/sclab/preprocess/_normalize_weighted.py +65 -0
- sclab-0.3.4/src/sclab/preprocess/_pca.py +51 -0
- sclab-0.3.4/src/sclab/preprocess/_preprocess.py +155 -0
- sclab-0.3.4/src/sclab/preprocess/_qc.py +38 -0
- sclab-0.3.4/src/sclab/preprocess/_rpca.py +116 -0
- sclab-0.3.4/src/sclab/preprocess/_subset.py +208 -0
- sclab-0.3.4/src/sclab/preprocess/_transfer_metadata.py +196 -0
- sclab-0.3.4/src/sclab/preprocess/_transform.py +82 -0
- sclab-0.3.4/src/sclab/preprocess/_utils.py +96 -0
- sclab-0.3.4/src/sclab/scanpy/__init__.py +0 -0
- sclab-0.3.4/src/sclab/scanpy/_compat.py +92 -0
- sclab-0.3.4/src/sclab/scanpy/_settings.py +526 -0
- sclab-0.3.4/src/sclab/scanpy/logging.py +290 -0
- sclab-0.3.4/src/sclab/scanpy/plotting/__init__.py +0 -0
- sclab-0.3.4/src/sclab/scanpy/plotting/_rcmod.py +73 -0
- sclab-0.3.4/src/sclab/scanpy/plotting/palettes.py +221 -0
- sclab-0.3.4/src/sclab/scanpy/readwrite.py +1108 -0
- sclab-0.3.4/src/sclab/tools/__init__.py +0 -0
- sclab-0.3.4/src/sclab/tools/cellflow/__init__.py +0 -0
- sclab-0.3.4/src/sclab/tools/cellflow/density_dynamics/__init__.py +0 -0
- sclab-0.3.4/src/sclab/tools/cellflow/density_dynamics/_density_dynamics.py +349 -0
- sclab-0.3.4/src/sclab/tools/cellflow/pseudotime/__init__.py +0 -0
- sclab-0.3.4/src/sclab/tools/cellflow/pseudotime/_pseudotime.py +336 -0
- sclab-0.3.4/src/sclab/tools/cellflow/pseudotime/timeseries.py +226 -0
- sclab-0.3.4/src/sclab/tools/cellflow/utils/__init__.py +0 -0
- sclab-0.3.4/src/sclab/tools/cellflow/utils/density_nd.py +215 -0
- sclab-0.3.4/src/sclab/tools/cellflow/utils/interpolate.py +334 -0
- sclab-0.3.4/src/sclab/tools/cellflow/utils/periodic_genes.py +106 -0
- sclab-0.3.4/src/sclab/tools/cellflow/utils/smoothen.py +124 -0
- sclab-0.3.4/src/sclab/tools/cellflow/utils/times.py +55 -0
- sclab-0.3.4/src/sclab/tools/differential_expression/__init__.py +7 -0
- sclab-0.3.4/src/sclab/tools/differential_expression/_pseudobulk_edger.py +309 -0
- sclab-0.3.4/src/sclab/tools/differential_expression/_pseudobulk_helpers.py +290 -0
- sclab-0.3.4/src/sclab/tools/differential_expression/_pseudobulk_limma.py +257 -0
- sclab-0.3.4/src/sclab/tools/doublet_detection/__init__.py +5 -0
- sclab-0.3.4/src/sclab/tools/doublet_detection/_scrublet.py +64 -0
- sclab-0.3.4/src/sclab/tools/embedding/__init__.py +0 -0
- sclab-0.3.4/src/sclab/tools/imputation/__init__.py +0 -0
- sclab-0.3.4/src/sclab/tools/imputation/_alra.py +135 -0
- sclab-0.3.4/src/sclab/tools/labeling/__init__.py +6 -0
- sclab-0.3.4/src/sclab/tools/labeling/sctype.py +233 -0
- sclab-0.3.4/src/sclab/tools/utils/__init__.py +5 -0
- sclab-0.3.4/src/sclab/tools/utils/_aggregate_and_filter.py +290 -0
- sclab-0.3.4/src/sclab/utils/__init__.py +5 -0
- sclab-0.3.4/src/sclab/utils/_write_excel.py +510 -0
- sclab-0.1.8/src/sclab/_io.py +0 -32
- sclab-0.1.8/src/sclab/_sclab.py +0 -80
- sclab-0.1.8/src/sclab/examples/processor_steps/__init__.py +0 -15
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/__init__.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/_exceptions.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/plotter/__init__.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/plotter/_controls.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/plotter/_plotter.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/plotter/_utils.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/processor/__init__.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/processor/step/__init__.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/dataset/processor/step/_basic_processor_step.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/event/__init__.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/event/_broker.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/event/_client.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/event/_utils.py +0 -0
- {sclab-0.1.8 → sclab-0.3.4}/src/sclab/examples/__init__.py +0 -0
sclab-0.3.4/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Argenis Arriojas
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sclab
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: sclab
|
|
5
5
|
Author-email: Argenis Arriojas <ArriojasMaldonado001@umb.edu>
|
|
6
6
|
Requires-Python: >=3.10,<3.13
|
|
@@ -10,23 +10,34 @@ Classifier: Programming Language :: Python :: 3
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
License-File: LICENSE
|
|
13
14
|
Requires-Dist: anndata
|
|
14
15
|
Requires-Dist: anywidget
|
|
15
16
|
Requires-Dist: ipywidgets
|
|
16
|
-
Requires-Dist: itables
|
|
17
|
+
Requires-Dist: itables<2.4
|
|
18
|
+
Requires-Dist: matplotlib
|
|
17
19
|
Requires-Dist: numpy<2.2
|
|
18
20
|
Requires-Dist: pandas
|
|
19
21
|
Requires-Dist: plotly<6.0
|
|
20
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: requests
|
|
23
|
+
Requires-Dist: ripser>=0.6.12
|
|
21
24
|
Requires-Dist: scikit-learn
|
|
22
|
-
Requires-Dist:
|
|
25
|
+
Requires-Dist: scipy<1.16
|
|
23
26
|
Requires-Dist: svgpathtools
|
|
27
|
+
Requires-Dist: tqdm
|
|
28
|
+
Requires-Dist: jupyterlab>=4.3.6 ; extra == "jupyter"
|
|
29
|
+
Requires-Dist: anndata2ri>=1.3 ; extra == "r"
|
|
30
|
+
Requires-Dist: rpy2>=3.5 ; extra == "r"
|
|
31
|
+
Requires-Dist: scanpy[leiden, skmisc]>=1.10 ; extra == "scanpy"
|
|
24
32
|
Requires-Dist: pytest>=8.3.4 ; extra == "test"
|
|
25
33
|
Project-URL: Bug Tracker, https://github.com/umbibio/sclab/issues
|
|
26
34
|
Project-URL: Changelog, https://github.com/umbibio/sclab/blob/main/CHANGELOG.md
|
|
27
35
|
Project-URL: Documentation, https://github.com/umbibio/sclab/docs
|
|
28
36
|
Project-URL: Homepage, https://github.com/umbibio/sclab
|
|
29
37
|
Project-URL: Repository, https://github.com/umbibio/sclab.git
|
|
38
|
+
Provides-Extra: jupyter
|
|
39
|
+
Provides-Extra: r
|
|
40
|
+
Provides-Extra: scanpy
|
|
30
41
|
Provides-Extra: test
|
|
31
42
|
|
|
32
43
|
# SCLab
|
|
@@ -55,7 +66,6 @@ Open a Jupyter Notebook and run the following:
|
|
|
55
66
|
```python
|
|
56
67
|
from IPython.display import display
|
|
57
68
|
from sclab import SCLabDashboard
|
|
58
|
-
from sclab.examples.processor_steps import QC, Preprocess, PCA, Neighbors, UMAP, Cluster
|
|
59
69
|
import scanpy as sc
|
|
60
70
|
|
|
61
71
|
# Load your data
|
|
@@ -63,8 +73,6 @@ adata = sc.read_10x_h5("your_data.h5")
|
|
|
63
73
|
|
|
64
74
|
# Create dashboard
|
|
65
75
|
dashboard = SCLabDashboard(adata, name="My Analysis")
|
|
66
|
-
# Add desired processing steps to the interface
|
|
67
|
-
dashboard.pr.add_steps({"Processing": [QC, Preprocess, PCA, Neighbors, UMAP, Cluster]})
|
|
68
76
|
|
|
69
77
|
# Display dashboard
|
|
70
78
|
display(dashboard)
|
|
@@ -74,8 +82,10 @@ display(dashboard)
|
|
|
74
82
|
# dashboard.pl # Plotter
|
|
75
83
|
# dashboard.pr # Processor
|
|
76
84
|
|
|
77
|
-
# the
|
|
85
|
+
# the active AnnData object is found within the dataset object:
|
|
78
86
|
# dashboard.ds.adata
|
|
87
|
+
|
|
88
|
+
# by default, the dashboard will update the loaded AnnData object in-place
|
|
79
89
|
```
|
|
80
90
|
|
|
81
91
|
## Components
|
|
@@ -84,6 +94,7 @@ display(dashboard)
|
|
|
84
94
|
|
|
85
95
|
The main interface that integrates all components with a tabbed layout:
|
|
86
96
|
- Main graph for visualizations
|
|
97
|
+
- Results panel
|
|
87
98
|
- Observations table
|
|
88
99
|
- Genes table
|
|
89
100
|
- Event logs
|
|
@@ -24,7 +24,6 @@ Open a Jupyter Notebook and run the following:
|
|
|
24
24
|
```python
|
|
25
25
|
from IPython.display import display
|
|
26
26
|
from sclab import SCLabDashboard
|
|
27
|
-
from sclab.examples.processor_steps import QC, Preprocess, PCA, Neighbors, UMAP, Cluster
|
|
28
27
|
import scanpy as sc
|
|
29
28
|
|
|
30
29
|
# Load your data
|
|
@@ -32,8 +31,6 @@ adata = sc.read_10x_h5("your_data.h5")
|
|
|
32
31
|
|
|
33
32
|
# Create dashboard
|
|
34
33
|
dashboard = SCLabDashboard(adata, name="My Analysis")
|
|
35
|
-
# Add desired processing steps to the interface
|
|
36
|
-
dashboard.pr.add_steps({"Processing": [QC, Preprocess, PCA, Neighbors, UMAP, Cluster]})
|
|
37
34
|
|
|
38
35
|
# Display dashboard
|
|
39
36
|
display(dashboard)
|
|
@@ -43,8 +40,10 @@ display(dashboard)
|
|
|
43
40
|
# dashboard.pl # Plotter
|
|
44
41
|
# dashboard.pr # Processor
|
|
45
42
|
|
|
46
|
-
# the
|
|
43
|
+
# the active AnnData object is found within the dataset object:
|
|
47
44
|
# dashboard.ds.adata
|
|
45
|
+
|
|
46
|
+
# by default, the dashboard will update the loaded AnnData object in-place
|
|
48
47
|
```
|
|
49
48
|
|
|
50
49
|
## Components
|
|
@@ -53,6 +52,7 @@ display(dashboard)
|
|
|
53
52
|
|
|
54
53
|
The main interface that integrates all components with a tabbed layout:
|
|
55
54
|
- Main graph for visualizations
|
|
55
|
+
- Results panel
|
|
56
56
|
- Observations table
|
|
57
57
|
- Genes table
|
|
58
58
|
- Event logs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sclab"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.4"
|
|
4
4
|
description = "sclab"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -18,14 +18,17 @@ dependencies = [
|
|
|
18
18
|
"anndata",
|
|
19
19
|
"anywidget",
|
|
20
20
|
"ipywidgets",
|
|
21
|
-
"itables",
|
|
21
|
+
"itables<2.4",
|
|
22
|
+
"matplotlib",
|
|
22
23
|
"numpy<2.2",
|
|
23
24
|
"pandas",
|
|
24
25
|
"plotly<6.0",
|
|
25
|
-
"
|
|
26
|
+
"requests",
|
|
27
|
+
"ripser>=0.6.12",
|
|
26
28
|
"scikit-learn",
|
|
27
|
-
"
|
|
29
|
+
"scipy<1.16",
|
|
28
30
|
"svgpathtools",
|
|
31
|
+
"tqdm",
|
|
29
32
|
]
|
|
30
33
|
|
|
31
34
|
[project.urls]
|
|
@@ -36,6 +39,9 @@ dependencies = [
|
|
|
36
39
|
"Changelog" = "https://github.com/umbibio/sclab/blob/main/CHANGELOG.md"
|
|
37
40
|
|
|
38
41
|
[project.optional-dependencies]
|
|
42
|
+
jupyter = ["jupyterlab>=4.3.6"]
|
|
43
|
+
r = ["anndata2ri>=1.3", "rpy2>=3.5"]
|
|
44
|
+
scanpy = ["scanpy[leiden,skmisc]>=1.10"]
|
|
39
45
|
test = ["pytest>=8.3.4"]
|
|
40
46
|
|
|
41
47
|
[build-system]
|
|
@@ -46,6 +52,9 @@ build-backend = "flit_core.buildapi"
|
|
|
46
52
|
name = "sclab"
|
|
47
53
|
source = "src/sclab"
|
|
48
54
|
|
|
55
|
+
[tool.uv.sources]
|
|
56
|
+
sclab-tools = { path = "../sclab-tools" }
|
|
57
|
+
|
|
49
58
|
[dependency-groups]
|
|
50
59
|
dev = [
|
|
51
60
|
"bump-my-version>=0.31.1",
|
|
@@ -56,4 +65,8 @@ dev = [
|
|
|
56
65
|
"mkdocs>=1.6.1",
|
|
57
66
|
"mkdocs-material>=9.6.3",
|
|
58
67
|
"mkdocs-jupyter>=0.25.1",
|
|
68
|
+
"scanpy[harmony,leiden,scrublet,skmisc]>=1.10.4",
|
|
69
|
+
"jupyterlab>=4.3.6",
|
|
70
|
+
"sclab-tools",
|
|
71
|
+
"scrublet>=0.2.3",
|
|
59
72
|
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from anndata import AnnData, read_h5ad
|
|
7
|
+
from tqdm.auto import tqdm
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def read_adata(path: str | Path, var_names: str = "gene_ids") -> AnnData:
|
|
11
|
+
from .scanpy.readwrite import read_10x_h5, read_10x_mtx
|
|
12
|
+
|
|
13
|
+
path = Path(path)
|
|
14
|
+
|
|
15
|
+
match path.suffix:
|
|
16
|
+
case ".h5":
|
|
17
|
+
adata = read_10x_h5(path)
|
|
18
|
+
case ".h5ad":
|
|
19
|
+
adata = read_h5ad(path)
|
|
20
|
+
case "":
|
|
21
|
+
assert path.is_dir()
|
|
22
|
+
adata = read_10x_mtx(path)
|
|
23
|
+
case _:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
"Input file must be a 10x h5, h5ad or a folder of 10x mtx files"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if var_names in adata.var:
|
|
29
|
+
adata.var = adata.var.set_index(var_names)
|
|
30
|
+
|
|
31
|
+
return adata
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_adata_from_url(
|
|
35
|
+
url: str,
|
|
36
|
+
var_names: str = "gene_ids",
|
|
37
|
+
progress: bool = True,
|
|
38
|
+
) -> AnnData:
|
|
39
|
+
"""
|
|
40
|
+
Load an AnnData object from a URL to an .h5ad file.
|
|
41
|
+
|
|
42
|
+
Parameters:
|
|
43
|
+
-----------
|
|
44
|
+
url : str
|
|
45
|
+
URL to the .h5ad file
|
|
46
|
+
var_names : str
|
|
47
|
+
Name of the variable column in the .h5ad file
|
|
48
|
+
progress : bool
|
|
49
|
+
Whether to show a progress bar
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
--------
|
|
53
|
+
anndata.AnnData
|
|
54
|
+
Loaded AnnData object
|
|
55
|
+
"""
|
|
56
|
+
from .scanpy.readwrite import read_10x_h5
|
|
57
|
+
|
|
58
|
+
assert is_valid_url(url), "URL is not valid"
|
|
59
|
+
url_path = Path(urlparse(url).path)
|
|
60
|
+
|
|
61
|
+
file_content = fetch_file(url, progress=progress)
|
|
62
|
+
match url_path.suffix:
|
|
63
|
+
case ".h5":
|
|
64
|
+
adata = read_10x_h5(file_content)
|
|
65
|
+
case ".h5ad":
|
|
66
|
+
adata = read_h5ad(file_content)
|
|
67
|
+
case _:
|
|
68
|
+
raise ValueError("Input file must be a 10x h5 or h5ad file")
|
|
69
|
+
|
|
70
|
+
if var_names in adata.var:
|
|
71
|
+
adata.var = adata.var.set_index(var_names)
|
|
72
|
+
|
|
73
|
+
return adata
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fetch_file(url: str, progress: bool = True) -> BytesIO:
|
|
77
|
+
response = requests.get(url, stream=True)
|
|
78
|
+
response.raise_for_status()
|
|
79
|
+
|
|
80
|
+
total_size_in_bytes = int(response.headers.get("content-length", 0))
|
|
81
|
+
block_size = 1024 # 1 Kibibyte
|
|
82
|
+
|
|
83
|
+
if progress:
|
|
84
|
+
progress_bar = tqdm(total=total_size_in_bytes, unit="iB", unit_scale=True)
|
|
85
|
+
|
|
86
|
+
result = BytesIO()
|
|
87
|
+
for data in response.iter_content(block_size):
|
|
88
|
+
result.write(data)
|
|
89
|
+
if progress:
|
|
90
|
+
progress_bar.update(len(data))
|
|
91
|
+
|
|
92
|
+
if progress:
|
|
93
|
+
progress_bar.close()
|
|
94
|
+
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def is_valid_url(url: str) -> bool:
|
|
99
|
+
if not isinstance(url, str):
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
result = urlparse(url)
|
|
103
|
+
return all([result.scheme, result.netloc])
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Callable, Type
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# full class definition is in .dataset/processor/step/_processor_step_base.py
|
|
5
|
+
class ProcessorStepBase:
|
|
6
|
+
name: str
|
|
7
|
+
description: str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
methods_registry: dict[str, list[ProcessorStepBase]] = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register_sclab_method(
|
|
14
|
+
category: str,
|
|
15
|
+
name: str | None = None,
|
|
16
|
+
description: str | None = None,
|
|
17
|
+
order: int | None = None,
|
|
18
|
+
) -> Callable:
|
|
19
|
+
"""
|
|
20
|
+
Decorator to register a class as a sclab method.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
category: The category to register the method under (e.g., "Processing")
|
|
24
|
+
name: Optional display name for the method. If None, uses the class name.
|
|
25
|
+
description: Optional description of the method.
|
|
26
|
+
order: Optional ordering within the category. Lower numbers appear first.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Decorated class
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def decorator(cls: Type[ProcessorStepBase]) -> Type[ProcessorStepBase]:
|
|
33
|
+
if name:
|
|
34
|
+
cls.name = name
|
|
35
|
+
|
|
36
|
+
if description:
|
|
37
|
+
cls.description = description
|
|
38
|
+
|
|
39
|
+
if order is not None:
|
|
40
|
+
cls.order = order
|
|
41
|
+
|
|
42
|
+
# Initialize the category in the registry if it doesn't exist
|
|
43
|
+
if category not in methods_registry:
|
|
44
|
+
methods_registry[category] = []
|
|
45
|
+
|
|
46
|
+
methods_list = methods_registry[category]
|
|
47
|
+
|
|
48
|
+
# Add the class to the registry
|
|
49
|
+
methods_list.append(cls)
|
|
50
|
+
|
|
51
|
+
# Sort the methods by order
|
|
52
|
+
methods_registry[category] = sorted(methods_list, key=lambda x: x.order)
|
|
53
|
+
|
|
54
|
+
return cls
|
|
55
|
+
|
|
56
|
+
return decorator
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_sclab_methods():
|
|
60
|
+
methods = {}
|
|
61
|
+
|
|
62
|
+
for category, methods_list in methods_registry.items():
|
|
63
|
+
methods[category] = sorted(methods_list, key=lambda x: x.order)
|
|
64
|
+
|
|
65
|
+
return methods
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from anndata import AnnData
|
|
6
|
+
from IPython.display import display
|
|
7
|
+
from ipywidgets.widgets import (
|
|
8
|
+
Button,
|
|
9
|
+
FileUpload,
|
|
10
|
+
GridBox,
|
|
11
|
+
HBox,
|
|
12
|
+
Label,
|
|
13
|
+
Layout,
|
|
14
|
+
Output,
|
|
15
|
+
Tab,
|
|
16
|
+
Text,
|
|
17
|
+
ToggleButtons,
|
|
18
|
+
VBox,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from ._io import is_valid_url, load_adata_from_url, read_adata
|
|
22
|
+
from .dataset import SCLabDataset
|
|
23
|
+
from .dataset.plotter import Plotter
|
|
24
|
+
from .dataset.processor import Processor
|
|
25
|
+
from .event import EventBroker
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SCLabDashboard(GridBox):
|
|
29
|
+
broker: EventBroker
|
|
30
|
+
dataset: SCLabDataset
|
|
31
|
+
plotter: Plotter
|
|
32
|
+
processor: Processor
|
|
33
|
+
main_content: Tab
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
adata_or_filepath_or_url: AnnData | str | None = None,
|
|
38
|
+
name: str = "SCLab Dashboard",
|
|
39
|
+
counts_layer: str = "counts",
|
|
40
|
+
batch_key: str | None = None,
|
|
41
|
+
copy: bool = False,
|
|
42
|
+
):
|
|
43
|
+
if adata_or_filepath_or_url is None:
|
|
44
|
+
adata = None
|
|
45
|
+
|
|
46
|
+
elif isinstance(adata_or_filepath_or_url, AnnData):
|
|
47
|
+
adata = adata_or_filepath_or_url
|
|
48
|
+
|
|
49
|
+
elif is_valid_url(adata_or_filepath_or_url):
|
|
50
|
+
url = adata_or_filepath_or_url
|
|
51
|
+
adata = load_adata_from_url(url)
|
|
52
|
+
|
|
53
|
+
elif isinstance(adata_or_filepath_or_url, str):
|
|
54
|
+
filepath = adata_or_filepath_or_url
|
|
55
|
+
adata = read_adata(filepath)
|
|
56
|
+
|
|
57
|
+
self.name = name
|
|
58
|
+
self.counts_layer = counts_layer
|
|
59
|
+
self.batch_key = batch_key
|
|
60
|
+
|
|
61
|
+
self.broker = EventBroker()
|
|
62
|
+
|
|
63
|
+
self.dataset = None
|
|
64
|
+
self.plotter = None
|
|
65
|
+
self.processor = None
|
|
66
|
+
self.main_content = None
|
|
67
|
+
|
|
68
|
+
self.data_loader_layout = Layout(
|
|
69
|
+
width="100%",
|
|
70
|
+
height="500px",
|
|
71
|
+
grid_template_columns="auto",
|
|
72
|
+
grid_template_areas=""" "data_loader" """,
|
|
73
|
+
border="0px solid black",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self.dashboard_layout = Layout(
|
|
77
|
+
width="100%",
|
|
78
|
+
height="100%",
|
|
79
|
+
grid_template_columns="350px auto",
|
|
80
|
+
grid_template_areas=""" "processor plotter" """,
|
|
81
|
+
border="0px solid black",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.data_loader = DataLoader(self)
|
|
85
|
+
|
|
86
|
+
GridBox.__init__(self)
|
|
87
|
+
if adata is not None:
|
|
88
|
+
self._load(adata, copy=copy)
|
|
89
|
+
else:
|
|
90
|
+
self.children = (self.data_loader,)
|
|
91
|
+
self.layout = self.data_loader_layout
|
|
92
|
+
|
|
93
|
+
def _load(self, adata: AnnData, copy: bool = False):
|
|
94
|
+
self.dataset = SCLabDataset(
|
|
95
|
+
adata,
|
|
96
|
+
name=self.name,
|
|
97
|
+
counts_layer=self.counts_layer,
|
|
98
|
+
copy=copy,
|
|
99
|
+
broker=self.broker,
|
|
100
|
+
)
|
|
101
|
+
self.plotter = Plotter(self.dataset)
|
|
102
|
+
self.processor = Processor(
|
|
103
|
+
self.dataset,
|
|
104
|
+
self.plotter,
|
|
105
|
+
batch_key=self.batch_key,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self.main_content = Tab(
|
|
109
|
+
children=[
|
|
110
|
+
self.plotter,
|
|
111
|
+
self.processor.results_panel,
|
|
112
|
+
self.dataset.obs_table,
|
|
113
|
+
self.dataset.var_table,
|
|
114
|
+
self.broker.logs_tab,
|
|
115
|
+
],
|
|
116
|
+
titles=[
|
|
117
|
+
"Main graph",
|
|
118
|
+
"Results",
|
|
119
|
+
"Observations",
|
|
120
|
+
"Genes",
|
|
121
|
+
"Logs",
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self.children = (
|
|
126
|
+
self.processor.main_accordion,
|
|
127
|
+
self.main_content,
|
|
128
|
+
)
|
|
129
|
+
self.layout = self.dashboard_layout
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def ds(self):
|
|
133
|
+
return self.dataset
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def pr(self):
|
|
137
|
+
return self.processor
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def pl(self):
|
|
141
|
+
return self.plotter
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class DataLoader(VBox):
|
|
145
|
+
dashboard: SCLabDashboard
|
|
146
|
+
adata: AnnData
|
|
147
|
+
|
|
148
|
+
upload: FileUpload
|
|
149
|
+
upload_info: Output
|
|
150
|
+
upload_row: HBox
|
|
151
|
+
upload_row_label: Label
|
|
152
|
+
|
|
153
|
+
url: Text
|
|
154
|
+
load_button: Button
|
|
155
|
+
url_row: HBox
|
|
156
|
+
url_row_label: Label
|
|
157
|
+
|
|
158
|
+
defined_adatas_dict: dict[str, AnnData]
|
|
159
|
+
defined_adatas: ToggleButtons
|
|
160
|
+
defined_adatas_row: HBox
|
|
161
|
+
defined_adatas_label: Label
|
|
162
|
+
|
|
163
|
+
progress_output: Output
|
|
164
|
+
continue_button: Button
|
|
165
|
+
|
|
166
|
+
def __init__(self, dashboard: SCLabDashboard):
|
|
167
|
+
self.dashboard = dashboard
|
|
168
|
+
|
|
169
|
+
self.upload_row_label = Label("Load from file:", layout=Layout(width="120px"))
|
|
170
|
+
self.upload = FileUpload(layout=Layout(width="200px"))
|
|
171
|
+
self.upload_info = Output(layout=Layout(width="95%"))
|
|
172
|
+
self.upload_row = HBox(
|
|
173
|
+
[
|
|
174
|
+
self.upload_row_label,
|
|
175
|
+
self.upload,
|
|
176
|
+
self.upload_info,
|
|
177
|
+
],
|
|
178
|
+
layout=Layout(width="100%"),
|
|
179
|
+
)
|
|
180
|
+
self.upload.observe(self.on_upload, "value")
|
|
181
|
+
|
|
182
|
+
self.url_row_label = Label("Load from URL:", layout=Layout(width="120px"))
|
|
183
|
+
self.url = Text(placeholder="https://...", layout=Layout(width="100%"))
|
|
184
|
+
self.load_button = Button(description="Load", layout=Layout(width="200px"))
|
|
185
|
+
self.url_row = HBox(
|
|
186
|
+
[self.url_row_label, self.url, self.load_button],
|
|
187
|
+
layout=Layout(width="100%"),
|
|
188
|
+
)
|
|
189
|
+
self.load_button.on_click(self.on_load_url)
|
|
190
|
+
|
|
191
|
+
user_f_locals = inspect.stack()[2].frame.f_locals
|
|
192
|
+
self.defined_adatas_dict = {}
|
|
193
|
+
for name, variable_type in [(k, type(v)) for k, v in user_f_locals.items()]:
|
|
194
|
+
if variable_type is AnnData and not name.startswith("_"):
|
|
195
|
+
self.defined_adatas_dict[name] = user_f_locals[name]
|
|
196
|
+
|
|
197
|
+
self.defined_adatas_label = Label(
|
|
198
|
+
"Defined datasets:", layout=Layout(width="120px")
|
|
199
|
+
)
|
|
200
|
+
self.defined_adatas = ToggleButtons(
|
|
201
|
+
options=list(self.defined_adatas_dict.keys()),
|
|
202
|
+
value=None,
|
|
203
|
+
layout=Layout(width="100%"),
|
|
204
|
+
)
|
|
205
|
+
self.defined_adatas_row = HBox(
|
|
206
|
+
[self.defined_adatas_label, self.defined_adatas],
|
|
207
|
+
layout=Layout(width="100%"),
|
|
208
|
+
)
|
|
209
|
+
self.defined_adatas.observe(self.on_defined_adatas_toggle, "value")
|
|
210
|
+
|
|
211
|
+
self.progress_output = Output(layout=Layout(width="95%"))
|
|
212
|
+
self.continue_button = Button(
|
|
213
|
+
description="Continue", layout=Layout(width="100%"), button_style="success"
|
|
214
|
+
)
|
|
215
|
+
self.continue_button.on_click(self.on_continue)
|
|
216
|
+
|
|
217
|
+
VBox.__init__(
|
|
218
|
+
self,
|
|
219
|
+
[
|
|
220
|
+
# self.url_row,
|
|
221
|
+
# self.upload_row,
|
|
222
|
+
self.defined_adatas_row,
|
|
223
|
+
self.progress_output,
|
|
224
|
+
],
|
|
225
|
+
layout=Layout(width="100%"),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def on_defined_adatas_toggle(self, *args, **kwargs):
|
|
229
|
+
adata = self.defined_adatas_dict[self.defined_adatas.value]
|
|
230
|
+
|
|
231
|
+
self.progress_output.clear_output()
|
|
232
|
+
with self.progress_output:
|
|
233
|
+
print(f"Loaded {adata.shape[0]} observations and {adata.shape[1]} genes\n")
|
|
234
|
+
print(adata)
|
|
235
|
+
display(self.continue_button)
|
|
236
|
+
|
|
237
|
+
self.adata = adata
|
|
238
|
+
|
|
239
|
+
def on_upload(self, *args, **kwargs):
|
|
240
|
+
import tempfile
|
|
241
|
+
|
|
242
|
+
from .scanpy.readwrite import read_10x_h5, read_h5ad
|
|
243
|
+
|
|
244
|
+
files = self.upload.value
|
|
245
|
+
if len(files) == 0:
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
file = files[0]
|
|
249
|
+
|
|
250
|
+
self.upload_info.clear_output()
|
|
251
|
+
with self.upload_info:
|
|
252
|
+
for k, v in file.items():
|
|
253
|
+
if k == "content":
|
|
254
|
+
continue
|
|
255
|
+
print(f"{k}: {v}")
|
|
256
|
+
|
|
257
|
+
filename = file["name"]
|
|
258
|
+
contents = BytesIO(file["content"].tobytes())
|
|
259
|
+
var_names = "gene_ids"
|
|
260
|
+
|
|
261
|
+
path = Path(filename)
|
|
262
|
+
|
|
263
|
+
match path.suffix:
|
|
264
|
+
case ".h5":
|
|
265
|
+
with tempfile.NamedTemporaryFile(suffix=".h5") as tmp:
|
|
266
|
+
tmp.write(contents.getbuffer())
|
|
267
|
+
tmp.flush()
|
|
268
|
+
adata = read_10x_h5(tmp.name)
|
|
269
|
+
case ".h5ad":
|
|
270
|
+
with tempfile.NamedTemporaryFile(suffix=".h5ad") as tmp:
|
|
271
|
+
tmp.write(contents.getbuffer())
|
|
272
|
+
tmp.flush()
|
|
273
|
+
adata = read_h5ad(tmp.name)
|
|
274
|
+
case _:
|
|
275
|
+
self.upload_info.clear_output()
|
|
276
|
+
with self.upload_info:
|
|
277
|
+
print(f"`{filename}` is not valid")
|
|
278
|
+
print("Please upload a 10x .h5 or .h5ad file")
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
if var_names in adata.var:
|
|
282
|
+
adata.var = adata.var.set_index(var_names)
|
|
283
|
+
|
|
284
|
+
with self.progress_output:
|
|
285
|
+
print(f"Loaded {adata.shape[0]} observations and {adata.shape[1]} genes\n")
|
|
286
|
+
print(adata)
|
|
287
|
+
display(self.continue_button)
|
|
288
|
+
|
|
289
|
+
self.adata = adata
|
|
290
|
+
|
|
291
|
+
def on_load_url(self, *args, **kwargs):
|
|
292
|
+
self.progress_output.clear_output()
|
|
293
|
+
with self.progress_output:
|
|
294
|
+
self.adata = load_adata_from_url(self.url.value)
|
|
295
|
+
display(self.continue_button)
|
|
296
|
+
|
|
297
|
+
def on_continue(self, *args, **kwargs):
|
|
298
|
+
self.dashboard._load(self.adata)
|
|
299
|
+
self.adata = None
|
|
300
|
+
self.defined_adatas_dict = {}
|