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