embkit 0.4__tar.gz → 0.5__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. embkit-0.5/PKG-INFO +89 -0
  2. embkit-0.5/README.md +67 -0
  3. {embkit-0.4 → embkit-0.5}/pyproject.toml +2 -2
  4. {embkit-0.4 → embkit-0.5}/setup.py +1 -1
  5. {embkit-0.4 → embkit-0.5}/src/embkit/align.py +44 -16
  6. embkit-0.5/src/embkit/c_bio/api.py +31 -0
  7. embkit-0.5/src/embkit/commands/matrix.py +97 -0
  8. {embkit-0.4 → embkit-0.5}/src/embkit/commands/model.py +50 -17
  9. {embkit-0.4 → embkit-0.5}/src/embkit/constraints/pathway_constraint.py +2 -1
  10. embkit-0.5/src/embkit/datasets/__init__.py +172 -0
  11. {embkit-0.4 → embkit-0.5}/src/embkit/encoding/__init__.py +140 -14
  12. {embkit-0.4 → embkit-0.5}/src/embkit/encoding/genome.py +1 -1
  13. {embkit-0.4 → embkit-0.5}/src/embkit/estimator/vae_estimator.py +10 -7
  14. {embkit-0.4 → embkit-0.5}/src/embkit/factory/__init__.py +4 -4
  15. embkit-0.5/src/embkit/factory/core.py +153 -0
  16. {embkit-0.4 → embkit-0.5}/src/embkit/factory/layers.py +85 -25
  17. {embkit-0.4 → embkit-0.5}/src/embkit/factory/mapping.py +58 -0
  18. {embkit-0.4 → embkit-0.5}/src/embkit/factory/registry.py +31 -0
  19. {embkit-0.4 → embkit-0.5}/src/embkit/files/h5.py +69 -6
  20. embkit-0.5/src/embkit/files/json.py +34 -0
  21. embkit-0.5/src/embkit/losses/__init__.py +23 -0
  22. embkit-0.5/src/embkit/losses/base.py +74 -0
  23. embkit-0.5/src/embkit/losses/vae_loss.py +242 -0
  24. embkit-0.5/src/embkit/models/pair.py +70 -0
  25. {embkit-0.4 → embkit-0.5}/src/embkit/models/vae/__init__.py +3 -4
  26. embkit-0.5/src/embkit/models/vae/decoder.py +115 -0
  27. embkit-0.5/src/embkit/models/vae/encoder.py +384 -0
  28. {embkit-0.4 → embkit-0.5}/src/embkit/models/vae/net_vae.py +146 -16
  29. {embkit-0.4 → embkit-0.5}/src/embkit/models/vae/rna_vae.py +73 -143
  30. embkit-0.5/src/embkit/models/vae/vae.py +400 -0
  31. embkit-0.5/src/embkit/modules/mha.py +135 -0
  32. {embkit-0.4 → embkit-0.5}/src/embkit/optimize/__init__.py +43 -12
  33. embkit-0.5/src/embkit/optimize/multitask.py +276 -0
  34. {embkit-0.4 → embkit-0.5}/src/embkit/pathway.py +3 -2
  35. {embkit-0.4 → embkit-0.5}/src/embkit/preprocessing/normalize.py +36 -1
  36. {embkit-0.4 → embkit-0.5}/src/embkit/resources/c_bio_portal.py +9 -0
  37. {embkit-0.4 → embkit-0.5}/src/embkit/resources/gtex.py +21 -5
  38. {embkit-0.4 → embkit-0.5}/src/embkit/resources/resource.py +38 -4
  39. embkit-0.5/src/embkit.egg-info/PKG-INFO +89 -0
  40. {embkit-0.4 → embkit-0.5}/src/embkit.egg-info/SOURCES.txt +4 -1
  41. embkit-0.4/PKG-INFO +0 -71
  42. embkit-0.4/README.md +0 -49
  43. embkit-0.4/src/embkit/c_bio/api.py +0 -16
  44. embkit-0.4/src/embkit/commands/matrix.py +0 -54
  45. embkit-0.4/src/embkit/datasets/__init__.py +0 -51
  46. embkit-0.4/src/embkit/factory/core.py +0 -100
  47. embkit-0.4/src/embkit/files/json.py +0 -20
  48. embkit-0.4/src/embkit/losses/__init__.py +0 -1
  49. embkit-0.4/src/embkit/losses/vae_loss.py +0 -114
  50. embkit-0.4/src/embkit/models/vae/base_vae.py +0 -240
  51. embkit-0.4/src/embkit/models/vae/decoder.py +0 -97
  52. embkit-0.4/src/embkit/models/vae/encoder.py +0 -180
  53. embkit-0.4/src/embkit/models/vae/vae.py +0 -111
  54. embkit-0.4/src/embkit.egg-info/PKG-INFO +0 -71
  55. {embkit-0.4 → embkit-0.5}/setup.cfg +0 -0
  56. {embkit-0.4 → embkit-0.5}/src/embkit/__init__.py +0 -0
  57. {embkit-0.4 → embkit-0.5}/src/embkit/__main__.py +0 -0
  58. {embkit-0.4 → embkit-0.5}/src/embkit/bmeg.py +0 -0
  59. {embkit-0.4 → embkit-0.5}/src/embkit/c_bio/__init__.py +0 -0
  60. {embkit-0.4 → embkit-0.5}/src/embkit/commands/__init__.py +0 -0
  61. {embkit-0.4 → embkit-0.5}/src/embkit/commands/align.py +0 -0
  62. {embkit-0.4 → embkit-0.5}/src/embkit/commands/cbio.py +0 -0
  63. {embkit-0.4 → embkit-0.5}/src/embkit/commands/protein.py +0 -0
  64. {embkit-0.4 → embkit-0.5}/src/embkit/commands/resources.py +0 -0
  65. {embkit-0.4 → embkit-0.5}/src/embkit/constraints/__init__.py +0 -0
  66. {embkit-0.4 → embkit-0.5}/src/embkit/encoding/protein.py +0 -0
  67. {embkit-0.4 → embkit-0.5}/src/embkit/estimator/__init__.py +0 -0
  68. {embkit-0.4 → embkit-0.5}/src/embkit/files/__init__.py +0 -0
  69. {embkit-0.4 → embkit-0.5}/src/embkit/files/loaders.py +0 -0
  70. {embkit-0.4 → embkit-0.5}/src/embkit/files/read_csv.py +0 -0
  71. {embkit-0.4 → embkit-0.5}/src/embkit/metrics/__init__.py +0 -0
  72. {embkit-0.4 → embkit-0.5}/src/embkit/metrics/distance/__init__.py +0 -0
  73. {embkit-0.4 → embkit-0.5}/src/embkit/metrics/distance/metrics.py +0 -0
  74. {embkit-0.4 → embkit-0.5}/src/embkit/models/__init__.py +0 -0
  75. {embkit-0.4 → embkit-0.5}/src/embkit/models/ffnn.py +0 -0
  76. {embkit-0.4 → embkit-0.5}/src/embkit/modules/__init__.py +0 -0
  77. {embkit-0.4 → embkit-0.5}/src/embkit/modules/masked_linear.py +0 -0
  78. {embkit-0.4 → embkit-0.5}/src/embkit/modules/pairwise_comparison_layer.py +0 -0
  79. {embkit-0.4 → embkit-0.5}/src/embkit/modules/tsp.py +0 -0
  80. {embkit-0.4 → embkit-0.5}/src/embkit/preprocessing/__init__.py +0 -0
  81. {embkit-0.4 → embkit-0.5}/src/embkit/preprocessing/dataset.py +0 -0
  82. {embkit-0.4 → embkit-0.5}/src/embkit/resources/__init__.py +0 -0
  83. {embkit-0.4 → embkit-0.5}/src/embkit/resources/hugo.py +0 -0
  84. {embkit-0.4 → embkit-0.5}/src/embkit/resources/sif.py +0 -0
  85. {embkit-0.4 → embkit-0.5}/src/embkit/resources/util.py +0 -0
  86. {embkit-0.4 → embkit-0.5}/src/embkit/utilities/__init__.py +0 -0
  87. {embkit-0.4 → embkit-0.5}/src/embkit/utilities/kmeans.py +0 -0
  88. {embkit-0.4 → embkit-0.5}/src/embkit/utilities/pca.py +0 -0
  89. {embkit-0.4 → embkit-0.5}/src/embkit/version.py +0 -0
  90. {embkit-0.4 → embkit-0.5}/src/embkit.egg-info/dependency_links.txt +0 -0
  91. {embkit-0.4 → embkit-0.5}/src/embkit.egg-info/entry_points.txt +0 -0
  92. {embkit-0.4 → embkit-0.5}/src/embkit.egg-info/requires.txt +0 -0
  93. {embkit-0.4 → embkit-0.5}/src/embkit.egg-info/top_level.txt +0 -0
  94. {embkit-0.4 → embkit-0.5}/tests/test_main.py +0 -0
