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,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
+ ]