diffbio 0.1.0__py3-none-any.whl
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.
- diffbio/__init__.py +39 -0
- diffbio/configs.py +75 -0
- diffbio/constants.py +204 -0
- diffbio/core/__init__.py +127 -0
- diffbio/core/base_operators.py +612 -0
- diffbio/core/data_types.py +260 -0
- diffbio/core/gnn_components.py +629 -0
- diffbio/core/graph_utils.py +149 -0
- diffbio/core/neural_components.py +270 -0
- diffbio/core/optimal_transport.py +133 -0
- diffbio/core/soft_ops/__init__.py +216 -0
- diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
- diffbio/core/soft_ops/_projections_simplex.py +240 -0
- diffbio/core/soft_ops/_projections_transport.py +508 -0
- diffbio/core/soft_ops/_sorting_network.py +204 -0
- diffbio/core/soft_ops/_types.py +15 -0
- diffbio/core/soft_ops/_utils.py +342 -0
- diffbio/core/soft_ops/autograd_safe.py +120 -0
- diffbio/core/soft_ops/comparison.py +235 -0
- diffbio/core/soft_ops/elementwise.py +309 -0
- diffbio/core/soft_ops/logical.py +146 -0
- diffbio/core/soft_ops/quantile.py +376 -0
- diffbio/core/soft_ops/selection.py +236 -0
- diffbio/core/soft_ops/sorting.py +926 -0
- diffbio/core/soft_ops/straight_through.py +261 -0
- diffbio/core/uncertainty.py +279 -0
- diffbio/evaluation/__init__.py +42 -0
- diffbio/evaluation/adapters.py +409 -0
- diffbio/evaluation/graders.py +223 -0
- diffbio/evaluation/problem.py +157 -0
- diffbio/evaluation/runner.py +277 -0
- diffbio/losses/__init__.py +59 -0
- diffbio/losses/alignment_losses.py +222 -0
- diffbio/losses/biological_regularization.py +288 -0
- diffbio/losses/metric_losses.py +139 -0
- diffbio/losses/singlecell_losses.py +387 -0
- diffbio/losses/statistical_losses.py +345 -0
- diffbio/operators/__init__.py +60 -0
- diffbio/operators/_count_vae.py +197 -0
- diffbio/operators/_loss_balancing.py +65 -0
- diffbio/operators/_masked_gene_transformer.py +118 -0
- diffbio/operators/_transformer_validation.py +50 -0
- diffbio/operators/alignment/__init__.py +51 -0
- diffbio/operators/alignment/profile_hmm.py +350 -0
- diffbio/operators/alignment/scoring.py +127 -0
- diffbio/operators/alignment/smith_waterman.py +261 -0
- diffbio/operators/alignment/soft_msa.py +419 -0
- diffbio/operators/assembly/__init__.py +27 -0
- diffbio/operators/assembly/gnn_assembly.py +252 -0
- diffbio/operators/assembly/metagenomic_binning.py +296 -0
- diffbio/operators/crispr/__init__.py +17 -0
- diffbio/operators/crispr/guide_scoring.py +269 -0
- diffbio/operators/drug_discovery/__init__.py +133 -0
- diffbio/operators/drug_discovery/_graph_utils.py +142 -0
- diffbio/operators/drug_discovery/admet_predictor.py +285 -0
- diffbio/operators/drug_discovery/attentive_fp.py +411 -0
- diffbio/operators/drug_discovery/dti.py +261 -0
- diffbio/operators/drug_discovery/fingerprint.py +490 -0
- diffbio/operators/drug_discovery/maccs_keys.py +267 -0
- diffbio/operators/drug_discovery/message_passing.py +200 -0
- diffbio/operators/drug_discovery/primitives.py +242 -0
- diffbio/operators/drug_discovery/property_predictor.py +163 -0
- diffbio/operators/drug_discovery/similarity.py +193 -0
- diffbio/operators/epigenomics/__init__.py +35 -0
- diffbio/operators/epigenomics/chromatin_state.py +491 -0
- diffbio/operators/epigenomics/contextual.py +288 -0
- diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
- diffbio/operators/epigenomics/peak_calling.py +555 -0
- diffbio/operators/foundation_models/__init__.py +119 -0
- diffbio/operators/foundation_models/adapters.py +114 -0
- diffbio/operators/foundation_models/contracts.py +245 -0
- diffbio/operators/foundation_models/embedding_probe.py +83 -0
- diffbio/operators/foundation_models/experimental.py +128 -0
- diffbio/operators/foundation_models/foundation_model.py +332 -0
- diffbio/operators/foundation_models/frozen.py +59 -0
- diffbio/operators/foundation_models/precomputed.py +270 -0
- diffbio/operators/foundation_models/transformer_encoder.py +564 -0
- diffbio/operators/mapping/__init__.py +17 -0
- diffbio/operators/mapping/neural_mapper.py +493 -0
- diffbio/operators/metabolomics/__init__.py +39 -0
- diffbio/operators/metabolomics/spectral_similarity.py +315 -0
- diffbio/operators/molecular_dynamics/__init__.py +51 -0
- diffbio/operators/molecular_dynamics/force_field.py +265 -0
- diffbio/operators/molecular_dynamics/integrator.py +304 -0
- diffbio/operators/molecular_dynamics/primitives.py +115 -0
- diffbio/operators/multiomics/__init__.py +38 -0
- diffbio/operators/multiomics/hic_contact.py +377 -0
- diffbio/operators/multiomics/multiomics_vae.py +325 -0
- diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
- diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
- diffbio/operators/normalization/__init__.py +42 -0
- diffbio/operators/normalization/embedding.py +222 -0
- diffbio/operators/normalization/phate.py +400 -0
- diffbio/operators/normalization/umap.py +261 -0
- diffbio/operators/normalization/vae_normalizer.py +258 -0
- diffbio/operators/population/__init__.py +17 -0
- diffbio/operators/population/ancestry_estimation.py +274 -0
- diffbio/operators/preprocessing/__init__.py +76 -0
- diffbio/operators/preprocessing/adapter_removal.py +311 -0
- diffbio/operators/preprocessing/duplicate_filter.py +317 -0
- diffbio/operators/preprocessing/error_correction.py +287 -0
- diffbio/operators/protein/__init__.py +31 -0
- diffbio/operators/protein/secondary_structure.py +509 -0
- diffbio/operators/quality_filter.py +128 -0
- diffbio/operators/rna_structure/__init__.py +35 -0
- diffbio/operators/rna_structure/rna_folding.py +509 -0
- diffbio/operators/rnaseq/__init__.py +23 -0
- diffbio/operators/rnaseq/motif_discovery.py +251 -0
- diffbio/operators/rnaseq/splicing_psi.py +216 -0
- diffbio/operators/singlecell/__init__.py +193 -0
- diffbio/operators/singlecell/ambient_removal.py +333 -0
- diffbio/operators/singlecell/archetypes.py +191 -0
- diffbio/operators/singlecell/batch_correction.py +288 -0
- diffbio/operators/singlecell/cell_annotation.py +519 -0
- diffbio/operators/singlecell/communication.py +704 -0
- diffbio/operators/singlecell/differential_distribution.py +243 -0
- diffbio/operators/singlecell/doublet_detection.py +657 -0
- diffbio/operators/singlecell/downsampling.py +166 -0
- diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
- diffbio/operators/singlecell/grn_inference.py +336 -0
- diffbio/operators/singlecell/imputation.py +429 -0
- diffbio/operators/singlecell/knockdown_filter.py +176 -0
- diffbio/operators/singlecell/ot_trajectory.py +277 -0
- diffbio/operators/singlecell/simulation.py +444 -0
- diffbio/operators/singlecell/sindy_grn.py +247 -0
- diffbio/operators/singlecell/soft_clustering.py +211 -0
- diffbio/operators/singlecell/spatial_domains.py +677 -0
- diffbio/operators/singlecell/switch_de.py +184 -0
- diffbio/operators/singlecell/trajectory.py +447 -0
- diffbio/operators/singlecell/velocity.py +361 -0
- diffbio/operators/statistical/__init__.py +35 -0
- diffbio/operators/statistical/em_quantification.py +260 -0
- diffbio/operators/statistical/hmm.py +234 -0
- diffbio/operators/statistical/nb_glm.py +272 -0
- diffbio/operators/variant/__init__.py +64 -0
- diffbio/operators/variant/classifier.py +333 -0
- diffbio/operators/variant/cnn_classifier.py +255 -0
- diffbio/operators/variant/cnv_segmentation.py +678 -0
- diffbio/operators/variant/deepvariant_pileup.py +426 -0
- diffbio/operators/variant/pileup.py +240 -0
- diffbio/operators/variant/quality_recalibration.py +274 -0
- diffbio/pipelines/__init__.py +65 -0
- diffbio/pipelines/differential_expression.py +279 -0
- diffbio/pipelines/enhanced_variant_calling.py +326 -0
- diffbio/pipelines/perturbation.py +407 -0
- diffbio/pipelines/preprocessing.py +267 -0
- diffbio/pipelines/single_cell.py +366 -0
- diffbio/pipelines/variant_calling.py +490 -0
- diffbio/samplers/__init__.py +9 -0
- diffbio/samplers/perturbation_sampler.py +142 -0
- diffbio/sequences/__init__.py +34 -0
- diffbio/sequences/dna.py +239 -0
- diffbio/sources/__init__.py +149 -0
- diffbio/sources/_anndata_shared.py +89 -0
- diffbio/sources/_batch_iteration.py +37 -0
- diffbio/sources/_benchmark_source.py +152 -0
- diffbio/sources/_indexed_batch_source.py +38 -0
- diffbio/sources/_utils.py +45 -0
- diffbio/sources/anndata_interop.py +387 -0
- diffbio/sources/anndata_source.py +361 -0
- diffbio/sources/archive_ii.py +174 -0
- diffbio/sources/balifam.py +207 -0
- diffbio/sources/bam.py +265 -0
- diffbio/sources/bengrn_ground_truth.py +306 -0
- diffbio/sources/contextual_epigenomics.py +242 -0
- diffbio/sources/dti.py +359 -0
- diffbio/sources/embeddings.py +203 -0
- diffbio/sources/encode_peaks.py +223 -0
- diffbio/sources/fasta.py +226 -0
- diffbio/sources/immune_human.py +172 -0
- diffbio/sources/indexed_embeddings.py +128 -0
- diffbio/sources/indexed_view.py +191 -0
- diffbio/sources/molnet.py +493 -0
- diffbio/sources/multiomics.py +279 -0
- diffbio/sources/pancreas.py +108 -0
- diffbio/sources/perturbation/__init__.py +69 -0
- diffbio/sources/perturbation/_types.py +51 -0
- diffbio/sources/perturbation/_utils.py +125 -0
- diffbio/sources/perturbation/concat_source.py +115 -0
- diffbio/sources/perturbation/control_mapping.py +215 -0
- diffbio/sources/perturbation/experiment_config.py +261 -0
- diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
- diffbio/sources/perturbation/output_space.py +52 -0
- diffbio/sources/perturbation/perturbation_source.py +513 -0
- diffbio/sources/seqfish.py +145 -0
- diffbio/sources/sequence_foundation.py +68 -0
- diffbio/sources/singlecell_foundation.py +68 -0
- diffbio/splitters/__init__.py +63 -0
- diffbio/splitters/base.py +251 -0
- diffbio/splitters/molecular.py +330 -0
- diffbio/splitters/perturbation.py +199 -0
- diffbio/splitters/random.py +217 -0
- diffbio/splitters/sequence.py +201 -0
- diffbio/utils/__init__.py +55 -0
- diffbio/utils/dependency_runtime.py +115 -0
- diffbio/utils/nn_utils.py +157 -0
- diffbio/utils/quality.py +45 -0
- diffbio/utils/training.py +585 -0
- diffbio-0.1.0.dist-info/METADATA +480 -0
- diffbio-0.1.0.dist-info/RECORD +202 -0
- diffbio-0.1.0.dist-info/WHEEL +4 -0
- diffbio-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Hi-C chromatin contact analysis operator.
|
|
2
|
+
|
|
3
|
+
This module provides differentiable analysis of Hi-C contact matrices
|
|
4
|
+
for chromatin structure inference.
|
|
5
|
+
|
|
6
|
+
Key technique: Uses neural network to learn bin embeddings from contact
|
|
7
|
+
patterns, then predicts compartments and TAD boundaries using attention
|
|
8
|
+
over neighboring bins.
|
|
9
|
+
|
|
10
|
+
Applications: Chromatin compartment identification, TAD boundary detection,
|
|
11
|
+
3D genome structure prediction.
|
|
12
|
+
|
|
13
|
+
Inherits from TemperatureOperator to get:
|
|
14
|
+
|
|
15
|
+
- _temperature property for temperature-controlled smoothing
|
|
16
|
+
- soft_max() for logsumexp-based smooth maximum
|
|
17
|
+
- soft_argmax() for soft position selection
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import jax
|
|
25
|
+
import jax.numpy as jnp
|
|
26
|
+
from artifex.generative_models.core.base import MLP
|
|
27
|
+
from datarax.core.config import OperatorConfig
|
|
28
|
+
from flax import nnx
|
|
29
|
+
from jaxtyping import Array, Float, PyTree
|
|
30
|
+
|
|
31
|
+
from diffbio.core.base_operators import TemperatureOperator
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class HiCContactAnalysisConfig(OperatorConfig):
|
|
38
|
+
"""Configuration for HiCContactAnalysis.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
n_bins: Number of genomic bins.
|
|
42
|
+
hidden_dim: Hidden dimension for neural networks.
|
|
43
|
+
num_layers: Number of encoder layers.
|
|
44
|
+
num_heads: Number of attention heads.
|
|
45
|
+
bin_features: Dimension of input bin features.
|
|
46
|
+
dropout_rate: Dropout rate for regularization.
|
|
47
|
+
temperature: Temperature for softmax operations.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
n_bins: int = 1000
|
|
51
|
+
hidden_dim: int = 128
|
|
52
|
+
num_layers: int = 3
|
|
53
|
+
num_heads: int = 4
|
|
54
|
+
bin_features: int = 16
|
|
55
|
+
dropout_rate: float = 0.1
|
|
56
|
+
temperature: float = 1.0
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
"""Validate Hi-C contact analysis configuration."""
|
|
60
|
+
super().__post_init__()
|
|
61
|
+
if self.num_layers < 1:
|
|
62
|
+
raise ValueError("HiCContactAnalysisConfig.num_layers must be at least 1.")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ContactEncoder(nnx.Module):
|
|
66
|
+
"""Encoder for Hi-C contact patterns."""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
n_bins: int,
|
|
71
|
+
hidden_dim: int,
|
|
72
|
+
num_layers: int,
|
|
73
|
+
*,
|
|
74
|
+
rngs: nnx.Rngs,
|
|
75
|
+
):
|
|
76
|
+
"""Initialize the contact encoder.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
n_bins: Number of bins.
|
|
80
|
+
hidden_dim: Hidden dimension.
|
|
81
|
+
num_layers: Number of layers.
|
|
82
|
+
rngs: Random number generators.
|
|
83
|
+
"""
|
|
84
|
+
super().__init__()
|
|
85
|
+
self.backbone = MLP(
|
|
86
|
+
hidden_dims=[hidden_dim] * num_layers,
|
|
87
|
+
in_features=n_bins,
|
|
88
|
+
activation="gelu",
|
|
89
|
+
output_activation="gelu",
|
|
90
|
+
use_batch_norm=False,
|
|
91
|
+
rngs=rngs,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def __call__(
|
|
95
|
+
self,
|
|
96
|
+
contact_matrix: Float[Array, "n_bins n_bins"],
|
|
97
|
+
) -> Float[Array, "n_bins hidden_dim"]:
|
|
98
|
+
"""Encode contact patterns.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
contact_matrix: Hi-C contact matrix.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Bin embeddings from contact patterns.
|
|
105
|
+
"""
|
|
106
|
+
backbone_output = self.backbone(contact_matrix)
|
|
107
|
+
if isinstance(backbone_output, tuple):
|
|
108
|
+
raise TypeError("Hi-C contact encoder backbone must return a single tensor.")
|
|
109
|
+
return backbone_output
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class BinFeatureEncoder(nnx.Module):
|
|
113
|
+
"""Encoder for genomic bin features."""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
bin_features: int,
|
|
118
|
+
hidden_dim: int,
|
|
119
|
+
*,
|
|
120
|
+
rngs: nnx.Rngs,
|
|
121
|
+
):
|
|
122
|
+
"""Initialize the bin feature encoder.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
bin_features: Input feature dimension.
|
|
126
|
+
hidden_dim: Hidden dimension.
|
|
127
|
+
rngs: Random number generators.
|
|
128
|
+
"""
|
|
129
|
+
super().__init__()
|
|
130
|
+
self.backbone = MLP(
|
|
131
|
+
hidden_dims=[hidden_dim, hidden_dim],
|
|
132
|
+
in_features=bin_features,
|
|
133
|
+
activation="gelu",
|
|
134
|
+
output_activation=None,
|
|
135
|
+
use_batch_norm=False,
|
|
136
|
+
rngs=rngs,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def __call__(
|
|
140
|
+
self,
|
|
141
|
+
bin_features: Float[Array, "n_bins bin_features"],
|
|
142
|
+
) -> Float[Array, "n_bins hidden_dim"]:
|
|
143
|
+
"""Encode bin features.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
bin_features: Genomic bin features.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Bin feature embeddings.
|
|
150
|
+
"""
|
|
151
|
+
backbone_output = self.backbone(bin_features)
|
|
152
|
+
if isinstance(backbone_output, tuple):
|
|
153
|
+
raise TypeError("Hi-C feature encoder backbone must return a single tensor.")
|
|
154
|
+
return backbone_output
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class LocalAttention(nnx.Module):
|
|
158
|
+
"""Local attention for TAD boundary detection."""
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
hidden_dim: int,
|
|
163
|
+
num_heads: int,
|
|
164
|
+
*,
|
|
165
|
+
rngs: nnx.Rngs,
|
|
166
|
+
):
|
|
167
|
+
"""Initialize local attention.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
hidden_dim: Hidden dimension.
|
|
171
|
+
num_heads: Number of attention heads.
|
|
172
|
+
rngs: Random number generators.
|
|
173
|
+
"""
|
|
174
|
+
super().__init__()
|
|
175
|
+
|
|
176
|
+
self.num_heads = num_heads
|
|
177
|
+
self.head_dim = hidden_dim // num_heads
|
|
178
|
+
self.scale = self.head_dim**-0.5
|
|
179
|
+
|
|
180
|
+
self.query = nnx.Linear(in_features=hidden_dim, out_features=hidden_dim, rngs=rngs)
|
|
181
|
+
self.key = nnx.Linear(in_features=hidden_dim, out_features=hidden_dim, rngs=rngs)
|
|
182
|
+
self.value = nnx.Linear(in_features=hidden_dim, out_features=hidden_dim, rngs=rngs)
|
|
183
|
+
self.output = nnx.Linear(in_features=hidden_dim, out_features=hidden_dim, rngs=rngs)
|
|
184
|
+
|
|
185
|
+
def __call__(
|
|
186
|
+
self,
|
|
187
|
+
x: Float[Array, "n_bins hidden_dim"],
|
|
188
|
+
) -> Float[Array, "n_bins hidden_dim"]:
|
|
189
|
+
"""Apply local attention.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
x: Input embeddings.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Attended embeddings.
|
|
196
|
+
"""
|
|
197
|
+
n_bins = x.shape[0]
|
|
198
|
+
|
|
199
|
+
Q = self.query(x).reshape(n_bins, self.num_heads, self.head_dim)
|
|
200
|
+
K = self.key(x).reshape(n_bins, self.num_heads, self.head_dim)
|
|
201
|
+
V = self.value(x).reshape(n_bins, self.num_heads, self.head_dim)
|
|
202
|
+
|
|
203
|
+
# Full attention (could be windowed for efficiency)
|
|
204
|
+
attn = jnp.einsum("qhd,khd->hqk", Q, K) * self.scale
|
|
205
|
+
attn = jax.nn.softmax(attn, axis=-1)
|
|
206
|
+
|
|
207
|
+
out = jnp.einsum("hqk,khd->qhd", attn, V)
|
|
208
|
+
out = out.reshape(n_bins, -1)
|
|
209
|
+
out = self.output(out)
|
|
210
|
+
|
|
211
|
+
return out
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class HiCContactAnalysis(TemperatureOperator):
|
|
215
|
+
"""Differentiable Hi-C contact analysis.
|
|
216
|
+
|
|
217
|
+
This operator analyzes Hi-C contact matrices to identify
|
|
218
|
+
chromatin compartments and TAD boundaries using neural networks.
|
|
219
|
+
|
|
220
|
+
Algorithm:
|
|
221
|
+
1. Encode contact patterns per bin
|
|
222
|
+
2. Encode genomic bin features
|
|
223
|
+
3. Combine contact and feature embeddings
|
|
224
|
+
4. Apply attention for context
|
|
225
|
+
5. Predict compartment scores
|
|
226
|
+
6. Detect TAD boundaries
|
|
227
|
+
7. Reconstruct contacts from embeddings
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
config: HiCContactAnalysisConfig with model parameters.
|
|
231
|
+
rngs: Flax NNX random number generators.
|
|
232
|
+
name: Optional operator name.
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
```python
|
|
236
|
+
config = HiCContactAnalysisConfig(n_bins=1000)
|
|
237
|
+
analyzer = HiCContactAnalysis(config, rngs=nnx.Rngs(42))
|
|
238
|
+
data = {"contact_matrix": contacts, "bin_features": features}
|
|
239
|
+
result, state, meta = analyzer.apply(data, {}, None)
|
|
240
|
+
```
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def __init__(
|
|
244
|
+
self,
|
|
245
|
+
config: HiCContactAnalysisConfig,
|
|
246
|
+
*,
|
|
247
|
+
rngs: nnx.Rngs | None = None,
|
|
248
|
+
name: str | None = None,
|
|
249
|
+
):
|
|
250
|
+
"""Initialize the Hi-C contact analyzer.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
config: Analysis configuration.
|
|
254
|
+
rngs: Random number generators for initialization.
|
|
255
|
+
name: Optional operator name.
|
|
256
|
+
"""
|
|
257
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
258
|
+
|
|
259
|
+
if rngs is None:
|
|
260
|
+
rngs = nnx.Rngs(0)
|
|
261
|
+
|
|
262
|
+
self.hidden_dim = config.hidden_dim
|
|
263
|
+
# Temperature is managed by TemperatureOperator via self._temperature
|
|
264
|
+
|
|
265
|
+
# Contact pattern encoder
|
|
266
|
+
self.contact_encoder = ContactEncoder(
|
|
267
|
+
n_bins=config.n_bins,
|
|
268
|
+
hidden_dim=config.hidden_dim,
|
|
269
|
+
num_layers=config.num_layers,
|
|
270
|
+
rngs=rngs,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Bin feature encoder
|
|
274
|
+
self.feature_encoder = BinFeatureEncoder(
|
|
275
|
+
bin_features=config.bin_features,
|
|
276
|
+
hidden_dim=config.hidden_dim,
|
|
277
|
+
rngs=rngs,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Combine contact and feature embeddings
|
|
281
|
+
self.combine = nnx.Linear(
|
|
282
|
+
in_features=config.hidden_dim * 2,
|
|
283
|
+
out_features=config.hidden_dim,
|
|
284
|
+
rngs=rngs,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Attention for context
|
|
288
|
+
self.attention = LocalAttention(
|
|
289
|
+
hidden_dim=config.hidden_dim,
|
|
290
|
+
num_heads=config.num_heads,
|
|
291
|
+
rngs=rngs,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Output heads
|
|
295
|
+
self.compartment_head = nnx.Linear(
|
|
296
|
+
in_features=config.hidden_dim,
|
|
297
|
+
out_features=1,
|
|
298
|
+
rngs=rngs,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
self.boundary_head = nnx.Linear(
|
|
302
|
+
in_features=config.hidden_dim,
|
|
303
|
+
out_features=1,
|
|
304
|
+
rngs=rngs,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def apply(
|
|
308
|
+
self,
|
|
309
|
+
data: PyTree,
|
|
310
|
+
state: PyTree,
|
|
311
|
+
metadata: dict[str, Any] | None,
|
|
312
|
+
random_params: Any = None,
|
|
313
|
+
stats: dict[str, Any] | None = None,
|
|
314
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
315
|
+
"""Apply Hi-C contact analysis.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
data: Dictionary containing:
|
|
319
|
+
- "contact_matrix": Hi-C contact matrix (n_bins, n_bins)
|
|
320
|
+
- "bin_features": Bin genomic features (n_bins, bin_features)
|
|
321
|
+
state: Element state (passed through unchanged)
|
|
322
|
+
metadata: Element metadata (passed through unchanged)
|
|
323
|
+
random_params: Not used
|
|
324
|
+
stats: Not used
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Tuple of (transformed_data, state, metadata):
|
|
328
|
+
- transformed_data contains:
|
|
329
|
+
|
|
330
|
+
- "contact_matrix": Original contacts
|
|
331
|
+
- "bin_features": Original features
|
|
332
|
+
- "bin_embeddings": Learned bin embeddings
|
|
333
|
+
- "compartment_scores": A/B compartment scores
|
|
334
|
+
- "tad_boundary_scores": TAD boundary probabilities
|
|
335
|
+
- "predicted_contacts": Reconstructed contacts
|
|
336
|
+
- state is passed through unchanged
|
|
337
|
+
- metadata is passed through unchanged
|
|
338
|
+
"""
|
|
339
|
+
contact_matrix = data["contact_matrix"]
|
|
340
|
+
bin_features = data["bin_features"]
|
|
341
|
+
|
|
342
|
+
# Encode contact patterns
|
|
343
|
+
contact_emb = self.contact_encoder(contact_matrix) # (n_bins, hidden_dim)
|
|
344
|
+
|
|
345
|
+
# Encode bin features
|
|
346
|
+
feature_emb = self.feature_encoder(bin_features) # (n_bins, hidden_dim)
|
|
347
|
+
|
|
348
|
+
# Combine embeddings
|
|
349
|
+
combined = jnp.concatenate([contact_emb, feature_emb], axis=-1)
|
|
350
|
+
combined = nnx.gelu(self.combine(combined)) # (n_bins, hidden_dim)
|
|
351
|
+
|
|
352
|
+
# Apply attention for context
|
|
353
|
+
attended = self.attention(combined) + combined # Residual
|
|
354
|
+
|
|
355
|
+
# Predict compartment scores (continuous A/B)
|
|
356
|
+
compartment_scores = self.compartment_head(attended).squeeze(-1) # (n_bins,)
|
|
357
|
+
|
|
358
|
+
# Predict TAD boundary scores
|
|
359
|
+
boundary_logits = self.boundary_head(attended).squeeze(-1) # (n_bins,)
|
|
360
|
+
tad_boundary_scores = jax.nn.sigmoid(boundary_logits)
|
|
361
|
+
|
|
362
|
+
# Reconstruct contacts from embeddings (dot product)
|
|
363
|
+
# (n_bins, hidden_dim) @ (hidden_dim, n_bins) -> (n_bins, n_bins)
|
|
364
|
+
predicted_contacts = jnp.einsum("ih,jh->ij", attended, attended)
|
|
365
|
+
predicted_contacts = jax.nn.softplus(predicted_contacts) # Non-negative
|
|
366
|
+
|
|
367
|
+
# Build output
|
|
368
|
+
transformed_data = {
|
|
369
|
+
"contact_matrix": contact_matrix,
|
|
370
|
+
"bin_features": bin_features,
|
|
371
|
+
"bin_embeddings": attended,
|
|
372
|
+
"compartment_scores": compartment_scores,
|
|
373
|
+
"tad_boundary_scores": tad_boundary_scores,
|
|
374
|
+
"predicted_contacts": predicted_contacts,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return transformed_data, state, metadata
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Multi-omics VAE with Product-of-Experts fusion.
|
|
2
|
+
|
|
3
|
+
This module implements a differentiable multi-omics variational autoencoder
|
|
4
|
+
that jointly integrates data from multiple modalities (e.g., RNA-seq, ATAC-seq,
|
|
5
|
+
protein). The model learns a shared latent space via Product-of-Experts (PoE)
|
|
6
|
+
fusion of per-modality posterior distributions, following the approach
|
|
7
|
+
described in MULTIVI (Ashuach et al., 2023).
|
|
8
|
+
|
|
9
|
+
Key algorithm:
|
|
10
|
+
1. Per-modality encoders map counts to (mu_m, logvar_m).
|
|
11
|
+
2. PoE fuses posteriors:
|
|
12
|
+
precision_joint = sum(1 / sigma_m^2)
|
|
13
|
+
mu_joint = (sum mu_m / sigma_m^2) / precision_joint
|
|
14
|
+
3. Reparameterised sample z ~ N(mu_joint, 1/precision_joint).
|
|
15
|
+
4. Per-modality decoders reconstruct counts from z.
|
|
16
|
+
5. ELBO = sum_m w_m * recon_loss_m + KL(q(z) || N(0,I)).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import jax
|
|
24
|
+
import jax.numpy as jnp
|
|
25
|
+
from artifex.generative_models.core.base import MLP
|
|
26
|
+
from artifex.generative_models.core.losses.base import reduce_loss
|
|
27
|
+
from artifex.generative_models.core.losses.divergence import gaussian_kl_divergence
|
|
28
|
+
from datarax.core.config import OperatorConfig
|
|
29
|
+
from flax import nnx
|
|
30
|
+
from jaxtyping import Array, Float, PyTree
|
|
31
|
+
|
|
32
|
+
from diffbio.constants import EPSILON
|
|
33
|
+
from diffbio.core.base_operators import EncoderDecoderOperator
|
|
34
|
+
from diffbio.operators._loss_balancing import LossBalancingMixin
|
|
35
|
+
from diffbio.utils.nn_utils import ensure_rngs
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
# Canonical key names for the two most common modalities.
|
|
40
|
+
_DEFAULT_MODALITY_KEYS = ("rna", "atac")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class MultiOmicsVAEConfig(OperatorConfig):
|
|
45
|
+
"""Configuration for DifferentiableMultiOmicsVAE.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
modality_dims: Feature dimension for each modality.
|
|
49
|
+
latent_dim: Shared latent space dimension.
|
|
50
|
+
hidden_dim: Hidden layer width for all encoders / decoders.
|
|
51
|
+
modality_weight_mode: How reconstruction losses are weighted.
|
|
52
|
+
'equal' gives uniform weight; 'learnable' uses softmax over
|
|
53
|
+
a learnable log-weight vector.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
modality_dims: list[int] = field(default_factory=lambda: [2000, 500])
|
|
57
|
+
latent_dim: int = 10
|
|
58
|
+
hidden_dim: int = 64
|
|
59
|
+
modality_weight_mode: str = "equal"
|
|
60
|
+
use_gradnorm: bool = False
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
"""Set stochastic defaults for VAE sampling and validate."""
|
|
64
|
+
object.__setattr__(self, "stochastic", True)
|
|
65
|
+
if self.stream_name is None:
|
|
66
|
+
object.__setattr__(self, "stream_name", "sample")
|
|
67
|
+
super().__post_init__()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DifferentiableMultiOmicsVAE(LossBalancingMixin, EncoderDecoderOperator):
|
|
71
|
+
"""Multi-omics VAE with Product-of-Experts latent fusion.
|
|
72
|
+
|
|
73
|
+
For each modality a dedicated encoder produces (mu_m, logvar_m).
|
|
74
|
+
These are combined via PoE into a joint posterior from which z is
|
|
75
|
+
sampled. Per-modality decoders then reconstruct counts from z.
|
|
76
|
+
|
|
77
|
+
The ELBO objective uses MSE reconstruction loss per modality,
|
|
78
|
+
optionally weighted by learnable per-modality weights, plus
|
|
79
|
+
a KL divergence term against a standard-normal prior.
|
|
80
|
+
|
|
81
|
+
Data keys follow the convention ``<name>_counts`` for input and
|
|
82
|
+
``<name>_reconstructed`` for output. When exactly two modalities
|
|
83
|
+
are used the canonical names ``rna`` and ``atac`` are applied;
|
|
84
|
+
otherwise ``modality_<i>`` is used.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
encoders: Per-modality encoder modules.
|
|
88
|
+
decoders: Per-modality decoder modules.
|
|
89
|
+
mu_heads: Per-modality linear projection for latent mean.
|
|
90
|
+
logvar_heads: Per-modality linear projection for latent logvar.
|
|
91
|
+
log_modality_weights: Learnable log-weights (only in 'learnable' mode).
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
config: MultiOmicsVAEConfig,
|
|
97
|
+
*,
|
|
98
|
+
rngs: nnx.Rngs | None = None,
|
|
99
|
+
name: str | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Initialise the multi-omics VAE.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
config: Operator configuration.
|
|
105
|
+
rngs: Flax NNX random number generators.
|
|
106
|
+
name: Optional operator name.
|
|
107
|
+
"""
|
|
108
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
109
|
+
|
|
110
|
+
rngs = ensure_rngs(rngs)
|
|
111
|
+
n_modalities = len(config.modality_dims)
|
|
112
|
+
|
|
113
|
+
# Per-modality encoders ------------------------------------------
|
|
114
|
+
encoders: list[MLP] = []
|
|
115
|
+
mu_heads: list[nnx.Linear] = []
|
|
116
|
+
logvar_heads: list[nnx.Linear] = []
|
|
117
|
+
|
|
118
|
+
for dim in config.modality_dims:
|
|
119
|
+
encoders.append(
|
|
120
|
+
MLP(
|
|
121
|
+
hidden_dims=[config.hidden_dim, config.hidden_dim],
|
|
122
|
+
in_features=dim,
|
|
123
|
+
activation="relu",
|
|
124
|
+
output_activation="relu",
|
|
125
|
+
use_batch_norm=False,
|
|
126
|
+
rngs=rngs,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
mu_heads.append(nnx.Linear(config.hidden_dim, config.latent_dim, rngs=rngs))
|
|
130
|
+
logvar_heads.append(nnx.Linear(config.hidden_dim, config.latent_dim, rngs=rngs))
|
|
131
|
+
|
|
132
|
+
self.encoders = nnx.List(encoders)
|
|
133
|
+
self.mu_heads = nnx.List(mu_heads)
|
|
134
|
+
self.logvar_heads = nnx.List(logvar_heads)
|
|
135
|
+
|
|
136
|
+
# Per-modality decoders ------------------------------------------
|
|
137
|
+
decoders: list[MLP] = []
|
|
138
|
+
for dim in config.modality_dims:
|
|
139
|
+
decoders.append(
|
|
140
|
+
MLP(
|
|
141
|
+
hidden_dims=[config.hidden_dim, config.hidden_dim, dim],
|
|
142
|
+
in_features=config.latent_dim,
|
|
143
|
+
activation="relu",
|
|
144
|
+
use_batch_norm=False,
|
|
145
|
+
rngs=rngs,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
self.decoders = nnx.List(decoders)
|
|
149
|
+
|
|
150
|
+
# Modality weight mode -------------------------------------------
|
|
151
|
+
self._weight_mode = nnx.static(config.modality_weight_mode)
|
|
152
|
+
if config.modality_weight_mode == "learnable":
|
|
153
|
+
self.log_modality_weights = nnx.Param(jnp.zeros(n_modalities))
|
|
154
|
+
|
|
155
|
+
# Assign canonical key names -------------------------------------
|
|
156
|
+
if n_modalities == 2:
|
|
157
|
+
self._modality_keys: tuple[str, ...] = _DEFAULT_MODALITY_KEYS
|
|
158
|
+
else:
|
|
159
|
+
self._modality_keys = tuple(f"modality_{i}" for i in range(n_modalities))
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
# PoE fusion
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def product_of_experts(
|
|
166
|
+
self,
|
|
167
|
+
mu_list: list[Float[Array, "batch latent"]],
|
|
168
|
+
logvar_list: list[Float[Array, "batch latent"]],
|
|
169
|
+
) -> tuple[Float[Array, "batch latent"], Float[Array, "batch latent"]]:
|
|
170
|
+
"""Fuse per-modality posteriors via Product-of-Experts.
|
|
171
|
+
|
|
172
|
+
For M modalities the PoE joint posterior is Gaussian with:
|
|
173
|
+
precision_joint = sum_m precision_m
|
|
174
|
+
mu_joint = (sum_m mu_m * precision_m) / precision_joint
|
|
175
|
+
where precision_m = 1 / sigma_m^2 = exp(-logvar_m).
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
mu_list: Per-modality means.
|
|
179
|
+
logvar_list: Per-modality log-variances.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
(mu_joint, logvar_joint) tuple for the fused posterior.
|
|
183
|
+
"""
|
|
184
|
+
# Stack precisions and weighted means across modalities
|
|
185
|
+
precision_sum = jnp.zeros_like(mu_list[0])
|
|
186
|
+
weighted_mu_sum = jnp.zeros_like(mu_list[0])
|
|
187
|
+
|
|
188
|
+
for mu_m, logvar_m in zip(mu_list, logvar_list):
|
|
189
|
+
precision_m = jnp.exp(-logvar_m)
|
|
190
|
+
precision_sum = precision_sum + precision_m
|
|
191
|
+
weighted_mu_sum = weighted_mu_sum + mu_m * precision_m
|
|
192
|
+
|
|
193
|
+
mu_joint = weighted_mu_sum / (precision_sum + EPSILON)
|
|
194
|
+
logvar_joint = -jnp.log(precision_sum + EPSILON)
|
|
195
|
+
|
|
196
|
+
return mu_joint, logvar_joint
|
|
197
|
+
|
|
198
|
+
# ------------------------------------------------------------------
|
|
199
|
+
# Modality weights
|
|
200
|
+
# ------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def _get_modality_weights(self) -> Float[Array, "n_modalities"]:
|
|
203
|
+
"""Return normalised modality weights.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Weight vector summing to 1 of length n_modalities.
|
|
207
|
+
"""
|
|
208
|
+
n_modalities = len(self.config.modality_dims)
|
|
209
|
+
if self._weight_mode == "learnable":
|
|
210
|
+
return jax.nn.softmax(self.log_modality_weights[...])
|
|
211
|
+
return jnp.ones(n_modalities) / n_modalities
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# Data key helpers
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def _input_key(self, idx: int) -> str:
|
|
218
|
+
"""Return the data-dict key for modality *idx* input counts.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
idx: Modality index.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
String key such as ``rna_counts`` or ``modality_0_counts``.
|
|
225
|
+
"""
|
|
226
|
+
return f"{self._modality_keys[idx]}_counts"
|
|
227
|
+
|
|
228
|
+
def _output_key(self, idx: int) -> str:
|
|
229
|
+
"""Return the data-dict key for modality *idx* reconstruction.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
idx: Modality index.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
String key such as ``rna_reconstructed``.
|
|
236
|
+
"""
|
|
237
|
+
return f"{self._modality_keys[idx]}_reconstructed"
|
|
238
|
+
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
# apply
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def apply(
|
|
244
|
+
self,
|
|
245
|
+
data: PyTree,
|
|
246
|
+
state: PyTree,
|
|
247
|
+
metadata: dict[str, Any] | None,
|
|
248
|
+
random_params: Any = None,
|
|
249
|
+
stats: dict[str, Any] | None = None,
|
|
250
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
251
|
+
"""Run the multi-omics VAE forward pass.
|
|
252
|
+
|
|
253
|
+
Steps:
|
|
254
|
+
1. Encode each modality to (mu_m, logvar_m).
|
|
255
|
+
2. PoE fusion -> (mu_joint, logvar_joint).
|
|
256
|
+
3. Reparameterise -> z.
|
|
257
|
+
4. Decode each modality from z.
|
|
258
|
+
5. Compute ELBO = weighted recon + KL.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
data: Dictionary with ``<modality>_counts`` keys, each of shape
|
|
262
|
+
(n_cells, modality_dim).
|
|
263
|
+
state: Operator state (passed through unchanged).
|
|
264
|
+
metadata: Operator metadata (passed through unchanged).
|
|
265
|
+
random_params: Not used.
|
|
266
|
+
stats: Not used.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Tuple of (result_data, state, metadata) where result_data
|
|
270
|
+
contains the original inputs plus ``joint_latent``,
|
|
271
|
+
``<modality>_reconstructed``, and ``elbo_loss``.
|
|
272
|
+
"""
|
|
273
|
+
n_modalities = len(self.config.modality_dims)
|
|
274
|
+
|
|
275
|
+
# 1. Encode each modality ----------------------------------------
|
|
276
|
+
mu_list: list[jax.Array] = []
|
|
277
|
+
logvar_list: list[jax.Array] = []
|
|
278
|
+
|
|
279
|
+
for i in range(n_modalities):
|
|
280
|
+
counts = data[self._input_key(i)]
|
|
281
|
+
h: jax.Array = self.encoders[i](jnp.log1p(counts))
|
|
282
|
+
mu = self.mu_heads[i](h)
|
|
283
|
+
logvar = jnp.clip(self.logvar_heads[i](h), -10.0, 10.0)
|
|
284
|
+
mu_list.append(mu)
|
|
285
|
+
logvar_list.append(logvar)
|
|
286
|
+
|
|
287
|
+
# 2. PoE fusion --------------------------------------------------
|
|
288
|
+
mu_joint, logvar_joint = self.product_of_experts(mu_list, logvar_list)
|
|
289
|
+
|
|
290
|
+
# 3. Sample z via reparameterisation (inherited) -----------------
|
|
291
|
+
z = self.reparameterize(mu_joint, logvar_joint)
|
|
292
|
+
|
|
293
|
+
# 4. Decode each modality ----------------------------------------
|
|
294
|
+
reconstructions: list[jax.Array] = []
|
|
295
|
+
for i in range(n_modalities):
|
|
296
|
+
reconstruction: jax.Array = self.decoders[i](z)
|
|
297
|
+
reconstructions.append(reconstruction)
|
|
298
|
+
|
|
299
|
+
# 5. Compute ELBO ------------------------------------------------
|
|
300
|
+
weights = self._get_modality_weights()
|
|
301
|
+
|
|
302
|
+
# Weighted reconstruction loss (MSE per modality, summed over features)
|
|
303
|
+
total_recon = jnp.array(0.0)
|
|
304
|
+
for i in range(n_modalities):
|
|
305
|
+
counts = data[self._input_key(i)]
|
|
306
|
+
per_sample = jnp.sum((counts - reconstructions[i]) ** 2, axis=-1)
|
|
307
|
+
mean_recon = reduce_loss(per_sample, reduction="mean")
|
|
308
|
+
total_recon = total_recon + weights[i] * mean_recon
|
|
309
|
+
|
|
310
|
+
# KL divergence (batch_sum: sum over latent, mean over batch)
|
|
311
|
+
kl = gaussian_kl_divergence(mu_joint, logvar_joint, reduction="batch_sum")
|
|
312
|
+
|
|
313
|
+
elbo_loss = total_recon + kl
|
|
314
|
+
|
|
315
|
+
# Build output dict -----------------------------------------------
|
|
316
|
+
result: dict[str, Any] = dict(data)
|
|
317
|
+
result["joint_latent"] = z
|
|
318
|
+
result["joint_mu"] = mu_joint
|
|
319
|
+
result["joint_logvar"] = logvar_joint
|
|
320
|
+
result["elbo_loss"] = elbo_loss
|
|
321
|
+
|
|
322
|
+
for i in range(n_modalities):
|
|
323
|
+
result[self._output_key(i)] = reconstructions[i]
|
|
324
|
+
|
|
325
|
+
return result, state, metadata
|