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,345 @@
|
|
|
1
|
+
"""Statistical loss functions for differentiable bioinformatics.
|
|
2
|
+
|
|
3
|
+
This module provides differentiable implementations of statistical loss
|
|
4
|
+
functions commonly used in bioinformatics applications.
|
|
5
|
+
|
|
6
|
+
Includes:
|
|
7
|
+
- zinb_negative_log_likelihood: Zero-Inflated Negative Binomial NLL
|
|
8
|
+
- NegativeBinomialLoss: For count data modeling (scRNA-seq, RNA-seq)
|
|
9
|
+
- VAELoss: ELBO loss for variational autoencoders
|
|
10
|
+
- HMMLikelihoodLoss: Negative log-likelihood for HMM sequence models
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import jax
|
|
14
|
+
import jax.numpy as jnp
|
|
15
|
+
from flax import nnx
|
|
16
|
+
from jaxtyping import Array, Float, Int
|
|
17
|
+
|
|
18
|
+
from diffbio.constants import EPSILON
|
|
19
|
+
from diffbio.core import soft_ops
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def zinb_negative_log_likelihood(
|
|
23
|
+
counts: Float[Array, "... n_genes"],
|
|
24
|
+
log_rate: Float[Array, "... n_genes"],
|
|
25
|
+
log_theta: Float[Array, "... n_genes"],
|
|
26
|
+
pi_logit: Float[Array, "... n_genes"],
|
|
27
|
+
) -> Float[Array, ""]:
|
|
28
|
+
"""Zero-Inflated Negative Binomial negative log-likelihood.
|
|
29
|
+
|
|
30
|
+
Uses the scVI-style logit-space formulation for numerical stability.
|
|
31
|
+
All log-sigmoid terms are computed via softplus, avoiding explicit
|
|
32
|
+
materialisation of ``sigmoid(pi_logit)`` which is unstable when
|
|
33
|
+
``pi_logit`` is large positive (pi near 1, so 1-pi near 0).
|
|
34
|
+
|
|
35
|
+
Key identities::
|
|
36
|
+
|
|
37
|
+
log(sigmoid(pi)) = -softplus(-pi)
|
|
38
|
+
log(1-sigmoid(pi)) = -softplus(pi)
|
|
39
|
+
|
|
40
|
+
ZINB: ``P(x) = sigmoid(pi) * delta_0(x) + (1 - sigmoid(pi)) * NB(x; mu, theta)``
|
|
41
|
+
where ``mu = exp(log_rate)``, ``theta = exp(log_theta)``.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
counts: Observed counts.
|
|
45
|
+
log_rate: Log mean parameter from decoder.
|
|
46
|
+
log_theta: Log dispersion parameter.
|
|
47
|
+
pi_logit: Logit of zero-inflation probability.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Negative log-likelihood (scalar, summed over all elements).
|
|
51
|
+
"""
|
|
52
|
+
mu = jnp.exp(log_rate)
|
|
53
|
+
theta = jnp.exp(jnp.clip(log_theta, -10.0, 10.0))
|
|
54
|
+
eps = EPSILON
|
|
55
|
+
|
|
56
|
+
# Log-space sigmoid computations (numerically stable)
|
|
57
|
+
softplus_pi = jax.nn.softplus(-pi_logit) # = -log(sigmoid(pi_logit))
|
|
58
|
+
log_theta_mu = jnp.log(theta + mu + eps)
|
|
59
|
+
|
|
60
|
+
# NB(0) in log-space combined with dropout logit
|
|
61
|
+
pi_theta_log = -pi_logit + theta * (jnp.log(theta + eps) - log_theta_mu)
|
|
62
|
+
|
|
63
|
+
# Case x == 0: log[sigmoid(pi) + (1 - sigmoid(pi)) * NB(0)]
|
|
64
|
+
case_zero = jax.nn.softplus(pi_theta_log) - softplus_pi
|
|
65
|
+
|
|
66
|
+
# Case x > 0: log[(1 - sigmoid(pi)) * NB(x)]
|
|
67
|
+
case_nonzero = (
|
|
68
|
+
-softplus_pi
|
|
69
|
+
+ pi_theta_log
|
|
70
|
+
+ counts * (jnp.log(mu + eps) - log_theta_mu)
|
|
71
|
+
+ jax.scipy.special.gammaln(counts + theta)
|
|
72
|
+
- jax.scipy.special.gammaln(theta)
|
|
73
|
+
- jax.scipy.special.gammaln(counts + 1.0)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
is_zero = soft_ops.less(counts, jnp.array(eps), softness=0.01)
|
|
77
|
+
log_prob = is_zero * case_zero + (1.0 - is_zero) * case_nonzero
|
|
78
|
+
|
|
79
|
+
return -jnp.sum(log_prob)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class NegativeBinomialLoss(nnx.Module):
|
|
83
|
+
"""Negative binomial log-likelihood loss for count data.
|
|
84
|
+
|
|
85
|
+
The negative binomial distribution is parameterized by mean (mu) and
|
|
86
|
+
dispersion (theta), suitable for overdispersed count data like RNA-seq.
|
|
87
|
+
|
|
88
|
+
NB(x | mu, theta) = Gamma(x + theta) / (Gamma(theta) * Gamma(x + 1))
|
|
89
|
+
* (theta / (theta + mu))^theta
|
|
90
|
+
* (mu / (theta + mu))^x
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
eps: Small constant for numerical stability.
|
|
94
|
+
rngs: Flax NNX random number generators.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
```python
|
|
98
|
+
loss_fn = NegativeBinomialLoss(rngs=nnx.Rngs(42))
|
|
99
|
+
loss = loss_fn(counts, mu, theta)
|
|
100
|
+
```
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
eps: float = 1e-8,
|
|
106
|
+
*,
|
|
107
|
+
rngs: nnx.Rngs | None = None,
|
|
108
|
+
):
|
|
109
|
+
"""Initialize the NB loss.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
eps: Numerical stability constant.
|
|
113
|
+
rngs: Random number generators (not used, for API consistency).
|
|
114
|
+
"""
|
|
115
|
+
super().__init__()
|
|
116
|
+
self.eps = eps
|
|
117
|
+
|
|
118
|
+
def __call__(
|
|
119
|
+
self,
|
|
120
|
+
counts: Float[Array, "batch genes"],
|
|
121
|
+
mu: Float[Array, "batch genes"],
|
|
122
|
+
theta: Float[Array, "genes"],
|
|
123
|
+
) -> Float[Array, ""]:
|
|
124
|
+
"""Compute negative binomial negative log-likelihood.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
counts: Observed counts.
|
|
128
|
+
mu: Predicted mean.
|
|
129
|
+
theta: Dispersion parameter (per gene).
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Mean negative log-likelihood (scalar).
|
|
133
|
+
"""
|
|
134
|
+
# Ensure positive values
|
|
135
|
+
mu = jnp.maximum(mu, self.eps)
|
|
136
|
+
theta = jnp.maximum(theta, self.eps)
|
|
137
|
+
|
|
138
|
+
# Log-likelihood of NB distribution
|
|
139
|
+
# log P(x | mu, theta) = log Gamma(x + theta) - log Gamma(theta) - log Gamma(x + 1)
|
|
140
|
+
# + theta * log(theta / (theta + mu))
|
|
141
|
+
# + x * log(mu / (theta + mu))
|
|
142
|
+
|
|
143
|
+
log_theta_mu = jnp.log(theta[None, :] + mu + self.eps)
|
|
144
|
+
|
|
145
|
+
ll = (
|
|
146
|
+
jax.scipy.special.gammaln(counts + theta[None, :])
|
|
147
|
+
- jax.scipy.special.gammaln(theta[None, :])
|
|
148
|
+
- jax.scipy.special.gammaln(counts + 1)
|
|
149
|
+
+ theta[None, :] * (jnp.log(theta[None, :] + self.eps) - log_theta_mu)
|
|
150
|
+
+ counts * (jnp.log(mu + self.eps) - log_theta_mu)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Return mean negative log-likelihood
|
|
154
|
+
return -jnp.mean(ll)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class VAELoss(nnx.Module):
|
|
158
|
+
"""Variational autoencoder ELBO loss.
|
|
159
|
+
|
|
160
|
+
Combines reconstruction loss with KL divergence regularization:
|
|
161
|
+
ELBO = E[log p(x|z)] - KL(q(z|x) || p(z))
|
|
162
|
+
|
|
163
|
+
For Gaussian encoder and prior:
|
|
164
|
+
KL = -0.5 * sum(1 + log(var) - mean^2 - var)
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
kl_weight: Weight for KL divergence term (beta-VAE).
|
|
168
|
+
reconstruction_type: Type of reconstruction loss ("mse" or "bce").
|
|
169
|
+
rngs: Flax NNX random number generators.
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
```python
|
|
173
|
+
loss_fn = VAELoss(kl_weight=1.0, rngs=nnx.Rngs(42))
|
|
174
|
+
loss = loss_fn(x, x_recon, mean, logvar)
|
|
175
|
+
```
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
kl_weight: float = 1.0,
|
|
181
|
+
reconstruction_type: str = "mse",
|
|
182
|
+
*,
|
|
183
|
+
rngs: nnx.Rngs | None = None,
|
|
184
|
+
):
|
|
185
|
+
"""Initialize the VAE loss.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
kl_weight: Weight for KL term.
|
|
189
|
+
reconstruction_type: "mse" or "bce".
|
|
190
|
+
rngs: Random number generators (not used, for API consistency).
|
|
191
|
+
"""
|
|
192
|
+
super().__init__()
|
|
193
|
+
self.kl_weight = kl_weight
|
|
194
|
+
self.reconstruction_type = reconstruction_type
|
|
195
|
+
|
|
196
|
+
def __call__(
|
|
197
|
+
self,
|
|
198
|
+
x: Float[Array, "batch features"],
|
|
199
|
+
x_recon: Float[Array, "batch features"],
|
|
200
|
+
mean: Float[Array, "batch latent"],
|
|
201
|
+
logvar: Float[Array, "batch latent"],
|
|
202
|
+
) -> Float[Array, ""]:
|
|
203
|
+
"""Compute VAE ELBO loss.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
x: Original input.
|
|
207
|
+
x_recon: Reconstructed input.
|
|
208
|
+
mean: Encoder mean.
|
|
209
|
+
logvar: Encoder log-variance.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Negative ELBO (scalar).
|
|
213
|
+
"""
|
|
214
|
+
# Reconstruction loss
|
|
215
|
+
if self.reconstruction_type == "mse":
|
|
216
|
+
recon_loss = jnp.mean((x - x_recon) ** 2)
|
|
217
|
+
else: # bce
|
|
218
|
+
x_recon = jax.nn.sigmoid(x_recon)
|
|
219
|
+
recon_loss = -jnp.mean(
|
|
220
|
+
x * jnp.log(x_recon + 1e-8) + (1 - x) * jnp.log(1 - x_recon + 1e-8)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# KL divergence: -0.5 * sum(1 + log(var) - mean^2 - var)
|
|
224
|
+
kl_loss = -0.5 * jnp.mean(1 + logvar - mean**2 - jnp.exp(logvar))
|
|
225
|
+
|
|
226
|
+
# Total loss
|
|
227
|
+
return recon_loss + self.kl_weight * kl_loss
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class HMMLikelihoodLoss(nnx.Module):
|
|
231
|
+
"""HMM negative log-likelihood loss.
|
|
232
|
+
|
|
233
|
+
Computes the negative log-likelihood of sequences under a Hidden
|
|
234
|
+
Markov Model using the forward algorithm with logsumexp for stability.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
n_states: Number of hidden states.
|
|
238
|
+
n_emissions: Number of emission symbols.
|
|
239
|
+
rngs: Flax NNX random number generators.
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
```python
|
|
243
|
+
loss_fn = HMMLikelihoodLoss(n_states=3, n_emissions=4, rngs=rnx.Rngs(42))
|
|
244
|
+
nll = loss_fn(observations)
|
|
245
|
+
```
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
def __init__(
|
|
249
|
+
self,
|
|
250
|
+
n_states: int,
|
|
251
|
+
n_emissions: int,
|
|
252
|
+
*,
|
|
253
|
+
rngs: nnx.Rngs | None = None,
|
|
254
|
+
):
|
|
255
|
+
"""Initialize the HMM loss.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
n_states: Number of hidden states.
|
|
259
|
+
n_emissions: Number of emission symbols.
|
|
260
|
+
rngs: Random number generators.
|
|
261
|
+
"""
|
|
262
|
+
super().__init__()
|
|
263
|
+
|
|
264
|
+
if rngs is None:
|
|
265
|
+
rngs = nnx.Rngs(0)
|
|
266
|
+
|
|
267
|
+
self.n_states = n_states
|
|
268
|
+
self.n_emissions = n_emissions
|
|
269
|
+
|
|
270
|
+
key = rngs.params()
|
|
271
|
+
k1, k2, k3 = jax.random.split(key, 3)
|
|
272
|
+
|
|
273
|
+
# Learnable parameters (log-space for stability)
|
|
274
|
+
# Initial state distribution
|
|
275
|
+
self.log_initial = nnx.Param(jax.random.normal(k1, (n_states,)) * 0.1)
|
|
276
|
+
|
|
277
|
+
# Transition matrix (log probabilities)
|
|
278
|
+
self.log_transitions = nnx.Param(jax.random.normal(k2, (n_states, n_states)) * 0.1)
|
|
279
|
+
|
|
280
|
+
# Emission matrix (log probabilities)
|
|
281
|
+
self.log_emissions = nnx.Param(jax.random.normal(k3, (n_states, n_emissions)) * 0.1)
|
|
282
|
+
|
|
283
|
+
def _get_log_initial(self) -> Float[Array, "n_states"]:
|
|
284
|
+
"""Get normalized log initial distribution."""
|
|
285
|
+
return jax.nn.log_softmax(self.log_initial[...])
|
|
286
|
+
|
|
287
|
+
def _get_log_transitions(self) -> Float[Array, "n_states n_states"]:
|
|
288
|
+
"""Get normalized log transition matrix."""
|
|
289
|
+
return jax.nn.log_softmax(self.log_transitions[...], axis=-1)
|
|
290
|
+
|
|
291
|
+
def _get_log_emissions(self) -> Float[Array, "n_states n_emissions"]:
|
|
292
|
+
"""Get normalized log emission matrix."""
|
|
293
|
+
return jax.nn.log_softmax(self.log_emissions[...], axis=-1)
|
|
294
|
+
|
|
295
|
+
def _forward_single(
|
|
296
|
+
self,
|
|
297
|
+
observations: Int[Array, "seq_len"],
|
|
298
|
+
) -> Float[Array, ""]:
|
|
299
|
+
"""Forward algorithm for a single sequence.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
observations: Integer-encoded observations.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Log-likelihood of the sequence.
|
|
306
|
+
"""
|
|
307
|
+
log_init = self._get_log_initial()
|
|
308
|
+
log_trans = self._get_log_transitions()
|
|
309
|
+
log_emit = self._get_log_emissions()
|
|
310
|
+
|
|
311
|
+
# Initialize with first observation
|
|
312
|
+
log_alpha = log_init + log_emit[:, observations[0]]
|
|
313
|
+
|
|
314
|
+
# Forward pass
|
|
315
|
+
def step(log_alpha, obs):
|
|
316
|
+
# log_alpha: (n_states,)
|
|
317
|
+
# Transition: log_alpha[i] + log_trans[i, j] for all j
|
|
318
|
+
log_alpha_expanded = log_alpha[:, None] + log_trans # (n_states, n_states)
|
|
319
|
+
log_alpha_new = jax.scipy.special.logsumexp(log_alpha_expanded, axis=0)
|
|
320
|
+
# Add emission
|
|
321
|
+
log_alpha_new = log_alpha_new + log_emit[:, obs]
|
|
322
|
+
return log_alpha_new, None
|
|
323
|
+
|
|
324
|
+
log_alpha, _ = jax.lax.scan(step, log_alpha, observations[1:])
|
|
325
|
+
|
|
326
|
+
# Final log-likelihood
|
|
327
|
+
return jax.scipy.special.logsumexp(log_alpha)
|
|
328
|
+
|
|
329
|
+
def __call__(
|
|
330
|
+
self,
|
|
331
|
+
observations: Int[Array, "batch seq_len"],
|
|
332
|
+
) -> Float[Array, ""]:
|
|
333
|
+
"""Compute mean negative log-likelihood over batch.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
observations: Batch of integer-encoded sequences.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Mean negative log-likelihood (scalar).
|
|
340
|
+
"""
|
|
341
|
+
# Compute log-likelihood for each sequence
|
|
342
|
+
log_probs = jax.vmap(self._forward_single)(observations)
|
|
343
|
+
|
|
344
|
+
# Return mean negative log-likelihood
|
|
345
|
+
return -jnp.mean(log_probs)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Differentiable bioinformatics operators for DiffBio.
|
|
2
|
+
|
|
3
|
+
This module provides differentiable operators for common bioinformatics
|
|
4
|
+
operations such as quality filtering, sequence alignment, variant calling,
|
|
5
|
+
epigenomics analysis, and RNA-seq processing.
|
|
6
|
+
|
|
7
|
+
All operators extend Datarax's OperatorModule for seamless integration.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from diffbio.operators.quality_filter import (
|
|
11
|
+
DifferentiableQualityFilter,
|
|
12
|
+
QualityFilterConfig,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Import submodules for convenient access
|
|
16
|
+
from diffbio.operators import alignment
|
|
17
|
+
from diffbio.operators import assembly
|
|
18
|
+
from diffbio.operators import crispr
|
|
19
|
+
from diffbio.operators import drug_discovery
|
|
20
|
+
from diffbio.operators import epigenomics
|
|
21
|
+
from diffbio.operators import foundation_models
|
|
22
|
+
from diffbio.operators import mapping
|
|
23
|
+
from diffbio.operators import metabolomics
|
|
24
|
+
from diffbio.operators import molecular_dynamics
|
|
25
|
+
from diffbio.operators import multiomics
|
|
26
|
+
from diffbio.operators import normalization
|
|
27
|
+
from diffbio.operators import population
|
|
28
|
+
from diffbio.operators import preprocessing
|
|
29
|
+
from diffbio.operators import protein
|
|
30
|
+
from diffbio.operators import rna_structure
|
|
31
|
+
from diffbio.operators import rnaseq
|
|
32
|
+
from diffbio.operators import singlecell
|
|
33
|
+
from diffbio.operators import statistical
|
|
34
|
+
from diffbio.operators import variant
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
# Core quality filter
|
|
38
|
+
"DifferentiableQualityFilter",
|
|
39
|
+
"QualityFilterConfig",
|
|
40
|
+
# Submodules
|
|
41
|
+
"alignment",
|
|
42
|
+
"assembly",
|
|
43
|
+
"crispr",
|
|
44
|
+
"drug_discovery",
|
|
45
|
+
"epigenomics",
|
|
46
|
+
"foundation_models",
|
|
47
|
+
"mapping",
|
|
48
|
+
"metabolomics",
|
|
49
|
+
"molecular_dynamics",
|
|
50
|
+
"multiomics",
|
|
51
|
+
"normalization",
|
|
52
|
+
"population",
|
|
53
|
+
"preprocessing",
|
|
54
|
+
"protein",
|
|
55
|
+
"rna_structure",
|
|
56
|
+
"rnaseq",
|
|
57
|
+
"singlecell",
|
|
58
|
+
"statistical",
|
|
59
|
+
"variant",
|
|
60
|
+
]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Shared count-VAE building blocks for DiffBio operators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from artifex.generative_models.core.base import MLP
|
|
8
|
+
from flax import nnx
|
|
9
|
+
import jax
|
|
10
|
+
import jax.numpy as jnp
|
|
11
|
+
from jaxtyping import Array, Float
|
|
12
|
+
|
|
13
|
+
from diffbio.losses.statistical_losses import zinb_negative_log_likelihood
|
|
14
|
+
from diffbio.utils.nn_utils import ensure_rngs
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CountVAEBackboneMixin:
|
|
18
|
+
"""Shared encoder/decoder backbone for count-based VAEs."""
|
|
19
|
+
|
|
20
|
+
encoder_backbone: MLP | None
|
|
21
|
+
fc_mean: nnx.Linear
|
|
22
|
+
fc_logvar: nnx.Linear
|
|
23
|
+
decoder_backbone: MLP | None
|
|
24
|
+
fc_output: nnx.Linear
|
|
25
|
+
n_genes: int
|
|
26
|
+
stream_name: Any
|
|
27
|
+
|
|
28
|
+
def _init_count_vae_backbone(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
n_inputs: int,
|
|
32
|
+
latent_dim: int,
|
|
33
|
+
hidden_dims: list[int],
|
|
34
|
+
n_outputs: int,
|
|
35
|
+
rngs: nnx.Rngs | None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialise the shared count-VAE encoder and decoder layers."""
|
|
38
|
+
safe_rngs = ensure_rngs(rngs)
|
|
39
|
+
|
|
40
|
+
encoder_hidden_dims = list(hidden_dims)
|
|
41
|
+
decoder_hidden_dims = list(reversed(hidden_dims))
|
|
42
|
+
|
|
43
|
+
if encoder_hidden_dims:
|
|
44
|
+
self.encoder_backbone = MLP(
|
|
45
|
+
hidden_dims=encoder_hidden_dims,
|
|
46
|
+
in_features=n_inputs,
|
|
47
|
+
activation="relu",
|
|
48
|
+
output_activation="relu",
|
|
49
|
+
use_batch_norm=False,
|
|
50
|
+
rngs=safe_rngs,
|
|
51
|
+
)
|
|
52
|
+
encoder_out_dim = encoder_hidden_dims[-1]
|
|
53
|
+
else:
|
|
54
|
+
self.encoder_backbone = None
|
|
55
|
+
encoder_out_dim = n_inputs
|
|
56
|
+
|
|
57
|
+
self.fc_mean = nnx.Linear(
|
|
58
|
+
in_features=encoder_out_dim,
|
|
59
|
+
out_features=latent_dim,
|
|
60
|
+
rngs=safe_rngs,
|
|
61
|
+
)
|
|
62
|
+
self.fc_logvar = nnx.Linear(
|
|
63
|
+
in_features=encoder_out_dim,
|
|
64
|
+
out_features=latent_dim,
|
|
65
|
+
rngs=safe_rngs,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if decoder_hidden_dims:
|
|
69
|
+
self.decoder_backbone = MLP(
|
|
70
|
+
hidden_dims=decoder_hidden_dims,
|
|
71
|
+
in_features=latent_dim,
|
|
72
|
+
activation="relu",
|
|
73
|
+
output_activation="relu",
|
|
74
|
+
use_batch_norm=False,
|
|
75
|
+
rngs=safe_rngs,
|
|
76
|
+
)
|
|
77
|
+
decoder_out_dim = decoder_hidden_dims[-1]
|
|
78
|
+
else:
|
|
79
|
+
self.decoder_backbone = None
|
|
80
|
+
decoder_out_dim = latent_dim
|
|
81
|
+
|
|
82
|
+
self.fc_output = nnx.Linear(
|
|
83
|
+
in_features=decoder_out_dim,
|
|
84
|
+
out_features=n_outputs,
|
|
85
|
+
rngs=safe_rngs,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _init_count_vae_operator(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
config: Any,
|
|
92
|
+
rngs: nnx.Rngs | None,
|
|
93
|
+
) -> nnx.Rngs:
|
|
94
|
+
"""Initialise shared count-VAE operator state and return safe RNGs."""
|
|
95
|
+
safe_rngs = ensure_rngs(rngs)
|
|
96
|
+
self.n_genes = config.n_genes
|
|
97
|
+
self.stream_name = nnx.static(config.stream_name)
|
|
98
|
+
self._init_count_vae_backbone(
|
|
99
|
+
n_inputs=config.n_genes,
|
|
100
|
+
latent_dim=config.latent_dim,
|
|
101
|
+
hidden_dims=config.hidden_dims,
|
|
102
|
+
n_outputs=config.n_genes,
|
|
103
|
+
rngs=safe_rngs,
|
|
104
|
+
)
|
|
105
|
+
return safe_rngs
|
|
106
|
+
|
|
107
|
+
def encode(
|
|
108
|
+
self,
|
|
109
|
+
counts: Float[Array, "... n_genes"],
|
|
110
|
+
) -> tuple[Float[Array, "... latent_dim"], Float[Array, "... latent_dim"]]:
|
|
111
|
+
"""Encode count vectors to latent Gaussian parameters."""
|
|
112
|
+
x = jnp.log1p(counts)
|
|
113
|
+
if self.encoder_backbone is not None:
|
|
114
|
+
encoded: jax.Array = self.encoder_backbone(x)
|
|
115
|
+
x = encoded
|
|
116
|
+
mean = self.fc_mean(x)
|
|
117
|
+
logvar = jnp.clip(self.fc_logvar(x), -10.0, 10.0)
|
|
118
|
+
return mean, logvar
|
|
119
|
+
|
|
120
|
+
def decode_hidden(
|
|
121
|
+
self,
|
|
122
|
+
z: Float[Array, "... latent_dim"],
|
|
123
|
+
) -> Float[Array, "... hidden_dim"]:
|
|
124
|
+
"""Decode latent vectors to the shared decoder hidden representation."""
|
|
125
|
+
if self.decoder_backbone is None:
|
|
126
|
+
return z
|
|
127
|
+
decoded: jax.Array = self.decoder_backbone(z)
|
|
128
|
+
return decoded
|
|
129
|
+
|
|
130
|
+
def decode_rates(
|
|
131
|
+
self,
|
|
132
|
+
z: Float[Array, "... latent_dim"],
|
|
133
|
+
) -> Float[Array, "... n_outputs"]:
|
|
134
|
+
"""Decode latent vectors directly to output log-rates."""
|
|
135
|
+
return self.fc_output(self.decode_hidden(z))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class CountVAEBackbone(CountVAEBackboneMixin, nnx.Module):
|
|
139
|
+
"""Concrete test harness for the shared count-VAE backbone."""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
n_inputs: int,
|
|
145
|
+
latent_dim: int,
|
|
146
|
+
hidden_dims: list[int],
|
|
147
|
+
n_outputs: int,
|
|
148
|
+
rngs: nnx.Rngs | None = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Initialise a standalone shared count-VAE backbone."""
|
|
151
|
+
super().__init__()
|
|
152
|
+
self._init_count_vae_backbone(
|
|
153
|
+
n_inputs=n_inputs,
|
|
154
|
+
latent_dim=latent_dim,
|
|
155
|
+
hidden_dims=hidden_dims,
|
|
156
|
+
n_outputs=n_outputs,
|
|
157
|
+
rngs=rngs,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class CountReconstructionMixin:
|
|
162
|
+
"""Shared reconstruction losses for count-based VAE operators."""
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _poisson_nll(
|
|
166
|
+
counts: Float[Array, "... n_genes"],
|
|
167
|
+
log_rate: Float[Array, "... n_genes"],
|
|
168
|
+
) -> Float[Array, ""]:
|
|
169
|
+
"""Compute Poisson negative log-likelihood."""
|
|
170
|
+
rate = jnp.exp(log_rate)
|
|
171
|
+
return jnp.sum(rate - counts * log_rate)
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _zinb_nll(
|
|
175
|
+
counts: Float[Array, "... n_genes"],
|
|
176
|
+
log_rate: Float[Array, "... n_genes"],
|
|
177
|
+
log_theta: Float[Array, "... n_genes"],
|
|
178
|
+
pi_logit: Float[Array, "... n_genes"],
|
|
179
|
+
) -> Float[Array, ""]:
|
|
180
|
+
"""Compute Zero-Inflated Negative Binomial negative log-likelihood."""
|
|
181
|
+
return zinb_negative_log_likelihood(counts, log_rate, log_theta, pi_logit)
|
|
182
|
+
|
|
183
|
+
def reconstruction_loss(
|
|
184
|
+
self,
|
|
185
|
+
counts: Float[Array, "... n_genes"],
|
|
186
|
+
decode_output: dict[str, jax.Array],
|
|
187
|
+
) -> Float[Array, ""]:
|
|
188
|
+
"""Compute reconstruction loss for Poisson or ZINB decoder outputs."""
|
|
189
|
+
log_rate = decode_output["log_rate"]
|
|
190
|
+
if "log_theta" in decode_output:
|
|
191
|
+
return self._zinb_nll(
|
|
192
|
+
counts,
|
|
193
|
+
log_rate,
|
|
194
|
+
decode_output["log_theta"],
|
|
195
|
+
decode_output["pi_logit"],
|
|
196
|
+
)
|
|
197
|
+
return self._poisson_nll(counts, log_rate)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Shared scalar-loss balancing helpers for DiffBio operators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from flax import nnx
|
|
9
|
+
from jaxtyping import Array, Float
|
|
10
|
+
from opifex.core.physics.gradnorm import GradNormBalancer
|
|
11
|
+
|
|
12
|
+
from diffbio.utils.nn_utils import ensure_rngs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def combine_scalar_losses(
|
|
16
|
+
losses: Mapping[str, Float[Array, ""]],
|
|
17
|
+
*,
|
|
18
|
+
use_gradnorm: bool,
|
|
19
|
+
rngs: nnx.Rngs | None = None,
|
|
20
|
+
) -> Float[Array, ""]:
|
|
21
|
+
"""Combine scalar losses with optional GradNorm-based balancing.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
losses: Named scalar losses to combine.
|
|
25
|
+
use_gradnorm: Whether to balance losses with ``GradNormBalancer``.
|
|
26
|
+
rngs: Optional random generators used when constructing GradNorm.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Combined scalar loss.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: If *losses* is empty.
|
|
33
|
+
"""
|
|
34
|
+
if not losses:
|
|
35
|
+
msg = "losses must contain at least one scalar loss"
|
|
36
|
+
raise ValueError(msg)
|
|
37
|
+
|
|
38
|
+
loss_values = list(losses.values())
|
|
39
|
+
if use_gradnorm:
|
|
40
|
+
balancer = GradNormBalancer(
|
|
41
|
+
num_losses=len(loss_values),
|
|
42
|
+
rngs=ensure_rngs(rngs),
|
|
43
|
+
)
|
|
44
|
+
return balancer(loss_values)
|
|
45
|
+
|
|
46
|
+
total_loss = loss_values[0]
|
|
47
|
+
for loss_value in loss_values[1:]:
|
|
48
|
+
total_loss = total_loss + loss_value
|
|
49
|
+
return total_loss
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LossBalancingMixin:
|
|
53
|
+
"""Reusable operator mixin exposing ``compute_balanced_loss``."""
|
|
54
|
+
|
|
55
|
+
config: Any
|
|
56
|
+
|
|
57
|
+
def compute_balanced_loss(
|
|
58
|
+
self,
|
|
59
|
+
losses: Mapping[str, Float[Array, ""]],
|
|
60
|
+
) -> Float[Array, ""]:
|
|
61
|
+
"""Combine operator loss terms using the config's GradNorm flag."""
|
|
62
|
+
return combine_scalar_losses(
|
|
63
|
+
losses,
|
|
64
|
+
use_gradnorm=bool(getattr(self.config, "use_gradnorm", False)),
|
|
65
|
+
)
|