bsvae 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 (58) hide show
  1. bsvae-0.3.0/PKG-INFO +162 -0
  2. bsvae-0.3.0/README.md +130 -0
  3. {bsvae-0.2.0 → bsvae-0.3.0}/pyproject.toml +18 -10
  4. bsvae-0.3.0/src/bsvae/__init__.py +13 -0
  5. bsvae-0.3.0/src/bsvae/cli/__init__.py +1 -0
  6. bsvae-0.3.0/src/bsvae/cli/networks.py +287 -0
  7. bsvae-0.3.0/src/bsvae/cli/simulate.py +161 -0
  8. bsvae-0.3.0/src/bsvae/cli/train.py +226 -0
  9. bsvae-0.3.0/src/bsvae/hyperparam.ini +9 -0
  10. bsvae-0.3.0/src/bsvae/latent/__init__.py +89 -0
  11. {bsvae-0.2.0 → bsvae-0.3.0}/src/bsvae/latent/latent_analysis.py +31 -23
  12. {bsvae-0.2.0 → bsvae-0.3.0}/src/bsvae/main.py +132 -27
  13. bsvae-0.3.0/src/bsvae/models/__init__.py +5 -0
  14. bsvae-0.3.0/src/bsvae/models/decoder.py +58 -0
  15. bsvae-0.3.0/src/bsvae/models/encoder.py +85 -0
  16. bsvae-0.3.0/src/bsvae/models/gmm_prior.py +288 -0
  17. bsvae-0.3.0/src/bsvae/models/gmvae.py +133 -0
  18. bsvae-0.3.0/src/bsvae/models/losses.py +432 -0
  19. bsvae-0.3.0/src/bsvae/networks/__init__.py +2 -0
  20. bsvae-0.3.0/src/bsvae/networks/cli.py +689 -0
  21. bsvae-0.3.0/src/bsvae/networks/extract_networks.py +363 -0
  22. bsvae-0.3.0/src/bsvae/networks/module_extraction.py +1047 -0
  23. {bsvae-0.2.0 → bsvae-0.3.0}/src/bsvae/networks/utils.py +7 -2
  24. bsvae-0.3.0/src/bsvae/simulation/__init__.py +5 -0
  25. bsvae-0.3.0/src/bsvae/simulation/generate.py +116 -0
  26. bsvae-0.3.0/src/bsvae/simulation/metrics.py +154 -0
  27. bsvae-0.3.0/src/bsvae/utils/__init__.py +1 -0
  28. bsvae-0.3.0/src/bsvae/utils/datasets.py +305 -0
  29. {bsvae-0.2.0 → bsvae-0.3.0}/src/bsvae/utils/evaluate.py +14 -7
  30. bsvae-0.3.0/src/bsvae/utils/hierarchy.py +172 -0
  31. bsvae-0.3.0/src/bsvae/utils/modelIO.py +190 -0
  32. bsvae-0.3.0/src/bsvae/utils/training.py +365 -0
  33. bsvae-0.2.0/PKG-INFO +0 -240
  34. bsvae-0.2.0/README.md +0 -207
  35. bsvae-0.2.0/src/bsvae/__init__.py +0 -12
  36. bsvae-0.2.0/src/bsvae/hyperparam.ini +0 -82
  37. bsvae-0.2.0/src/bsvae/latent/__init__.py +0 -6
  38. bsvae-0.2.0/src/bsvae/models/__init__.py +0 -4
  39. bsvae-0.2.0/src/bsvae/models/decoder.py +0 -92
  40. bsvae-0.2.0/src/bsvae/models/encoder.py +0 -71
  41. bsvae-0.2.0/src/bsvae/models/losses.py +0 -116
  42. bsvae-0.2.0/src/bsvae/models/structured.py +0 -79
  43. bsvae-0.2.0/src/bsvae/networks/__init__.py +0 -13
  44. bsvae-0.2.0/src/bsvae/networks/cli.py +0 -318
  45. bsvae-0.2.0/src/bsvae/networks/extract_networks.py +0 -1229
  46. bsvae-0.2.0/src/bsvae/networks/module_extraction.py +0 -426
  47. bsvae-0.2.0/src/bsvae/ppi_cli.py +0 -51
  48. bsvae-0.2.0/src/bsvae/utils/__init__.py +0 -2
  49. bsvae-0.2.0/src/bsvae/utils/datasets.py +0 -187
  50. bsvae-0.2.0/src/bsvae/utils/mapping.py +0 -218
  51. bsvae-0.2.0/src/bsvae/utils/modelIO.py +0 -187
  52. bsvae-0.2.0/src/bsvae/utils/ppi.py +0 -158
  53. bsvae-0.2.0/src/bsvae/utils/training.py +0 -143
  54. {bsvae-0.2.0 → bsvae-0.3.0}/LICENSE +0 -0
  55. {bsvae-0.2.0 → bsvae-0.3.0}/src/bsvae/latent/latent_export.py +0 -0
  56. {bsvae-0.2.0 → bsvae-0.3.0}/src/bsvae/models/vae.py +0 -0
  57. {bsvae-0.2.0 → bsvae-0.3.0}/src/bsvae/utils/helpers.py +0 -0
  58. {bsvae-0.2.0 → bsvae-0.3.0}/src/bsvae/utils/initialization.py +0 -0
