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