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,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