diffbio 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffbio/__init__.py +39 -0
- diffbio/configs.py +75 -0
- diffbio/constants.py +204 -0
- diffbio/core/__init__.py +127 -0
- diffbio/core/base_operators.py +612 -0
- diffbio/core/data_types.py +260 -0
- diffbio/core/gnn_components.py +629 -0
- diffbio/core/graph_utils.py +149 -0
- diffbio/core/neural_components.py +270 -0
- diffbio/core/optimal_transport.py +133 -0
- diffbio/core/soft_ops/__init__.py +216 -0
- diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
- diffbio/core/soft_ops/_projections_simplex.py +240 -0
- diffbio/core/soft_ops/_projections_transport.py +508 -0
- diffbio/core/soft_ops/_sorting_network.py +204 -0
- diffbio/core/soft_ops/_types.py +15 -0
- diffbio/core/soft_ops/_utils.py +342 -0
- diffbio/core/soft_ops/autograd_safe.py +120 -0
- diffbio/core/soft_ops/comparison.py +235 -0
- diffbio/core/soft_ops/elementwise.py +309 -0
- diffbio/core/soft_ops/logical.py +146 -0
- diffbio/core/soft_ops/quantile.py +376 -0
- diffbio/core/soft_ops/selection.py +236 -0
- diffbio/core/soft_ops/sorting.py +926 -0
- diffbio/core/soft_ops/straight_through.py +261 -0
- diffbio/core/uncertainty.py +279 -0
- diffbio/evaluation/__init__.py +42 -0
- diffbio/evaluation/adapters.py +409 -0
- diffbio/evaluation/graders.py +223 -0
- diffbio/evaluation/problem.py +157 -0
- diffbio/evaluation/runner.py +277 -0
- diffbio/losses/__init__.py +59 -0
- diffbio/losses/alignment_losses.py +222 -0
- diffbio/losses/biological_regularization.py +288 -0
- diffbio/losses/metric_losses.py +139 -0
- diffbio/losses/singlecell_losses.py +387 -0
- diffbio/losses/statistical_losses.py +345 -0
- diffbio/operators/__init__.py +60 -0
- diffbio/operators/_count_vae.py +197 -0
- diffbio/operators/_loss_balancing.py +65 -0
- diffbio/operators/_masked_gene_transformer.py +118 -0
- diffbio/operators/_transformer_validation.py +50 -0
- diffbio/operators/alignment/__init__.py +51 -0
- diffbio/operators/alignment/profile_hmm.py +350 -0
- diffbio/operators/alignment/scoring.py +127 -0
- diffbio/operators/alignment/smith_waterman.py +261 -0
- diffbio/operators/alignment/soft_msa.py +419 -0
- diffbio/operators/assembly/__init__.py +27 -0
- diffbio/operators/assembly/gnn_assembly.py +252 -0
- diffbio/operators/assembly/metagenomic_binning.py +296 -0
- diffbio/operators/crispr/__init__.py +17 -0
- diffbio/operators/crispr/guide_scoring.py +269 -0
- diffbio/operators/drug_discovery/__init__.py +133 -0
- diffbio/operators/drug_discovery/_graph_utils.py +142 -0
- diffbio/operators/drug_discovery/admet_predictor.py +285 -0
- diffbio/operators/drug_discovery/attentive_fp.py +411 -0
- diffbio/operators/drug_discovery/dti.py +261 -0
- diffbio/operators/drug_discovery/fingerprint.py +490 -0
- diffbio/operators/drug_discovery/maccs_keys.py +267 -0
- diffbio/operators/drug_discovery/message_passing.py +200 -0
- diffbio/operators/drug_discovery/primitives.py +242 -0
- diffbio/operators/drug_discovery/property_predictor.py +163 -0
- diffbio/operators/drug_discovery/similarity.py +193 -0
- diffbio/operators/epigenomics/__init__.py +35 -0
- diffbio/operators/epigenomics/chromatin_state.py +491 -0
- diffbio/operators/epigenomics/contextual.py +288 -0
- diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
- diffbio/operators/epigenomics/peak_calling.py +555 -0
- diffbio/operators/foundation_models/__init__.py +119 -0
- diffbio/operators/foundation_models/adapters.py +114 -0
- diffbio/operators/foundation_models/contracts.py +245 -0
- diffbio/operators/foundation_models/embedding_probe.py +83 -0
- diffbio/operators/foundation_models/experimental.py +128 -0
- diffbio/operators/foundation_models/foundation_model.py +332 -0
- diffbio/operators/foundation_models/frozen.py +59 -0
- diffbio/operators/foundation_models/precomputed.py +270 -0
- diffbio/operators/foundation_models/transformer_encoder.py +564 -0
- diffbio/operators/mapping/__init__.py +17 -0
- diffbio/operators/mapping/neural_mapper.py +493 -0
- diffbio/operators/metabolomics/__init__.py +39 -0
- diffbio/operators/metabolomics/spectral_similarity.py +315 -0
- diffbio/operators/molecular_dynamics/__init__.py +51 -0
- diffbio/operators/molecular_dynamics/force_field.py +265 -0
- diffbio/operators/molecular_dynamics/integrator.py +304 -0
- diffbio/operators/molecular_dynamics/primitives.py +115 -0
- diffbio/operators/multiomics/__init__.py +38 -0
- diffbio/operators/multiomics/hic_contact.py +377 -0
- diffbio/operators/multiomics/multiomics_vae.py +325 -0
- diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
- diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
- diffbio/operators/normalization/__init__.py +42 -0
- diffbio/operators/normalization/embedding.py +222 -0
- diffbio/operators/normalization/phate.py +400 -0
- diffbio/operators/normalization/umap.py +261 -0
- diffbio/operators/normalization/vae_normalizer.py +258 -0
- diffbio/operators/population/__init__.py +17 -0
- diffbio/operators/population/ancestry_estimation.py +274 -0
- diffbio/operators/preprocessing/__init__.py +76 -0
- diffbio/operators/preprocessing/adapter_removal.py +311 -0
- diffbio/operators/preprocessing/duplicate_filter.py +317 -0
- diffbio/operators/preprocessing/error_correction.py +287 -0
- diffbio/operators/protein/__init__.py +31 -0
- diffbio/operators/protein/secondary_structure.py +509 -0
- diffbio/operators/quality_filter.py +128 -0
- diffbio/operators/rna_structure/__init__.py +35 -0
- diffbio/operators/rna_structure/rna_folding.py +509 -0
- diffbio/operators/rnaseq/__init__.py +23 -0
- diffbio/operators/rnaseq/motif_discovery.py +251 -0
- diffbio/operators/rnaseq/splicing_psi.py +216 -0
- diffbio/operators/singlecell/__init__.py +193 -0
- diffbio/operators/singlecell/ambient_removal.py +333 -0
- diffbio/operators/singlecell/archetypes.py +191 -0
- diffbio/operators/singlecell/batch_correction.py +288 -0
- diffbio/operators/singlecell/cell_annotation.py +519 -0
- diffbio/operators/singlecell/communication.py +704 -0
- diffbio/operators/singlecell/differential_distribution.py +243 -0
- diffbio/operators/singlecell/doublet_detection.py +657 -0
- diffbio/operators/singlecell/downsampling.py +166 -0
- diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
- diffbio/operators/singlecell/grn_inference.py +336 -0
- diffbio/operators/singlecell/imputation.py +429 -0
- diffbio/operators/singlecell/knockdown_filter.py +176 -0
- diffbio/operators/singlecell/ot_trajectory.py +277 -0
- diffbio/operators/singlecell/simulation.py +444 -0
- diffbio/operators/singlecell/sindy_grn.py +247 -0
- diffbio/operators/singlecell/soft_clustering.py +211 -0
- diffbio/operators/singlecell/spatial_domains.py +677 -0
- diffbio/operators/singlecell/switch_de.py +184 -0
- diffbio/operators/singlecell/trajectory.py +447 -0
- diffbio/operators/singlecell/velocity.py +361 -0
- diffbio/operators/statistical/__init__.py +35 -0
- diffbio/operators/statistical/em_quantification.py +260 -0
- diffbio/operators/statistical/hmm.py +234 -0
- diffbio/operators/statistical/nb_glm.py +272 -0
- diffbio/operators/variant/__init__.py +64 -0
- diffbio/operators/variant/classifier.py +333 -0
- diffbio/operators/variant/cnn_classifier.py +255 -0
- diffbio/operators/variant/cnv_segmentation.py +678 -0
- diffbio/operators/variant/deepvariant_pileup.py +426 -0
- diffbio/operators/variant/pileup.py +240 -0
- diffbio/operators/variant/quality_recalibration.py +274 -0
- diffbio/pipelines/__init__.py +65 -0
- diffbio/pipelines/differential_expression.py +279 -0
- diffbio/pipelines/enhanced_variant_calling.py +326 -0
- diffbio/pipelines/perturbation.py +407 -0
- diffbio/pipelines/preprocessing.py +267 -0
- diffbio/pipelines/single_cell.py +366 -0
- diffbio/pipelines/variant_calling.py +490 -0
- diffbio/samplers/__init__.py +9 -0
- diffbio/samplers/perturbation_sampler.py +142 -0
- diffbio/sequences/__init__.py +34 -0
- diffbio/sequences/dna.py +239 -0
- diffbio/sources/__init__.py +149 -0
- diffbio/sources/_anndata_shared.py +89 -0
- diffbio/sources/_batch_iteration.py +37 -0
- diffbio/sources/_benchmark_source.py +152 -0
- diffbio/sources/_indexed_batch_source.py +38 -0
- diffbio/sources/_utils.py +45 -0
- diffbio/sources/anndata_interop.py +387 -0
- diffbio/sources/anndata_source.py +361 -0
- diffbio/sources/archive_ii.py +174 -0
- diffbio/sources/balifam.py +207 -0
- diffbio/sources/bam.py +265 -0
- diffbio/sources/bengrn_ground_truth.py +306 -0
- diffbio/sources/contextual_epigenomics.py +242 -0
- diffbio/sources/dti.py +359 -0
- diffbio/sources/embeddings.py +203 -0
- diffbio/sources/encode_peaks.py +223 -0
- diffbio/sources/fasta.py +226 -0
- diffbio/sources/immune_human.py +172 -0
- diffbio/sources/indexed_embeddings.py +128 -0
- diffbio/sources/indexed_view.py +191 -0
- diffbio/sources/molnet.py +493 -0
- diffbio/sources/multiomics.py +279 -0
- diffbio/sources/pancreas.py +108 -0
- diffbio/sources/perturbation/__init__.py +69 -0
- diffbio/sources/perturbation/_types.py +51 -0
- diffbio/sources/perturbation/_utils.py +125 -0
- diffbio/sources/perturbation/concat_source.py +115 -0
- diffbio/sources/perturbation/control_mapping.py +215 -0
- diffbio/sources/perturbation/experiment_config.py +261 -0
- diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
- diffbio/sources/perturbation/output_space.py +52 -0
- diffbio/sources/perturbation/perturbation_source.py +513 -0
- diffbio/sources/seqfish.py +145 -0
- diffbio/sources/sequence_foundation.py +68 -0
- diffbio/sources/singlecell_foundation.py +68 -0
- diffbio/splitters/__init__.py +63 -0
- diffbio/splitters/base.py +251 -0
- diffbio/splitters/molecular.py +330 -0
- diffbio/splitters/perturbation.py +199 -0
- diffbio/splitters/random.py +217 -0
- diffbio/splitters/sequence.py +201 -0
- diffbio/utils/__init__.py +55 -0
- diffbio/utils/dependency_runtime.py +115 -0
- diffbio/utils/nn_utils.py +157 -0
- diffbio/utils/quality.py +45 -0
- diffbio/utils/training.py +585 -0
- diffbio-0.1.0.dist-info/METADATA +480 -0
- diffbio-0.1.0.dist-info/RECORD +202 -0
- diffbio-0.1.0.dist-info/WHEEL +4 -0
- diffbio-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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
|