sampledisco 0.3.0__tar.gz → 0.4.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 (94) hide show
  1. {sampledisco-0.3.0/src/sampledisco.egg-info → sampledisco-0.4.0}/PKG-INFO +45 -39
  2. {sampledisco-0.3.0 → sampledisco-0.4.0}/README.md +43 -37
  3. {sampledisco-0.3.0 → sampledisco-0.4.0}/pyproject.toml +1 -1
  4. {sampledisco-0.3.0 → sampledisco-0.4.0}/requirements.txt +3 -3
  5. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/cli.py +14 -1
  6. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/config/config_demo.yaml +6 -3
  7. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/parameter_selection/autotune.py +16 -10
  8. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/cell_type_cpu.py +6 -5
  9. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/cell_type_gpu.py +6 -5
  10. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_embedding/__init__.py +5 -1
  11. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_embedding/blocks.py +420 -48
  12. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_embedding/sample_embedding.py +57 -52
  13. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_embedding/sample_embedding_gpu.py +72 -65
  14. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/wrapper/atac_wrapper.py +5 -2
  15. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/wrapper/multiomics_wrapper.py +11 -3
  16. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/wrapper/rna_wrapper.py +5 -2
  17. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/wrapper/wrapper.py +37 -15
  18. {sampledisco-0.3.0 → sampledisco-0.4.0/src/sampledisco.egg-info}/PKG-INFO +45 -39
  19. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco.egg-info/SOURCES.txt +4 -1
  20. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco.egg-info/requires.txt +1 -1
  21. sampledisco-0.4.0/tests/test_config_validation.py +54 -0
  22. sampledisco-0.4.0/tests/test_inplace_uns_and_equal_weight.py +186 -0
  23. sampledisco-0.4.0/tests/test_sample_embedding_equivalence.py +148 -0
  24. {sampledisco-0.3.0 → sampledisco-0.4.0}/LICENSE +0 -0
  25. {sampledisco-0.3.0 → sampledisco-0.4.0}/setup.cfg +0 -0
  26. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/__init__.py +0 -0
  27. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/gene_activity/ATAC_ArchR.py +0 -0
  28. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/gene_activity/ATAC_RNA_harmony.py +0 -0
  29. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/gene_activity/ATAC_gene_activity.py +0 -0
  30. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/gene_activity/ATAC_peak_annotation.py +0 -0
  31. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/gene_activity/RNA_name_convertor.py +0 -0
  32. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/gene_activity/__init__.py +0 -0
  33. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/gene_activity/pseudo_correlation.py +0 -0
  34. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/gene_activity/test_pseudo_correlation.py +0 -0
  35. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/gene_activity/validation.py +0 -0
  36. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/parameter_selection/__init__.py +0 -0
  37. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/ATAC_cell_type.py +0 -0
  38. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/ATAC_cell_type_gpu.py +0 -0
  39. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/__init__.py +0 -0
  40. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/atac_preprocess_cpu.py +0 -0
  41. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/atac_preprocess_gpu.py +0 -0
  42. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/multi_omics_batch_correction.py +0 -0
  43. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/multi_omics_cell_type_cpu.py +0 -0
  44. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/multi_omics_cell_type_gpu.py +0 -0
  45. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/multi_omics_glue.py +0 -0
  46. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/multi_omics_merge.py +0 -0
  47. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/rna_preprocess_cpu.py +0 -0
  48. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/preparation/rna_preprocess_gpu.py +0 -0
  49. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_association/__init__.py +0 -0
  50. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_association/association.py +0 -0
  51. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_clustering/RAISIN.py +0 -0
  52. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_clustering/RAISIN_TEST.py +0 -0
  53. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_clustering/__init__.py +0 -0
  54. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_clustering/cluster.py +0 -0
  55. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_clustering/cluster_severity_reconcile.py +0 -0
  56. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_clustering/proportion_test.py +0 -0
  57. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_distance/ChiSquare.py +0 -0
  58. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_distance/__init__.py +0 -0
  59. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_distance/distance_test.py +0 -0
  60. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_distance/jensenshannon.py +0 -0
  61. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_distance/sample_distance.py +0 -0
  62. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_trajectory/CCA.py +0 -0
  63. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_trajectory/CCA_test.py +0 -0
  64. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_trajectory/TSCAN.py +0 -0
  65. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_trajectory/__init__.py +0 -0
  66. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_trajectory/trajectory_DGE_visualization.py +0 -0
  67. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/sample_trajectory/trajectory_diff_gene.py +0 -0
  68. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/Grouping.py +0 -0
  69. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/__init__.py +0 -0
  70. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/batch_regress.py +0 -0
  71. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/embedding_keys.py +0 -0
  72. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/harmony_compat.py +0 -0
  73. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/imbalance_cell_type_handler.py +0 -0
  74. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/inspector.py +0 -0
  75. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/limma.py +0 -0
  76. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/merge_sample_meta.py +0 -0
  77. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/random_seed.py +0 -0
  78. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/safe_save.py +0 -0
  79. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/slim_adata.py +0 -0
  80. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/subsample.py +0 -0
  81. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/subsample_generator.py +0 -0
  82. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/tf_idf.py +0 -0
  83. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/utils/unify_optimal.py +0 -0
  84. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/visualization/ATAC_visualization.py +0 -0
  85. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/visualization/DEG_visualization.py +0 -0
  86. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/visualization/__init__.py +0 -0
  87. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/visualization/multi_omics_visualization.py +0 -0
  88. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/visualization/visualization_embedding.py +0 -0
  89. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/visualization/visualization_helper.py +0 -0
  90. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/visualization/visualization_other.py +0 -0
  91. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco/wrapper/__init__.py +0 -0
  92. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco.egg-info/dependency_links.txt +0 -0
  93. {sampledisco-0.3.0 → sampledisco-0.4.0}/src/sampledisco.egg-info/entry_points.txt +0 -0
  94. {sampledisco-0.3.0 → sampledisco-0.4.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.0
3
+ Version: 0.4.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>
@@ -28,7 +28,7 @@ Requires-Dist: patsy>=0.5
28
28
  Requires-Dist: scanpy>=1.11
29
29
  Requires-Dist: anndata>=0.10
30
30
  Requires-Dist: muon>=0.1
31
- Requires-Dist: harmonypy<0.0.10,>=0.0.9
31
+ Requires-Dist: harmonypy==0.2.0
32
32
  Requires-Dist: python-igraph>=0.10
33
33
  Requires-Dist: leidenalg>=0.9
34
34
  Requires-Dist: networkx>=3.0
@@ -51,7 +51,7 @@ A cross-omics, cross-condition **sample embedding** tool for single-cell data.
51
51
 
52
52
  SampleDisco takes a cell-level embedding (from any standard scRNA / scATAC / multi-omics integration method) and lifts it to a **sample-level embedding** that captures both cell-type composition and the per-cell-type state of each sample. Every downstream analysis — sample-to-sample distance, clustering, trajectory inference, phenotype association — then runs on that single shared sample embedding, regardless of modality.
53
53
 
54
- Paper draft: [`/users/hjiang/GenoDistance/SampleDisco_Draft-11.pdf`](../SampleDisco_Draft-11.pdf)
54
+ Documentation and tutorial: <https://j041120h.github.io/SampleDisco_tutorial/>
55
55
 
56
56
  ---
57
57
 
@@ -79,46 +79,50 @@ The four blocks are inverse-variance weighted, Frobenius-stacked, PCA-reduced to
79
79
 
80
80
  ```
81
81
  code/
82
- ├── SampleDisc.py # CLI entry point (simple or complex mode)
83
- ├── config/ # 9 YAML configs covering covid / blood / eye / heart / ENCODE / tabula / long_covid / unpaired / default
84
- ├── wrapper/ # Orchestration
85
- │ ├── wrapper.py # Master wrapper; gates RNA + ATAC + multiomics + shared downstream
86
- │ ├── rna_wrapper.py
87
- │ ├── atac_wrapper.py
88
- │ └── multiomics_wrapper.py
89
- ├── preparation/ # Preprocessing
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
- │ ├── ATAC_cell_type{,_gpu}.py # ATAC-specific cell typing variants
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_comp
96
- │ ├── multi_omics_merge.py # post-GLUE merge + per-modality preprocess/slimming
97
- │ └── multi_omics_cell_type_{cpu,gpu}.py # RNA-Leiden + k-NN label transfer to ATAC
98
- ├── sample_embedding/ # Core method
99
- │ ├── blocks.py # composition, RMD, weighting, Frobenius stack, final PCA + Harmony
100
- │ ├── sample_embedding.py # CPU pipeline
101
- │ └── sample_embedding_gpu.py # GPU pipeline (cuML + cupy)
102
- ├── parameter_selection/
103
- │ └── autotune.py # Bayesian GP sweep over RMD α; adaptive proxy ensemble
104
- ├── sample_distance/ # Pairwise sample distances (DR / EMD / chi-square / JS)
105
- ├── sample_clustering/ # Hierarchical (HRA / HRC / NN / UPGMA / consensus), K-means, proportion test, RAISIN
106
- ├── sample_trajectory/ # CCA (supervised) and TSCAN (unsupervised) + GAM-based trajectory DGE
107
- ├── sample_association/ # Per-PC variance explained vs sample-level covariates (permutation FDR)
108
- ├── visualization/ # Embedding plots, dendrograms, DGE volcanos, modality-aware multi-omics scatters
109
- ├── utils/ # Shared helpers: seed, safe h5ad I/O, limma, TF-IDF, batch regress, Grouping
110
- ├── gene_activity/ # ATAC peak → gene activity inference + RNA-ATAC validation
111
- └── claude/ # Active one-off run scripts (rerun launchers, monitored SE, parameter sweeps)
82
+ ├── pyproject.toml # package metadata; `pip install -e .` installs the `sampledisco` command
83
+ ├── config/ # 10 YAML configs for the paper's datasets: covid / blood (1M-scBloodNL) / eye / heart / ENCODE / healthy-aging PBMC / long_covid / unpaired (+ RNA-tuned) / default
84
+ ├── src/sampledisco/ # the package
85
+ │ ├── cli.py # `sampledisco` command: `-m complex --config <yaml>`, `--init-config <path>`
86
+ │ ├── config/config_demo.yaml # demo config written by `sampledisco --init-config`
87
+ │ ├── wrapper/ # Orchestration
88
+ │ │ ├── wrapper.py # Master wrapper; gates RNA + ATAC + multiomics + shared downstream
89
+ │ │ ├── rna_wrapper.py
90
+ │ │ ├── atac_wrapper.py
91
+ │ │ └── multiomics_wrapper.py
92
+ │ ├── preparation/ # Preprocessing
93
+ │ │ ├── rna_preprocess_{cpu,gpu}.py # QC → HVG → PCA → dual Harmony → Z_comp + Z_rmd
94
+ │ │ ├── atac_preprocess_{cpu,gpu}.py # QC → TF-IDF → HVF → LSI → dual Harmony → Z_comp + Z_rmd
95
+ │ │ ├── cell_type_{cpu,gpu}.py # Leiden clustering on Z_comp (RNA or ATAC)
96
+ │ │ ├── ATAC_cell_type{,_gpu}.py # ATAC-specific cell typing variants
97
+ │ │ ├── multi_omics_glue.py # scGLUE integration (cross-modality VAE + guidance graph)
98
+ │ │ ├── multi_omics_batch_correction.py # Harmony post-pass on X_glue → Z_comp
99
+ │ │ ├── multi_omics_merge.py # post-GLUE merge + per-modality preprocess/slimming
100
+ │ │ └── multi_omics_cell_type_{cpu,gpu}.py # RNA-Leiden + k-NN label transfer to ATAC
101
+ │ ├── sample_embedding/ # Core method
102
+ │ │ ├── blocks.py # composition, RMD, weighting, Frobenius stack, final PCA + Harmony
103
+ │ │ ├── sample_embedding.py # CPU pipeline
104
+ │ │ └── sample_embedding_gpu.py # GPU pipeline (cuML + cupy)
105
+ │ ├── parameter_selection/
106
+ │ │ └── autotune.py # Bayesian GP sweep over RMD α; adaptive proxy ensemble
107
+ │ ├── sample_distance/ # Pairwise sample distances (DR / EMD / chi-square / JS)
108
+ │ ├── sample_clustering/ # Hierarchical (HRA / HRC / NN / UPGMA / consensus), K-means, proportion test, RAISIN
109
+ │ ├── sample_trajectory/ # CCA (supervised) and TSCAN (unsupervised) + GAM-based trajectory DGE
110
+ │ ├── sample_association/ # Per-PC variance explained vs sample-level covariates (permutation FDR)
111
+ │ ├── visualization/ # Embedding plots, dendrograms, DGE volcanos, modality-aware multi-omics scatters
112
+ │ ├── utils/ # Shared helpers: seed, safe h5ad I/O, limma, TF-IDF, batch regress, Grouping
113
+ │ └── gene_activity/ # ATAC peak → gene activity inference + RNA-ATAC validation
114
+ ├── Benchmark_covid/, Benchmark_multiomics/ # benchmark drivers used for the paper
115
+ └── claude/ # one-off run scripts (rerun launchers, monitored runs, parameter sweeps)
112
116
  ```
113
117
 
114
118
  ---
115
119
 
116
120
  ## Usage
117
121
 
118
- ### Complex mode (recommended) — YAML-driven
122
+ ### Run — YAML-driven
119
123
 
120
124
  ```bash
121
- python SampleDisc.py -m complex --config config/config.yaml
125
+ sampledisco -m complex --config config/config.yaml # same as: python -m sampledisco.cli -m complex --config ...
122
126
  ```
123
127
 
124
128
  The YAML drives every flag and parameter for all three pipelines:
@@ -128,14 +132,16 @@ The YAML drives every flag and parameter for all three pipelines:
128
132
  - **Per-modality downstream gates** (Phase 2): `*_sample_distance_calculation`, `*_trajectory_analysis`, `*_trajectory_dge`, `*_sample_cluster`, `*_proportion_test`, `*_cluster_dge`, `*_visualize_data`, `*_dimension_association_analysis`
129
133
  - **Multi-omics-specific**: `multiomics_run_glue_*`, `multiomics_treat_sample_as_batch`, `multiomics_run_glue_twice_for_sample_removal`
130
134
 
131
- The 9 ready-to-use configs in `config/` are point-in-time snapshots for the datasets used in the paper; copy one and adjust paths / column names for your own data.
135
+ The 10 ready-to-use configs in `config/` are point-in-time snapshots for the datasets used in the paper; copy one and adjust paths / column names for your own data.
132
136
 
133
- ### Simple mode — one positional file, defaults everywhere
137
+ ### Starter config
134
138
 
135
139
  ```bash
136
- python SampleDisc.py -m simple -c <count_data.h5ad> -o <output_dir>
140
+ sampledisco --init-config my_config.yaml
137
141
  ```
138
142
 
143
+ writes the demo config (tuned for the Zenodo demo dataset) to `my_config.yaml`. The YAML is validated against every `wrapper()` parameter, so edit values rather than deleting keys.
144
+
139
145
  ---
140
146
 
141
147
  ## Inputs
@@ -182,7 +188,7 @@ detects and uses them at runtime. There is no separate "GPU build" of SampleDisc
182
188
  ### 1. Core install (CPU)
183
189
 
184
190
  ```bash
185
- pip install sampledisco # once published — or `pip install -e .` from a clone
191
+ pip install sampledisco # or `pip install -e .` from a clone
186
192
  ```
187
193
 
188
194
  ### 2. System prerequisite — bedtools
@@ -4,7 +4,7 @@ A cross-omics, cross-condition **sample embedding** tool for single-cell data.
4
4
 
5
5
  SampleDisco takes a cell-level embedding (from any standard scRNA / scATAC / multi-omics integration method) and lifts it to a **sample-level embedding** that captures both cell-type composition and the per-cell-type state of each sample. Every downstream analysis — sample-to-sample distance, clustering, trajectory inference, phenotype association — then runs on that single shared sample embedding, regardless of modality.
6
6
 
7
- Paper draft: [`/users/hjiang/GenoDistance/SampleDisco_Draft-11.pdf`](../SampleDisco_Draft-11.pdf)
7
+ Documentation and tutorial: <https://j041120h.github.io/SampleDisco_tutorial/>
8
8
 
9
9
  ---
10
10
 
@@ -32,46 +32,50 @@ The four blocks are inverse-variance weighted, Frobenius-stacked, PCA-reduced to
32
32
 
33
33
  ```
34
34
  code/
35
- ├── SampleDisc.py # CLI entry point (simple or complex mode)
36
- ├── config/ # 9 YAML configs covering covid / blood / eye / heart / ENCODE / tabula / long_covid / unpaired / default
37
- ├── wrapper/ # Orchestration
38
- │ ├── wrapper.py # Master wrapper; gates RNA + ATAC + multiomics + shared downstream
39
- │ ├── rna_wrapper.py
40
- │ ├── atac_wrapper.py
41
- │ └── multiomics_wrapper.py
42
- ├── preparation/ # Preprocessing
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
- │ ├── ATAC_cell_type{,_gpu}.py # ATAC-specific cell typing variants
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_comp
49
- │ ├── multi_omics_merge.py # post-GLUE merge + per-modality preprocess/slimming
50
- │ └── multi_omics_cell_type_{cpu,gpu}.py # RNA-Leiden + k-NN label transfer to ATAC
51
- ├── sample_embedding/ # Core method
52
- │ ├── blocks.py # composition, RMD, weighting, Frobenius stack, final PCA + Harmony
53
- │ ├── sample_embedding.py # CPU pipeline
54
- │ └── sample_embedding_gpu.py # GPU pipeline (cuML + cupy)
55
- ├── parameter_selection/
56
- │ └── autotune.py # Bayesian GP sweep over RMD α; adaptive proxy ensemble
57
- ├── sample_distance/ # Pairwise sample distances (DR / EMD / chi-square / JS)
58
- ├── sample_clustering/ # Hierarchical (HRA / HRC / NN / UPGMA / consensus), K-means, proportion test, RAISIN
59
- ├── sample_trajectory/ # CCA (supervised) and TSCAN (unsupervised) + GAM-based trajectory DGE
60
- ├── sample_association/ # Per-PC variance explained vs sample-level covariates (permutation FDR)
61
- ├── visualization/ # Embedding plots, dendrograms, DGE volcanos, modality-aware multi-omics scatters
62
- ├── utils/ # Shared helpers: seed, safe h5ad I/O, limma, TF-IDF, batch regress, Grouping
63
- ├── gene_activity/ # ATAC peak → gene activity inference + RNA-ATAC validation
64
- └── claude/ # Active one-off run scripts (rerun launchers, monitored SE, parameter sweeps)
35
+ ├── pyproject.toml # package metadata; `pip install -e .` installs the `sampledisco` command
36
+ ├── config/ # 10 YAML configs for the paper's datasets: covid / blood (1M-scBloodNL) / eye / heart / ENCODE / healthy-aging PBMC / long_covid / unpaired (+ RNA-tuned) / default
37
+ ├── src/sampledisco/ # the package
38
+ │ ├── cli.py # `sampledisco` command: `-m complex --config <yaml>`, `--init-config <path>`
39
+ │ ├── config/config_demo.yaml # demo config written by `sampledisco --init-config`
40
+ │ ├── wrapper/ # Orchestration
41
+ │ │ ├── wrapper.py # Master wrapper; gates RNA + ATAC + multiomics + shared downstream
42
+ │ │ ├── rna_wrapper.py
43
+ │ │ ├── atac_wrapper.py
44
+ │ │ └── multiomics_wrapper.py
45
+ │ ├── preparation/ # Preprocessing
46
+ │ │ ├── rna_preprocess_{cpu,gpu}.py # QC → HVG → PCA → dual Harmony → Z_comp + Z_rmd
47
+ │ │ ├── atac_preprocess_{cpu,gpu}.py # QC → TF-IDF → HVF → LSI → dual Harmony → Z_comp + Z_rmd
48
+ │ │ ├── cell_type_{cpu,gpu}.py # Leiden clustering on Z_comp (RNA or ATAC)
49
+ │ │ ├── ATAC_cell_type{,_gpu}.py # ATAC-specific cell typing variants
50
+ │ │ ├── multi_omics_glue.py # scGLUE integration (cross-modality VAE + guidance graph)
51
+ │ │ ├── multi_omics_batch_correction.py # Harmony post-pass on X_glue → Z_comp
52
+ │ │ ├── multi_omics_merge.py # post-GLUE merge + per-modality preprocess/slimming
53
+ │ │ └── multi_omics_cell_type_{cpu,gpu}.py # RNA-Leiden + k-NN label transfer to ATAC
54
+ │ ├── sample_embedding/ # Core method
55
+ │ │ ├── blocks.py # composition, RMD, weighting, Frobenius stack, final PCA + Harmony
56
+ │ │ ├── sample_embedding.py # CPU pipeline
57
+ │ │ └── sample_embedding_gpu.py # GPU pipeline (cuML + cupy)
58
+ │ ├── parameter_selection/
59
+ │ │ └── autotune.py # Bayesian GP sweep over RMD α; adaptive proxy ensemble
60
+ │ ├── sample_distance/ # Pairwise sample distances (DR / EMD / chi-square / JS)
61
+ │ ├── sample_clustering/ # Hierarchical (HRA / HRC / NN / UPGMA / consensus), K-means, proportion test, RAISIN
62
+ │ ├── sample_trajectory/ # CCA (supervised) and TSCAN (unsupervised) + GAM-based trajectory DGE
63
+ │ ├── sample_association/ # Per-PC variance explained vs sample-level covariates (permutation FDR)
64
+ │ ├── visualization/ # Embedding plots, dendrograms, DGE volcanos, modality-aware multi-omics scatters
65
+ │ ├── utils/ # Shared helpers: seed, safe h5ad I/O, limma, TF-IDF, batch regress, Grouping
66
+ │ └── gene_activity/ # ATAC peak → gene activity inference + RNA-ATAC validation
67
+ ├── Benchmark_covid/, Benchmark_multiomics/ # benchmark drivers used for the paper
68
+ └── claude/ # one-off run scripts (rerun launchers, monitored runs, parameter sweeps)
65
69
  ```
66
70
 
67
71
  ---
68
72
 
69
73
  ## Usage
70
74
 
71
- ### Complex mode (recommended) — YAML-driven
75
+ ### Run — YAML-driven
72
76
 
73
77
  ```bash
74
- python SampleDisc.py -m complex --config config/config.yaml
78
+ sampledisco -m complex --config config/config.yaml # same as: python -m sampledisco.cli -m complex --config ...
75
79
  ```
76
80
 
77
81
  The YAML drives every flag and parameter for all three pipelines:
@@ -81,14 +85,16 @@ The YAML drives every flag and parameter for all three pipelines:
81
85
  - **Per-modality downstream gates** (Phase 2): `*_sample_distance_calculation`, `*_trajectory_analysis`, `*_trajectory_dge`, `*_sample_cluster`, `*_proportion_test`, `*_cluster_dge`, `*_visualize_data`, `*_dimension_association_analysis`
82
86
  - **Multi-omics-specific**: `multiomics_run_glue_*`, `multiomics_treat_sample_as_batch`, `multiomics_run_glue_twice_for_sample_removal`
83
87
 
84
- The 9 ready-to-use configs in `config/` are point-in-time snapshots for the datasets used in the paper; copy one and adjust paths / column names for your own data.
88
+ The 10 ready-to-use configs in `config/` are point-in-time snapshots for the datasets used in the paper; copy one and adjust paths / column names for your own data.
85
89
 
86
- ### Simple mode — one positional file, defaults everywhere
90
+ ### Starter config
87
91
 
88
92
  ```bash
89
- python SampleDisc.py -m simple -c <count_data.h5ad> -o <output_dir>
93
+ sampledisco --init-config my_config.yaml
90
94
  ```
91
95
 
96
+ writes the demo config (tuned for the Zenodo demo dataset) to `my_config.yaml`. The YAML is validated against every `wrapper()` parameter, so edit values rather than deleting keys.
97
+
92
98
  ---
93
99
 
94
100
  ## Inputs
@@ -135,7 +141,7 @@ detects and uses them at runtime. There is no separate "GPU build" of SampleDisc
135
141
  ### 1. Core install (CPU)
136
142
 
137
143
  ```bash
138
- pip install sampledisco # once published — or `pip install -e .` from a clone
144
+ pip install sampledisco # or `pip install -e .` from a clone
139
145
  ```
140
146
 
141
147
  ### 2. System prerequisite — bedtools
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "sampledisco"
12
- version = "0.3.0"
12
+ version = "0.4.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"
@@ -1,5 +1,5 @@
1
- # SampleDisco core (CPU) dependencies — no PyTorch.
2
- # The deep-learning stack (torch, scglue, harmony-pytorch) and RAPIDS GPU
1
+ # SampleDisco core (CPU) dependencies. PyTorch is installed as a dependency of harmonypy 0.2.0.
2
+ # The deep-learning stack (scglue, harmony-pytorch, a CUDA build of torch) and RAPIDS GPU
3
3
  # acceleration are NOT installed here — they're OS/driver-specific and you
4
4
  # install them yourself (see the Installation page). SampleDisco falls back to
5
5
  # the CPU path when they are absent.
@@ -14,7 +14,7 @@ patsy>=0.5
14
14
  scanpy>=1.11
15
15
  anndata>=0.10
16
16
  muon>=0.1
17
- harmonypy>=0.0.9,<0.0.10 # pin to the pure-NumPy release; harmonypy 0.0.10+ imports torch
17
+ harmonypy==0.2.0 # the version used for the published results; it requires torch
18
18
  python-igraph>=0.10
19
19
  leidenalg>=0.9
20
20
  networkx>=3.0
@@ -35,14 +35,27 @@ def load_config(config_path):
35
35
  return yaml.safe_load(f)
36
36
 
37
37
 
38
+ # Keys added after 0.2.0. Configs written before them may omit these keys, which
39
+ # then take their wrapper() default; every other key stays required.
40
+ OPTIONAL_KEYS = frozenset(f"{m}_save_cell_adata_after_embedding"
41
+ for m in ("rna", "atac", "multiomics"))
42
+
43
+
38
44
  def validate_config(config, func):
39
45
  valid_params = inspect.signature(func).parameters
40
46
  for key in config:
41
47
  if key not in valid_params:
42
48
  raise ValueError(f"Unexpected parameter in config: '{key}'")
43
49
  for key in valid_params:
44
- if key not in config:
50
+ if key not in config and key not in OPTIONAL_KEYS:
45
51
  raise ValueError(f"Missing required parameter in config: '{key}'")
52
+ from sampledisco.sample_embedding.blocks import check_rmd_weight
53
+ for key in config:
54
+ if key.endswith("_sample_embedding_rmd_weight"):
55
+ try:
56
+ check_rmd_weight(config[key])
57
+ except ValueError as e:
58
+ raise ValueError(f"Invalid value for '{key}': {e}") from None
46
59
 
47
60
 
48
61
  def write_template(dest):
@@ -86,9 +86,10 @@ rna_sample_embedding_rmd_dim: 8
86
86
  rna_sample_embedding_use_clr: false
87
87
  rna_sample_embedding_use_rmd: true
88
88
  rna_sample_embedding_block_weights:
89
- rna_sample_embedding_rmd_weight: 0.60
89
+ rna_sample_embedding_rmd_weight: "equal" # RMD block weight α: "equal" = same energy as the three composition blocks together (α² = w_A1² + w_A2² + w_A3²); a number fixes α (0.60 = previous default)
90
90
  rna_sample_embedding_pca_components: 10
91
91
  rna_sample_embedding_batch_method: "harmony"
92
+ rna_save_cell_adata_after_embedding: true # store .uns['X_DR_sample'] in the cell-level h5ad (updated in place; false = keep only the CSV)
92
93
 
93
94
  # --- Autotune Parameters ---
94
95
  rna_autotune_enable: false
@@ -207,9 +208,10 @@ atac_sample_embedding_rmd_dim: 8
207
208
  atac_sample_embedding_use_clr: false
208
209
  atac_sample_embedding_use_rmd: true
209
210
  atac_sample_embedding_block_weights:
210
- atac_sample_embedding_rmd_weight: 0.60
211
+ atac_sample_embedding_rmd_weight: "equal" # RMD block weight α: "equal" = same energy as the three composition blocks together (α² = w_A1² + w_A2² + w_A3²); a number fixes α (0.60 = previous default)
211
212
  atac_sample_embedding_pca_components: 10
212
213
  atac_sample_embedding_batch_method: "harmony"
214
+ atac_save_cell_adata_after_embedding: true # store .uns['X_DR_sample'] in the cell-level h5ad (updated in place; false = keep only the CSV)
213
215
 
214
216
  # --- Autotune Parameters ---
215
217
  atac_autotune_enable: false
@@ -375,9 +377,10 @@ multiomics_sample_embedding_rmd_dim: 8
375
377
  multiomics_sample_embedding_use_clr: false
376
378
  multiomics_sample_embedding_use_rmd: true
377
379
  multiomics_sample_embedding_block_weights:
378
- multiomics_sample_embedding_rmd_weight: 0.60
380
+ multiomics_sample_embedding_rmd_weight: "equal" # RMD block weight α: "equal" = same energy as the three composition blocks together (α² = w_A1² + w_A2² + w_A3²); a number fixes α (0.60 = previous default)
379
381
  multiomics_sample_embedding_pca_components: 10
380
382
  multiomics_sample_embedding_batch_method: "harmony"
383
+ multiomics_save_cell_adata_after_embedding: true # store .uns['X_DR_sample'] in the cell-level h5ad (updated in place; false = keep only the CSV)
381
384
 
382
385
  # --- Autotune Parameters ---
383
386
  multiomics_autotune_enable: false
@@ -25,7 +25,6 @@ from typing import Callable, Dict, List, Optional, Tuple, Union
25
25
 
26
26
  import numpy as np
27
27
  import pandas as pd
28
- import scanpy as sc
29
28
  from anndata import AnnData
30
29
  from sklearn.cluster import MiniBatchKMeans
31
30
  from sklearn.cross_decomposition import CCA
@@ -44,7 +43,10 @@ from sampledisco.sample_embedding.blocks import (
44
43
  composition_per_unit,
45
44
  derive_weights,
46
45
  loo_rmd,
46
+ resolve_rmd_weight,
47
+ save_embedding_to_h5ad,
47
48
  soft_assign,
49
+ warn_if_stale_embedding,
48
50
  )
49
51
  from sampledisco.utils.embedding_keys import resolve_comp_key, resolve_rmd_key
50
52
 
@@ -606,6 +608,7 @@ def run_autotune(
606
608
  verbose: bool = True,
607
609
  tune_on_modality: Optional[str] = None,
608
610
  cluster_emb_key: Optional[str] = None,
611
+ save_cell_adata: bool = True,
609
612
  ) -> Dict:
610
613
  """Run autotune and return the best params + final sample-AnnData.
611
614
 
@@ -660,11 +663,12 @@ def run_autotune(
660
663
  f"scoring on {int(score_mask.sum())}/{blocks['n_units']} units")
661
664
 
662
665
  if not score_meta["has_batch"] and not score_meta["has_grouping"]:
666
+ alpha = resolve_rmd_weight("equal", blocks["K_c"], blocks["K_med"], blocks["K_fine"])
663
667
  if verbose:
664
- print("[autotune] no batch and no grouping column → using fixed defaults; "
665
- "no search performed.")
668
+ print(f"[autotune] no batch and no grouping column → using the default "
669
+ f"rmd_weight='equal' (α={alpha}); no search performed.")
666
670
  weights = derive_weights(blocks["K_c"], blocks["K_med"], blocks["K_fine"],
667
- rmd_weight=0.60, n_blocks=4)
671
+ rmd_weight=alpha, n_blocks=4)
668
672
  final_emb = build_emb_from_blocks(
669
673
  [blocks["A1"], blocks["A2"], blocks["A3"], blocks["RMD"]],
670
674
  weights,
@@ -675,13 +679,13 @@ def run_autotune(
675
679
  seed=seed, verbose=verbose,
676
680
  )
677
681
  return _finalize(adata, blocks, final_emb, weights,
678
- best_params={"rmd_weight": 0.60},
682
+ best_params={"rmd_weight": alpha},
679
683
  best_score=float("nan"),
680
684
  trace=[], search=search, scoring=scoring,
681
685
  scope=scope, alpha_bounds=alpha_bounds,
682
686
  pca_components=pca_components,
683
687
  batch_method=batch_method,
684
- output_dir=output_dir, save=save,
688
+ output_dir=output_dir, save=save, save_cell_adata=save_cell_adata,
685
689
  t_start=t0, verbose=verbose,
686
690
  tune_on_modality=tune_on_modality,
687
691
  score_n_units=int(score_mask.sum()))
@@ -737,7 +741,7 @@ def run_autotune(
737
741
  scope=scope, alpha_bounds=alpha_bounds,
738
742
  pca_components=pca_components,
739
743
  batch_method=batch_method,
740
- output_dir=output_dir, save=save,
744
+ output_dir=output_dir, save=save, save_cell_adata=save_cell_adata,
741
745
  t_start=t0, verbose=verbose,
742
746
  tune_on_modality=tune_on_modality,
743
747
  score_n_units=int(score_mask.sum()))
@@ -869,7 +873,7 @@ def _finalize(adata, blocks, final_emb, weights, *,
869
873
  search, scoring, scope, alpha_bounds,
870
874
  pca_components, batch_method,
871
875
  output_dir, save, t_start, verbose,
872
- tune_on_modality=None, score_n_units=None):
876
+ tune_on_modality=None, score_n_units=None, save_cell_adata=True):
873
877
  """Write the autotuned embedding into the cell-level adata and persist artifacts."""
874
878
  elapsed = time.time() - t_start
875
879
  adata.uns["X_DR_sample"] = final_emb.copy()
@@ -905,9 +909,11 @@ def _finalize(adata, blocks, final_emb, weights, *,
905
909
  final_emb.to_csv(emb_csv)
906
910
 
907
911
  preprocessed_h5 = os.path.join(output_dir, "preprocess", "adata_preprocessed.h5ad")
908
- if os.path.exists(preprocessed_h5):
912
+ if not save_cell_adata:
913
+ warn_if_stale_embedding(preprocessed_h5, emb_csv)
914
+ if save_cell_adata and os.path.exists(preprocessed_h5):
909
915
  try:
910
- sc.write(preprocessed_h5, adata)
916
+ save_embedding_to_h5ad(preprocessed_h5, adata)
911
917
  except Exception as exc:
912
918
  if verbose:
913
919
  print(f"[autotune] WARNING: could not re-save "
@@ -94,20 +94,21 @@ def cell_types(
94
94
  if verbose:
95
95
  print(f"{indent}[cell_types] Found {num_clusters_found} clusters")
96
96
 
97
+ # Recursive results fall through (no early return) so the depth-0
98
+ # call still runs its UMAP / CSV / save block below.
99
+ new_resolution = leiden_cluster_resolution + RESOLUTION_STEP
97
100
  if num_clusters_found >= n_target_clusters:
98
101
  if num_clusters_found > n_target_clusters and verbose:
99
102
  print(f"{indent}[cell_types] Over-shot target; recursing with dendrogram aggregation")
100
103
 
101
- return cell_types(
104
+ adata = cell_types(
102
105
  anndata_cell=adata, cell_type_column="cell_type",
103
106
  existing_cell_types=True, n_target_clusters=n_target_clusters, umap=False, save=False,
104
107
  cell_embedding_column=cell_embedding_column, cell_embedding_num_PCs=cell_embedding_num_PCs,
105
108
  verbose=verbose, umap_plots=False, _recursion_depth=_recursion_depth + 1,
106
109
  )
107
-
108
- new_resolution = leiden_cluster_resolution + RESOLUTION_STEP
109
- if new_resolution <= MAX_RESOLUTION:
110
- return cell_types(
110
+ elif new_resolution <= MAX_RESOLUTION:
111
+ adata = cell_types(
111
112
  anndata_cell=adata, cell_type_column=cell_type_column,
112
113
  existing_cell_types=False, n_target_clusters=n_target_clusters, umap=False, save=False,
113
114
  leiden_cluster_resolution=new_resolution, cell_embedding_column=cell_embedding_column,
@@ -98,20 +98,21 @@ def cell_types_gpu(
98
98
  if verbose:
99
99
  print(f"{indent}[cell_types] Found {num_clusters_found} clusters")
100
100
 
101
+ # Recursive results fall through (no early return) so the
102
+ # depth-0 call still runs its UMAP / CSV / save block below.
103
+ new_resolution = leiden_cluster_resolution + RESOLUTION_STEP
101
104
  if num_clusters_found >= n_target_clusters:
102
105
  if num_clusters_found > n_target_clusters and verbose:
103
106
  print(f"{indent}[cell_types] Over-shot target; recursing with dendrogram aggregation")
104
107
 
105
- return cell_types_gpu(
108
+ adata = cell_types_gpu(
106
109
  anndata_cell=adata, cell_type_column="cell_type",
107
110
  existing_cell_types=True, n_target_clusters=n_target_clusters, umap=False, save=False,
108
111
  cell_embedding_column=cell_embedding_column, cell_embedding_num_PCs=cell_embedding_num_PCs,
109
112
  verbose=verbose, umap_plots=False, _recursion_depth=_recursion_depth + 1,
110
113
  )
111
-
112
- new_resolution = leiden_cluster_resolution + RESOLUTION_STEP
113
- if new_resolution <= MAX_RESOLUTION:
114
- return cell_types_gpu(
114
+ elif new_resolution <= MAX_RESOLUTION:
115
+ adata = cell_types_gpu(
115
116
  anndata_cell=adata, cell_type_column=cell_type_column,
116
117
  existing_cell_types=False, n_target_clusters=n_target_clusters, umap=False, save=False,
117
118
  leiden_cluster_resolution=new_resolution, cell_embedding_column=cell_embedding_column,
@@ -37,13 +37,14 @@ def compute_sample_embedding(
37
37
  use_clr: bool = False,
38
38
  use_rmd: bool = True,
39
39
  block_weights: Optional[List[float]] = None,
40
- rmd_weight: float = 0.60,
40
+ rmd_weight: Union[float, str] = "equal",
41
41
  pca_components: int = 10,
42
42
  batch_method: str = "harmony",
43
43
  save: bool = True,
44
44
  verbose: bool = True,
45
45
  seed: int = 42,
46
46
  cluster_emb_key: Optional[str] = None,
47
+ save_cell_adata: bool = True,
47
48
  ) -> AnnData:
48
49
  """Dispatch to the CPU or GPU implementation.
49
50
 
@@ -53,6 +54,8 @@ def compute_sample_embedding(
53
54
  around the module import is not enough (the import succeeds and the crash
54
55
  lands mid-run). We probe for the stack up front and also guard the call.
55
56
  """
57
+ from sampledisco.sample_embedding.blocks import check_rmd_weight
58
+ check_rmd_weight(rmd_weight) # before the GPU attempt, so a bad value is not reported as "GPU unavailable"
56
59
  kwargs = dict(
57
60
  sample_col=sample_col,
58
61
  celltype_col=celltype_col,
@@ -73,6 +76,7 @@ def compute_sample_embedding(
73
76
  save=save,
74
77
  verbose=verbose,
75
78
  seed=seed,
79
+ save_cell_adata=save_cell_adata,
76
80
  )
77
81
 
78
82
  if use_gpu and not _gpu_stack_available():