bsvae-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: bsvae
3
+ Version: 0.3.0
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 centered on `GMMModuleVAE`, a Gaussian-mixture variational autoencoder for feature-level module discovery in omics data.
40
+
41
+ ## What It Does
42
+
43
+ - Trains a two-phase GMM-VAE (`bsvae-train`)
44
+ - Extracts feature-feature networks from trained models (`bsvae-networks`)
45
+ - Extracts module assignments and optional eigengenes (`bsvae-networks`)
46
+ - Exports latents (`mu`, `logvar`, `gamma`) as `.npz`
47
+ - Simulates synthetic datasets and benchmarks module recovery (`bsvae-simulate`)
48
+
49
+ ## Installation
50
+
51
+ From PyPI:
52
+
53
+ ```bash
54
+ pip install bsvae
55
+ ```
56
+
57
+ From source:
58
+
59
+ ```bash
60
+ git clone https://github.com/heart-gen/BSVAE.git
61
+ cd BSVAE
62
+ pip install -e .
63
+ ```
64
+
65
+ ## CLI Entry Points
66
+
67
+ - `bsvae-train`
68
+ - `bsvae-networks`
69
+ - `bsvae-simulate`
70
+
71
+ ## Quickstart
72
+
73
+ For a full walkthrough (minimal run, production run, post-training analysis, simulation benchmark, troubleshooting, and migration), see `docs/tutorial.md`.
74
+
75
+ ### 1. Train
76
+
77
+ Input matrix must be `features x samples` with feature IDs in row index and sample IDs in columns.
78
+
79
+ ```bash
80
+ bsvae-train exp1 \
81
+ --dataset data/expression.csv \
82
+ --epochs 100 \
83
+ --n-modules 20 \
84
+ --latent-dim 32
85
+ ```
86
+
87
+ ### 2. Extract networks
88
+
89
+ ```bash
90
+ bsvae-networks extract-networks \
91
+ --model-path results/exp1 \
92
+ --dataset data/expression.csv \
93
+ --output-dir results/exp1/networks \
94
+ --methods mu_cosine gamma_knn
95
+ ```
96
+
97
+ ### 3. Extract modules
98
+
99
+ ```bash
100
+ bsvae-networks extract-modules \
101
+ --model-path results/exp1 \
102
+ --dataset data/expression.csv \
103
+ --output-dir results/exp1/modules
104
+ ```
105
+
106
+ ### 4. Export latents
107
+
108
+ ```bash
109
+ bsvae-networks export-latents \
110
+ --model-path results/exp1 \
111
+ --dataset data/expression.csv \
112
+ --output results/exp1/latents.npz
113
+ ```
114
+
115
+ ### 5. Simulate and benchmark
116
+
117
+ ```bash
118
+ bsvae-simulate generate \
119
+ --output data/sim_expr.csv \
120
+ --save-ground-truth data/sim_truth.csv
121
+
122
+ bsvae-simulate benchmark \
123
+ --dataset data/sim_expr.csv \
124
+ --ground-truth data/sim_truth.csv \
125
+ --model-path results/exp1 \
126
+ --output results/exp1/sim_metrics.json
127
+ ```
128
+
129
+ ## Training Outputs
130
+
131
+ `bsvae-train` writes to `results/<experiment>/`:
132
+
133
+ - `model.pt` (weights)
134
+ - `specs.json` (metadata and run args)
135
+ - `train_losses.csv` (epoch/component losses)
136
+ - `model-<epoch>.pt` checkpoints when `--checkpoint-every` is set
137
+
138
+ ## Data Formats
139
+
140
+ The loader supports:
141
+
142
+ - `.csv` / `.csv.gz`
143
+ - `.tsv` / `.tsv.gz`
144
+ - `.h5` / `.hdf5`
145
+ - `.h5ad` (optional `anndata` dependency)
146
+
147
+ ## Python API (Minimal)
148
+
149
+ ```python
150
+ from bsvae.utils.modelIO import load_model
151
+ from bsvae.networks.extract_networks import create_dataloader_from_expression, run_extraction
152
+
153
+ model = load_model("results/exp1", is_gpu=False)
154
+ loader, feature_ids, _ = create_dataloader_from_expression("data/expression.csv", batch_size=128)
155
+ results = run_extraction(model, loader, feature_ids=feature_ids, methods=["mu_cosine"], top_k=50)
156
+ print(results[0].method, results[0].adjacency.shape)
157
+ ```
158
+
159
+ ## License
160
+
161
+ This project is licensed under the [GNU General Public License v3.0](LICENSE).
162
+
bsvae-0.3.0/README.md ADDED
@@ -0,0 +1,130 @@
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 centered on `GMMModuleVAE`, a Gaussian-mixture variational autoencoder for feature-level module discovery in omics data.
9
+
10
+ ## What It Does
11
+
12
+ - Trains a two-phase GMM-VAE (`bsvae-train`)
13
+ - Extracts feature-feature networks from trained models (`bsvae-networks`)
14
+ - Extracts module assignments and optional eigengenes (`bsvae-networks`)
15
+ - Exports latents (`mu`, `logvar`, `gamma`) as `.npz`
16
+ - Simulates synthetic datasets and benchmarks module recovery (`bsvae-simulate`)
17
+
18
+ ## Installation
19
+
20
+ From PyPI:
21
+
22
+ ```bash
23
+ pip install bsvae
24
+ ```
25
+
26
+ From source:
27
+
28
+ ```bash
29
+ git clone https://github.com/heart-gen/BSVAE.git
30
+ cd BSVAE
31
+ pip install -e .
32
+ ```
33
+
34
+ ## CLI Entry Points
35
+
36
+ - `bsvae-train`
37
+ - `bsvae-networks`
38
+ - `bsvae-simulate`
39
+
40
+ ## Quickstart
41
+
42
+ For a full walkthrough (minimal run, production run, post-training analysis, simulation benchmark, troubleshooting, and migration), see `docs/tutorial.md`.
43
+
44
+ ### 1. Train
45
+
46
+ Input matrix must be `features x samples` with feature IDs in row index and sample IDs in columns.
47
+
48
+ ```bash
49
+ bsvae-train exp1 \
50
+ --dataset data/expression.csv \
51
+ --epochs 100 \
52
+ --n-modules 20 \
53
+ --latent-dim 32
54
+ ```
55
+
56
+ ### 2. Extract networks
57
+
58
+ ```bash
59
+ bsvae-networks extract-networks \
60
+ --model-path results/exp1 \
61
+ --dataset data/expression.csv \
62
+ --output-dir results/exp1/networks \
63
+ --methods mu_cosine gamma_knn
64
+ ```
65
+
66
+ ### 3. Extract modules
67
+
68
+ ```bash
69
+ bsvae-networks extract-modules \
70
+ --model-path results/exp1 \
71
+ --dataset data/expression.csv \
72
+ --output-dir results/exp1/modules
73
+ ```
74
+
75
+ ### 4. Export latents
76
+
77
+ ```bash
78
+ bsvae-networks export-latents \
79
+ --model-path results/exp1 \
80
+ --dataset data/expression.csv \
81
+ --output results/exp1/latents.npz
82
+ ```
83
+
84
+ ### 5. Simulate and benchmark
85
+
86
+ ```bash
87
+ bsvae-simulate generate \
88
+ --output data/sim_expr.csv \
89
+ --save-ground-truth data/sim_truth.csv
90
+
91
+ bsvae-simulate benchmark \
92
+ --dataset data/sim_expr.csv \
93
+ --ground-truth data/sim_truth.csv \
94
+ --model-path results/exp1 \
95
+ --output results/exp1/sim_metrics.json
96
+ ```
97
+
98
+ ## Training Outputs
99
+
100
+ `bsvae-train` writes to `results/<experiment>/`:
101
+
102
+ - `model.pt` (weights)
103
+ - `specs.json` (metadata and run args)
104
+ - `train_losses.csv` (epoch/component losses)
105
+ - `model-<epoch>.pt` checkpoints when `--checkpoint-every` is set
106
+
107
+ ## Data Formats
108
+
109
+ The loader supports:
110
+
111
+ - `.csv` / `.csv.gz`
112
+ - `.tsv` / `.tsv.gz`
113
+ - `.h5` / `.hdf5`
114
+ - `.h5ad` (optional `anndata` dependency)
115
+
116
+ ## Python API (Minimal)
117
+
118
+ ```python
119
+ from bsvae.utils.modelIO import load_model
120
+ from bsvae.networks.extract_networks import create_dataloader_from_expression, run_extraction
121
+
122
+ model = load_model("results/exp1", is_gpu=False)
123
+ loader, feature_ids, _ = create_dataloader_from_expression("data/expression.csv", batch_size=128)
124
+ results = run_extraction(model, loader, feature_ids=feature_ids, methods=["mu_cosine"], top_k=50)
125
+ print(results[0].method, results[0].adjacency.shape)
126
+ ```
127
+
128
+ ## License
129
+
130
+ This project is licensed under the [GNU General Public License v3.0](LICENSE).
@@ -1,18 +1,19 @@
1
1
  [tool.poetry]
