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,261 @@
|
|
|
1
|
+
"""Differentiable UMAP dimensionality reduction.
|
|
2
|
+
|
|
3
|
+
This module implements a differentiable version of UMAP (Uniform Manifold
|
|
4
|
+
Approximation and Projection) for dimensionality reduction with end-to-end
|
|
5
|
+
gradient flow.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import flax.nnx as nnx
|
|
12
|
+
import jax
|
|
13
|
+
import jax.numpy as jnp
|
|
14
|
+
from artifex.generative_models.core.base import MLP
|
|
15
|
+
from datarax.core.config import OperatorConfig
|
|
16
|
+
from datarax.core.operator import OperatorModule
|
|
17
|
+
|
|
18
|
+
from diffbio.constants import DISTANCE_MASK_SENTINEL
|
|
19
|
+
from diffbio.core.graph_utils import (
|
|
20
|
+
compute_fuzzy_membership,
|
|
21
|
+
compute_pairwise_distances,
|
|
22
|
+
symmetrize_graph,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class UMAPConfig(OperatorConfig):
|
|
28
|
+
"""Configuration for differentiable UMAP.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
n_components: Number of dimensions in the embedding.
|
|
32
|
+
n_neighbors: Number of neighbors for local structure preservation.
|
|
33
|
+
metric: Distance metric ('euclidean' or 'cosine').
|
|
34
|
+
input_features: Number of input features (required for initialization).
|
|
35
|
+
hidden_dim: Hidden dimension for projection network.
|
|
36
|
+
stream_name: Name of the data stream to process.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
n_components: int = 2
|
|
40
|
+
n_neighbors: int = 15
|
|
41
|
+
metric: str = "euclidean"
|
|
42
|
+
input_features: int = 64
|
|
43
|
+
hidden_dim: int = 32
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
"""Validate the supported UMAP configuration surface."""
|
|
47
|
+
super().__post_init__()
|
|
48
|
+
|
|
49
|
+
if self.n_components <= 0:
|
|
50
|
+
raise ValueError(f"n_components must be positive, got {self.n_components}")
|
|
51
|
+
if self.n_neighbors <= 0:
|
|
52
|
+
raise ValueError(f"n_neighbors must be positive, got {self.n_neighbors}")
|
|
53
|
+
if self.input_features <= 0:
|
|
54
|
+
raise ValueError(f"input_features must be positive, got {self.input_features}")
|
|
55
|
+
if self.hidden_dim <= 0:
|
|
56
|
+
raise ValueError(f"hidden_dim must be positive, got {self.hidden_dim}")
|
|
57
|
+
if self.metric not in {"euclidean", "cosine"}:
|
|
58
|
+
raise ValueError(f"metric must be 'euclidean' or 'cosine', got '{self.metric}'")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ParametricUMAPHead(nnx.Module):
|
|
62
|
+
"""Parametric UMAP embedding head with learnable similarity curve."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, config: UMAPConfig, *, rngs: nnx.Rngs):
|
|
65
|
+
"""Initialize the parametric embedding head."""
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.curve_params = nnx.Param(jnp.array([1.929, 0.7915], dtype=jnp.float32))
|
|
68
|
+
self.projection_backbone = MLP(
|
|
69
|
+
hidden_dims=[config.hidden_dim, config.n_components],
|
|
70
|
+
in_features=config.input_features,
|
|
71
|
+
activation="relu",
|
|
72
|
+
output_activation=None,
|
|
73
|
+
use_batch_norm=False,
|
|
74
|
+
rngs=rngs,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def project(self, features: jax.Array) -> jax.Array:
|
|
78
|
+
"""Project high-dimensional features to low-dimensional embedding."""
|
|
79
|
+
embedding = self.projection_backbone(features)
|
|
80
|
+
if isinstance(embedding, tuple):
|
|
81
|
+
raise TypeError("ParametricUMAPHead projection backbone must return a single tensor.")
|
|
82
|
+
return embedding
|
|
83
|
+
|
|
84
|
+
def curve_coefficients(self) -> tuple[jax.Array, jax.Array]:
|
|
85
|
+
"""Return positive low-dimensional similarity curve coefficients."""
|
|
86
|
+
a = jnp.abs(self.curve_params[0]) + 1e-6
|
|
87
|
+
b = jnp.abs(self.curve_params[1]) + 1e-6
|
|
88
|
+
return a, b
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class DifferentiableUMAP(OperatorModule):
|
|
92
|
+
"""Differentiable UMAP for dimensionality reduction.
|
|
93
|
+
|
|
94
|
+
This operator implements a simplified differentiable version of UMAP that
|
|
95
|
+
learns a low-dimensional embedding while preserving local structure.
|
|
96
|
+
|
|
97
|
+
The UMAP loss function is:
|
|
98
|
+
L = sum_edges [p_ij * log(q_ij) + (1 - p_ij) * log(1 - q_ij)]
|
|
99
|
+
|
|
100
|
+
where:
|
|
101
|
+
- p_ij is the high-dimensional similarity (fuzzy set membership)
|
|
102
|
+
- q_ij is the low-dimensional similarity
|
|
103
|
+
|
|
104
|
+
This implementation uses a parametric approach with learnable curve
|
|
105
|
+
parameters (a, b) for the low-dimensional similarity function.
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
```python
|
|
109
|
+
config = UMAPConfig(
|
|
110
|
+
n_components=2,
|
|
111
|
+
n_neighbors=15,
|
|
112
|
+
)
|
|
113
|
+
umap = DifferentiableUMAP(config, rngs=rngs)
|
|
114
|
+
|
|
115
|
+
data = {"features": high_dim_data} # (n_samples, n_features)
|
|
116
|
+
result, state, metadata = umap.apply(data, {}, None)
|
|
117
|
+
embedding = result["embedding"] # (n_samples, n_components)
|
|
118
|
+
```
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, config: UMAPConfig, *, rngs: nnx.Rngs | None = None):
|
|
122
|
+
"""Initialize the differentiable UMAP.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
config: Configuration for UMAP.
|
|
126
|
+
rngs: Random number generators for initialization.
|
|
127
|
+
"""
|
|
128
|
+
super().__init__(config, rngs=rngs)
|
|
129
|
+
|
|
130
|
+
if rngs is None:
|
|
131
|
+
rngs = nnx.Rngs(0)
|
|
132
|
+
|
|
133
|
+
self.embedding_head = ParametricUMAPHead(config, rngs=rngs)
|
|
134
|
+
|
|
135
|
+
def _project(self, features: jax.Array) -> jax.Array:
|
|
136
|
+
"""Project high-dimensional features to low-dimensional embedding.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
features: Input features of shape (n_samples, n_features).
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Embedding of shape (n_samples, n_components).
|
|
143
|
+
"""
|
|
144
|
+
return self.embedding_head.project(features)
|
|
145
|
+
|
|
146
|
+
def _compute_high_dim_similarities(self, features: jax.Array) -> jax.Array:
|
|
147
|
+
"""Compute high-dimensional fuzzy set membership (p_ij).
|
|
148
|
+
|
|
149
|
+
Uses k-nearest neighbors and Gaussian kernel with local bandwidth.
|
|
150
|
+
Delegates to reusable graph utilities in ``diffbio.core.graph_utils``.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
features: Input features of shape (n_samples, n_features).
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Similarity matrix of shape (n_samples, n_samples).
|
|
157
|
+
"""
|
|
158
|
+
n_samples = features.shape[0]
|
|
159
|
+
n_neighbors = min(self.config.n_neighbors, n_samples - 1)
|
|
160
|
+
|
|
161
|
+
distances = compute_pairwise_distances(features, metric=self.config.metric)
|
|
162
|
+
distances = distances + jnp.eye(n_samples) * DISTANCE_MASK_SENTINEL
|
|
163
|
+
|
|
164
|
+
p_ij = compute_fuzzy_membership(distances, k=n_neighbors)
|
|
165
|
+
return symmetrize_graph(p_ij)
|
|
166
|
+
|
|
167
|
+
def _compute_low_dim_similarities(self, embedding: jax.Array) -> jax.Array:
|
|
168
|
+
"""Compute low-dimensional similarities (q_ij).
|
|
169
|
+
|
|
170
|
+
Uses the UMAP student-t like kernel:
|
|
171
|
+
q(d) = 1 / (1 + a * d^(2b))
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
embedding: Low-dimensional embedding of shape (n_samples, n_components).
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Similarity matrix of shape (n_samples, n_samples).
|
|
178
|
+
"""
|
|
179
|
+
n_samples = embedding.shape[0]
|
|
180
|
+
|
|
181
|
+
# Compute pairwise distances in embedding space
|
|
182
|
+
diff = embedding[:, None, :] - embedding[None, :, :]
|
|
183
|
+
distances_sq = jnp.sum(diff**2, axis=-1)
|
|
184
|
+
|
|
185
|
+
# UMAP similarity kernel
|
|
186
|
+
a, b = self.embedding_head.curve_coefficients()
|
|
187
|
+
|
|
188
|
+
q_ij = 1.0 / (1.0 + a * jnp.power(distances_sq + 1e-8, b))
|
|
189
|
+
|
|
190
|
+
# Set diagonal to 0
|
|
191
|
+
q_ij = q_ij * (1 - jnp.eye(n_samples))
|
|
192
|
+
|
|
193
|
+
return q_ij
|
|
194
|
+
|
|
195
|
+
def _compute_umap_loss(self, p_ij: jax.Array, q_ij: jax.Array) -> jax.Array:
|
|
196
|
+
"""Compute UMAP cross-entropy loss.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
p_ij: High-dimensional similarities.
|
|
200
|
+
q_ij: Low-dimensional similarities.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Scalar loss value.
|
|
204
|
+
"""
|
|
205
|
+
# Cross-entropy: -sum(p * log(q) + (1-p) * log(1-q))
|
|
206
|
+
eps = 1e-8
|
|
207
|
+
q_ij = jnp.clip(q_ij, eps, 1 - eps)
|
|
208
|
+
|
|
209
|
+
# Attractive term
|
|
210
|
+
attractive = -jnp.sum(p_ij * jnp.log(q_ij))
|
|
211
|
+
|
|
212
|
+
# Repulsive term
|
|
213
|
+
repulsive = -jnp.sum((1 - p_ij) * jnp.log(1 - q_ij))
|
|
214
|
+
|
|
215
|
+
return attractive + repulsive
|
|
216
|
+
|
|
217
|
+
def apply(
|
|
218
|
+
self,
|
|
219
|
+
data: dict[str, Any],
|
|
220
|
+
state: dict[str, Any],
|
|
221
|
+
metadata: dict | None,
|
|
222
|
+
random_params: dict | None = None,
|
|
223
|
+
stats: dict | None = None,
|
|
224
|
+
) -> tuple[dict, dict, dict | None]:
|
|
225
|
+
"""Apply UMAP dimensionality reduction.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
data: Dictionary containing:
|
|
229
|
+
- 'features': High-dimensional features of shape (n_samples, n_features)
|
|
230
|
+
state: Operator state dictionary.
|
|
231
|
+
metadata: Optional metadata dictionary.
|
|
232
|
+
random_params: Optional random parameters (unused).
|
|
233
|
+
stats: Optional statistics dictionary (unused).
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Tuple of (output_data, state, metadata) where output_data contains:
|
|
237
|
+
|
|
238
|
+
- 'features': Original high-dimensional features
|
|
239
|
+
- 'embedding': Low-dimensional embedding
|
|
240
|
+
- 'high_dim_similarities': Fuzzy set memberships (p_ij)
|
|
241
|
+
- 'low_dim_similarities': Embedding similarities (q_ij)
|
|
242
|
+
"""
|
|
243
|
+
del random_params, stats # Unused
|
|
244
|
+
|
|
245
|
+
features = data["features"]
|
|
246
|
+
|
|
247
|
+
# Project to low-dimensional space
|
|
248
|
+
embedding = self._project(features)
|
|
249
|
+
|
|
250
|
+
# Compute similarities
|
|
251
|
+
p_ij = self._compute_high_dim_similarities(features)
|
|
252
|
+
q_ij = self._compute_low_dim_similarities(embedding)
|
|
253
|
+
|
|
254
|
+
output_data = {
|
|
255
|
+
**data,
|
|
256
|
+
"embedding": embedding,
|
|
257
|
+
"high_dim_similarities": p_ij,
|
|
258
|
+
"low_dim_similarities": q_ij,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return output_data, state, metadata
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""VAE-based count normalization operator.
|
|
2
|
+
|
|
3
|
+
This module provides a variational autoencoder for normalizing gene
|
|
4
|
+
expression count data, inspired by scVI (Lopez et al., 2018).
|
|
5
|
+
|
|
6
|
+
Key technique: Learn a latent representation of cell state while
|
|
7
|
+
modeling count data with a configurable likelihood (Poisson or ZINB).
|
|
8
|
+
|
|
9
|
+
The ZINB (Zero-Inflated Negative Binomial) likelihood is particularly
|
|
10
|
+
suited for single-cell RNA-seq data which exhibits both overdispersion
|
|
11
|
+
and excess zeros (dropout events).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
import jax
|
|
19
|
+
import jax.numpy as jnp
|
|
20
|
+
from artifex.generative_models.core.losses.divergence import gaussian_kl_divergence
|
|
21
|
+
from datarax.core.config import OperatorConfig
|
|
22
|
+
from flax import nnx
|
|
23
|
+
from jaxtyping import Array, Float, PyTree
|
|
24
|
+
|
|
25
|
+
from diffbio.core.base_operators import EncoderDecoderOperator
|
|
26
|
+
from diffbio.operators._count_vae import CountReconstructionMixin, CountVAEBackboneMixin
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class VAENormalizerConfig(OperatorConfig):
|
|
33
|
+
"""Configuration for VAENormalizer.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
latent_dim: Dimension of latent space.
|
|
37
|
+
hidden_dims: Hidden layer dimensions for encoder/decoder.
|
|
38
|
+
n_genes: Number of genes (input/output dimension).
|
|
39
|
+
use_batch_correction: Whether to include batch effects.
|
|
40
|
+
likelihood: Likelihood model for reconstruction loss.
|
|
41
|
+
'poisson' for standard Poisson NLL, 'zinb' for
|
|
42
|
+
Zero-Inflated Negative Binomial.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
latent_dim: int = 10
|
|
46
|
+
hidden_dims: list[int] = field(default_factory=lambda: [128, 64])
|
|
47
|
+
n_genes: int = 2000
|
|
48
|
+
use_batch_correction: bool = False
|
|
49
|
+
likelihood: Literal["poisson", "zinb"] = "poisson"
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
"""Set stochastic defaults for VAE sampling and validate."""
|
|
53
|
+
object.__setattr__(self, "stochastic", True)
|
|
54
|
+
if self.stream_name is None:
|
|
55
|
+
object.__setattr__(self, "stream_name", "sample")
|
|
56
|
+
super().__post_init__()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class VAENormalizer(CountReconstructionMixin, CountVAEBackboneMixin, EncoderDecoderOperator):
|
|
60
|
+
"""Variational autoencoder for count normalization.
|
|
61
|
+
|
|
62
|
+
This operator learns a low-dimensional latent representation of
|
|
63
|
+
single-cell gene expression data while accounting for technical
|
|
64
|
+
factors like library size.
|
|
65
|
+
|
|
66
|
+
The model:
|
|
67
|
+
- Encoder: counts -> latent (mean, logvar)
|
|
68
|
+
- Reparameterization: z = mean + exp(0.5 * logvar) * epsilon
|
|
69
|
+
- Decoder: z -> gene expression rates (and optionally dispersion/dropout)
|
|
70
|
+
|
|
71
|
+
Supports two likelihood models:
|
|
72
|
+
- Poisson: Simple count model (default)
|
|
73
|
+
- ZINB: Zero-Inflated Negative Binomial for overdispersed data
|
|
74
|
+
with excess zeros, as used in scVI
|
|
75
|
+
|
|
76
|
+
Inherits from EncoderDecoderOperator to get:
|
|
77
|
+
|
|
78
|
+
- reparameterize() for sampling with reparameterization trick
|
|
79
|
+
- kl_divergence() for KL from standard normal
|
|
80
|
+
- elbo_loss() for combining reconstruction and KL losses
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config: VAENormalizerConfig with model parameters.
|
|
84
|
+
rngs: Flax NNX random number generators.
|
|
85
|
+
name: Optional operator name.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
```python
|
|
89
|
+
config = VAENormalizerConfig(n_genes=2000, latent_dim=10)
|
|
90
|
+
normalizer = VAENormalizer(config, rngs=nnx.Rngs(42))
|
|
91
|
+
data = {"counts": counts, "library_size": lib_size}
|
|
92
|
+
result, state, meta = normalizer.apply(data, {}, None)
|
|
93
|
+
```
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
config: VAENormalizerConfig,
|
|
99
|
+
*,
|
|
100
|
+
rngs: nnx.Rngs | None = None,
|
|
101
|
+
name: str | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Initialize the VAE normalizer.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config: VAE configuration.
|
|
107
|
+
rngs: Random number generators for initialization and sampling.
|
|
108
|
+
name: Optional operator name.
|
|
109
|
+
"""
|
|
110
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
111
|
+
safe_rngs = self._init_count_vae_operator(config=config, rngs=rngs)
|
|
112
|
+
|
|
113
|
+
decoder_out_dim = config.hidden_dims[0] if config.hidden_dims else config.latent_dim
|
|
114
|
+
|
|
115
|
+
# ZINB-specific decoder heads
|
|
116
|
+
if config.likelihood == "zinb":
|
|
117
|
+
self.fc_log_theta = nnx.Linear(
|
|
118
|
+
in_features=decoder_out_dim, out_features=config.n_genes, rngs=safe_rngs
|
|
119
|
+
)
|
|
120
|
+
self.fc_pi_logit = nnx.Linear(
|
|
121
|
+
in_features=decoder_out_dim, out_features=config.n_genes, rngs=safe_rngs
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def decode(
|
|
125
|
+
self,
|
|
126
|
+
z: Float[Array, "latent_dim"],
|
|
127
|
+
library_size: Float[Array, ""],
|
|
128
|
+
) -> dict[str, jax.Array]:
|
|
129
|
+
"""Decode latent representation to gene expression parameters.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
z: Latent representation.
|
|
133
|
+
library_size: Total counts (library size) for normalization.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dictionary with keys:
|
|
137
|
+
- 'log_rate': Log rates for each gene (always present).
|
|
138
|
+
- 'log_theta': Log dispersion parameter (ZINB only).
|
|
139
|
+
- 'pi_logit': Dropout logit for zero inflation (ZINB only).
|
|
140
|
+
"""
|
|
141
|
+
x = self.decode_hidden(z)
|
|
142
|
+
|
|
143
|
+
# Output layer (log rates, normalized by library size)
|
|
144
|
+
log_rate = self.fc_output(x)
|
|
145
|
+
|
|
146
|
+
result: dict[str, jax.Array] = {}
|
|
147
|
+
|
|
148
|
+
# ZINB heads are computed before adding library size
|
|
149
|
+
if self.config.likelihood == "zinb":
|
|
150
|
+
result["log_theta"] = self.fc_log_theta(x)
|
|
151
|
+
result["pi_logit"] = self.fc_pi_logit(x)
|
|
152
|
+
|
|
153
|
+
# Add library size effect (log scale)
|
|
154
|
+
log_rate = log_rate + jnp.log(library_size + 1e-8)
|
|
155
|
+
result["log_rate"] = log_rate
|
|
156
|
+
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
# kl_divergence() is inherited from EncoderDecoderOperator
|
|
160
|
+
|
|
161
|
+
def compute_elbo_loss(
|
|
162
|
+
self,
|
|
163
|
+
counts: Float[Array, "n_genes"],
|
|
164
|
+
library_size: Float[Array, ""],
|
|
165
|
+
) -> Float[Array, ""]:
|
|
166
|
+
"""Compute negative ELBO loss.
|
|
167
|
+
|
|
168
|
+
Uses gaussian_kl_divergence from artifex for the KL term.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
counts: Gene expression counts.
|
|
172
|
+
library_size: Total counts.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Negative ELBO (reconstruction loss + KL divergence).
|
|
176
|
+
"""
|
|
177
|
+
# Encode
|
|
178
|
+
mean, logvar = self.encode(counts)
|
|
179
|
+
|
|
180
|
+
# Sample latent using inherited reparameterize (uses self.rngs)
|
|
181
|
+
z = self.reparameterize(mean, logvar)
|
|
182
|
+
|
|
183
|
+
# Decode (returns dict)
|
|
184
|
+
decode_output = self.decode(z, library_size)
|
|
185
|
+
|
|
186
|
+
# Reconstruction loss
|
|
187
|
+
recon_loss = self.reconstruction_loss(counts, decode_output)
|
|
188
|
+
|
|
189
|
+
# KL divergence via artifex (handles unbatched 1-D inputs with sum)
|
|
190
|
+
kl = gaussian_kl_divergence(mean, logvar, reduction="sum")
|
|
191
|
+
|
|
192
|
+
return recon_loss + kl
|
|
193
|
+
|
|
194
|
+
def apply(
|
|
195
|
+
self,
|
|
196
|
+
data: PyTree,
|
|
197
|
+
state: PyTree,
|
|
198
|
+
metadata: dict[str, Any] | None,
|
|
199
|
+
random_params: Any = None,
|
|
200
|
+
stats: dict[str, Any] | None = None,
|
|
201
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
202
|
+
"""Apply VAE normalization to count data.
|
|
203
|
+
|
|
204
|
+
This method encodes the counts to latent space, samples a
|
|
205
|
+
latent representation, and decodes to normalized expression.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
data: Dictionary containing:
|
|
209
|
+
- "counts": Gene expression counts (n_genes,)
|
|
210
|
+
- "library_size": Total counts for the cell
|
|
211
|
+
state: Element state (passed through unchanged)
|
|
212
|
+
metadata: Element metadata (passed through unchanged)
|
|
213
|
+
random_params: Optional random parameters (not used)
|
|
214
|
+
stats: Not used
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Tuple of (transformed_data, state, metadata):
|
|
218
|
+
- transformed_data contains:
|
|
219
|
+
|
|
220
|
+
- "counts": Original counts
|
|
221
|
+
- "normalized": Normalized expression
|
|
222
|
+
- "latent_z": Sampled latent representation
|
|
223
|
+
- "latent_mean": Mean of latent distribution
|
|
224
|
+
- "latent_logvar": Log variance of latent distribution
|
|
225
|
+
- "log_rate": Decoded log rates
|
|
226
|
+
- state is passed through unchanged
|
|
227
|
+
- metadata is passed through unchanged
|
|
228
|
+
"""
|
|
229
|
+
counts = data["counts"]
|
|
230
|
+
library_size = data["library_size"]
|
|
231
|
+
|
|
232
|
+
# Encode to latent distribution
|
|
233
|
+
mean, logvar = self.encode(counts)
|
|
234
|
+
|
|
235
|
+
# Sample from latent distribution using inherited reparameterize
|
|
236
|
+
# (uses self.rngs from EncoderDecoderOperator)
|
|
237
|
+
z = self.reparameterize(mean, logvar)
|
|
238
|
+
|
|
239
|
+
# Decode to gene expression rates (returns dict)
|
|
240
|
+
decode_output = self.decode(z, library_size)
|
|
241
|
+
log_rate = decode_output["log_rate"]
|
|
242
|
+
|
|
243
|
+
# Compute normalized expression (rate normalized by library size)
|
|
244
|
+
# This is the "denoised" expression
|
|
245
|
+
normalized = jnp.exp(log_rate - jnp.log(library_size + 1e-8))
|
|
246
|
+
|
|
247
|
+
# Build output data
|
|
248
|
+
transformed_data = {
|
|
249
|
+
"counts": counts,
|
|
250
|
+
"library_size": library_size,
|
|
251
|
+
"normalized": normalized,
|
|
252
|
+
"latent_z": z,
|
|
253
|
+
"latent_mean": mean,
|
|
254
|
+
"latent_logvar": logvar,
|
|
255
|
+
"log_rate": log_rate,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return transformed_data, state, metadata
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Population genetics operators.
|
|
2
|
+
|
|
3
|
+
This module provides differentiable operators for population genetics analysis
|
|
4
|
+
including ancestry estimation, phasing, and imputation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from diffbio.operators.population.ancestry_estimation import (
|
|
8
|
+
AncestryEstimatorConfig,
|
|
9
|
+
DifferentiableAncestryEstimator,
|
|
10
|
+
create_ancestry_estimator,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AncestryEstimatorConfig",
|
|
15
|
+
"DifferentiableAncestryEstimator",
|
|
16
|
+
"create_ancestry_estimator",
|
|
17
|
+
]
|