sampledisco 0.2.0__tar.gz → 0.3.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.
- {sampledisco-0.2.0/src/sampledisco.egg-info → sampledisco-0.3.0}/PKG-INFO +11 -11
- {sampledisco-0.2.0 → sampledisco-0.3.0}/README.md +10 -10
- {sampledisco-0.2.0 → sampledisco-0.3.0}/pyproject.toml +1 -1
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/config/config_demo.yaml +6 -6
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/parameter_selection/autotune.py +114 -50
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/ATAC_cell_type.py +6 -3
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/ATAC_cell_type_gpu.py +7 -4
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/atac_preprocess_cpu.py +10 -7
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/atac_preprocess_gpu.py +10 -7
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/cell_type_cpu.py +6 -5
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/cell_type_gpu.py +6 -5
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/multi_omics_batch_correction.py +19 -12
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/multi_omics_cell_type_cpu.py +7 -6
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/multi_omics_cell_type_gpu.py +6 -4
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/multi_omics_glue.py +25 -14
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/multi_omics_merge.py +5 -4
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/rna_preprocess_cpu.py +10 -7
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/rna_preprocess_gpu.py +10 -7
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_association/association.py +2 -2
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_distance/sample_distance.py +18 -18
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_embedding/__init__.py +3 -1
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_embedding/blocks.py +5 -5
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_embedding/sample_embedding.py +38 -28
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_embedding/sample_embedding_gpu.py +27 -18
- sampledisco-0.3.0/src/sampledisco/utils/embedding_keys.py +189 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/multi_omics_visualization.py +2 -2
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/visualization_other.py +4 -4
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/wrapper/atac_wrapper.py +6 -5
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/wrapper/multiomics_wrapper.py +42 -60
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/wrapper/rna_wrapper.py +6 -5
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/wrapper/wrapper.py +7 -7
- {sampledisco-0.2.0 → sampledisco-0.3.0/src/sampledisco.egg-info}/PKG-INFO +11 -11
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco.egg-info/SOURCES.txt +1 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/LICENSE +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/requirements.txt +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/setup.cfg +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/cli.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/ATAC_ArchR.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/ATAC_RNA_harmony.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/ATAC_gene_activity.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/ATAC_peak_annotation.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/RNA_name_convertor.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/pseudo_correlation.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/test_pseudo_correlation.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/validation.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/parameter_selection/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_association/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/RAISIN.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/RAISIN_TEST.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/cluster.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/cluster_severity_reconcile.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/proportion_test.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_distance/ChiSquare.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_distance/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_distance/distance_test.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_distance/jensenshannon.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/CCA.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/CCA_test.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/TSCAN.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/trajectory_DGE_visualization.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/trajectory_diff_gene.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/Grouping.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/batch_regress.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/harmony_compat.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/imbalance_cell_type_handler.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/inspector.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/limma.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/merge_sample_meta.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/random_seed.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/safe_save.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/slim_adata.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/subsample.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/subsample_generator.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/tf_idf.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/unify_optimal.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/ATAC_visualization.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/DEG_visualization.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/visualization_embedding.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/visualization_helper.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/wrapper/__init__.py +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco.egg-info/dependency_links.txt +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco.egg-info/entry_points.txt +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco.egg-info/requires.txt +0 -0
- {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sampledisco
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Cross-omics, cross-condition sample embedding for single-cell data (RNA + ATAC)
|
|
5
5
|
Author: Hongkai Ji
|
|
6
6
|
Author-email: Harry Jiang <hjiang55@jh.edu>
|
|
@@ -61,15 +61,15 @@ For each modality (RNA, ATAC, or integrated multi-omics) the pipeline produces t
|
|
|
61
61
|
|
|
62
62
|
| Key | Role | Source |
|
|
63
63
|
|---|---|---|
|
|
64
|
-
| **`
|
|
65
|
-
| **`
|
|
64
|
+
| **`Z_comp`** | sample-removed embedding — used for cell typing and the composition blocks | Harmony (single-omics) / Harmony post-pass on scGLUE (multi-omics) |
|
|
65
|
+
| **`Z_rmd`** | sample-preserved embedding — used for the reference-relative mean displacement (RMD) block | second Harmony pass (single-omics) / scGLUE primary output (multi-omics) |
|
|
66
66
|
|
|
67
67
|
It then assembles **four blocks** per sample (or per sample × modality for multi-omics):
|
|
68
68
|
|
|
69
69
|
1. **A1** — one-hot cell-type composition
|
|
70
70
|
2. **A2** — soft k-means composition at K_med (≈120)
|
|
71
71
|
3. **A3** — soft k-means composition at K_fine (≈300)
|
|
72
|
-
4. **
|
|
72
|
+
4. **RMD** — leave-one-out cell-type-resolved displacement on `Z_rmd`
|
|
73
73
|
|
|
74
74
|
The four blocks are inverse-variance weighted, Frobenius-stacked, PCA-reduced to 10 dimensions, and Harmony-corrected at sample level. The result is stored as `adata.uns['X_DR_sample']` and feeds every downstream module.
|
|
75
75
|
|
|
@@ -87,20 +87,20 @@ code/
|
|
|
87
87
|
│ ├── atac_wrapper.py
|
|
88
88
|
│ └── multiomics_wrapper.py
|
|
89
89
|
├── preparation/ # Preprocessing
|
|
90
|
-
│ ├── rna_preprocess_{cpu,gpu}.py # QC → HVG → PCA → dual Harmony →
|
|
91
|
-
│ ├── atac_preprocess_{cpu,gpu}.py # QC → TF-IDF → HVF → LSI → dual Harmony →
|
|
92
|
-
│ ├── cell_type_{cpu,gpu}.py # Leiden clustering on
|
|
90
|
+
│ ├── rna_preprocess_{cpu,gpu}.py # QC → HVG → PCA → dual Harmony → Z_comp + Z_rmd
|
|
91
|
+
│ ├── atac_preprocess_{cpu,gpu}.py # QC → TF-IDF → HVF → LSI → dual Harmony → Z_comp + Z_rmd
|
|
92
|
+
│ ├── cell_type_{cpu,gpu}.py # Leiden clustering on Z_comp (RNA or ATAC)
|
|
93
93
|
│ ├── ATAC_cell_type{,_gpu}.py # ATAC-specific cell typing variants
|
|
94
94
|
│ ├── multi_omics_glue.py # scGLUE integration (cross-modality VAE + guidance graph)
|
|
95
|
-
│ ├── multi_omics_batch_correction.py # Harmony post-pass on X_glue →
|
|
95
|
+
│ ├── multi_omics_batch_correction.py # Harmony post-pass on X_glue → Z_comp
|
|
96
96
|
│ ├── multi_omics_merge.py # post-GLUE merge + per-modality preprocess/slimming
|
|
97
97
|
│ └── multi_omics_cell_type_{cpu,gpu}.py # RNA-Leiden + k-NN label transfer to ATAC
|
|
98
98
|
├── sample_embedding/ # Core method
|
|
99
|
-
│ ├── blocks.py # composition,
|
|
99
|
+
│ ├── blocks.py # composition, RMD, weighting, Frobenius stack, final PCA + Harmony
|
|
100
100
|
│ ├── sample_embedding.py # CPU pipeline
|
|
101
101
|
│ └── sample_embedding_gpu.py # GPU pipeline (cuML + cupy)
|
|
102
102
|
├── parameter_selection/
|
|
103
|
-
│ └── autotune.py # Bayesian GP sweep over
|
|
103
|
+
│ └── autotune.py # Bayesian GP sweep over RMD α; adaptive proxy ensemble
|
|
104
104
|
├── sample_distance/ # Pairwise sample distances (DR / EMD / chi-square / JS)
|
|
105
105
|
├── sample_clustering/ # Hierarchical (HRA / HRC / NN / UPGMA / consensus), K-means, proportion test, RAISIN
|
|
106
106
|
├── sample_trajectory/ # CCA (supervised) and TSCAN (unsupervised) + GAM-based trajectory DGE
|
|
@@ -165,7 +165,7 @@ For multi-omics, the pipeline takes two separate h5ads (RNA + ATAC) and integrat
|
|
|
165
165
|
├── atac/ (parallel structure)
|
|
166
166
|
├── multiomics/
|
|
167
167
|
│ ├── integration/glue/{rna-pp,atac-pp,guidance.graphml.gz}
|
|
168
|
-
│ ├── preprocess/adata_sample.h5ad # post-GLUE merged adata with
|
|
168
|
+
│ ├── preprocess/adata_sample.h5ad # post-GLUE merged adata with Z_comp + Z_rmd
|
|
169
169
|
│ ├── sample_embedding/sample_embedding.csv
|
|
170
170
|
│ └── (same downstream subdirs as single-omics)
|
|
171
171
|
└── sys_log/main_process_status.json # which stages completed
|
|
@@ -14,15 +14,15 @@ For each modality (RNA, ATAC, or integrated multi-omics) the pipeline produces t
|
|
|
14
14
|
|
|
15
15
|
| Key | Role | Source |
|
|
16
16
|
|---|---|---|
|
|
17
|
-
| **`
|
|
18
|
-
| **`
|
|
17
|
+
| **`Z_comp`** | sample-removed embedding — used for cell typing and the composition blocks | Harmony (single-omics) / Harmony post-pass on scGLUE (multi-omics) |
|
|
18
|
+
| **`Z_rmd`** | sample-preserved embedding — used for the reference-relative mean displacement (RMD) block | second Harmony pass (single-omics) / scGLUE primary output (multi-omics) |
|
|
19
19
|
|
|
20
20
|
It then assembles **four blocks** per sample (or per sample × modality for multi-omics):
|
|
21
21
|
|
|
22
22
|
1. **A1** — one-hot cell-type composition
|
|
23
23
|
2. **A2** — soft k-means composition at K_med (≈120)
|
|
24
24
|
3. **A3** — soft k-means composition at K_fine (≈300)
|
|
25
|
-
4. **
|
|
25
|
+
4. **RMD** — leave-one-out cell-type-resolved displacement on `Z_rmd`
|
|
26
26
|
|
|
27
27
|
The four blocks are inverse-variance weighted, Frobenius-stacked, PCA-reduced to 10 dimensions, and Harmony-corrected at sample level. The result is stored as `adata.uns['X_DR_sample']` and feeds every downstream module.
|
|
28
28
|
|
|
@@ -40,20 +40,20 @@ code/
|
|
|
40
40
|
│ ├── atac_wrapper.py
|
|
41
41
|
│ └── multiomics_wrapper.py
|
|
42
42
|
├── preparation/ # Preprocessing
|
|
43
|
-
│ ├── rna_preprocess_{cpu,gpu}.py # QC → HVG → PCA → dual Harmony →
|
|
44
|
-
│ ├── atac_preprocess_{cpu,gpu}.py # QC → TF-IDF → HVF → LSI → dual Harmony →
|
|
45
|
-
│ ├── cell_type_{cpu,gpu}.py # Leiden clustering on
|
|
43
|
+
│ ├── rna_preprocess_{cpu,gpu}.py # QC → HVG → PCA → dual Harmony → Z_comp + Z_rmd
|
|
44
|
+
│ ├── atac_preprocess_{cpu,gpu}.py # QC → TF-IDF → HVF → LSI → dual Harmony → Z_comp + Z_rmd
|
|
45
|
+
│ ├── cell_type_{cpu,gpu}.py # Leiden clustering on Z_comp (RNA or ATAC)
|
|
46
46
|
│ ├── ATAC_cell_type{,_gpu}.py # ATAC-specific cell typing variants
|
|
47
47
|
│ ├── multi_omics_glue.py # scGLUE integration (cross-modality VAE + guidance graph)
|
|
48
|
-
│ ├── multi_omics_batch_correction.py # Harmony post-pass on X_glue →
|
|
48
|
+
│ ├── multi_omics_batch_correction.py # Harmony post-pass on X_glue → Z_comp
|
|
49
49
|
│ ├── multi_omics_merge.py # post-GLUE merge + per-modality preprocess/slimming
|
|
50
50
|
│ └── multi_omics_cell_type_{cpu,gpu}.py # RNA-Leiden + k-NN label transfer to ATAC
|
|
51
51
|
├── sample_embedding/ # Core method
|
|
52
|
-
│ ├── blocks.py # composition,
|
|
52
|
+
│ ├── blocks.py # composition, RMD, weighting, Frobenius stack, final PCA + Harmony
|
|
53
53
|
│ ├── sample_embedding.py # CPU pipeline
|
|
54
54
|
│ └── sample_embedding_gpu.py # GPU pipeline (cuML + cupy)
|
|
55
55
|
├── parameter_selection/
|
|
56
|
-
│ └── autotune.py # Bayesian GP sweep over
|
|
56
|
+
│ └── autotune.py # Bayesian GP sweep over RMD α; adaptive proxy ensemble
|
|
57
57
|
├── sample_distance/ # Pairwise sample distances (DR / EMD / chi-square / JS)
|
|
58
58
|
├── sample_clustering/ # Hierarchical (HRA / HRC / NN / UPGMA / consensus), K-means, proportion test, RAISIN
|
|
59
59
|
├── sample_trajectory/ # CCA (supervised) and TSCAN (unsupervised) + GAM-based trajectory DGE
|
|
@@ -118,7 +118,7 @@ For multi-omics, the pipeline takes two separate h5ads (RNA + ATAC) and integrat
|
|
|
118
118
|
├── atac/ (parallel structure)
|
|
119
119
|
├── multiomics/
|
|
120
120
|
│ ├── integration/glue/{rna-pp,atac-pp,guidance.graphml.gz}
|
|
121
|
-
│ ├── preprocess/adata_sample.h5ad # post-GLUE merged adata with
|
|
121
|
+
│ ├── preprocess/adata_sample.h5ad # post-GLUE merged adata with Z_comp + Z_rmd
|
|
122
122
|
│ ├── sample_embedding/sample_embedding.csv
|
|
123
123
|
│ └── (same downstream subdirs as single-omics)
|
|
124
124
|
└── sys_log/main_process_status.json # which stages completed
|
|
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
|
|
10
10
|
[project]
|
|
11
11
|
name = "sampledisco"
|
|
12
|
-
version = "0.
|
|
12
|
+
version = "0.3.0"
|
|
13
13
|
description = "Cross-omics, cross-condition sample embedding for single-cell data (RNA + ATAC)"
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.10"
|
|
@@ -95,7 +95,7 @@ rna_autotune_enable: false
|
|
|
95
95
|
rna_autotune_search: "bayesian"
|
|
96
96
|
rna_autotune_scoring: "auto"
|
|
97
97
|
rna_autotune_scope: "alpha_only"
|
|
98
|
-
rna_autotune_alpha_bounds: [0.1,
|
|
98
|
+
rna_autotune_alpha_bounds: [0.1, 100.0]
|
|
99
99
|
rna_autotune_grouping_col:
|
|
100
100
|
|
|
101
101
|
# --- Trajectory Analysis Parameters ---
|
|
@@ -216,7 +216,7 @@ atac_autotune_enable: false
|
|
|
216
216
|
atac_autotune_search: "bayesian"
|
|
217
217
|
atac_autotune_scoring: "auto"
|
|
218
218
|
atac_autotune_scope: "alpha_only"
|
|
219
|
-
atac_autotune_alpha_bounds: [0.1,
|
|
219
|
+
atac_autotune_alpha_bounds: [0.1, 100.0]
|
|
220
220
|
atac_autotune_grouping_col:
|
|
221
221
|
|
|
222
222
|
# --- Trajectory Analysis Parameters ---
|
|
@@ -332,9 +332,9 @@ multiomics_treat_sample_as_batch: false # GLUE preserves per-sample variance (V
|
|
|
332
332
|
|
|
333
333
|
# --- Sample-removed cluster embedding (X_glue from scGLUE is the RMD/sample-preserved emb) ---
|
|
334
334
|
# By default, one Harmony pass on X_glue removes per-sample variance to
|
|
335
|
-
# produce
|
|
335
|
+
# produce Z_comp (used by cell typing + A1/A2/A3). Alternative:
|
|
336
336
|
# train scGLUE a second time with treat_sample_as_batch=True; that run's
|
|
337
|
-
# embedding is merged in as
|
|
337
|
+
# embedding is merged in as Z_comp and the Harmony pass auto-skips.
|
|
338
338
|
multiomics_harmonize_xglue_max_iter: 30
|
|
339
339
|
multiomics_run_glue_twice_for_sample_removal: false
|
|
340
340
|
multiomics_save_prefix: "glue"
|
|
@@ -355,7 +355,7 @@ multiomics_metric: "cosine"
|
|
|
355
355
|
multiomics_existing_cell_types: false
|
|
356
356
|
multiomics_n_target_clusters: 8
|
|
357
357
|
multiomics_cluster_resolution: 0.8
|
|
358
|
-
# null → auto-resolve to
|
|
358
|
+
# null → auto-resolve to Z_comp (sample-removed); legacy key names accepted
|
|
359
359
|
multiomics_use_rep_celltype:
|
|
360
360
|
multiomics_markers:
|
|
361
361
|
multiomics_generate_umap_celltype: true
|
|
@@ -384,7 +384,7 @@ multiomics_autotune_enable: false
|
|
|
384
384
|
multiomics_autotune_search: "bayesian"
|
|
385
385
|
multiomics_autotune_scoring: "auto"
|
|
386
386
|
multiomics_autotune_scope: "alpha_only"
|
|
387
|
-
multiomics_autotune_alpha_bounds: [0.1,
|
|
387
|
+
multiomics_autotune_alpha_bounds: [0.1, 100.0]
|
|
388
388
|
multiomics_autotune_grouping_col:
|
|
389
389
|
|
|
390
390
|
# --- Trajectory Analysis Parameters ---
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import math
|
|
21
21
|
import os
|
|
22
22
|
import time
|
|
23
|
+
import warnings
|
|
23
24
|
from typing import Callable, Dict, List, Optional, Tuple, Union
|
|
24
25
|
|
|
25
26
|
import numpy as np
|
|
@@ -45,9 +46,17 @@ from sampledisco.sample_embedding.blocks import (
|
|
|
45
46
|
loo_rmd,
|
|
46
47
|
soft_assign,
|
|
47
48
|
)
|
|
49
|
+
from sampledisco.utils.embedding_keys import resolve_comp_key, resolve_rmd_key
|
|
48
50
|
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
# Only the ceiling moved (10.0 -> 100.0). Datasets whose signal lives in the RMD
|
|
53
|
+
# block were being truncated by the old ceiling: a dense sweep puts the optimum
|
|
54
|
+
# at alpha = 16 on the 1M-scBloodNL stimulation time-course, and several saved
|
|
55
|
+
# runs (covid ATAC, covid 279, unpaired paper) had returned alpha pinned at 10.0.
|
|
56
|
+
# The floor stays at 0.1: no dataset has ever optimised against it, and on a
|
|
57
|
+
# composition-dominated cohort the objective is flat below ~1.5, so a lower floor
|
|
58
|
+
# only lets alpha drift down until the RMD block is numerically absent.
|
|
59
|
+
DEFAULT_ALPHA_BOUNDS = (0.1, 100.0)
|
|
51
60
|
|
|
52
61
|
|
|
53
62
|
# ============================================================ #
|
|
@@ -57,7 +66,7 @@ def build_blocks(
|
|
|
57
66
|
adata: AnnData,
|
|
58
67
|
sample_col: str,
|
|
59
68
|
celltype_col: str,
|
|
60
|
-
|
|
69
|
+
comp_emb_key: Optional[str] = None,
|
|
61
70
|
rmd_emb_key: Optional[str] = None,
|
|
62
71
|
modality_col: Optional[str] = None,
|
|
63
72
|
batch_col: Optional[str] = None,
|
|
@@ -69,12 +78,12 @@ def build_blocks(
|
|
|
69
78
|
verbose: bool = True,
|
|
70
79
|
) -> Dict:
|
|
71
80
|
"""Build composition + RMD blocks once. Returns a dict the inner loop reuses."""
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
comp_key = resolve_comp_key(adata, comp_emb_key, context="autotune")
|
|
82
|
+
rmd_key = resolve_rmd_key(adata, rmd_emb_key, comp_key=comp_key,
|
|
83
|
+
context="autotune")
|
|
75
84
|
|
|
76
|
-
units, unit_cellids, unit_ids, unit_groups, unit_batches, all_cellids,
|
|
77
|
-
assemble_units(adata, sample_col,
|
|
85
|
+
units, unit_cellids, unit_ids, unit_groups, unit_batches, all_cellids, Z_comp = \
|
|
86
|
+
assemble_units(adata, sample_col, comp_key,
|
|
78
87
|
modality_col=modality_col, batch_col=batch_col)
|
|
79
88
|
n_units = len(units)
|
|
80
89
|
cellid_idx = {cid: i for i, cid in enumerate(all_cellids)}
|
|
@@ -85,26 +94,26 @@ def build_blocks(
|
|
|
85
94
|
|
|
86
95
|
# A1: hard cell-type-label composition (one-hot, no clustering)
|
|
87
96
|
L1 = {ct: i for i, ct in enumerate(unique_cts)}
|
|
88
|
-
soft1 = np.zeros((
|
|
97
|
+
soft1 = np.zeros((Z_comp.shape[0], K_c), dtype=np.float32)
|
|
89
98
|
for i, ct in enumerate(cell_type):
|
|
90
99
|
soft1[i, L1[ct]] = 1.0
|
|
91
100
|
unit_cellids_list = [unit_cellids[uid] for uid in unit_ids]
|
|
92
101
|
A1 = composition_per_unit(unit_cellids_list, soft1, cellid_idx)
|
|
93
102
|
|
|
94
|
-
K_med = min(medium_K, max(2,
|
|
103
|
+
K_med = min(medium_K, max(2, Z_comp.shape[0] // 200))
|
|
95
104
|
if verbose:
|
|
96
105
|
print(f"[autotune.build_blocks] K-means K={K_med}...")
|
|
97
106
|
km_med = MiniBatchKMeans(n_clusters=K_med, random_state=seed,
|
|
98
|
-
batch_size=4096, n_init=5, max_iter=200).fit(
|
|
99
|
-
soft2 = soft_assign(
|
|
107
|
+
batch_size=4096, n_init=5, max_iter=200).fit(Z_comp)
|
|
108
|
+
soft2 = soft_assign(Z_comp, km_med.cluster_centers_)
|
|
100
109
|
A2 = composition_per_unit(unit_cellids_list, soft2, cellid_idx)
|
|
101
110
|
|
|
102
|
-
K_fine = min(fine_K, max(2,
|
|
111
|
+
K_fine = min(fine_K, max(2, Z_comp.shape[0] // 100))
|
|
103
112
|
if verbose:
|
|
104
113
|
print(f"[autotune.build_blocks] K-means K={K_fine}...")
|
|
105
114
|
km_fine = MiniBatchKMeans(n_clusters=K_fine, random_state=seed + 1,
|
|
106
|
-
batch_size=4096, n_init=5, max_iter=200).fit(
|
|
107
|
-
soft3 = soft_assign(
|
|
115
|
+
batch_size=4096, n_init=5, max_iter=200).fit(Z_comp)
|
|
116
|
+
soft3 = soft_assign(Z_comp, km_fine.cluster_centers_)
|
|
108
117
|
A3 = composition_per_unit(unit_cellids_list, soft3, cellid_idx)
|
|
109
118
|
|
|
110
119
|
# RMD
|
|
@@ -152,7 +161,7 @@ def build_blocks(
|
|
|
152
161
|
unit_ids=unit_ids, unit_groups=unit_groups, unit_batches=unit_batches,
|
|
153
162
|
n_units=n_units, grouping=grouping_arr, batch=batch_arr,
|
|
154
163
|
has_batch=has_batch, has_grouping=has_grouping,
|
|
155
|
-
|
|
164
|
+
comp_emb_key=comp_key, rmd_emb_key=rmd_key,
|
|
156
165
|
)
|
|
157
166
|
|
|
158
167
|
|
|
@@ -441,8 +450,29 @@ def make_scorer(name: str, meta: Dict, lam: float = 0.5) -> Callable[[np.ndarray
|
|
|
441
450
|
# ============================================================ #
|
|
442
451
|
# Search strategies #
|
|
443
452
|
# ============================================================ #
|
|
444
|
-
def
|
|
445
|
-
"""
|
|
453
|
+
def _log10_bounds(bounds: Tuple[float, float]) -> Tuple[float, float]:
|
|
454
|
+
"""Map an alpha interval to log10 space, guarding against a non-positive low end."""
|
|
455
|
+
lo, hi = float(bounds[0]), float(bounds[1])
|
|
456
|
+
if hi <= 0:
|
|
457
|
+
raise ValueError(f"alpha_bounds upper limit must be positive, got {hi}")
|
|
458
|
+
lo = max(lo, hi * 1e-6)
|
|
459
|
+
return math.log10(lo), math.log10(hi)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def search_grid(objective: Callable, alpha_grid: Optional[List[float]] = None,
|
|
463
|
+
bounds: Optional[Tuple[float, float]] = None, n: int = 15):
|
|
464
|
+
"""Exhaustive grid search. Returns (best_alpha, best_score, trace).
|
|
465
|
+
|
|
466
|
+
With ``alpha_grid`` the caller's points are used verbatim. Otherwise a
|
|
467
|
+
log-spaced grid of ``n`` points is built from ``bounds`` — alpha is a scale
|
|
468
|
+
parameter, so equal spacing in log10 gives each order of magnitude the same
|
|
469
|
+
number of probes.
|
|
470
|
+
"""
|
|
471
|
+
if alpha_grid is None:
|
|
472
|
+
if bounds is None:
|
|
473
|
+
raise ValueError("search_grid needs alpha_grid or bounds")
|
|
474
|
+
t_lo, t_hi = _log10_bounds(bounds)
|
|
475
|
+
alpha_grid = [float(10.0 ** t) for t in np.linspace(t_lo, t_hi, n)]
|
|
446
476
|
trace = []
|
|
447
477
|
best = None
|
|
448
478
|
for a in alpha_grid:
|
|
@@ -453,31 +483,34 @@ def search_grid(objective: Callable, alpha_grid: List[float]):
|
|
|
453
483
|
return best[0], best[1], trace
|
|
454
484
|
|
|
455
485
|
|
|
456
|
-
def search_golden(objective: Callable, bounds=
|
|
457
|
-
"""Golden-section search for a unimodal objective
|
|
486
|
+
def search_golden(objective: Callable, bounds=DEFAULT_ALPHA_BOUNDS, max_iter=12):
|
|
487
|
+
"""Golden-section search over log10(alpha) for a unimodal objective.
|
|
488
|
+
|
|
489
|
+
Returns (best_alpha, best_score, trace); the trace is in alpha, not log10.
|
|
490
|
+
"""
|
|
458
491
|
phi = (1 + math.sqrt(5)) / 2
|
|
459
|
-
a, b = bounds
|
|
492
|
+
a, b = _log10_bounds(bounds)
|
|
460
493
|
resphi = 2 - phi
|
|
461
494
|
x1 = a + resphi * (b - a)
|
|
462
495
|
x2 = b - resphi * (b - a)
|
|
463
|
-
f1 = objective(x1)
|
|
464
|
-
f2 = objective(x2)
|
|
465
|
-
trace = [(x1, f1), (x2, f2)]
|
|
496
|
+
f1 = objective(10.0 ** x1)
|
|
497
|
+
f2 = objective(10.0 ** x2)
|
|
498
|
+
trace = [(10.0 ** x1, f1), (10.0 ** x2, f2)]
|
|
466
499
|
for _ in range(max_iter):
|
|
467
500
|
if f1 > f2:
|
|
468
501
|
b = x2
|
|
469
502
|
x2 = x1
|
|
470
503
|
f2 = f1
|
|
471
504
|
x1 = a + resphi * (b - a)
|
|
472
|
-
f1 = objective(x1)
|
|
473
|
-
trace.append((x1, f1))
|
|
505
|
+
f1 = objective(10.0 ** x1)
|
|
506
|
+
trace.append((10.0 ** x1, f1))
|
|
474
507
|
else:
|
|
475
508
|
a = x1
|
|
476
509
|
x1 = x2
|
|
477
510
|
f1 = f2
|
|
478
511
|
x2 = b - resphi * (b - a)
|
|
479
|
-
f2 = objective(x2)
|
|
480
|
-
trace.append((x2, f2))
|
|
512
|
+
f2 = objective(10.0 ** x2)
|
|
513
|
+
trace.append((10.0 ** x2, f2))
|
|
481
514
|
best = max(trace, key=lambda x: x[1])
|
|
482
515
|
return best[0], best[1], trace
|
|
483
516
|
|
|
@@ -493,45 +526,53 @@ def _gp_ei(gp, X_grid, y_best, xi=0.01):
|
|
|
493
526
|
return ei
|
|
494
527
|
|
|
495
528
|
|
|
496
|
-
def search_bayesian(objective: Callable, bounds=
|
|
529
|
+
def search_bayesian(objective: Callable, bounds=DEFAULT_ALPHA_BOUNDS,
|
|
497
530
|
n_init=5, n_iter=10, seed=42):
|
|
498
|
-
"""GP-EI Bayesian optimisation over
|
|
499
|
-
|
|
500
|
-
|
|
531
|
+
"""GP-EI Bayesian optimisation over log10(alpha).
|
|
532
|
+
|
|
533
|
+
Seeds with ``n_init`` log-spaced evaluations, then acquires via Expected
|
|
534
|
+
Improvement on a log-spaced grid. Returns (best_alpha, best_score, trace),
|
|
535
|
+
with the trace reported in alpha rather than log10.
|
|
536
|
+
|
|
537
|
+
The search runs in log space because alpha is a scale parameter: seeding
|
|
538
|
+
linearly over, say, [0.01, 1000] would place the first five probes at
|
|
539
|
+
0.01/250/500/750/1000 and never examine the sub-unit region where the
|
|
540
|
+
composition-dominated datasets have their optimum.
|
|
541
|
+
"""
|
|
501
542
|
rng = np.random.default_rng(seed)
|
|
502
|
-
|
|
503
|
-
X_init = np.linspace(lo, hi, n_init).reshape(-1, 1)
|
|
543
|
+
t_lo, t_hi = _log10_bounds(bounds)
|
|
504
544
|
trace = []
|
|
505
|
-
for
|
|
506
|
-
|
|
507
|
-
trace.append((
|
|
508
|
-
X = np.array([[a] for a, _ in trace])
|
|
545
|
+
for t in np.linspace(t_lo, t_hi, n_init):
|
|
546
|
+
a = float(10.0 ** t)
|
|
547
|
+
trace.append((a, objective(a)))
|
|
548
|
+
X = np.array([[math.log10(a)] for a, _ in trace])
|
|
509
549
|
y = np.array([s for _, s in trace])
|
|
510
550
|
kernel = (ConstantKernel(1.0, (1e-3, 1e3))
|
|
511
551
|
* Matern(length_scale=1.0, length_scale_bounds=(1e-2, 1e2), nu=2.5)
|
|
512
552
|
+ WhiteKernel(noise_level=1e-3, noise_level_bounds=(1e-5, 1e-1)))
|
|
513
|
-
grid = np.linspace(
|
|
553
|
+
grid = np.linspace(t_lo, t_hi, 500).reshape(-1, 1)
|
|
514
554
|
for _ in range(n_iter):
|
|
515
555
|
try:
|
|
516
556
|
gp = GaussianProcessRegressor(kernel=kernel, normalize_y=True,
|
|
517
557
|
n_restarts_optimizer=2,
|
|
518
558
|
random_state=seed).fit(X, y)
|
|
519
559
|
ei = _gp_ei(gp, grid, y.max())
|
|
520
|
-
|
|
560
|
+
t_next = float(grid[int(np.argmax(ei))][0])
|
|
521
561
|
except Exception:
|
|
522
|
-
|
|
523
|
-
if any(abs(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
562
|
+
t_next = float(rng.uniform(t_lo, t_hi))
|
|
563
|
+
if any(abs(t_next - math.log10(a)) < 1e-3 for a, _ in trace):
|
|
564
|
+
t_next = float(rng.uniform(t_lo, t_hi))
|
|
565
|
+
a_next = float(10.0 ** t_next)
|
|
566
|
+
s = objective(a_next)
|
|
567
|
+
trace.append((a_next, s))
|
|
568
|
+
X = np.vstack([X, [[t_next]]])
|
|
528
569
|
y = np.append(y, s)
|
|
529
570
|
best = max(trace, key=lambda x: x[1])
|
|
530
571
|
return best[0], best[1], trace
|
|
531
572
|
|
|
532
573
|
|
|
533
574
|
SEARCH_FUNCS = {
|
|
534
|
-
"grid": lambda obj, b: search_grid(obj,
|
|
575
|
+
"grid": lambda obj, b: search_grid(obj, bounds=b, n=15),
|
|
535
576
|
"golden_section": lambda obj, b: search_golden(obj, bounds=b, max_iter=12),
|
|
536
577
|
"bayesian": lambda obj, b: search_bayesian(obj, bounds=b, n_init=5, n_iter=10),
|
|
537
578
|
}
|
|
@@ -546,7 +587,7 @@ def run_autotune(
|
|
|
546
587
|
*,
|
|
547
588
|
sample_col: str = "sample",
|
|
548
589
|
celltype_col: str = "cell_type",
|
|
549
|
-
|
|
590
|
+
comp_emb_key: Optional[str] = None,
|
|
550
591
|
rmd_emb_key: Optional[str] = None,
|
|
551
592
|
modality_col: Optional[str] = None,
|
|
552
593
|
batch_col: Optional[Union[str, List[str]]] = None,
|
|
@@ -564,6 +605,7 @@ def run_autotune(
|
|
|
564
605
|
save: bool = True,
|
|
565
606
|
verbose: bool = True,
|
|
566
607
|
tune_on_modality: Optional[str] = None,
|
|
608
|
+
cluster_emb_key: Optional[str] = None,
|
|
567
609
|
) -> Dict:
|
|
568
610
|
"""Run autotune and return the best params + final sample-AnnData.
|
|
569
611
|
|
|
@@ -574,6 +616,12 @@ def run_autotune(
|
|
|
574
616
|
how does the other modality fare under that α?". Set to ``None`` (default)
|
|
575
617
|
to score on every unit (current behaviour).
|
|
576
618
|
"""
|
|
619
|
+
if cluster_emb_key is not None:
|
|
620
|
+
warnings.warn("cluster_emb_key= is deprecated and will be removed in 1.0; "
|
|
621
|
+
"use comp_emb_key=.", FutureWarning, stacklevel=2)
|
|
622
|
+
if comp_emb_key is None:
|
|
623
|
+
comp_emb_key = cluster_emb_key
|
|
624
|
+
|
|
577
625
|
t0 = time.time()
|
|
578
626
|
primary_batch = batch_col[0] if isinstance(batch_col, (list, tuple)) and batch_col else batch_col
|
|
579
627
|
if isinstance(primary_batch, list):
|
|
@@ -581,7 +629,7 @@ def run_autotune(
|
|
|
581
629
|
|
|
582
630
|
blocks = build_blocks(
|
|
583
631
|
adata, sample_col=sample_col, celltype_col=celltype_col,
|
|
584
|
-
|
|
632
|
+
comp_emb_key=comp_emb_key, rmd_emb_key=rmd_emb_key,
|
|
585
633
|
modality_col=modality_col, batch_col=primary_batch,
|
|
586
634
|
grouping_col=grouping_col, medium_K=medium_K, fine_K=fine_K,
|
|
587
635
|
rmd_dim=rmd_dim, seed=seed, verbose=verbose,
|
|
@@ -755,7 +803,7 @@ def _format_autotune_report(*, best_params, best_score, trace, weights,
|
|
|
755
803
|
lines.append(f" K_c (cell types) : {int(blocks['K_c'])}")
|
|
756
804
|
lines.append(f" K_med (k-means) : {int(blocks['K_med'])}")
|
|
757
805
|
lines.append(f" K_fine (k-means) : {int(blocks['K_fine'])}")
|
|
758
|
-
lines.append(f"
|
|
806
|
+
lines.append(f" comp_emb_key : {blocks.get('comp_emb_key', '?')}")
|
|
759
807
|
lines.append(f" rmd_emb_key : {blocks.get('rmd_emb_key', '?')}")
|
|
760
808
|
lines.append("")
|
|
761
809
|
lines.append("Active scoring proxies (gated by data availability)")
|
|
@@ -782,6 +830,20 @@ def _format_autotune_report(*, best_params, best_score, trace, weights,
|
|
|
782
830
|
+ " (A1, A2, A3, RMD)")
|
|
783
831
|
lines.append(f" total evaluations : {len(trace)}")
|
|
784
832
|
lines.append(f" wall time : {elapsed_s:.2f} s")
|
|
833
|
+
# A flat objective makes the single winning α arbitrary within the plateau;
|
|
834
|
+
# report the plateau so the number is not read as more precise than it is.
|
|
835
|
+
near = [a for a, s in trace
|
|
836
|
+
if best_score == best_score and s >= best_score - 0.01 * abs(best_score)]
|
|
837
|
+
if len(near) > 1:
|
|
838
|
+
lines.append(f" α within 1% of best: [{min(near):g}, {max(near):g}] "
|
|
839
|
+
f"({len(near)}/{len(trace)} evaluations) — the objective is "
|
|
840
|
+
f"flat over this range")
|
|
841
|
+
alpha_best = best_params.get("rmd_weight")
|
|
842
|
+
if alpha_best is not None and any(
|
|
843
|
+
abs(alpha_best - b) <= 1e-9 * max(1.0, abs(b)) for b in alpha_bounds):
|
|
844
|
+
lines.append(f" NOTE: α equals a search bound; the optimum may lie outside "
|
|
845
|
+
f"[{alpha_bounds[0]:g}, {alpha_bounds[1]:g}] — widen "
|
|
846
|
+
f"alpha_bounds and re-run.")
|
|
785
847
|
lines.append("")
|
|
786
848
|
lines.append(f"Search trace ({len(trace)} evals, top 25 by score)")
|
|
787
849
|
lines.append("-" * 68)
|
|
@@ -821,7 +883,9 @@ def _finalize(adata, blocks, final_emb, weights, *,
|
|
|
821
883
|
"K_c": int(blocks["K_c"]),
|
|
822
884
|
"K_med": int(blocks["K_med"]),
|
|
823
885
|
"K_fine": int(blocks["K_fine"]),
|
|
824
|
-
"
|
|
886
|
+
"comp_emb_key": str(blocks.get("comp_emb_key", "")),
|
|
887
|
+
# Deprecated 0.2.0 spelling; removed in 1.0.
|
|
888
|
+
"cluster_emb_key": str(blocks.get("comp_emb_key", "")),
|
|
825
889
|
"rmd_emb_key": str(blocks.get("rmd_emb_key", "")),
|
|
826
890
|
"n_evals": len(trace),
|
|
827
891
|
"autotuned": True,
|
|
@@ -8,6 +8,7 @@ import scanpy as sc
|
|
|
8
8
|
import time
|
|
9
9
|
import matplotlib.pyplot as plt
|
|
10
10
|
from sampledisco.utils.safe_save import safe_h5ad_write # Importing the new safe save method
|
|
11
|
+
from sampledisco.utils.embedding_keys import resolve_comp_key
|
|
11
12
|
|
|
12
13
|
def clean_obs_for_saving(adata, verbose=True):
|
|
13
14
|
"""
|
|
@@ -214,7 +215,7 @@ def cell_types_atac(
|
|
|
214
215
|
Save=False,
|
|
215
216
|
output_dir=None,
|
|
216
217
|
cluster_resolution=0.8,
|
|
217
|
-
use_rep=
|
|
218
|
+
use_rep=None,
|
|
218
219
|
peaks=None,
|
|
219
220
|
method='average',
|
|
220
221
|
metric='euclidean',
|
|
@@ -234,7 +235,7 @@ def cell_types_atac(
|
|
|
234
235
|
Assigns cell types based on existing annotations or performs Leiden clustering if no annotation exists.
|
|
235
236
|
Uses recursive strategy to adaptively find optimal clustering resolution when target clusters specified.
|
|
236
237
|
|
|
237
|
-
ATAC VERSION: Uses dimension reduction (
|
|
238
|
+
ATAC VERSION: Uses dimension reduction (Z_comp) for dendrogram construction and differential peaks.
|
|
238
239
|
|
|
239
240
|
Parameters:
|
|
240
241
|
- adata: AnnData object
|
|
@@ -245,7 +246,7 @@ def cell_types_atac(
|
|
|
245
246
|
- Save: Boolean, whether to save the output
|
|
246
247
|
- output_dir: Directory to save the output if Save=True
|
|
247
248
|
- cluster_resolution: Starting resolution for Leiden clustering
|
|
248
|
-
- use_rep: Representation to use for neighborhood graph (default: '
|
|
249
|
+
- use_rep: Representation to use for neighborhood graph (default: 'Z_comp')
|
|
249
250
|
- peaks: List of peak names for mapping numeric IDs to names
|
|
250
251
|
- method, metric, distance_mode: Parameters for hierarchical clustering
|
|
251
252
|
- num_DMs: Number of diffusion map components for neighborhood graph
|
|
@@ -264,6 +265,8 @@ def cell_types_atac(
|
|
|
264
265
|
start_time = time.time() if verbose else None
|
|
265
266
|
from sampledisco.utils.random_seed import set_global_seed
|
|
266
267
|
set_global_seed(seed = 42)
|
|
268
|
+
use_rep = resolve_comp_key(adata, use_rep, fallbacks=('X_lsi',),
|
|
269
|
+
context="cell_types_atac")
|
|
267
270
|
|
|
268
271
|
if _recursion_depth > 10:
|
|
269
272
|
raise RuntimeError(f"Maximum recursion depth exceeded. Could not achieve {n_target_clusters} clusters.")
|
|
@@ -8,6 +8,7 @@ import rapids_singlecell as rsc
|
|
|
8
8
|
import scanpy as sc
|
|
9
9
|
from sklearn.neighbors import KNeighborsTransformer
|
|
10
10
|
import time
|
|
11
|
+
from sampledisco.utils.embedding_keys import resolve_comp_key
|
|
11
12
|
|
|
12
13
|
def standardize_cell_type_column_gpu(adata, verbose=True):
|
|
13
14
|
"""Coerce obs['cell_type'] to string categorical, handling Leiden integer output."""
|
|
@@ -56,7 +57,7 @@ def cell_types_atac_gpu(
|
|
|
56
57
|
Save=False,
|
|
57
58
|
output_dir=None,
|
|
58
59
|
cluster_resolution=0.8,
|
|
59
|
-
use_rep=
|
|
60
|
+
use_rep=None,
|
|
60
61
|
peaks=None,
|
|
61
62
|
method='average',
|
|
62
63
|
metric='euclidean',
|
|
@@ -75,6 +76,8 @@ def cell_types_atac_gpu(
|
|
|
75
76
|
_recursion_depth is internal; do not set manually.
|
|
76
77
|
"""
|
|
77
78
|
start_time = time.time() if verbose else None
|
|
79
|
+
use_rep = resolve_comp_key(adata, use_rep, fallbacks=('X_lsi',),
|
|
80
|
+
context="cell_types_atac_gpu")
|
|
78
81
|
|
|
79
82
|
if _recursion_depth > 10:
|
|
80
83
|
raise RuntimeError(f"Maximum recursion depth exceeded. Could not achieve {n_target_clusters} clusters.")
|
|
@@ -284,7 +287,7 @@ def cell_type_dendrogram_atac_gpu(
|
|
|
284
287
|
method='average',
|
|
285
288
|
metric='euclidean',
|
|
286
289
|
distance_mode='centroid',
|
|
287
|
-
use_rep=
|
|
290
|
+
use_rep=None,
|
|
288
291
|
num_DMs=20,
|
|
289
292
|
verbose=True
|
|
290
293
|
):
|
|
@@ -297,8 +300,8 @@ def cell_type_dendrogram_atac_gpu(
|
|
|
297
300
|
print(f'=== Preparing data for dendrogram (using {use_rep}) ===')
|
|
298
301
|
if groupby not in adata.obs.columns:
|
|
299
302
|
raise ValueError(f"The groupby key '{groupby}' is not present in adata.obs.")
|
|
300
|
-
|
|
301
|
-
|
|
303
|
+
use_rep = resolve_comp_key(adata, use_rep, fallbacks=('X_lsi',),
|
|
304
|
+
context="cell_type_dendrogram_atac_gpu")
|
|
302
305
|
|
|
303
306
|
# Convert from GPU array if needed.
|
|
304
307
|
if hasattr(adata.obsm[use_rep], 'get'):
|
|
@@ -8,6 +8,7 @@ import scanpy as sc
|
|
|
8
8
|
import muon as mu
|
|
9
9
|
from muon import atac as ac
|
|
10
10
|
from sampledisco.utils.harmony_compat import harmonize_embedding
|
|
11
|
+
from sampledisco.utils.embedding_keys import COMP_KEY, LEGACY_COMP_KEY, write_comp_key
|
|
11
12
|
from scipy.sparse import issparse
|
|
12
13
|
|
|
13
14
|
from sampledisco.utils.safe_save import safe_h5ad_write
|
|
@@ -20,7 +21,7 @@ def _run_lsi_on_hvg(adata, n_comps, drop_first, hvg_col="highly_variable", out_k
|
|
|
20
21
|
|
|
21
22
|
muon's `ac.tl.lsi` doesn't accept `use_highly_variable`, so we run on a
|
|
22
23
|
temporary HVF-only copy. varm['LSI'] and uns['lsi'] are only written for the
|
|
23
|
-
canonical out_key='X_lsi' to avoid clobbering the
|
|
24
|
+
canonical out_key='X_lsi' to avoid clobbering the Z_comp basis.
|
|
24
25
|
"""
|
|
25
26
|
hvg_mask = adata.var[hvg_col].values
|
|
26
27
|
if hvg_mask.sum() < n_comps + 1:
|
|
@@ -62,7 +63,7 @@ def anndata_cluster(
|
|
|
62
63
|
- `.X` TF-IDF + log1p normalized
|
|
63
64
|
- `.var['highly_variable']` HVF flag (no subsetting)
|
|
64
65
|
- `.obsm['X_lsi']` LSI on HVF subset (drop_first applied if set)
|
|
65
|
-
- `.obsm['
|
|
66
|
+
- `.obsm['Z_comp']` sample-removed Harmony
|
|
66
67
|
- `.obsm['Z_rmd']` sample-preserved Harmony (RMD)
|
|
67
68
|
|
|
68
69
|
Writes a single file: `<output_dir>/adata_preprocessed.h5ad`.
|
|
@@ -102,18 +103,18 @@ def anndata_cluster(
|
|
|
102
103
|
print("=== [CPU] Harmony pass 1: WITH sample (sample-removed) ===")
|
|
103
104
|
print(" batch keys:", ", ".join(cell_level_batch_key_for_harmony or []))
|
|
104
105
|
if cell_level_batch_key_for_harmony:
|
|
105
|
-
adata
|
|
106
|
+
write_comp_key(adata, harmonize_embedding(
|
|
106
107
|
adata.obsm["X_lsi"], adata.obs,
|
|
107
108
|
batch_key=cell_level_batch_key_for_harmony,
|
|
108
109
|
max_iter_harmony=num_harmony_iterations,
|
|
109
110
|
use_gpu=False,
|
|
110
|
-
)
|
|
111
|
+
))
|
|
111
112
|
else:
|
|
112
|
-
adata
|
|
113
|
+
write_comp_key(adata, adata.obsm["X_lsi"].copy())
|
|
113
114
|
|
|
114
115
|
# HVF2 for RMD basis: sample-naive (batch_key=None). ATAC has no raw-counts
|
|
115
116
|
# layer post TF-IDF, so HVF2 runs on the same TF-IDF+log1p .X as HVF1.
|
|
116
|
-
# Placed after
|
|
117
|
+
# Placed after Z_comp Harmony to preserve RNG ordering.
|
|
117
118
|
adata.var["highly_variable_clust"] = adata.var["highly_variable"].to_numpy().copy()
|
|
118
119
|
if verbose:
|
|
119
120
|
print("Running HVF2 (sample-naive) selection...")
|
|
@@ -146,7 +147,9 @@ def anndata_cluster(
|
|
|
146
147
|
adata.obsm["X_lsi_rmd"], dtype=np.float32)
|
|
147
148
|
|
|
148
149
|
if verbose:
|
|
149
|
-
print(f"
|
|
150
|
+
print(f" Z_comp shape: {adata.obsm[COMP_KEY].shape}")
|
|
151
|
+
if LEGACY_COMP_KEY in adata.obsm:
|
|
152
|
+
print(f" (legacy alias obsm['{LEGACY_COMP_KEY}'] also written; removed in 1.0)")
|
|
150
153
|
print(f" X_lsi_rmd shape: {adata.obsm['X_lsi_rmd'].shape}")
|
|
151
154
|
print(f" Z_rmd shape: {adata.obsm['Z_rmd'].shape}")
|
|
152
155
|
|