bsvae 0.3.1__tar.gz → 0.3.2__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 (41) hide show
  1. bsvae-0.3.2/PKG-INFO +224 -0
  2. bsvae-0.3.2/README.md +192 -0
  3. {bsvae-0.3.1 → bsvae-0.3.2}/pyproject.toml +1 -1
  4. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/cli/networks.py +59 -0
  5. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/cli/sweep_k.py +1 -1
  6. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/cli/train.py +76 -5
  7. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/models/gmm_prior.py +5 -0
  8. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/models/gmvae.py +20 -4
  9. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/models/losses.py +265 -85
  10. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/networks/extract_networks.py +13 -7
  11. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/networks/module_extraction.py +4 -8
  12. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/simulation/scenario.py +61 -6
  13. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/utils/modelIO.py +5 -4
  14. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/utils/training.py +12 -3
  15. bsvae-0.3.1/PKG-INFO +0 -204
  16. bsvae-0.3.1/README.md +0 -172
  17. {bsvae-0.3.1 → bsvae-0.3.2}/LICENSE +0 -0
  18. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/__init__.py +0 -0
  19. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/cli/__init__.py +0 -0
  20. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/cli/simulate.py +0 -0
  21. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/hyperparam.ini +0 -0
  22. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/latent/__init__.py +0 -0
  23. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/latent/latent_analysis.py +0 -0
  24. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/latent/latent_export.py +0 -0
  25. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/main.py +0 -0
  26. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/models/__init__.py +0 -0
  27. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/models/decoder.py +0 -0
  28. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/models/encoder.py +0 -0
  29. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/models/vae.py +0 -0
  30. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/networks/__init__.py +0 -0
  31. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/networks/cli.py +0 -0
  32. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/networks/utils.py +0 -0
  33. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/simulation/__init__.py +0 -0
  34. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/simulation/generate.py +0 -0
  35. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/simulation/metrics.py +0 -0
  36. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/utils/__init__.py +0 -0
  37. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/utils/datasets.py +0 -0
  38. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/utils/evaluate.py +0 -0
  39. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/utils/helpers.py +0 -0
  40. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/utils/hierarchy.py +0 -0
  41. {bsvae-0.3.1 → bsvae-0.3.2}/src/bsvae/utils/initialization.py +0 -0