embkit-0.5/PKG-INFO ADDED
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: embkit
3
+ Version: 0.5
4
+ Summary: Embedding Kit for embedding models
5
+ Author-email: Kyle Ellrott <ellrott@ohsu.edu>, Raphael Kirchgaessner <kirchgae@ohsu.edu>
6
+ License-Expression: MIT
7
+ Keywords: embeddings,machine learning,deep learning
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: torch>=2.0
11
+ Requires-Dist: pandas
12
+ Requires-Dist: numpy
13
+ Requires-Dist: click
14
+ Requires-Dist: scikit-learn
15
+ Requires-Dist: scipy
16
+ Requires-Dist: gripql
17
+ Requires-Dist: tqdm
18
+ Requires-Dist: biopython
19
+ Requires-Dist: fair-esm
20
+ Requires-Dist: faiss-cpu
21
+ Requires-Dist: h5py
22
+
23
+
24
+ # Embedding Kit
25
+
26
+ Methods for data normalization, embedding, synthesis and transformation in computational biology workflows.
27
+
28
+ Embedding Kit (embkit) is a toolkit for building and applying embedding models. It combines:
29
+
30
+ - CLI commands for repeatable model training and encoding
31
+ - PyTorch VAE model components for custom pipelines
32
+ - Utilities for loading, normalizing, and aligning large molecular datasets
33
+ - ESM2-based protein sequence embeddings
34
+
35
+ Use Embedding Kit when you want to move from tabular molecular data (RNA-seq, proteomics, methylation) to trainable latent representations that can be reused for downstream analysis.
36
+
37
+ ## Features
38
+
39
+ - **VAE & NetVAE training** — Train variational autoencoders, including pathway-constrained NetVAE models, from tabular or HDF5 matrices.
40
+ - **Beta-KL scheduling** — Schedule the KL-regularization weight across training epochs for stable convergence.
41
+ - **Protein embeddings** — Generate sequence embeddings from FASTA files using ESM2 models.
42
+ - **Normalization utilities** — Min-max and exponential min-max normalization for expression matrices.
43
+ - **Device auto-detection** — Runs on CPU, CUDA, or Apple Metal GPUs.
44
+ - **Python API** — Composable layers, losses, and model factories for custom pipelines beyond the CLI.
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install embkit
50
+ ```
51
+
52
+
53
+ ## Training in Python
54
+
55
+ ```python
56
+ from embkit import dataframe_loader
57
+ from embkit.models.vae import VAE
58
+ from embkit.factory.layers import Layer
59
+ from embkit.losses import BCEWithLogitsVAELoss
60
+ from embkit.factory import save, load
61
+ from embkit import optimize
62
+
63
+ loader = dataframe_loader(df_norm, batch_size=256)
64
+
65
+ vae = VAE(
66
+ features=list(df_norm.columns),
67
+ latent_dim=128,
68
+ encoder_layers=[Layer(512, activation="relu"), Layer(256, activation="relu")],
69
+ decoder_layers=[Layer(512, activation="relu")],
70
+ )
71
+
72
+ optimize.fit_vae(vae, X=loader, epochs=60, lr=1e-3, loss=BCEWithLogitsVAELoss())
73
+
74
+ save(vae, "vae.model")
75
+ ```
76
+
77
+ ## Documentation
78
+
79
+ Full documentation, including core concepts, the training guide, the CLI reference, and API docs, is available in the [docs folder](docs/). To build it locally, see [DEV.md](DEV.md).
80
+
81
+ ## License
82
+
83
+ MIT
84
+
85
+ ## Authors
86
+
87
+ - Kyle Ellrott — ellrott@ohsu.edu
88
+ - Raphael Kirchgaessner — kirchgae@ohsu.edu
89
+
embkit-0.5/README.md ADDED
@@ -0,0 +1,67 @@
1
+
2
+ # Embedding Kit
3
+
4
+ Methods for data normalization, embedding, synthesis and transformation in computational biology workflows.
5
+
6
+ Embedding Kit (embkit) is a toolkit for building and applying embedding models. It combines:
7
+
8
+ - CLI commands for repeatable model training and encoding
9
+ - PyTorch VAE model components for custom pipelines
10
+ - Utilities for loading, normalizing, and aligning large molecular datasets
11
+ - ESM2-based protein sequence embeddings
12
+
13
+ Use Embedding Kit when you want to move from tabular molecular data (RNA-seq, proteomics, methylation) to trainable latent representations that can be reused for downstream analysis.
14
+
15
+ ## Features
16
+
17
+ - **VAE & NetVAE training** — Train variational autoencoders, including pathway-constrained NetVAE models, from tabular or HDF5 matrices.
18
+ - **Beta-KL scheduling** — Schedule the KL-regularization weight across training epochs for stable convergence.
19
+ - **Protein embeddings** — Generate sequence embeddings from FASTA files using ESM2 models.
20
+ - **Normalization utilities** — Min-max and exponential min-max normalization for expression matrices.
21
+ - **Device auto-detection** — Runs on CPU, CUDA, or Apple Metal GPUs.
22
+ - **Python API** — Composable layers, losses, and model factories for custom pipelines beyond the CLI.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install embkit
28
+ ```
29
+
30
+
31
+ ## Training in Python
32
+
33
+ ```python
34
+ from embkit import dataframe_loader
35
+ from embkit.models.vae import VAE
36
+ from embkit.factory.layers import Layer
37
+ from embkit.losses import BCEWithLogitsVAELoss
38
+ from embkit.factory import save, load
39
+ from embkit import optimize
40
+
41
+ loader = dataframe_loader(df_norm, batch_size=256)
42
+
43
+ vae = VAE(
44
+ features=list(df_norm.columns),
45
+ latent_dim=128,
46
+ encoder_layers=[Layer(512, activation="relu"), Layer(256, activation="relu")],
47
+ decoder_layers=[Layer(512, activation="relu")],
48
+ )
49
+
50
+ optimize.fit_vae(vae, X=loader, epochs=60, lr=1e-3, loss=BCEWithLogitsVAELoss())
51
+
52
+ save(vae, "vae.model")
53
+ ```
54
+
55
+ ## Documentation
56
+
57
+ Full documentation, including core concepts, the training guide, the CLI reference, and API docs, is available in the [docs folder](docs/). To build it locally, see [DEV.md](DEV.md).
58
+
59
+ ## License
60
+
61
+ MIT
62
+
63
+ ## Authors
64
+
65
+ - Kyle Ellrott — ellrott@ohsu.edu
66
+ - Raphael Kirchgaessner — kirchgae@ohsu.edu
67
+
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "embkit"
7
- version = "0.4"
7
+ version = "0.5"
8
8
  description = "Embedding Kit for embedding models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
