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.
- diffbio/__init__.py +39 -0
- diffbio/configs.py +75 -0
- diffbio/constants.py +204 -0
- diffbio/core/__init__.py +127 -0
- diffbio/core/base_operators.py +612 -0
- diffbio/core/data_types.py +260 -0
- diffbio/core/gnn_components.py +629 -0
- diffbio/core/graph_utils.py +149 -0
- diffbio/core/neural_components.py +270 -0
- diffbio/core/optimal_transport.py +133 -0
- diffbio/core/soft_ops/__init__.py +216 -0
- diffbio/core/soft_ops/_projections_permutahedron.py +1864 -0
- diffbio/core/soft_ops/_projections_simplex.py +240 -0
- diffbio/core/soft_ops/_projections_transport.py +508 -0
- diffbio/core/soft_ops/_sorting_network.py +204 -0
- diffbio/core/soft_ops/_types.py +15 -0
- diffbio/core/soft_ops/_utils.py +342 -0
- diffbio/core/soft_ops/autograd_safe.py +120 -0
- diffbio/core/soft_ops/comparison.py +235 -0
- diffbio/core/soft_ops/elementwise.py +309 -0
- diffbio/core/soft_ops/logical.py +146 -0
- diffbio/core/soft_ops/quantile.py +376 -0
- diffbio/core/soft_ops/selection.py +236 -0
- diffbio/core/soft_ops/sorting.py +926 -0
- diffbio/core/soft_ops/straight_through.py +261 -0
- diffbio/core/uncertainty.py +279 -0
- diffbio/evaluation/__init__.py +42 -0
- diffbio/evaluation/adapters.py +409 -0
- diffbio/evaluation/graders.py +223 -0
- diffbio/evaluation/problem.py +157 -0
- diffbio/evaluation/runner.py +277 -0
- diffbio/losses/__init__.py +59 -0
- diffbio/losses/alignment_losses.py +222 -0
- diffbio/losses/biological_regularization.py +288 -0
- diffbio/losses/metric_losses.py +139 -0
- diffbio/losses/singlecell_losses.py +387 -0
- diffbio/losses/statistical_losses.py +345 -0
- diffbio/operators/__init__.py +60 -0
- diffbio/operators/_count_vae.py +197 -0
- diffbio/operators/_loss_balancing.py +65 -0
- diffbio/operators/_masked_gene_transformer.py +118 -0
- diffbio/operators/_transformer_validation.py +50 -0
- diffbio/operators/alignment/__init__.py +51 -0
- diffbio/operators/alignment/profile_hmm.py +350 -0
- diffbio/operators/alignment/scoring.py +127 -0
- diffbio/operators/alignment/smith_waterman.py +261 -0
- diffbio/operators/alignment/soft_msa.py +419 -0
- diffbio/operators/assembly/__init__.py +27 -0
- diffbio/operators/assembly/gnn_assembly.py +252 -0
- diffbio/operators/assembly/metagenomic_binning.py +296 -0
- diffbio/operators/crispr/__init__.py +17 -0
- diffbio/operators/crispr/guide_scoring.py +269 -0
- diffbio/operators/drug_discovery/__init__.py +133 -0
- diffbio/operators/drug_discovery/_graph_utils.py +142 -0
- diffbio/operators/drug_discovery/admet_predictor.py +285 -0
- diffbio/operators/drug_discovery/attentive_fp.py +411 -0
- diffbio/operators/drug_discovery/dti.py +261 -0
- diffbio/operators/drug_discovery/fingerprint.py +490 -0
- diffbio/operators/drug_discovery/maccs_keys.py +267 -0
- diffbio/operators/drug_discovery/message_passing.py +200 -0
- diffbio/operators/drug_discovery/primitives.py +242 -0
- diffbio/operators/drug_discovery/property_predictor.py +163 -0
- diffbio/operators/drug_discovery/similarity.py +193 -0
- diffbio/operators/epigenomics/__init__.py +35 -0
- diffbio/operators/epigenomics/chromatin_state.py +491 -0
- diffbio/operators/epigenomics/contextual.py +288 -0
- diffbio/operators/epigenomics/fno_peak_calling.py +153 -0
- diffbio/operators/epigenomics/peak_calling.py +555 -0
- diffbio/operators/foundation_models/__init__.py +119 -0
- diffbio/operators/foundation_models/adapters.py +114 -0
- diffbio/operators/foundation_models/contracts.py +245 -0
- diffbio/operators/foundation_models/embedding_probe.py +83 -0
- diffbio/operators/foundation_models/experimental.py +128 -0
- diffbio/operators/foundation_models/foundation_model.py +332 -0
- diffbio/operators/foundation_models/frozen.py +59 -0
- diffbio/operators/foundation_models/precomputed.py +270 -0
- diffbio/operators/foundation_models/transformer_encoder.py +564 -0
- diffbio/operators/mapping/__init__.py +17 -0
- diffbio/operators/mapping/neural_mapper.py +493 -0
- diffbio/operators/metabolomics/__init__.py +39 -0
- diffbio/operators/metabolomics/spectral_similarity.py +315 -0
- diffbio/operators/molecular_dynamics/__init__.py +51 -0
- diffbio/operators/molecular_dynamics/force_field.py +265 -0
- diffbio/operators/molecular_dynamics/integrator.py +304 -0
- diffbio/operators/molecular_dynamics/primitives.py +115 -0
- diffbio/operators/multiomics/__init__.py +38 -0
- diffbio/operators/multiomics/hic_contact.py +377 -0
- diffbio/operators/multiomics/multiomics_vae.py +325 -0
- diffbio/operators/multiomics/spatial_deconvolution.py +316 -0
- diffbio/operators/multiomics/spatial_gene_detection.py +493 -0
- diffbio/operators/normalization/__init__.py +42 -0
- diffbio/operators/normalization/embedding.py +222 -0
- diffbio/operators/normalization/phate.py +400 -0
- diffbio/operators/normalization/umap.py +261 -0
- diffbio/operators/normalization/vae_normalizer.py +258 -0
- diffbio/operators/population/__init__.py +17 -0
- diffbio/operators/population/ancestry_estimation.py +274 -0
- diffbio/operators/preprocessing/__init__.py +76 -0
- diffbio/operators/preprocessing/adapter_removal.py +311 -0
- diffbio/operators/preprocessing/duplicate_filter.py +317 -0
- diffbio/operators/preprocessing/error_correction.py +287 -0
- diffbio/operators/protein/__init__.py +31 -0
- diffbio/operators/protein/secondary_structure.py +509 -0
- diffbio/operators/quality_filter.py +128 -0
- diffbio/operators/rna_structure/__init__.py +35 -0
- diffbio/operators/rna_structure/rna_folding.py +509 -0
- diffbio/operators/rnaseq/__init__.py +23 -0
- diffbio/operators/rnaseq/motif_discovery.py +251 -0
- diffbio/operators/rnaseq/splicing_psi.py +216 -0
- diffbio/operators/singlecell/__init__.py +193 -0
- diffbio/operators/singlecell/ambient_removal.py +333 -0
- diffbio/operators/singlecell/archetypes.py +191 -0
- diffbio/operators/singlecell/batch_correction.py +288 -0
- diffbio/operators/singlecell/cell_annotation.py +519 -0
- diffbio/operators/singlecell/communication.py +704 -0
- diffbio/operators/singlecell/differential_distribution.py +243 -0
- diffbio/operators/singlecell/doublet_detection.py +657 -0
- diffbio/operators/singlecell/downsampling.py +166 -0
- diffbio/operators/singlecell/enhanced_batch_correction.py +519 -0
- diffbio/operators/singlecell/grn_inference.py +336 -0
- diffbio/operators/singlecell/imputation.py +429 -0
- diffbio/operators/singlecell/knockdown_filter.py +176 -0
- diffbio/operators/singlecell/ot_trajectory.py +277 -0
- diffbio/operators/singlecell/simulation.py +444 -0
- diffbio/operators/singlecell/sindy_grn.py +247 -0
- diffbio/operators/singlecell/soft_clustering.py +211 -0
- diffbio/operators/singlecell/spatial_domains.py +677 -0
- diffbio/operators/singlecell/switch_de.py +184 -0
- diffbio/operators/singlecell/trajectory.py +447 -0
- diffbio/operators/singlecell/velocity.py +361 -0
- diffbio/operators/statistical/__init__.py +35 -0
- diffbio/operators/statistical/em_quantification.py +260 -0
- diffbio/operators/statistical/hmm.py +234 -0
- diffbio/operators/statistical/nb_glm.py +272 -0
- diffbio/operators/variant/__init__.py +64 -0
- diffbio/operators/variant/classifier.py +333 -0
- diffbio/operators/variant/cnn_classifier.py +255 -0
- diffbio/operators/variant/cnv_segmentation.py +678 -0
- diffbio/operators/variant/deepvariant_pileup.py +426 -0
- diffbio/operators/variant/pileup.py +240 -0
- diffbio/operators/variant/quality_recalibration.py +274 -0
- diffbio/pipelines/__init__.py +65 -0
- diffbio/pipelines/differential_expression.py +279 -0
- diffbio/pipelines/enhanced_variant_calling.py +326 -0
- diffbio/pipelines/perturbation.py +407 -0
- diffbio/pipelines/preprocessing.py +267 -0
- diffbio/pipelines/single_cell.py +366 -0
- diffbio/pipelines/variant_calling.py +490 -0
- diffbio/samplers/__init__.py +9 -0
- diffbio/samplers/perturbation_sampler.py +142 -0
- diffbio/sequences/__init__.py +34 -0
- diffbio/sequences/dna.py +239 -0
- diffbio/sources/__init__.py +149 -0
- diffbio/sources/_anndata_shared.py +89 -0
- diffbio/sources/_batch_iteration.py +37 -0
- diffbio/sources/_benchmark_source.py +152 -0
- diffbio/sources/_indexed_batch_source.py +38 -0
- diffbio/sources/_utils.py +45 -0
- diffbio/sources/anndata_interop.py +387 -0
- diffbio/sources/anndata_source.py +361 -0
- diffbio/sources/archive_ii.py +174 -0
- diffbio/sources/balifam.py +207 -0
- diffbio/sources/bam.py +265 -0
- diffbio/sources/bengrn_ground_truth.py +306 -0
- diffbio/sources/contextual_epigenomics.py +242 -0
- diffbio/sources/dti.py +359 -0
- diffbio/sources/embeddings.py +203 -0
- diffbio/sources/encode_peaks.py +223 -0
- diffbio/sources/fasta.py +226 -0
- diffbio/sources/immune_human.py +172 -0
- diffbio/sources/indexed_embeddings.py +128 -0
- diffbio/sources/indexed_view.py +191 -0
- diffbio/sources/molnet.py +493 -0
- diffbio/sources/multiomics.py +279 -0
- diffbio/sources/pancreas.py +108 -0
- diffbio/sources/perturbation/__init__.py +69 -0
- diffbio/sources/perturbation/_types.py +51 -0
- diffbio/sources/perturbation/_utils.py +125 -0
- diffbio/sources/perturbation/concat_source.py +115 -0
- diffbio/sources/perturbation/control_mapping.py +215 -0
- diffbio/sources/perturbation/experiment_config.py +261 -0
- diffbio/sources/perturbation/h5_metadata_cache.py +218 -0
- diffbio/sources/perturbation/output_space.py +52 -0
- diffbio/sources/perturbation/perturbation_source.py +513 -0
- diffbio/sources/seqfish.py +145 -0
- diffbio/sources/sequence_foundation.py +68 -0
- diffbio/sources/singlecell_foundation.py +68 -0
- diffbio/splitters/__init__.py +63 -0
- diffbio/splitters/base.py +251 -0
- diffbio/splitters/molecular.py +330 -0
- diffbio/splitters/perturbation.py +199 -0
- diffbio/splitters/random.py +217 -0
- diffbio/splitters/sequence.py +201 -0
- diffbio/utils/__init__.py +55 -0
- diffbio/utils/dependency_runtime.py +115 -0
- diffbio/utils/nn_utils.py +157 -0
- diffbio/utils/quality.py +45 -0
- diffbio/utils/training.py +585 -0
- diffbio-0.1.0.dist-info/METADATA +480 -0
- diffbio-0.1.0.dist-info/RECORD +202 -0
- diffbio-0.1.0.dist-info/WHEEL +4 -0
- 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
|
+
]
|