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,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()")
|