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,509 @@
1
+ """Differentiable RNA secondary structure prediction.
2
+
3
+ This module implements a differentiable version of the McCaskill partition
4
+ function algorithm for computing RNA base pair probabilities. The algorithm
5
+ uses dynamic programming with temperature-controlled softmax for gradient flow.
6
+
7
+ The McCaskill algorithm (1990) computes:
8
+ - Z = Σ_P exp(-E(P)/RT): Partition function over all structures
9
+ - P^bp[i,j]: Probability that positions i and j are base-paired
10
+
11
+ Key features:
12
+
13
+ - McCaskill-style inside-outside computation
14
+ - Base pair probability matrix
15
+ - Temperature-controlled smoothing for differentiability
16
+ - Watson-Crick + wobble base pairing
17
+
18
+ For differentiable optimization, we use a generalization of McCaskill's
19
+ algorithm that operates on continuous probability distributions over
20
+ nucleotides, following Matthies et al. (2024).
21
+
22
+ References:
23
+ McCaskill, J. S. (1990). The equilibrium partition function and base
24
+ pair binding probabilities for RNA secondary structure.
25
+ Biopolymers 29, 1105-1119.
26
+
27
+ Matthies, M. C. et al. (2024). Differentiable partition function
28
+ calculation for RNA. Nucleic Acids Research 52(3), e14.
29
+
30
+ Krueger, R. et al. (2025). JAX-RNAfold: Scalable differentiable folding.
31
+ Bioinformatics 41(5), btaf203.
32
+ """
33
+
34
+ import logging
35
+ from dataclasses import dataclass
36
+ from typing import Any
37
+
38
+ import jax
39
+ import jax.numpy as jnp
40
+ from datarax.core.config import OperatorConfig
41
+ from flax import nnx
42
+ from jaxtyping import Array, Float, PyTree
43
+
44
+ from diffbio.core.base_operators import TemperatureOperator
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+ # RNA nucleotide indices (standard one-hot encoding)
49
+ NUC_A = 0 # Adenine
50
+ NUC_C = 1 # Cytosine
51
+ NUC_G = 2 # Guanine
52
+ NUC_U = 3 # Uracil
53
+
54
+ # Base pair energies (in units of RT, negative = favorable)
55
+ # Simplified Nussinov-style scoring where each base pair contributes
56
+ # a fixed energy independent of context.
57
+ BP_ENERGY_AU = -2.0 # A-U pair (2 hydrogen bonds)
58
+ BP_ENERGY_GC = -3.0 # G-C pair (3 hydrogen bonds, stronger)
59
+ BP_ENERGY_GU = -1.0 # G-U wobble pair (weaker)
60
+
61
+ # Minimum hairpin loop size (standard is 3 unpaired nucleotides)
62
+ DEFAULT_MIN_HAIRPIN = 3
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class _RNAFoldRuntimeConfig:
67
+ """Runtime and caching configuration for RNA folding."""
68
+
69
+ cacheable: bool = True
70
+ temperature: float = 1.0
71
+ min_hairpin_loop: int = DEFAULT_MIN_HAIRPIN
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class _RNAFoldEnergyConfig:
76
+ """Alphabet and base-pair energy configuration."""
77
+
78
+ alphabet_size: int = 4
79
+ bp_energy_au: float = BP_ENERGY_AU
80
+ bp_energy_gc: float = BP_ENERGY_GC
81
+ bp_energy_gu: float = BP_ENERGY_GU
82
+ learnable_temperature: bool = False
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class RNAFoldConfig(_RNAFoldRuntimeConfig, _RNAFoldEnergyConfig, OperatorConfig):
87
+ """Configuration for DifferentiableRNAFold."""
88
+
89
+ def __post_init__(self) -> None:
90
+ """Validate RNA folding configuration."""
91
+ super().__post_init__()
92
+
93
+ if self.temperature <= 0.0:
94
+ raise ValueError("temperature must be positive.")
95
+ if self.min_hairpin_loop < 0:
96
+ raise ValueError("min_hairpin_loop must be non-negative.")
97
+ if self.alphabet_size != 4:
98
+ raise ValueError("alphabet_size must be 4 for canonical RNA one-hot encoding.")
99
+ if self.bp_energy_au > 0.0:
100
+ raise ValueError("bp_energy_au must be non-positive.")
101
+ if self.bp_energy_gc > 0.0:
102
+ raise ValueError("bp_energy_gc must be non-positive.")
103
+ if self.bp_energy_gu > 0.0:
104
+ raise ValueError("bp_energy_gu must be non-positive.")
105
+
106
+
107
+ def compute_pair_energy_matrix(
108
+ sequence: Float[Array, "length 4"],
109
+ bp_energy_au: float = BP_ENERGY_AU,
110
+ bp_energy_gc: float = BP_ENERGY_GC,
111
+ bp_energy_gu: float = BP_ENERGY_GU,
112
+ ) -> Float[Array, "length length"]:
113
+ """Compute base pair energy matrix for RNA sequence.
114
+
115
+ Uses Watson-Crick and wobble base pairing rules:
116
+ - A-U: 2 hydrogen bonds (medium strength)
117
+ - G-C: 3 hydrogen bonds (strongest)
118
+ - G-U: Wobble pair (weakest)
119
+
120
+ For soft/probabilistic sequences, the energy is weighted by the
121
+ probability of each nucleotide at each position.
122
+
123
+ Args:
124
+ sequence: One-hot encoded RNA sequence (A=0, C=1, G=2, U=3).
125
+ bp_energy_au: Energy for A-U pair.
126
+ bp_energy_gc: Energy for G-C pair.
127
+ bp_energy_gu: Energy for G-U wobble pair.
128
+
129
+ Returns:
130
+ Energy matrix where [i,j] is the pairing energy for positions i,j.
131
+ More negative = more favorable pairing.
132
+ """
133
+ # Get soft nucleotide probabilities at each position
134
+ p_a = sequence[:, NUC_A] # (length,)
135
+ p_c = sequence[:, NUC_C]
136
+ p_g = sequence[:, NUC_G]
137
+ p_u = sequence[:, NUC_U]
138
+
139
+ # Compute pairwise base pair compatibility using outer products
140
+ # A-U pairing (in both directions)
141
+ au_prob = jnp.outer(p_a, p_u) + jnp.outer(p_u, p_a)
142
+
143
+ # G-C pairing
144
+ gc_prob = jnp.outer(p_g, p_c) + jnp.outer(p_c, p_g)
145
+
146
+ # G-U wobble pairing
147
+ gu_prob = jnp.outer(p_g, p_u) + jnp.outer(p_u, p_g)
148
+
149
+ # Combined energy (weighted by pairing probability)
150
+ energy = bp_energy_au * au_prob + bp_energy_gc * gc_prob + bp_energy_gu * gu_prob
151
+
152
+ return energy
153
+
154
+
155
+ def mccaskill_partition_function(
156
+ energy_matrix: Float[Array, "length length"],
157
+ min_hairpin: int = DEFAULT_MIN_HAIRPIN,
158
+ temperature: float = 1.0,
159
+ ) -> tuple[Float[Array, "length length"], Float[Array, ""]]:
160
+ """Compute partition function using McCaskill algorithm.
161
+
162
+ Implements the inside algorithm for RNA partition function computation.
163
+ The recursion follows McCaskill (1990):
164
+
165
+ Q[i,j] = Q[i,j-1] + Σ_k Q[i,k-1] * Q^bp[k,j]
166
+ Q^bp[i,j] = exp(-E[i,j]/RT) * (1 + Q[i+1,j-1])
167
+
168
+ For differentiability, we work in log space and use logsumexp.
169
+
170
+ Args:
171
+ energy_matrix: Base pair energy matrix from compute_pair_energy_matrix.
172
+ min_hairpin: Minimum hairpin loop size.
173
+ temperature: Temperature (RT) for Boltzmann weights.
174
+
175
+ Returns:
176
+ Tuple of (log_Q, log_Z) where:
177
+ - log_Q[i,j] is log partition function for subsequence [i,j]
178
+ - log_Z is total log partition function
179
+ """
180
+ n = energy_matrix.shape[0]
181
+
182
+ # Initialize log partition functions
183
+ # log_Q[i,j] = log of partition function for subsequence [i,j]
184
+ # log_Qbp[i,j] = log of partition function for [i,j] where i,j are paired
185
+
186
+ # For subsequences shorter than min_hairpin+2, no pairing is possible
187
+ # Q[i,i] = 1 (empty structure), so log_Q[i,i] = 0
188
+ log_Q = jnp.zeros((n, n))
189
+
190
+ # Boltzmann weights for base pairs: exp(-E/T)
191
+ # In log space: -E/T
192
+ log_boltzmann = -energy_matrix / temperature
193
+
194
+ # Create validity mask (positions can pair if |i-j| > min_hairpin)
195
+ i_idx = jnp.arange(n)[:, None]
196
+ j_idx = jnp.arange(n)[None, :]
197
+ valid_pair = jnp.abs(i_idx - j_idx) > min_hairpin
198
+
199
+ # Apply mask: invalid pairs get -inf in log space (probability 0)
200
+ log_boltzmann = jnp.where(valid_pair, log_boltzmann, -jnp.inf)
201
+
202
+ # Fill DP table using scan over diagonal lengths
203
+ # For each length d, compute Q[i, i+d] for all valid i
204
+
205
+ def fill_length(log_Q, d):
206
+ """Fill all entries with subsequence length d."""
207
+
208
+ def compute_entry(i):
209
+ """Compute the log partition function Q[i, i+d] for one entry."""
210
+ j = i + d
211
+
212
+ # Case 1: j is unpaired, use Q[i, j-1]
213
+ log_unpaired = log_Q[i, j - 1] if j > i else 0.0
214
+
215
+ # Case 2: j pairs with some k in [i, j-min_hairpin)
216
+ # Sum over all k: Q[i,k-1] * exp(-E[k,j]/T) * (1 + Q[k+1,j-1])
217
+
218
+ def pair_term(k):
219
+ """Compute the log contribution of pairing position k with j."""
220
+ # Boltzmann factor for k,j pair
221
+ log_bp = log_boltzmann[k, j]
222
+
223
+ # Left fragment [i, k-1]
224
+ log_left = jnp.where(k > i, log_Q[i, k - 1], 0.0)
225
+
226
+ # Inside fragment [k+1, j-1]
227
+ # Q_inside = 1 + Q[k+1, j-1] means in log space:
228
+ # log(1 + exp(log_Q)) = log1p(exp(log_Q))
229
+ log_inside_q = jnp.where(
230
+ k + 1 <= j - 1,
231
+ log_Q[k + 1, j - 1],
232
+ 0.0, # Empty inside, log(1) = 0
233
+ )
234
+ # log(1 + Q) = log(exp(0) + exp(log_Q)) = logsumexp([0, log_Q])
235
+ log_one_plus_inside = jax.scipy.special.logsumexp(jnp.array([0.0, log_inside_q]))
236
+
237
+ return log_left + log_bp + log_one_plus_inside
238
+
239
+ # Valid pairing partners
240
+ k_values = jnp.arange(n)
241
+ log_pair_terms = jax.vmap(pair_term)(k_values)
242
+
243
+ # Mask invalid k values
244
+ k_valid = (k_values >= i) & (k_values <= j - min_hairpin - 1)
245
+ log_pair_terms = jnp.where(k_valid, log_pair_terms, -jnp.inf)
246
+
247
+ # Combine unpaired case with all pairing cases
248
+ all_terms = jnp.concatenate([jnp.array([log_unpaired]), log_pair_terms])
249
+ log_total = jax.scipy.special.logsumexp(all_terms)
250
+
251
+ return log_total
252
+
253
+ # Compute entries for this diagonal
254
+ # Use fori_loop for better tracing
255
+ def body_fn(i, log_Q):
256
+ """Update the DP table for entry (i, i+d) if within bounds."""
257
+ j = i + d
258
+ valid = j < n
259
+ new_val = jax.lax.cond(valid, lambda: compute_entry(i), lambda: 0.0)
260
+ log_Q = jax.lax.cond(valid, lambda q: q.at[i, j].set(new_val), lambda q: q, log_Q)
261
+ return log_Q
262
+
263
+ log_Q = jax.lax.fori_loop(0, n, body_fn, log_Q)
264
+ return log_Q, None
265
+
266
+ # Fill for all lengths from min_hairpin+1 to n-1
267
+ log_Q, _ = jax.lax.scan(fill_length, log_Q, jnp.arange(min_hairpin + 1, n))
268
+
269
+ # Total partition function is Q[0, n-1]
270
+ log_Z = log_Q[0, n - 1]
271
+
272
+ return log_Q, log_Z
273
+
274
+
275
+ def compute_base_pair_probabilities(
276
+ energy_matrix: Float[Array, "length length"],
277
+ min_hairpin: int = DEFAULT_MIN_HAIRPIN,
278
+ temperature: Array | float = 1.0,
279
+ ) -> tuple[Float[Array, "length length"], Float[Array, ""]]:
280
+ """Compute base pair probability matrix.
281
+
282
+ Uses a simplified approach where probabilities are derived from
283
+ the Boltzmann-weighted base pair energies normalized over all
284
+ valid positions.
285
+
286
+ For full McCaskill, one would compute inside-outside probabilities,
287
+ but for differentiability and simplicity, we use:
288
+ P[i,j] ∝ exp(-E[i,j]/T) * validity_mask[i,j]
289
+
290
+ Args:
291
+ energy_matrix: Base pair energy matrix.
292
+ min_hairpin: Minimum hairpin loop size.
293
+ temperature: Temperature for Boltzmann distribution.
294
+
295
+ Returns:
296
+ Tuple of (bp_probs, log_Z) where:
297
+ - bp_probs[i,j] is probability that i and j are paired
298
+ - log_Z is log partition function (logsumexp of valid pairs)
299
+ """
300
+ n = energy_matrix.shape[0]
301
+
302
+ # Create validity mask
303
+ i_idx = jnp.arange(n)[:, None]
304
+ j_idx = jnp.arange(n)[None, :]
305
+ valid_mask = (jnp.abs(i_idx - j_idx) > min_hairpin).astype(jnp.float32)
306
+
307
+ # Boltzmann weights: exp(-E/T)
308
+ # In a normalized probability, negative energies give higher probability
309
+ log_weights = -energy_matrix / temperature
310
+
311
+ # Mask invalid positions
312
+ log_weights_masked = jnp.where(
313
+ valid_mask > 0.5, log_weights, jnp.full_like(log_weights, -jnp.inf)
314
+ )
315
+
316
+ # Normalize via softmax over all valid positions
317
+ flat_log_weights = log_weights_masked.flatten()
318
+ flat_probs = jax.nn.softmax(flat_log_weights)
319
+ bp_probs = flat_probs.reshape(n, n)
320
+
321
+ # The partition function is the sum of Boltzmann weights
322
+ # log_Z = logsumexp(-E/T) over valid pairs
323
+ log_Z = jax.scipy.special.logsumexp(jnp.where(valid_mask > 0.5, log_weights, -jnp.inf))
324
+
325
+ # Ensure symmetry (A pairs with B == B pairs with A)
326
+ bp_probs = (bp_probs + bp_probs.T) / 2
327
+
328
+ # Re-normalize after symmetrization
329
+ total = bp_probs.sum()
330
+ bp_probs = jnp.where(total > 1e-10, bp_probs / total, bp_probs)
331
+
332
+ return bp_probs, log_Z
333
+
334
+
335
+ class DifferentiableRNAFold(TemperatureOperator):
336
+ """Differentiable RNA secondary structure prediction.
337
+
338
+ This operator computes base pair probabilities for RNA sequences
339
+ using a McCaskill-style partition function algorithm. The implementation
340
+ uses temperature-controlled softmax for full differentiability.
341
+
342
+ The McCaskill algorithm computes Z = Σ_P exp(-E(P)/RT), the partition
343
+ function over all possible secondary structures. From this, base pair
344
+ probabilities are derived as the marginal probability that positions
345
+ i and j are paired in the ensemble.
346
+
347
+ For differentiability, we generalize the algorithm to operate on
348
+ continuous probability distributions over nucleotides, following
349
+ Matthies et al. (2024).
350
+
351
+ Input data structure:
352
+ - sequence: Float[Array, "length 4"] or Float[Array, "batch length 4"]
353
+ One-hot encoded RNA sequence (A=0, C=1, G=2, U=3)
354
+
355
+ Output data structure (adds):
356
+ - bp_probs: Float[Array, "length length"] - Base pair probabilities
357
+ - partition_function: Float[Array, ""] - Log partition function
358
+
359
+ Example:
360
+ ```python
361
+ config = RNAFoldConfig(temperature=1.0)
362
+ predictor = DifferentiableRNAFold(config, rngs=nnx.Rngs(42))
363
+ sequence = jax.nn.one_hot(seq_indices, num_classes=4)
364
+ result, _, _ = predictor.apply({"sequence": sequence}, {}, None)
365
+ bp_probs = result["bp_probs"] # (length, length)
366
+ ```
367
+ """
368
+
369
+ def __init__(
370
+ self,
371
+ config: RNAFoldConfig,
372
+ *,
373
+ rngs: nnx.Rngs,
374
+ name: str | None = None,
375
+ ):
376
+ """Initialize the RNA fold predictor.
377
+
378
+ Args:
379
+ config: Configuration with folding parameters.
380
+ rngs: Random number generators.
381
+ name: Optional operator name.
382
+ """
383
+ super().__init__(config, rngs=rngs, name=name)
384
+
385
+ def _fold_single(
386
+ self,
387
+ sequence: Float[Array, "length 4"],
388
+ ) -> tuple[Float[Array, "length length"], Float[Array, ""]]:
389
+ """Compute folding for a single sequence.
390
+
391
+ Args:
392
+ sequence: One-hot encoded RNA sequence.
393
+
394
+ Returns:
395
+ Tuple of (base_pair_probabilities, log_partition_function).
396
+ """
397
+ temperature = self._temperature
398
+ config = self.config
399
+
400
+ # Compute base pair energy matrix
401
+ energy_matrix = compute_pair_energy_matrix(
402
+ sequence,
403
+ bp_energy_au=config.bp_energy_au,
404
+ bp_energy_gc=config.bp_energy_gc,
405
+ bp_energy_gu=config.bp_energy_gu,
406
+ )
407
+
408
+ # Compute base pair probabilities
409
+ bp_probs, log_z = compute_base_pair_probabilities(
410
+ energy_matrix,
411
+ min_hairpin=config.min_hairpin_loop,
412
+ temperature=temperature,
413
+ )
414
+
415
+ return bp_probs, log_z
416
+
417
+ def apply(
418
+ self,
419
+ data: PyTree,
420
+ state: PyTree,
421
+ metadata: dict[str, Any] | None,
422
+ random_params: Any = None,
423
+ stats: dict[str, Any] | None = None,
424
+ ) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
425
+ """Apply RNA folding prediction to sequence data.
426
+
427
+ Args:
428
+ data: Dictionary containing:
429
+ - "sequence": One-hot encoded RNA sequence
430
+ Shape: (length, 4) or (batch, length, 4)
431
+ state: Element state (passed through unchanged)
432
+ metadata: Element metadata (passed through unchanged)
433
+ random_params: Not used
434
+ stats: Not used
435
+
436
+ Returns:
437
+ Tuple of (transformed_data, state, metadata):
438
+ - transformed_data contains:
439
+
440
+ - All original keys from data
441
+ - "bp_probs": Base pair probability matrix
442
+ - "partition_function": Log partition function
443
+ - state is passed through unchanged
444
+ - metadata is passed through unchanged
445
+ """
446
+ del random_params, stats # Unused
447
+
448
+ sequence = data["sequence"]
449
+
450
+ # Handle batched vs single sequence
451
+ if sequence.ndim == 2:
452
+ # Single sequence: (length, 4)
453
+ bp_probs, log_z = self._fold_single(sequence)
454
+ else:
455
+ # Batched: (batch, length, 4)
456
+ bp_probs, log_z = jax.vmap(self._fold_single)(sequence)
457
+
458
+ # Build output data, preserving all input keys
459
+ transformed_data = {
460
+ **data,
461
+ "bp_probs": bp_probs,
462
+ "partition_function": log_z,
463
+ }
464
+
465
+ return transformed_data, state, metadata
466
+
467
+
468
+ def create_rna_fold_predictor(
469
+ temperature: float = 1.0,
470
+ min_hairpin_loop: int = DEFAULT_MIN_HAIRPIN,
471
+ bp_energy_au: float = BP_ENERGY_AU,
472
+ bp_energy_gc: float = BP_ENERGY_GC,
473
+ bp_energy_gu: float = BP_ENERGY_GU,
474
+ *,
475
+ rngs: nnx.Rngs | None = None,
476
+ ) -> DifferentiableRNAFold:
477
+ """Create an RNA fold predictor with given parameters.
478
+
479
+ Factory function for convenient predictor creation.
480
+
481
+ Args:
482
+ temperature: Softmax temperature for Boltzmann distribution.
483
+ min_hairpin_loop: Minimum hairpin loop size.
484
+ bp_energy_au: Energy for A-U base pair.
485
+ bp_energy_gc: Energy for G-C base pair.
486
+ bp_energy_gu: Energy for G-U wobble pair.
487
+ rngs: Random number generators.
488
+
489
+ Returns:
490
+ Configured DifferentiableRNAFold instance.
491
+
492
+ Example:
493
+ ```python
494
+ predictor = create_rna_fold_predictor(temperature=0.5)
495
+ result, _, _ = predictor.apply({"sequence": seq}, {}, None)
496
+ ```
497
+ """
498
+ if rngs is None:
499
+ rngs = nnx.Rngs(0)
500
+
501
+ config = RNAFoldConfig(
502
+ temperature=temperature,
503
+ min_hairpin_loop=min_hairpin_loop,
504
+ bp_energy_au=bp_energy_au,
505
+ bp_energy_gc=bp_energy_gc,
506
+ bp_energy_gu=bp_energy_gu,
507
+ )
508
+
509
+ return DifferentiableRNAFold(config, rngs=rngs)
@@ -0,0 +1,23 @@
1
+ """RNA-seq operators for differentiable transcriptomics analysis.
2
+
3
+ This module provides differentiable operators for RNA-seq data analysis,
4
+ including splicing PSI calculation, motif discovery, and differential expression.
5
+ """
6
+
7
+ from diffbio.operators.rnaseq.motif_discovery import (
8
+ DifferentiableMotifDiscovery,
9
+ MotifDiscoveryConfig,
10
+ )
11
+ from diffbio.operators.rnaseq.splicing_psi import (
12
+ SplicingPSI,
13
+ SplicingPSIConfig,
14
+ )
15
+
16
+ __all__ = [
17
+ # Splicing PSI
18
+ "SplicingPSI",
19
+ "SplicingPSIConfig",
20
+ # Motif Discovery
21
+ "DifferentiableMotifDiscovery",
22
+ "MotifDiscoveryConfig",
23
+ ]