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,261 @@
1
+ """Smooth Smith-Waterman alignment operator.
2
+
3
+ This module provides a differentiable implementation of the Smith-Waterman
4
+ local alignment algorithm using the logsumexp relaxation (SMURF-style).
5
+
6
+ Key technique: Replace max with logsumexp and argmax with softmax
7
+ to enable gradient flow through the alignment computation.
8
+
9
+ Reference:
10
+ Petti et al. "End-to-end learning of multiple sequence alignments with
11
+ differentiable Smith-Waterman." Bioinformatics 39(1):btac724, 2023.
12
+ """
13
+
14
+ import logging
15
+ from dataclasses import dataclass
16
+ from typing import Any, NamedTuple
17
+
18
+ import jax
19
+ import jax.numpy as jnp
20
+ from flax import nnx
21
+ from jaxtyping import Array, Float, PyTree
22
+
23
+ from diffbio.configs import TemperatureConfig
24
+ from diffbio.constants import DEFAULT_GAP_EXTEND, DEFAULT_GAP_OPEN
25
+ from diffbio.core.base_operators import TemperatureOperator
26
+ from diffbio.utils.nn_utils import init_learnable_param
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class SmithWatermanConfig(TemperatureConfig):
33
+ """Configuration for SmoothSmithWaterman.
34
+
35
+ Attributes:
36
+ temperature: Temperature for logsumexp smoothing.
37
+ Lower = sharper (closer to hard max), Higher = smoother.
38
+ gap_open: Penalty for opening a gap.
39
+ gap_extend: Penalty for extending a gap.
40
+ """
41
+
42
+ cacheable: bool = True
43
+ gap_open: float = DEFAULT_GAP_OPEN
44
+ gap_extend: float = DEFAULT_GAP_EXTEND
45
+
46
+
47
+ class AlignmentResult(NamedTuple):
48
+ """Result of a smooth alignment.
49
+
50
+ Attributes:
51
+ score: The soft alignment score.
52
+ alignment_matrix: The DP matrix H[i,j] of shape (len1+1, len2+1).
53
+ soft_alignment: Soft alignment matrix showing position correspondences.
54
+ """
55
+
56
+ score: Float[Array, ""]
57
+ alignment_matrix: Float[Array, "len1_plus1 len2_plus1"]
58
+ soft_alignment: Float[Array, "len1 len2"]
59
+
60
+
61
+ class SmoothSmithWaterman(TemperatureOperator):
62
+ """Differentiable Smith-Waterman local alignment.
63
+
64
+ This operator implements a smooth version of the Smith-Waterman algorithm
65
+ where max operations are replaced with logsumexp, enabling gradient flow
66
+ through the alignment computation.
67
+
68
+ The smoothness is controlled by the temperature parameter:
69
+ - temperature -> 0: Approaches hard max (standard Smith-Waterman)
70
+ - temperature -> inf: Uniform averaging
71
+
72
+ Inherits from TemperatureOperator to get:
73
+
74
+ - Learnable temperature parameter management
75
+ - soft_max() method using logsumexp relaxation
76
+
77
+ Args:
78
+ config: SmithWatermanConfig with alignment parameters.
79
+ scoring_matrix: Scoring matrix for matches/mismatches.
80
+ rngs: Flax NNX random number generators (optional).
81
+ name: Optional operator name.
82
+
83
+ Example:
84
+ ```python
85
+ config = SmithWatermanConfig(temperature=1.0)
86
+ scoring = create_dna_scoring_matrix(match=2.0, mismatch=-1.0)
87
+ aligner = SmoothSmithWaterman(config, scoring_matrix=scoring)
88
+ result = aligner.align(seq1, seq2)
89
+ print(result.score)
90
+ ```
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ config: SmithWatermanConfig,
96
+ scoring_matrix: Array,
97
+ *,
98
+ rngs: nnx.Rngs | None = None,
99
+ name: str | None = None,
100
+ ):
101
+ """Initialize the smooth Smith-Waterman aligner.
102
+
103
+ Args:
104
+ config: Alignment configuration.
105
+ scoring_matrix: Scoring matrix (alphabet_size, alphabet_size).
106
+ rngs: Random number generators (optional).
107
+ name: Optional operator name.
108
+ """
109
+ super().__init__(config, rngs=rngs, name=name)
110
+
111
+ # Domain-specific learnable parameters (temperature managed by base class)
112
+ self.scoring_matrix = nnx.Param(scoring_matrix)
113
+ self.gap_open = init_learnable_param(config.gap_open)
114
+ self.gap_extend = init_learnable_param(config.gap_extend)
115
+
116
+ def _compute_score_matrix(
117
+ self,
118
+ seq1: Float[Array, "len1 alphabet"],
119
+ seq2: Float[Array, "len2 alphabet"],
120
+ ) -> Float[Array, "len1 len2"]:
121
+ """Compute pairwise scoring matrix between sequences.
122
+
123
+ Args:
124
+ seq1: First sequence, one-hot encoded (len1, alphabet_size).
125
+ seq2: Second sequence, one-hot encoded (len2, alphabet_size).
126
+
127
+ Returns:
128
+ Score matrix of shape (len1, len2).
129
+ """
130
+ # S[i,j] = seq1[i] @ scoring_matrix @ seq2[j].T
131
+ # Using einsum for clarity
132
+ scoring = self.scoring_matrix[...]
133
+ return jnp.einsum("ia,ab,jb->ij", seq1, scoring, seq2)
134
+
135
+ def align(
136
+ self,
137
+ seq1: Float[Array, "len1 alphabet"],
138
+ seq2: Float[Array, "len2 alphabet"],
139
+ ) -> AlignmentResult:
140
+ """Perform smooth Smith-Waterman local alignment.
141
+
142
+ Args:
143
+ seq1: First sequence, one-hot encoded (len1, alphabet_size).
144
+ seq2: Second sequence, one-hot encoded (len2, alphabet_size).
145
+
146
+ Returns:
147
+ AlignmentResult with score, alignment matrix, and soft alignment.
148
+ """
149
+ len1, len2 = seq1.shape[0], seq2.shape[0]
150
+
151
+ # Compute pairwise scores
152
+ score_matrix = self._compute_score_matrix(seq1, seq2)
153
+
154
+ # Gap penalties
155
+ gap_open = self.gap_open[...]
156
+ gap_extend = self.gap_extend[...]
157
+ # Simplified: use linear gap penalty (gap_open + gap_extend per position)
158
+ gap_penalty = gap_open + gap_extend
159
+
160
+ # Initialize DP matrices
161
+ # H[i,j] = alignment score ending at seq1[i-1], seq2[j-1]
162
+ # Using scan for efficient JAX computation
163
+
164
+ # Initialize H matrix with zeros
165
+ H = jnp.zeros((len1 + 1, len2 + 1))
166
+
167
+ # Fill the DP matrix row by row
168
+ # Note: fori_loop body signature is (i, carry) -> carry
169
+ def fill_row(i, H):
170
+ """Fill row i+1 of the DP matrix."""
171
+
172
+ def cell_update(H_prev_col, j_idx):
173
+ """Update single cell H[i+1, j+1]."""
174
+ j = j_idx.astype(jnp.int32)
175
+ s = score_matrix[i, j]
176
+ diag = H[i, j] + s
177
+ up = H[i, j + 1] + gap_penalty
178
+ left = H_prev_col + gap_penalty
179
+
180
+ # Use inherited soft_max from TemperatureOperator
181
+ candidates = jnp.stack([jnp.array(0.0), diag, up, left], axis=-1)
182
+ h_new = self.soft_max(candidates, axis=-1)
183
+ return h_new, h_new
184
+
185
+ # Scan across columns
186
+ _, new_row = jax.lax.scan(
187
+ cell_update, jnp.array(0.0), jnp.arange(len2, dtype=jnp.int32)
188
+ )
189
+
190
+ # Update H matrix
191
+ H = H.at[i + 1, 1:].set(new_row)
192
+ return H
193
+
194
+ # Fill all rows using fori_loop for efficiency
195
+ H = jax.lax.fori_loop(0, len1, fill_row, H)
196
+
197
+ # Compute final score as smooth max over all positions
198
+ # (local alignment can end anywhere)
199
+ final_candidates = jnp.stack([jnp.array(0.0), jnp.max(H)], axis=-1)
200
+ final_score = self.soft_max(final_candidates, axis=-1)
201
+
202
+ # Compute soft alignment (position correspondence probabilities)
203
+ # This is the softmax of the DP matrix (excluding borders)
204
+ H_inner = H[1:, 1:] # (len1, len2)
205
+ temp = self._temperature # Use property from TemperatureOperator
206
+ soft_alignment = jax.nn.softmax(H_inner.flatten() / temp).reshape(len1, len2)
207
+
208
+ return AlignmentResult(
209
+ score=final_score,
210
+ alignment_matrix=H,
211
+ soft_alignment=soft_alignment,
212
+ )
213
+
214
+ def apply(
215
+ self,
216
+ data: PyTree,
217
+ state: PyTree,
218
+ metadata: dict[str, Any] | None,
219
+ random_params: Any = None,
220
+ stats: dict[str, Any] | None = None,
221
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
222
+ """Apply alignment to sequence pair data.
223
+
224
+ This method implements the OperatorModule interface for batch processing.
225
+ It expects data containing two sequences and returns alignment results.
226
+
227
+ Note: Output preserves input keys for Datarax vmap compatibility,
228
+ while adding alignment result keys.
229
+
230
+ Args:
231
+ data: Dictionary containing:
232
+ - "seq1": First sequence, one-hot encoded (len1, alphabet_size)
233
+ - "seq2": Second sequence, one-hot encoded (len2, alphabet_size)
234
+ state: Element state (passed through unchanged)
235
+ metadata: Element metadata (passed through unchanged)
236
+ random_params: Not used (deterministic operator)
237
+ stats: Not used
238
+
239
+ Returns:
240
+ Tuple of (transformed_data, state, metadata):
241
+ - transformed_data contains input sequences plus alignment results
242
+ (score, alignment_matrix, soft_alignment)
243
+ - state is passed through unchanged
244
+ - metadata is passed through unchanged
245
+ """
246
+ seq1 = data["seq1"]
247
+ seq2 = data["seq2"]
248
+
249
+ # Perform alignment
250
+ result = self.align(seq1, seq2)
251
+
252
+ # Build output data - preserve input keys for Datarax vmap compatibility
253
+ transformed_data = {
254
+ "seq1": seq1,
255
+ "seq2": seq2,
256
+ "score": result.score,
257
+ "alignment_matrix": result.alignment_matrix,
258
+ "soft_alignment": result.soft_alignment,
259
+ }
260
+
261
+ return transformed_data, state, metadata
@@ -0,0 +1,419 @@
1
+ """Soft progressive multiple sequence alignment operator.
2
+
3
+ This module provides differentiable multiple sequence alignment using
4
+ soft operations throughout the alignment process.
5
+
6
+ Key technique: Uses neural network sequence encoders to compute pairwise
7
+ similarities, builds a soft guide tree, and performs progressive profile
8
+ alignment with soft gap handling.
9
+
10
+ Applications: Multiple sequence alignment for homology detection, phylogenetic
11
+ analysis, and protein family characterization.
12
+
13
+ Inherits from TemperatureOperator to get:
14
+
15
+ - _temperature property for temperature-controlled smoothing
16
+ - soft_max() for logsumexp-based smooth maximum
17
+ - soft_argmax() for soft position selection
18
+ """
19
+
20
+ import logging
21
+ from dataclasses import dataclass
22
+ from typing import Any
23
+
24
+ import jax
25
+ import jax.numpy as jnp
26
+ from artifex.generative_models.core.base import MLP
27
+ from datarax.core.config import OperatorConfig
28
+ from flax import nnx
29
+ from jaxtyping import Array, Float, PyTree
30
+
31
+ from diffbio.core.base_operators import TemperatureOperator
32
+ from diffbio.utils.nn_utils import (
33
+ ARTIFEX_GELU_MLP_KWARGS,
34
+ ARTIFEX_GELU_NO_OUTPUT_MLP_KWARGS,
35
+ ensure_rngs,
36
+ )
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class SoftProgressiveMSAConfig(OperatorConfig):
43
+ """Configuration for SoftProgressiveMSA.
44
+
45
+ Attributes:
46
+ max_seq_length: Maximum sequence length.
47
+ hidden_dim: Hidden dimension for neural networks.
48
+ num_layers: Number of encoder layers.
49
+ alphabet_size: Size of sequence alphabet (4 for DNA, 20 for protein).
50
+ temperature: Temperature for softmax operations.
51
+ gap_open_penalty: Gap opening penalty.
52
+ gap_extend_penalty: Gap extension penalty.
53
+ """
54
+
55
+ max_seq_length: int = 100
56
+ hidden_dim: int = 64
57
+ num_layers: int = 2
58
+ alphabet_size: int = 4
59
+ temperature: float = 1.0
60
+ gap_open_penalty: float = -10.0
61
+ gap_extend_penalty: float = -1.0
62
+
63
+ def __post_init__(self) -> None:
64
+ """Validate soft MSA configuration."""
65
+ super().__post_init__()
66
+ if self.num_layers < 1:
67
+ raise ValueError("SoftProgressiveMSAConfig.num_layers must be at least 1.")
68
+
69
+
70
+ class SequenceEncoder(nnx.Module):
71
+ """Encoder for biological sequences."""
72
+
73
+ def __init__(
74
+ self,
75
+ alphabet_size: int,
76
+ hidden_dim: int,
77
+ num_layers: int,
78
+ *,
79
+ rngs: nnx.Rngs,
80
+ ):
81
+ """Initialize the sequence encoder.
82
+
83
+ Args:
84
+ alphabet_size: Size of input alphabet.
85
+ hidden_dim: Hidden dimension.
86
+ num_layers: Number of layers.
87
+ rngs: Random number generators.
88
+ """
89
+ super().__init__()
90
+ self.backbone = MLP(
91
+ hidden_dims=[hidden_dim] * num_layers,
92
+ in_features=alphabet_size,
93
+ rngs=rngs,
94
+ **ARTIFEX_GELU_MLP_KWARGS,
95
+ )
96
+
97
+ # Output projection for sequence embedding
98
+ self.output_proj = nnx.Linear(
99
+ in_features=hidden_dim,
100
+ out_features=hidden_dim,
101
+ rngs=rngs,
102
+ )
103
+
104
+ def __call__(
105
+ self,
106
+ sequence: Float[Array, "seq_len alphabet_size"],
107
+ ) -> Float[Array, "hidden_dim"]:
108
+ """Encode a sequence to a fixed-size embedding.
109
+
110
+ Args:
111
+ sequence: One-hot encoded sequence.
112
+
113
+ Returns:
114
+ Sequence embedding vector.
115
+ """
116
+ backbone_output = self.backbone(sequence)
117
+ if isinstance(backbone_output, tuple):
118
+ raise TypeError("Soft MSA sequence backbone must return a single tensor.")
119
+
120
+ # Global average pooling to get fixed-size embedding
121
+ embedding = jnp.mean(backbone_output, axis=0) # (hidden_dim,)
122
+ embedding = self.output_proj(embedding)
123
+
124
+ return embedding
125
+
126
+
127
+ class ProfileBuilder(nnx.Module):
128
+ """Builds alignment profiles from sequences."""
129
+
130
+ def __init__(
131
+ self,
132
+ hidden_dim: int,
133
+ alphabet_size: int,
134
+ *,
135
+ rngs: nnx.Rngs,
136
+ ):
137
+ """Initialize the profile builder.
138
+
139
+ Args:
140
+ hidden_dim: Hidden dimension.
141
+ alphabet_size: Alphabet size.
142
+ rngs: Random number generators.
143
+ """
144
+ super().__init__()
145
+
146
+ self.hidden_dim = hidden_dim
147
+ self.alphabet_size = alphabet_size
148
+
149
+ self.backbone = MLP(
150
+ hidden_dims=[hidden_dim, alphabet_size],
151
+ in_features=alphabet_size,
152
+ rngs=rngs,
153
+ **ARTIFEX_GELU_NO_OUTPUT_MLP_KWARGS,
154
+ )
155
+
156
+ def __call__(
157
+ self,
158
+ sequences: Float[Array, "n_seqs seq_len alphabet_size"],
159
+ weights: Float[Array, "n_seqs"],
160
+ ) -> Float[Array, "seq_len alphabet_size"]:
161
+ """Build a profile from weighted sequences.
162
+
163
+ Args:
164
+ sequences: Stack of aligned sequences.
165
+ weights: Weights for each sequence.
166
+
167
+ Returns:
168
+ Profile (position-specific scoring matrix).
169
+ """
170
+ # Weighted average of sequences
171
+ weights = weights / (jnp.sum(weights) + 1e-8)
172
+ profile = jnp.einsum("n,nla->la", weights, sequences)
173
+
174
+ # Refine profile
175
+ refinement = self.backbone(profile)
176
+ if isinstance(refinement, tuple):
177
+ raise TypeError("Soft MSA profile backbone must return a single tensor.")
178
+ profile = profile + 0.1 * refinement # Small residual update
179
+
180
+ # Normalize to valid probability distribution
181
+ profile = jax.nn.softmax(profile, axis=-1)
182
+
183
+ return profile
184
+
185
+
186
+ class SoftProgressiveMSA(TemperatureOperator):
187
+ """Differentiable progressive multiple sequence alignment.
188
+
189
+ This operator performs multiple sequence alignment using soft
190
+ operations that maintain gradient flow throughout the process.
191
+
192
+ Algorithm:
193
+ 1. Encode each sequence to get embeddings
194
+ 2. Compute pairwise distances from embeddings
195
+ 3. Build soft guide tree from distances
196
+ 4. Progressive alignment following guide tree order
197
+ 5. Build consensus profile
198
+
199
+ Inherits from TemperatureOperator to get:
200
+
201
+ - _temperature property for temperature-controlled smoothing
202
+ - soft_max() for logsumexp-based smooth maximum
203
+ - soft_argmax() for soft position selection
204
+
205
+ Args:
206
+ config: SoftProgressiveMSAConfig with model parameters.
207
+ rngs: Flax NNX random number generators.
208
+ name: Optional operator name.
209
+
210
+ Example:
211
+ ```python
212
+ config = SoftProgressiveMSAConfig(max_seq_length=100)
213
+ msa = SoftProgressiveMSA(config, rngs=nnx.Rngs(42))
214
+ data = {"sequences": seqs} # (n_seqs, seq_len, alphabet_size)
215
+ result, state, meta = msa.apply(data, {}, None)
216
+ ```
217
+ """
218
+
219
+ def __init__(
220
+ self,
221
+ config: SoftProgressiveMSAConfig,
222
+ *,
223
+ rngs: nnx.Rngs | None = None,
224
+ name: str | None = None,
225
+ ):
226
+ """Initialize the soft progressive MSA operator.
227
+
228
+ Args:
229
+ config: MSA configuration.
230
+ rngs: Random number generators for initialization.
231
+ name: Optional operator name.
232
+ """
233
+ super().__init__(config, rngs=rngs, name=name)
234
+
235
+ rngs = ensure_rngs(rngs)
236
+
237
+ self.hidden_dim = config.hidden_dim
238
+ # Temperature is now managed by TemperatureOperator via self._temperature
239
+ self.alphabet_size = config.alphabet_size
240
+
241
+ # Sequence encoder for computing pairwise similarities
242
+ self.seq_encoder = SequenceEncoder(
243
+ alphabet_size=config.alphabet_size,
244
+ hidden_dim=config.hidden_dim,
245
+ num_layers=config.num_layers,
246
+ rngs=rngs,
247
+ )
248
+
249
+ # Profile builder for progressive alignment
250
+ self.profile_builder = ProfileBuilder(
251
+ hidden_dim=config.hidden_dim,
252
+ alphabet_size=config.alphabet_size,
253
+ rngs=rngs,
254
+ )
255
+
256
+ # Alignment scoring
257
+ self.align_score = nnx.Linear(
258
+ in_features=config.alphabet_size * 2,
259
+ out_features=1,
260
+ rngs=rngs,
261
+ )
262
+
263
+ def _compute_pairwise_distances(
264
+ self,
265
+ sequences: Float[Array, "n_seqs seq_len alphabet_size"],
266
+ ) -> Float[Array, "n_seqs n_seqs"]:
267
+ """Compute pairwise distances between sequences.
268
+
269
+ Args:
270
+ sequences: Input sequences.
271
+
272
+ Returns:
273
+ Pairwise distance matrix.
274
+ """
275
+ n_seqs = sequences.shape[0]
276
+
277
+ # Encode all sequences
278
+ embeddings = jax.vmap(self.seq_encoder)(sequences) # (n_seqs, hidden_dim)
279
+
280
+ # Compute pairwise distances (negative cosine similarity)
281
+ norms = jnp.linalg.norm(embeddings, axis=-1, keepdims=True) + 1e-8
282
+ normalized = embeddings / norms
283
+
284
+ # Cosine similarity -> distance
285
+ similarities = jnp.einsum("ih,jh->ij", normalized, normalized)
286
+ distances = 1.0 - similarities
287
+
288
+ # Zero diagonal
289
+ distances = distances * (1.0 - jnp.eye(n_seqs))
290
+
291
+ return distances
292
+
293
+ def _soft_align_pair(
294
+ self,
295
+ seq1: Float[Array, "len1 alphabet_size"],
296
+ seq2: Float[Array, "len2 alphabet_size"],
297
+ ) -> tuple[
298
+ Float[Array, "max_len alphabet_size"],
299
+ Float[Array, "max_len alphabet_size"],
300
+ Float[Array, ""],
301
+ ]:
302
+ """Perform soft pairwise alignment.
303
+
304
+ Args:
305
+ seq1: First sequence.
306
+ seq2: Second sequence.
307
+
308
+ Returns:
309
+ Tuple of (aligned_seq1, aligned_seq2, alignment_score).
310
+ """
311
+ len1, len2 = seq1.shape[0], seq2.shape[0]
312
+ max_len = max(len1, len2)
313
+
314
+ # Compute position-wise similarity scores
315
+ # (len1, alphabet) x (len2, alphabet) -> (len1, len2)
316
+ match_scores = jnp.einsum("ia,ja->ij", seq1, seq2)
317
+
318
+ # Soft alignment via attention
319
+ # Each position in seq1 attends to positions in seq2
320
+ # Use inherited _temperature property from TemperatureOperator
321
+ attn_weights = jax.nn.softmax(match_scores / self._temperature, axis=-1)
322
+
323
+ # Soft-aligned seq2 based on seq1 positions
324
+ aligned_seq2_to_seq1 = jnp.einsum("ij,ja->ia", attn_weights, seq2)
325
+
326
+ # Pad to max_len
327
+ pad1 = max_len - len1
328
+
329
+ aligned1 = jnp.pad(seq1, ((0, pad1), (0, 0)))
330
+ aligned2 = jnp.pad(aligned_seq2_to_seq1, ((0, pad1), (0, 0)))
331
+
332
+ # Compute alignment score
333
+ alignment_score = jnp.mean(match_scores * attn_weights)
334
+
335
+ return aligned1, aligned2, alignment_score
336
+
337
+ def apply(
338
+ self,
339
+ data: PyTree,
340
+ state: PyTree,
341
+ metadata: dict[str, Any] | None,
342
+ random_params: Any = None,
343
+ stats: dict[str, Any] | None = None,
344
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
345
+ """Apply soft progressive MSA.
346
+
347
+ Args:
348
+ data: Dictionary containing:
349
+ - "sequences": Input sequences (n_seqs, seq_len, alphabet_size)
350
+ state: Element state (passed through unchanged)
351
+ metadata: Element metadata (passed through unchanged)
352
+ random_params: Not used
353
+ stats: Not used
354
+
355
+ Returns:
356
+ Tuple of (transformed_data, state, metadata):
357
+ - transformed_data contains:
358
+
359
+ - "sequences": Original sequences
360
+ - "aligned_sequences": Soft-aligned sequences
361
+ - "pairwise_distances": Guide tree distances
362
+ - "alignment_scores": Pairwise alignment scores
363
+ - "consensus_profile": Consensus profile
364
+ - state is passed through unchanged
365
+ - metadata is passed through unchanged
366
+ """
367
+ sequences = data["sequences"]
368
+ n_seqs = sequences.shape[0]
369
+
370
+ # Step 1: Compute pairwise distances for guide tree
371
+ pairwise_distances = self._compute_pairwise_distances(sequences)
372
+
373
+ # Step 2: Progressive alignment
374
+ # For simplicity, align all sequences to the first one
375
+ # (full progressive alignment would follow guide tree)
376
+ aligned_sequences = []
377
+ alignment_scores = []
378
+
379
+ # Use first sequence as anchor
380
+ anchor = sequences[0]
381
+ aligned_sequences.append(anchor)
382
+
383
+ for i in range(1, n_seqs):
384
+ aligned1, aligned2, score = self._soft_align_pair(anchor, sequences[i])
385
+ # Keep the aligned version of sequences[i]
386
+ aligned_sequences.append(aligned2[: sequences[i].shape[0]])
387
+ alignment_scores.append(score)
388
+
389
+ # Stack aligned sequences (pad to same length)
390
+ max_len = max(s.shape[0] for s in aligned_sequences)
391
+ padded_aligned = []
392
+ for seq in aligned_sequences:
393
+ pad_len = max_len - seq.shape[0]
394
+ padded = jnp.pad(seq, ((0, pad_len), (0, 0)))
395
+ padded_aligned.append(padded)
396
+
397
+ aligned_stack = jnp.stack(padded_aligned, axis=0)
398
+
399
+ # Step 3: Build consensus profile
400
+ uniform_weights = jnp.ones(n_seqs) / n_seqs
401
+ consensus = self.profile_builder(aligned_stack, uniform_weights)
402
+
403
+ # Alignment scores matrix
404
+ scores_matrix = jnp.zeros((n_seqs, n_seqs))
405
+ if alignment_scores:
406
+ scores_array = jnp.array(alignment_scores)
407
+ # Fill first row/column with computed scores
408
+ scores_matrix = scores_matrix.at[0, 1:].set(scores_array)
409
+ scores_matrix = scores_matrix.at[1:, 0].set(scores_array)
410
+
411
+ transformed_data = {
412
+ "sequences": sequences,
413
+ "aligned_sequences": aligned_stack,
414
+ "pairwise_distances": pairwise_distances,
415
+ "alignment_scores": scores_matrix,
416
+ "consensus_profile": consensus,
417
+ }
418
+
419
+ return transformed_data, state, metadata
@@ -0,0 +1,27 @@
1
+ """Assembly operators for differentiable genome assembly.
2
+
3
+ This module provides graph neural network-based approaches to
4
+ assembly graph traversal that enable gradient flow through the
5
+ assembly process.
6
+
7
+ - GNNAssemblyNavigator: Message passing GNN for soft edge selection
8
+ - DifferentiableMetagenomicBinner: VAMB-style VAE for metagenomic binning
9
+ """
10
+
11
+ from diffbio.operators.assembly.gnn_assembly import (
12
+ GNNAssemblyNavigator,
13
+ GNNAssemblyNavigatorConfig,
14
+ )
15
+ from diffbio.operators.assembly.metagenomic_binning import (
16
+ DifferentiableMetagenomicBinner,
17
+ MetagenomicBinnerConfig,
18
+ create_metagenomic_binner,
19
+ )
20
+
21
+ __all__ = [
22
+ "GNNAssemblyNavigator",
23
+ "GNNAssemblyNavigatorConfig",
24
+ "DifferentiableMetagenomicBinner",
25
+ "MetagenomicBinnerConfig",
26
+ "create_metagenomic_binner",
27
+ ]