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,426 @@
|
|
|
1
|
+
"""DeepVariant-style pileup image generation for variant calling."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import jax
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
from flax import nnx
|
|
9
|
+
from jaxtyping import Array, Float, Int, PyTree
|
|
10
|
+
|
|
11
|
+
from diffbio.configs import TemperatureConfig
|
|
12
|
+
from diffbio.core.base_operators import TemperatureOperator
|
|
13
|
+
|
|
14
|
+
_DEFAULT_DEEPVARIANT_CHANNELS = (
|
|
15
|
+
"base",
|
|
16
|
+
"base_quality",
|
|
17
|
+
"mapping_quality",
|
|
18
|
+
"strand",
|
|
19
|
+
"supports_variant",
|
|
20
|
+
"differs_from_ref",
|
|
21
|
+
)
|
|
22
|
+
_DEEPVARIANT_CHANNEL_WIDTHS = {
|
|
23
|
+
"base": 4,
|
|
24
|
+
"base_quality": 1,
|
|
25
|
+
"mapping_quality": 1,
|
|
26
|
+
"strand": 1,
|
|
27
|
+
"supports_variant": 1,
|
|
28
|
+
"differs_from_ref": 1,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class DeepVariantPileupConfig(TemperatureConfig):
|
|
34
|
+
"""Configuration for DeepVariant-style pileup generation.
|
|
35
|
+
|
|
36
|
+
Inherits from TemperatureConfig to get temperature parameter for
|
|
37
|
+
soft/differentiable operations.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
window_size: Width of pileup image in base pairs (default: 221)
|
|
41
|
+
max_reads: Height of pileup image / max reads to include (default: 100)
|
|
42
|
+
channels: Ordered channel set to emit in the pileup image.
|
|
43
|
+
quality_max: Maximum quality score for normalization (default: 40)
|
|
44
|
+
mapq_max: Maximum mapping quality for normalization (default: 60)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
window_size: int = 221
|
|
48
|
+
max_reads: int = 100
|
|
49
|
+
channels: tuple[str, ...] = _DEFAULT_DEEPVARIANT_CHANNELS
|
|
50
|
+
quality_max: float = 40.0
|
|
51
|
+
mapq_max: float = 60.0
|
|
52
|
+
|
|
53
|
+
def __post_init__(self) -> None:
|
|
54
|
+
"""Validate the supported DeepVariant pileup configuration surface."""
|
|
55
|
+
super().__post_init__()
|
|
56
|
+
|
|
57
|
+
if self.window_size <= 0:
|
|
58
|
+
raise ValueError(f"window_size must be positive, got {self.window_size}")
|
|
59
|
+
if self.max_reads <= 0:
|
|
60
|
+
raise ValueError(f"max_reads must be positive, got {self.max_reads}")
|
|
61
|
+
if self.quality_max <= 0.0:
|
|
62
|
+
raise ValueError(f"quality_max must be positive, got {self.quality_max}")
|
|
63
|
+
if self.mapq_max <= 0.0:
|
|
64
|
+
raise ValueError(f"mapq_max must be positive, got {self.mapq_max}")
|
|
65
|
+
if not self.channels:
|
|
66
|
+
raise ValueError("channels must contain at least one DeepVariant channel")
|
|
67
|
+
|
|
68
|
+
invalid_channels = tuple(
|
|
69
|
+
channel for channel in self.channels if channel not in _DEEPVARIANT_CHANNEL_WIDTHS
|
|
70
|
+
)
|
|
71
|
+
if invalid_channels:
|
|
72
|
+
invalid = ", ".join(invalid_channels)
|
|
73
|
+
raise ValueError(f"channels contains unsupported values: {invalid}")
|
|
74
|
+
|
|
75
|
+
if len(set(self.channels)) != len(self.channels):
|
|
76
|
+
raise ValueError("channels must not contain duplicates")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DeepVariantStylePileup(TemperatureOperator):
|
|
80
|
+
"""DeepVariant-style multi-channel pileup image generator.
|
|
81
|
+
|
|
82
|
+
Generates pileup images compatible with DeepVariant's CNN architecture
|
|
83
|
+
while maintaining full differentiability for end-to-end training.
|
|
84
|
+
|
|
85
|
+
The pileup image has shape (max_reads, window_size, num_channels) where
|
|
86
|
+
each read occupies a row and each column represents a base position.
|
|
87
|
+
|
|
88
|
+
Inherits from TemperatureOperator to get:
|
|
89
|
+
|
|
90
|
+
- _temperature property for temperature-controlled smoothing
|
|
91
|
+
- soft_max() for logsumexp-based smooth maximum
|
|
92
|
+
- soft_argmax() for soft position selection
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
```python
|
|
96
|
+
config = DeepVariantPileupConfig(window_size=101, max_reads=50)
|
|
97
|
+
pileup = DeepVariantStylePileup(config)
|
|
98
|
+
data = {
|
|
99
|
+
"reads": reads, # (num_reads, read_length, 4)
|
|
100
|
+
"reference": reference, # (window_size, 4)
|
|
101
|
+
"base_qualities": qualities, # (num_reads, read_length)
|
|
102
|
+
"mapping_qualities": mapq, # (num_reads,)
|
|
103
|
+
"strands": strands, # (num_reads,)
|
|
104
|
+
"positions": positions, # (num_reads,)
|
|
105
|
+
}
|
|
106
|
+
result, _, _ = pileup.apply(data, {}, None)
|
|
107
|
+
pileup_image = result["pileup_image"] # (50, 101, num_channels)
|
|
108
|
+
```
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
config: DeepVariantPileupConfig,
|
|
114
|
+
*,
|
|
115
|
+
rngs: nnx.Rngs | None = None,
|
|
116
|
+
name: str | None = None,
|
|
117
|
+
):
|
|
118
|
+
"""Initialize DeepVariantStylePileup.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
config: Pileup configuration
|
|
122
|
+
rngs: Random number generators (optional)
|
|
123
|
+
name: Optional operator name
|
|
124
|
+
"""
|
|
125
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
126
|
+
|
|
127
|
+
# Calculate number of output channels
|
|
128
|
+
self._num_channels = sum(
|
|
129
|
+
_DEEPVARIANT_CHANNEL_WIDTHS[channel] for channel in config.channels
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def num_channels(self) -> int:
|
|
134
|
+
"""Return the number of output channels."""
|
|
135
|
+
return self._num_channels
|
|
136
|
+
|
|
137
|
+
def _scatter_reads_to_image(
|
|
138
|
+
self,
|
|
139
|
+
values: Float[Array, "num_reads read_length ..."],
|
|
140
|
+
positions: Int[Array, "num_reads"],
|
|
141
|
+
read_length: int,
|
|
142
|
+
) -> Float[Array, "max_reads window_size ..."]:
|
|
143
|
+
"""Scatter per-read, per-position values into a 2D (or 3D) pileup image.
|
|
144
|
+
|
|
145
|
+
This is the generic helper that handles the common scan+fori_loop pattern
|
|
146
|
+
used by all channel computation methods. It processes reads one at a time
|
|
147
|
+
via lax.scan, and for each read scatters its values into the correct
|
|
148
|
+
window positions via lax.fori_loop.
|
|
149
|
+
|
|
150
|
+
Supports both scalar per-position values (image shape: max_reads x window_size)
|
|
151
|
+
and vector per-position values (image shape: max_reads x window_size x D).
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
values: Pre-computed values to scatter. Shape is either
|
|
155
|
+
(num_reads, read_length) for scalar channels or
|
|
156
|
+
(num_reads, read_length, D) for vector channels (e.g., one-hot bases).
|
|
157
|
+
positions: Starting position of each read in the window (num_reads,).
|
|
158
|
+
read_length: Length of each read.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Image with scattered values. Shape is (max_reads, window_size) for
|
|
162
|
+
scalar values or (max_reads, window_size, D) for vector values.
|
|
163
|
+
"""
|
|
164
|
+
config = self.config
|
|
165
|
+
is_vector = values.ndim == 3
|
|
166
|
+
if is_vector:
|
|
167
|
+
value_dim = values.shape[2]
|
|
168
|
+
image = jnp.zeros((config.max_reads, config.window_size, value_dim), dtype=jnp.float32)
|
|
169
|
+
else:
|
|
170
|
+
image = jnp.zeros((config.max_reads, config.window_size), dtype=jnp.float32)
|
|
171
|
+
|
|
172
|
+
def scatter_read(carry, read_data):
|
|
173
|
+
img, read_idx = carry
|
|
174
|
+
read_vals, pos = read_data
|
|
175
|
+
|
|
176
|
+
valid = read_idx < config.max_reads
|
|
177
|
+
read_positions = pos + jnp.arange(read_length)
|
|
178
|
+
in_bounds = (read_positions >= 0) & (read_positions < config.window_size)
|
|
179
|
+
|
|
180
|
+
def update_position(i, current_img):
|
|
181
|
+
pos_idx = jnp.clip(read_positions[i], 0, config.window_size - 1)
|
|
182
|
+
is_valid = valid & in_bounds[i]
|
|
183
|
+
if is_vector:
|
|
184
|
+
return jnp.where(
|
|
185
|
+
is_valid,
|
|
186
|
+
current_img.at[read_idx, pos_idx, :].set(read_vals[i]),
|
|
187
|
+
current_img,
|
|
188
|
+
)
|
|
189
|
+
return jnp.where(
|
|
190
|
+
is_valid,
|
|
191
|
+
current_img.at[read_idx, pos_idx].set(read_vals[i]),
|
|
192
|
+
current_img,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
new_img = jax.lax.fori_loop(0, read_length, update_position, img)
|
|
196
|
+
return (new_img, read_idx + 1), None
|
|
197
|
+
|
|
198
|
+
(image, _), _ = jax.lax.scan(
|
|
199
|
+
scatter_read,
|
|
200
|
+
(image, 0),
|
|
201
|
+
(values, positions),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return image
|
|
205
|
+
|
|
206
|
+
def compute_pileup_image(
|
|
207
|
+
self,
|
|
208
|
+
reads: Float[Array, "num_reads read_length 4"],
|
|
209
|
+
reference: Float[Array, "window_size 4"],
|
|
210
|
+
base_qualities: Float[Array, "num_reads read_length"],
|
|
211
|
+
mapping_qualities: Float[Array, "num_reads"],
|
|
212
|
+
strands: Float[Array, "num_reads"],
|
|
213
|
+
positions: Int[Array, "num_reads"],
|
|
214
|
+
) -> Float[Array, "max_reads window_size num_channels"]:
|
|
215
|
+
"""Compute DeepVariant-style pileup image.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
reads: One-hot encoded reads (num_reads, read_length, 4)
|
|
219
|
+
reference: One-hot encoded reference (window_size, 4)
|
|
220
|
+
base_qualities: Phred quality scores (num_reads, read_length)
|
|
221
|
+
mapping_qualities: Mapping quality scores (num_reads,)
|
|
222
|
+
strands: Strand orientation, 0=forward, 1=reverse (num_reads,)
|
|
223
|
+
positions: Starting position of each read in window (num_reads,)
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Pileup image of shape (max_reads, window_size, num_channels)
|
|
227
|
+
"""
|
|
228
|
+
config = self.config
|
|
229
|
+
read_length = reads.shape[1]
|
|
230
|
+
|
|
231
|
+
# Initialize output image with zeros
|
|
232
|
+
pileup_image = jnp.zeros(
|
|
233
|
+
(config.max_reads, config.window_size, self._num_channels),
|
|
234
|
+
dtype=jnp.float32,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Build the pileup image channel by channel
|
|
238
|
+
channel_idx = 0
|
|
239
|
+
|
|
240
|
+
for channel in config.channels:
|
|
241
|
+
if channel == "base":
|
|
242
|
+
base_image = self._compute_base_channels(reads, positions, read_length)
|
|
243
|
+
pileup_image = pileup_image.at[:, :, channel_idx : channel_idx + 4].set(base_image)
|
|
244
|
+
channel_idx += 4
|
|
245
|
+
elif channel == "base_quality":
|
|
246
|
+
quality_image = self._compute_quality_channel(
|
|
247
|
+
base_qualities, positions, read_length
|
|
248
|
+
)
|
|
249
|
+
pileup_image = pileup_image.at[:, :, channel_idx].set(quality_image)
|
|
250
|
+
channel_idx += 1
|
|
251
|
+
elif channel == "mapping_quality":
|
|
252
|
+
mapq_image = self._compute_mapq_channel(mapping_qualities, positions, read_length)
|
|
253
|
+
pileup_image = pileup_image.at[:, :, channel_idx].set(mapq_image)
|
|
254
|
+
channel_idx += 1
|
|
255
|
+
elif channel == "strand":
|
|
256
|
+
strand_image = self._compute_strand_channel(strands, positions, read_length)
|
|
257
|
+
pileup_image = pileup_image.at[:, :, channel_idx].set(strand_image)
|
|
258
|
+
channel_idx += 1
|
|
259
|
+
elif channel == "supports_variant":
|
|
260
|
+
variant_image = self._compute_variant_support_channel(
|
|
261
|
+
reads, reference, positions, read_length
|
|
262
|
+
)
|
|
263
|
+
pileup_image = pileup_image.at[:, :, channel_idx].set(variant_image)
|
|
264
|
+
channel_idx += 1
|
|
265
|
+
else:
|
|
266
|
+
diff_image = self._compute_diff_from_ref_channel(
|
|
267
|
+
reads, reference, positions, read_length
|
|
268
|
+
)
|
|
269
|
+
pileup_image = pileup_image.at[:, :, channel_idx].set(diff_image)
|
|
270
|
+
channel_idx += 1
|
|
271
|
+
|
|
272
|
+
return pileup_image
|
|
273
|
+
|
|
274
|
+
def _compute_base_channels(
|
|
275
|
+
self,
|
|
276
|
+
reads: Float[Array, "num_reads read_length 4"],
|
|
277
|
+
positions: Int[Array, "num_reads"],
|
|
278
|
+
read_length: int,
|
|
279
|
+
) -> Float[Array, "max_reads window_size 4"]:
|
|
280
|
+
"""Compute base identity channels (one-hot A/C/G/T).
|
|
281
|
+
|
|
282
|
+
Places each read's bases at the correct position in the image.
|
|
283
|
+
Values are the one-hot base vectors from the reads directly.
|
|
284
|
+
"""
|
|
285
|
+
return self._scatter_reads_to_image(reads, positions, read_length)
|
|
286
|
+
|
|
287
|
+
def _compute_quality_channel(
|
|
288
|
+
self,
|
|
289
|
+
base_qualities: Float[Array, "num_reads read_length"],
|
|
290
|
+
positions: Int[Array, "num_reads"],
|
|
291
|
+
read_length: int,
|
|
292
|
+
) -> Float[Array, "max_reads window_size"]:
|
|
293
|
+
"""Compute base quality channel normalized to [0, 1]."""
|
|
294
|
+
normalized_qual = jnp.clip(base_qualities / self.config.quality_max, 0.0, 1.0)
|
|
295
|
+
return self._scatter_reads_to_image(normalized_qual, positions, read_length)
|
|
296
|
+
|
|
297
|
+
def _compute_mapq_channel(
|
|
298
|
+
self,
|
|
299
|
+
mapping_qualities: Float[Array, "num_reads"],
|
|
300
|
+
positions: Int[Array, "num_reads"],
|
|
301
|
+
read_length: int,
|
|
302
|
+
) -> Float[Array, "max_reads window_size"]:
|
|
303
|
+
"""Compute mapping quality channel normalized to [0, 1].
|
|
304
|
+
|
|
305
|
+
MAPQ is constant across a read, so we broadcast to all positions.
|
|
306
|
+
"""
|
|
307
|
+
normalized_mapq = jnp.clip(mapping_qualities / self.config.mapq_max, 0.0, 1.0)
|
|
308
|
+
# Broadcast constant MAPQ to all positions: (num_reads,) -> (num_reads, read_length)
|
|
309
|
+
mapq_per_position = jnp.broadcast_to(
|
|
310
|
+
normalized_mapq[:, None], (normalized_mapq.shape[0], read_length)
|
|
311
|
+
)
|
|
312
|
+
return self._scatter_reads_to_image(mapq_per_position, positions, read_length)
|
|
313
|
+
|
|
314
|
+
def _compute_strand_channel(
|
|
315
|
+
self,
|
|
316
|
+
strands: Float[Array, "num_reads"],
|
|
317
|
+
positions: Int[Array, "num_reads"],
|
|
318
|
+
read_length: int,
|
|
319
|
+
) -> Float[Array, "max_reads window_size"]:
|
|
320
|
+
"""Compute strand channel (0=forward, 1=reverse).
|
|
321
|
+
|
|
322
|
+
Strand is constant across a read, so we broadcast to all positions.
|
|
323
|
+
"""
|
|
324
|
+
# Broadcast constant strand to all positions: (num_reads,) -> (num_reads, read_length)
|
|
325
|
+
strand_per_position = jnp.broadcast_to(strands[:, None], (strands.shape[0], read_length))
|
|
326
|
+
return self._scatter_reads_to_image(strand_per_position, positions, read_length)
|
|
327
|
+
|
|
328
|
+
def _compute_variant_support_channel(
|
|
329
|
+
self,
|
|
330
|
+
reads: Float[Array, "num_reads read_length 4"],
|
|
331
|
+
reference: Float[Array, "window_size 4"],
|
|
332
|
+
positions: Int[Array, "num_reads"],
|
|
333
|
+
read_length: int,
|
|
334
|
+
) -> Float[Array, "max_reads window_size"]:
|
|
335
|
+
"""Compute variant support channel.
|
|
336
|
+
|
|
337
|
+
This is a soft indicator of whether a read base differs from reference,
|
|
338
|
+
which can indicate support for a variant allele.
|
|
339
|
+
|
|
340
|
+
Uses soft comparison for differentiability: mismatch = 1 - dot(read, ref).
|
|
341
|
+
"""
|
|
342
|
+
# Pre-compute mismatch values for all reads and positions
|
|
343
|
+
# ref_positions[r, i] = positions[r] + i
|
|
344
|
+
ref_positions = positions[:, None] + jnp.arange(read_length)[None, :]
|
|
345
|
+
clipped_ref_positions = jnp.clip(ref_positions, 0, self.config.window_size - 1)
|
|
346
|
+
|
|
347
|
+
# Look up reference bases at each position: (num_reads, read_length, 4)
|
|
348
|
+
ref_bases = reference[clipped_ref_positions]
|
|
349
|
+
|
|
350
|
+
# Soft mismatch: 1 - dot product of one-hot vectors
|
|
351
|
+
match_scores = jnp.sum(reads * ref_bases, axis=-1) # (num_reads, read_length)
|
|
352
|
+
mismatch_values = 1.0 - match_scores
|
|
353
|
+
|
|
354
|
+
return self._scatter_reads_to_image(mismatch_values, positions, read_length)
|
|
355
|
+
|
|
356
|
+
def _compute_diff_from_ref_channel(
|
|
357
|
+
self,
|
|
358
|
+
reads: Float[Array, "num_reads read_length 4"],
|
|
359
|
+
reference: Float[Array, "window_size 4"],
|
|
360
|
+
positions: Int[Array, "num_reads"],
|
|
361
|
+
read_length: int,
|
|
362
|
+
) -> Float[Array, "max_reads window_size"]:
|
|
363
|
+
"""Compute 'differs from reference' channel.
|
|
364
|
+
|
|
365
|
+
Similar to variant support but provides a direct mismatch signal.
|
|
366
|
+
In practice, this is equivalent to variant_support for standard pileups.
|
|
367
|
+
"""
|
|
368
|
+
# For standard pileups, this is the same as variant support
|
|
369
|
+
# DeepVariant distinguishes them for multi-allelic calling
|
|
370
|
+
return self._compute_variant_support_channel(reads, reference, positions, read_length)
|
|
371
|
+
|
|
372
|
+
def apply(
|
|
373
|
+
self,
|
|
374
|
+
data: PyTree,
|
|
375
|
+
state: PyTree,
|
|
376
|
+
metadata: dict[str, Any] | None,
|
|
377
|
+
random_params: Any = None, # noqa: ARG002
|
|
378
|
+
stats: dict[str, Any] | None = None, # noqa: ARG002
|
|
379
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
380
|
+
"""Apply DeepVariant-style pileup generation.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
data: Dictionary containing:
|
|
384
|
+
- "reads": One-hot encoded reads (num_reads, read_length, 4)
|
|
385
|
+
- "reference": One-hot encoded reference (window_size, 4)
|
|
386
|
+
- "base_qualities": Phred quality scores (num_reads, read_length)
|
|
387
|
+
- "mapping_qualities": Mapping quality scores (num_reads,)
|
|
388
|
+
- "strands": Strand orientation (num_reads,)
|
|
389
|
+
- "positions": Read start positions in window (num_reads,)
|
|
390
|
+
state: Element state (passed through unchanged)
|
|
391
|
+
metadata: Element metadata (passed through unchanged)
|
|
392
|
+
random_params: Not used (deterministic operator)
|
|
393
|
+
stats: Not used
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Tuple of (transformed_data, state, metadata):
|
|
397
|
+
- transformed_data contains input data plus pileup_image
|
|
398
|
+
- state is passed through unchanged
|
|
399
|
+
- metadata is passed through unchanged
|
|
400
|
+
"""
|
|
401
|
+
del random_params, stats # Unused parameters
|
|
402
|
+
|
|
403
|
+
reads = data["reads"]
|
|
404
|
+
reference = data["reference"]
|
|
405
|
+
base_qualities = data["base_qualities"]
|
|
406
|
+
mapping_qualities = data["mapping_qualities"]
|
|
407
|
+
strands = data["strands"]
|
|
408
|
+
positions = data["positions"]
|
|
409
|
+
|
|
410
|
+
# Compute pileup image
|
|
411
|
+
pileup_image = self.compute_pileup_image(
|
|
412
|
+
reads=reads,
|
|
413
|
+
reference=reference,
|
|
414
|
+
base_qualities=base_qualities,
|
|
415
|
+
mapping_qualities=mapping_qualities,
|
|
416
|
+
strands=strands,
|
|
417
|
+
positions=positions,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Build output data - preserve input keys for Datarax compatibility
|
|
421
|
+
transformed_data = {
|
|
422
|
+
**data,
|
|
423
|
+
"pileup_image": pileup_image,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return transformed_data, state, metadata
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Differentiable pileup generation for variant calling."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import jax
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
from flax import nnx
|
|
9
|
+
from jaxtyping import Array, Float, Int, PyTree
|
|
10
|
+
|
|
11
|
+
from diffbio.configs import TemperatureConfig
|
|
12
|
+
from diffbio.constants import EPSILON
|
|
13
|
+
from diffbio.core.base_operators import TemperatureOperator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class PileupConfig(TemperatureConfig):
|
|
18
|
+
"""Configuration for differentiable pileup.
|
|
19
|
+
|
|
20
|
+
Inherits from TemperatureConfig to get temperature and learnable_temperature fields.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
use_quality_weights: Whether to weight bases by quality scores.
|
|
24
|
+
reference_length: Length of reference sequence (required for batch processing).
|
|
25
|
+
All reads in a batch must align to the same reference length.
|
|
26
|
+
return_coverage: Whether to return coverage channel in output.
|
|
27
|
+
return_quality: Whether to return mean quality channel in output.
|
|
28
|
+
apply_softmax: Whether to apply softmax to final pileup (set False to preserve
|
|
29
|
+
raw weighted sums, which is better for variant detection).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
use_quality_weights: bool = True
|
|
33
|
+
reference_length: int = 100
|
|
34
|
+
return_coverage: bool = False
|
|
35
|
+
return_quality: bool = False
|
|
36
|
+
apply_softmax: bool = True
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
"""Validate the supported pileup configuration surface."""
|
|
40
|
+
super().__post_init__()
|
|
41
|
+
|
|
42
|
+
if self.reference_length <= 0:
|
|
43
|
+
raise ValueError(f"reference_length must be positive, got {self.reference_length}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DifferentiablePileup(TemperatureOperator):
|
|
47
|
+
"""Differentiable pileup generator.
|
|
48
|
+
|
|
49
|
+
Aggregates aligned reads into a position-wise nucleotide distribution
|
|
50
|
+
that can be used for variant calling. Unlike traditional pileup which
|
|
51
|
+
simply counts bases, this implementation uses soft weighting that
|
|
52
|
+
allows gradients to flow through.
|
|
53
|
+
|
|
54
|
+
Inherits from TemperatureOperator to get:
|
|
55
|
+
|
|
56
|
+
- _temperature property for temperature-controlled smoothing
|
|
57
|
+
- soft_max() for logsumexp-based smooth maximum
|
|
58
|
+
- soft_argmax() for soft position selection
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
config: Pileup configuration.
|
|
62
|
+
rngs: Flax NNX random number generators.
|
|
63
|
+
name: Optional operator name.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
config: PileupConfig,
|
|
69
|
+
*,
|
|
70
|
+
rngs: nnx.Rngs | None = None,
|
|
71
|
+
name: str | None = None,
|
|
72
|
+
):
|
|
73
|
+
"""Initialize differentiable pileup.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
config: Pileup configuration.
|
|
77
|
+
rngs: Random number generators (optional).
|
|
78
|
+
name: Optional operator name.
|
|
79
|
+
"""
|
|
80
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
81
|
+
# Temperature is now managed by TemperatureOperator via self._temperature
|
|
82
|
+
|
|
83
|
+
def compute_pileup(
|
|
84
|
+
self,
|
|
85
|
+
reads: Float[Array, "num_reads read_length 4"],
|
|
86
|
+
positions: Int[Array, "num_reads"],
|
|
87
|
+
quality: Float[Array, "num_reads read_length"],
|
|
88
|
+
reference_length: int,
|
|
89
|
+
) -> dict[str, Float[Array, "..."]]:
|
|
90
|
+
"""Generate pileup from aligned reads.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
reads: One-hot encoded reads (num_reads, read_length, 4).
|
|
94
|
+
positions: Starting position of each read (num_reads,).
|
|
95
|
+
quality: Quality scores for each base (num_reads, read_length).
|
|
96
|
+
reference_length: Length of reference sequence.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Dictionary containing:
|
|
100
|
+
- pileup: (reference_length, 4) nucleotide distributions
|
|
101
|
+
- coverage: (reference_length, 1) read depth at each position (if return_coverage)
|
|
102
|
+
- mean_quality: (reference_length, 1) mean quality at each position (if return_quality)
|
|
103
|
+
"""
|
|
104
|
+
_, read_length, _ = reads.shape
|
|
105
|
+
|
|
106
|
+
# Convert quality scores to weights
|
|
107
|
+
if self.config.use_quality_weights:
|
|
108
|
+
# Phred to probability: p_error = 10^(-Q/10)
|
|
109
|
+
# Weight = 1 - p_error
|
|
110
|
+
p_error = jnp.power(10.0, -quality / 10.0)
|
|
111
|
+
weights = 1.0 - p_error
|
|
112
|
+
else:
|
|
113
|
+
weights = jnp.ones_like(quality)
|
|
114
|
+
|
|
115
|
+
# Create position indices for all bases in all reads
|
|
116
|
+
# For each read i at position p[i], base j maps to reference position p[i] + j
|
|
117
|
+
read_offsets = jnp.arange(read_length) # [0, 1, ..., read_length-1]
|
|
118
|
+
# Broadcast to get absolute positions: (num_reads, read_length)
|
|
119
|
+
absolute_positions = positions[:, None] + read_offsets[None, :]
|
|
120
|
+
|
|
121
|
+
# Flatten everything for scatter operation
|
|
122
|
+
flat_positions = absolute_positions.reshape(-1) # (num_reads * read_length,)
|
|
123
|
+
flat_reads = reads.reshape(-1, 4) # (num_reads * read_length, 4)
|
|
124
|
+
flat_weights = weights.reshape(-1, 1) # (num_reads * read_length, 1)
|
|
125
|
+
flat_quality = quality.reshape(-1, 1) # (num_reads * read_length, 1)
|
|
126
|
+
|
|
127
|
+
# Mask out-of-bounds positions
|
|
128
|
+
in_bounds = (flat_positions >= 0) & (flat_positions < reference_length)
|
|
129
|
+
in_bounds_mask = in_bounds[:, None].astype(jnp.float32)
|
|
130
|
+
flat_weights_masked = flat_weights * in_bounds_mask
|
|
131
|
+
|
|
132
|
+
# Weighted reads
|
|
133
|
+
weighted_reads = flat_reads * flat_weights_masked
|
|
134
|
+
|
|
135
|
+
# Use segment_sum to aggregate bases at each position
|
|
136
|
+
# First, clip positions to valid range (we've already masked weights for invalid)
|
|
137
|
+
clipped_positions = jnp.clip(flat_positions, 0, reference_length - 1)
|
|
138
|
+
|
|
139
|
+
# Aggregate nucleotide counts at each position
|
|
140
|
+
pileup = jax.ops.segment_sum(
|
|
141
|
+
weighted_reads,
|
|
142
|
+
clipped_positions.astype(jnp.int32),
|
|
143
|
+
num_segments=reference_length,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Aggregate coverage at each position (sum of weights)
|
|
147
|
+
coverage = jax.ops.segment_sum(
|
|
148
|
+
flat_weights_masked,
|
|
149
|
+
clipped_positions.astype(jnp.int32),
|
|
150
|
+
num_segments=reference_length,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Normalize by coverage to get nucleotide distribution
|
|
154
|
+
# Add small epsilon to avoid division by zero
|
|
155
|
+
coverage_safe = jnp.maximum(coverage, EPSILON)
|
|
156
|
+
pileup_normalized = pileup / coverage_safe
|
|
157
|
+
|
|
158
|
+
# Optionally apply softmax
|
|
159
|
+
# Use inherited _temperature property from TemperatureOperator
|
|
160
|
+
if self.config.apply_softmax:
|
|
161
|
+
pileup_normalized = jax.nn.softmax(pileup_normalized / self._temperature, axis=-1)
|
|
162
|
+
|
|
163
|
+
result = {"pileup": pileup_normalized}
|
|
164
|
+
|
|
165
|
+
# Add coverage channel if requested
|
|
166
|
+
if self.config.return_coverage:
|
|
167
|
+
result["coverage"] = coverage
|
|
168
|
+
|
|
169
|
+
# Add mean quality channel if requested
|
|
170
|
+
if self.config.return_quality:
|
|
171
|
+
# Aggregate quality * weight, then divide by weight sum
|
|
172
|
+
weighted_quality = flat_quality * flat_weights_masked
|
|
173
|
+
quality_sum = jax.ops.segment_sum(
|
|
174
|
+
weighted_quality,
|
|
175
|
+
clipped_positions.astype(jnp.int32),
|
|
176
|
+
num_segments=reference_length,
|
|
177
|
+
)
|
|
178
|
+
mean_quality = quality_sum / coverage_safe
|
|
179
|
+
result["mean_quality"] = mean_quality
|
|
180
|
+
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
def apply(
|
|
184
|
+
self,
|
|
185
|
+
data: PyTree,
|
|
186
|
+
state: PyTree,
|
|
187
|
+
metadata: dict[str, Any] | None,
|
|
188
|
+
random_params: Any = None,
|
|
189
|
+
stats: dict[str, Any] | None = None,
|
|
190
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
191
|
+
"""Apply pileup generation to read data.
|
|
192
|
+
|
|
193
|
+
This method implements the OperatorModule interface for batch processing.
|
|
194
|
+
It expects data containing reads and their positions, and returns pileup.
|
|
195
|
+
|
|
196
|
+
Note: reference_length is taken from config (not data) because it must be
|
|
197
|
+
static for JAX's segment_sum. All reads in a batch must align to the same
|
|
198
|
+
reference. Output preserves input keys for Datarax vmap compatibility.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
data: Dictionary containing:
|
|
202
|
+
- "reads": One-hot encoded reads (num_reads, read_length, 4)
|
|
203
|
+
- "positions": Starting position of each read (num_reads,)
|
|
204
|
+
- "quality": Quality scores for each base (num_reads, read_length)
|
|
205
|
+
state: Element state (passed through unchanged)
|
|
206
|
+
metadata: Element metadata (passed through unchanged)
|
|
207
|
+
random_params: Not used (deterministic operator)
|
|
208
|
+
stats: Not used
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Tuple of (transformed_data, state, metadata):
|
|
212
|
+
- transformed_data contains input data plus pileup array
|
|
213
|
+
- state is passed through unchanged
|
|
214
|
+
- metadata is passed through unchanged
|
|
215
|
+
"""
|
|
216
|
+
reads = data["reads"]
|
|
217
|
+
positions = data["positions"]
|
|
218
|
+
quality = data["quality"]
|
|
219
|
+
|
|
220
|
+
# Use reference_length from config (must be static for segment_sum)
|
|
221
|
+
reference_length = self.config.reference_length
|
|
222
|
+
|
|
223
|
+
# Compute pileup (returns dict with pileup and optional coverage/quality)
|
|
224
|
+
pileup_result = self.compute_pileup(reads, positions, quality, reference_length)
|
|
225
|
+
|
|
226
|
+
# Build output data - preserve input keys for Datarax vmap compatibility
|
|
227
|
+
transformed_data = {
|
|
228
|
+
"reads": reads,
|
|
229
|
+
"positions": positions,
|
|
230
|
+
"quality": quality,
|
|
231
|
+
"pileup": pileup_result["pileup"],
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Add coverage and mean_quality if present
|
|
235
|
+
if "coverage" in pileup_result:
|
|
236
|
+
transformed_data["coverage"] = pileup_result["coverage"]
|
|
237
|
+
if "mean_quality" in pileup_result:
|
|
238
|
+
transformed_data["mean_quality"] = pileup_result["mean_quality"]
|
|
239
|
+
|
|
240
|
+
return transformed_data, state, metadata
|