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,317 @@
|
|
|
1
|
+
"""Differentiable duplicate weighting operator.
|
|
2
|
+
|
|
3
|
+
This module provides a probabilistic duplicate weighting operator that assigns
|
|
4
|
+
soft weights to reads based on their uniqueness, instead of hard duplicate
|
|
5
|
+
removal.
|
|
6
|
+
|
|
7
|
+
Key technique: Soft clustering of reads by sequence similarity with weights
|
|
8
|
+
inversely proportional to cluster size.
|
|
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
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class DuplicateWeightingConfig(OperatorConfig):
|
|
35
|
+
"""Configuration for DifferentiableDuplicateWeighting.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
temperature: Temperature for soft similarity computation.
|
|
39
|
+
Lower = sharper clustering, Higher = smoother.
|
|
40
|
+
similarity_threshold: Minimum similarity to consider as duplicate.
|
|
41
|
+
embedding_dim: Dimension of learned sequence embedding.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
temperature: float = 1.0
|
|
45
|
+
learnable_temperature: bool = True
|
|
46
|
+
similarity_threshold: float = 0.9
|
|
47
|
+
embedding_dim: int = 32
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DifferentiableDuplicateWeighting(TemperatureOperator):
|
|
51
|
+
"""Differentiable duplicate weighting for sequencing reads.
|
|
52
|
+
|
|
53
|
+
This operator assigns probabilistic weights to reads based on their
|
|
54
|
+
uniqueness within a batch. Instead of hard duplicate removal, it
|
|
55
|
+
down-weights reads that are similar to others, maintaining gradient flow.
|
|
56
|
+
|
|
57
|
+
The algorithm:
|
|
58
|
+
1. Embed sequences using learned convolutional features
|
|
59
|
+
2. Compute pairwise soft similarity matrix
|
|
60
|
+
3. Compute soft cluster sizes from similarity matrix
|
|
61
|
+
4. Assign weights inversely proportional to cluster size
|
|
62
|
+
|
|
63
|
+
Note: This operator works on batched data where reads can be compared.
|
|
64
|
+
For single-read processing, it returns weight=1.0.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
config: DuplicateWeightingConfig with weighting parameters.
|
|
68
|
+
rngs: Flax NNX random number generators.
|
|
69
|
+
name: Optional operator name.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
```python
|
|
73
|
+
config = DuplicateWeightingConfig(similarity_threshold=0.9)
|
|
74
|
+
weighter = DifferentiableDuplicateWeighting(config, rngs=nnx.Rngs(42))
|
|
75
|
+
data = {"sequence": encoded_seq, "quality_scores": quality}
|
|
76
|
+
result, state, meta = weighter.apply(data, {}, None)
|
|
77
|
+
```
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
config: DuplicateWeightingConfig,
|
|
83
|
+
*,
|
|
84
|
+
rngs: nnx.Rngs | None = None,
|
|
85
|
+
name: str | None = None,
|
|
86
|
+
):
|
|
87
|
+
"""Initialize the duplicate weighting operator.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
config: Duplicate weighting configuration.
|
|
91
|
+
rngs: Random number generators for initialization.
|
|
92
|
+
name: Optional operator name.
|
|
93
|
+
"""
|
|
94
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
95
|
+
|
|
96
|
+
# Learnable parameters
|
|
97
|
+
# Temperature is managed by TemperatureOperator via self._temperature
|
|
98
|
+
self.similarity_threshold = nnx.Param(jnp.array(config.similarity_threshold))
|
|
99
|
+
|
|
100
|
+
# Simple embedding: learned projection from one-hot to embedding
|
|
101
|
+
# This will be applied via convolution for position-invariant features
|
|
102
|
+
embedding_dim = config.embedding_dim
|
|
103
|
+
if rngs is not None:
|
|
104
|
+
key = rngs.params()
|
|
105
|
+
else:
|
|
106
|
+
key = jax.random.key(0)
|
|
107
|
+
|
|
108
|
+
# Convolution kernel for sequence embedding (kernel_size=7)
|
|
109
|
+
kernel_shape = (7, 4, embedding_dim) # (kernel_size, in_channels, out_channels)
|
|
110
|
+
self.conv_kernel = nnx.Param(jax.random.normal(key, kernel_shape) * 0.1)
|
|
111
|
+
|
|
112
|
+
def _embed_sequence(
|
|
113
|
+
self,
|
|
114
|
+
sequence: Float[Array, "length alphabet"],
|
|
115
|
+
) -> Float[Array, "embedding_dim"]:
|
|
116
|
+
"""Embed a sequence into a fixed-size vector.
|
|
117
|
+
|
|
118
|
+
Uses 1D convolution followed by global average pooling to create
|
|
119
|
+
a fixed-size embedding regardless of sequence length.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
sequence: One-hot encoded sequence (length, 4).
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Embedding vector of shape (embedding_dim,).
|
|
126
|
+
"""
|
|
127
|
+
kernel = self.conv_kernel[...]
|
|
128
|
+
|
|
129
|
+
# 1D convolution: (length, alphabet) -> (length - kernel_size + 1, embedding_dim)
|
|
130
|
+
# Using jax.lax.conv for 1D convolution
|
|
131
|
+
# Need to reshape for conv: add batch and channel dims
|
|
132
|
+
seq_reshaped = sequence[None, :, :] # (1, length, 4)
|
|
133
|
+
|
|
134
|
+
# jax.lax.conv expects (batch, in_channels, spatial)
|
|
135
|
+
seq_transposed = jnp.transpose(seq_reshaped, (0, 2, 1)) # (1, 4, length)
|
|
136
|
+
kernel_transposed = jnp.transpose(kernel, (2, 1, 0)) # (out, in, kernel_size)
|
|
137
|
+
|
|
138
|
+
# Perform convolution
|
|
139
|
+
conv_out = jax.lax.conv_general_dilated(
|
|
140
|
+
seq_transposed,
|
|
141
|
+
kernel_transposed,
|
|
142
|
+
window_strides=(1,),
|
|
143
|
+
padding="VALID",
|
|
144
|
+
dimension_numbers=("NCH", "OIH", "NCH"),
|
|
145
|
+
) # (1, embedding_dim, new_length)
|
|
146
|
+
|
|
147
|
+
# Apply ReLU activation
|
|
148
|
+
conv_out = nnx.relu(conv_out)
|
|
149
|
+
|
|
150
|
+
# Global average pooling
|
|
151
|
+
embedding = jnp.mean(conv_out, axis=-1).squeeze(0) # (embedding_dim,)
|
|
152
|
+
|
|
153
|
+
# L2 normalize for cosine similarity
|
|
154
|
+
embedding = embedding / (jnp.linalg.norm(embedding) + 1e-8)
|
|
155
|
+
|
|
156
|
+
return embedding
|
|
157
|
+
|
|
158
|
+
def _compute_similarity_matrix(
|
|
159
|
+
self,
|
|
160
|
+
embeddings: Float[Array, "batch embedding_dim"],
|
|
161
|
+
) -> Float[Array, "batch batch"]:
|
|
162
|
+
"""Compute pairwise cosine similarity matrix.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
embeddings: Batch of embeddings (batch_size, embedding_dim).
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Similarity matrix of shape (batch_size, batch_size).
|
|
169
|
+
"""
|
|
170
|
+
# Cosine similarity: embeddings are already L2 normalized
|
|
171
|
+
similarity = jnp.einsum("ie,je->ij", embeddings, embeddings)
|
|
172
|
+
return similarity
|
|
173
|
+
|
|
174
|
+
def _compute_soft_cluster_sizes(
|
|
175
|
+
self,
|
|
176
|
+
similarity: Float[Array, "batch batch"],
|
|
177
|
+
) -> Float[Array, "batch"]:
|
|
178
|
+
"""Compute soft cluster size for each sequence.
|
|
179
|
+
|
|
180
|
+
Uses thresholded similarity to compute how many sequences are
|
|
181
|
+
"similar" to each sequence (soft duplicate count).
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
similarity: Pairwise similarity matrix.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Soft cluster size for each sequence.
|
|
188
|
+
"""
|
|
189
|
+
temp = self._temperature
|
|
190
|
+
threshold = self.similarity_threshold[...]
|
|
191
|
+
|
|
192
|
+
# Soft thresholding: sigmoid((similarity - threshold) / temperature)
|
|
193
|
+
soft_membership = soft_ops.greater(similarity, threshold, softness=temp)
|
|
194
|
+
|
|
195
|
+
# Sum memberships for each sequence (including self)
|
|
196
|
+
cluster_sizes = jnp.sum(soft_membership, axis=1)
|
|
197
|
+
|
|
198
|
+
return cluster_sizes
|
|
199
|
+
|
|
200
|
+
def _compute_uniqueness_weights(
|
|
201
|
+
self,
|
|
202
|
+
cluster_sizes: Float[Array, "batch"],
|
|
203
|
+
) -> Float[Array, "batch"]:
|
|
204
|
+
"""Compute uniqueness weights from cluster sizes.
|
|
205
|
+
|
|
206
|
+
Weight = 1 / cluster_size (normalized to sum to batch_size).
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
cluster_sizes: Soft cluster size for each sequence.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Uniqueness weight for each sequence.
|
|
213
|
+
"""
|
|
214
|
+
# Weight inversely proportional to cluster size
|
|
215
|
+
raw_weights = 1.0 / jnp.maximum(cluster_sizes, 1.0)
|
|
216
|
+
|
|
217
|
+
# Normalize weights to have mean 1.0
|
|
218
|
+
weights = raw_weights / jnp.mean(raw_weights)
|
|
219
|
+
|
|
220
|
+
return weights
|
|
221
|
+
|
|
222
|
+
def apply(
|
|
223
|
+
self,
|
|
224
|
+
data: PyTree,
|
|
225
|
+
state: PyTree,
|
|
226
|
+
metadata: dict[str, Any] | None,
|
|
227
|
+
random_params: Any = None,
|
|
228
|
+
stats: dict[str, Any] | None = None,
|
|
229
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
230
|
+
"""Apply duplicate weighting to sequence data.
|
|
231
|
+
|
|
232
|
+
For single sequences, returns weight=1.0.
|
|
233
|
+
For batched sequences, computes uniqueness-based weights.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
data: Dictionary containing:
|
|
237
|
+
- "sequence": One-hot encoded sequence (length, alphabet_size)
|
|
238
|
+
or batch (batch, length, alphabet_size)
|
|
239
|
+
- "quality_scores": Quality scores (length,) or (batch, length)
|
|
240
|
+
state: Element state (passed through unchanged)
|
|
241
|
+
metadata: Element metadata (passed through unchanged)
|
|
242
|
+
random_params: Not used (deterministic operator)
|
|
243
|
+
stats: Not used
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Tuple of (transformed_data, state, metadata):
|
|
247
|
+
- transformed_data contains:
|
|
248
|
+
|
|
249
|
+
- "sequence": Original sequence (unchanged)
|
|
250
|
+
- "quality_scores": Original quality scores (unchanged)
|
|
251
|
+
- "uniqueness_weight": Weight based on uniqueness
|
|
252
|
+
- "embedding": Sequence embedding (for downstream use)
|
|
253
|
+
- state is passed through unchanged
|
|
254
|
+
- metadata is passed through unchanged
|
|
255
|
+
"""
|
|
256
|
+
sequence = data["sequence"]
|
|
257
|
+
quality_scores = data["quality_scores"]
|
|
258
|
+
|
|
259
|
+
# Check if input is batched
|
|
260
|
+
if sequence.ndim == 2:
|
|
261
|
+
# Single sequence: weight = 1.0
|
|
262
|
+
embedding = self._embed_sequence(sequence)
|
|
263
|
+
uniqueness_weight = jnp.array(1.0)
|
|
264
|
+
else:
|
|
265
|
+
# Batched sequences: compute pairwise weights
|
|
266
|
+
batch_size = sequence.shape[0]
|
|
267
|
+
|
|
268
|
+
# Embed all sequences
|
|
269
|
+
embeddings = jax.vmap(self._embed_sequence)(sequence)
|
|
270
|
+
|
|
271
|
+
# Compute similarity and weights
|
|
272
|
+
similarity = self._compute_similarity_matrix(embeddings)
|
|
273
|
+
cluster_sizes = self._compute_soft_cluster_sizes(similarity)
|
|
274
|
+
weights = self._compute_uniqueness_weights(cluster_sizes)
|
|
275
|
+
|
|
276
|
+
# For single sequence output, take first embedding and weight
|
|
277
|
+
embedding = embeddings[0] if batch_size > 0 else jnp.zeros(32)
|
|
278
|
+
uniqueness_weight = weights[0] if batch_size > 0 else jnp.array(1.0)
|
|
279
|
+
|
|
280
|
+
# Build output data
|
|
281
|
+
transformed_data = {
|
|
282
|
+
"sequence": sequence,
|
|
283
|
+
"quality_scores": quality_scores,
|
|
284
|
+
"uniqueness_weight": uniqueness_weight,
|
|
285
|
+
"embedding": embedding,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return transformed_data, state, metadata
|
|
289
|
+
|
|
290
|
+
def apply_batch(
|
|
291
|
+
self,
|
|
292
|
+
sequences: Float[Array, "batch length alphabet"],
|
|
293
|
+
quality_scores: Float[Array, "batch length"],
|
|
294
|
+
) -> tuple[Float[Array, "batch"], Float[Array, "batch embedding_dim"]]:
|
|
295
|
+
"""Apply duplicate weighting to a batch of sequences.
|
|
296
|
+
|
|
297
|
+
This is a convenience method for processing multiple sequences
|
|
298
|
+
and computing their pairwise uniqueness weights.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
sequences: Batch of one-hot encoded sequences.
|
|
302
|
+
quality_scores: Batch of quality scores.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Tuple of (weights, embeddings):
|
|
306
|
+
- weights: Uniqueness weight for each sequence
|
|
307
|
+
- embeddings: Sequence embeddings
|
|
308
|
+
"""
|
|
309
|
+
# Embed all sequences
|
|
310
|
+
embeddings = jax.vmap(self._embed_sequence)(sequences)
|
|
311
|
+
|
|
312
|
+
# Compute similarity and weights
|
|
313
|
+
similarity = self._compute_similarity_matrix(embeddings)
|
|
314
|
+
cluster_sizes = self._compute_soft_cluster_sizes(similarity)
|
|
315
|
+
weights = self._compute_uniqueness_weights(cluster_sizes)
|
|
316
|
+
|
|
317
|
+
return weights, embeddings
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Differentiable error correction operator.
|
|
2
|
+
|
|
3
|
+
This module provides a neural network-based error correction operator that
|
|
4
|
+
refines base calls using local sequence context and quality scores.
|
|
5
|
+
|
|
6
|
+
Key technique: Use a small MLP to predict corrected base probabilities
|
|
7
|
+
from a sliding window of sequence and quality data.
|
|
8
|
+
|
|
9
|
+
Inspired by DeepConsensus approach for consensus calling.
|
|
10
|
+
|
|
11
|
+
Inherits from TemperatureOperator to get:
|
|
12
|
+
|
|
13
|
+
- _temperature property for temperature-controlled smoothing
|
|
14
|
+
- soft_max() for logsumexp-based smooth maximum
|
|
15
|
+
- soft_argmax() for soft position selection
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import jax
|
|
23
|
+
import jax.numpy as jnp
|
|
24
|
+
from artifex.generative_models.core.base import MLP
|
|
25
|
+
from datarax.core.config import OperatorConfig
|
|
26
|
+
from flax import nnx
|
|
27
|
+
from jaxtyping import Array, Float, PyTree
|
|
28
|
+
|
|
29
|
+
from diffbio.core.base_operators import TemperatureOperator
|
|
30
|
+
from diffbio.utils.nn_utils import ensure_rngs, init_learnable_param
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ErrorCorrectionConfig(OperatorConfig):
|
|
37
|
+
"""Configuration for SoftErrorCorrection.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
window_size: Size of context window around each position.
|
|
41
|
+
Must be odd. Default is 11 (5 bases on each side).
|
|
42
|
+
hidden_dim: Hidden layer dimension in the MLP.
|
|
43
|
+
num_layers: Number of hidden layers in the MLP.
|
|
44
|
+
use_quality: Whether to include quality scores as input.
|
|
45
|
+
temperature: Temperature for output softmax.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
window_size: int = 11
|
|
49
|
+
hidden_dim: int = 64
|
|
50
|
+
num_layers: int = 2
|
|
51
|
+
use_quality: bool = True
|
|
52
|
+
temperature: float = 1.0
|
|
53
|
+
learnable_temperature: bool = True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SoftErrorCorrection(TemperatureOperator):
|
|
57
|
+
"""Differentiable error correction for sequencing reads.
|
|
58
|
+
|
|
59
|
+
This operator uses a neural network to refine base calls based on
|
|
60
|
+
local sequence context and quality scores. It outputs soft base
|
|
61
|
+
probabilities that maintain gradient flow.
|
|
62
|
+
|
|
63
|
+
The algorithm:
|
|
64
|
+
1. For each position, extract a window of sequence and quality data
|
|
65
|
+
2. Pass through MLP to predict corrected base probabilities
|
|
66
|
+
3. Output soft one-hot representation blending original and corrected
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
config: ErrorCorrectionConfig with model parameters.
|
|
70
|
+
rngs: Flax NNX random number generators.
|
|
71
|
+
name: Optional operator name.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
```python
|
|
75
|
+
config = ErrorCorrectionConfig(window_size=11, hidden_dim=64)
|
|
76
|
+
corrector = SoftErrorCorrection(config, rngs=nnx.Rngs(42))
|
|
77
|
+
data = {"sequence": encoded_seq, "quality_scores": quality}
|
|
78
|
+
result, state, meta = corrector.apply(data, {}, None)
|
|
79
|
+
```
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
config: ErrorCorrectionConfig,
|
|
85
|
+
*,
|
|
86
|
+
rngs: nnx.Rngs | None = None,
|
|
87
|
+
name: str | None = None,
|
|
88
|
+
):
|
|
89
|
+
"""Initialize the error correction operator.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
config: Error correction configuration.
|
|
93
|
+
rngs: Random number generators for initialization.
|
|
94
|
+
name: Optional operator name.
|
|
95
|
+
"""
|
|
96
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
97
|
+
|
|
98
|
+
rngs = ensure_rngs(rngs)
|
|
99
|
+
|
|
100
|
+
self.window_size = config.window_size
|
|
101
|
+
self.use_quality = config.use_quality
|
|
102
|
+
|
|
103
|
+
# Input dimension: window_size * (4 alphabet + 1 quality if used)
|
|
104
|
+
alphabet_size = 4
|
|
105
|
+
features_per_position = alphabet_size + (1 if config.use_quality else 0)
|
|
106
|
+
input_dim = config.window_size * features_per_position
|
|
107
|
+
|
|
108
|
+
if config.num_layers > 0:
|
|
109
|
+
self.backbone = MLP(
|
|
110
|
+
hidden_dims=[config.hidden_dim] * config.num_layers,
|
|
111
|
+
in_features=input_dim,
|
|
112
|
+
activation="relu",
|
|
113
|
+
output_activation="relu",
|
|
114
|
+
use_batch_norm=False,
|
|
115
|
+
rngs=rngs,
|
|
116
|
+
)
|
|
117
|
+
out_dim = config.hidden_dim
|
|
118
|
+
else:
|
|
119
|
+
self.backbone = None
|
|
120
|
+
out_dim = input_dim
|
|
121
|
+
|
|
122
|
+
# Output layer (predicts 4 base probabilities)
|
|
123
|
+
self.output_layer = nnx.Linear(in_features=out_dim, out_features=alphabet_size, rngs=rngs)
|
|
124
|
+
|
|
125
|
+
# Temperature is managed by TemperatureOperator via self._temperature
|
|
126
|
+
|
|
127
|
+
# Learnable blending weight (how much to trust correction vs original)
|
|
128
|
+
self.correction_weight = init_learnable_param(0.5)
|
|
129
|
+
|
|
130
|
+
def _extract_window(
|
|
131
|
+
self,
|
|
132
|
+
sequence: Float[Array, "length alphabet"],
|
|
133
|
+
quality_scores: Float[Array, "length"],
|
|
134
|
+
position: Array | int,
|
|
135
|
+
) -> Float[Array, "window_features"]:
|
|
136
|
+
"""Extract feature window around a position.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
sequence: One-hot encoded sequence.
|
|
140
|
+
quality_scores: Quality scores for each position.
|
|
141
|
+
position: Center position for the window.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Flattened feature vector for the window.
|
|
145
|
+
"""
|
|
146
|
+
seq_len = sequence.shape[0]
|
|
147
|
+
half_window = self.window_size // 2
|
|
148
|
+
|
|
149
|
+
# Build window features with padding for edge positions
|
|
150
|
+
def get_position_features(offset: Array | int) -> Float[Array, "features"]:
|
|
151
|
+
"""Extract sequence and quality features at a single window offset."""
|
|
152
|
+
pos = position + offset - half_window
|
|
153
|
+
# Handle boundary conditions with zeros
|
|
154
|
+
in_bounds = (pos >= 0) & (pos < seq_len)
|
|
155
|
+
|
|
156
|
+
# Get sequence features
|
|
157
|
+
clipped_pos = jnp.clip(pos, 0, seq_len - 1)
|
|
158
|
+
slice_val = jax.lax.dynamic_slice(sequence, (clipped_pos, 0), (1, 4))
|
|
159
|
+
seq_features = jnp.where(in_bounds, slice_val.squeeze(0), jnp.zeros(4))
|
|
160
|
+
|
|
161
|
+
if self.use_quality:
|
|
162
|
+
# Get quality feature (normalized to [0, 1])
|
|
163
|
+
qual_feature = jnp.where(
|
|
164
|
+
in_bounds,
|
|
165
|
+
quality_scores[jnp.clip(pos, 0, seq_len - 1)] / 40.0, # Normalize by Q40
|
|
166
|
+
0.0,
|
|
167
|
+
)
|
|
168
|
+
return jnp.concatenate([seq_features, jnp.array([qual_feature])])
|
|
169
|
+
return seq_features
|
|
170
|
+
|
|
171
|
+
# Extract all window positions
|
|
172
|
+
features = jax.vmap(get_position_features)(jnp.arange(self.window_size))
|
|
173
|
+
return features.flatten()
|
|
174
|
+
|
|
175
|
+
def _predict_correction(
|
|
176
|
+
self,
|
|
177
|
+
window_features: Float[Array, "window_features"],
|
|
178
|
+
) -> Float[Array, "alphabet"]:
|
|
179
|
+
"""Predict corrected base probabilities from window features.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
window_features: Flattened window feature vector.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Soft base probabilities (4,).
|
|
186
|
+
"""
|
|
187
|
+
x = window_features
|
|
188
|
+
|
|
189
|
+
if self.backbone is not None:
|
|
190
|
+
hidden: jax.Array = self.backbone(window_features)
|
|
191
|
+
x = hidden
|
|
192
|
+
|
|
193
|
+
# Output layer
|
|
194
|
+
logits = self.output_layer(x)
|
|
195
|
+
|
|
196
|
+
# Apply temperature-scaled softmax
|
|
197
|
+
temp = self._temperature
|
|
198
|
+
probs = jax.nn.softmax(logits / temp)
|
|
199
|
+
|
|
200
|
+
return probs
|
|
201
|
+
|
|
202
|
+
def _correct_position(
|
|
203
|
+
self,
|
|
204
|
+
sequence: Float[Array, "length alphabet"],
|
|
205
|
+
quality_scores: Float[Array, "length"],
|
|
206
|
+
position: Array | int,
|
|
207
|
+
) -> Float[Array, "alphabet"]:
|
|
208
|
+
"""Correct a single position using context.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
sequence: One-hot encoded sequence.
|
|
212
|
+
quality_scores: Quality scores.
|
|
213
|
+
position: Position to correct.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Corrected soft one-hot for this position.
|
|
217
|
+
"""
|
|
218
|
+
# Extract window features
|
|
219
|
+
window = self._extract_window(sequence, quality_scores, position)
|
|
220
|
+
|
|
221
|
+
# Predict correction
|
|
222
|
+
correction = self._predict_correction(window)
|
|
223
|
+
|
|
224
|
+
# Blend with original based on correction weight
|
|
225
|
+
original = sequence[position]
|
|
226
|
+
weight = jax.nn.sigmoid(self.correction_weight[...])
|
|
227
|
+
corrected = weight * correction + (1 - weight) * original
|
|
228
|
+
|
|
229
|
+
# Renormalize to ensure valid probability distribution
|
|
230
|
+
corrected = corrected / (jnp.sum(corrected) + 1e-8)
|
|
231
|
+
|
|
232
|
+
return corrected
|
|
233
|
+
|
|
234
|
+
def apply(
|
|
235
|
+
self,
|
|
236
|
+
data: PyTree,
|
|
237
|
+
state: PyTree,
|
|
238
|
+
metadata: dict[str, Any] | None,
|
|
239
|
+
random_params: Any = None,
|
|
240
|
+
stats: dict[str, Any] | None = None,
|
|
241
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
242
|
+
"""Apply error correction to sequence data.
|
|
243
|
+
|
|
244
|
+
This method corrects each position in the sequence using the
|
|
245
|
+
neural network model, producing soft corrected base probabilities.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
data: Dictionary containing:
|
|
249
|
+
- "sequence": One-hot encoded sequence (length, alphabet_size)
|
|
250
|
+
- "quality_scores": Phred quality scores (length,)
|
|
251
|
+
state: Element state (passed through unchanged)
|
|
252
|
+
metadata: Element metadata (passed through unchanged)
|
|
253
|
+
random_params: Not used (deterministic operator)
|
|
254
|
+
stats: Not used
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Tuple of (transformed_data, state, metadata):
|
|
258
|
+
- transformed_data contains:
|
|
259
|
+
|
|
260
|
+
- "sequence": Corrected soft one-hot sequence
|
|
261
|
+
- "quality_scores": Original quality scores
|
|
262
|
+
- "correction_confidence": Average correction weight
|
|
263
|
+
- state is passed through unchanged
|
|
264
|
+
- metadata is passed through unchanged
|
|
265
|
+
"""
|
|
266
|
+
sequence = data["sequence"]
|
|
267
|
+
quality_scores = data["quality_scores"]
|
|
268
|
+
seq_len = sequence.shape[0]
|
|
269
|
+
|
|
270
|
+
# Correct each position
|
|
271
|
+
def correct_fn(position: Array | int) -> Float[Array, "alphabet"]:
|
|
272
|
+
"""Correct base probabilities at a single position."""
|
|
273
|
+
return self._correct_position(sequence, quality_scores, position)
|
|
274
|
+
|
|
275
|
+
corrected_sequence = jax.vmap(correct_fn)(jnp.arange(seq_len))
|
|
276
|
+
|
|
277
|
+
# Compute confidence metric (based on correction weight)
|
|
278
|
+
confidence = jax.nn.sigmoid(self.correction_weight[...])
|
|
279
|
+
|
|
280
|
+
# Build output data
|
|
281
|
+
transformed_data = {
|
|
282
|
+
"sequence": corrected_sequence,
|
|
283
|
+
"quality_scores": quality_scores,
|
|
284
|
+
"correction_confidence": confidence,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return transformed_data, state, metadata
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Protein structure operators for DiffBio.
|
|
2
|
+
|
|
3
|
+
This module provides differentiable operators for protein structure analysis,
|
|
4
|
+
including secondary structure prediction using the DSSP algorithm.
|
|
5
|
+
|
|
6
|
+
Operators:
|
|
7
|
+
DifferentiableSecondaryStructure: PyDSSP-style secondary structure prediction
|
|
8
|
+
with continuous hydrogen bond matrix for gradient-based optimization.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
```python
|
|
12
|
+
from diffbio.operators.protein import create_secondary_structure_predictor
|
|
13
|
+
predictor = create_secondary_structure_predictor()
|
|
14
|
+
result, _, _ = predictor.apply({"coordinates": coords}, {}, None)
|
|
15
|
+
ss_probs = result["ss_onehot"] # (batch, length, 3)
|
|
16
|
+
```
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from diffbio.operators.protein.secondary_structure import (
|
|
20
|
+
DifferentiableSecondaryStructure,
|
|
21
|
+
SecondaryStructureConfig,
|
|
22
|
+
compute_hydrogen_position,
|
|
23
|
+
create_secondary_structure_predictor,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"DifferentiableSecondaryStructure",
|
|
28
|
+
"SecondaryStructureConfig",
|
|
29
|
+
"compute_hydrogen_position",
|
|
30
|
+
"create_secondary_structure_predictor",
|
|
31
|
+
]
|