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.
Files changed (202) hide show
  1. diffbio/__init__.py +39 -0
  2. diffbio/configs.py +75 -0
  3. diffbio/constants.py +204 -0
  4. diffbio/core/__init__.py +127 -0
  5. diffbio/core/base_operators.py +612 -0
  6. diffbio/core/data_types.py +260 -0
  7. diffbio/core/gnn_components.py +629 -0
  8. diffbio/core/graph_utils.py +149 -0
  9. diffbio/core/neural_components.py +270 -0
  10. diffbio/core/optimal_transport.py +133 -0
  11. diffbio/core/soft_ops/__init__.py +216 -0
  12. diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
  13. diffbio/core/soft_ops/_projections_simplex.py +240 -0
  14. diffbio/core/soft_ops/_projections_transport.py +508 -0
  15. diffbio/core/soft_ops/_sorting_network.py +204 -0
  16. diffbio/core/soft_ops/_types.py +15 -0
  17. diffbio/core/soft_ops/_utils.py +342 -0
  18. diffbio/core/soft_ops/autograd_safe.py +120 -0
  19. diffbio/core/soft_ops/comparison.py +235 -0
  20. diffbio/core/soft_ops/elementwise.py +309 -0
  21. diffbio/core/soft_ops/logical.py +146 -0
  22. diffbio/core/soft_ops/quantile.py +376 -0
  23. diffbio/core/soft_ops/selection.py +236 -0
  24. diffbio/core/soft_ops/sorting.py +926 -0
  25. diffbio/core/soft_ops/straight_through.py +261 -0
  26. diffbio/core/uncertainty.py +279 -0
  27. diffbio/evaluation/__init__.py +42 -0
  28. diffbio/evaluation/adapters.py +409 -0
  29. diffbio/evaluation/graders.py +223 -0
  30. diffbio/evaluation/problem.py +157 -0
  31. diffbio/evaluation/runner.py +277 -0
  32. diffbio/losses/__init__.py +59 -0
  33. diffbio/losses/alignment_losses.py +222 -0
  34. diffbio/losses/biological_regularization.py +288 -0
  35. diffbio/losses/metric_losses.py +139 -0
  36. diffbio/losses/singlecell_losses.py +387 -0
  37. diffbio/losses/statistical_losses.py +345 -0
  38. diffbio/operators/__init__.py +60 -0
  39. diffbio/operators/_count_vae.py +197 -0
  40. diffbio/operators/_loss_balancing.py +65 -0
  41. diffbio/operators/_masked_gene_transformer.py +118 -0
  42. diffbio/operators/_transformer_validation.py +50 -0
  43. diffbio/operators/alignment/__init__.py +51 -0
  44. diffbio/operators/alignment/profile_hmm.py +350 -0
  45. diffbio/operators/alignment/scoring.py +127 -0
  46. diffbio/operators/alignment/smith_waterman.py +261 -0
  47. diffbio/operators/alignment/soft_msa.py +419 -0
  48. diffbio/operators/assembly/__init__.py +27 -0
  49. diffbio/operators/assembly/gnn_assembly.py +252 -0
  50. diffbio/operators/assembly/metagenomic_binning.py +296 -0
  51. diffbio/operators/crispr/__init__.py +17 -0
  52. diffbio/operators/crispr/guide_scoring.py +269 -0
  53. diffbio/operators/drug_discovery/__init__.py +133 -0
  54. diffbio/operators/drug_discovery/_graph_utils.py +142 -0
  55. diffbio/operators/drug_discovery/admet_predictor.py +285 -0
  56. diffbio/operators/drug_discovery/attentive_fp.py +411 -0
  57. diffbio/operators/drug_discovery/dti.py +261 -0
  58. diffbio/operators/drug_discovery/fingerprint.py +490 -0
  59. diffbio/operators/drug_discovery/maccs_keys.py +267 -0
  60. diffbio/operators/drug_discovery/message_passing.py +200 -0
  61. diffbio/operators/drug_discovery/primitives.py +242 -0
  62. diffbio/operators/drug_discovery/property_predictor.py +163 -0
  63. diffbio/operators/drug_discovery/similarity.py +193 -0
  64. diffbio/operators/epigenomics/__init__.py +35 -0
  65. diffbio/operators/epigenomics/chromatin_state.py +491 -0
  66. diffbio/operators/epigenomics/contextual.py +288 -0
  67. diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
  68. diffbio/operators/epigenomics/peak_calling.py +555 -0
  69. diffbio/operators/foundation_models/__init__.py +119 -0
  70. diffbio/operators/foundation_models/adapters.py +114 -0
  71. diffbio/operators/foundation_models/contracts.py +245 -0
  72. diffbio/operators/foundation_models/embedding_probe.py +83 -0
  73. diffbio/operators/foundation_models/experimental.py +128 -0
  74. diffbio/operators/foundation_models/foundation_model.py +332 -0
  75. diffbio/operators/foundation_models/frozen.py +59 -0
  76. diffbio/operators/foundation_models/precomputed.py +270 -0
  77. diffbio/operators/foundation_models/transformer_encoder.py +564 -0
  78. diffbio/operators/mapping/__init__.py +17 -0
  79. diffbio/operators/mapping/neural_mapper.py +493 -0
  80. diffbio/operators/metabolomics/__init__.py +39 -0
  81. diffbio/operators/metabolomics/spectral_similarity.py +315 -0
  82. diffbio/operators/molecular_dynamics/__init__.py +51 -0
  83. diffbio/operators/molecular_dynamics/force_field.py +265 -0
  84. diffbio/operators/molecular_dynamics/integrator.py +304 -0
  85. diffbio/operators/molecular_dynamics/primitives.py +115 -0
  86. diffbio/operators/multiomics/__init__.py +38 -0
  87. diffbio/operators/multiomics/hic_contact.py +377 -0
  88. diffbio/operators/multiomics/multiomics_vae.py +325 -0
  89. diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
  90. diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
  91. diffbio/operators/normalization/__init__.py +42 -0
  92. diffbio/operators/normalization/embedding.py +222 -0
  93. diffbio/operators/normalization/phate.py +400 -0
  94. diffbio/operators/normalization/umap.py +261 -0
  95. diffbio/operators/normalization/vae_normalizer.py +258 -0
  96. diffbio/operators/population/__init__.py +17 -0
  97. diffbio/operators/population/ancestry_estimation.py +274 -0
  98. diffbio/operators/preprocessing/__init__.py +76 -0
  99. diffbio/operators/preprocessing/adapter_removal.py +311 -0
  100. diffbio/operators/preprocessing/duplicate_filter.py +317 -0
  101. diffbio/operators/preprocessing/error_correction.py +287 -0
  102. diffbio/operators/protein/__init__.py +31 -0
  103. diffbio/operators/protein/secondary_structure.py +509 -0
  104. diffbio/operators/quality_filter.py +128 -0
  105. diffbio/operators/rna_structure/__init__.py +35 -0
  106. diffbio/operators/rna_structure/rna_folding.py +509 -0
  107. diffbio/operators/rnaseq/__init__.py +23 -0
  108. diffbio/operators/rnaseq/motif_discovery.py +251 -0
  109. diffbio/operators/rnaseq/splicing_psi.py +216 -0
  110. diffbio/operators/singlecell/__init__.py +193 -0
  111. diffbio/operators/singlecell/ambient_removal.py +333 -0
  112. diffbio/operators/singlecell/archetypes.py +191 -0
  113. diffbio/operators/singlecell/batch_correction.py +288 -0
  114. diffbio/operators/singlecell/cell_annotation.py +519 -0
  115. diffbio/operators/singlecell/communication.py +704 -0
  116. diffbio/operators/singlecell/differential_distribution.py +243 -0
  117. diffbio/operators/singlecell/doublet_detection.py +657 -0
  118. diffbio/operators/singlecell/downsampling.py +166 -0
  119. diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
  120. diffbio/operators/singlecell/grn_inference.py +336 -0
  121. diffbio/operators/singlecell/imputation.py +429 -0
  122. diffbio/operators/singlecell/knockdown_filter.py +176 -0
  123. diffbio/operators/singlecell/ot_trajectory.py +277 -0
  124. diffbio/operators/singlecell/simulation.py +444 -0
  125. diffbio/operators/singlecell/sindy_grn.py +247 -0
  126. diffbio/operators/singlecell/soft_clustering.py +211 -0
  127. diffbio/operators/singlecell/spatial_domains.py +677 -0
  128. diffbio/operators/singlecell/switch_de.py +184 -0
  129. diffbio/operators/singlecell/trajectory.py +447 -0
  130. diffbio/operators/singlecell/velocity.py +361 -0
  131. diffbio/operators/statistical/__init__.py +35 -0
  132. diffbio/operators/statistical/em_quantification.py +260 -0
  133. diffbio/operators/statistical/hmm.py +234 -0
  134. diffbio/operators/statistical/nb_glm.py +272 -0
  135. diffbio/operators/variant/__init__.py +64 -0
  136. diffbio/operators/variant/classifier.py +333 -0
  137. diffbio/operators/variant/cnn_classifier.py +255 -0
  138. diffbio/operators/variant/cnv_segmentation.py +678 -0
  139. diffbio/operators/variant/deepvariant_pileup.py +426 -0
  140. diffbio/operators/variant/pileup.py +240 -0
  141. diffbio/operators/variant/quality_recalibration.py +274 -0
  142. diffbio/pipelines/__init__.py +65 -0
  143. diffbio/pipelines/differential_expression.py +279 -0
  144. diffbio/pipelines/enhanced_variant_calling.py +326 -0
  145. diffbio/pipelines/perturbation.py +407 -0
  146. diffbio/pipelines/preprocessing.py +267 -0
  147. diffbio/pipelines/single_cell.py +366 -0
  148. diffbio/pipelines/variant_calling.py +490 -0
  149. diffbio/samplers/__init__.py +9 -0
  150. diffbio/samplers/perturbation_sampler.py +142 -0
  151. diffbio/sequences/__init__.py +34 -0
  152. diffbio/sequences/dna.py +239 -0
  153. diffbio/sources/__init__.py +149 -0
  154. diffbio/sources/_anndata_shared.py +89 -0
  155. diffbio/sources/_batch_iteration.py +37 -0
  156. diffbio/sources/_benchmark_source.py +152 -0
  157. diffbio/sources/_indexed_batch_source.py +38 -0
  158. diffbio/sources/_utils.py +45 -0
  159. diffbio/sources/anndata_interop.py +387 -0
  160. diffbio/sources/anndata_source.py +361 -0
  161. diffbio/sources/archive_ii.py +174 -0
  162. diffbio/sources/balifam.py +207 -0
  163. diffbio/sources/bam.py +265 -0
  164. diffbio/sources/bengrn_ground_truth.py +306 -0
  165. diffbio/sources/contextual_epigenomics.py +242 -0
  166. diffbio/sources/dti.py +359 -0
  167. diffbio/sources/embeddings.py +203 -0
  168. diffbio/sources/encode_peaks.py +223 -0
  169. diffbio/sources/fasta.py +226 -0
  170. diffbio/sources/immune_human.py +172 -0
  171. diffbio/sources/indexed_embeddings.py +128 -0
  172. diffbio/sources/indexed_view.py +191 -0
  173. diffbio/sources/molnet.py +493 -0
  174. diffbio/sources/multiomics.py +279 -0
  175. diffbio/sources/pancreas.py +108 -0
  176. diffbio/sources/perturbation/__init__.py +69 -0
  177. diffbio/sources/perturbation/_types.py +51 -0
  178. diffbio/sources/perturbation/_utils.py +125 -0
  179. diffbio/sources/perturbation/concat_source.py +115 -0
  180. diffbio/sources/perturbation/control_mapping.py +215 -0
  181. diffbio/sources/perturbation/experiment_config.py +261 -0
  182. diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
  183. diffbio/sources/perturbation/output_space.py +52 -0
  184. diffbio/sources/perturbation/perturbation_source.py +513 -0
  185. diffbio/sources/seqfish.py +145 -0
  186. diffbio/sources/sequence_foundation.py +68 -0
  187. diffbio/sources/singlecell_foundation.py +68 -0
  188. diffbio/splitters/__init__.py +63 -0
  189. diffbio/splitters/base.py +251 -0
  190. diffbio/splitters/molecular.py +330 -0
  191. diffbio/splitters/perturbation.py +199 -0
  192. diffbio/splitters/random.py +217 -0
  193. diffbio/splitters/sequence.py +201 -0
  194. diffbio/utils/__init__.py +55 -0
  195. diffbio/utils/dependency_runtime.py +115 -0
  196. diffbio/utils/nn_utils.py +157 -0
  197. diffbio/utils/quality.py +45 -0
  198. diffbio/utils/training.py +585 -0
  199. diffbio-0.1.0.dist-info/METADATA +480 -0
  200. diffbio-0.1.0.dist-info/RECORD +202 -0
  201. diffbio-0.1.0.dist-info/WHEEL +4 -0
  202. 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