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,333 @@
1
+ """Variant classifier for differentiable variant calling.
2
+
3
+ This module provides neural network classifiers for identifying variants
4
+ from pileup representations, including a cell-type-aware classifier that
5
+ weights variant calls by soft cell-type assignments.
6
+ """
7
+
8
+ import logging
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ import jax
13
+ import jax.numpy as jnp
14
+ from artifex.generative_models.core.base import MLP
15
+ from datarax.core.config import OperatorConfig
16
+ from datarax.core.operator import OperatorModule
17
+ from flax import nnx
18
+ from jaxtyping import Array, Float, PyTree
19
+
20
+ from diffbio.configs import ClassifierConfig
21
+ from diffbio.constants import DEFAULT_PILEUP_WINDOW_SIZE, DNA_ALPHABET_SIZE
22
+ from diffbio.utils.nn_utils import ARTIFEX_RELU_MLP_KWARGS, ensure_rngs
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class VariantClassifierConfig(ClassifierConfig):
29
+ """Configuration for variant classifier.
30
+
31
+ Attributes:
32
+ num_classes: Number of variant classes (default: 3 for REF/SNV/INDEL).
33
+ hidden_dim: Hidden layer dimension.
34
+ num_layers: Number of hidden layers.
35
+ dropout_rate: Dropout rate for regularization.
36
+ input_window: Default input window size for pileup.
37
+ """
38
+
39
+ input_window: int = DEFAULT_PILEUP_WINDOW_SIZE
40
+
41
+
42
+ class VariantClassifier(OperatorModule):
43
+ """Neural network classifier for variant calling.
44
+
45
+ Takes a window of pileup data around a position and classifies it
46
+ as reference, SNV, or indel. Uses a simple MLP architecture that
47
+ is fully differentiable.
48
+
49
+ Args:
50
+ config: Classifier configuration.
51
+ rngs: Flax NNX random number generators.
52
+ name: Optional operator name.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ config: VariantClassifierConfig,
58
+ *,
59
+ rngs: nnx.Rngs,
60
+ name: str | None = None,
61
+ ) -> None:
62
+ """Initialize variant classifier.
63
+
64
+ Args:
65
+ config: Classifier configuration.
66
+ rngs: Random number generators.
67
+ name: Optional operator name.
68
+ """
69
+ super().__init__(config, rngs=rngs, name=name)
70
+
71
+ # Input dimension: window_size * alphabet_size (nucleotides)
72
+ input_dim = config.input_window * DNA_ALPHABET_SIZE
73
+
74
+ if config.num_layers < 1:
75
+ raise ValueError("VariantClassifierConfig.num_layers must be at least 1.")
76
+
77
+ self.backbone = MLP(
78
+ hidden_dims=[config.hidden_dim] * config.num_layers,
79
+ in_features=input_dim,
80
+ dropout_rate=config.dropout_rate,
81
+ rngs=rngs,
82
+ **ARTIFEX_RELU_MLP_KWARGS,
83
+ )
84
+
85
+ # Output layer
86
+ self.output_layer = nnx.Linear(config.hidden_dim, config.num_classes, rngs=rngs)
87
+
88
+ def classify(
89
+ self,
90
+ pileup_window: Float[Array, "window_size 4"],
91
+ ) -> Float[Array, "num_classes"]:
92
+ """Classify variant from pileup window.
93
+
94
+ Args:
95
+ pileup_window: Pileup data for window around position.
96
+ Shape: (window_size, 4) with nucleotide distributions.
97
+
98
+ Returns:
99
+ Logits for each variant class. Shape: (num_classes,).
100
+ """
101
+ # Flatten pileup window
102
+ x = pileup_window.reshape(-1)
103
+ backbone_output = self.backbone(x)
104
+ if isinstance(backbone_output, tuple):
105
+ raise TypeError("VariantClassifier backbone must return a single tensor output.")
106
+ x = backbone_output
107
+
108
+ # Output
109
+ logits = self.output_layer(x)
110
+
111
+ return logits
112
+
113
+ def apply(
114
+ self,
115
+ data: PyTree,
116
+ state: PyTree,
117
+ metadata: dict[str, Any] | None,
118
+ random_params: Any = None,
119
+ stats: dict[str, Any] | None = None,
120
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
121
+ """Apply variant classification to pileup data.
122
+
123
+ This method implements the OperatorModule interface for batch processing.
124
+ It expects data containing a pileup window and returns classification logits.
125
+
126
+ Note: Output preserves input keys for Datarax vmap compatibility,
127
+ while adding classification result keys.
128
+
129
+ Args:
130
+ data: Dictionary containing:
131
+ - "pileup_window": Pileup data around position (window_size, 4)
132
+ state: Element state (passed through unchanged)
133
+ metadata: Element metadata (passed through unchanged)
134
+ random_params: Not used (dropout handled by eval/train mode)
135
+ stats: Not used
136
+
137
+ Returns:
138
+ Tuple of (transformed_data, state, metadata):
139
+ - transformed_data contains input pileup_window plus logits and
140
+ probabilities
141
+ - state is passed through unchanged
142
+ - metadata is passed through unchanged
143
+ """
144
+ import jax.nn
145
+
146
+ pileup_window = data["pileup_window"]
147
+
148
+ # Classify
149
+ logits = self.classify(pileup_window)
150
+
151
+ # Build output data - preserve input keys for Datarax vmap compatibility
152
+ transformed_data = {
153
+ "pileup_window": pileup_window,
154
+ "logits": logits,
155
+ "probabilities": jax.nn.softmax(logits),
156
+ }
157
+
158
+ return transformed_data, state, metadata
159
+
160
+
161
+ @dataclass(frozen=True)
162
+ class CellTypeAwareVariantClassifierConfig(OperatorConfig):
163
+ """Configuration for cell-type-aware variant classifier.
164
+
165
+ This classifier uses separate classification heads per cell type,
166
+ weighted by soft cell-type assignments to produce cell-type-specific
167
+ variant calling thresholds.
168
+
169
+ Attributes:
170
+ n_classes: Number of variant types (e.g., SNP, indel, ref).
171
+ hidden_dim: Hidden layer dimension for the shared feature encoder.
172
+ n_cell_types: Number of cell types for per-type heads.
173
+ pileup_channels: Number of channels in pileup input.
174
+ pileup_width: Width of pileup input.
175
+ """
176
+
177
+ n_classes: int = 3
178
+ hidden_dim: int = 64
179
+ n_cell_types: int = 5
180
+ pileup_channels: int = 6
181
+ pileup_width: int = 100
182
+
183
+
184
+ class CellTypeAwareVariantClassifier(OperatorModule):
185
+ """Cell-type-aware variant classifier with per-type classification heads.
186
+
187
+ Uses separate classification heads for each cell type, weighted by soft
188
+ cell-type assignment probabilities. This allows different variant calling
189
+ thresholds per cell type, enabling more accurate variant detection in
190
+ heterogeneous cell populations (e.g., single-cell sequencing).
191
+
192
+ Architecture:
193
+ 1. Shared feature encoder: pileup -> flatten -> Linear -> ReLU -> hidden features
194
+ 2. Per-type classification heads: n_cell_types separate Linear(hidden, n_classes)
195
+ 3. Each head produces type-specific variant logits -> softmax probabilities
196
+ 4. Final aggregation: sum_t(cell_type_weights[:, t] * head_t_probs)
197
+
198
+ Args:
199
+ config: CellTypeAwareVariantClassifierConfig with model parameters.
200
+ rngs: Flax NNX random number generators.
201
+ name: Optional operator name.
202
+
203
+ Example:
204
+ ```python
205
+ config = CellTypeAwareVariantClassifierConfig(n_classes=3, n_cell_types=5)
206
+ classifier = CellTypeAwareVariantClassifier(config, rngs=nnx.Rngs(42))
207
+ data = {
208
+ "pileup": pileup_batch, # (n, channels, width)
209
+ "cell_type_assignments": assignments, # (n, n_cell_types)
210
+ }
211
+ result, state, meta = classifier.apply(data, {}, None)
212
+ # result["variant_probabilities"] -> (n, n_classes)
213
+ # result["per_type_probabilities"] -> (n, n_cell_types, n_classes)
214
+ ```
215
+ """
216
+
217
+ def __init__(
218
+ self,
219
+ config: CellTypeAwareVariantClassifierConfig,
220
+ *,
221
+ rngs: nnx.Rngs | None = None,
222
+ name: str | None = None,
223
+ ) -> None:
224
+ """Initialize cell-type-aware variant classifier.
225
+
226
+ Args:
227
+ config: Classifier configuration.
228
+ rngs: Random number generators for parameter initialization.
229
+ name: Optional operator name.
230
+ """
231
+ super().__init__(config, rngs=rngs, name=name)
232
+
233
+ rngs = ensure_rngs(rngs)
234
+
235
+ input_dim = config.pileup_channels * config.pileup_width
236
+
237
+ # Shared feature encoder: pileup -> hidden features
238
+ self.encoder = nnx.Linear(input_dim, config.hidden_dim, rngs=rngs)
239
+
240
+ # Per-cell-type classification heads
241
+ heads = []
242
+ for _ in range(config.n_cell_types):
243
+ heads.append(nnx.Linear(config.hidden_dim, config.n_classes, rngs=rngs))
244
+ self.classification_heads = nnx.List(heads)
245
+
246
+ def _encode(
247
+ self,
248
+ pileup: Float[Array, "n channels width"],
249
+ ) -> Float[Array, "n hidden_dim"]:
250
+ """Encode pileup into hidden features via the shared encoder.
251
+
252
+ Args:
253
+ pileup: Batch of pileup data, shape (n, channels, width).
254
+
255
+ Returns:
256
+ Hidden feature vectors, shape (n, hidden_dim).
257
+ """
258
+ x = pileup.reshape(pileup.shape[0], -1) # (n, channels * width)
259
+ x = self.encoder(x)
260
+ return nnx.relu(x)
261
+
262
+ def _classify_per_type(
263
+ self,
264
+ features: Float[Array, "n hidden_dim"],
265
+ ) -> Float[Array, "n n_cell_types n_classes"]:
266
+ """Run each cell-type classification head on shared features.
267
+
268
+ Args:
269
+ features: Shared hidden features, shape (n, hidden_dim).
270
+
271
+ Returns:
272
+ Per-type softmax probabilities, shape (n, n_cell_types, n_classes).
273
+ """
274
+ head_outputs = []
275
+ for head in self.classification_heads:
276
+ logits = head(features) # (n, n_classes)
277
+ probs = jax.nn.softmax(logits, axis=-1)
278
+ head_outputs.append(probs)
279
+ # Stack: (n_cell_types, n, n_classes) -> transpose to (n, n_cell_types, n_classes)
280
+ return jnp.stack(head_outputs, axis=1)
281
+
282
+ def apply(
283
+ self,
284
+ data: PyTree,
285
+ state: PyTree,
286
+ metadata: dict[str, Any] | None,
287
+ random_params: Any = None,
288
+ stats: dict[str, Any] | None = None,
289
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
290
+ """Apply cell-type-aware variant classification.
291
+
292
+ Computes per-type variant probabilities and aggregates them using
293
+ cell-type assignment weights.
294
+
295
+ Args:
296
+ data: Dictionary containing:
297
+ - "pileup": Pileup data, shape (n, channels, width).
298
+ - "cell_type_assignments": Soft cell-type weights, shape (n, n_cell_types).
299
+ state: Element state (passed through unchanged).
300
+ metadata: Element metadata (passed through unchanged).
301
+ random_params: Not used.
302
+ stats: Not used.
303
+
304
+ Returns:
305
+ Tuple of (transformed_data, state, metadata):
306
+ - transformed_data contains all input keys plus:
307
+ - "variant_probabilities": Aggregated probabilities (n, n_classes)
308
+ - "per_type_probabilities": Per-type probabilities
309
+ (n, n_cell_types, n_classes)
310
+ - state passed through unchanged
311
+ - metadata passed through unchanged
312
+ """
313
+ pileup = data["pileup"]
314
+ cell_type_assignments = data["cell_type_assignments"]
315
+
316
+ # 1. Shared feature encoding
317
+ features = self._encode(pileup)
318
+
319
+ # 2. Per-type classification
320
+ per_type_probs = self._classify_per_type(features) # (n, n_cell_types, n_classes)
321
+
322
+ # 3. Weighted aggregation: sum_t( assignments[:, t] * per_type_probs[:, t, :] )
323
+ # assignments: (n, n_cell_types) -> (n, n_cell_types, 1)
324
+ weights = cell_type_assignments[:, :, None]
325
+ variant_probs = jnp.sum(weights * per_type_probs, axis=1) # (n, n_classes)
326
+
327
+ transformed_data = {
328
+ **data,
329
+ "variant_probabilities": variant_probs,
330
+ "per_type_probabilities": per_type_probs,
331
+ }
332
+
333
+ return transformed_data, state, metadata
@@ -0,0 +1,255 @@
1
+ """CNN Variant Classifier for DeepVariant-style pileup classification.
2
+
3
+ This module provides a convolutional neural network classifier for
4
+ variant calling from pileup images, inspired by DeepVariant.
5
+
6
+ Key technique: 2D convolutions on pileup images enable learning
7
+ spatial patterns in read alignments for accurate variant detection.
8
+
9
+ Applications: Germline/somatic variant calling, variant quality scoring.
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.core.operator import OperatorModule
20
+ from flax import nnx
21
+ from jaxtyping import Array, Float, PyTree
22
+
23
+ from diffbio.constants import DEFAULT_DROPOUT_RATE, DEFAULT_NUM_CLASSES
24
+ from diffbio.utils.nn_utils import ensure_rngs
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class CNNVariantClassifierConfig(OperatorConfig):
31
+ """Configuration for CNNVariantClassifier.
32
+
33
+ Attributes:
34
+ num_classes: Number of variant classes (default: 3 for REF/SNV/INDEL).
35
+ input_height: Height of pileup image (coverage depth).
36
+ input_width: Width of pileup image (context window).
37
+ num_channels: Number of input channels (A, C, G, T, quality, strand).
38
+ hidden_channels: Number of channels in each conv layer.
39
+ fc_dims: Dimensions of fully connected layers.
40
+ dropout_rate: Dropout rate for regularization.
41
+ """
42
+
43
+ num_classes: int = DEFAULT_NUM_CLASSES
44
+ input_height: int = 100 # coverage depth
45
+ input_width: int = 221 # context window
46
+ num_channels: int = 6 # A, C, G, T, quality, strand
47
+ hidden_channels: tuple[int, ...] = (64, 128, 256)
48
+ fc_dims: tuple[int, ...] = (256, 128)
49
+ dropout_rate: float = DEFAULT_DROPOUT_RATE
50
+
51
+ def __post_init__(self) -> None:
52
+ """Set stochastic config based on dropout usage."""
53
+ if self.dropout_rate > 0:
54
+ object.__setattr__(self, "stochastic", True)
55
+ if self.stream_name is None:
56
+ object.__setattr__(self, "stream_name", "dropout")
57
+ super().__post_init__()
58
+
59
+
60
+ class CNNVariantClassifier(OperatorModule):
61
+ """CNN classifier for DeepVariant-style variant calling.
62
+
63
+ This operator implements a convolutional neural network that processes
64
+ pileup images to classify genomic positions as reference, SNV, or indel.
65
+
66
+ Architecture:
67
+ - Multiple Conv2D layers with batch normalization and ReLU
68
+ - Max pooling for spatial reduction
69
+ - Global average pooling before FC layers
70
+ - Fully connected layers with dropout
71
+ - Softmax output for class probabilities
72
+
73
+ Args:
74
+ config: CNNVariantClassifierConfig with model parameters.
75
+ rngs: Flax NNX random number generators.
76
+ name: Optional operator name.
77
+
78
+ Example:
79
+ ```python
80
+ config = CNNVariantClassifierConfig(num_classes=3)
81
+ classifier = CNNVariantClassifier(config, rngs=nnx.Rngs(42))
82
+ data = {"pileup_image": image_batch} # (B, H, W, C)
83
+ result, state, meta = classifier.apply(data, {}, None)
84
+ ```
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ config: CNNVariantClassifierConfig,
90
+ *,
91
+ rngs: nnx.Rngs | None = None,
92
+ name: str | None = None,
93
+ ):
94
+ """Initialize the CNN variant classifier.
95
+
96
+ Args:
97
+ config: Classifier configuration.
98
+ rngs: Random number generators for initialization.
99
+ name: Optional operator name.
100
+ """
101
+ super().__init__(config, rngs=rngs, name=name)
102
+
103
+ rngs = ensure_rngs(rngs)
104
+
105
+ self.num_classes = config.num_classes
106
+ self.dropout_rate = config.dropout_rate
107
+
108
+ # Build convolutional layers
109
+ conv_layers = []
110
+ in_channels = config.num_channels
111
+ for out_channels in config.hidden_channels:
112
+ conv_layers.append(
113
+ nnx.Conv(
114
+ in_features=in_channels,
115
+ out_features=out_channels,
116
+ kernel_size=(3, 3),
117
+ padding="SAME",
118
+ rngs=rngs,
119
+ )
120
+ )
121
+ in_channels = out_channels
122
+ self.conv_layers = nnx.List(conv_layers)
123
+
124
+ # Fully connected layers
125
+ fc_layers = []
126
+ # After global average pooling, input dim is last conv channel count
127
+ fc_in_dim = config.hidden_channels[-1] if config.hidden_channels else config.num_channels
128
+ for fc_dim in config.fc_dims:
129
+ fc_layers.append(nnx.Linear(fc_in_dim, fc_dim, rngs=rngs))
130
+ fc_in_dim = fc_dim
131
+ self.fc_layers = nnx.List(fc_layers)
132
+
133
+ # Dropout layer
134
+ if config.dropout_rate > 0:
135
+ self.dropout = nnx.Dropout(rate=config.dropout_rate, rngs=rngs)
136
+ else:
137
+ self.dropout = None
138
+
139
+ # Output layer
140
+ self.output_layer = nnx.Linear(fc_in_dim, config.num_classes, rngs=rngs)
141
+
142
+ def _classify_single(
143
+ self,
144
+ pileup_image: Float[Array, "height width channels"],
145
+ ) -> Float[Array, "num_classes"]:
146
+ """Classify a single pileup image.
147
+
148
+ Args:
149
+ pileup_image: Single pileup image (H, W, C).
150
+
151
+ Returns:
152
+ Logits for each variant class.
153
+ """
154
+ # Add batch dimension
155
+ x = pileup_image[None, ...] # (1, H, W, C)
156
+
157
+ # Convolutional layers with ReLU and pooling
158
+ for conv in self.conv_layers:
159
+ x = conv(x)
160
+ x = nnx.relu(x)
161
+ # Max pooling (2x2 with stride 2)
162
+ x = nnx.max_pool(x, window_shape=(2, 2), strides=(2, 2), padding="VALID")
163
+
164
+ # Global average pooling
165
+ x = jnp.mean(x, axis=(1, 2)) # (1, channels)
166
+
167
+ # Fully connected layers
168
+ for fc in self.fc_layers:
169
+ x = fc(x)
170
+ x = nnx.relu(x)
171
+ if self.dropout is not None:
172
+ x = self.dropout(x)
173
+
174
+ # Output
175
+ logits = self.output_layer(x)
176
+
177
+ return logits[0] # Remove batch dimension
178
+
179
+ def classify(
180
+ self,
181
+ pileup_image: Float[Array, "batch height width channels"],
182
+ ) -> Float[Array, "batch num_classes"]:
183
+ """Classify batch of pileup images.
184
+
185
+ Args:
186
+ pileup_image: Batch of pileup images (B, H, W, C).
187
+
188
+ Returns:
189
+ Logits for each variant class per image.
190
+ """
191
+ x = pileup_image
192
+
193
+ # Convolutional layers with ReLU and pooling
194
+ for conv in self.conv_layers:
195
+ x = conv(x)
196
+ x = nnx.relu(x)
197
+ # Max pooling (2x2 with stride 2)
198
+ x = nnx.max_pool(x, window_shape=(2, 2), strides=(2, 2), padding="VALID")
199
+
200
+ # Global average pooling
201
+ x = jnp.mean(x, axis=(1, 2)) # (batch, channels)
202
+
203
+ # Fully connected layers
204
+ for fc in self.fc_layers:
205
+ x = fc(x)
206
+ x = nnx.relu(x)
207
+ if self.dropout is not None:
208
+ x = self.dropout(x)
209
+
210
+ # Output
211
+ logits = self.output_layer(x)
212
+
213
+ return logits
214
+
215
+ def apply(
216
+ self,
217
+ data: PyTree,
218
+ state: PyTree,
219
+ metadata: dict[str, Any] | None,
220
+ random_params: Any = None,
221
+ stats: dict[str, Any] | None = None,
222
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
223
+ """Apply CNN classification to pileup images.
224
+
225
+ Args:
226
+ data: Dictionary containing:
227
+ - "pileup_image": Pileup images (batch, height, width, channels)
228
+ state: Element state (passed through unchanged)
229
+ metadata: Element metadata (passed through unchanged)
230
+ random_params: Not used
231
+ stats: Not used
232
+
233
+ Returns:
234
+ Tuple of (transformed_data, state, metadata):
235
+ - transformed_data contains:
236
+
237
+ - "pileup_image": Original input
238
+ - "logits": Raw classification scores
239
+ - "class_probs": Softmax probabilities
240
+ - state is passed through unchanged
241
+ - metadata is passed through unchanged
242
+ """
243
+ pileup_image = data["pileup_image"]
244
+
245
+ # Classify
246
+ logits = self.classify(pileup_image)
247
+
248
+ # Build output data
249
+ transformed_data = {
250
+ "pileup_image": pileup_image,
251
+ "logits": logits,
252
+ "class_probs": jax.nn.softmax(logits, axis=-1),
253
+ }
254
+
255
+ return transformed_data, state, metadata