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.
Files changed (202) hide show
  1. diffbio/__init__.py +39 -0
  2. diffbio/configs.py +75 -0
  3. diffbio/constants.py +204 -0
  4. diffbio/core/__init__.py +127 -0
  5. diffbio/core/base_operators.py +612 -0
  6. diffbio/core/data_types.py +260 -0
  7. diffbio/core/gnn_components.py +629 -0
  8. diffbio/core/graph_utils.py +149 -0
  9. diffbio/core/neural_components.py +270 -0
  10. diffbio/core/optimal_transport.py +133 -0
  11. diffbio/core/soft_ops/__init__.py +216 -0
  12. diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
  13. diffbio/core/soft_ops/_projections_simplex.py +240 -0
  14. diffbio/core/soft_ops/_projections_transport.py +508 -0
  15. diffbio/core/soft_ops/_sorting_network.py +204 -0
  16. diffbio/core/soft_ops/_types.py +15 -0
  17. diffbio/core/soft_ops/_utils.py +342 -0
  18. diffbio/core/soft_ops/autograd_safe.py +120 -0
  19. diffbio/core/soft_ops/comparison.py +235 -0
  20. diffbio/core/soft_ops/elementwise.py +309 -0
  21. diffbio/core/soft_ops/logical.py +146 -0
  22. diffbio/core/soft_ops/quantile.py +376 -0
  23. diffbio/core/soft_ops/selection.py +236 -0
  24. diffbio/core/soft_ops/sorting.py +926 -0
  25. diffbio/core/soft_ops/straight_through.py +261 -0
  26. diffbio/core/uncertainty.py +279 -0
  27. diffbio/evaluation/__init__.py +42 -0
  28. diffbio/evaluation/adapters.py +409 -0
  29. diffbio/evaluation/graders.py +223 -0
  30. diffbio/evaluation/problem.py +157 -0
  31. diffbio/evaluation/runner.py +277 -0
  32. diffbio/losses/__init__.py +59 -0
  33. diffbio/losses/alignment_losses.py +222 -0
  34. diffbio/losses/biological_regularization.py +288 -0
  35. diffbio/losses/metric_losses.py +139 -0
  36. diffbio/losses/singlecell_losses.py +387 -0
  37. diffbio/losses/statistical_losses.py +345 -0
  38. diffbio/operators/__init__.py +60 -0
  39. diffbio/operators/_count_vae.py +197 -0
  40. diffbio/operators/_loss_balancing.py +65 -0
  41. diffbio/operators/_masked_gene_transformer.py +118 -0
  42. diffbio/operators/_transformer_validation.py +50 -0
  43. diffbio/operators/alignment/__init__.py +51 -0
  44. diffbio/operators/alignment/profile_hmm.py +350 -0
  45. diffbio/operators/alignment/scoring.py +127 -0
  46. diffbio/operators/alignment/smith_waterman.py +261 -0
  47. diffbio/operators/alignment/soft_msa.py +419 -0
  48. diffbio/operators/assembly/__init__.py +27 -0
  49. diffbio/operators/assembly/gnn_assembly.py +252 -0
  50. diffbio/operators/assembly/metagenomic_binning.py +296 -0
  51. diffbio/operators/crispr/__init__.py +17 -0
  52. diffbio/operators/crispr/guide_scoring.py +269 -0
  53. diffbio/operators/drug_discovery/__init__.py +133 -0
  54. diffbio/operators/drug_discovery/_graph_utils.py +142 -0
  55. diffbio/operators/drug_discovery/admet_predictor.py +285 -0
  56. diffbio/operators/drug_discovery/attentive_fp.py +411 -0
  57. diffbio/operators/drug_discovery/dti.py +261 -0
  58. diffbio/operators/drug_discovery/fingerprint.py +490 -0
  59. diffbio/operators/drug_discovery/maccs_keys.py +267 -0
  60. diffbio/operators/drug_discovery/message_passing.py +200 -0
  61. diffbio/operators/drug_discovery/primitives.py +242 -0
  62. diffbio/operators/drug_discovery/property_predictor.py +163 -0
  63. diffbio/operators/drug_discovery/similarity.py +193 -0
  64. diffbio/operators/epigenomics/__init__.py +35 -0
  65. diffbio/operators/epigenomics/chromatin_state.py +491 -0
  66. diffbio/operators/epigenomics/contextual.py +288 -0
  67. diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
  68. diffbio/operators/epigenomics/peak_calling.py +555 -0
  69. diffbio/operators/foundation_models/__init__.py +119 -0
  70. diffbio/operators/foundation_models/adapters.py +114 -0
  71. diffbio/operators/foundation_models/contracts.py +245 -0
  72. diffbio/operators/foundation_models/embedding_probe.py +83 -0
  73. diffbio/operators/foundation_models/experimental.py +128 -0
  74. diffbio/operators/foundation_models/foundation_model.py +332 -0
  75. diffbio/operators/foundation_models/frozen.py +59 -0
  76. diffbio/operators/foundation_models/precomputed.py +270 -0
  77. diffbio/operators/foundation_models/transformer_encoder.py +564 -0
  78. diffbio/operators/mapping/__init__.py +17 -0
  79. diffbio/operators/mapping/neural_mapper.py +493 -0
  80. diffbio/operators/metabolomics/__init__.py +39 -0
  81. diffbio/operators/metabolomics/spectral_similarity.py +315 -0
  82. diffbio/operators/molecular_dynamics/__init__.py +51 -0
  83. diffbio/operators/molecular_dynamics/force_field.py +265 -0
  84. diffbio/operators/molecular_dynamics/integrator.py +304 -0
  85. diffbio/operators/molecular_dynamics/primitives.py +115 -0
  86. diffbio/operators/multiomics/__init__.py +38 -0
  87. diffbio/operators/multiomics/hic_contact.py +377 -0
  88. diffbio/operators/multiomics/multiomics_vae.py +325 -0
  89. diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
  90. diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
  91. diffbio/operators/normalization/__init__.py +42 -0
  92. diffbio/operators/normalization/embedding.py +222 -0
  93. diffbio/operators/normalization/phate.py +400 -0
  94. diffbio/operators/normalization/umap.py +261 -0
  95. diffbio/operators/normalization/vae_normalizer.py +258 -0
  96. diffbio/operators/population/__init__.py +17 -0
  97. diffbio/operators/population/ancestry_estimation.py +274 -0
  98. diffbio/operators/preprocessing/__init__.py +76 -0
  99. diffbio/operators/preprocessing/adapter_removal.py +311 -0
  100. diffbio/operators/preprocessing/duplicate_filter.py +317 -0
  101. diffbio/operators/preprocessing/error_correction.py +287 -0
  102. diffbio/operators/protein/__init__.py +31 -0
  103. diffbio/operators/protein/secondary_structure.py +509 -0
  104. diffbio/operators/quality_filter.py +128 -0
  105. diffbio/operators/rna_structure/__init__.py +35 -0
  106. diffbio/operators/rna_structure/rna_folding.py +509 -0
  107. diffbio/operators/rnaseq/__init__.py +23 -0
  108. diffbio/operators/rnaseq/motif_discovery.py +251 -0
  109. diffbio/operators/rnaseq/splicing_psi.py +216 -0
  110. diffbio/operators/singlecell/__init__.py +193 -0
  111. diffbio/operators/singlecell/ambient_removal.py +333 -0
  112. diffbio/operators/singlecell/archetypes.py +191 -0
  113. diffbio/operators/singlecell/batch_correction.py +288 -0
  114. diffbio/operators/singlecell/cell_annotation.py +519 -0
  115. diffbio/operators/singlecell/communication.py +704 -0
  116. diffbio/operators/singlecell/differential_distribution.py +243 -0
  117. diffbio/operators/singlecell/doublet_detection.py +657 -0
  118. diffbio/operators/singlecell/downsampling.py +166 -0
  119. diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
  120. diffbio/operators/singlecell/grn_inference.py +336 -0
  121. diffbio/operators/singlecell/imputation.py +429 -0
  122. diffbio/operators/singlecell/knockdown_filter.py +176 -0
  123. diffbio/operators/singlecell/ot_trajectory.py +277 -0
  124. diffbio/operators/singlecell/simulation.py +444 -0
  125. diffbio/operators/singlecell/sindy_grn.py +247 -0
  126. diffbio/operators/singlecell/soft_clustering.py +211 -0
  127. diffbio/operators/singlecell/spatial_domains.py +677 -0
  128. diffbio/operators/singlecell/switch_de.py +184 -0
  129. diffbio/operators/singlecell/trajectory.py +447 -0
  130. diffbio/operators/singlecell/velocity.py +361 -0
  131. diffbio/operators/statistical/__init__.py +35 -0
  132. diffbio/operators/statistical/em_quantification.py +260 -0
  133. diffbio/operators/statistical/hmm.py +234 -0
  134. diffbio/operators/statistical/nb_glm.py +272 -0
  135. diffbio/operators/variant/__init__.py +64 -0
  136. diffbio/operators/variant/classifier.py +333 -0
  137. diffbio/operators/variant/cnn_classifier.py +255 -0
  138. diffbio/operators/variant/cnv_segmentation.py +678 -0
  139. diffbio/operators/variant/deepvariant_pileup.py +426 -0
  140. diffbio/operators/variant/pileup.py +240 -0
  141. diffbio/operators/variant/quality_recalibration.py +274 -0
  142. diffbio/pipelines/__init__.py +65 -0
  143. diffbio/pipelines/differential_expression.py +279 -0
  144. diffbio/pipelines/enhanced_variant_calling.py +326 -0
  145. diffbio/pipelines/perturbation.py +407 -0
  146. diffbio/pipelines/preprocessing.py +267 -0
  147. diffbio/pipelines/single_cell.py +366 -0
  148. diffbio/pipelines/variant_calling.py +490 -0
  149. diffbio/samplers/__init__.py +9 -0
  150. diffbio/samplers/perturbation_sampler.py +142 -0
  151. diffbio/sequences/__init__.py +34 -0
  152. diffbio/sequences/dna.py +239 -0
  153. diffbio/sources/__init__.py +149 -0
  154. diffbio/sources/_anndata_shared.py +89 -0
  155. diffbio/sources/_batch_iteration.py +37 -0
  156. diffbio/sources/_benchmark_source.py +152 -0
  157. diffbio/sources/_indexed_batch_source.py +38 -0
  158. diffbio/sources/_utils.py +45 -0
  159. diffbio/sources/anndata_interop.py +387 -0
  160. diffbio/sources/anndata_source.py +361 -0
  161. diffbio/sources/archive_ii.py +174 -0
  162. diffbio/sources/balifam.py +207 -0
  163. diffbio/sources/bam.py +265 -0
  164. diffbio/sources/bengrn_ground_truth.py +306 -0
  165. diffbio/sources/contextual_epigenomics.py +242 -0
  166. diffbio/sources/dti.py +359 -0
  167. diffbio/sources/embeddings.py +203 -0
  168. diffbio/sources/encode_peaks.py +223 -0
  169. diffbio/sources/fasta.py +226 -0
  170. diffbio/sources/immune_human.py +172 -0
  171. diffbio/sources/indexed_embeddings.py +128 -0
  172. diffbio/sources/indexed_view.py +191 -0
  173. diffbio/sources/molnet.py +493 -0
  174. diffbio/sources/multiomics.py +279 -0
  175. diffbio/sources/pancreas.py +108 -0
  176. diffbio/sources/perturbation/__init__.py +69 -0
  177. diffbio/sources/perturbation/_types.py +51 -0
  178. diffbio/sources/perturbation/_utils.py +125 -0
  179. diffbio/sources/perturbation/concat_source.py +115 -0
  180. diffbio/sources/perturbation/control_mapping.py +215 -0
  181. diffbio/sources/perturbation/experiment_config.py +261 -0
  182. diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
  183. diffbio/sources/perturbation/output_space.py +52 -0
  184. diffbio/sources/perturbation/perturbation_source.py +513 -0
  185. diffbio/sources/seqfish.py +145 -0
  186. diffbio/sources/sequence_foundation.py +68 -0
  187. diffbio/sources/singlecell_foundation.py +68 -0
  188. diffbio/splitters/__init__.py +63 -0
  189. diffbio/splitters/base.py +251 -0
  190. diffbio/splitters/molecular.py +330 -0
  191. diffbio/splitters/perturbation.py +199 -0
  192. diffbio/splitters/random.py +217 -0
  193. diffbio/splitters/sequence.py +201 -0
  194. diffbio/utils/__init__.py +55 -0
  195. diffbio/utils/dependency_runtime.py +115 -0
  196. diffbio/utils/nn_utils.py +157 -0
  197. diffbio/utils/quality.py +45 -0
  198. diffbio/utils/training.py +585 -0
  199. diffbio-0.1.0.dist-info/METADATA +480 -0
  200. diffbio-0.1.0.dist-info/RECORD +202 -0
  201. diffbio-0.1.0.dist-info/WHEEL +4 -0
  202. 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)