11
- license = { text = "MIT" }
11
+ license = "MIT"
12
12
  authors = [
13
13
  { name = "Kyle Ellrott", email = "ellrott@ohsu.edu" },
14
14
  { name = "Raphael Kirchgaessner", email = "kirchgae@ohsu.edu" }
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='embedding-kit',
5
- version='0.4',
5
+ version='0.5',
6
6
  packages=find_packages(),
7
7
  install_requires=[
8
8
  # List your dependencies here!
@@ -58,8 +58,7 @@ def matrix_spearman_alignment_linear(a, b, cutoff=0.0):
58
58
 
59
59
  def procrustes(X, Y):
60
60
  """
61
- Computes the Procrustes transformation (optimal PURE rotation)
62
- to align X to Y, correcting for reflections.
61
+ Computes the Procrustes transformation (optimal PURE rotation) to align X to Y, correcting for reflections.
63
62
 
64
63
  Args:
65
64
  X: The first matrix (N_points, N_dims).
@@ -67,6 +66,7 @@ def procrustes(X, Y):
67
66
 
68
67
  Returns:
69
68
  R: The optimal rotation matrix (guaranteed det(R) = +1).
69
+ S: Singular values of X.T @ Y
70
70
  """
71
71
  assert X.shape == Y.shape
72
72
 
@@ -74,37 +74,65 @@ def procrustes(X, Y):
74
74
 
75
75
  U, S, Vt = np.linalg.svd(M)
76
76
 
77
- R = np.dot(U, Vt)
77
+ R = np.dot(U, Vt) # R can be optimized indep. of scalar
78
78
 
79
79
  if np.linalg.det(R) < 0:
80
80
  Vt_corrected = Vt.copy()
81
81
  Vt_corrected[-1, :] *= -1
82
82
  R = np.dot(U, Vt_corrected)
83
83
 
84
- return R
85
-
84
+ return R,S
86
85
 
87
86
  def procrustes_scale(X, Y):
88
87
  """
89
- Compute the procrustes transformation (R), then compute a factor (k) to rescale the src matrix.
88
+ Compute the procrustes transformation (R), then a single (scalar) scale factor k = ||Y||_F / ||X||_F (equivalently ||Y||_F / ||X @ R||_F, since rotation preserves Frobenius norm).
89
+ Note: Scaling is calculated without Least-Squares, unlike more traditional approaches, to match Y's spread directly and avoid shrinking under noisy correspondances.
90
90
 
91
- To apply transformtion:
91
+ To apply transformation:
92
92
  src.dot(R) * k
93
93
 
94
-
95
94
  Args:
96
95
  X: The first matrix (N_points, N_dims).
97
96
  Y: The second matrix (N_points, N_dims).
98
97
 
99
98
  Returns:
100
99
  R: The optimal rotation matrix (guaranteed det(R) = +1).
101
- k: Scaling factor
100
+ k: Scalar scaling factor, ||Y||_F / ||X||_F
101
+ S: Singular values of X.T @ Y
102
102
 
103
103
  """
104
- R = procrustes(X,Y)
105
- A = np.array(X.dot(R))
106
- B = np.array(Y)
107
- numerators = np.sum(A * B, axis=0)
108
- denominators = np.sum(A * A, axis=0)
109
- k = np.divide(numerators, denominators, out=np.zeros_like(numerators), where=denominators!=0)
110
- return R, k
104
+ R, S = procrustes(X, Y)
105
+ Xnorm = np.linalg.norm(X)
106
+ k = np.linalg.norm(Y) / Xnorm if Xnorm != 0 else 0.0 # Unlike Least Squares (superceded), we dont bias towards shrinking under noisy correspondence.
107
+ return R, k, S
108
+
109
+ def procrustes_scale_centered(X, Y):
110
+ """
111
+ Mean-centers X and Y, fits the rotation, then scales: k = ||Yc||_F / ||Xc||_F (equivalently ||Yc||_F / ||Xc @ R||_F, since rotation preserves Frobenius norm).
112
+ Note: Scaling is calculated without Least-Squares, unlike more traditional approaches, to match Y's spread directly and avoid shrinking under noisy correspondances.
113
+
114
+ To apply transformation:
115
+ (src - Xmean).dot(R) * k + Ymean
116
+
117
+ Args:
118
+ X: The first matrix (N_points, N_dims).
119
+ Y: The second matrix (N_points, N_dims).
120
+
121
+ Returns:
122
+ R: The optimal rotation matrix (guaranteed det(R) = +1).
123
+ k: Scalar scaling factor, ||Yc||_F / ||Xc||_F
124
+ Xmean: Per-dimension mean of X (shape: (N_dims,)), subtract before applying R/k
125
+ Ymean: Per-dimension mean of Y (shape: (N_dims,)), add back after applying R/k
126
+ S: Singular values of (X - Xmean).T @ (Y - Ymean)
127
+
128
+ """
129
+ Xmean = np.array(X).mean(axis=0)
130
+ Ymean = np.array(Y).mean(axis=0)
131
+ Xc = np.array(X) - Xmean
132
+ Yc = np.array(Y) - Ymean
133
+ R, k, S = procrustes_scale(Xc, Yc)
134
+ return R, k, Xmean, Ymean, S
135
+
136
+
137
+
138
+
@@ -0,0 +1,31 @@
1
+ import requests
2
+ import logging
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+
7
+ class CBIOAPI:
8
+ """
9
+ Small client for the cBioPortal public API.
10
+
11
+ cBioPortal is a public catalog of cancer genomics studies. This helper
12
+ wraps the lightweight metadata endpoints (like listing available studies)
13
+ that don't require downloading study data — use the
14
+ :class:`~embkit.resources.c_bio_portal.CBIOPortal` resource for fetching an
15
+ actual study's molecular profile data.
16
+ """
17
+
18
+ @staticmethod
19
+ def list_studies():
20
+ """
21
+ List the cBioPortal studies available for download.
22
+
23
+ Returns:
24
+ The API's JSON list of studies, or ``None`` if the request failed.
25
+ """
26
+ try:
27
+ studies = requests.get("https://www.cbioportal.org/api/studies").json()
28
+ return studies
29
+ except requests.RequestException as e:
30
+ logger.error("Error fetching studies: %s", e)
31
+ return None
@@ -0,0 +1,97 @@
1
+ """
2
+ embkit matrix — preprocessing of expression/count matrices.
3
+
4
+ The ``matrix`` command group prepares the tab-separated matrices that become the
5
+ input to an embedding model. Its subcommands implement the "normalize" and "pca"
6
+ major operations: rescaling raw feature values to a comparable ``[0, 1]`` range,
7
+ and projecting a matrix onto its leading principal components. These are the
8
+ data-preparation steps you run before (and independently of) training.
9
+ """
10
+ import click
11
+ import pandas as pd
12
+ from pathlib import Path
13
+ from ..utilities.pca import run_pca
14
+
15
+
16
+ matrix = click.Group(name="matrix", help="Model commands.")
17
+
18
+ @matrix.command()
19
+ @click.argument("srcs", nargs=-1)
20
+ @click.option("--out", "-o", required=True)
21
+ @click.option("--features", default=None, type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str))
22
+ @click.option("--col-quantile", "col_quantile", default=False, is_flag=True)
23
+ @click.option("--quantile-max", "quantile_max", default=0.9, type=float)
24
+ @click.option("--precision", "-p", type=int, default=5)
25
+ def normalize(srcs, out, features, col_quantile, quantile_max, precision):
26
+ """
27
+ embkit matrix normalize — rescale one or more matrices for embedding training.
28
+
29
+ Reads tab-separated expression/count matrices, optionally subsets them to a
30
+ list of features, concatenates them, and writes a single normalized matrix in
31
+ which each sample's values fall in ``[0, 1]``. This is the preprocessing step
32
+ that puts the input on a comparable scale before a VAE / NetVAE is trained.
33
+
34
+ By default each *sample* (row) is divided by its own ``quantile_max``
35
+ quantile; passing ``--col-quantile`` divides each *feature* (column) by its
36
+ quantile instead.
37
+
38
+ Args:
39
+ srcs: One or more input TSV matrices (index column required).
40
+ out: Path to write the normalized TSV.
41
+ features: Optional file listing feature names to keep.
42
+ col_quantile: Normalize per feature (column) rather than per sample.
43
+ quantile_max: The quantile used as the divisor (default 0.9).
44
+ precision: Number of decimals to round to in the output.
45
+ """
46
+
47
+ features_list = None
48
+ if features is not None:
49
+ with open(features, encoding="ascii") as handle:
50
+ features_list = list(line.rstrip() for line in handle)
51
+
52
+ dfs = []
53
+ for i in srcs:
54
+ df = pd.read_csv(i, sep="\t", index_col=0)
55
+ if features_list is not None:
56
+ df = df[features_list]
57
+ dfs.append(df)
58
+ if len(dfs) == 0:
59
+ click.echo("No matrices defined")
60
+ return
61
+ df = pd.concat( dfs )
62
+
63
+ if not col_quantile:
64
+ normDF = (df.transpose() / df.quantile(quantile_max, axis=1)).transpose().clip(upper=1.0, lower=0.0).fillna(0.0)
65
+ else:
66
+ normDF = (df / df.quantile(quantile_max)).clip(upper=1.0, lower=0.0).fillna(0.0)
67
+ normDF.round(decimals=precision).to_csv(out, sep="\t")
68
+
69
+ @matrix.command()
70
+ @click.argument("input_path", type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str))
71
+ @click.option("--pca-size", required=True, type=int, help="Number of principal components.")
72
+ @click.option("--out", "-o", default=None, type=str, help="Output TSV path.")
73
+ def pca(input_path, pca_size, out):
74
+ """
75
+ embkit matrix pca — project a matrix onto its leading principal components.
76
+
77
+ Runs principal component analysis on a tab-separated matrix and writes the
78
+ first ``pca_size`` components to a TSV. Useful for reducing dimensionality
79
+ before downstream analysis or visualization (e.g. inspecting whether a
80
+ normalized matrix separates samples as expected).
81
+
82
+ If no ``--out`` is given, the output defaults to ``<input_stem>.pca.tsv``.
83
+
84
+ Args:
85
+ input_path: Input TSV matrix.
86
+ pca_size: Number of principal components to retain (must be > 0).
87
+ out: Output TSV path (optional; defaults to ``<input_stem>.pca.tsv``).
88
+ """
89
+ if pca_size <= 0:
90
+ raise click.BadParameter("--pca-size must be a positive integer.")
91
+
92
+ if out is None:
93
+ out = f"{Path(input_path).stem}.pca.tsv"
94
+ click.echo(f"No output path provided, using default naming: {out}")
95
+
96
+ run_pca(input_file=input_path, pca_size=pca_size, output_file=out)
97
+ click.echo(f"PCA saved to {out}")
@@ -13,11 +13,11 @@ from ..files import H5Reader
13
13
  from ..factory import save, load
