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.
Files changed (91) hide show
  1. {sampledisco-0.2.0/src/sampledisco.egg-info → sampledisco-0.3.0}/PKG-INFO +11 -11
  2. {sampledisco-0.2.0 → sampledisco-0.3.0}/README.md +10 -10
  3. {sampledisco-0.2.0 → sampledisco-0.3.0}/pyproject.toml +1 -1
  4. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/config/config_demo.yaml +6 -6
  5. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/parameter_selection/autotune.py +114 -50
  6. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/ATAC_cell_type.py +6 -3
  7. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/ATAC_cell_type_gpu.py +7 -4
  8. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/atac_preprocess_cpu.py +10 -7
  9. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/atac_preprocess_gpu.py +10 -7
  10. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/cell_type_cpu.py +6 -5
  11. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/cell_type_gpu.py +6 -5
  12. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/multi_omics_batch_correction.py +19 -12
  13. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/multi_omics_cell_type_cpu.py +7 -6
  14. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/multi_omics_cell_type_gpu.py +6 -4
  15. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/multi_omics_glue.py +25 -14
  16. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/multi_omics_merge.py +5 -4
  17. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/rna_preprocess_cpu.py +10 -7
  18. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/rna_preprocess_gpu.py +10 -7
  19. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_association/association.py +2 -2
  20. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_distance/sample_distance.py +18 -18
  21. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_embedding/__init__.py +3 -1
  22. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_embedding/blocks.py +5 -5
  23. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_embedding/sample_embedding.py +38 -28
  24. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_embedding/sample_embedding_gpu.py +27 -18
  25. sampledisco-0.3.0/src/sampledisco/utils/embedding_keys.py +189 -0
  26. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/multi_omics_visualization.py +2 -2
  27. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/visualization_other.py +4 -4
  28. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/wrapper/atac_wrapper.py +6 -5
  29. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/wrapper/multiomics_wrapper.py +42 -60
  30. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/wrapper/rna_wrapper.py +6 -5
  31. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/wrapper/wrapper.py +7 -7
  32. {sampledisco-0.2.0 → sampledisco-0.3.0/src/sampledisco.egg-info}/PKG-INFO +11 -11
  33. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco.egg-info/SOURCES.txt +1 -0
  34. {sampledisco-0.2.0 → sampledisco-0.3.0}/LICENSE +0 -0
  35. {sampledisco-0.2.0 → sampledisco-0.3.0}/requirements.txt +0 -0
  36. {sampledisco-0.2.0 → sampledisco-0.3.0}/setup.cfg +0 -0
  37. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/__init__.py +0 -0
  38. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/cli.py +0 -0
  39. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/ATAC_ArchR.py +0 -0
  40. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/ATAC_RNA_harmony.py +0 -0
  41. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/ATAC_gene_activity.py +0 -0
  42. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/ATAC_peak_annotation.py +0 -0
  43. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/RNA_name_convertor.py +0 -0
  44. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/__init__.py +0 -0
  45. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/pseudo_correlation.py +0 -0
  46. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/test_pseudo_correlation.py +0 -0
  47. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/gene_activity/validation.py +0 -0
  48. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/parameter_selection/__init__.py +0 -0
  49. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/preparation/__init__.py +0 -0
  50. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_association/__init__.py +0 -0
  51. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/RAISIN.py +0 -0
  52. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/RAISIN_TEST.py +0 -0
  53. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/__init__.py +0 -0
  54. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/cluster.py +0 -0
  55. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/cluster_severity_reconcile.py +0 -0
  56. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_clustering/proportion_test.py +0 -0
  57. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_distance/ChiSquare.py +0 -0
  58. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_distance/__init__.py +0 -0
  59. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_distance/distance_test.py +0 -0
  60. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_distance/jensenshannon.py +0 -0
  61. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/CCA.py +0 -0
  62. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/CCA_test.py +0 -0
  63. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/TSCAN.py +0 -0
  64. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/__init__.py +0 -0
  65. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/trajectory_DGE_visualization.py +0 -0
  66. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/sample_trajectory/trajectory_diff_gene.py +0 -0
  67. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/Grouping.py +0 -0
  68. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/__init__.py +0 -0
  69. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/batch_regress.py +0 -0
  70. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/harmony_compat.py +0 -0
  71. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/imbalance_cell_type_handler.py +0 -0
  72. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/inspector.py +0 -0
  73. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/limma.py +0 -0
  74. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/merge_sample_meta.py +0 -0
  75. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/random_seed.py +0 -0
  76. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/safe_save.py +0 -0
  77. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/slim_adata.py +0 -0
  78. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/subsample.py +0 -0
  79. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/subsample_generator.py +0 -0
  80. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/tf_idf.py +0 -0
  81. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/utils/unify_optimal.py +0 -0
  82. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/ATAC_visualization.py +0 -0
  83. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/DEG_visualization.py +0 -0
  84. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/__init__.py +0 -0
  85. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/visualization_embedding.py +0 -0
  86. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/visualization/visualization_helper.py +0 -0
  87. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco/wrapper/__init__.py +0 -0
  88. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco.egg-info/dependency_links.txt +0 -0
  89. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco.egg-info/entry_points.txt +0 -0
  90. {sampledisco-0.2.0 → sampledisco-0.3.0}/src/sampledisco.egg-info/requires.txt +0 -0
  91. {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.2.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
- | **`Z_clust`** | sample-removed embedding — used for clustering and composition blocks | Harmony (single-omics) / Harmony post-pass on scGLUE (multi-omics) |
65
- | **`Z_cmd`** | sample-preserved embedding — used for the counterfactual displacement (CMD) block | second Harmony pass (single-omics) / scGLUE primary output (multi-omics) |
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. **CMD** — leave-one-out cell-type-resolved displacement on `Z_cmd`
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 → Z_clust + Z_cmd
91
- │ ├── atac_preprocess_{cpu,gpu}.py # QC → TF-IDF → HVF → LSI → dual Harmony → Z_clust + Z_cmd
92
- │ ├── cell_type_{cpu,gpu}.py # Leiden clustering on Z_clust (RNA or ATAC)
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 → Z_clust
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, CMD, weighting, Frobenius stack, final PCA + Harmony
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 CMD α; adaptive proxy ensemble
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 Z_clust + Z_cmd
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
- | **`Z_clust`** | sample-removed embedding — used for clustering and composition blocks | Harmony (single-omics) / Harmony post-pass on scGLUE (multi-omics) |
18
- | **`Z_cmd`** | sample-preserved embedding — used for the counterfactual displacement (CMD) block | second Harmony pass (single-omics) / scGLUE primary output (multi-omics) |
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. **CMD** — leave-one-out cell-type-resolved displacement on `Z_cmd`
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 → Z_clust + Z_cmd
44
- │ ├── atac_preprocess_{cpu,gpu}.py # QC → TF-IDF → HVF → LSI → dual Harmony → Z_clust + Z_cmd
45
- │ ├── cell_type_{cpu,gpu}.py # Leiden clustering on Z_clust (RNA or ATAC)
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 → Z_clust
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, CMD, weighting, Frobenius stack, final PCA + Harmony
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 CMD α; adaptive proxy ensemble
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 Z_clust + Z_cmd
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.2.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, 10.0]
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, 10.0]
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 X_glue_harmony (used by cell typing + A1/A2/A3). Alternative:
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 X_glue_harmony and the Harmony pass auto-skips.
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 X_glue_harmony (sample-removed) if present, else X_glue
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, 10.0]
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
- DEFAULT_ALPHA_BOUNDS = (0.1, 10.0)
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
- cluster_emb_key: str,
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
- rmd_key = rmd_emb_key if rmd_emb_key and rmd_emb_key in adata.obsm else (
73
- "Z_rmd" if "Z_rmd" in adata.obsm else cluster_emb_key
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, Z_clust = \
77
- assemble_units(adata, sample_col, cluster_emb_key,
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((Z_clust.shape[0], K_c), dtype=np.float32)
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, Z_clust.shape[0] // 200))
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(Z_clust)
99
- soft2 = soft_assign(Z_clust, km_med.cluster_centers_)
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, Z_clust.shape[0] // 100))
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(Z_clust)
107
- soft3 = soft_assign(Z_clust, km_fine.cluster_centers_)
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
- cluster_emb_key=cluster_emb_key, rmd_emb_key=rmd_key,
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 search_grid(objective: Callable, alpha_grid: List[float]):
445
- """Exhaustive grid search over ``alpha_grid``. Returns (best_alpha, best_score, trace)."""
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=(0.1, 10.0), max_iter=12):
457
- """Golden-section search for a unimodal objective over ``bounds``. Returns (best_alpha, best_score, trace)."""
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=(0.1, 10.0),
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 a 1-D ``bounds`` interval.
499
- Seeds with ``n_init`` equally-spaced evaluations, then acquires via Expected Improvement.
500
- Returns (best_alpha, best_score, trace)."""
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
- lo, hi = bounds
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 x in X_init:
506
- s = objective(float(x[0]))
507
- trace.append((float(x[0]), s))
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(lo, hi, 500).reshape(-1, 1)
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
- x_next = float(grid[int(np.argmax(ei))][0])
560
+ t_next = float(grid[int(np.argmax(ei))][0])
521
561
  except Exception:
522
- x_next = float(rng.uniform(lo, hi))
523
- if any(abs(x_next - x) < 1e-3 for x, _ in trace):
524
- x_next = float(rng.uniform(lo, hi))
525
- s = objective(x_next)
526
- trace.append((x_next, s))
527
- X = np.vstack([X, [[x_next]]])
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, [0.1, 0.5, 1.0, 2.0, 4.0, 6.0, 8.0]),
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
- cluster_emb_key: str = "Z_clust",
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
- cluster_emb_key=cluster_emb_key, rmd_emb_key=rmd_emb_key,
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" cluster_emb_key : {blocks.get('cluster_emb_key', '?')}")
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
- "cluster_emb_key": str(blocks.get("cluster_emb_key", "")),
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='Z_clust',
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 (Z_clust) for dendrogram construction and differential peaks.
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: 'Z_clust')
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='Z_clust',
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='Z_clust',
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
- if use_rep not in adata.obsm:
301
- raise ValueError(f"The representation '{use_rep}' is not present in adata.obsm.")
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 Z_clust basis.
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['Z_clust']` sample-removed Harmony
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.obsm["Z_clust"] = harmonize_embedding(
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.obsm["Z_clust"] = adata.obsm["X_lsi"].copy()
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 Z_clust Harmony to preserve RNG ordering.
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" Z_clust shape: {adata.obsm['Z_clust'].shape}")
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