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,333 @@
|
|
|
1
|
+
"""Differentiable ambient RNA removal operator.
|
|
2
|
+
|
|
3
|
+
This module provides CellBender-style ambient RNA removal using a VAE
|
|
4
|
+
architecture that learns to separate cell-intrinsic from ambient signal.
|
|
5
|
+
|
|
6
|
+
Key technique: Uses variational autoencoder to model cell-specific expression
|
|
7
|
+
and ambient contamination fraction, enabling decontamination with uncertainty
|
|
8
|
+
quantification.
|
|
9
|
+
|
|
10
|
+
Applications: Removing ambient RNA contamination from single-cell RNA-seq data,
|
|
11
|
+
improving cell type identification and differential expression analysis.
|
|
12
|
+
|
|
13
|
+
Inherits from EncoderDecoderOperator to get:
|
|
14
|
+
|
|
15
|
+
- reparameterize() for sampling with reparameterization trick
|
|
16
|
+
- kl_divergence() for KL from standard normal
|
|
17
|
+
- elbo_loss() for combining reconstruction and KL losses
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import jax
|
|
25
|
+
import jax.numpy as jnp
|
|
26
|
+
from artifex.generative_models.core.base import MLP
|
|
27
|
+
from datarax.core.config import OperatorConfig
|
|
28
|
+
from flax import nnx
|
|
29
|
+
from jaxtyping import Array, Float, PyTree
|
|
30
|
+
|
|
31
|
+
from diffbio.configs import apply_stochastic_sampling_defaults
|
|
32
|
+
from diffbio.core.base_operators import EncoderDecoderOperator
|
|
33
|
+
from diffbio.utils.nn_utils import ARTIFEX_GELU_MLP_KWARGS
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class AmbientRemovalConfig(OperatorConfig):
|
|
40
|
+
"""Configuration for DifferentiableAmbientRemoval.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
n_genes: Number of genes in expression profiles.
|
|
44
|
+
latent_dim: Dimension of latent space.
|
|
45
|
+
hidden_dims: Hidden layer dimensions for encoder/decoder.
|
|
46
|
+
ambient_prior: Prior probability of ambient contamination.
|
|
47
|
+
temperature: Temperature for softmax operations.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
n_genes: int = 2000
|
|
51
|
+
latent_dim: int = 64
|
|
52
|
+
hidden_dims: list[int] = field(default_factory=lambda: [256, 128])
|
|
53
|
+
ambient_prior: float = 0.01
|
|
54
|
+
temperature: float = 1.0
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
"""Set stochastic defaults and validate."""
|
|
58
|
+
apply_stochastic_sampling_defaults(self)
|
|
59
|
+
super().__post_init__()
|
|
60
|
+
if not self.hidden_dims:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"AmbientRemovalConfig.hidden_dims must contain at least one hidden dimension."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AmbientEncoder(nnx.Module):
|
|
67
|
+
"""Encoder network for ambient removal VAE."""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
n_genes: int,
|
|
72
|
+
hidden_dims: list[int],
|
|
73
|
+
latent_dim: int,
|
|
74
|
+
*,
|
|
75
|
+
rngs: nnx.Rngs,
|
|
76
|
+
):
|
|
77
|
+
"""Initialize the encoder.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
n_genes: Number of input genes.
|
|
81
|
+
hidden_dims: Hidden layer dimensions.
|
|
82
|
+
latent_dim: Latent space dimension.
|
|
83
|
+
rngs: Random number generators.
|
|
84
|
+
"""
|
|
85
|
+
super().__init__()
|
|
86
|
+
self.backbone = MLP(
|
|
87
|
+
hidden_dims=hidden_dims,
|
|
88
|
+
in_features=n_genes,
|
|
89
|
+
rngs=rngs,
|
|
90
|
+
**ARTIFEX_GELU_MLP_KWARGS,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Latent projections
|
|
94
|
+
self.mean_proj = nnx.Linear(in_features=hidden_dims[-1], out_features=latent_dim, rngs=rngs)
|
|
95
|
+
self.logvar_proj = nnx.Linear(
|
|
96
|
+
in_features=hidden_dims[-1], out_features=latent_dim, rngs=rngs
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Contamination fraction projection
|
|
100
|
+
self.contamination_proj = nnx.Linear(in_features=hidden_dims[-1], out_features=1, rngs=rngs)
|
|
101
|
+
|
|
102
|
+
def __call__(
|
|
103
|
+
self,
|
|
104
|
+
counts: Float[Array, "n_cells n_genes"],
|
|
105
|
+
) -> tuple[
|
|
106
|
+
Float[Array, "n_cells latent_dim"],
|
|
107
|
+
Float[Array, "n_cells latent_dim"],
|
|
108
|
+
Float[Array, "n_cells"],
|
|
109
|
+
]:
|
|
110
|
+
"""Encode counts to latent space.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
counts: Input count matrix.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Tuple of (mean, logvar, contamination_fraction).
|
|
117
|
+
"""
|
|
118
|
+
# Log-normalize for encoder input
|
|
119
|
+
backbone_output = self.backbone(jnp.log1p(counts))
|
|
120
|
+
if isinstance(backbone_output, tuple):
|
|
121
|
+
raise TypeError("AmbientEncoder backbone must return a single tensor output.")
|
|
122
|
+
|
|
123
|
+
# Latent parameters
|
|
124
|
+
mean = self.mean_proj(backbone_output)
|
|
125
|
+
logvar = self.logvar_proj(backbone_output)
|
|
126
|
+
|
|
127
|
+
# Contamination fraction (bounded 0-1)
|
|
128
|
+
contamination = jax.nn.sigmoid(self.contamination_proj(backbone_output)).squeeze(-1)
|
|
129
|
+
|
|
130
|
+
return mean, logvar, contamination
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class AmbientDecoder(nnx.Module):
|
|
134
|
+
"""Decoder network for ambient removal VAE."""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
latent_dim: int,
|
|
139
|
+
hidden_dims: list[int],
|
|
140
|
+
n_genes: int,
|
|
141
|
+
*,
|
|
142
|
+
rngs: nnx.Rngs,
|
|
143
|
+
):
|
|
144
|
+
"""Initialize the decoder.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
latent_dim: Latent space dimension.
|
|
148
|
+
hidden_dims: Hidden layer dimensions (reversed from encoder).
|
|
149
|
+
n_genes: Number of output genes.
|
|
150
|
+
rngs: Random number generators.
|
|
151
|
+
"""
|
|
152
|
+
super().__init__()
|
|
153
|
+
decoder_hidden_dims = list(reversed(hidden_dims))
|
|
154
|
+
self.backbone = MLP(
|
|
155
|
+
hidden_dims=decoder_hidden_dims,
|
|
156
|
+
in_features=latent_dim,
|
|
157
|
+
rngs=rngs,
|
|
158
|
+
**ARTIFEX_GELU_MLP_KWARGS,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Output projection (log-rate for Poisson/NB)
|
|
162
|
+
self.output_proj = nnx.Linear(
|
|
163
|
+
in_features=decoder_hidden_dims[-1], out_features=n_genes, rngs=rngs
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def __call__(
|
|
167
|
+
self,
|
|
168
|
+
z: Float[Array, "n_cells latent_dim"],
|
|
169
|
+
) -> Float[Array, "n_cells n_genes"]:
|
|
170
|
+
"""Decode latent to gene expression rates.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
z: Latent representation.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Log-rate parameters for gene expression.
|
|
177
|
+
"""
|
|
178
|
+
backbone_output = self.backbone(z)
|
|
179
|
+
if isinstance(backbone_output, tuple):
|
|
180
|
+
raise TypeError("AmbientDecoder backbone must return a single tensor output.")
|
|
181
|
+
|
|
182
|
+
# Output log-rates
|
|
183
|
+
log_rate = self.output_proj(backbone_output)
|
|
184
|
+
|
|
185
|
+
return log_rate
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class DifferentiableAmbientRemoval(EncoderDecoderOperator):
|
|
189
|
+
"""Differentiable ambient RNA removal using VAE.
|
|
190
|
+
|
|
191
|
+
This operator removes ambient RNA contamination from single-cell
|
|
192
|
+
count data using a variational autoencoder that models both
|
|
193
|
+
cell-intrinsic expression and ambient contamination.
|
|
194
|
+
|
|
195
|
+
Algorithm:
|
|
196
|
+
1. Encode counts to latent space + contamination fraction
|
|
197
|
+
2. Sample latent (reparameterization trick)
|
|
198
|
+
3. Decode to cell-intrinsic expression rate
|
|
199
|
+
4. Compute decontaminated counts by subtracting ambient contribution
|
|
200
|
+
|
|
201
|
+
Inherits from EncoderDecoderOperator to get:
|
|
202
|
+
|
|
203
|
+
- reparameterize() for sampling with reparameterization trick
|
|
204
|
+
- kl_divergence() for KL from standard normal
|
|
205
|
+
- elbo_loss() for combining reconstruction and KL losses
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
config: AmbientRemovalConfig with model parameters.
|
|
209
|
+
rngs: Flax NNX random number generators.
|
|
210
|
+
name: Optional operator name.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
```python
|
|
214
|
+
config = AmbientRemovalConfig(n_genes=2000)
|
|
215
|
+
remover = DifferentiableAmbientRemoval(config, rngs=nnx.Rngs(42))
|
|
216
|
+
data = {"counts": counts, "ambient_profile": ambient}
|
|
217
|
+
result, state, meta = remover.apply(data, {}, None)
|
|
218
|
+
```
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
config: AmbientRemovalConfig,
|
|
224
|
+
*,
|
|
225
|
+
rngs: nnx.Rngs | None = None,
|
|
226
|
+
name: str | None = None,
|
|
227
|
+
):
|
|
228
|
+
"""Initialize the ambient removal operator.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
config: Ambient removal configuration.
|
|
232
|
+
rngs: Random number generators for initialization.
|
|
233
|
+
name: Optional operator name.
|
|
234
|
+
"""
|
|
235
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
236
|
+
|
|
237
|
+
if rngs is None:
|
|
238
|
+
rngs = nnx.Rngs(0)
|
|
239
|
+
|
|
240
|
+
self.ambient_prior = config.ambient_prior
|
|
241
|
+
self.stochastic = nnx.static(config.stochastic)
|
|
242
|
+
|
|
243
|
+
# Encoder
|
|
244
|
+
self.encoder = AmbientEncoder(
|
|
245
|
+
n_genes=config.n_genes,
|
|
246
|
+
hidden_dims=config.hidden_dims,
|
|
247
|
+
latent_dim=config.latent_dim,
|
|
248
|
+
rngs=rngs,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Decoder
|
|
252
|
+
self.decoder = AmbientDecoder(
|
|
253
|
+
latent_dim=config.latent_dim,
|
|
254
|
+
hidden_dims=config.hidden_dims,
|
|
255
|
+
n_genes=config.n_genes,
|
|
256
|
+
rngs=rngs,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# reparameterize() is inherited from EncoderDecoderOperator
|
|
260
|
+
# kl_divergence() is inherited from EncoderDecoderOperator
|
|
261
|
+
# elbo_loss() is inherited from EncoderDecoderOperator
|
|
262
|
+
|
|
263
|
+
def apply(
|
|
264
|
+
self,
|
|
265
|
+
data: PyTree,
|
|
266
|
+
state: PyTree,
|
|
267
|
+
metadata: dict[str, Any] | None,
|
|
268
|
+
random_params: Any = None,
|
|
269
|
+
stats: dict[str, Any] | None = None,
|
|
270
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
271
|
+
"""Apply ambient RNA removal.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
data: Dictionary containing:
|
|
275
|
+
- "counts": Raw count matrix (n_cells, n_genes)
|
|
276
|
+
- "ambient_profile": Ambient expression profile (n_genes,)
|
|
277
|
+
state: Element state (passed through unchanged)
|
|
278
|
+
metadata: Element metadata (passed through unchanged)
|
|
279
|
+
random_params: Random key for stochastic sampling
|
|
280
|
+
stats: Not used
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Tuple of (transformed_data, state, metadata):
|
|
284
|
+
- transformed_data contains:
|
|
285
|
+
|
|
286
|
+
- "counts": Original counts
|
|
287
|
+
- "ambient_profile": Original ambient profile
|
|
288
|
+
- "decontaminated_counts": Decontaminated counts
|
|
289
|
+
- "contamination_fraction": Estimated contamination per cell
|
|
290
|
+
- "latent": Latent representation
|
|
291
|
+
- "latent_mean": Mean of latent distribution
|
|
292
|
+
- "latent_logvar": Log variance of latent distribution
|
|
293
|
+
- "reconstructed": Reconstructed expression
|
|
294
|
+
- state is passed through unchanged
|
|
295
|
+
- metadata is passed through unchanged
|
|
296
|
+
"""
|
|
297
|
+
counts = data["counts"]
|
|
298
|
+
ambient_profile = data["ambient_profile"]
|
|
299
|
+
|
|
300
|
+
# Encode
|
|
301
|
+
mean, logvar, contamination = self.encoder(counts)
|
|
302
|
+
|
|
303
|
+
# Sample latent using inherited reparameterize (uses self.rngs)
|
|
304
|
+
z = self.reparameterize(mean, logvar)
|
|
305
|
+
|
|
306
|
+
# Decode to cell-intrinsic expression rate
|
|
307
|
+
log_rate = self.decoder(z)
|
|
308
|
+
cell_rate = jax.nn.softplus(log_rate) # Non-negative
|
|
309
|
+
|
|
310
|
+
# Compute total counts per cell for scaling
|
|
311
|
+
total_counts = jnp.sum(counts, axis=-1, keepdims=True)
|
|
312
|
+
|
|
313
|
+
# Model: observed = (1 - contamination) * cell + contamination * ambient * total
|
|
314
|
+
# Decontaminated = observed - contamination * ambient * total
|
|
315
|
+
ambient_contribution = contamination[:, None] * ambient_profile[None, :] * total_counts
|
|
316
|
+
decontaminated = jnp.maximum(counts - ambient_contribution, 0.0)
|
|
317
|
+
|
|
318
|
+
# Reconstructed expression (for loss computation)
|
|
319
|
+
cell_contribution = (1.0 - contamination[:, None]) * cell_rate
|
|
320
|
+
reconstructed = cell_contribution + ambient_contribution
|
|
321
|
+
|
|
322
|
+
transformed_data = {
|
|
323
|
+
"counts": counts,
|
|
324
|
+
"ambient_profile": ambient_profile,
|
|
325
|
+
"decontaminated_counts": decontaminated,
|
|
326
|
+
"contamination_fraction": contamination,
|
|
327
|
+
"latent": z,
|
|
328
|
+
"latent_mean": mean,
|
|
329
|
+
"latent_logvar": logvar,
|
|
330
|
+
"reconstructed": reconstructed,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return transformed_data, state, metadata
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Differentiable archetypal analysis for single-cell data.
|
|
2
|
+
|
|
3
|
+
Implements PCHA (Principal Convex Hull Analysis, Morup & Hansen 2012) as a
|
|
4
|
+
differentiable autoencoder with softmax bottleneck. Each cell is represented
|
|
5
|
+
as a temperature-controlled convex combination of learnable archetype
|
|
6
|
+
prototypes.
|
|
7
|
+
|
|
8
|
+
Algorithm:
|
|
9
|
+
1. Encode cells to archetype weight space via MLP.
|
|
10
|
+
2. Apply temperature-scaled softmax to enforce simplex constraints.
|
|
11
|
+
3. Reconstruct cells as the convex combination ``weights @ archetypes``.
|
|
12
|
+
|
|
13
|
+
Inherits from ``TemperatureOperator`` to get temperature-controlled smoothing.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import jax
|
|
21
|
+
from artifex.generative_models.core.base import MLP
|
|
22
|
+
from datarax.core.config import OperatorConfig
|
|
23
|
+
from flax import nnx
|
|
24
|
+
from jaxtyping import Array, Float, PyTree
|
|
25
|
+
|
|
26
|
+
from diffbio.core.base_operators import TemperatureOperator
|
|
27
|
+
from diffbio.utils.nn_utils import ensure_rngs, get_rng_key
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ArchetypalAnalysisConfig",
|
|
33
|
+
"DifferentiableArchetypalAnalysis",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ArchetypalAnalysisConfig(OperatorConfig):
|
|
39
|
+
"""Configuration for DifferentiableArchetypalAnalysis.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
n_genes: Number of input genes (features per cell).
|
|
43
|
+
n_archetypes: Number of archetype prototypes to learn.
|
|
44
|
+
hidden_dim: Hidden dimension for the encoder MLP.
|
|
45
|
+
temperature: Softmax temperature (lower = sharper assignments).
|
|
46
|
+
learnable_temperature: Whether temperature is a learnable parameter.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
n_genes: int = 2000
|
|
50
|
+
n_archetypes: int = 5
|
|
51
|
+
hidden_dim: int = 64
|
|
52
|
+
temperature: float = 1.0
|
|
53
|
+
learnable_temperature: bool = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DifferentiableArchetypalAnalysis(TemperatureOperator):
|
|
57
|
+
"""Differentiable archetypal analysis with softmax simplex constraints.
|
|
58
|
+
|
|
59
|
+
Each cell is encoded into archetype weight space via an MLP, then
|
|
60
|
+
temperature-controlled softmax produces simplex weights. The
|
|
61
|
+
reconstruction is the convex combination of learnable archetype
|
|
62
|
+
prototypes, enabling end-to-end gradient-based optimisation.
|
|
63
|
+
|
|
64
|
+
Inherits from ``TemperatureOperator`` to get:
|
|
65
|
+
|
|
66
|
+
- ``_temperature`` property for temperature-controlled smoothing
|
|
67
|
+
- ``soft_max()`` for logsumexp-based smooth maximum
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config: ArchetypalAnalysisConfig with model parameters.
|
|
71
|
+
rngs: Flax NNX random number generators.
|
|
72
|
+
name: Optional operator name.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
```python
|
|
76
|
+
import jax.numpy as jnp
|
|
77
|
+
config = ArchetypalAnalysisConfig(n_genes=2000, n_archetypes=5)
|
|
78
|
+
op = DifferentiableArchetypalAnalysis(config, rngs=nnx.Rngs(0))
|
|
79
|
+
data = {"counts": jnp.ones((100, 2000))}
|
|
80
|
+
result, state, meta = op.apply(data, {}, None)
|
|
81
|
+
```
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
config: ArchetypalAnalysisConfig,
|
|
87
|
+
*,
|
|
88
|
+
rngs: nnx.Rngs | None = None,
|
|
89
|
+
name: str | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Initialize the archetypal analysis operator.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
config: Archetypal analysis configuration.
|
|
95
|
+
rngs: Random number generators for weight initialisation.
|
|
96
|
+
name: Optional operator name.
|
|
97
|
+
"""
|
|
98
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
99
|
+
|
|
100
|
+
rngs = ensure_rngs(rngs)
|
|
101
|
+
|
|
102
|
+
self.encoder_layers = MLP(
|
|
103
|
+
hidden_dims=[config.hidden_dim],
|
|
104
|
+
in_features=config.n_genes,
|
|
105
|
+
activation="relu",
|
|
106
|
+
output_activation="relu",
|
|
107
|
+
use_batch_norm=False,
|
|
108
|
+
rngs=rngs,
|
|
109
|
+
)
|
|
110
|
+
self.projection = nnx.Linear(
|
|
111
|
+
in_features=config.hidden_dim,
|
|
112
|
+
out_features=config.n_archetypes,
|
|
113
|
+
rngs=rngs,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Learnable archetype prototypes (n_archetypes, n_genes)
|
|
117
|
+
key = get_rng_key(rngs, "params", fallback_seed=1)
|
|
118
|
+
init_archetypes = jax.random.normal(key, (config.n_archetypes, config.n_genes)) * 0.1
|
|
119
|
+
self.archetypes = nnx.Param(init_archetypes)
|
|
120
|
+
|
|
121
|
+
def encode(
|
|
122
|
+
self,
|
|
123
|
+
counts: Float[Array, "n_cells n_genes"],
|
|
124
|
+
) -> Float[Array, "n_cells n_archetypes"]:
|
|
125
|
+
"""Encode cells to simplex weights over archetypes.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
counts: Cell-by-gene count matrix.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Simplex weights of shape ``(n_cells, n_archetypes)``.
|
|
132
|
+
"""
|
|
133
|
+
hidden: jax.Array = self.encoder_layers(counts)
|
|
134
|
+
logits = self.projection(hidden)
|
|
135
|
+
weights = jax.nn.softmax(logits / self._temperature, axis=-1)
|
|
136
|
+
return weights
|
|
137
|
+
|
|
138
|
+
def reconstruct(
|
|
139
|
+
self,
|
|
140
|
+
weights: Float[Array, "n_cells n_archetypes"],
|
|
141
|
+
) -> Float[Array, "n_cells n_genes"]:
|
|
142
|
+
"""Reconstruct cells as convex combinations of archetypes.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
weights: Simplex weights per cell.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Reconstructed cell-by-gene matrix.
|
|
149
|
+
"""
|
|
150
|
+
return weights @ self.archetypes[...]
|
|
151
|
+
|
|
152
|
+
def apply(
|
|
153
|
+
self,
|
|
154
|
+
data: PyTree,
|
|
155
|
+
state: PyTree,
|
|
156
|
+
metadata: dict[str, Any] | None,
|
|
157
|
+
random_params: Any = None,
|
|
158
|
+
stats: dict[str, Any] | None = None,
|
|
159
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
160
|
+
"""Apply archetypal analysis to a cell-by-gene count matrix.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
data: Dictionary containing:
|
|
164
|
+
- ``"counts"``: Cell-by-gene matrix ``(n_cells, n_genes)``
|
|
165
|
+
state: Element state (passed through unchanged).
|
|
166
|
+
metadata: Element metadata (passed through unchanged).
|
|
167
|
+
random_params: Not used.
|
|
168
|
+
stats: Not used.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Tuple of ``(transformed_data, state, metadata)`` where
|
|
172
|
+
``transformed_data`` contains:
|
|
173
|
+
|
|
174
|
+
- ``"counts"``: Original count matrix
|
|
175
|
+
- ``"archetype_weights"``: Simplex weights ``(n_cells, n_archetypes)``
|
|
176
|
+
- ``"archetypes"``: Archetype prototypes ``(n_archetypes, n_genes)``
|
|
177
|
+
- ``"reconstructed"``: Reconstructed counts ``(n_cells, n_genes)``
|
|
178
|
+
"""
|
|
179
|
+
counts = data["counts"]
|
|
180
|
+
|
|
181
|
+
weights = self.encode(counts)
|
|
182
|
+
reconstructed = self.reconstruct(weights)
|
|
183
|
+
|
|
184
|
+
transformed_data = {
|
|
185
|
+
**data,
|
|
186
|
+
"archetype_weights": weights,
|
|
187
|
+
"archetypes": self.archetypes[...],
|
|
188
|
+
"reconstructed": reconstructed,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return transformed_data, state, metadata
|