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,490 @@
1
+ """End-to-end differentiable variant calling pipeline.
2
+
3
+ This module provides a complete variant calling pipeline that composes:
4
+ 1. Quality filtering - Filter low-quality reads
5
+ 2. Pileup generation - Aggregate reads at each position
6
+ 3. Variant classification - Classify each position as variant/reference
7
+
8
+ The pipeline is fully differentiable, enabling gradient-based optimization
9
+ of all components jointly.
10
+ """
11
+
12
+ import logging
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+ import jax
17
+ import jax.numpy as jnp
18
+ from datarax.core.config import OperatorConfig
19
+ from datarax.typing import Batch
20
+ from datarax.core.operator import OperatorModule
21
+ from flax import nnx
22
+ from jaxtyping import Array, Float
23
+
24
+ from diffbio.constants import ClassifierType
25
+ from diffbio.utils.nn_utils import extract_windows_1d
26
+ from diffbio.utils.quality import apply_quality_filter
27
+ from diffbio.operators.quality_filter import (
28
+ DifferentiableQualityFilter,
29
+ QualityFilterConfig,
30
+ )
31
+ from diffbio.operators.variant import (
32
+ CNNVariantClassifier,
33
+ CNNVariantClassifierConfig,
34
+ DifferentiablePileup,
35
+ PileupConfig,
36
+ VariantClassifier,
37
+ VariantClassifierConfig,
38
+ )
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class VariantCallingPipelineConfig(OperatorConfig):
45
+ # pylint: disable=too-many-instance-attributes
46
+ """Configuration for the variant calling pipeline.
47
+
48
+ Attributes:
49
+ reference_length: Length of reference sequence
50
+ num_classes: Number of variant classes (default: 3 for ref/snp/indel)
51
+ quality_threshold: Initial quality score threshold for filtering
52
+ pileup_window_size: Window size for pileup context
53
+ classifier_hidden_dim: Hidden dimension for classifier MLP
54
+ use_quality_weights: Whether to weight pileup by quality scores
55
+ classifier_type: Type of classifier (ClassifierType.MLP or ClassifierType.CNN)
56
+ cnn_hidden_channels: Hidden channels for CNN classifier
57
+ cnn_fc_dims: Fully connected layer dimensions for CNN
58
+ apply_pileup_softmax: Whether to apply softmax to pileup output
59
+ """
60
+
61
+ reference_length: int = 100
62
+ num_classes: int = 3
63
+ quality_threshold: float = 20.0
64
+ pileup_window_size: int = 11
65
+ classifier_hidden_dim: int = 64
66
+ use_quality_weights: bool = True
67
+ classifier_type: str = ClassifierType.MLP # ClassifierType.MLP or ClassifierType.CNN
68
+ cnn_hidden_channels: tuple[int, ...] = (32, 64)
69
+ cnn_fc_dims: tuple[int, ...] = (64, 32)
70
+ apply_pileup_softmax: bool = True # False is better for variant detection
71
+
72
+
73
+ class VariantCallingPipeline(OperatorModule):
74
+ """End-to-end differentiable variant calling pipeline.
75
+
76
+ This pipeline processes sequencing reads to call variants:
77
+
78
+ Input data structure:
79
+ - reads: Float[Array, "num_reads read_length 4"] - One-hot encoded reads
80
+ - positions: Int[Array, "num_reads"] - Read start positions on reference
81
+ - quality: Float[Array, "num_reads read_length"] - Base quality scores
82
+
83
+ Output data structure (adds):
84
+ - pileup: Float[Array, "reference_length 4"] - Aggregated base frequencies
85
+ - logits: Float[Array, "reference_length num_classes"] - Raw predictions
86
+ - probabilities: Float[Array, "reference_length num_classes"] - Class probs
87
+
88
+ The pipeline is fully differentiable, supporting gradient-based training
89
+ to optimize quality filtering, pileup aggregation, and classification jointly.
90
+
91
+ Example:
92
+ ```python
93
+ config = VariantCallingPipelineConfig(reference_length=100)
94
+ pipeline = VariantCallingPipeline(config, rngs=nnx.Rngs(42))
95
+ pipeline.eval_mode() # Disable dropout for inference
96
+ # Process a batch of samples
97
+ result_batch = pipeline(input_batch)
98
+ probs = result_batch.data.get_value()["probabilities"]
99
+ ```
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ config: VariantCallingPipelineConfig,
105
+ *,
106
+ rngs: nnx.Rngs,
107
+ name: str | None = None,
108
+ ):
109
+ """Initialize the variant calling pipeline.
110
+
111
+ Args:
112
+ config: Pipeline configuration
113
+ rngs: Random number generators for parameter initialization
114
+ name: Optional name for the pipeline
115
+ """
116
+ super().__init__(config, rngs=rngs, name=name)
117
+
118
+ # Initialize sub-operators
119
+ # 1. Quality filter for preprocessing reads
120
+ self.quality_filter = DifferentiableQualityFilter(
121
+ QualityFilterConfig(initial_threshold=config.quality_threshold),
122
+ rngs=rngs,
123
+ )
124
+
125
+ # 2. Pileup generator - always return coverage and quality for CNN
126
+ use_multichannel = config.classifier_type == ClassifierType.CNN
127
+ self.pileup = DifferentiablePileup(
128
+ PileupConfig(
129
+ use_quality_weights=config.use_quality_weights,
130
+ reference_length=config.reference_length,
131
+ return_coverage=use_multichannel,
132
+ return_quality=use_multichannel,
133
+ apply_softmax=config.apply_pileup_softmax,
134
+ ),
135
+ rngs=rngs,
136
+ )
137
+
138
+ # 3. Variant classifier (per-position)
139
+ if config.classifier_type == ClassifierType.CNN:
140
+ # CNN classifier takes pileup images with multiple channels
141
+ # Channels: 4 (base) + 1 (coverage) + 1 (quality) = 6
142
+ self.classifier = CNNVariantClassifier(
143
+ CNNVariantClassifierConfig(
144
+ num_classes=config.num_classes,
145
+ input_height=1, # Single "row" per position
146
+ input_width=config.pileup_window_size,
147
+ num_channels=6, # base(4) + coverage(1) + quality(1)
148
+ hidden_channels=config.cnn_hidden_channels,
149
+ fc_dims=config.cnn_fc_dims,
150
+ ),
151
+ rngs=rngs,
152
+ )
153
+ else:
154
+ # MLP classifier
155
+ self.classifier = VariantClassifier(
156
+ VariantClassifierConfig(
157
+ num_classes=config.num_classes,
158
+ hidden_dim=config.classifier_hidden_dim,
159
+ input_window=config.pileup_window_size,
160
+ ),
161
+ rngs=rngs,
162
+ )
163
+
164
+ def set_training(self, training: bool = True) -> None:
165
+ """Set pipeline training mode.
166
+
167
+ Args:
168
+ training: If True, enable dropout. If False, disable dropout.
169
+ """
170
+ if training:
171
+ self.classifier.train()
172
+ else:
173
+ self.classifier.eval()
174
+
175
+ def train_mode(self) -> None:
176
+ """Set pipeline to training mode (enables dropout)."""
177
+ self.classifier.train()
178
+
179
+ def eval_mode(self) -> None:
180
+ """Set pipeline to evaluation mode (disables dropout)."""
181
+ self.classifier.eval()
182
+
183
+ def apply(
184
+ self,
185
+ data: dict[str, Array],
186
+ state: dict[str, Any],
187
+ metadata: dict[str, Any] | None,
188
+ random_params: Any = None,
189
+ stats: dict[str, Any] | None = None,
190
+ ) -> tuple[dict[str, Array], dict[str, Any], dict[str, Any] | None]:
191
+ """Apply the full variant calling pipeline to a single sample.
192
+
193
+ Args:
194
+ data: Input data containing:
195
+ - reads: Float[Array, "num_reads read_length 4"]
196
+ - positions: Int[Array, "num_reads"]
197
+ - quality: Float[Array, "num_reads read_length"]
198
+ state: Element state (passed through)
199
+ metadata: Element metadata (passed through)
200
+ random_params: Not used (deterministic pipeline)
201
+ stats: Optional statistics dict
202
+
203
+ Returns:
204
+ Tuple of (output_data, state, metadata) where output_data contains
205
+ all input keys plus pileup, logits, and probabilities.
206
+ """
207
+ reads = data["reads"]
208
+ positions = data["positions"]
209
+ quality = data["quality"]
210
+
211
+ # Step 1: Quality-weighted filtering
212
+ # Apply quality filter to each read position
213
+ filtered_reads, filtered_quality = apply_quality_filter(self.quality_filter, reads, quality)
214
+
215
+ # Step 2: Generate pileup
216
+ pileup_data = {
217
+ "reads": filtered_reads,
218
+ "positions": positions,
219
+ "quality": filtered_quality,
220
+ }
221
+ pileup_result, _, _ = self.pileup.apply(pileup_data, {}, None)
222
+ pileup = pileup_result["pileup"] # Shape: (reference_length, 4)
223
+
224
+ # Extract coverage and quality for CNN (if available)
225
+ coverage = pileup_result.get("coverage") # (reference_length, 1) or None
226
+ mean_quality = pileup_result.get("mean_quality") # (reference_length, 1) or None
227
+
228
+ # Step 3: Classify each position using sliding window
229
+ logits, probabilities = self._classify_positions(pileup, coverage, mean_quality)
230
+
231
+ # Build output preserving input keys
232
+ output_data = {
233
+ **data,
234
+ "filtered_reads": filtered_reads,
235
+ "filtered_quality": filtered_quality,
236
+ "pileup": pileup,
237
+ "logits": logits,
238
+ "probabilities": probabilities,
239
+ }
240
+
241
+ # Add coverage and quality if available
242
+ if coverage is not None:
243
+ output_data["coverage"] = coverage
244
+ if mean_quality is not None:
245
+ output_data["mean_quality"] = mean_quality
246
+
247
+ return output_data, state, metadata
248
+
249
+ def to_dag(self) -> Any:
250
+ """Build a datarax DAG representation of this pipeline.
251
+
252
+ Returns a ``Sequential`` node graph suitable for execution via
253
+ ``datarax.dag.DAGExecutor``.
254
+
255
+ Returns:
256
+ A datarax ``Sequential`` node containing the pipeline stages.
257
+ """
258
+ from datarax.dag import OperatorNode, Sequential # noqa: PLC0415
259
+
260
+ return Sequential(
261
+ [
262
+ OperatorNode(self.quality_filter),
263
+ OperatorNode(self.pileup),
264
+ OperatorNode(self.classifier),
265
+ ]
266
+ )
267
+
268
+ def _classify_positions(
269
+ self,
270
+ pileup: Float[Array, "reference_length 4"],
271
+ coverage: Float[Array, "reference_length 1"] | None = None,
272
+ mean_quality: Float[Array, "reference_length 1"] | None = None,
273
+ ) -> tuple[
274
+ Float[Array, "reference_length num_classes"], Float[Array, "reference_length num_classes"]
275
+ ]:
276
+ """Classify each reference position using pileup windows.
277
+
278
+ Extracts a window around each position and classifies it.
279
+ Uses padding at boundaries.
280
+
281
+ For MLP: Uses batch processing through the classifier's linear layers.
282
+ For CNN: Constructs 6-channel pileup images and uses CNN classifier.
283
+ """
284
+ reference_length = pileup.shape[0]
285
+ window_size = self.config.pileup_window_size
286
+ half_window = window_size // 2
287
+
288
+ if self.config.classifier_type == ClassifierType.CNN:
289
+ return self._classify_positions_cnn(
290
+ pileup, coverage, mean_quality, reference_length, window_size, half_window
291
+ )
292
+ else:
293
+ return self._classify_positions_mlp(pileup, reference_length, window_size, half_window)
294
+
295
+ def _classify_positions_mlp(
296
+ self,
297
+ pileup: Float[Array, "reference_length 4"],
298
+ reference_length: int,
299
+ window_size: int,
300
+ half_window: int,
301
+ ) -> tuple[
302
+ Float[Array, "reference_length num_classes"], Float[Array, "reference_length num_classes"]
303
+ ]:
304
+ """Classify positions using MLP classifier."""
305
+ del reference_length, half_window # Not needed - handled by extract_windows_1d
306
+
307
+ # Extract all windows using utility function
308
+ all_windows = extract_windows_1d(
309
+ pileup, window_size=window_size, pad_mode="edge"
310
+ ) # (reference_length, window_size, 4)
311
+
312
+ # Classify each extracted window through the classifier's public API.
313
+ logits = jax.vmap(self.classifier.classify)(all_windows)
314
+ probabilities = jax.nn.softmax(logits, axis=-1)
315
+
316
+ return logits, probabilities
317
+
318
+ def _classify_positions_cnn(
319
+ self,
320
+ pileup: Float[Array, "reference_length 4"],
321
+ coverage: Float[Array, "reference_length 1"] | None,
322
+ mean_quality: Float[Array, "reference_length 1"] | None,
323
+ reference_length: int,
324
+ window_size: int,
325
+ half_window: int,
326
+ ) -> tuple[
327
+ Float[Array, "reference_length num_classes"], Float[Array, "reference_length num_classes"]
328
+ ]:
329
+ """Classify positions using CNN classifier.
330
+
331
+ Creates 6-channel pileup images:
332
+ - Channels 0-3: Base distributions (A, C, G, T)
333
+ - Channel 4: Coverage (normalized by observed maximum coverage)
334
+ - Channel 5: Mean quality (normalized to 0-1)
335
+ """
336
+ del half_window # Not needed - handled by extract_windows_1d
337
+
338
+ # Normalize coverage and quality if provided
339
+ if coverage is not None:
340
+ coverage_scale = jnp.maximum(jnp.max(coverage), 1.0)
341
+ norm_coverage = coverage / coverage_scale # (reference_length, 1)
342
+ else:
343
+ norm_coverage = jnp.zeros((reference_length, 1))
344
+
345
+ if mean_quality is not None:
346
+ norm_quality = mean_quality / 40.0 # Normalize to ~0-1 (Phred max ~40)
347
+ else:
348
+ norm_quality = jnp.zeros((reference_length, 1))
349
+
350
+ # Concatenate all channels: (reference_length, 6)
351
+ pileup_6ch = jnp.concatenate([pileup, norm_coverage, norm_quality], axis=-1)
352
+
353
+ # Extract all windows using utility function
354
+ all_windows = extract_windows_1d(
355
+ pileup_6ch, window_size=window_size, pad_mode="edge"
356
+ ) # (reference_length, window_size, 6)
357
+
358
+ # Reshape for CNN: (batch, height, width=window_size, channels=6)
359
+ # We need height >= 2 for CNN's max_pool(2,2) to work
360
+ # Replicate the single row to create a 2D image that the CNN can process
361
+ min_height = 8 # Minimum height for CNN pooling
362
+ pileup_images = all_windows[:, None, :, :] # (reference_length, 1, window_size, 6)
363
+ pileup_images = jnp.tile(pileup_images, (1, min_height, 1, 1)) # (ref_len, 8, window, 6)
364
+
365
+ # Classify using CNN
366
+ logits = self.classifier.classify(pileup_images) # (reference_length, num_classes)
367
+ probabilities = jax.nn.softmax(logits, axis=-1)
368
+
369
+ return logits, probabilities
370
+
371
+ def call_variants(
372
+ self,
373
+ batch: Batch,
374
+ threshold: float = 0.5,
375
+ ) -> dict[str, Array]:
376
+ """Convenience method to call variants from a batch.
377
+
378
+ Args:
379
+ batch: Input batch with reads, positions, quality
380
+ threshold: Probability threshold for variant calling
381
+
382
+ Returns:
383
+ Dict containing:
384
+ - predictions: Int[Array, "batch reference_length"] - Predicted classes
385
+ - probabilities: Float[Array, "batch reference_length num_classes"]
386
+ - variant_positions: List of (batch_idx, position) tuples
387
+ """
388
+ # Process batch
389
+ result_batch = self.apply_batch(batch)
390
+ result_data = result_batch.data.get_value()
391
+
392
+ probabilities = result_data["probabilities"]
393
+ predictions = jnp.argmax(probabilities, axis=-1)
394
+
395
+ return {
396
+ "predictions": predictions,
397
+ "probabilities": probabilities,
398
+ "pileup": result_data["pileup"],
399
+ }
400
+
401
+
402
+ def create_variant_calling_pipeline(
403
+ reference_length: int = 100,
404
+ num_classes: int = 3,
405
+ quality_threshold: float = 20.0,
406
+ hidden_dim: int = 64,
407
+ classifier_type: str = ClassifierType.MLP,
408
+ pileup_window_size: int = 11,
409
+ apply_pileup_softmax: bool = True,
410
+ seed: int = 42,
411
+ ) -> VariantCallingPipeline:
412
+ """Factory function to create a variant calling pipeline.
413
+
414
+ Args:
415
+ reference_length: Length of reference sequence
416
+ num_classes: Number of variant classes
417
+ quality_threshold: Quality score threshold
418
+ hidden_dim: Hidden dimension for classifier
419
+ classifier_type: Type of classifier (ClassifierType.MLP or ClassifierType.CNN)
420
+ pileup_window_size: Window size for pileup context
421
+ apply_pileup_softmax: Whether to apply softmax to pileup (False is better
422
+ for variant detection as it preserves raw coverage-weighted signals)
423
+ seed: Random seed
424
+
425
+ Returns:
426
+ Configured VariantCallingPipeline instance
427
+ """
428
+ config = VariantCallingPipelineConfig(
429
+ reference_length=reference_length,
430
+ num_classes=num_classes,
431
+ quality_threshold=quality_threshold,
432
+ classifier_hidden_dim=hidden_dim,
433
+ classifier_type=classifier_type,
434
+ pileup_window_size=pileup_window_size,
435
+ apply_pileup_softmax=apply_pileup_softmax,
436
+ )
437
+ rngs = nnx.Rngs(seed)
438
+ pipeline = VariantCallingPipeline(config, rngs=rngs)
439
+ pipeline.eval_mode()
440
+ return pipeline
441
+
442
+
443
+ def create_cnn_variant_pipeline(
444
+ reference_length: int = 100,
445
+ num_classes: int = 3,
446
+ quality_threshold: float = 20.0,
447
+ pileup_window_size: int = 21,
448
+ cnn_hidden_channels: tuple[int, ...] | None = None,
449
+ cnn_fc_dims: tuple[int, ...] | None = None,
450
+ seed: int = 42,
451
+ ) -> VariantCallingPipeline:
452
+ """Factory function to create a CNN-based variant calling pipeline.
453
+
454
+ This creates a pipeline using CNN-based classification, which processes
455
+ multi-channel pileup images similar to DeepVariant. The 6 channels are:
456
+ - 4 base distribution channels (A, C, G, T)
457
+ - 1 coverage channel (normalized)
458
+ - 1 quality channel (normalized)
459
+
460
+ Args:
461
+ reference_length: Length of reference sequence
462
+ num_classes: Number of variant classes
463
+ quality_threshold: Quality score threshold
464
+ pileup_window_size: Window size for pileup context (recommend 21+ for CNN)
465
+ cnn_hidden_channels: Hidden channels for CNN layers (default: (32, 64))
466
+ cnn_fc_dims: FC layer dimensions (default: (64, 32))
467
+ seed: Random seed
468
+
469
+ Returns:
470
+ Configured VariantCallingPipeline instance with CNN classifier
471
+ """
472
+ if cnn_hidden_channels is None:
473
+ cnn_hidden_channels = (32, 64)
474
+ if cnn_fc_dims is None:
475
+ cnn_fc_dims = (64, 32)
476
+
477
+ config = VariantCallingPipelineConfig(
478
+ reference_length=reference_length,
479
+ num_classes=num_classes,
480
+ quality_threshold=quality_threshold,
481
+ classifier_type=ClassifierType.CNN,
482
+ pileup_window_size=pileup_window_size,
483
+ cnn_hidden_channels=cnn_hidden_channels,
484
+ cnn_fc_dims=cnn_fc_dims,
485
+ apply_pileup_softmax=False, # Better for variant detection
486
+ )
487
+ rngs = nnx.Rngs(seed)
488
+ pipeline = VariantCallingPipeline(config, rngs=rngs)
489
+ pipeline.eval_mode()
490
+ return pipeline
@@ -0,0 +1,9 @@
1
+ """DiffBio samplers module.
2
+
3
+ Provides specialized sampler implementations extending datarax's SamplerModule
4
+ for bioinformatics applications.
5
+
6
+ Samplers:
7
+ PerturbationBatchSampler: Groups cells by (cell_type, perturbation) for
8
+ efficient batch construction in perturbation experiments.
9
+ """
@@ -0,0 +1,142 @@
1
+ """Perturbation-aware batch sampler for single-cell experiments.
2
+
3
+ Groups cells by (cell_type, perturbation) into "sentences", then combines
4
+ sentences into batches. Uses integer group codes for fast grouping.
5
+
6
+ References:
7
+ - cell-load/src/cell_load/data_modules/samplers.py (PerturbationBatchSampler)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from collections import defaultdict
14
+ from collections.abc import Iterator
15
+ from dataclasses import dataclass
16
+
17
+ import numpy as np
18
+
19
+ from datarax.core.config import StructuralConfig
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class PerturbationSamplerConfig(StructuralConfig):
26
+ """Configuration for PerturbationBatchSampler.
27
+
28
+ Attributes:
29
+ sentence_size: Number of cells per "sentence" (same perturbation
30
+ and cell type).
31
+ sentences_per_batch: Number of sentences combined into one batch.
32
+ seed: Random seed for shuffling.
33
+ drop_last: Whether to drop the last incomplete batch.
34
+ downsample_cells: Maximum cells per (cell_type, perturbation) group.
35
+ None means no downsampling.
36
+ """
37
+
38
+ sentence_size: int = 512
39
+ sentences_per_batch: int = 1
40
+ seed: int = 42
41
+ drop_last: bool = False
42
+ downsample_cells: int | None = None
43
+
44
+
45
+ class PerturbationBatchSampler:
46
+ """Groups cells by (cell_type, perturbation) into sentence-based batches.
47
+
48
+ Creates "sentences" where all cells share the same group code (typically
49
+ encoding cell_type and perturbation). Sentences are then combined into
50
+ batches. Supports epoch-aware shuffling and cell downsampling.
51
+
52
+ Args:
53
+ config: Sampler configuration.
54
+ group_codes: Per-cell integer group codes (from
55
+ ``PerturbationAnnDataSource.get_group_codes()``).
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ config: PerturbationSamplerConfig,
61
+ group_codes: np.ndarray,
62
+ ) -> None:
63
+ self._config = config
64
+ self._group_codes = group_codes
65
+ self._epoch = 0
66
+ self._sentences = self._create_sentences()
67
+
68
+ def __iter__(self) -> Iterator[list[int]]:
69
+ """Yield batches of cell indices.
70
+
71
+ Each batch contains ``sentences_per_batch`` sentences, where each
72
+ sentence is a group of ``sentence_size`` cells from the same
73
+ (cell_type, perturbation) group.
74
+
75
+ Yields:
76
+ Lists of cell indices forming each batch.
77
+ """
78
+ rng = np.random.default_rng(self._config.seed + self._epoch)
79
+
80
+ # Shuffle sentence order (not within sentences)
81
+ sentence_order = rng.permutation(len(self._sentences))
82
+
83
+ spb = self._config.sentences_per_batch
84
+ n_batches = len(self._sentences) // spb
85
+
86
+ for batch_idx in range(n_batches):
87
+ batch_indices: list[int] = []
88
+ for s_offset in range(spb):
89
+ s_idx = sentence_order[batch_idx * spb + s_offset]
90
+ batch_indices.extend(self._sentences[s_idx])
91
+ yield batch_indices
92
+
93
+ # Handle remainder unless drop_last
94
+ remainder_start = n_batches * spb
95
+ if not self._config.drop_last and remainder_start < len(self._sentences):
96
+ batch_indices = []
97
+ for s_idx in sentence_order[remainder_start:]:
98
+ batch_indices.extend(self._sentences[s_idx])
99
+ if batch_indices:
100
+ yield batch_indices
101
+
102
+ def __len__(self) -> int:
103
+ """Return the number of batches per epoch."""
104
+ spb = self._config.sentences_per_batch
105
+ n_full = len(self._sentences) // spb
106
+ has_remainder = not self._config.drop_last and len(self._sentences) % spb > 0
107
+ return n_full + int(has_remainder)
108
+
109
+ def set_epoch(self, epoch: int) -> None:
110
+ """Set the epoch for deterministic shuffling.
111
+
112
+ Args:
113
+ epoch: Current epoch number.
114
+ """
115
+ self._epoch = epoch
116
+
117
+ def _create_sentences(self) -> list[list[int]]:
118
+ """Group cell indices by group code and split into sentences."""
119
+ rng = np.random.default_rng(self._config.seed)
120
+ sentence_size = self._config.sentence_size
121
+ downsample = self._config.downsample_cells
122
+
123
+ # Group indices by group code
124
+ groups: dict[int, list[int]] = defaultdict(list)
125
+ for idx, code in enumerate(self._group_codes):
126
+ groups[int(code)].append(idx)
127
+
128
+ sentences: list[list[int]] = []
129
+
130
+ for _, indices in sorted(groups.items()):
131
+ cell_indices = np.array(indices)
132
+
133
+ # Apply cell downsampling
134
+ if downsample is not None and len(cell_indices) > downsample:
135
+ cell_indices = rng.choice(cell_indices, size=downsample, replace=False)
136
+
137
+ # Split into sentences
138
+ for start in range(0, len(cell_indices), sentence_size):
139
+ sentence = cell_indices[start : start + sentence_size].tolist()
140
+ sentences.append(sentence)
141
+
142
+ return sentences
@@ -0,0 +1,34 @@
1
+ """Biological sequence data types for DiffBio.
2
+
3
+ This module provides JAX-compatible data types for representing biological
4
+ sequences (DNA, RNA, Protein) that integrate with the Datarax Element system.
5
+ """
6
+
7
+ from diffbio.sequences.dna import (
8
+ DNA_ALPHABET,
9
+ DNA_ALPHABET_SIZE,
10
+ complement_dna,
11
+ create_dna_element_data,
12
+ decode_dna_onehot,
13
+ encode_dna_string,
14
+ gc_content,
15
+ phred_to_probability,
16
+ probability_to_phred,
17
+ reverse_complement_dna,
18
+ soft_encode_dna,
19
+ )
20
+
21
+
22
+ __all__ = [
23
+ "DNA_ALPHABET",
24
+ "DNA_ALPHABET_SIZE",
25
+ "complement_dna",
26
+ "create_dna_element_data",
27
+ "decode_dna_onehot",
28
+ "encode_dna_string",
29
+ "gc_content",
30
+ "phred_to_probability",
31
+ "probability_to_phred",
32
+ "reverse_complement_dna",
33
+ "soft_encode_dna",
34
+ ]