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,585 @@
|
|
|
1
|
+
"""Training utilities for differentiable bioinformatics pipelines.
|
|
2
|
+
|
|
3
|
+
This module provides training loops and utilities for end-to-end gradient-based
|
|
4
|
+
optimization of DiffBio pipelines using Flax NNX patterns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
import jax
|
|
13
|
+
import jax.numpy as jnp
|
|
14
|
+
import optax
|
|
15
|
+
from datarax.core.operator import OperatorModule
|
|
16
|
+
from flax import nnx
|
|
17
|
+
from jaxtyping import Array, Float
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class TrainingConfig:
|
|
24
|
+
"""Configuration for training loop.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
learning_rate: Learning rate for optimizer
|
|
28
|
+
num_epochs: Number of training epochs
|
|
29
|
+
log_every: Log metrics every N steps
|
|
30
|
+
grad_clip_norm: Maximum gradient norm (None to disable)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
learning_rate: float = 1e-3
|
|
34
|
+
num_epochs: int = 100
|
|
35
|
+
log_every: int = 10
|
|
36
|
+
grad_clip_norm: float | None = 1.0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class TrainingState:
|
|
41
|
+
"""State maintained during training.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
step: Current training step
|
|
45
|
+
epoch: Current epoch
|
|
46
|
+
loss_history: List of loss values
|
|
47
|
+
best_loss: Best loss seen so far
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
step: int = 0
|
|
51
|
+
epoch: int = 0
|
|
52
|
+
loss_history: list[float] | None = None
|
|
53
|
+
best_loss: float = float("inf")
|
|
54
|
+
|
|
55
|
+
def __post_init__(self):
|
|
56
|
+
if self.loss_history is None:
|
|
57
|
+
self.loss_history = []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def create_optax_optimizer(
|
|
61
|
+
config: TrainingConfig,
|
|
62
|
+
) -> optax.GradientTransformation:
|
|
63
|
+
"""Create optax optimizer with optional gradient clipping.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
config: Training configuration
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Optax optimizer
|
|
70
|
+
"""
|
|
71
|
+
transforms = []
|
|
72
|
+
|
|
73
|
+
if config.grad_clip_norm is not None:
|
|
74
|
+
transforms.append(optax.clip_by_global_norm(config.grad_clip_norm))
|
|
75
|
+
|
|
76
|
+
transforms.append(optax.adam(config.learning_rate))
|
|
77
|
+
|
|
78
|
+
return optax.chain(*transforms)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cross_entropy_loss(
|
|
82
|
+
logits: Float[Array, "... num_classes"],
|
|
83
|
+
labels: Float[Array, "..."],
|
|
84
|
+
num_classes: int = 3,
|
|
85
|
+
) -> Float[Array, ""]:
|
|
86
|
+
"""Compute cross-entropy loss for variant classification.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
logits: Raw model predictions
|
|
90
|
+
labels: Integer class labels
|
|
91
|
+
num_classes: Number of classes
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Scalar loss value
|
|
95
|
+
"""
|
|
96
|
+
one_hot_labels = jax.nn.one_hot(labels.astype(jnp.int32), num_classes)
|
|
97
|
+
log_probs = jax.nn.log_softmax(logits, axis=-1)
|
|
98
|
+
return -jnp.mean(jnp.sum(one_hot_labels * log_probs, axis=-1))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Trainer:
|
|
102
|
+
"""Training loop for DiffBio pipelines using Flax NNX patterns.
|
|
103
|
+
|
|
104
|
+
This class handles the training loop using NNX's stateful approach:
|
|
105
|
+
- Uses nnx.Optimizer for automatic parameter updates
|
|
106
|
+
- Uses @nnx.jit for JIT compilation with state management
|
|
107
|
+
- Supports gradient clipping and metric logging
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
```python
|
|
111
|
+
pipeline = create_variant_calling_pipeline(reference_length=100)
|
|
112
|
+
trainer = Trainer(pipeline, TrainingConfig(learning_rate=1e-3))
|
|
113
|
+
# Define loss function
|
|
114
|
+
def loss_fn(predictions, targets):
|
|
115
|
+
return cross_entropy_loss(
|
|
116
|
+
predictions["logits"],
|
|
117
|
+
targets["labels"],
|
|
118
|
+
)
|
|
119
|
+
# Train
|
|
120
|
+
trainer.train(data_iterator_fn, loss_fn)
|
|
121
|
+
trained_pipeline = trainer.pipeline
|
|
122
|
+
```
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
pipeline: OperatorModule,
|
|
128
|
+
config: TrainingConfig,
|
|
129
|
+
):
|
|
130
|
+
"""Initialize trainer.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
pipeline: Pipeline to train
|
|
134
|
+
config: Training configuration
|
|
135
|
+
"""
|
|
136
|
+
self.pipeline = pipeline
|
|
137
|
+
self.config = config
|
|
138
|
+
|
|
139
|
+
# Create NNX optimizer (holds mutable reference to model)
|
|
140
|
+
optax_opt = create_optax_optimizer(config)
|
|
141
|
+
self.optimizer = nnx.Optimizer(pipeline, optax_opt, wrt=nnx.Param)
|
|
142
|
+
|
|
143
|
+
# Training state
|
|
144
|
+
self.training_state = TrainingState()
|
|
145
|
+
|
|
146
|
+
def _create_train_step(
|
|
147
|
+
self,
|
|
148
|
+
loss_fn: Callable[[dict[str, Array], dict[str, Array]], Float[Array, ""]],
|
|
149
|
+
):
|
|
150
|
+
"""Create JIT-compiled training step using NNX patterns.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
loss_fn: Loss function taking (predictions, targets) and returning scalar
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
JIT-compiled training step function
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
@nnx.jit
|
|
160
|
+
def train_step(
|
|
161
|
+
model: OperatorModule,
|
|
162
|
+
optimizer: nnx.Optimizer,
|
|
163
|
+
batch_data: dict[str, Array],
|
|
164
|
+
targets: dict[str, Array],
|
|
165
|
+
) -> tuple[Float[Array, ""], dict[str, Any]]:
|
|
166
|
+
"""Single training step with NNX state management.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
model: The pipeline model
|
|
170
|
+
optimizer: NNX optimizer
|
|
171
|
+
batch_data: Input batch data
|
|
172
|
+
targets: Target labels
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Tuple of (loss, metrics)
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def compute_loss(model_inner: OperatorModule):
|
|
179
|
+
"""Apply the pipeline and compute the loss for gradient computation."""
|
|
180
|
+
# Apply pipeline
|
|
181
|
+
result_data, _, _ = model_inner.apply(batch_data, {}, None)
|
|
182
|
+
# Compute loss
|
|
183
|
+
loss = loss_fn(result_data, targets)
|
|
184
|
+
return loss
|
|
185
|
+
|
|
186
|
+
# Compute loss and gradients
|
|
187
|
+
loss, grads = nnx.value_and_grad(compute_loss)(model)
|
|
188
|
+
|
|
189
|
+
# Update parameters in-place via optimizer
|
|
190
|
+
# As of Flax 0.11.0, update requires both model and grads
|
|
191
|
+
optimizer.update(model, grads)
|
|
192
|
+
|
|
193
|
+
# Compute gradient norm for metrics
|
|
194
|
+
grad_leaves = jax.tree.leaves(nnx.state(grads, nnx.Param))
|
|
195
|
+
if grad_leaves:
|
|
196
|
+
grad_norm = jnp.sqrt(sum(jnp.sum(g**2) for g in grad_leaves))
|
|
197
|
+
else:
|
|
198
|
+
grad_norm = jnp.array(0.0)
|
|
199
|
+
|
|
200
|
+
metrics = {
|
|
201
|
+
"loss": loss,
|
|
202
|
+
"grad_norm": grad_norm,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return loss, metrics
|
|
206
|
+
|
|
207
|
+
return train_step
|
|
208
|
+
|
|
209
|
+
def train_epoch(
|
|
210
|
+
self,
|
|
211
|
+
data_iterator: Iterator[tuple[dict[str, Array], dict[str, Array]]],
|
|
212
|
+
loss_fn: Callable,
|
|
213
|
+
) -> dict[str, float]:
|
|
214
|
+
"""Train for one epoch.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
data_iterator: Iterator yielding (batch_data, targets) tuples
|
|
218
|
+
loss_fn: Loss function
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Dict of epoch metrics
|
|
222
|
+
"""
|
|
223
|
+
epoch_losses = []
|
|
224
|
+
train_step = self._create_train_step(loss_fn)
|
|
225
|
+
|
|
226
|
+
for batch_data, targets in data_iterator:
|
|
227
|
+
# Run training step (updates model in-place via optimizer)
|
|
228
|
+
loss, metrics = train_step(
|
|
229
|
+
self.pipeline,
|
|
230
|
+
self.optimizer,
|
|
231
|
+
batch_data,
|
|
232
|
+
targets,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
epoch_losses.append(float(loss))
|
|
236
|
+
self.training_state.step += 1
|
|
237
|
+
self.training_state.loss_history.append(float(loss))
|
|
238
|
+
|
|
239
|
+
# Log progress
|
|
240
|
+
if self.training_state.step % self.config.log_every == 0:
|
|
241
|
+
logger.info(
|
|
242
|
+
"Step %d: loss=%.4f, grad_norm=%.4f",
|
|
243
|
+
self.training_state.step,
|
|
244
|
+
float(loss),
|
|
245
|
+
float(metrics["grad_norm"]),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Update best loss
|
|
249
|
+
avg_loss = sum(epoch_losses) / len(epoch_losses) if epoch_losses else 0
|
|
250
|
+
if avg_loss < self.training_state.best_loss:
|
|
251
|
+
self.training_state.best_loss = avg_loss
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"avg_loss": avg_loss,
|
|
255
|
+
"min_loss": min(epoch_losses) if epoch_losses else 0,
|
|
256
|
+
"max_loss": max(epoch_losses) if epoch_losses else 0,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
def train(
|
|
260
|
+
self,
|
|
261
|
+
data_iterator_fn: Callable,
|
|
262
|
+
loss_fn: Callable,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Run full training loop.
|
|
265
|
+
|
|
266
|
+
After training, the pipeline is updated in-place with trained parameters.
|
|
267
|
+
Access via trainer.pipeline.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
data_iterator_fn: Function that returns a fresh data iterator
|
|
271
|
+
loss_fn: Loss function
|
|
272
|
+
"""
|
|
273
|
+
# Set pipeline to training mode
|
|
274
|
+
if hasattr(self.pipeline, "set_training"):
|
|
275
|
+
self.pipeline.set_training(True)
|
|
276
|
+
elif hasattr(self.pipeline, "train_mode"):
|
|
277
|
+
self.pipeline.train_mode()
|
|
278
|
+
|
|
279
|
+
for epoch in range(self.config.num_epochs):
|
|
280
|
+
self.training_state.epoch = epoch
|
|
281
|
+
data_iterator = data_iterator_fn()
|
|
282
|
+
|
|
283
|
+
metrics = self.train_epoch(data_iterator, loss_fn)
|
|
284
|
+
|
|
285
|
+
logger.info(
|
|
286
|
+
"Epoch %d/%d: avg_loss=%.4f",
|
|
287
|
+
epoch + 1,
|
|
288
|
+
self.config.num_epochs,
|
|
289
|
+
metrics["avg_loss"],
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Set back to eval mode
|
|
293
|
+
if hasattr(self.pipeline, "set_training"):
|
|
294
|
+
self.pipeline.set_training(False)
|
|
295
|
+
elif hasattr(self.pipeline, "eval_mode"):
|
|
296
|
+
self.pipeline.eval_mode()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def create_synthetic_training_data(
|
|
300
|
+
num_samples: int = 100,
|
|
301
|
+
num_reads: int = 10,
|
|
302
|
+
read_length: int = 50,
|
|
303
|
+
reference_length: int = 100,
|
|
304
|
+
variant_rate: float = 0.1,
|
|
305
|
+
seed: int = 42,
|
|
306
|
+
) -> tuple[list[dict[str, Array]], list[dict[str, Array]]]:
|
|
307
|
+
"""Create synthetic training data for variant calling.
|
|
308
|
+
|
|
309
|
+
Generates reads with simulated variants for training.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
num_samples: Number of samples to generate
|
|
313
|
+
num_reads: Number of reads per sample
|
|
314
|
+
read_length: Length of each read
|
|
315
|
+
reference_length: Length of reference sequence
|
|
316
|
+
variant_rate: Probability of variant at each position
|
|
317
|
+
seed: Random seed
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Tuple of (inputs, targets) where:
|
|
321
|
+
- inputs: List of dicts with reads, positions, quality
|
|
322
|
+
- targets: List of dicts with labels (0=ref, 1=snp, 2=indel)
|
|
323
|
+
"""
|
|
324
|
+
key = jax.random.PRNGKey(seed)
|
|
325
|
+
inputs = []
|
|
326
|
+
targets = []
|
|
327
|
+
|
|
328
|
+
for i in range(num_samples):
|
|
329
|
+
key, k1, k2, k3, k4 = jax.random.split(key, 5)
|
|
330
|
+
|
|
331
|
+
# Generate reference sequence
|
|
332
|
+
ref_indices = jax.random.randint(k1, (reference_length,), 0, 4)
|
|
333
|
+
ref_one_hot = jax.nn.one_hot(ref_indices, 4)
|
|
334
|
+
|
|
335
|
+
# Generate variant labels
|
|
336
|
+
variant_mask = jax.random.uniform(k2, (reference_length,)) < variant_rate
|
|
337
|
+
labels = jnp.where(
|
|
338
|
+
variant_mask,
|
|
339
|
+
jax.random.randint(k3, (reference_length,), 1, 3), # SNP or indel
|
|
340
|
+
0, # Reference
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Generate reads from reference (with variants)
|
|
344
|
+
positions = jax.random.randint(k4, (num_reads,), 0, reference_length - read_length)
|
|
345
|
+
|
|
346
|
+
# Extract read segments
|
|
347
|
+
def get_read(pos):
|
|
348
|
+
"""Extract a one-hot encoded read segment starting at the given position."""
|
|
349
|
+
segment = jax.lax.dynamic_slice(ref_one_hot, (pos, 0), (read_length, 4))
|
|
350
|
+
return segment
|
|
351
|
+
|
|
352
|
+
reads = jax.vmap(get_read)(positions)
|
|
353
|
+
|
|
354
|
+
# Add noise to reads at variant positions
|
|
355
|
+
key, k5 = jax.random.split(key)
|
|
356
|
+
noise = jax.random.normal(k5, reads.shape) * 0.1
|
|
357
|
+
reads = reads + noise
|
|
358
|
+
reads = jax.nn.softmax(reads, axis=-1) # Renormalize
|
|
359
|
+
|
|
360
|
+
# Generate quality scores
|
|
361
|
+
key, k6 = jax.random.split(key)
|
|
362
|
+
quality = jax.random.uniform(k6, (num_reads, read_length), minval=20.0, maxval=40.0)
|
|
363
|
+
|
|
364
|
+
inputs.append(
|
|
365
|
+
{
|
|
366
|
+
"reads": reads,
|
|
367
|
+
"positions": positions,
|
|
368
|
+
"quality": quality,
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
targets.append(
|
|
372
|
+
{
|
|
373
|
+
"labels": labels,
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
return inputs, targets
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def data_iterator(
|
|
381
|
+
inputs: list[dict[str, Array]],
|
|
382
|
+
targets: list[dict[str, Array]],
|
|
383
|
+
batch_size: int = 1,
|
|
384
|
+
) -> Iterator[tuple[dict[str, Array], dict[str, Array]]]:
|
|
385
|
+
"""Create an iterator over training data.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
inputs: List of input dicts
|
|
389
|
+
targets: List of target dicts
|
|
390
|
+
batch_size: Batch size (currently only supports 1)
|
|
391
|
+
|
|
392
|
+
Yields:
|
|
393
|
+
Tuples of (batch_data, targets)
|
|
394
|
+
"""
|
|
395
|
+
# For simplicity, yield one sample at a time
|
|
396
|
+
yield from zip(inputs, targets)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def create_realistic_training_data(
|
|
400
|
+
num_samples: int = 100,
|
|
401
|
+
num_reads: int = 20,
|
|
402
|
+
read_length: int = 50,
|
|
403
|
+
reference_length: int = 100,
|
|
404
|
+
variant_rate: float = 0.05,
|
|
405
|
+
heterozygous_rate: float = 0.5,
|
|
406
|
+
error_rate: float = 0.01,
|
|
407
|
+
seed: int = 42,
|
|
408
|
+
) -> tuple[list[dict[str, Array]], list[dict[str, Array]]]:
|
|
409
|
+
"""Create realistic synthetic training data for variant calling.
|
|
410
|
+
|
|
411
|
+
Unlike `create_synthetic_training_data`, this function generates reads that
|
|
412
|
+
actually contain variants at the labeled positions, making it possible for
|
|
413
|
+
models to learn meaningful patterns.
|
|
414
|
+
|
|
415
|
+
Features:
|
|
416
|
+
- SNP simulation: Substitutes reference bases with alternate alleles
|
|
417
|
+
- Heterozygous/homozygous modeling: Controls allele frequency in reads
|
|
418
|
+
- Quality modeling: Position-dependent quality (higher in read center)
|
|
419
|
+
- Sequencing errors: Random substitutions with low quality scores
|
|
420
|
+
- Strand information: Assigns reads to forward/reverse strands
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
num_samples: Number of samples to generate
|
|
424
|
+
num_reads: Number of reads per sample
|
|
425
|
+
read_length: Length of each read
|
|
426
|
+
reference_length: Length of reference sequence
|
|
427
|
+
variant_rate: Probability of variant at each position (default 0.05)
|
|
428
|
+
heterozygous_rate: Fraction of variants that are heterozygous (default 0.5)
|
|
429
|
+
error_rate: Probability of sequencing error per base (default 0.01)
|
|
430
|
+
seed: Random seed
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Tuple of (inputs, targets) where:
|
|
434
|
+
- inputs: List of dicts with reads, positions, quality, strand
|
|
435
|
+
- targets: List of dicts with labels (0=ref, 1=snp, 2=indel),
|
|
436
|
+
variant_alleles, zygosity
|
|
437
|
+
"""
|
|
438
|
+
key = jax.random.PRNGKey(seed)
|
|
439
|
+
inputs = []
|
|
440
|
+
targets = []
|
|
441
|
+
|
|
442
|
+
for _ in range(num_samples):
|
|
443
|
+
key, *keys = jax.random.split(key, 9)
|
|
444
|
+
k_ref, k_var, k_type, k_pos, k_het, k_alt, k_read_var, k_strand = keys
|
|
445
|
+
|
|
446
|
+
# Generate reference sequence (A=0, C=1, G=2, T=3)
|
|
447
|
+
ref_indices = jax.random.randint(k_ref, (reference_length,), 0, 4)
|
|
448
|
+
ref_one_hot = jax.nn.one_hot(ref_indices, 4)
|
|
449
|
+
|
|
450
|
+
# Generate variant positions and types
|
|
451
|
+
variant_mask = jax.random.uniform(k_var, (reference_length,)) < variant_rate
|
|
452
|
+
# Variant types: 1=SNP, 2=deletion (simplified - no insertions for now)
|
|
453
|
+
# Focus on SNPs (type 1) for better learning signal
|
|
454
|
+
variant_types = jnp.where(
|
|
455
|
+
variant_mask,
|
|
456
|
+
jnp.where(
|
|
457
|
+
jax.random.uniform(k_type, (reference_length,)) < 0.9,
|
|
458
|
+
1, # SNP (90%)
|
|
459
|
+
2, # Deletion (10%)
|
|
460
|
+
),
|
|
461
|
+
0, # Reference
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Determine zygosity for each variant (0=hom, 1=het)
|
|
465
|
+
is_heterozygous = jax.random.uniform(k_het, (reference_length,)) < heterozygous_rate
|
|
466
|
+
|
|
467
|
+
# Generate alternate alleles for SNPs (different from reference)
|
|
468
|
+
# For each position, pick a random base that's different from reference
|
|
469
|
+
alt_offsets = jax.random.randint(k_alt, (reference_length,), 1, 4)
|
|
470
|
+
alt_indices = (ref_indices + alt_offsets) % 4
|
|
471
|
+
alt_one_hot = jax.nn.one_hot(alt_indices, 4)
|
|
472
|
+
|
|
473
|
+
# Generate read positions
|
|
474
|
+
positions = jax.random.randint(k_pos, (num_reads,), 0, reference_length - read_length)
|
|
475
|
+
|
|
476
|
+
# Determine which reads carry variant allele (for heterozygous sites)
|
|
477
|
+
# For homozygous variants: all reads show variant
|
|
478
|
+
# For heterozygous variants: ~50% of reads show variant
|
|
479
|
+
read_shows_variant = jax.random.uniform(k_read_var, (num_reads,)) < 0.5
|
|
480
|
+
|
|
481
|
+
# Generate strand assignments (0=forward, 1=reverse)
|
|
482
|
+
strands = jax.random.randint(k_strand, (num_reads,), 0, 2)
|
|
483
|
+
|
|
484
|
+
# Build reads with actual variants
|
|
485
|
+
def build_read(read_idx):
|
|
486
|
+
"""Build a single read with variants applied at appropriate positions."""
|
|
487
|
+
pos = positions[read_idx]
|
|
488
|
+
shows_var = read_shows_variant[read_idx]
|
|
489
|
+
|
|
490
|
+
# Get reference segment for this read
|
|
491
|
+
def get_base_at(offset):
|
|
492
|
+
"""Return the base at a read offset, substituting variants when applicable."""
|
|
493
|
+
ref_pos = pos + offset
|
|
494
|
+
ref_base = ref_one_hot[ref_pos]
|
|
495
|
+
alt_base = alt_one_hot[ref_pos]
|
|
496
|
+
|
|
497
|
+
# Check if this position is a variant
|
|
498
|
+
is_var = variant_types[ref_pos] == 1 # SNP
|
|
499
|
+
is_het = is_heterozygous[ref_pos]
|
|
500
|
+
|
|
501
|
+
# Use variant allele if:
|
|
502
|
+
# - Position is a variant AND
|
|
503
|
+
# - (homozygous OR (heterozygous AND this read shows variant))
|
|
504
|
+
use_variant = is_var & (~is_het | shows_var)
|
|
505
|
+
|
|
506
|
+
return jnp.where(use_variant, alt_base, ref_base)
|
|
507
|
+
|
|
508
|
+
# Build read base by base
|
|
509
|
+
read_bases = jax.vmap(get_base_at)(jnp.arange(read_length))
|
|
510
|
+
return read_bases
|
|
511
|
+
|
|
512
|
+
reads = jax.vmap(build_read)(jnp.arange(num_reads))
|
|
513
|
+
|
|
514
|
+
# Add sequencing errors (random substitutions at error_rate)
|
|
515
|
+
key, k_err_pos, k_err_base = jax.random.split(key, 3)
|
|
516
|
+
error_mask = jax.random.uniform(k_err_pos, (num_reads, read_length)) < error_rate
|
|
517
|
+
|
|
518
|
+
# Generate random error bases
|
|
519
|
+
error_offsets = jax.random.randint(k_err_base, (num_reads, read_length), 1, 4)
|
|
520
|
+
current_bases = jnp.argmax(reads, axis=-1) # Get current base indices
|
|
521
|
+
error_bases = (current_bases + error_offsets) % 4
|
|
522
|
+
error_one_hot = jax.nn.one_hot(error_bases, 4)
|
|
523
|
+
|
|
524
|
+
# Apply errors
|
|
525
|
+
reads = jnp.where(error_mask[..., None], error_one_hot, reads)
|
|
526
|
+
|
|
527
|
+
# Generate quality scores with position-dependent profile
|
|
528
|
+
# Quality is higher in middle of read, lower at ends
|
|
529
|
+
key, k_qual_noise = jax.random.split(key, 2)
|
|
530
|
+
|
|
531
|
+
# Position-dependent base quality (parabolic profile)
|
|
532
|
+
pos_in_read = jnp.arange(read_length)
|
|
533
|
+
center = read_length / 2
|
|
534
|
+
# Quality ranges from 25 at ends to 35 in middle
|
|
535
|
+
base_quality = 35.0 - 10.0 * ((pos_in_read - center) / center) ** 2
|
|
536
|
+
base_quality = jnp.broadcast_to(base_quality, (num_reads, read_length))
|
|
537
|
+
|
|
538
|
+
# Add random variation
|
|
539
|
+
quality_noise = jax.random.uniform(
|
|
540
|
+
k_qual_noise,
|
|
541
|
+
(num_reads, read_length),
|
|
542
|
+
minval=-3.0,
|
|
543
|
+
maxval=3.0,
|
|
544
|
+
)
|
|
545
|
+
quality = base_quality + quality_noise
|
|
546
|
+
|
|
547
|
+
# Lower quality at error positions
|
|
548
|
+
quality = jnp.where(error_mask, jnp.minimum(quality, 15.0), quality)
|
|
549
|
+
|
|
550
|
+
# Slightly lower quality near variant positions in reads
|
|
551
|
+
def check_variant_overlap(read_idx):
|
|
552
|
+
"""Check which positions in a read overlap with variant sites."""
|
|
553
|
+
pos = positions[read_idx]
|
|
554
|
+
|
|
555
|
+
# Get variant status for each position in this read
|
|
556
|
+
def is_var_at(offset):
|
|
557
|
+
"""Return whether the position at this offset is a variant site."""
|
|
558
|
+
ref_pos = pos + offset
|
|
559
|
+
return variant_types[ref_pos] > 0
|
|
560
|
+
|
|
561
|
+
return jax.vmap(is_var_at)(jnp.arange(read_length))
|
|
562
|
+
|
|
563
|
+
variant_in_read = jax.vmap(check_variant_overlap)(jnp.arange(num_reads))
|
|
564
|
+
quality = jnp.where(variant_in_read, quality - 2.0, quality)
|
|
565
|
+
|
|
566
|
+
# Clamp quality to valid range
|
|
567
|
+
quality = jnp.clip(quality, 5.0, 40.0)
|
|
568
|
+
|
|
569
|
+
inputs.append(
|
|
570
|
+
{
|
|
571
|
+
"reads": reads,
|
|
572
|
+
"positions": positions,
|
|
573
|
+
"quality": quality,
|
|
574
|
+
"strand": strands,
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
targets.append(
|
|
578
|
+
{
|
|
579
|
+
"labels": variant_types,
|
|
580
|
+
"variant_alleles": alt_indices,
|
|
581
|
+
"is_heterozygous": is_heterozygous.astype(jnp.int32),
|
|
582
|
+
}
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
return inputs, targets
|