14
14
  from ..factory.layers import Layer, LayerList
15
15
  from ..optimize import fit_vae
16
- from ..models.vae.vae import VAE
16
+ from ..models.vae.vae import BaseVAE
17
17
  from ..models.vae.net_vae import NetVAE
18
18
  from ..preprocessing import ExpMinMaxScaler, get_dataset_nonzero_mask
19
19
  from ..datasets import DatasetMask
20
- from ..losses import bce_with_logits, bce, mse
20
+ from ..losses import get_vae_loss
21
21
  from ..pathway import extract_sif_interactions, feature_map_intersect, build_feature_map_indices
22
22
 
23
23
  model = click.Group(name="model", help="VAE Model commands.")
@@ -37,6 +37,7 @@ model = click.Group(name="model", help="VAE Model commands.")
37
37
  @click.option("--schedule", "-s", type=str, default=None, help="20:0,20:0.1,40:.3,40:.4")
38
38
  @click.option("--loss", type=click.Choice(["mse", "bce", "bce-logit"]), default="bce-logit")
39
39
  @click.option("--save-stats", is_flag=True)
40
+ @click.option("--save-latent", is_flag=True)
40
41
  @click.option("--zero-mask", default=None, type=float)
41
42
  @click.option("--seed", default=42, type=int)