bsvae-0.3.2/PKG-INFO ADDED
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.4
2
+ Name: bsvae
3
+ Version: 0.3.2
4
+ Summary: Gaussian Mixture VAE for multi-modal biological module discovery in omics data.
5
+ License: GPL-3.0-only
6
+ License-File: LICENSE
7
+ Keywords: variational-autoencoder,gaussian-mixture-model,pytorch,bioinformatics,gene-expression,module-discovery,omics
8
+ Author: Kynon J Benjamin
9
+ Author-email: kj.benjamin90@gmail.com
10
+ Requires-Python: >=3.11,<3.14
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
19
+ Requires-Dist: faiss-cpu (>=1.7)
20
+ Requires-Dist: h5py (>=3.0)
21
+ Requires-Dist: numpy (<2.3)
22
+ Requires-Dist: pandas (>=2.3.2,<3.0.0)
23
+ Requires-Dist: scikit-learn (>=1.7.2,<2.0.0)
24
+ Requires-Dist: scipy (>=1.16.2,<2.0.0)
25
+ Requires-Dist: torch (>=2.8.0,<3.0.0)
26
+ Requires-Dist: tqdm (>=4.67.1,<5.0.0)
27
+ Project-URL: Bug Tracker, https://github.com/heart-gen/BSVAE/issues
28
+ Project-URL: homepage, https://bsvae.readthedocs.io/en/latest/
29
+ Project-URL: repository, https://github.com/heart-gen/BSVAE
30
+ Description-Content-Type: text/markdown
31
+
32
+ # BSVAE: Gaussian Mixture VAE for Module Discovery
33
+
34
+ [![PyTorch](https://img.shields.io/badge/PyTorch-%3E%3D2.8-orange)](https://pytorch.org/)
35
+ [![Python](https://img.shields.io/badge/Python-%3E%3D3.11-blue)](https://python.org/)
36
+ [![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](LICENSE)
37
+ [![Documentation](https://readthedocs.org/projects/bsvae/badge/?version=latest)](https://bsvae.readthedocs.io/)
38
+
39
+ BSVAE is a PyTorch package for feature-level module discovery in omics data. The main model, `GMMModuleVAE`, learns latent structure across features such as genes, transcripts, or proteins, then supports downstream network extraction, module assignment, latent analysis, and simulation-based benchmarking.
40
+
41
+ ## What The Package Provides
42
+
43
+ - `bsvae-train` for training a two-phase GMM-VAE
44
+ - `bsvae-sweep-k` for selecting the number of modules (`K`) with held-out validation and optional stability replicates
45
+ - `bsvae-networks` for post-training network extraction, module extraction, latent export, and latent analysis
46
+ - `bsvae-simulate` for synthetic data generation, scenario-grid simulation, and module-recovery benchmarking
47
+
48
+ ## Installation
49
+
50
+ Install from PyPI:
51
+
52
+ ```bash
53
+ pip install bsvae
54
+ ```
55
+
56
+ Install from source:
57
+
58
+ ```bash
59
+ git clone https://github.com/heart-gen/BSVAE.git
60
+ cd BSVAE
61
+ pip install -e .
62
+ ```
63
+
64
+ Optional dependency:
65
+
66
+ - `anndata` is only needed for `.h5ad` input or `.h5ad` latent export helpers
67
+
68
+ ## Input Data Contract
69
+
70
+ Training and most downstream commands expect an expression matrix in `features x samples` orientation:
71
+
72
+ - rows are features such as genes, transcripts, or proteins
73
+ - columns are sample IDs
74
+ - the first column is the feature index when using CSV/TSV
75
+
76
+ Supported loaders:
77
+
78
+ - `.csv` / `.csv.gz`
79
+ - `.tsv` / `.tsv.gz`
80
+ - `.h5` / `.hdf5`
81
+ - `.h5ad` with optional `anndata`
82
+
83
+ ## CLI Entry Points
84
+
85
+ - `bsvae-train`
86
+ - `bsvae-sweep-k`
87
+ - `bsvae-networks`
88
+ - `bsvae-simulate`
89
+
90
+ ## Quickstart
91
+
92
+ Train a small model:
93
+
94
+ ```bash
95
+ bsvae-train pilot_run \
96
+ --dataset data/expression.csv \
97
+ --epochs 50 \
98
+ --n-modules 12 \
99
+ --latent-dim 16
100
+ ```
101
+
102
+ Recommended production flow: select `K` first, then use the retrained final model.
103
+
104
+ ```bash
105
+ bsvae-sweep-k sweep1 \
106
+ --dataset data/expression.csv \
107
+ --k-grid 8,12,16,24,32 \
108
+ --sweep-epochs 60 \
109
+ --stability-reps 5 \
110
+ --val-frac 0.1
111
+ ```
112
+
113
+ This writes sweep outputs under `results/sweep1/sweep_k/` and retrains the selected model under `results/sweep1/final_k<K>/`.
114
+
115
+ Extract module assignments from the final model:
116
+
117
+ ```bash
118
+ bsvae-networks extract-modules \
119
+ --model-path results/sweep1/final_k16 \
120
+ --dataset data/expression.csv \
121
+ --output-dir results/sweep1/final_k16/modules \
122
+ --expr data/expression.csv \
123
+ --soft-eigengenes
124
+ ```
125
+
126
+ Export latents:
127
+
128
+ ```bash
129
+ bsvae-networks export-latents \
130
+ --model-path results/sweep1/final_k16 \
131
+ --dataset data/expression.csv \
132
+ --output results/sweep1/final_k16/latents
133
+ ```
134
+
135
+ This command writes `results/sweep1/final_k16/latents.npz` containing `mu`, `logvar`, `gamma`, and `feature_ids`.
136
+
137
+ Extract feature-feature networks:
138
+
139
+ ```bash
140
+ bsvae-networks extract-networks \
141
+ --model-path results/sweep1/final_k16 \
142
+ --dataset data/expression.csv \
143
+ --output-dir results/sweep1/final_k16/networks \
144
+ --methods mu_cosine gamma_knn \
145
+ --top-k 50
146
+ ```
147
+
148
+ Run latent-space analysis:
149
+
150
+ ```bash
151
+ bsvae-networks latent-analysis \
152
+ --model-path results/sweep1/final_k16 \
153
+ --dataset data/expression.csv \
154
+ --output-dir results/sweep1/final_k16/latent_analysis \
155
+ --kmeans-k 16 \
156
+ --umap
157
+ ```
158
+
159
+ ## Training Outputs
160
+
161
+ `bsvae-train` writes to `results/<experiment>/` by default:
162
+
163
+ - `model.pt`
164
+ - `specs.json`
165
+ - `train_losses.csv`
166
+ - `model-<epoch>.pt` checkpoints when `--checkpoint-every` is greater than zero
167
+
168
+ `bsvae-sweep-k` writes:
169
+
170
+ - `results/<name>/sweep_k/sweep_results.csv`
171
+ - `results/<name>/sweep_k/sweep_summary.json`
172
+ - `results/<name>/sweep_k/k<K>/rep_<rep>/...` per-sweep run outputs
173
+ - `results/<name>/final_k<K>/...` for the final retrained model when `--train-final` is enabled
174
+
175
+ ## Simulation and Benchmarking
176
+
177
+ Generate one synthetic dataset:
178
+
179
+ ```bash
180
+ bsvae-simulate generate \
181
+ --output data/sim_expr.csv \
182
+ --save-ground-truth data/sim_truth.csv
183
+ ```
184
+
185
+ Generate a scenario grid:
186
+
187
+ ```bash
188
+ bsvae-simulate init-config --output sim.yaml
189
+
190
+ bsvae-simulate generate-grid \
191
+ --config sim.yaml \
192
+ --outdir results/sim_pub_v1 \
193
+ --reps 30 \
194
+ --base-seed 13
195
+ ```
196
+
197
+ Validate the generated grid:
198
+
199
+ ```bash
200
+ bsvae-simulate validate-grid --grid-dir results/sim_pub_v1
201
+ ```
202
+
203
+ Each scenario replicate writes method-ready files under `results/sim_pub_v1/scenarios/<scenario_id>/rep_<rep>/`, including:
204
+
205
+ - `expr/features_x_samples.tsv.gz`
206
+ - `expr/samples_x_features.tsv.gz`
207
+ - `truth/modules_hard.csv`
208
+ - `method_inputs.json`
209
+
210
+ ## Documentation
211
+
212
+ See the docs for the full workflow and command reference:
213
+
214
+ - `docs/index.md`
215
+ - `docs/quickstart.md`
216
+ - `docs/tutorial.md`
217
+ - `docs/cli.md`
218
+ - `docs/networks.md`
219
+ - `docs/hyperparameters.md`
220
+
221
+ ## License
222
+
223
+ This project is licensed under the [GNU General Public License v3.0](LICENSE).
224
+
bsvae-0.3.2/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # BSVAE: Gaussian Mixture VAE for Module Discovery
2
+
3
+ [![PyTorch](https://img.shields.io/badge/PyTorch-%3E%3D2.8-orange)](https://pytorch.org/)
4
+ [![Python](https://img.shields.io/badge/Python-%3E%3D3.11-blue)](https://python.org/)
5
+ [![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](LICENSE)
6
+ [![Documentation](https://readthedocs.org/projects/bsvae/badge/?version=latest)](https://bsvae.readthedocs.io/)
7
+
8
+ BSVAE is a PyTorch package for feature-level module discovery in omics data. The main model, `GMMModuleVAE`, learns latent structure across features such as genes, transcripts, or proteins, then supports downstream network extraction, module assignment, latent analysis, and simulation-based benchmarking.
9
+
10
+ ## What The Package Provides
11
+
12
+ - `bsvae-train` for training a two-phase GMM-VAE
13
+ - `bsvae-sweep-k` for selecting the number of modules (`K`) with held-out validation and optional stability replicates
14
+ - `bsvae-networks` for post-training network extraction, module extraction, latent export, and latent analysis
15
+ - `bsvae-simulate` for synthetic data generation, scenario-grid simulation, and module-recovery benchmarking
16
+
17
+ ## Installation
18
+
19
+ Install from PyPI:
20
+
21
+ ```bash
22
+ pip install bsvae
23
+ ```
24
+
25
+ Install from source:
26
+
27
+ ```bash
28
+ git clone https://github.com/heart-gen/BSVAE.git
29
+ cd BSVAE
30
+ pip install -e .
31
+ ```
32
+
33
+ Optional dependency:
34
+
35
+ - `anndata` is only needed for `.h5ad` input or `.h5ad` latent export helpers
36
+
37
+ ## Input Data Contract
38
+
39
+ Training and most downstream commands expect an expression matrix in `features x samples` orientation:
40
+
41
+ - rows are features such as genes, transcripts, or proteins
42
+ - columns are sample IDs
43
+ - the first column is the feature index when using CSV/TSV
44
+
45
+ Supported loaders:
46
+
47
+ - `.csv` / `.csv.gz`
48
+ - `.tsv` / `.tsv.gz`
49
+ - `.h5` / `.hdf5`
50
+ - `.h5ad` with optional `anndata`
51
+
52
+ ## CLI Entry Points
53
+
54
+ - `bsvae-train`
55
+ - `bsvae-sweep-k`
56
+ - `bsvae-networks`
57
+ - `bsvae-simulate`
58
+
59
+ ## Quickstart
60
+
61
+ Train a small model:
62
+
63
+ ```bash
64
+ bsvae-train pilot_run \
65
+ --dataset data/expression.csv \
66
+ --epochs 50 \
67
+ --n-modules 12 \
68
+ --latent-dim 16
69
+ ```
70
+
71
+ Recommended production flow: select `K` first, then use the retrained final model.
72
+
73
+ ```bash
74
+ bsvae-sweep-k sweep1 \
75
+ --dataset data/expression.csv \
76
+ --k-grid 8,12,16,24,32 \
77
+ --sweep-epochs 60 \
78
+ --stability-reps 5 \
79
+ --val-frac 0.1
80
+ ```
81
+
82
+ This writes sweep outputs under `results/sweep1/sweep_k/` and retrains the selected model under `results/sweep1/final_k<K>/`.
83
+
84
+ Extract module assignments from the final model:
85
+
86
+ ```bash
87
+ bsvae-networks extract-modules \
88
+ --model-path results/sweep1/final_k16 \
89
+ --dataset data/expression.csv \
90
+ --output-dir results/sweep1/final_k16/modules \
91
+ --expr data/expression.csv \
92
+ --soft-eigengenes
93
+ ```
94
+
95
+ Export latents:
96
+
97
+ ```bash
98
+ bsvae-networks export-latents \
99
+ --model-path results/sweep1/final_k16 \
100
+ --dataset data/expression.csv \
101
+ --output results/sweep1/final_k16/latents
102
+ ```
103
+
104
+ This command writes `results/sweep1/final_k16/latents.npz` containing `mu`, `logvar`, `gamma`, and `feature_ids`.
105
+
106
+ Extract feature-feature networks:
107
+
108
+ ```bash
109
+ bsvae-networks extract-networks \
110
+ --model-path results/sweep1/final_k16 \
111
+ --dataset data/expression.csv \
112
+ --output-dir results/sweep1/final_k16/networks \
113
+ --methods mu_cosine gamma_knn \
114
+ --top-k 50
115
+ ```
116
+
117
+ Run latent-space analysis:
118
+
119
+ ```bash
120
+ bsvae-networks latent-analysis \
121
+ --model-path results/sweep1/final_k16 \
122
+ --dataset data/expression.csv \
123
+ --output-dir results/sweep1/final_k16/latent_analysis \
124
+ --kmeans-k 16 \
125
+ --umap
126
+ ```
127
+
128
+ ## Training Outputs
129
+
130
+ `bsvae-train` writes to `results/<experiment>/` by default:
131
+
132
+ - `model.pt`
133
+ - `specs.json`
134
+ - `train_losses.csv`
135
+ - `model-<epoch>.pt` checkpoints when `--checkpoint-every` is greater than zero
136
+
137
+ `bsvae-sweep-k` writes:
138
+
139
+ - `results/<name>/sweep_k/sweep_results.csv`
140
+ - `results/<name>/sweep_k/sweep_summary.json`
141
+ - `results/<name>/sweep_k/k<K>/rep_<rep>/...` per-sweep run outputs
142
+ - `results/<name>/final_k<K>/...` for the final retrained model when `--train-final` is enabled
143
+
144
+ ## Simulation and Benchmarking
145
+
146
+ Generate one synthetic dataset:
147
+
148
+ ```bash
149
+ bsvae-simulate generate \
150
+ --output data/sim_expr.csv \
151
+ --save-ground-truth data/sim_truth.csv
152
+ ```
153
+
154
+ Generate a scenario grid:
155
+
156
+ ```bash
157
+ bsvae-simulate init-config --output sim.yaml
158
+
159
+ bsvae-simulate generate-grid \
160
+ --config sim.yaml \
161
+ --outdir results/sim_pub_v1 \
162
+ --reps 30 \
163
+ --base-seed 13
164
+ ```
165
+
166
+ Validate the generated grid:
167
+
168
+ ```bash
169
+ bsvae-simulate validate-grid --grid-dir results/sim_pub_v1
170
+ ```
171
+
172
+ Each scenario replicate writes method-ready files under `results/sim_pub_v1/scenarios/<scenario_id>/rep_<rep>/`, including:
173
+
174
+ - `expr/features_x_samples.tsv.gz`
175
+ - `expr/samples_x_features.tsv.gz`
176
+ - `truth/modules_hard.csv`
177
+ - `method_inputs.json`
178
+
179
+ ## Documentation
180
+
181
+ See the docs for the full workflow and command reference:
182
+
183
+ - `docs/index.md`
184
+ - `docs/quickstart.md`
185
+ - `docs/tutorial.md`
186
+ - `docs/cli.md`
187
+ - `docs/networks.md`
188
+ - `docs/hyperparameters.md`
189
+
190
+ ## License
191
+
192
+ This project is licensed under the [GNU General Public License v3.0](LICENSE).
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bsvae"
3
- version = "0.3.1" # dynamically resolved by poetry-dynamic-versioning
3
+ version = "0.3.2" # dynamically resolved by poetry-dynamic-versioning
4
4
  description = "Gaussian Mixture VAE for multi-modal biological module discovery in omics data."
5
5
  readme = "README.md"
6
6
  authors = ["Kynon J Benjamin <kj.benjamin90@gmail.com>"]
@@ -64,6 +64,12 @@ def build_parser() -> argparse.ArgumentParser:
64
64
  mp.add_argument("--use-leiden", action="store_true",
65
65
  help="Also run Leiden clustering as fallback comparison.")
66
66
  mp.add_argument("--leiden-resolution", type=float, default=1.0)
67
+ mp.add_argument("--tx2gene", type=str, default=None,
68
+ help="TSV (transcript_id, gene_id) for isoform-to-gene aggregation.")
69
+ mp.add_argument("--aggregate-to-gene", action="store_true", default=False,
70
+ help="Aggregate isoform-level γ to gene level by averaging across "
71
+ "isoforms per gene. Saves gamma_gene.npz and "
72
+ "hard_assignments_gene.npz. Requires --tx2gene.")
67
73
 
68
74
  # ------------------------------------------------------------------
69
75
  # export-latents
@@ -120,6 +126,35 @@ def handle_extract_networks(args, logger: logging.Logger) -> None:
120
126
  logger.info("Network extraction complete → %s", args.output_dir)
121
127
 
122
128
 
129
+ def _aggregate_gamma_to_gene(
130
+ gamma: "np.ndarray",
131
+ feature_ids: List[str],
132
+ tx2gene_path: str,
133
+ ) -> "tuple[np.ndarray, list[str]]":
134
+ """Average isoform-level γ (F_tx, K) to gene level, renormalise rows to sum 1."""
135
+ import numpy as np
136
+ from collections import defaultdict
137
+ from bsvae.utils.hierarchy import load_tx2gene
138
+
139
+ tx2gene = load_tx2gene(path=tx2gene_path, feature_ids=feature_ids)
140
+ # Map every feature to its gene_id; fall back to itself for unmapped features
141
+ fid_to_gene = {fid: tx2gene.get(fid, fid) for fid in feature_ids}
142
+
143
+ gene_to_idxs: dict = defaultdict(list)
144
+ for i, fid in enumerate(feature_ids):
145
+ gene_to_idxs[fid_to_gene[fid]].append(i)
146
+
147
+ gene_ids = list(gene_to_idxs.keys())
148
+ gamma_gene = np.zeros((len(gene_ids), gamma.shape[1]), dtype=np.float32)
149
+ for j, gid in enumerate(gene_ids):
150
+ gamma_gene[j] = gamma[gene_to_idxs[gid]].mean(axis=0)
151
+
152
+ row_sums = gamma_gene.sum(axis=1, keepdims=True)
153
+ row_sums = np.where(row_sums > 0, row_sums, 1.0)
154
+ gamma_gene /= row_sums
155
+ return gamma_gene, gene_ids
156
+
157
+
123
158
  def handle_extract_modules(args, logger: logging.Logger) -> None:
124
159
  import numpy as np
125
160
  from bsvae.utils.modelIO import load_model
@@ -144,6 +179,30 @@ def handle_extract_modules(args, logger: logging.Logger) -> None:
144
179
  model.n_modules, len(feature_ids),
145
180
  )
146
181
 
182
+ if args.aggregate_to_gene:
183
+ if not args.tx2gene:
184
+ logger.warning("--aggregate-to-gene requires --tx2gene; skipping.")
185
+ else:
186
+ gamma_gene, gene_ids = _aggregate_gamma_to_gene(
187
+ result["gamma"], result["feature_ids"], args.tx2gene
188
+ )
189
+ hard_gene = gamma_gene.argmax(axis=1).astype(np.int32)
190
+ out = Path(args.output_dir)
191
+ np.savez_compressed(
192
+ out / "gamma_gene.npz",
193
+ gamma=gamma_gene,
194
+ feature_ids=np.array(gene_ids, dtype=object),
195
+ )
196
+ np.savez_compressed(
197
+ out / "hard_assignments_gene.npz",
198
+ hard_assignments=hard_gene,
199
+ feature_ids=np.array(gene_ids, dtype=object),
200
+ )
201
+ logger.info(
202
+ "Gene-level aggregation: %d transcripts → %d genes",
203
+ len(result["feature_ids"]), len(gene_ids),
204
+ )
205
+
147
206
  if args.soft_eigengenes and args.expr:
148
207
  import pandas as pd
149
208
  expr = pd.read_csv(args.expr, index_col=0, sep="\t" if args.expr.endswith(".tsv") else ",")
@@ -320,7 +320,7 @@ def parse_args(cli_args=None):
320
320
  p.add_argument("--kl-cycle-length", type=int, default=50)
321
321
  p.add_argument("--sep-strength", type=float, default=0.1)
322
322
  p.add_argument("--sep-alpha", type=float, default=2.0)
323
- p.add_argument("--bal-strength", type=float, default=0.01)
323
+ p.add_argument("--bal-strength", type=float, default=0.1)
324
324
  p.add_argument("--bal-ema-blend", type=float, default=0.5)
325
325
  p.add_argument("--pi-entropy-strength", type=float, default=0.0)
326
326
  p.add_argument("--hier-strength", type=float, default=0.0)
@@ -85,11 +85,16 @@ def parse_args(cli_args=None):
85
85
  p.add_argument("--use-batch-norm", action="store_true", dest="use_batch_norm", default=True)
86
86
  p.add_argument("--sigma-min", type=float, default=0.3,
87
87
  help="GMM component σ floor (default: 0.3).")
88
+ p.add_argument("--normalize-input", action="store_true", default=False,
89
+ help="Z-score each feature profile across samples before encoding. "
90
+ "Recommended for count-derived data (log2-CPM, etc.).")
88
91
 
89
92
  # --- Loss hyperparameters ---
90
93
  p.add_argument("--beta", type=float, default=1.0, help="KL weight (default: 1.0).")
91
- p.add_argument("--free-bits", type=float, default=0.5,
92
- help="Per-dim KL lower bound in nats (default: 0.5).")
94
+ p.add_argument("--free-bits", type=float, default=0.0,
95
+ help="Per-dim KL lower bound in nats (default: 0.0). "
96
+ "Non-zero values hurt GMM cluster recovery by flooring γ to uniform; "
97
+ "leave at 0.0 for normal use.")
93
98
  p.add_argument("--kl-warmup-epochs", type=int, default=0,
94
99
  help="Epochs for β linear ramp within Phase 2 (default: 0).")
95
100
  p.add_argument("--kl-anneal-mode", choices=["linear", "cyclical"], default="linear")
@@ -98,16 +103,36 @@ def parse_args(cli_args=None):
98
103
  help="λ_sep: σ-scaled separation loss weight (default: 0.1).")
99
104
  p.add_argument("--sep-alpha", type=float, default=2.0,
100
105
  help="α: margin multiplier for separation loss (default: 2.0).")
101
- p.add_argument("--bal-strength", type=float, default=0.01,
102
- help="λ_bal: γ-usage balance loss weight (default: 0.01).")
106
+ p.add_argument("--bal-strength", type=float, default=0.1,
107
+ help="λ_bal: γ-usage balance loss weight (default: 0.1). "
108
+ "Higher values force uniform usage; lower values allow natural "
109
+ "module-size variation but risk dead components.")
103
110
  p.add_argument("--bal-ema-blend", type=float, default=0.5,
104
111
  help="Balance loss blend α between EMA and batch (default: 0.5).")
112
+ p.add_argument("--masked-recon", action="store_true", default=False,
113
+ help="Compute reconstruction loss only on non-zero input entries. "
114
+ "Recommended for zero-inflated count data.")
105
115
  p.add_argument("--pi-entropy-strength", type=float, default=0.0,
106
116
  help="λ_pi: KL(uniform || π) penalty weight (default: 0.0).")
107
117
  p.add_argument("--hier-strength", type=float, default=0.0,
108
118
  help="λ_hier: hierarchical isoform loss weight (default: 0.0=disabled).")
119
+ p.add_argument("--corr-strength", type=float, default=0.0,
120
+ help="λ_corr: reconstruction-space pairwise correlation loss weight (default: 0.0=disabled). "
121
+ "Penalises MSE between the batch-level Pearson correlation matrices of inputs "
122
+ "and reconstructions. Recommended: 0.05–0.2 for NB count data with normalize-input.")
123
+ p.add_argument("--latent-corr-strength", type=float, default=0.0,
124
+ help="λ_lcorr: latent-space correlation alignment loss weight (default: 0.0=disabled). "
125
+ "Aligns cosine similarities of encoder μ with input Pearson correlations. "
126
+ "More direct than --corr-strength; recommended: 0.1–1.0 for NB count data.")
109
127
  p.add_argument("--tx2gene", type=str, default=None,
110
128
  help="TSV with (transcript_id, gene_id) for hierarchical loss.")
129
+ p.add_argument("--isoform-stratified", action="store_true", default=False,
130
+ help="Use IsoformStratifiedSampler so isoforms of the same gene "
131
+ "co-occur in batches, making --hier-strength effective. "
132
+ "Requires --tx2gene.")
133
+ p.add_argument("--p-multi", type=float, default=0.5,
134
+ help="Probability of drawing a full batch from multi-isoform features "
135
+ "(default: 0.5). Only active with --isoform-stratified.")
111
136
 
112
137
  # --- Evaluation ---
113
138
  p.add_argument("--no-eval", action="store_true", default=False,
@@ -145,12 +170,53 @@ def main(args):
145
170
 
146
171
  # --- Hierarchy ---
147
172
  gene_groups = {}
148
- if args.hier_strength > 0 and args.tx2gene:
173
+ if args.tx2gene and (args.hier_strength > 0 or args.isoform_stratified):
149
174
  from bsvae.utils.hierarchy import load_tx2gene, group_isoforms_by_gene
150
175
  tx2gene = load_tx2gene(path=args.tx2gene, feature_ids=feature_ids)
151
176
  gene_groups = group_isoforms_by_gene(tx2gene, feature_ids)
152
177
  logger.info("Hierarchical groups: %d multi-isoform genes", len(gene_groups))
153
178
 
179
+ # Validate gene_groups indices are in-bounds
180
+ if gene_groups:
181
+ n_features_total = len(feature_ids)
182
+ for gene_id, idxs in gene_groups.items():
183
+ bad = [i for i in idxs if i < 0 or i >= n_features_total]
184
+ if bad:
185
+ raise ValueError(
186
+ f"gene_groups['{gene_id}'] contains out-of-bounds indices {bad} "
187
+ f"(dataset has {n_features_total} features)"
188
+ )
189
+
190
+ # Replace DataLoader with IsoformStratifiedSampler if requested
191
+ if args.isoform_stratified and gene_groups:
192
+ import torch.utils.data as tud
193
+ from bsvae.utils.hierarchy import IsoformStratifiedSampler
194
+ multi_indices = sorted({idx for idxs in gene_groups.values() for idx in idxs})
195
+ sampler = IsoformStratifiedSampler(
196
+ n_features=len(feature_ids),
197
+ multi_isoform_indices=multi_indices,
198
+ batch_size=args.batch_size,
199
+ p_multi=args.p_multi,
200
+ seed=args.seed,
201
+ )
202
+ train_loader = tud.DataLoader(
203
+ train_loader.dataset,
204
+ batch_size=args.batch_size,
205
+ sampler=sampler,
206
+ drop_last=True,
207
+ pin_memory=train_loader.pin_memory,
208
+ num_workers=train_loader.num_workers,
209
+ )
210
+ logger.info(
211
+ "IsoformStratifiedSampler: %d multi-isoform features, p_multi=%.2f",
212
+ len(multi_indices), args.p_multi,
213
+ )
214
+ elif args.isoform_stratified and not gene_groups:
215
+ logger.warning(
216
+ "--isoform-stratified requested but no multi-isoform gene groups found; "
217
+ "using standard random sampler."
218
+ )
219
+
154
220
  # --- Model ---
155
221
  model = GMMModuleVAE(
156
222
  n_features=n_features,
@@ -160,6 +226,7 @@ def main(args):
160
226
  dropout=args.dropout,
161
227
  use_batch_norm=args.use_batch_norm,
162
228
  sigma_min=args.sigma_min,
229
+ normalize_input=args.normalize_input,
163
230
  ).to(device)
164
231
  logger.info(
165
232
  "Model: %d features, %d latent dims, %d modules | params=%d",
@@ -179,6 +246,10 @@ def main(args):
179
246
  pi_entropy_strength=args.pi_entropy_strength,
180
247
  bal_ema_blend=args.bal_ema_blend,
181
248
  hier_strength=args.hier_strength,
249
+ normalize_input=args.normalize_input,
250
+ masked_recon=args.masked_recon,
251
+ corr_strength=args.corr_strength,
252
+ latent_corr_strength=args.latent_corr_strength,
182
253
  )
183
254
  warmup_loss = WarmupLoss(
184
255
  beta=args.beta,
@@ -272,6 +272,11 @@ class GaussianMixturePrior(nn.Module):
272
272
  K = self.K
273
273
  X = mu_samples.cpu().numpy()
274
274
 
275
+ if len(X) < K:
276
+ raise ValueError(
277
+ f"kmeans_init_: need at least K={K} samples but got {len(X)}. "
278
+ "Increase the dataset size or reduce n_modules."
279
+ )
275
280
  km = KMeans(n_clusters=K, random_state=0, n_init=10)
276
281
  labels = km.fit_predict(X)
277
282
  centers = torch.tensor(km.cluster_centers_, dtype=torch.float32)