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,612 @@
1
+ """Base operator classes for DiffBio.
2
+
3
+ This module provides domain-specific base classes that operators can inherit
4
+ to get shared functionality. Following DRY principle, common patterns like
5
+ temperature-controlled smoothing, sequence validation, and VAE reparameterization
6
+ are centralized here.
7
+
8
+ Inheritance patterns:
9
+ - TemperatureOperator: For operators using logsumexp smoothing
10
+ - SequenceOperator: For operators processing one-hot encoded sequences
11
+ - EncoderDecoderOperator: For VAE-style operators
12
+ - GraphOperator: For GNN-based operators
13
+ - HMMOperator: For HMM-based operators with forward-backward
14
+
15
+ Multiple inheritance is supported:
16
+ class MyAligner(TemperatureOperator, SequenceOperator):
17
+ ...
18
+ """
19
+
20
+ from typing import Any, Literal
21
+
22
+ import jax
23
+ import jax.numpy as jnp
24
+ from datarax.core.config import OperatorConfig
25
+ from datarax.core.operator import OperatorModule
26
+ from flax import nnx
27
+ from jaxtyping import Array, Float, Int, PyTree
28
+
29
+ from diffbio.constants import DEFAULT_TEMPERATURE, EPSILON
30
+ from diffbio.core.soft_ops import sorting as soft_sorting
31
+ from diffbio.utils.nn_utils import ensure_rngs, get_rng_key, init_learnable_param
32
+
33
+ __all__ = [
34
+ "TemperatureOperator",
35
+ "SequenceOperator",
36
+ "EncoderDecoderOperator",
37
+ "GraphOperator",
38
+ "HMMOperator",
39
+ ]
40
+
41
+
42
+ class TemperatureOperator(OperatorModule):
43
+ """Base class for operators using temperature-controlled smoothing.
44
+
45
+ Provides the soft_max method using logsumexp relaxation, which is used
46
+ by many differentiable bioinformatics algorithms including:
47
+ - Smith-Waterman alignment
48
+ - Nussinov RNA folding
49
+ - Viterbi decoding
50
+
51
+ The temperature parameter controls the trade-off between accuracy and
52
+ differentiability:
53
+ - temperature -> 0: Approaches hard max (accurate but less differentiable)
54
+ - temperature -> inf: Uniform averaging (smooth but uninformative)
55
+
56
+ Subclasses should define their config with a 'temperature' field.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ config: OperatorConfig,
62
+ *,
63
+ rngs: nnx.Rngs | None = None,
64
+ name: str | None = None,
65
+ ):
66
+ """Initialize TemperatureOperator.
67
+
68
+ Args:
69
+ config: Configuration with 'temperature' and optionally
70
+ 'learnable_temperature' fields.
71
+ rngs: Flax NNX random number generators.
72
+ name: Optional operator name.
73
+ """
74
+ super().__init__(config, rngs=rngs, name=name)
75
+
76
+ temperature = getattr(config, "temperature", DEFAULT_TEMPERATURE)
77
+ is_learnable = getattr(config, "learnable_temperature", False)
78
+
79
+ if is_learnable:
80
+ self.temperature = init_learnable_param(temperature)
81
+ else:
82
+ self._temperature_value = temperature
83
+
84
+ @property
85
+ def _temperature(self) -> Float[Array, ""] | float:
86
+ """Get current temperature value."""
87
+ if hasattr(self, "temperature"):
88
+ return self.temperature[...]
89
+ return self._temperature_value
90
+
91
+ def soft_max(
92
+ self,
93
+ values: Float[Array, "..."],
94
+ axis: int | None = None,
95
+ ) -> Float[Array, "..."]:
96
+ """Compute smooth maximum using logsumexp.
97
+
98
+ Uses ``temperature * logsumexp(values / temperature)`` which
99
+ is an upper bound on the true max. This property is essential
100
+ for dynamic programming algorithms (Smith-Waterman, Viterbi).
101
+
102
+ Args:
103
+ values: Input array.
104
+ axis: Axis along which to compute max.
105
+
106
+ Returns:
107
+ Smooth maximum value(s), always >= hard max.
108
+ """
109
+ temp = self._temperature
110
+ return temp * jax.scipy.special.logsumexp(values / temp, axis=axis)
111
+
112
+ def soft_argmax(
113
+ self,
114
+ logits: Float[Array, "..."],
115
+ axis: int = -1,
116
+ ) -> Float[Array, "..."]:
117
+ """Compute soft argmax returning SoftIndex.
118
+
119
+ Delegates to :func:`diffbio.core.soft_ops.sorting.argmax`.
120
+
121
+ Args:
122
+ logits: Input logits.
123
+ axis: Axis along which to compute argmax.
124
+
125
+ Returns:
126
+ SoftIndex probability distribution.
127
+ """
128
+ return soft_sorting.argmax(
129
+ logits,
130
+ axis=axis,
131
+ softness=self._temperature,
132
+ mode="smooth",
133
+ standardize=False,
134
+ )
135
+
136
+ def apply(
137
+ self,
138
+ data: PyTree,
139
+ state: PyTree,
140
+ metadata: dict[str, Any] | None,
141
+ random_params: Any = None,
142
+ stats: dict[str, Any] | None = None,
143
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
144
+ """Base apply method - should be overridden by subclasses."""
145
+ raise NotImplementedError("Subclasses must implement apply()")
146
+
147
+
148
+ class SequenceOperator(OperatorModule):
149
+ """Base class for operators processing biological sequences.
150
+
151
+ Provides utilities for sequence validation and manipulation including:
152
+ - Validation of one-hot encoded sequences
153
+ - Sequence normalization to valid probability distributions
154
+ - Alphabet handling
155
+
156
+ Subclasses should define their config with 'alphabet_size' and
157
+ optionally 'max_length' fields.
158
+ """
159
+
160
+ def __init__(
161
+ self,
162
+ config: OperatorConfig,
163
+ *,
164
+ rngs: nnx.Rngs | None = None,
165
+ name: str | None = None,
166
+ ):
167
+ """Initialize SequenceOperator.
168
+
169
+ Args:
170
+ config: Configuration with 'alphabet_size' field.
171
+ rngs: Flax NNX random number generators.
172
+ name: Optional operator name.
173
+ """
174
+ super().__init__(config, rngs=rngs, name=name)
175
+
176
+ self.alphabet_size = getattr(config, "alphabet_size", 4)
177
+ self.max_length = getattr(config, "max_length", None)
178
+
179
+ def validate_sequence(
180
+ self,
181
+ sequence: Float[Array, "length alphabet"],
182
+ ) -> bool:
183
+ """Check if sequence has valid shape for this operator.
184
+
185
+ Args:
186
+ sequence: Sequence to validate.
187
+
188
+ Returns:
189
+ True if sequence is valid.
190
+ """
191
+ if sequence.ndim != 2:
192
+ return False
193
+ if sequence.shape[1] != self.alphabet_size:
194
+ return False
195
+ if self.max_length is not None and sequence.shape[0] > self.max_length:
196
+ return False
197
+ return True
198
+
199
+ def normalize_sequence(
200
+ self,
201
+ sequence: Float[Array, "length alphabet"],
202
+ ) -> Float[Array, "length alphabet"]:
203
+ """Normalize sequence so each position sums to 1.
204
+
205
+ Args:
206
+ sequence: Possibly unnormalized sequence.
207
+
208
+ Returns:
209
+ Normalized sequence (valid probability distribution at each position).
210
+ """
211
+ return jax.nn.softmax(sequence, axis=-1)
212
+
213
+ def mask_sequence(
214
+ self,
215
+ sequence: Float[Array, "length alphabet"],
216
+ mask: Float[Array, "length"],
217
+ ) -> Float[Array, "length alphabet"]:
218
+ """Apply mask to sequence.
219
+
220
+ Args:
221
+ sequence: Input sequence.
222
+ mask: Boolean or soft mask.
223
+
224
+ Returns:
225
+ Masked sequence (masked positions set to uniform).
226
+ """
227
+ uniform = jnp.ones(self.alphabet_size) / self.alphabet_size
228
+ return jnp.where(mask[:, None], sequence, uniform)
229
+
230
+ def apply(
231
+ self,
232
+ data: PyTree,
233
+ state: PyTree,
234
+ metadata: dict[str, Any] | None,
235
+ random_params: Any = None,
236
+ stats: dict[str, Any] | None = None,
237
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
238
+ """Base apply method - should be overridden by subclasses."""
239
+ raise NotImplementedError("Subclasses must implement apply()")
240
+
241
+
242
+ class EncoderDecoderOperator(OperatorModule):
243
+ """Base class for VAE-style encoder-decoder operators.
244
+
245
+ Provides utilities for variational autoencoders:
246
+ - Reparameterization trick for sampling
247
+ - KL divergence computation
248
+ - ELBO loss components
249
+
250
+ Used by single-cell analysis, normalization, and generative models.
251
+
252
+ Subclasses should define their config with 'latent_dim' and
253
+ 'hidden_dim' fields.
254
+ """
255
+
256
+ def __init__(
257
+ self,
258
+ config: OperatorConfig,
259
+ *,
260
+ rngs: nnx.Rngs | None = None,
261
+ name: str | None = None,
262
+ ):
263
+ """Initialize EncoderDecoderOperator.
264
+
265
+ Args:
266
+ config: Configuration with 'latent_dim' field.
267
+ rngs: Flax NNX random number generators.
268
+ name: Optional operator name.
269
+ """
270
+ super().__init__(config, rngs=rngs, name=name)
271
+
272
+ self.latent_dim = getattr(config, "latent_dim", 10)
273
+ self.hidden_dim = getattr(config, "hidden_dim", 64)
274
+ self.rngs = ensure_rngs(rngs)
275
+
276
+ def reparameterize(
277
+ self,
278
+ mean: Float[Array, "... latent_dim"],
279
+ log_var: Float[Array, "... latent_dim"],
280
+ ) -> Float[Array, "... latent_dim"]:
281
+ """Sample from latent distribution using reparameterization trick.
282
+
283
+ z = mean + std * epsilon, where epsilon ~ N(0, 1)
284
+
285
+ This allows gradients to flow through the sampling operation.
286
+
287
+ Args:
288
+ mean: Mean of the latent distribution.
289
+ log_var: Log variance of the latent distribution.
290
+
291
+ Returns:
292
+ Sampled latent representation.
293
+ """
294
+ key = get_rng_key(self.rngs, "sample", fallback_seed=0)
295
+ std = jnp.exp(0.5 * log_var)
296
+ epsilon = jax.random.normal(key, mean.shape)
297
+ return mean + std * epsilon
298
+
299
+ def kl_divergence(
300
+ self,
301
+ mean: Float[Array, "... latent_dim"],
302
+ log_var: Float[Array, "... latent_dim"],
303
+ ) -> Float[Array, ""]:
304
+ """Compute KL divergence from standard normal.
305
+
306
+ KL(q(z|x) || p(z)) where p(z) = N(0, I)
307
+
308
+ Args:
309
+ mean: Mean of approximate posterior.
310
+ log_var: Log variance of approximate posterior.
311
+
312
+ Returns:
313
+ Scalar KL divergence.
314
+ """
315
+ # KL = -0.5 * sum(1 + log_var - mean^2 - exp(log_var))
316
+ kl = -0.5 * jnp.sum(1 + log_var - mean**2 - jnp.exp(log_var))
317
+ return kl
318
+
319
+ def elbo_loss(
320
+ self,
321
+ recon_loss: Float[Array, ""],
322
+ mean: Float[Array, "... latent_dim"],
323
+ log_var: Float[Array, "... latent_dim"],
324
+ beta: float = 1.0,
325
+ ) -> Float[Array, ""]:
326
+ """Compute negative ELBO loss.
327
+
328
+ loss = recon_loss + beta * KL_divergence
329
+
330
+ Args:
331
+ recon_loss: Reconstruction loss (e.g., MSE or BCE).
332
+ mean: Mean of latent distribution.
333
+ log_var: Log variance of latent distribution.
334
+ beta: Weight for KL term (default 1.0, >1 for beta-VAE).
335
+
336
+ Returns:
337
+ Negative ELBO loss.
338
+ """
339
+ kl = self.kl_divergence(mean, log_var)
340
+ return recon_loss + beta * kl
341
+
342
+ def apply(
343
+ self,
344
+ data: PyTree,
345
+ state: PyTree,
346
+ metadata: dict[str, Any] | None,
347
+ random_params: Any = None,
348
+ stats: dict[str, Any] | None = None,
349
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
350
+ """Base apply method - should be overridden by subclasses."""
351
+ raise NotImplementedError("Subclasses must implement apply()")
352
+
353
+
354
+ class GraphOperator(OperatorModule):
355
+ """Base class for graph neural network operators.
356
+
357
+ Provides utilities for graph-structured data processing:
358
+ - Scatter-based aggregation (sum, mean, max)
359
+ - Edge handling utilities
360
+ - Graph pooling operations
361
+
362
+ Used by assembly graphs, molecular graphs, and phylogenetic trees.
363
+
364
+ Subclasses should define their config with 'node_features',
365
+ 'edge_features', and 'num_heads' fields.
366
+ """
367
+
368
+ def __init__(
369
+ self,
370
+ config: OperatorConfig,
371
+ *,
372
+ rngs: nnx.Rngs | None = None,
373
+ name: str | None = None,
374
+ ):
375
+ """Initialize GraphOperator.
376
+
377
+ Args:
378
+ config: Configuration with graph-related fields.
379
+ rngs: Flax NNX random number generators.
380
+ name: Optional operator name.
381
+ """
382
+ super().__init__(config, rngs=rngs, name=name)
383
+
384
+ self.node_features = getattr(config, "node_features", 32)
385
+ self.edge_features = getattr(config, "edge_features", 8)
386
+ self.num_heads = getattr(config, "num_heads", 1)
387
+
388
+ def scatter_aggregate(
389
+ self,
390
+ messages: Float[Array, "num_messages features"],
391
+ indices: Int[Array, "num_messages"],
392
+ num_nodes: int,
393
+ aggregation: Literal["sum", "mean", "max"] = "sum",
394
+ ) -> Float[Array, "num_nodes features"]:
395
+ """Aggregate messages at nodes using scatter operations.
396
+
397
+ Args:
398
+ messages: Message features to aggregate.
399
+ indices: Target node indices for each message.
400
+ num_nodes: Total number of nodes.
401
+ aggregation: Aggregation method.
402
+
403
+ Returns:
404
+ Aggregated features for each node.
405
+ """
406
+ if aggregation == "sum":
407
+ return jax.ops.segment_sum(messages, indices, num_segments=num_nodes)
408
+ elif aggregation == "mean":
409
+ sum_messages = jax.ops.segment_sum(messages, indices, num_segments=num_nodes)
410
+ counts = jax.ops.segment_sum(
411
+ jnp.ones(messages.shape[0]), indices, num_segments=num_nodes
412
+ )
413
+ return sum_messages / (counts[:, None] + EPSILON)
414
+ elif aggregation == "max":
415
+ result = jax.ops.segment_max(messages, indices, num_segments=num_nodes)
416
+ # Replace -inf with 0
417
+ return jnp.where(jnp.isinf(result), jnp.zeros_like(result), result)
418
+ else:
419
+ raise ValueError(f"Unknown aggregation: {aggregation}")
420
+
421
+ def global_pool(
422
+ self,
423
+ node_features: Float[Array, "num_nodes features"],
424
+ batch: Int[Array, "num_nodes"] | None = None,
425
+ aggregation: Literal["sum", "mean", "max"] = "mean",
426
+ ) -> Float[Array, "batch_size features"] | Float[Array, "features"]:
427
+ """Pool node features to graph-level representation.
428
+
429
+ Args:
430
+ node_features: Node feature matrix.
431
+ batch: Batch assignment for each node (None for single graph).
432
+ aggregation: Pooling method.
433
+
434
+ Returns:
435
+ Graph-level features.
436
+ """
437
+ if batch is None:
438
+ # Single graph
439
+ if aggregation == "sum":
440
+ return jnp.sum(node_features, axis=0)
441
+ elif aggregation == "mean":
442
+ return jnp.mean(node_features, axis=0)
443
+ elif aggregation == "max":
444
+ return jnp.max(node_features, axis=0)
445
+ else:
446
+ raise ValueError(f"Unknown aggregation: {aggregation}")
447
+ else:
448
+ # Batched graphs
449
+ num_graphs = int(jnp.max(batch)) + 1
450
+ return self.scatter_aggregate(node_features, batch, num_graphs, aggregation)
451
+
452
+ def apply(
453
+ self,
454
+ data: PyTree,
455
+ state: PyTree,
456
+ metadata: dict[str, Any] | None,
457
+ random_params: Any = None,
458
+ stats: dict[str, Any] | None = None,
459
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
460
+ """Base apply method - should be overridden by subclasses."""
461
+ raise NotImplementedError("Subclasses must implement apply()")
462
+
463
+
464
+ class HMMOperator(OperatorModule):
465
+ """Base class for Hidden Markov Model operators.
466
+
467
+ Provides the core HMM algorithms:
468
+ - Forward algorithm for likelihood computation
469
+ - Forward-backward for posterior computation
470
+ - Viterbi for MAP decoding (soft version)
471
+
472
+ Used by variant calling, gene finding, chromatin state annotation.
473
+
474
+ Subclasses should define their config with 'num_states',
475
+ 'num_emissions', and 'temperature' fields.
476
+ """
477
+
478
+ def __init__(
479
+ self,
480
+ config: OperatorConfig,
481
+ *,
482
+ rngs: nnx.Rngs | None = None,
483
+ name: str | None = None,
484
+ ):
485
+ """Initialize HMMOperator.
486
+
487
+ Args:
488
+ config: Configuration with HMM-related fields.
489
+ rngs: Flax NNX random number generators.
490
+ name: Optional operator name.
491
+ """
492
+ super().__init__(config, rngs=rngs, name=name)
493
+
494
+ self.num_states = getattr(config, "num_states", 3)
495
+ self.num_emissions = getattr(config, "num_emissions", 4)
496
+ self.temperature = getattr(config, "temperature", DEFAULT_TEMPERATURE)
497
+
498
+ # Initialize HMM parameters
499
+ rngs = ensure_rngs(rngs)
500
+
501
+ # Transition logits (will be normalized via log_softmax)
502
+ key = get_rng_key(rngs, "params", fallback_seed=0)
503
+ init_trans = jax.random.normal(key, (self.num_states, self.num_states)) * 0.1
504
+ self.log_transition_params = nnx.Param(init_trans)
505
+
506
+ # Emission logits
507
+ key = get_rng_key(rngs, "params", fallback_seed=1)
508
+ init_emit = jax.random.normal(key, (self.num_states, self.num_emissions)) * 0.1
509
+ self.log_emission_params = nnx.Param(init_emit)
510
+
511
+ # Initial state logits
512
+ key = get_rng_key(rngs, "params", fallback_seed=2)
513
+ init_initial = jax.random.normal(key, (self.num_states,)) * 0.1
514
+ self.log_initial_params = nnx.Param(init_initial)
515
+
516
+ def get_log_transition_matrix(self) -> Float[Array, "num_states num_states"]:
517
+ """Get normalized log transition matrix."""
518
+ return jax.nn.log_softmax(self.log_transition_params[...] / self.temperature, axis=1)
519
+
520
+ def get_log_emission_matrix(self) -> Float[Array, "num_states num_emissions"]:
521
+ """Get normalized log emission matrix."""
522
+ return jax.nn.log_softmax(self.log_emission_params[...] / self.temperature, axis=1)
523
+
524
+ def get_log_initial_distribution(self) -> Float[Array, "num_states"]:
525
+ """Get normalized log initial state distribution."""
526
+ return jax.nn.log_softmax(self.log_initial_params[...] / self.temperature)
527
+
528
+ def forward_pass(
529
+ self,
530
+ observations: Int[Array, "seq_len"],
531
+ ) -> Float[Array, ""]:
532
+ """Compute log probability using forward algorithm.
533
+
534
+ Args:
535
+ observations: Integer-encoded observations.
536
+
537
+ Returns:
538
+ Log probability of the observation sequence.
539
+ """
540
+ log_trans = self.get_log_transition_matrix()
541
+ log_emit = self.get_log_emission_matrix()
542
+ log_init = self.get_log_initial_distribution()
543
+
544
+ # Initialize
545
+ log_alpha = log_init + log_emit[:, observations[0]]
546
+
547
+ # Forward pass
548
+ def forward_step(log_alpha, obs):
549
+ log_alpha_expanded = log_alpha[:, None]
550
+ log_alpha_new = jax.scipy.special.logsumexp(log_alpha_expanded + log_trans, axis=0)
551
+ log_alpha_new = log_alpha_new + log_emit[:, obs]
552
+ return log_alpha_new, None
553
+
554
+ log_alpha, _ = jax.lax.scan(forward_step, log_alpha, observations[1:])
555
+
556
+ return jax.scipy.special.logsumexp(log_alpha)
557
+
558
+ def forward_backward_posteriors(
559
+ self,
560
+ observations: Int[Array, "seq_len"],
561
+ ) -> Float[Array, "seq_len num_states"]:
562
+ """Compute state posteriors using forward-backward.
563
+
564
+ Args:
565
+ observations: Integer-encoded observations.
566
+
567
+ Returns:
568
+ State posteriors P(state | observations) at each position.
569
+ """
570
+ log_trans = self.get_log_transition_matrix()
571
+ log_emit = self.get_log_emission_matrix()
572
+ log_init = self.get_log_initial_distribution()
573
+
574
+ # Forward pass - store all alpha values
575
+ def forward_step(log_alpha, obs):
576
+ log_alpha_expanded = log_alpha[:, None]
577
+ log_alpha_new = jax.scipy.special.logsumexp(log_alpha_expanded + log_trans, axis=0)
578
+ log_alpha_new = log_alpha_new + log_emit[:, obs]
579
+ return log_alpha_new, log_alpha_new
580
+
581
+ log_alpha_init = log_init + log_emit[:, observations[0]]
582
+ _, log_alphas = jax.lax.scan(forward_step, log_alpha_init, observations[1:])
583
+ log_alphas = jnp.concatenate([log_alpha_init[None, :], log_alphas], axis=0)
584
+
585
+ # Backward pass
586
+ def backward_step(log_beta, obs):
587
+ log_beta_expanded = log_beta + log_emit[:, obs]
588
+ log_beta_new = jax.scipy.special.logsumexp(log_trans + log_beta_expanded, axis=1)
589
+ return log_beta_new, log_beta_new
590
+
591
+ log_beta_init = jnp.zeros(self.num_states)
592
+ _, log_betas_rev = jax.lax.scan(backward_step, log_beta_init, observations[1:][::-1])
593
+ log_betas = jnp.concatenate([log_betas_rev[::-1], log_beta_init[None, :]], axis=0)
594
+
595
+ # Compute posteriors
596
+ log_posteriors = log_alphas + log_betas
597
+ log_posteriors = log_posteriors - jax.scipy.special.logsumexp(
598
+ log_posteriors, axis=1, keepdims=True
599
+ )
600
+
601
+ return jnp.exp(log_posteriors)
602
+
603
+ def apply(
604
+ self,
605
+ data: PyTree,
606
+ state: PyTree,
607
+ metadata: dict[str, Any] | None,
608
+ random_params: Any = None,
609
+ stats: dict[str, Any] | None = None,
610
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
611
+ """Base apply method - should be overridden by subclasses."""
612
+ raise NotImplementedError("Subclasses must implement apply()")