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,274 @@
|
|
|
1
|
+
"""Differentiable Ancestry Estimation Operator.
|
|
2
|
+
|
|
3
|
+
This module implements a Neural ADMIXTURE-style differentiable ancestry estimator
|
|
4
|
+
using an autoencoder architecture. The model learns to decompose individual
|
|
5
|
+
genotypes into ancestry proportions from K ancestral populations.
|
|
6
|
+
|
|
7
|
+
Reference:
|
|
8
|
+
Dias et al. (2022). "Neural ADMIXTURE: A Neural Network Approach for
|
|
9
|
+
Fast and Accurate Estimation of Population Structure."
|
|
10
|
+
https://github.com/AI-sandbox/neural-admixture
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import jax.numpy as jnp
|
|
18
|
+
from artifex.generative_models.core.base import MLP
|
|
19
|
+
from flax import nnx
|
|
20
|
+
|
|
21
|
+
from diffbio.configs import TemperatureConfig
|
|
22
|
+
from diffbio.core.base_operators import TemperatureOperator
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class AncestryEstimatorConfig(TemperatureConfig):
|
|
29
|
+
"""Configuration for DifferentiableAncestryEstimator.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
n_snps: Number of SNP markers in genotype input.
|
|
33
|
+
n_populations: Number of ancestral populations (K).
|
|
34
|
+
hidden_dims: Hidden layer dimensions for encoder.
|
|
35
|
+
dropout_rate: Dropout rate for regularization.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
n_snps: int = 10000
|
|
39
|
+
n_populations: int = 5
|
|
40
|
+
hidden_dims: tuple[int, ...] = (128, 64)
|
|
41
|
+
dropout_rate: float = 0.1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DifferentiableAncestryEstimator(TemperatureOperator):
|
|
45
|
+
"""Neural ADMIXTURE-style differentiable ancestry estimator.
|
|
46
|
+
|
|
47
|
+
This operator uses an autoencoder architecture to estimate ancestry
|
|
48
|
+
proportions from genotype data. The encoder maps genotypes to a latent
|
|
49
|
+
representation, which is then transformed to ancestry proportions via
|
|
50
|
+
temperature-controlled softmax. The decoder reconstructs genotypes
|
|
51
|
+
from ancestry proportions, enabling unsupervised learning.
|
|
52
|
+
|
|
53
|
+
The model follows the ADMIXTURE generative model:
|
|
54
|
+
G_ij = sum_k Q_ik * P_kj
|
|
55
|
+
Where:
|
|
56
|
+
- G is the genotype matrix (individuals x SNPs)
|
|
57
|
+
- Q is the ancestry proportion matrix (individuals x K populations)
|
|
58
|
+
- P is the population allele frequency matrix (K x SNPs)
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
config: Operator configuration.
|
|
62
|
+
backbone: Shared encoder MLP, or ``None`` when ``hidden_dims`` is empty.
|
|
63
|
+
ancestry_head: Linear layer for ancestry proportions.
|
|
64
|
+
population_frequencies: Learnable population allele frequencies (P matrix).
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
```python
|
|
68
|
+
from diffbio.operators.population import (
|
|
69
|
+
DifferentiableAncestryEstimator,
|
|
70
|
+
AncestryEstimatorConfig,
|
|
71
|
+
)
|
|
72
|
+
config = AncestryEstimatorConfig(n_snps=1000, n_populations=5)
|
|
73
|
+
estimator = DifferentiableAncestryEstimator(config, rngs=nnx.Rngs(42))
|
|
74
|
+
data = {"genotypes": genotype_matrix} # (n_samples, n_snps)
|
|
75
|
+
result, _, _ = estimator.apply(data, {}, None)
|
|
76
|
+
ancestry = result["ancestry_proportions"] # (n_samples, K)
|
|
77
|
+
```
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
config: AncestryEstimatorConfig,
|
|
83
|
+
*,
|
|
84
|
+
rngs: nnx.Rngs,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Initialize the ancestry estimator.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
config: Operator configuration.
|
|
90
|
+
rngs: Flax NNX random number generators.
|
|
91
|
+
"""
|
|
92
|
+
super().__init__(config, rngs=rngs)
|
|
93
|
+
|
|
94
|
+
self.config = config
|
|
95
|
+
|
|
96
|
+
if config.hidden_dims:
|
|
97
|
+
self.backbone = MLP(
|
|
98
|
+
hidden_dims=list(config.hidden_dims),
|
|
99
|
+
in_features=config.n_snps,
|
|
100
|
+
activation="relu",
|
|
101
|
+
dropout_rate=config.dropout_rate,
|
|
102
|
+
output_activation="relu",
|
|
103
|
+
use_batch_norm=False,
|
|
104
|
+
rngs=rngs,
|
|
105
|
+
)
|
|
106
|
+
latent_dim = config.hidden_dims[-1]
|
|
107
|
+
else:
|
|
108
|
+
self.backbone = None
|
|
109
|
+
latent_dim = config.n_snps
|
|
110
|
+
|
|
111
|
+
# Ancestry proportion head
|
|
112
|
+
self.ancestry_head = nnx.Linear(latent_dim, config.n_populations, rngs=rngs)
|
|
113
|
+
|
|
114
|
+
# Learnable population allele frequencies (P matrix)
|
|
115
|
+
# Shape: (n_populations, n_snps)
|
|
116
|
+
# Initialized with small random values
|
|
117
|
+
self.population_frequencies = nnx.Param(
|
|
118
|
+
jnp.abs(
|
|
119
|
+
jnp.array(
|
|
120
|
+
nnx.initializers.normal(0.1)(
|
|
121
|
+
rngs.params(),
|
|
122
|
+
(config.n_populations, config.n_snps),
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
+ 0.01
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def encode(self, genotypes: jnp.ndarray) -> jnp.ndarray:
|
|
130
|
+
"""Encode genotypes to latent representation.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
genotypes: Genotype matrix of shape (n_samples, n_snps).
|
|
134
|
+
Values should be 0, 1, or 2 representing allele counts.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Latent representation of shape ``(n_samples, hidden_dims[-1])`` when
|
|
138
|
+
hidden layers are configured, otherwise the original genotype matrix.
|
|
139
|
+
"""
|
|
140
|
+
if self.backbone is None:
|
|
141
|
+
return genotypes
|
|
142
|
+
latent: jnp.ndarray = self.backbone(genotypes)
|
|
143
|
+
return latent
|
|
144
|
+
|
|
145
|
+
def compute_ancestry(self, latent: jnp.ndarray) -> jnp.ndarray:
|
|
146
|
+
"""Compute ancestry proportions from latent representation.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
latent: Latent representation of shape (n_samples, hidden_dims[-1]).
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Ancestry proportions of shape (n_samples, n_populations).
|
|
153
|
+
Each row sums to 1 and all values are non-negative.
|
|
154
|
+
"""
|
|
155
|
+
# Get raw ancestry logits
|
|
156
|
+
logits = self.ancestry_head(latent)
|
|
157
|
+
|
|
158
|
+
# Apply temperature-controlled softmax
|
|
159
|
+
temperature = jnp.maximum(self._temperature, 1e-6)
|
|
160
|
+
proportions = nnx.softmax(logits / temperature, axis=-1)
|
|
161
|
+
|
|
162
|
+
return proportions
|
|
163
|
+
|
|
164
|
+
def decode(self, ancestry: jnp.ndarray) -> jnp.ndarray:
|
|
165
|
+
"""Decode ancestry proportions to reconstructed genotypes.
|
|
166
|
+
|
|
167
|
+
Following the ADMIXTURE model: G = Q @ P
|
|
168
|
+
Where Q is ancestry proportions and P is population frequencies.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
ancestry: Ancestry proportions of shape (n_samples, n_populations).
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Reconstructed genotypes of shape (n_samples, n_snps).
|
|
175
|
+
Values represent expected allele counts (continuous 0-2).
|
|
176
|
+
"""
|
|
177
|
+
# Get population frequencies normalized to [0, 1]
|
|
178
|
+
pop_freqs = nnx.sigmoid(self.population_frequencies[...])
|
|
179
|
+
|
|
180
|
+
# Reconstruct: G = Q @ P, scaled to 0-2 range
|
|
181
|
+
# ancestry: (n_samples, n_populations)
|
|
182
|
+
# pop_freqs: (n_populations, n_snps)
|
|
183
|
+
reconstructed = 2.0 * jnp.matmul(ancestry, pop_freqs)
|
|
184
|
+
|
|
185
|
+
return reconstructed
|
|
186
|
+
|
|
187
|
+
def apply(
|
|
188
|
+
self,
|
|
189
|
+
data: dict[str, Any],
|
|
190
|
+
state: dict[str, Any],
|
|
191
|
+
metadata: dict[str, Any] | None,
|
|
192
|
+
random_params: Any = None,
|
|
193
|
+
stats: dict[str, Any] | None = None,
|
|
194
|
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]:
|
|
195
|
+
"""Apply ancestry estimation to genotype data.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
data: Dictionary containing:
|
|
199
|
+
- "genotypes": Genotype matrix (n_samples, n_snps) with values 0/1/2.
|
|
200
|
+
state: Per-element state (passed through).
|
|
201
|
+
metadata: Optional metadata (passed through).
|
|
202
|
+
random_params: Random parameters for stochastic operations.
|
|
203
|
+
stats: Optional statistics dictionary.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Tuple of (transformed_data, state, metadata) where transformed_data
|
|
207
|
+
contains:
|
|
208
|
+
|
|
209
|
+
- "genotypes": Original genotype matrix.
|
|
210
|
+
- "ancestry_proportions": Estimated ancestry (n_samples, K).
|
|
211
|
+
- "reconstructed": Reconstructed genotypes (n_samples, n_snps).
|
|
212
|
+
- "latent": Latent representation.
|
|
213
|
+
"""
|
|
214
|
+
genotypes = data["genotypes"]
|
|
215
|
+
|
|
216
|
+
# Encode genotypes to latent space
|
|
217
|
+
latent = self.encode(genotypes)
|
|
218
|
+
|
|
219
|
+
# Compute ancestry proportions
|
|
220
|
+
ancestry = self.compute_ancestry(latent)
|
|
221
|
+
|
|
222
|
+
# Decode to reconstructed genotypes
|
|
223
|
+
reconstructed = self.decode(ancestry)
|
|
224
|
+
|
|
225
|
+
# Build output
|
|
226
|
+
output = {
|
|
227
|
+
**data,
|
|
228
|
+
"ancestry_proportions": ancestry,
|
|
229
|
+
"reconstructed": reconstructed,
|
|
230
|
+
"latent": latent,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return output, state, metadata
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def create_ancestry_estimator(
|
|
237
|
+
n_snps: int,
|
|
238
|
+
n_populations: int,
|
|
239
|
+
hidden_dims: tuple[int, ...] = (128, 64),
|
|
240
|
+
temperature: float = 1.0,
|
|
241
|
+
dropout_rate: float = 0.1,
|
|
242
|
+
seed: int = 42,
|
|
243
|
+
) -> DifferentiableAncestryEstimator:
|
|
244
|
+
"""Factory function to create an ancestry estimator.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
n_snps: Number of SNP markers.
|
|
248
|
+
n_populations: Number of ancestral populations (K).
|
|
249
|
+
hidden_dims: Hidden layer dimensions for encoder.
|
|
250
|
+
temperature: Softmax temperature for ancestry proportions.
|
|
251
|
+
dropout_rate: Dropout rate for regularization.
|
|
252
|
+
seed: Random seed for initialization.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Configured DifferentiableAncestryEstimator instance.
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
```python
|
|
259
|
+
estimator = create_ancestry_estimator(
|
|
260
|
+
n_snps=10000,
|
|
261
|
+
n_populations=5,
|
|
262
|
+
)
|
|
263
|
+
result, _, _ = estimator.apply({"genotypes": data}, {}, None)
|
|
264
|
+
```
|
|
265
|
+
"""
|
|
266
|
+
config = AncestryEstimatorConfig(
|
|
267
|
+
n_snps=n_snps,
|
|
268
|
+
n_populations=n_populations,
|
|
269
|
+
hidden_dims=hidden_dims,
|
|
270
|
+
temperature=temperature,
|
|
271
|
+
dropout_rate=dropout_rate,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return DifferentiableAncestryEstimator(config, rngs=nnx.Rngs(seed))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Differentiable preprocessing operators for bioinformatics sequences.
|
|
2
|
+
|
|
3
|
+
This module provides preprocessing operators that maintain gradient flow
|
|
4
|
+
for end-to-end trainable pipelines.
|
|
5
|
+
|
|
6
|
+
Operators:
|
|
7
|
+
SoftAdapterRemoval: Differentiable adapter trimming using soft alignment
|
|
8
|
+
DifferentiableDuplicateWeighting: Probabilistic duplicate weighting
|
|
9
|
+
SoftErrorCorrection: Neural network-based error correction
|
|
10
|
+
|
|
11
|
+
Factories:
|
|
12
|
+
wrap_probabilistic: Wrap any preprocessing operator in a datarax
|
|
13
|
+
ProbabilisticOperator for random augmentation during training.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from diffbio.operators.preprocessing.adapter_removal import (
|
|
21
|
+
AdapterRemovalConfig,
|
|
22
|
+
SoftAdapterRemoval,
|
|
23
|
+
)
|
|
24
|
+
from diffbio.operators.preprocessing.duplicate_filter import (
|
|
25
|
+
DifferentiableDuplicateWeighting,
|
|
26
|
+
DuplicateWeightingConfig,
|
|
27
|
+
)
|
|
28
|
+
from diffbio.operators.preprocessing.error_correction import (
|
|
29
|
+
ErrorCorrectionConfig,
|
|
30
|
+
SoftErrorCorrection,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from datarax.core.operator import OperatorModule
|
|
35
|
+
from datarax.operators import ProbabilisticOperator
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def wrap_probabilistic(
|
|
39
|
+
operator: OperatorModule,
|
|
40
|
+
probability: float = 0.5,
|
|
41
|
+
) -> ProbabilisticOperator:
|
|
42
|
+
"""Wrap a preprocessing operator in a ProbabilisticOperator.
|
|
43
|
+
|
|
44
|
+
The wrapped operator is applied with the given probability during
|
|
45
|
+
each forward pass, enabling random augmentation during training.
|
|
46
|
+
When not applied, the input data passes through unchanged.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
operator: A preprocessing operator to wrap.
|
|
50
|
+
probability: Probability of applying the operator (0.0 to 1.0).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A ProbabilisticOperator wrapping the given operator.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> adapter_remover = SoftAdapterRemoval(AdapterRemovalConfig(), rngs=rngs)
|
|
57
|
+
>>> prob_remover = wrap_probabilistic(adapter_remover, probability=0.8)
|
|
58
|
+
"""
|
|
59
|
+
from datarax.operators import ( # noqa: PLC0415
|
|
60
|
+
ProbabilisticOperator,
|
|
61
|
+
ProbabilisticOperatorConfig,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
config = ProbabilisticOperatorConfig(operator=operator, probability=probability)
|
|
65
|
+
return ProbabilisticOperator(config)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"AdapterRemovalConfig",
|
|
70
|
+
"DifferentiableDuplicateWeighting",
|
|
71
|
+
"DuplicateWeightingConfig",
|
|
72
|
+
"ErrorCorrectionConfig",
|
|
73
|
+
"SoftAdapterRemoval",
|
|
74
|
+
"SoftErrorCorrection",
|
|
75
|
+
"wrap_probabilistic",
|
|
76
|
+
]
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Differentiable adapter removal operator.
|
|
2
|
+
|
|
3
|
+
This module provides a soft adapter removal operator that uses differentiable
|
|
4
|
+
alignment to find adapter sequences and applies soft trimming to maintain
|
|
5
|
+
gradient flow.
|
|
6
|
+
|
|
7
|
+
Key technique: Use SmoothSmithWaterman for adapter matching, then apply
|
|
8
|
+
sigmoid-weighted soft trimming based on the match position.
|
|
9
|
+
|
|
10
|
+
Inherits from TemperatureOperator to get:
|
|
11
|
+
|
|
12
|
+
- _temperature property for temperature-controlled smoothing
|
|
13
|
+
- soft_max() for logsumexp-based smooth maximum
|
|
14
|
+
- soft_argmax() for soft position selection
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import jax
|
|
22
|
+
import jax.numpy as jnp
|
|
23
|
+
from datarax.core.config import OperatorConfig
|
|
24
|
+
from flax import nnx
|
|
25
|
+
from jaxtyping import Array, Float, PyTree
|
|
26
|
+
|
|
27
|
+
from diffbio.core import soft_ops
|
|
28
|
+
from diffbio.core.base_operators import TemperatureOperator
|
|
29
|
+
from diffbio.sequences.dna import encode_dna_string
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class AdapterRemovalConfig(OperatorConfig):
|
|
36
|
+
"""Configuration for SoftAdapterRemoval.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
adapter_sequence: Adapter sequence to remove (default: Illumina universal).
|
|
40
|
+
temperature: Temperature for soft matching and trimming.
|
|
41
|
+
Lower = sharper trimming, Higher = smoother.
|
|
42
|
+
match_threshold: Minimum alignment score ratio to consider a match.
|
|
43
|
+
min_overlap: Minimum overlap length to consider adapter presence.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
adapter_sequence: str = "AGATCGGAAGAG" # Illumina universal adapter
|
|
47
|
+
temperature: float = 1.0
|
|
48
|
+
learnable_temperature: bool = True
|
|
49
|
+
match_threshold: float = 0.5
|
|
50
|
+
min_overlap: int = 6
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SoftAdapterRemoval(TemperatureOperator):
|
|
54
|
+
"""Differentiable adapter removal for sequencing reads.
|
|
55
|
+
|
|
56
|
+
This operator performs soft adapter trimming using a differentiable
|
|
57
|
+
approach. It finds potential adapter matches at the 3' end of reads
|
|
58
|
+
and applies sigmoid-weighted trimming that maintains gradient flow.
|
|
59
|
+
|
|
60
|
+
The algorithm:
|
|
61
|
+
1. Compute soft alignment scores between sequence suffix and adapter
|
|
62
|
+
2. Find the soft trim position using weighted position averaging
|
|
63
|
+
3. Apply sigmoid-weighted retention to each position
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
config: AdapterRemovalConfig with adapter parameters.
|
|
67
|
+
rngs: Flax NNX random number generators (optional).
|
|
68
|
+
name: Optional operator name.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
```python
|
|
72
|
+
config = AdapterRemovalConfig(adapter_sequence="AGATCGGAAGAG")
|
|
73
|
+
remover = SoftAdapterRemoval(config)
|
|
74
|
+
data = {"sequence": encoded_seq, "quality_scores": quality}
|
|
75
|
+
result, state, meta = remover.apply(data, {}, None)
|
|
76
|
+
```
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
config: AdapterRemovalConfig,
|
|
82
|
+
*,
|
|
83
|
+
rngs: nnx.Rngs | None = None,
|
|
84
|
+
name: str | None = None,
|
|
85
|
+
):
|
|
86
|
+
"""Initialize the soft adapter removal operator.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
config: Adapter removal configuration.
|
|
90
|
+
rngs: Random number generators (optional).
|
|
91
|
+
name: Optional operator name.
|
|
92
|
+
"""
|
|
93
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
94
|
+
|
|
95
|
+
# Encode adapter sequence as one-hot
|
|
96
|
+
adapter_onehot = encode_dna_string(config.adapter_sequence)
|
|
97
|
+
self.adapter = nnx.Param(adapter_onehot)
|
|
98
|
+
|
|
99
|
+
# Learnable parameters
|
|
100
|
+
# Temperature is managed by TemperatureOperator via self._temperature
|
|
101
|
+
self.match_threshold = nnx.Param(jnp.array(config.match_threshold))
|
|
102
|
+
|
|
103
|
+
# Scoring matrix for DNA alignment (match=2, mismatch=-1)
|
|
104
|
+
scoring = jnp.array(
|
|
105
|
+
[
|
|
106
|
+
[2.0, -1.0, -1.0, -1.0], # A
|
|
107
|
+
[-1.0, 2.0, -1.0, -1.0], # C
|
|
108
|
+
[-1.0, -1.0, 2.0, -1.0], # G
|
|
109
|
+
[-1.0, -1.0, -1.0, 2.0], # T
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
self.scoring_matrix = nnx.Param(scoring)
|
|
113
|
+
|
|
114
|
+
# Store config values
|
|
115
|
+
self.min_overlap = config.min_overlap
|
|
116
|
+
|
|
117
|
+
def _compute_suffix_adapter_scores(
|
|
118
|
+
self,
|
|
119
|
+
sequence: Float[Array, "length alphabet"],
|
|
120
|
+
) -> Float[Array, "length"]:
|
|
121
|
+
"""Compute adapter match scores for each suffix position.
|
|
122
|
+
|
|
123
|
+
For each position i, compute how well the suffix starting at i
|
|
124
|
+
matches the adapter prefix. This finds where the adapter might start.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
sequence: One-hot encoded sequence (length, 4).
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Scores for adapter match starting at each position (length,).
|
|
131
|
+
"""
|
|
132
|
+
seq_len = sequence.shape[0]
|
|
133
|
+
adapter = self.adapter[...]
|
|
134
|
+
adapter_len = adapter.shape[0]
|
|
135
|
+
scoring = self.scoring_matrix[...]
|
|
136
|
+
|
|
137
|
+
# For each starting position, compute match score with adapter
|
|
138
|
+
def score_at_position(start_pos: Array | int) -> Float[Array, ""]:
|
|
139
|
+
"""Compute alignment score for suffix starting at start_pos."""
|
|
140
|
+
# Length of overlap
|
|
141
|
+
overlap_len = jnp.minimum(seq_len - start_pos, adapter_len)
|
|
142
|
+
|
|
143
|
+
# Extract overlapping regions
|
|
144
|
+
# Use dynamic slicing with masking for differentiability
|
|
145
|
+
positions = jnp.arange(adapter_len)
|
|
146
|
+
mask = positions < overlap_len
|
|
147
|
+
|
|
148
|
+
# Compute scores for each position in the overlap
|
|
149
|
+
def score_position(pos: Array | int) -> Float[Array, ""]:
|
|
150
|
+
seq_pos = start_pos + pos
|
|
151
|
+
# Handle out-of-bounds with zeros
|
|
152
|
+
valid = (seq_pos < seq_len) & (pos < adapter_len)
|
|
153
|
+
seq_base = jnp.where(
|
|
154
|
+
valid,
|
|
155
|
+
jax.lax.dynamic_slice(sequence, (seq_pos, 0), (1, 4)).squeeze(0),
|
|
156
|
+
jnp.zeros(4),
|
|
157
|
+
)
|
|
158
|
+
adapter_base = jnp.where(
|
|
159
|
+
valid,
|
|
160
|
+
jax.lax.dynamic_slice(adapter, (pos, 0), (1, 4)).squeeze(0),
|
|
161
|
+
jnp.zeros(4),
|
|
162
|
+
)
|
|
163
|
+
# Score = seq @ scoring @ adapter.T
|
|
164
|
+
score = jnp.einsum("a,ab,b->", seq_base, scoring, adapter_base)
|
|
165
|
+
return jnp.where(valid, score, 0.0)
|
|
166
|
+
|
|
167
|
+
# Sum scores across overlap positions
|
|
168
|
+
scores = jax.vmap(score_position)(jnp.arange(adapter_len))
|
|
169
|
+
total_score = jnp.sum(scores * mask)
|
|
170
|
+
|
|
171
|
+
# Normalize by maximum possible score for this overlap length
|
|
172
|
+
max_score = 2.0 * overlap_len # Perfect match score
|
|
173
|
+
normalized = jnp.where(
|
|
174
|
+
overlap_len >= self.min_overlap,
|
|
175
|
+
total_score / jnp.maximum(max_score, 1.0),
|
|
176
|
+
0.0,
|
|
177
|
+
)
|
|
178
|
+
return normalized
|
|
179
|
+
|
|
180
|
+
# Compute scores for all suffix positions
|
|
181
|
+
scores = jax.vmap(score_at_position)(jnp.arange(seq_len))
|
|
182
|
+
return scores
|
|
183
|
+
|
|
184
|
+
def _compute_soft_trim_position(
|
|
185
|
+
self,
|
|
186
|
+
adapter_scores: Float[Array, "length"],
|
|
187
|
+
) -> Float[Array, ""]:
|
|
188
|
+
"""Compute soft trim position using weighted average.
|
|
189
|
+
|
|
190
|
+
Uses softmax over adapter scores to compute a soft trim position.
|
|
191
|
+
High adapter match scores at position i suggest trimming should
|
|
192
|
+
start there.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
adapter_scores: Adapter match scores at each position.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Soft trim position (continuous value).
|
|
199
|
+
"""
|
|
200
|
+
seq_len = adapter_scores.shape[0]
|
|
201
|
+
temp = self._temperature
|
|
202
|
+
threshold = self.match_threshold[...]
|
|
203
|
+
|
|
204
|
+
# Apply threshold - only consider positions with good matches
|
|
205
|
+
thresholded_scores = nnx.relu(adapter_scores - threshold)
|
|
206
|
+
|
|
207
|
+
# Soft position selection using softmax
|
|
208
|
+
# Add small epsilon for numerical stability
|
|
209
|
+
weights = jax.nn.softmax(thresholded_scores / temp + 1e-10)
|
|
210
|
+
|
|
211
|
+
# Weighted average of positions
|
|
212
|
+
positions = jnp.arange(seq_len, dtype=jnp.float32)
|
|
213
|
+
soft_position = jnp.sum(weights * positions)
|
|
214
|
+
|
|
215
|
+
# If no adapter found (all scores below threshold), return seq_len
|
|
216
|
+
has_adapter = jnp.any(adapter_scores > threshold)
|
|
217
|
+
soft_position = jnp.where(has_adapter, soft_position, float(seq_len))
|
|
218
|
+
|
|
219
|
+
return soft_position
|
|
220
|
+
|
|
221
|
+
def _apply_soft_trimming(
|
|
222
|
+
self,
|
|
223
|
+
sequence: Float[Array, "length alphabet"],
|
|
224
|
+
quality_scores: Float[Array, "length"],
|
|
225
|
+
soft_trim_pos: Float[Array, ""],
|
|
226
|
+
) -> tuple[Float[Array, "length alphabet"], Float[Array, "length"]]:
|
|
227
|
+
"""Apply soft trimming to sequence and quality scores.
|
|
228
|
+
|
|
229
|
+
Uses sigmoid to create smooth retention weights based on position
|
|
230
|
+
relative to the trim point. Positions before trim_pos have high
|
|
231
|
+
retention, positions after have low retention.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
sequence: One-hot encoded sequence.
|
|
235
|
+
quality_scores: Quality scores for each position.
|
|
236
|
+
soft_trim_pos: Soft trim position.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Tuple of (weighted_sequence, weighted_quality).
|
|
240
|
+
"""
|
|
241
|
+
seq_len = sequence.shape[0]
|
|
242
|
+
temp = self._temperature
|
|
243
|
+
|
|
244
|
+
# Create position-based retention weights
|
|
245
|
+
# retention = sigmoid((trim_pos - position) / temperature)
|
|
246
|
+
positions = jnp.arange(seq_len, dtype=jnp.float32)
|
|
247
|
+
retention_weights = soft_ops.greater(soft_trim_pos, positions, softness=temp)
|
|
248
|
+
|
|
249
|
+
# Apply weights
|
|
250
|
+
weighted_sequence = sequence * retention_weights[:, None]
|
|
251
|
+
weighted_quality = quality_scores * retention_weights
|
|
252
|
+
|
|
253
|
+
return weighted_sequence, weighted_quality
|
|
254
|
+
|
|
255
|
+
def apply(
|
|
256
|
+
self,
|
|
257
|
+
data: PyTree,
|
|
258
|
+
state: PyTree,
|
|
259
|
+
metadata: dict[str, Any] | None,
|
|
260
|
+
random_params: Any = None,
|
|
261
|
+
stats: dict[str, Any] | None = None,
|
|
262
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
263
|
+
"""Apply soft adapter removal to sequence data.
|
|
264
|
+
|
|
265
|
+
This method finds potential adapter sequences and applies
|
|
266
|
+
differentiable soft trimming to remove them while maintaining
|
|
267
|
+
gradient flow.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
data: Dictionary containing:
|
|
271
|
+
- "sequence": One-hot encoded sequence (length, alphabet_size)
|
|
272
|
+
- "quality_scores": Phred quality scores (length,)
|
|
273
|
+
state: Element state (passed through unchanged)
|
|
274
|
+
metadata: Element metadata (passed through unchanged)
|
|
275
|
+
random_params: Not used (deterministic operator)
|
|
276
|
+
stats: Not used
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Tuple of (transformed_data, state, metadata):
|
|
280
|
+
- transformed_data contains:
|
|
281
|
+
|
|
282
|
+
- "sequence": Soft-trimmed sequence
|
|
283
|
+
- "quality_scores": Soft-trimmed quality scores
|
|
284
|
+
- "adapter_score": Maximum adapter match score
|
|
285
|
+
- "trim_position": Soft trim position
|
|
286
|
+
- state is passed through unchanged
|
|
287
|
+
- metadata is passed through unchanged
|
|
288
|
+
"""
|
|
289
|
+
sequence = data["sequence"]
|
|
290
|
+
quality_scores = data["quality_scores"]
|
|
291
|
+
|
|
292
|
+
# Compute adapter match scores for each suffix position
|
|
293
|
+
adapter_scores = self._compute_suffix_adapter_scores(sequence)
|
|
294
|
+
|
|
295
|
+
# Find soft trim position
|
|
296
|
+
soft_trim_pos = self._compute_soft_trim_position(adapter_scores)
|
|
297
|
+
|
|
298
|
+
# Apply soft trimming
|
|
299
|
+
trimmed_sequence, trimmed_quality = self._apply_soft_trimming(
|
|
300
|
+
sequence, quality_scores, soft_trim_pos
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Build output data
|
|
304
|
+
transformed_data = {
|
|
305
|
+
"sequence": trimmed_sequence,
|
|
306
|
+
"quality_scores": trimmed_quality,
|
|
307
|
+
"adapter_score": jnp.max(adapter_scores),
|
|
308
|
+
"trim_position": soft_trim_pos,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return transformed_data, state, metadata
|