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,222 @@
|
|
|
1
|
+
"""Alignment loss functions for differentiable sequence alignment.
|
|
2
|
+
|
|
3
|
+
This module provides loss functions for training differentiable alignment
|
|
4
|
+
models, including alignment score losses, soft edit distance, and
|
|
5
|
+
alignment consistency losses for multi-sequence alignment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import jax
|
|
9
|
+
import jax.numpy as jnp
|
|
10
|
+
from flax import nnx
|
|
11
|
+
from jaxtyping import Array, Float
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AlignmentScoreLoss(nnx.Module):
|
|
15
|
+
"""Loss function based on alignment quality score.
|
|
16
|
+
|
|
17
|
+
Computes a loss that measures how well the alignment captures
|
|
18
|
+
the similarity between two sequences. Lower loss indicates
|
|
19
|
+
better alignment of similar positions.
|
|
20
|
+
|
|
21
|
+
The loss computes the weighted sum of position-wise mismatches,
|
|
22
|
+
where weights come from the soft alignment matrix.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
rngs: Flax NNX random number generators.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, *, rngs: nnx.Rngs | None = None):
|
|
29
|
+
"""Initialize alignment score loss.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
rngs: Random number generators (optional).
|
|
33
|
+
"""
|
|
34
|
+
super().__init__()
|
|
35
|
+
|
|
36
|
+
def __call__(
|
|
37
|
+
self,
|
|
38
|
+
seq1: Float[Array, "len1 alphabet"],
|
|
39
|
+
seq2: Float[Array, "len2 alphabet"],
|
|
40
|
+
alignment: Float[Array, "len1 len2"],
|
|
41
|
+
) -> Float[Array, ""]:
|
|
42
|
+
"""Compute alignment score loss.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
seq1: First sequence, soft one-hot encoded (len1, alphabet).
|
|
46
|
+
seq2: Second sequence, soft one-hot encoded (len2, alphabet).
|
|
47
|
+
alignment: Soft alignment matrix where alignment[i,j] indicates
|
|
48
|
+
probability of aligning position i to position j.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Scalar loss value. Lower is better alignment.
|
|
52
|
+
"""
|
|
53
|
+
# Compute position-wise similarity: seq1[i] dot seq2[j]
|
|
54
|
+
# Higher similarity when same nucleotide
|
|
55
|
+
similarity = jnp.einsum("ia,ja->ij", seq1, seq2)
|
|
56
|
+
|
|
57
|
+
# Weight by alignment probabilities
|
|
58
|
+
# High alignment probability * high similarity = good
|
|
59
|
+
weighted_similarity = jnp.sum(alignment * similarity)
|
|
60
|
+
|
|
61
|
+
# Convert to loss (negate similarity, normalize)
|
|
62
|
+
max_possible = jnp.sum(alignment) # If all positions perfectly matched
|
|
63
|
+
loss = 1.0 - (weighted_similarity / jnp.maximum(max_possible, 1e-8))
|
|
64
|
+
|
|
65
|
+
return loss
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SoftEditDistanceLoss(nnx.Module):
|
|
69
|
+
"""Differentiable approximation of edit distance.
|
|
70
|
+
|
|
71
|
+
Computes a soft version of edit distance between two sequences
|
|
72
|
+
that allows gradient flow. Uses the relationship between
|
|
73
|
+
edit distance and alignment scores.
|
|
74
|
+
|
|
75
|
+
The edit distance is approximated as the complement of the
|
|
76
|
+
optimal alignment score, scaled appropriately.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
normalize: Whether to normalize by sequence length.
|
|
80
|
+
temperature: Temperature for soft minimum operations.
|
|
81
|
+
rngs: Flax NNX random number generators.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
normalize: bool = False,
|
|
87
|
+
temperature: float = 0.1,
|
|
88
|
+
*,
|
|
89
|
+
rngs: nnx.Rngs | None = None,
|
|
90
|
+
):
|
|
91
|
+
"""Initialize soft edit distance loss.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
normalize: Whether to normalize by sequence length.
|
|
95
|
+
temperature: Temperature for softmax operations. Lower values
|
|
96
|
+
give sharper approximation of true edit distance.
|
|
97
|
+
Default 0.1 works well for one-hot encoded sequences.
|
|
98
|
+
rngs: Random number generators (optional).
|
|
99
|
+
"""
|
|
100
|
+
super().__init__()
|
|
101
|
+
self.normalize = normalize
|
|
102
|
+
self.temperature = nnx.Param(jnp.array(temperature))
|
|
103
|
+
|
|
104
|
+
def __call__(
|
|
105
|
+
self,
|
|
106
|
+
seq1: Float[Array, "len1 alphabet"],
|
|
107
|
+
seq2: Float[Array, "len2 alphabet"],
|
|
108
|
+
) -> Float[Array, ""]:
|
|
109
|
+
"""Compute soft edit distance between sequences.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
seq1: First sequence, soft one-hot encoded (len1, alphabet).
|
|
113
|
+
seq2: Second sequence, soft one-hot encoded (len2, alphabet).
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Scalar soft edit distance. 0 for identical sequences.
|
|
117
|
+
"""
|
|
118
|
+
len1, len2 = seq1.shape[0], seq2.shape[0]
|
|
119
|
+
|
|
120
|
+
# Compute position-wise similarity matrix
|
|
121
|
+
# similarity[i,j] = probability that seq1[i] matches seq2[j]
|
|
122
|
+
similarity = jnp.einsum("ia,ja->ij", seq1, seq2)
|
|
123
|
+
|
|
124
|
+
temp = self.temperature[...]
|
|
125
|
+
|
|
126
|
+
# Use soft assignment to find best match per position
|
|
127
|
+
# Softmax over similarities gives assignment weights
|
|
128
|
+
soft_assign_row = jax.nn.softmax(similarity / temp, axis=1)
|
|
129
|
+
soft_assign_col = jax.nn.softmax(similarity / temp, axis=0)
|
|
130
|
+
|
|
131
|
+
# Compute expected similarity under soft assignment
|
|
132
|
+
# For identical sequences: assignment concentrates on diagonal (sim=1)
|
|
133
|
+
# For different sequences: assignment spread out (sim=0 everywhere)
|
|
134
|
+
expected_sim_row = jnp.sum(soft_assign_row * similarity, axis=1)
|
|
135
|
+
expected_sim_col = jnp.sum(soft_assign_col * similarity, axis=0)
|
|
136
|
+
|
|
137
|
+
# Total match score = sum of best similarities per position
|
|
138
|
+
match_score_row = jnp.sum(expected_sim_row)
|
|
139
|
+
match_score_col = jnp.sum(expected_sim_col)
|
|
140
|
+
|
|
141
|
+
# Distance = unmatched positions
|
|
142
|
+
# Average of (len - match_score) from both perspectives
|
|
143
|
+
dist_row = len1 - match_score_row
|
|
144
|
+
dist_col = len2 - match_score_col
|
|
145
|
+
total_distance = (dist_row + dist_col) / 2.0
|
|
146
|
+
|
|
147
|
+
# Ensure non-negative (numerical precision)
|
|
148
|
+
total_distance = jnp.maximum(total_distance, 0.0)
|
|
149
|
+
|
|
150
|
+
# Add length difference penalty
|
|
151
|
+
length_penalty = jnp.abs(len1 - len2).astype(jnp.float32)
|
|
152
|
+
total_distance = total_distance + length_penalty
|
|
153
|
+
|
|
154
|
+
if self.normalize:
|
|
155
|
+
# Normalize by total length
|
|
156
|
+
total_distance = total_distance / (len1 + len2)
|
|
157
|
+
|
|
158
|
+
return total_distance
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class AlignmentConsistencyLoss(nnx.Module):
|
|
162
|
+
"""Loss for enforcing transitivity in multi-sequence alignments.
|
|
163
|
+
|
|
164
|
+
For three sequences A, B, C with pairwise alignments:
|
|
165
|
+
- A->B (align_ab)
|
|
166
|
+
- B->C (align_bc)
|
|
167
|
+
- A->C (align_ac)
|
|
168
|
+
|
|
169
|
+
The alignments are consistent if: align_ac ≈ align_ab @ align_bc
|
|
170
|
+
|
|
171
|
+
This loss penalizes violations of this transitivity property,
|
|
172
|
+
which is important for producing coherent multiple sequence alignments.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
rngs: Flax NNX random number generators.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(self, *, rngs: nnx.Rngs | None = None):
|
|
179
|
+
"""Initialize alignment consistency loss.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
rngs: Random number generators (optional).
|
|
183
|
+
"""
|
|
184
|
+
super().__init__()
|
|
185
|
+
|
|
186
|
+
def __call__(
|
|
187
|
+
self,
|
|
188
|
+
align_ab: Float[Array, "len_a len_b"],
|
|
189
|
+
align_bc: Float[Array, "len_b len_c"],
|
|
190
|
+
align_ac: Float[Array, "len_a len_c"],
|
|
191
|
+
) -> Float[Array, ""]:
|
|
192
|
+
"""Compute alignment consistency loss.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
align_ab: Soft alignment from sequence A to B.
|
|
196
|
+
align_bc: Soft alignment from sequence B to C.
|
|
197
|
+
align_ac: Soft alignment from sequence A to C.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Scalar loss measuring transitivity violation.
|
|
201
|
+
"""
|
|
202
|
+
# Compute expected A->C alignment through B
|
|
203
|
+
# align_ac_expected[i,k] = sum_j align_ab[i,j] * align_bc[j,k]
|
|
204
|
+
align_ac_expected = jnp.matmul(align_ab, align_bc)
|
|
205
|
+
|
|
206
|
+
# Normalize to make it a proper probability distribution
|
|
207
|
+
align_ac_expected = align_ac_expected / jnp.maximum(
|
|
208
|
+
jnp.sum(align_ac_expected, axis=1, keepdims=True), 1e-8
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Compute KL divergence between expected and actual A->C alignment
|
|
212
|
+
# KL(actual || expected) = sum(actual * log(actual / expected))
|
|
213
|
+
eps = 1e-8
|
|
214
|
+
kl_div = jnp.sum(align_ac * jnp.log((align_ac + eps) / (align_ac_expected + eps)))
|
|
215
|
+
|
|
216
|
+
# Also compute reverse KL for symmetry
|
|
217
|
+
kl_div_reverse = jnp.sum(
|
|
218
|
+
align_ac_expected * jnp.log((align_ac_expected + eps) / (align_ac + eps))
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Return symmetric KL (Jensen-Shannon style)
|
|
222
|
+
return (kl_div + kl_div_reverse) / 2.0
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Biological regularization losses for differentiable bioinformatics.
|
|
2
|
+
|
|
3
|
+
This module provides regularization losses that help prevent adversarial
|
|
4
|
+
optimization of differentiable bioinformatics components. These losses
|
|
5
|
+
encourage biologically plausible sequences and alignments.
|
|
6
|
+
|
|
7
|
+
Reference:
|
|
8
|
+
Petti et al. (2023) observed that purely differentiable alignment can
|
|
9
|
+
produce biologically implausible solutions without proper regularization.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
import jax.numpy as jnp
|
|
15
|
+
from flax import nnx
|
|
16
|
+
from jaxtyping import Array, Float
|
|
17
|
+
|
|
18
|
+
from diffbio.core import soft_ops
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class BiologicalRegularizationConfig:
|
|
23
|
+
"""Configuration for biological regularization losses.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
gc_content_weight: Weight for GC content regularization.
|
|
27
|
+
gap_pattern_weight: Weight for gap pattern regularization.
|
|
28
|
+
complexity_weight: Weight for sequence complexity loss.
|
|
29
|
+
target_gc_content: Target GC content (typically 0.4-0.6).
|
|
30
|
+
target_gc_tolerance: Tolerance around target GC content.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
gc_content_weight: float = 1.0
|
|
34
|
+
gap_pattern_weight: float = 1.0
|
|
35
|
+
complexity_weight: float = 1.0
|
|
36
|
+
target_gc_content: float = 0.5
|
|
37
|
+
target_gc_tolerance: float = 0.2
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GCContentRegularization(nnx.Module):
|
|
41
|
+
"""Regularization loss for GC content.
|
|
42
|
+
|
|
43
|
+
Penalizes sequences with GC content far from biological norms.
|
|
44
|
+
For most organisms, GC content ranges from 25% to 75%.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
target_gc: Target GC content (default 0.5 for balanced).
|
|
48
|
+
tolerance: Tolerance around target before penalizing.
|
|
49
|
+
rngs: Flax NNX random number generators.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
target_gc: float = 0.5,
|
|
55
|
+
tolerance: float = 0.2,
|
|
56
|
+
*,
|
|
57
|
+
rngs: nnx.Rngs | None = None,
|
|
58
|
+
):
|
|
59
|
+
"""Initialize GC content regularization.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
target_gc: Target GC content.
|
|
63
|
+
tolerance: Tolerance around target.
|
|
64
|
+
rngs: Random number generators (optional).
|
|
65
|
+
"""
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.target_gc = nnx.Param(jnp.array(target_gc))
|
|
68
|
+
self.tolerance = nnx.Param(jnp.array(tolerance))
|
|
69
|
+
|
|
70
|
+
def __call__(
|
|
71
|
+
self,
|
|
72
|
+
sequence: Float[Array, "length alphabet"],
|
|
73
|
+
) -> Float[Array, ""]:
|
|
74
|
+
"""Compute GC content regularization loss.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
sequence: Soft one-hot encoded sequence (length, alphabet_size).
|
|
78
|
+
Assumes alphabet order: A, C, G, T (indices 0, 1, 2, 3).
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Scalar loss penalizing deviation from target GC content.
|
|
82
|
+
"""
|
|
83
|
+
# GC content = sum of C and G probabilities
|
|
84
|
+
# C is index 1, G is index 2
|
|
85
|
+
gc_content = jnp.mean(sequence[:, 1] + sequence[:, 2])
|
|
86
|
+
|
|
87
|
+
# Compute deviation from target
|
|
88
|
+
target = self.target_gc[...]
|
|
89
|
+
tolerance = self.tolerance[...]
|
|
90
|
+
|
|
91
|
+
# Soft penalty: quadratic beyond tolerance
|
|
92
|
+
deviation = jnp.abs(gc_content - target)
|
|
93
|
+
excess = soft_ops.relu(deviation - tolerance, softness=0.1)
|
|
94
|
+
|
|
95
|
+
return excess**2
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class GapPatternRegularization(nnx.Module):
|
|
99
|
+
"""Regularization loss for gap patterns in alignments.
|
|
100
|
+
|
|
101
|
+
Penalizes unrealistic gap patterns such as:
|
|
102
|
+
- Very long consecutive gaps
|
|
103
|
+
- Many scattered small gaps
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
max_gap_length: Maximum expected gap length before penalizing.
|
|
107
|
+
rngs: Flax NNX random number generators.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
max_gap_length: int = 10,
|
|
113
|
+
*,
|
|
114
|
+
rngs: nnx.Rngs | None = None,
|
|
115
|
+
):
|
|
116
|
+
"""Initialize gap pattern regularization.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
max_gap_length: Maximum expected gap length.
|
|
120
|
+
rngs: Random number generators (optional).
|
|
121
|
+
"""
|
|
122
|
+
super().__init__()
|
|
123
|
+
self.max_gap_length = max_gap_length
|
|
124
|
+
|
|
125
|
+
def __call__(
|
|
126
|
+
self,
|
|
127
|
+
alignment_weights: Float[Array, "len1 len2"],
|
|
128
|
+
) -> Float[Array, ""]:
|
|
129
|
+
"""Compute gap pattern regularization loss.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
alignment_weights: Soft alignment matrix where entry (i,j)
|
|
133
|
+
indicates probability of aligning position i to j.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Scalar loss penalizing unrealistic gap patterns.
|
|
137
|
+
"""
|
|
138
|
+
# Compute row-wise and column-wise alignment strengths
|
|
139
|
+
row_aligned = soft_ops.max(alignment_weights, axis=1, softness=0.1)
|
|
140
|
+
col_aligned = soft_ops.max(alignment_weights, axis=0, softness=0.1)
|
|
141
|
+
|
|
142
|
+
# Penalize positions with very low alignment probability (gaps)
|
|
143
|
+
# Using smooth measure of "gappiness"
|
|
144
|
+
row_gap_penalty = jnp.mean(1.0 - row_aligned)
|
|
145
|
+
col_gap_penalty = jnp.mean(1.0 - col_aligned)
|
|
146
|
+
|
|
147
|
+
# Also penalize non-monotonic alignments (jumps)
|
|
148
|
+
# A good alignment should roughly follow the diagonal
|
|
149
|
+
len1, len2 = alignment_weights.shape
|
|
150
|
+
expected_diag = jnp.linspace(0, len2 - 1, len1)
|
|
151
|
+
|
|
152
|
+
# Compute weighted average position for each row
|
|
153
|
+
positions = jnp.arange(len2)
|
|
154
|
+
weighted_pos = jnp.sum(alignment_weights * positions[None, :], axis=1) / jnp.maximum(
|
|
155
|
+
jnp.sum(alignment_weights, axis=1), 1e-8
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Penalize deviation from expected diagonal progression
|
|
159
|
+
diag_penalty = jnp.mean((weighted_pos - expected_diag) ** 2) / (len2**2)
|
|
160
|
+
|
|
161
|
+
return row_gap_penalty + col_gap_penalty + diag_penalty
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class SequenceComplexityLoss(nnx.Module):
|
|
165
|
+
"""Regularization loss for sequence complexity.
|
|
166
|
+
|
|
167
|
+
Penalizes low-complexity sequences that might arise from adversarial
|
|
168
|
+
optimization (e.g., all-A sequences, repetitive patterns).
|
|
169
|
+
|
|
170
|
+
Uses entropy as a measure of complexity.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
min_entropy: Minimum expected entropy per position.
|
|
174
|
+
rngs: Flax NNX random number generators.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
min_entropy: float = 1.0,
|
|
180
|
+
*,
|
|
181
|
+
rngs: nnx.Rngs | None = None,
|
|
182
|
+
):
|
|
183
|
+
"""Initialize sequence complexity loss.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
min_entropy: Minimum expected entropy.
|
|
187
|
+
rngs: Random number generators (optional).
|
|
188
|
+
"""
|
|
189
|
+
super().__init__()
|
|
190
|
+
self.min_entropy = nnx.Param(jnp.array(min_entropy))
|
|
191
|
+
|
|
192
|
+
def __call__(
|
|
193
|
+
self,
|
|
194
|
+
sequence: Float[Array, "length alphabet"],
|
|
195
|
+
) -> Float[Array, ""]:
|
|
196
|
+
"""Compute sequence complexity loss.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
sequence: Soft one-hot encoded sequence (length, alphabet_size).
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Scalar loss penalizing low-complexity sequences.
|
|
203
|
+
"""
|
|
204
|
+
# Compute per-position entropy
|
|
205
|
+
# Add small epsilon for numerical stability
|
|
206
|
+
eps = 1e-8
|
|
207
|
+
entropy = -jnp.sum(sequence * jnp.log(sequence + eps), axis=-1)
|
|
208
|
+
|
|
209
|
+
# Average entropy across positions
|
|
210
|
+
avg_entropy = jnp.mean(entropy)
|
|
211
|
+
|
|
212
|
+
# Penalize if entropy is below minimum
|
|
213
|
+
min_ent = self.min_entropy[...]
|
|
214
|
+
deficit = jnp.maximum(min_ent - avg_entropy, 0.0)
|
|
215
|
+
|
|
216
|
+
return deficit**2
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class BiologicalPlausibilityLoss(nnx.Module):
|
|
220
|
+
"""Combined biological plausibility regularization.
|
|
221
|
+
|
|
222
|
+
Combines multiple regularization terms to encourage biologically
|
|
223
|
+
plausible sequences and alignments during differentiable optimization.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
config: BiologicalRegularizationConfig with weights and targets.
|
|
227
|
+
rngs: Flax NNX random number generators.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def __init__(
|
|
231
|
+
self,
|
|
232
|
+
config: BiologicalRegularizationConfig,
|
|
233
|
+
*,
|
|
234
|
+
rngs: nnx.Rngs | None = None,
|
|
235
|
+
):
|
|
236
|
+
"""Initialize combined biological plausibility loss.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
config: Configuration with weights and targets.
|
|
240
|
+
rngs: Random number generators (optional).
|
|
241
|
+
"""
|
|
242
|
+
super().__init__()
|
|
243
|
+
self.config = config
|
|
244
|
+
|
|
245
|
+
# Initialize component losses
|
|
246
|
+
self.gc_loss = GCContentRegularization(
|
|
247
|
+
target_gc=config.target_gc_content,
|
|
248
|
+
tolerance=config.target_gc_tolerance,
|
|
249
|
+
rngs=rngs,
|
|
250
|
+
)
|
|
251
|
+
self.complexity_loss = SequenceComplexityLoss(
|
|
252
|
+
min_entropy=1.0,
|
|
253
|
+
rngs=rngs,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def __call__(
|
|
257
|
+
self,
|
|
258
|
+
sequence: Float[Array, "length alphabet"],
|
|
259
|
+
alignment_weights: Float[Array, "len1 len2"] | None = None,
|
|
260
|
+
) -> Float[Array, ""]:
|
|
261
|
+
"""Compute combined biological plausibility loss.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
sequence: Soft one-hot encoded sequence.
|
|
265
|
+
alignment_weights: Optional soft alignment matrix.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Scalar combined regularization loss.
|
|
269
|
+
"""
|
|
270
|
+
total_loss = jnp.array(0.0)
|
|
271
|
+
|
|
272
|
+
# GC content regularization
|
|
273
|
+
if self.config.gc_content_weight > 0:
|
|
274
|
+
gc_loss = self.gc_loss(sequence)
|
|
275
|
+
total_loss = total_loss + self.config.gc_content_weight * gc_loss
|
|
276
|
+
|
|
277
|
+
# Sequence complexity regularization
|
|
278
|
+
if self.config.complexity_weight > 0:
|
|
279
|
+
complexity_loss = self.complexity_loss(sequence)
|
|
280
|
+
total_loss = total_loss + self.config.complexity_weight * complexity_loss
|
|
281
|
+
|
|
282
|
+
# Gap pattern regularization (if alignment provided)
|
|
283
|
+
if alignment_weights is not None and self.config.gap_pattern_weight > 0:
|
|
284
|
+
gap_loss_fn = GapPatternRegularization(rngs=None)
|
|
285
|
+
gap_loss = gap_loss_fn(alignment_weights)
|
|
286
|
+
total_loss = total_loss + self.config.gap_pattern_weight * gap_loss
|
|
287
|
+
|
|
288
|
+
return total_loss
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Differentiable metric-based loss functions.
|
|
2
|
+
|
|
3
|
+
This module provides differentiable approximations of evaluation metrics
|
|
4
|
+
so they can be used as training objectives with gradient-based optimizers,
|
|
5
|
+
plus exact evaluation metrics backed by calibrax.
|
|
6
|
+
|
|
7
|
+
Includes:
|
|
8
|
+
- DifferentiableAUROC: Sigmoid-approximated Area Under the ROC Curve (training)
|
|
9
|
+
- ExactAUROC: Trapezoidal-rule AUROC via calibrax (evaluation)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import jax
|
|
13
|
+
import jax.numpy as jnp
|
|
14
|
+
from calibrax.metrics.functional.classification import roc_auc
|
|
15
|
+
from flax import nnx
|
|
16
|
+
from jaxtyping import Array, Float
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DifferentiableAUROC(nnx.Module):
|
|
20
|
+
"""Differentiable approximation of the Area Under the ROC Curve.
|
|
21
|
+
|
|
22
|
+
This is a smooth training surrogate. For exact AUROC evaluation use
|
|
23
|
+
:class:`ExactAUROC`, which delegates to calibrax's trapezoidal-rule
|
|
24
|
+
implementation.
|
|
25
|
+
|
|
26
|
+
Approximates AUROC by replacing the hard indicator in the Wilcoxon-Mann-Whitney
|
|
27
|
+
statistic with a sigmoid function, making it fully differentiable and
|
|
28
|
+
JIT-compatible.
|
|
29
|
+
|
|
30
|
+
For every (positive, negative) pair the hard AUROC checks whether the
|
|
31
|
+
positive score exceeds the negative score. This module replaces that
|
|
32
|
+
indicator with ``sigmoid((pos - neg) / temperature)``, yielding a smooth
|
|
33
|
+
surrogate whose gradient can drive optimisation.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
temperature: Controls sharpness of the sigmoid approximation.
|
|
37
|
+
Lower values approach the hard indicator; higher values give
|
|
38
|
+
smoother gradients. Default 1.0.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
```python
|
|
42
|
+
auroc_loss = DifferentiableAUROC(temperature=1.0)
|
|
43
|
+
predictions = jnp.array([0.9, 0.8, 0.1, 0.2])
|
|
44
|
+
labels = jnp.array([1.0, 1.0, 0.0, 0.0])
|
|
45
|
+
value = auroc_loss(predictions, labels)
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, temperature: float = 1.0) -> None:
|
|
50
|
+
"""Initialise the differentiable AUROC loss.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
temperature: Sigmoid temperature. Lower values produce a
|
|
54
|
+
sharper (closer to hard) approximation.
|
|
55
|
+
"""
|
|
56
|
+
super().__init__()
|
|
57
|
+
self.temperature = nnx.Param(jnp.array(temperature))
|
|
58
|
+
|
|
59
|
+
def __call__(
|
|
60
|
+
self,
|
|
61
|
+
predictions: Float[Array, " n"],
|
|
62
|
+
labels: Float[Array, " n"],
|
|
63
|
+
) -> Float[Array, ""]:
|
|
64
|
+
"""Compute the differentiable AUROC approximation.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
predictions: Model output scores, shape ``(n,)``.
|
|
68
|
+
labels: Binary ground-truth labels (0 or 1), shape ``(n,)``.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Scalar AUROC approximation in ``[0, 1]``.
|
|
72
|
+
"""
|
|
73
|
+
temp = self.temperature[...]
|
|
74
|
+
|
|
75
|
+
pos_mask = labels == 1 # (n,)
|
|
76
|
+
neg_mask = labels == 0 # (n,)
|
|
77
|
+
|
|
78
|
+
n_pos = jnp.sum(pos_mask)
|
|
79
|
+
n_neg = jnp.sum(neg_mask)
|
|
80
|
+
|
|
81
|
+
# Pairwise differences: pos_scores[:, None] - neg_scores[None, :]
|
|
82
|
+
# Built via broadcasting with masks to stay JIT-compatible.
|
|
83
|
+
# pos_vals[i] = predictions[i] where label==1, else 0
|
|
84
|
+
pos_vals = jnp.where(pos_mask, predictions, 0.0) # (n,)
|
|
85
|
+
neg_vals = jnp.where(neg_mask, predictions, 0.0) # (n,)
|
|
86
|
+
|
|
87
|
+
# Outer difference over all (i, j) pairs
|
|
88
|
+
diffs = pos_vals[:, None] - neg_vals[None, :] # (n, n)
|
|
89
|
+
|
|
90
|
+
# Mask to select only valid (positive_i, negative_j) pairs
|
|
91
|
+
pair_mask = pos_mask[:, None] & neg_mask[None, :] # (n, n)
|
|
92
|
+
|
|
93
|
+
sigmoid_diffs = jax.nn.sigmoid(diffs / temp) # (n, n)
|
|
94
|
+
|
|
95
|
+
# Mean over valid pairs
|
|
96
|
+
auroc = jnp.sum(sigmoid_diffs * pair_mask) / jnp.maximum(n_pos * n_neg, 1.0)
|
|
97
|
+
|
|
98
|
+
return auroc
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ExactAUROC(nnx.Module):
|
|
102
|
+
"""Exact AUROC metric using calibrax's trapezoidal-rule implementation.
|
|
103
|
+
|
|
104
|
+
Delegates to :func:`calibrax.metrics.functional.classification.roc_auc`
|
|
105
|
+
to compute the exact Area Under the ROC Curve via threshold-sweep and
|
|
106
|
+
the trapezoidal rule.
|
|
107
|
+
|
|
108
|
+
Use this for evaluation; use :class:`DifferentiableAUROC` for training
|
|
109
|
+
(the sorting-based trapezoidal rule has zero gradients w.r.t. predictions
|
|
110
|
+
because ``argsort`` is not differentiable).
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
```python
|
|
114
|
+
exact = ExactAUROC()
|
|
115
|
+
predictions = jnp.array([0.9, 0.8, 0.1, 0.2])
|
|
116
|
+
labels = jnp.array([1.0, 1.0, 0.0, 0.0])
|
|
117
|
+
value = exact(predictions, labels) # 1.0
|
|
118
|
+
```
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self) -> None:
|
|
122
|
+
"""Initialise the exact AUROC metric (no learnable parameters)."""
|
|
123
|
+
super().__init__()
|
|
124
|
+
|
|
125
|
+
def __call__(
|
|
126
|
+
self,
|
|
127
|
+
predictions: Float[Array, " n"],
|
|
128
|
+
labels: Float[Array, " n"],
|
|
129
|
+
) -> Float[Array, ""]:
|
|
130
|
+
"""Compute the exact AUROC via calibrax.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
predictions: Model output scores, shape ``(n,)``.
|
|
134
|
+
labels: Binary ground-truth labels (0 or 1), shape ``(n,)``.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Scalar AUROC in ``[0, 1]``.
|
|
138
|
+
"""
|
|
139
|
+
return roc_auc(predictions, labels)
|