42
43
  @click.option("--bfloat16", is_flag=True)
@@ -54,6 +55,7 @@ def train_vae(input_path: str,
54
55
  schedule:str,
55
56
  zero_mask: float,
56
57
  save_stats: bool,
58
+ save_latent:bool,
57
59
  seed: int,
58
60
  bfloat16: bool,
59
61
  sampling: bool
@@ -100,12 +102,11 @@ def train_vae(input_path: str,
100
102
 
101
103
  dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
102
104
 
103
-
104
105
  layer_sizes = list( int(i) for i in encode_layers.split(",") )
105
106
  enc_layers_list = LayerList( layer_sizes )
106
107
 
107
108
  layer_sizes = list( int(i) for i in decode_layers.split(",") )
108
- dec_layers_list = LayerList( layer_sizes, end_activation=final_activation )
109
+ dec_layers_list = LayerList( layer_sizes, output_activation=final_activation )
109
110
 
110
111
  beta_schedule = None
111
112
  if schedule is not None:
@@ -113,18 +114,14 @@ def train_vae(input_path: str,
113
114
  for b in schedule.split(","):
114
115
  e, b = b.split(":")
115
116
  beta_schedule.append( (float(b), int(e)) )
116
- vae = VAE(features=features,
117
+ vae = BaseVAE(features=features,
117
118
  latent_dim=latent,
118
119
  encoder_layers=enc_layers_list,
119
120
  decoder_layers=dec_layers_list,
120
121
  sampling=sampling,
121
122
  device=device, dtype=dtype)
122
123
 
123
- loss_func = bce_with_logits
124
- if loss == "mse":
125
- loss_func = mse
126
- elif loss == "bce":
127
- loss_func = bce
124
+ loss_func = get_vae_loss(loss)
128
125
 
129
126
  fit_vae(vae, dataloader, epochs=epochs,
130
127
  beta_schedule=beta_schedule, lr=learning_rate, loss=loss_func)
@@ -143,6 +140,46 @@ def train_vae(input_path: str,
143
140
  stats.to_csv(stats_path, sep="\t")
144
141
  click.echo(f"Stats saved, to {stats_path}")
145
142
 
143
+ if save_latent:
144
+ vae.eval() # Makes output deterministic. Otherwise, model in training.
145
+ losses_df = pd.DataFrame({
146
+ "epoch": range(1, len(vae.history["loss"]) + 1),
147
+ "loss": vae.history["loss"],
148
+ "recon": vae.history["recon"],
149
+ "kl": vae.history["kl"]})
150
+
151
+ losses_df.to_csv(f"{out}.losses_stats.tsv", sep="\t", index=False)
152
+ click.echo(f"Training losses saved, to {out}.losses_stats.tsv")
153
+
154
+ exportloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
155
+ all_mu = []
156
+ all_logvar = []
157
+
158
+ with torch.no_grad():
159
+ for batch in exportloader:
160
+ x_tensor = batch[0] if isinstance(batch, (tuple, list)) else batch
161
+ x_tensor = x_tensor.to(device=device, dtype=dtype)
162
+ _, mu, logvar, _ = vae(x_tensor)
163
+ all_mu.append(mu.cpu())
164
+ all_logvar.append(logvar.cpu())
165
+
166
+ mu = torch.cat(all_mu, dim=0)
167
+ logvar = torch.cat(all_logvar, dim=0)
168
+ std = torch.sqrt(torch.exp(logvar)) # sigma
169
+
170
+ index = df.index if df is not None else None
171
+
172
+ mu_np = mu.to(device="cpu", dtype=torch.float32).detach().numpy()
173
+ std_np = std.to(device="cpu", dtype=torch.float32).detach().numpy()
174
+ mu_df = pd.DataFrame(mu_np, index=index, columns=[f"mu_{i}" for i in range(mu.shape[1])])
175
+ std_df = pd.DataFrame(std_np, index=index, columns=[f"std_{i}" for i in range(std.shape[1])])
176
+ mu_path = f"{out}.latent_mu.tsv"
177
+ std_path = f"{out}.latent_std.tsv"
178
+ mu_df.to_csv(mu_path, sep="\t")
179
+ std_df.to_csv(std_path, sep="\t")
180
+ click.echo(f"Latent mu saved, to {mu_path}")
181
+ click.echo(f"Latent std (sigma) saved, to {std_path}")
182
+
146
183
 
147
184
  @model.command()
148
185
  @click.argument("input_path", type=click.Path(exists=True, dir_okay=False, readable=True, path_type=str))
@@ -153,7 +190,7 @@ def train_vae(input_path: str,
153
190
  @click.option("--out", "-o", type=str, default=None)
154
191
  @click.option("--schedule", "-s", type=str, default=None, help="20:0,20:0.1,40:.3,40:.4")
155
192
  @click.option("--loss", type=click.Choice(["mse", "bce", "bce-logit"]), default="bce-logit")
156
- @click.option("--min-group-size", type=int, default=0, show_default=True, help="Minimum group size filter for pathway feature map (including self if present).")
193
+ @click.option("--min-group-size", type=int, default=2, show_default=True, help="Minimum group size filter for pathway feature map (including self if present).")
157
194
  @click.option("--group-layer-scale", default="5,2,1", show_default=True,
158
195
  help="Comma-separated per-group widths for NetVAE masked layers.")
159
196
  @click.option("--save-stats", is_flag=True)
@@ -197,11 +234,7 @@ def train_netvae(input_path: str, pathway_sif:str, out:str,
197
234
  if not gcounts or any(v <= 0 for v in gcounts):
198
235
  raise click.BadParameter("--group-layer-scale must contain one or more positive integers.")
199
236
 
200
- loss_func = bce_with_logits
201
- if loss == "mse":
202
- loss_func = mse
203
- elif loss == "bce":
204
- loss_func = bce
237
+ loss_func = get_vae_loss(loss)
205
238
 
206
239
  if beta_schedule is None:
207
240
  beta_schedule = [(0.0, epochs)]
@@ -243,7 +276,7 @@ def encode(input_path: str, model_path:str, normalize:str, out:str):
243
276
  m.to(get_device())
244
277
  result = m.encoder(df_tensor)
245
278
 
246
- matrix = result[0].detach().cpu().numpy()
279
+ matrix = result[2].detach().cpu().numpy()
247
280
  out_df = pd.DataFrame(matrix, index=df.index)
248
281
  out_df.to_csv(out, sep="\t")
249
282
 
@@ -2,6 +2,7 @@ from typing import Any, Dict, List, Literal, Optional
2
2
 
3
3
  import numpy as np
4
4
 
5
+ from ..factory import nn_module
5
6
  from ..factory.layers import ConstraintInfo
6
7
  from ..pathway import (
7
8
  _normalize_index,
@@ -12,7 +13,7 @@ from ..pathway import (
12
13
 
13
14
  ConstraintOP = Literal["features-to-group", "group-to-features", "group-to-group"]
14
15
 
15
-
16
+ @nn_module
16
17
  class PathwayConstraintInfo(ConstraintInfo):
17
18
  """
18
19
  ConstraintInfo for pathway-based masking.