2
2
  name = "bsvae"
3
- version = "0.2.0" # dynamically resolved by poetry-dynamic-versioning
4
- description = "Biologically structured variational autoencoder for gene expression data with PPI priors."
3
+ version = "0.3.0" # dynamically resolved by poetry-dynamic-versioning
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>"]
7
7
  license = "GPL-3.0-only"
8
8
  packages = [{ include = "bsvae", from = "src" }]
9
9
  keywords = [
10
10
  "variational-autoencoder",
11
- "structured-vae",
11
+ "gaussian-mixture-model",
12
12
  "pytorch",
13
13
  "bioinformatics",
14
14
  "gene-expression",
15
- "protein-protein-interaction",
15
+ "module-discovery",
16
+ "omics",
16
17
  ]
17
18
  classifiers = [
18
19
  "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
@@ -30,12 +31,15 @@ python = ">=3.11,<3.14"
30
31
  torch = ">=2.8.0,<3.0.0"
31
32
  numpy = "<2.3"
32
33
  pandas = ">=2.3.2,<3.0.0"
33
- pyarrow = ">=19.0.0,<20.0.0"
34
34
  scikit-learn = ">=1.7.2,<2.0.0"
35
35
  tqdm = ">=4.67.1,<5.0.0"
36
- mygene = ">=3.2.2,<4.0.0"
37
- networkx = ">=3.5,<4.0"
38
36
  scipy = ">=1.16.2,<2.0.0"
37
+ h5py = ">=3.0"
38
+ faiss-cpu = ">=1.7"
39
+
40
+ [tool.poetry.dependencies.anndata]
41
+ version = ">=0.10"
42
+ optional = true
39
43
 
40
44
  [tool.poetry.urls]
41
45
  homepage = "https://bsvae.readthedocs.io/en/latest/"
@@ -43,9 +47,9 @@ repository = "https://github.com/heart-gen/BSVAE"
43
47
  "Bug Tracker" = "https://github.com/heart-gen/BSVAE/issues"
44
48
 
45
49
  [tool.poetry.scripts]
46
- bsvae-train = "bsvae.main:cli"
47
- bsvae-download-ppi = "bsvae.ppi_cli:cli"
48
- bsvae-networks = "bsvae.networks.cli:cli"
50
+ bsvae-train = "bsvae.cli.train:cli"
51
+ bsvae-networks = "bsvae.cli.networks:cli"
52
+ bsvae-simulate = "bsvae.cli.simulate:cli"
49
53
 
50
54
  [tool.poetry.group.dev.dependencies]
51
55
  pytest = ">=8.4.2,<9.0.0"
@@ -53,6 +57,10 @@ pytest-cov = ">=7.0.0,<8.0.0"
53
57
 
54
58
  [tool.pytest.ini_options]
55
59
  pythonpath = ["src"]
60
+ markers = [
61
+ "slow: marks tests as slow-running integration tests (deselect with '-m not slow')",
62
+ ]
63
+ addopts = ["-m", "not slow"]
56
64
 
57
65
  [tool.poetry-dynamic-versioning]
58
66
  enable = false
@@ -0,0 +1,13 @@
1
+ """BSVAE — Gaussian Mixture VAE for multi-modal biological module discovery."""
2
+
3
+ from importlib import import_module
4
+ from importlib.metadata import PackageNotFoundError, version
5
+
6
+ from . import latent, networks, simulation
7
+
8
+ try:
9
+ __version__ = version(__name__)
10
+ except PackageNotFoundError:
11
+ __version__ = "0.0.0"
12
+
13
+ __all__ = ["networks", "latent", "simulation", "__version__"]
@@ -0,0 +1 @@
1
+ """CLI entry points for bsvae."""
@@ -0,0 +1,287 @@
1
+ """
2
+ bsvae-networks — Network extraction, module assignment, latent export.
3
+
4
+ Subcommands
5
+ -----------
6
+ extract-networks Method A (μ cosine) or Method B (FAISS γ-kNN)
7
+ extract-modules GMM soft/hard assignments + soft-weighted eigengenes
8
+ export-latents Save μ, logσ², γ as NPZ
9
+ latent-analysis UMAP, clustering, covariate correlation
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import logging
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import List, Optional
19
+
20
+
21
+ def setup_logging(level: str = "INFO") -> logging.Logger:
22
+ logging.basicConfig(
23
+ level=getattr(logging, level.upper(), logging.INFO),
24
+ format="%(asctime)s %(levelname)s: %(message)s",
25
+ )
26
+ return logging.getLogger(__name__)
27
+
28
+
29
+ def build_parser() -> argparse.ArgumentParser:
30
+ parser = argparse.ArgumentParser(
31
+ prog="bsvae-networks",
32
+ description="Network extraction and latent analysis for GMMModuleVAE.",
33
+ )
34
+ subs = parser.add_subparsers(dest="command", required=True)
35
+
36
+ # ------------------------------------------------------------------
37
+ # extract-networks
38
+ # ------------------------------------------------------------------
39
+ ep = subs.add_parser("extract-networks", help="Build feature-feature networks from trained model.")
40
+ ep.add_argument("--model-path", required=True)
41
+ ep.add_argument("--dataset", required=True)
42
+ ep.add_argument("--output-dir", required=True)
43
+ ep.add_argument(
44
+ "--methods", nargs="+", default=["mu_cosine"],
45
+ choices=["mu_cosine", "gamma_knn"],
46
+ help="Methods to run (default: mu_cosine).",
47
+ )
48
+ ep.add_argument("--top-k", type=int, default=50, help="Top-K edges per feature (default: 50).")
49
+ ep.add_argument("--batch-size", type=int, default=128)
50
+ ep.add_argument("--no-cuda", action="store_true", default=False)
51
+
52
+ # ------------------------------------------------------------------
53
+ # extract-modules
54
+ # ------------------------------------------------------------------
55
+ mp = subs.add_parser("extract-modules", help="Extract GMM module assignments.")
56
+ mp.add_argument("--model-path", required=True)
57
+ mp.add_argument("--dataset", required=True)
58
+ mp.add_argument("--output-dir", required=True)
59
+ mp.add_argument("--batch-size", type=int, default=128)
60
+ mp.add_argument("--no-cuda", action="store_true", default=False)
61
+ mp.add_argument("--expr", help="Expression matrix for eigengene computation (features × samples).")
62
+ mp.add_argument("--soft-eigengenes", action="store_true",
63
+ help="Compute soft-weighted eigengenes (requires --expr).")
64
+ mp.add_argument("--use-leiden", action="store_true",
65
+ help="Also run Leiden clustering as fallback comparison.")
66
+ mp.add_argument("--leiden-resolution", type=float, default=1.0)
67
+
68
+ # ------------------------------------------------------------------
69
+ # export-latents
70
+ # ------------------------------------------------------------------
71
+ lp = subs.add_parser("export-latents", help="Export μ, logσ², γ for all features.")
72
+ lp.add_argument("--model-path", required=True)
73
+ lp.add_argument("--dataset", required=True)
74
+ lp.add_argument("--output", required=True, help="Output path (.npz).")
75
+ lp.add_argument("--batch-size", type=int, default=128)
76
+ lp.add_argument("--no-cuda", action="store_true", default=False)
77
+
78
+ # ------------------------------------------------------------------
79
+ # latent-analysis
80
+ # ------------------------------------------------------------------
81
+ la = subs.add_parser("latent-analysis", help="UMAP, clustering, covariate correlation on μ.")
82
+ la.add_argument("--model-path", required=True)
83
+ la.add_argument("--dataset", required=True)
84
+ la.add_argument("--output-dir", required=True)
85
+ la.add_argument("--batch-size", type=int, default=128)
86
+ la.add_argument("--no-cuda", action="store_true", default=False)
87
+ la.add_argument("--covariates", help="TSV/CSV with sample covariates.")
88
+ la.add_argument("--kmeans-k", type=int, default=0)
89
+ la.add_argument("--gmm-k", type=int, default=0)
90
+ la.add_argument("--umap", action="store_true")
91
+ la.add_argument("--tsne", action="store_true")
92
+ la.add_argument("--tsne-perplexity", type=float, default=30.0)
93
+
94
+ return parser
95
+
96
+
97
+ # ------------------------------------------------------------------
98
+ # Handlers
99
+ # ------------------------------------------------------------------
100
+
101
+ def handle_extract_networks(args, logger: logging.Logger) -> None:
102
+ import torch
103
+ from bsvae.utils.modelIO import load_model
104
+ from bsvae.networks.extract_networks import run_extraction, create_dataloader_from_expression
105
+
106
+ device = torch.device("cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu")
107
+ model = load_model(args.model_path, is_gpu=not args.no_cuda)
108
+ dataloader, feature_ids, _ = create_dataloader_from_expression(
109
+ args.dataset, batch_size=args.batch_size
110
+ )
111
+ logger.info("Running methods: %s | top_k=%d", args.methods, args.top_k)
112
+ run_extraction(
113
+ model=model,
114
+ dataloader=dataloader,
115
+ feature_ids=feature_ids,
116
+ methods=args.methods,
117
+ top_k=args.top_k,
118
+ output_dir=args.output_dir,
119
+ )
120
+ logger.info("Network extraction complete → %s", args.output_dir)
121
+
122
+
123
+ def handle_extract_modules(args, logger: logging.Logger) -> None:
124
+ import numpy as np
125
+ from bsvae.utils.modelIO import load_model
126
+ from bsvae.networks.extract_networks import create_dataloader_from_expression
127
+ from bsvae.networks.module_extraction import (
128
+ extract_gmm_modules,
129
+ compute_module_eigengenes_from_soft,
130
+ )
131
+
132
+ model = load_model(args.model_path, is_gpu=not args.no_cuda)
133
+ dataloader, feature_ids, _ = create_dataloader_from_expression(
134
+ args.dataset, batch_size=args.batch_size
135
+ )
136
+ result = extract_gmm_modules(
137
+ model=model,
138
+ dataloader=dataloader,
139
+ feature_ids=feature_ids,
140
+ output_dir=args.output_dir,
141
+ )
142
+ logger.info(
143
+ "Extracted %d-module GMM assignments for %d features",
144
+ model.n_modules, len(feature_ids),
145
+ )
146
+
147
+ if args.soft_eigengenes and args.expr:
148
+ import pandas as pd
149
+ expr = pd.read_csv(args.expr, index_col=0, sep="\t" if args.expr.endswith(".tsv") else ",")
150
+ eigengenes = compute_module_eigengenes_from_soft(
151
+ expr, result["gamma"], feature_ids
152
+ )
153
+ out = Path(args.output_dir) / "soft_eigengenes.csv"
154
+ eigengenes.to_csv(out)
155
+ logger.info("Soft-weighted eigengenes saved to %s", out)
156
+
157
+ if args.use_leiden:
158
+ from bsvae.networks.module_extraction import leiden_modules, save_modules
159
+ from bsvae.networks.extract_networks import (
160
+ method_a_cosine,
161
+ extract_mu_gamma,
162
+ )
163
+ device = None
164
+ mu, gamma, _ = extract_mu_gamma(model, dataloader, device=device)
165
+ adj = method_a_cosine(mu, top_k=50)
166
+ import pandas as pd
167
+ adj_df = pd.DataFrame(
168
+ adj.toarray(), index=feature_ids, columns=feature_ids
169
+ )
170
+ modules = leiden_modules(adj_df, resolution=args.leiden_resolution)
171
+ save_modules(modules, Path(args.output_dir) / "leiden_modules.csv")
172
+ logger.info("Leiden fallback modules saved.")
173
+
174
+
175
+ def handle_export_latents(args, logger: logging.Logger) -> None:
176
+ import numpy as np
177
+ from bsvae.utils.modelIO import load_model
178
+ from bsvae.networks.extract_networks import extract_mu_gamma, create_dataloader_from_expression
179
+
180
+ model = load_model(args.model_path, is_gpu=not args.no_cuda)
181
+ dataloader, feature_ids, _ = create_dataloader_from_expression(
182
+ args.dataset, batch_size=args.batch_size
183
+ )
184
+ mu, gamma, extracted_ids = extract_mu_gamma(model, dataloader)
185
+ if not feature_ids:
186
+ feature_ids = extracted_ids
187
+
188
+ # Also get logvar
189
+ import torch
190
+ device = next(model.parameters()).device
191
+ model.eval()
192
+ logvar_list = []
193
+ with torch.no_grad():
194
+ for batch in dataloader:
195
+ x = batch[0] if isinstance(batch, (list, tuple)) else batch
196
+ x = x.to(device)
197
+ _, logvar = model.encode(x)
198
+ logvar_list.append(logvar.cpu().numpy())
199
+ import numpy as np
200
+ logvar = np.concatenate(logvar_list, axis=0).astype(np.float32)
201
+
202
+ out = args.output
203
+ if not out.endswith(".npz"):
204
+ out = out + ".npz"
205
+ np.savez_compressed(
206
+ out,
207
+ mu=mu,
208
+ logvar=logvar,
209
+ gamma=gamma,
210
+ feature_ids=np.array(feature_ids, dtype=object),
211
+ )
212
+ logger.info("Latents saved to %s", out)
213
+
214
+
215
+ def handle_latent_analysis(args, logger: logging.Logger) -> None:
216
+ from bsvae.utils.modelIO import load_model
217
+ from bsvae.networks.extract_networks import create_dataloader_from_expression
218
+ from bsvae.latent.latent_analysis import (
219
+ extract_latents,
220
+ kmeans_on_mu,
221
+ gmm_on_z,
222
+ umap_mu,
223
+ tsne_mu,
224
+ correlate_with_covariates,
225
+ save_latent_results,
226
+ )
227
+ import pandas as pd
228
+ import numpy as np
229
+
230
+ model = load_model(args.model_path, is_gpu=not args.no_cuda)
231
+ dataloader, feature_ids, _ = create_dataloader_from_expression(
232
+ args.dataset, batch_size=args.batch_size
233
+ )
234
+
235
+ mu, logvar, z = extract_latents(model, dataloader)
236
+
237
+ clusters = None
238
+ if args.kmeans_k:
239
+ clusters = kmeans_on_mu(mu, k=args.kmeans_k)
240
+ elif args.gmm_k:
241
+ clusters, _ = gmm_on_z(z, n_components=args.gmm_k)
242
+
243
+ embedding = None
244
+ if args.umap:
245
+ embedding = umap_mu(mu)
246
+ elif args.tsne:
247
+ embedding = tsne_mu(mu, perplexity=args.tsne_perplexity)
248
+
249
+ correlation_df = None
250
+ if args.covariates:
251
+ sep = "\t" if args.covariates.endswith(".tsv") else ","
252
+ cov_df = pd.read_csv(args.covariates, sep=sep, index_col=0)
253
+ cov_df = cov_df.reindex(feature_ids)
254
+ if not cov_df.empty:
255
+ correlation_df = correlate_with_covariates(
256
+ pd.DataFrame(mu, index=feature_ids), cov_df
257
+ )
258
+
259
+ save_latent_results(
260
+ mu=mu, logvar=logvar, row_ids=feature_ids,
261
+ output_dir=args.output_dir,
262
+ cluster_labels=clusters,
263
+ embedding=embedding,
264
+ correlation_df=correlation_df,
265
+ )
266
+ logger.info("Latent analysis saved to %s", args.output_dir)
267
+
268
+
269
+ def cli(argv=None):
270
+ parser = build_parser()
271
+ args = parser.parse_args(argv)
272
+ logger = setup_logging()
273
+
274
+ if args.command == "extract-networks":
275
+ handle_extract_networks(args, logger)
276
+ elif args.command == "extract-modules":
277
+ handle_extract_modules(args, logger)
278
+ elif args.command == "export-latents":
279
+ handle_export_latents(args, logger)
280
+ elif args.command == "latent-analysis":
281
+ handle_latent_analysis(args, logger)
282
+ else:
283
+ parser.error(f"Unknown command: {args.command}")
284
+
285
+
286
+ if __name__ == "__main__":
287
+ cli()