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,222 @@
|
|
|
1
|
+
"""Sequence embedding operators.
|
|
2
|
+
|
|
3
|
+
This module provides operators for converting one-hot encoded
|
|
4
|
+
DNA sequences into dense embeddings using convolutional networks.
|
|
5
|
+
|
|
6
|
+
Key technique: Use 1D convolutions to extract local sequence features,
|
|
7
|
+
then aggregate into a fixed-size representation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import jax
|
|
15
|
+
import jax.numpy as jnp
|
|
16
|
+
from datarax.core.config import OperatorConfig
|
|
17
|
+
from datarax.core.operator import OperatorModule
|
|
18
|
+
from flax import nnx
|
|
19
|
+
from jaxtyping import Array, Float, PyTree
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SequenceEmbeddingConfig(OperatorConfig):
|
|
26
|
+
"""Configuration for SequenceEmbedding.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
embedding_dim: Dimension of output embedding.
|
|
30
|
+
method: Embedding method ("conv" for convolutional).
|
|
31
|
+
kernel_size: Convolution kernel size.
|
|
32
|
+
num_conv_layers: Number of convolutional layers.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
embedding_dim: int = 64
|
|
36
|
+
method: str = "conv"
|
|
37
|
+
kernel_size: int = 7
|
|
38
|
+
num_conv_layers: int = 3
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SequenceEmbedding(OperatorModule):
|
|
42
|
+
"""Convolutional sequence embedding operator.
|
|
43
|
+
|
|
44
|
+
This operator converts one-hot encoded DNA sequences into dense
|
|
45
|
+
embeddings using a stack of 1D convolutions followed by global
|
|
46
|
+
average pooling.
|
|
47
|
+
|
|
48
|
+
The architecture:
|
|
49
|
+
1. Input: one-hot sequence (length, 4)
|
|
50
|
+
2. 1D convolutions with ReLU activation
|
|
51
|
+
3. Per-position features (length, embedding_dim)
|
|
52
|
+
4. Global average pooling -> fixed embedding (embedding_dim,)
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config: SequenceEmbeddingConfig with model parameters.
|
|
56
|
+
rngs: Flax NNX random number generators.
|
|
57
|
+
name: Optional operator name.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
```python
|
|
61
|
+
config = SequenceEmbeddingConfig(embedding_dim=64)
|
|
62
|
+
embedder = SequenceEmbedding(config, rngs=nnx.Rngs(42))
|
|
63
|
+
data = {"sequence": encoded_seq}
|
|
64
|
+
result, state, meta = embedder.apply(data, {}, None)
|
|
65
|
+
```
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
config: SequenceEmbeddingConfig,
|
|
71
|
+
*,
|
|
72
|
+
rngs: nnx.Rngs | None = None,
|
|
73
|
+
name: str | None = None,
|
|
74
|
+
):
|
|
75
|
+
"""Initialize the sequence embedding operator.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
config: Embedding configuration.
|
|
79
|
+
rngs: Random number generators for initialization.
|
|
80
|
+
name: Optional operator name.
|
|
81
|
+
"""
|
|
82
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
83
|
+
|
|
84
|
+
if rngs is None:
|
|
85
|
+
rngs = nnx.Rngs(0)
|
|
86
|
+
|
|
87
|
+
self.embedding_dim = config.embedding_dim
|
|
88
|
+
self.kernel_size = config.kernel_size
|
|
89
|
+
|
|
90
|
+
# Build convolutional layers
|
|
91
|
+
# Using Linear layers to simulate 1D convolution via sliding windows
|
|
92
|
+
# This is more compatible with varying sequence lengths
|
|
93
|
+
alphabet_size = 4
|
|
94
|
+
conv_layers: list[nnx.Linear] = []
|
|
95
|
+
|
|
96
|
+
# First layer: alphabet -> embedding_dim
|
|
97
|
+
# Input features: kernel_size * alphabet_size
|
|
98
|
+
first_in = config.kernel_size * alphabet_size
|
|
99
|
+
conv_layers.append(
|
|
100
|
+
nnx.Linear(in_features=first_in, out_features=config.embedding_dim, rngs=rngs)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Subsequent layers: embedding_dim -> embedding_dim
|
|
104
|
+
for _ in range(config.num_conv_layers - 1):
|
|
105
|
+
conv_layers.append(
|
|
106
|
+
nnx.Linear(
|
|
107
|
+
in_features=config.kernel_size * config.embedding_dim,
|
|
108
|
+
out_features=config.embedding_dim,
|
|
109
|
+
rngs=rngs,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self.conv_layers = nnx.List(conv_layers)
|
|
114
|
+
|
|
115
|
+
def _extract_windows(
|
|
116
|
+
self,
|
|
117
|
+
sequence: Float[Array, "length features"],
|
|
118
|
+
kernel_size: int,
|
|
119
|
+
) -> Float[Array, "length window_features"]:
|
|
120
|
+
"""Extract sliding windows from sequence.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
sequence: Input sequence (length, features).
|
|
124
|
+
kernel_size: Size of the sliding window.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Windows of shape (length, kernel_size * features).
|
|
128
|
+
"""
|
|
129
|
+
seq_len, num_features = sequence.shape
|
|
130
|
+
half_k = kernel_size // 2
|
|
131
|
+
|
|
132
|
+
# Pad sequence for edge handling
|
|
133
|
+
padded = jnp.pad(sequence, ((half_k, half_k), (0, 0)), mode="constant", constant_values=0.0)
|
|
134
|
+
|
|
135
|
+
# Extract windows using vmap
|
|
136
|
+
def extract_window(center: Array | int) -> Float[Array, "window_features"]:
|
|
137
|
+
window = jax.lax.dynamic_slice(padded, (center, 0), (kernel_size, num_features))
|
|
138
|
+
return window.flatten()
|
|
139
|
+
|
|
140
|
+
windows = jax.vmap(extract_window)(jnp.arange(seq_len))
|
|
141
|
+
return windows
|
|
142
|
+
|
|
143
|
+
def _apply_conv_layer(
|
|
144
|
+
self,
|
|
145
|
+
sequence: Float[Array, "length features"],
|
|
146
|
+
layer: nnx.Linear,
|
|
147
|
+
kernel_size: int,
|
|
148
|
+
) -> Float[Array, "length out_features"]:
|
|
149
|
+
"""Apply a convolutional layer using sliding windows.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
sequence: Input sequence.
|
|
153
|
+
layer: Linear layer to apply to each window.
|
|
154
|
+
kernel_size: Window size.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Output features at each position.
|
|
158
|
+
"""
|
|
159
|
+
# Extract windows
|
|
160
|
+
windows = self._extract_windows(sequence, kernel_size)
|
|
161
|
+
|
|
162
|
+
# Apply linear transformation to each window
|
|
163
|
+
output = jax.vmap(layer)(windows)
|
|
164
|
+
|
|
165
|
+
return output
|
|
166
|
+
|
|
167
|
+
def apply(
|
|
168
|
+
self,
|
|
169
|
+
data: PyTree,
|
|
170
|
+
state: PyTree,
|
|
171
|
+
metadata: dict[str, Any] | None,
|
|
172
|
+
random_params: Any = None,
|
|
173
|
+
stats: dict[str, Any] | None = None,
|
|
174
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
175
|
+
"""Apply sequence embedding to sequence data.
|
|
176
|
+
|
|
177
|
+
This method extracts dense embeddings from one-hot encoded
|
|
178
|
+
DNA sequences using convolutional feature extraction.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
data: Dictionary containing:
|
|
182
|
+
- "sequence": One-hot encoded sequence (length, alphabet_size)
|
|
183
|
+
state: Element state (passed through unchanged)
|
|
184
|
+
metadata: Element metadata (passed through unchanged)
|
|
185
|
+
random_params: Not used (deterministic operator)
|
|
186
|
+
stats: Not used
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Tuple of (transformed_data, state, metadata):
|
|
190
|
+
- transformed_data contains:
|
|
191
|
+
|
|
192
|
+
- "sequence": Original sequence
|
|
193
|
+
- "embedding": Global sequence embedding (embedding_dim,)
|
|
194
|
+
- "position_embeddings": Per-position features (length, embedding_dim)
|
|
195
|
+
- state is passed through unchanged
|
|
196
|
+
- metadata is passed through unchanged
|
|
197
|
+
"""
|
|
198
|
+
sequence = data["sequence"]
|
|
199
|
+
|
|
200
|
+
# Apply first conv layer
|
|
201
|
+
x = self._apply_conv_layer(sequence, self.conv_layers[0], self.kernel_size)
|
|
202
|
+
x = nnx.relu(x)
|
|
203
|
+
|
|
204
|
+
# Apply remaining conv layers
|
|
205
|
+
for layer in self.conv_layers[1:]:
|
|
206
|
+
x = self._apply_conv_layer(x, layer, self.kernel_size)
|
|
207
|
+
x = nnx.relu(x)
|
|
208
|
+
|
|
209
|
+
# x is now (length, embedding_dim)
|
|
210
|
+
position_embeddings = x
|
|
211
|
+
|
|
212
|
+
# Global average pooling to get fixed-size embedding
|
|
213
|
+
embedding = jnp.mean(position_embeddings, axis=0)
|
|
214
|
+
|
|
215
|
+
# Build output data
|
|
216
|
+
transformed_data = {
|
|
217
|
+
"sequence": sequence,
|
|
218
|
+
"embedding": embedding,
|
|
219
|
+
"position_embeddings": position_embeddings,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return transformed_data, state, metadata
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Differentiable PHATE dimensionality reduction.
|
|
2
|
+
|
|
3
|
+
This module implements a differentiable version of PHATE (Potential of
|
|
4
|
+
Heat-diffusion for Affinity-based Trajectory Embedding) for dimensionality
|
|
5
|
+
reduction with end-to-end gradient flow.
|
|
6
|
+
|
|
7
|
+
PHATE embeds high-dimensional data by:
|
|
8
|
+
|
|
9
|
+
1. Building an alpha-decay affinity kernel from pairwise distances.
|
|
10
|
+
2. Symmetrizing and row-normalizing to a Markov transition matrix.
|
|
11
|
+
3. Powering the matrix to diffusion time *t* via eigendecomposition.
|
|
12
|
+
4. Computing potential distances (log or sqrt transform of diffused matrix).
|
|
13
|
+
5. Applying classical MDS to the potential distance matrix for embedding.
|
|
14
|
+
|
|
15
|
+
Reference:
|
|
16
|
+
Moon et al., *Visualizing transitions and structure for biological data
|
|
17
|
+
exploration*, Nature Biotechnology, 2019.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import jax
|
|
25
|
+
import jax.numpy as jnp
|
|
26
|
+
from datarax.core.config import OperatorConfig
|
|
27
|
+
from datarax.core.operator import OperatorModule
|
|
28
|
+
from flax import nnx
|
|
29
|
+
|
|
30
|
+
from diffbio.constants import DISTANCE_MASK_SENTINEL, EPSILON
|
|
31
|
+
from diffbio.core import soft_ops
|
|
32
|
+
from diffbio.core.graph_utils import compute_pairwise_distances, symmetrize_graph
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# Regularization added to eigenvalues to prevent NaN gradients from
|
|
37
|
+
# repeated or near-zero eigenvalues in ``jnp.linalg.eigh``.
|
|
38
|
+
_EIGENVALUE_REGULARIZATION = 1e-6
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class PHATEConfig(OperatorConfig):
|
|
43
|
+
"""Configuration for differentiable PHATE.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
n_components: Number of dimensions in the embedding.
|
|
47
|
+
n_neighbors: Number of nearest neighbors for local bandwidth.
|
|
48
|
+
decay: Exponent for the alpha-decaying kernel. Higher values
|
|
49
|
+
produce sharper kernel tails (PHATE default 40).
|
|
50
|
+
diffusion_t: Power to which the diffusion operator is raised.
|
|
51
|
+
Controls the level of diffusion smoothing.
|
|
52
|
+
gamma: Informational distance constant. ``gamma=1`` gives the log
|
|
53
|
+
potential, ``gamma=0`` gives the sqrt potential.
|
|
54
|
+
input_features: Number of input features (used for projection network).
|
|
55
|
+
hidden_dim: Hidden dimension for the projection network.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
n_components: int = 2
|
|
59
|
+
n_neighbors: int = 5
|
|
60
|
+
decay: float = 40.0
|
|
61
|
+
diffusion_t: int = 10
|
|
62
|
+
gamma: float = 1.0
|
|
63
|
+
input_features: int = 64
|
|
64
|
+
hidden_dim: int = 32
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DifferentiablePHATE(OperatorModule):
|
|
68
|
+
"""Differentiable PHATE for dimensionality reduction.
|
|
69
|
+
|
|
70
|
+
Implements the full PHATE pipeline in a differentiable manner using JAX:
|
|
71
|
+
|
|
72
|
+
1. Pairwise distances via ``compute_pairwise_distances`` (DRY).
|
|
73
|
+
2. Alpha-decay affinity kernel: ``K(i,j) = exp(-(d(i,j)/sigma_i)^decay)``
|
|
74
|
+
where ``sigma_i`` is the distance to the k-th neighbor.
|
|
75
|
+
3. Symmetrize via ``symmetrize_graph`` (DRY).
|
|
76
|
+
4. Row-normalize to Markov matrix ``M``.
|
|
77
|
+
5. Diffusion ``M^t`` via eigendecomposition.
|
|
78
|
+
6. Potential distance: ``-log(M^t + eps)`` for ``gamma=1`` (log),
|
|
79
|
+
or ``(M^t)^((1-gamma)/2) / ((1-gamma)/2)`` otherwise.
|
|
80
|
+
7. Classical MDS on the potential distance matrix: center, eigendecompose,
|
|
81
|
+
take top ``n_components`` eigenvectors.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
```python
|
|
85
|
+
config = PHATEConfig(n_components=2, n_neighbors=5, diffusion_t=10)
|
|
86
|
+
phate = DifferentiablePHATE(config, rngs=rngs)
|
|
87
|
+
|
|
88
|
+
data = {"features": high_dim_data} # (n_samples, n_features)
|
|
89
|
+
result, state, metadata = phate.apply(data, {}, None)
|
|
90
|
+
embedding = result["embedding"] # (n_samples, n_components)
|
|
91
|
+
```
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
config: PHATEConfig,
|
|
97
|
+
*,
|
|
98
|
+
rngs: nnx.Rngs | None = None,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Initialize the differentiable PHATE operator.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
config: Configuration for PHATE.
|
|
104
|
+
rngs: Random number generators for parameter initialization.
|
|
105
|
+
"""
|
|
106
|
+
super().__init__(config, rngs=rngs)
|
|
107
|
+
self.config = config
|
|
108
|
+
|
|
109
|
+
def _build_alpha_decay_affinity(
|
|
110
|
+
self,
|
|
111
|
+
distances: jax.Array,
|
|
112
|
+
k: int,
|
|
113
|
+
decay: float,
|
|
114
|
+
) -> jax.Array:
|
|
115
|
+
"""Build alpha-decaying kernel following PHATE.
|
|
116
|
+
|
|
117
|
+
Computes a locality-adaptive Gaussian kernel:
|
|
118
|
+
``K(i,j) = exp(-alpha * (d(i,j) / sigma_i)^2)``
|
|
119
|
+
where ``sigma_i`` is the distance to the k-th nearest neighbor and
|
|
120
|
+
``alpha = decay / 2``.
|
|
121
|
+
|
|
122
|
+
This is a differentiable relaxation of the original PHATE kernel
|
|
123
|
+
``exp(-(d/sigma)^decay)`` which has vanishing gradients for high decay.
|
|
124
|
+
The Gaussian formulation preserves the same adaptive-bandwidth locality
|
|
125
|
+
structure while maintaining stable gradient flow.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
distances: Pairwise distance matrix of shape ``(n, n)`` with
|
|
129
|
+
diagonal masked to ``DISTANCE_MASK_SENTINEL``.
|
|
130
|
+
k: Number of neighbors for local bandwidth estimation.
|
|
131
|
+
decay: Controls kernel sharpness. Mapped to Gaussian scale
|
|
132
|
+
``alpha = decay / 2``. Higher values produce sharper falloff.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Affinity matrix of shape ``(n, n)`` with zero diagonal.
|
|
136
|
+
"""
|
|
137
|
+
n = distances.shape[0]
|
|
138
|
+
k_eff = min(k, n - 1)
|
|
139
|
+
|
|
140
|
+
# Local bandwidth: distance to the k-th nearest neighbor
|
|
141
|
+
sorted_dists = soft_ops.sort(distances, axis=-1, softness=0.1)
|
|
142
|
+
sigma = jnp.maximum(sorted_dists[:, k_eff - 1], EPSILON)
|
|
143
|
+
|
|
144
|
+
# Gaussian kernel with adaptive bandwidth:
|
|
145
|
+
# K(i,j) = exp(-alpha * (d(i,j) / sigma_i)^2)
|
|
146
|
+
# alpha = decay / 2 maps the PHATE decay parameter to
|
|
147
|
+
# the Gaussian scale while preserving locality structure.
|
|
148
|
+
alpha = decay / 2.0
|
|
149
|
+
ratio = distances / sigma[:, None]
|
|
150
|
+
affinity = jnp.exp(-alpha * ratio**2)
|
|
151
|
+
|
|
152
|
+
# Zero diagonal
|
|
153
|
+
affinity = affinity * (1.0 - jnp.eye(n))
|
|
154
|
+
|
|
155
|
+
return affinity
|
|
156
|
+
|
|
157
|
+
def _build_markov_matrix(self, affinity_sym: jax.Array) -> jax.Array:
|
|
158
|
+
"""Row-normalize a symmetric affinity matrix to a Markov matrix.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
affinity_sym: Symmetric affinity matrix of shape ``(n, n)``.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Row-stochastic Markov matrix of shape ``(n, n)``.
|
|
165
|
+
"""
|
|
166
|
+
row_sums = jnp.sum(affinity_sym, axis=1, keepdims=True)
|
|
167
|
+
return affinity_sym / jnp.maximum(row_sums, EPSILON)
|
|
168
|
+
|
|
169
|
+
def _diffuse_eigendecomposition(
|
|
170
|
+
self,
|
|
171
|
+
markov: jax.Array,
|
|
172
|
+
t: int,
|
|
173
|
+
) -> jax.Array:
|
|
174
|
+
"""Compute ``M^t`` via eigendecomposition.
|
|
175
|
+
|
|
176
|
+
Symmetrizes the Markov matrix via the similarity transform
|
|
177
|
+
``S = D^{1/2} M D^{-1/2}`` (where ``D = diag(rowsums)``),
|
|
178
|
+
eigendecomposes the symmetric ``S``, powers the eigenvalues,
|
|
179
|
+
and reconstructs ``M^t = D^{-1/2} V diag(lambda^t) V^T D^{1/2}``.
|
|
180
|
+
|
|
181
|
+
A small regularization is added to diagonal of ``S`` before
|
|
182
|
+
eigendecomposition so that repeated eigenvalues are slightly split,
|
|
183
|
+
preventing NaN gradients in the backward pass.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
markov: Row-stochastic Markov matrix of shape ``(n, n)``.
|
|
187
|
+
t: Diffusion time (power to raise the matrix to).
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Diffusion operator ``M^t`` of shape ``(n, n)``.
|
|
191
|
+
"""
|
|
192
|
+
n = markov.shape[0]
|
|
193
|
+
|
|
194
|
+
if t == 0:
|
|
195
|
+
return jnp.eye(n)
|
|
196
|
+
|
|
197
|
+
# Similarity transform to symmetric matrix with same spectrum
|
|
198
|
+
degree = jnp.sum(markov, axis=1)
|
|
199
|
+
degree = jnp.maximum(degree, EPSILON)
|
|
200
|
+
d_sqrt = jnp.sqrt(degree)
|
|
201
|
+
d_inv_sqrt = 1.0 / d_sqrt
|
|
202
|
+
|
|
203
|
+
sym_matrix = d_sqrt[:, None] * markov * d_inv_sqrt[None, :]
|
|
204
|
+
|
|
205
|
+
# Force exact symmetry
|
|
206
|
+
sym_matrix = 0.5 * (sym_matrix + sym_matrix.T)
|
|
207
|
+
|
|
208
|
+
# Add tiny diagonal regularization to split repeated eigenvalues
|
|
209
|
+
reg = _EIGENVALUE_REGULARIZATION * jnp.eye(n)
|
|
210
|
+
sym_matrix = sym_matrix + reg
|
|
211
|
+
|
|
212
|
+
# Eigendecompose
|
|
213
|
+
eigenvalues, eigenvectors = jnp.linalg.eigh(sym_matrix)
|
|
214
|
+
|
|
215
|
+
# Clamp eigenvalues to non-negative, normalize by max
|
|
216
|
+
eigenvalues = jnp.maximum(eigenvalues, 0.0)
|
|
217
|
+
lambda_max = jnp.maximum(eigenvalues[-1], EPSILON)
|
|
218
|
+
eigenvalues_normalized = eigenvalues / lambda_max
|
|
219
|
+
|
|
220
|
+
# Power the normalized eigenvalues
|
|
221
|
+
eigenvalues_t = eigenvalues_normalized**t
|
|
222
|
+
|
|
223
|
+
# Reconstruct: M^t = D^{-1/2} V diag(lambda^t) V^T D^{1/2}
|
|
224
|
+
v_left = d_inv_sqrt[:, None] * eigenvectors
|
|
225
|
+
v_right = d_sqrt[:, None] * eigenvectors
|
|
226
|
+
diffusion_op = v_left @ jnp.diag(eigenvalues_t) @ v_right.T
|
|
227
|
+
|
|
228
|
+
# Ensure row-stochasticity after reconstruction
|
|
229
|
+
row_sums = jnp.sum(diffusion_op, axis=1, keepdims=True)
|
|
230
|
+
diffusion_op = diffusion_op / jnp.maximum(row_sums, EPSILON)
|
|
231
|
+
|
|
232
|
+
# Clamp to non-negative (numerical noise can create tiny negatives)
|
|
233
|
+
diffusion_op = jnp.maximum(diffusion_op, 0.0)
|
|
234
|
+
|
|
235
|
+
return diffusion_op
|
|
236
|
+
|
|
237
|
+
def _compute_potential_distances(
|
|
238
|
+
self,
|
|
239
|
+
diffusion_op: jax.Array,
|
|
240
|
+
gamma: float,
|
|
241
|
+
) -> jax.Array:
|
|
242
|
+
"""Compute potential distances from the diffusion operator.
|
|
243
|
+
|
|
244
|
+
Following PHATE:
|
|
245
|
+
- ``gamma=1``: log potential ``D = -log(M^t + eps)``
|
|
246
|
+
- ``gamma=0``: sqrt potential ``D = (M^t)^{1/2} / (1/2)``
|
|
247
|
+
- ``gamma=-1``: identity (no transform)
|
|
248
|
+
- general: ``D = (M^t)^c / c`` where ``c = (1-gamma)/2``
|
|
249
|
+
|
|
250
|
+
The result is symmetrized for use in MDS.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
diffusion_op: Diffusion operator ``M^t`` of shape ``(n, n)``.
|
|
254
|
+
gamma: Informational distance constant.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Symmetric potential distance matrix of shape ``(n, n)``.
|
|
258
|
+
"""
|
|
259
|
+
if gamma == 1.0:
|
|
260
|
+
# Log potential (standard PHATE)
|
|
261
|
+
potential = -jnp.log(diffusion_op + EPSILON)
|
|
262
|
+
elif gamma == -1.0:
|
|
263
|
+
# Identity (no transform)
|
|
264
|
+
potential = diffusion_op
|
|
265
|
+
else:
|
|
266
|
+
# General power transform, covers gamma=0 (sqrt) etc.
|
|
267
|
+
c = (1.0 - gamma) / 2.0
|
|
268
|
+
potential = jnp.power(diffusion_op + EPSILON, c) / jnp.maximum(c, EPSILON)
|
|
269
|
+
|
|
270
|
+
# Symmetrize for MDS
|
|
271
|
+
potential = 0.5 * (potential + potential.T)
|
|
272
|
+
|
|
273
|
+
return potential
|
|
274
|
+
|
|
275
|
+
def _classical_mds(
|
|
276
|
+
self,
|
|
277
|
+
distance_matrix: jax.Array,
|
|
278
|
+
n_components: int,
|
|
279
|
+
) -> jax.Array:
|
|
280
|
+
"""Perform classical MDS on a distance matrix.
|
|
281
|
+
|
|
282
|
+
Classical MDS (Torgerson scaling):
|
|
283
|
+
|
|
284
|
+
1. Square the distances: ``D2 = D^2``
|
|
285
|
+
2. Double-center: ``B = -0.5 * (D2 - row_mean - col_mean + grand_mean)``
|
|
286
|
+
3. Eigendecompose B and take the top ``n_components`` eigenvectors
|
|
287
|
+
scaled by ``sqrt(eigenvalue)``.
|
|
288
|
+
|
|
289
|
+
A small diagonal regularization is added to the centered matrix
|
|
290
|
+
before eigendecomposition to split repeated eigenvalues and ensure
|
|
291
|
+
stable gradients.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
distance_matrix: Symmetric distance matrix of shape ``(n, n)``.
|
|
295
|
+
n_components: Number of embedding dimensions.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Embedding of shape ``(n, n_components)``.
|
|
299
|
+
"""
|
|
300
|
+
n = distance_matrix.shape[0]
|
|
301
|
+
d_squared = distance_matrix**2
|
|
302
|
+
|
|
303
|
+
# Double centering
|
|
304
|
+
row_mean = jnp.mean(d_squared, axis=1, keepdims=True)
|
|
305
|
+
col_mean = jnp.mean(d_squared, axis=0, keepdims=True)
|
|
306
|
+
grand_mean = jnp.mean(d_squared)
|
|
307
|
+
centered = -0.5 * (d_squared - row_mean - col_mean + grand_mean)
|
|
308
|
+
|
|
309
|
+
# Force symmetry
|
|
310
|
+
centered = 0.5 * (centered + centered.T)
|
|
311
|
+
|
|
312
|
+
# Regularize to split degenerate eigenvalues for stable gradients
|
|
313
|
+
centered = centered + _EIGENVALUE_REGULARIZATION * jnp.eye(n)
|
|
314
|
+
|
|
315
|
+
# Eigendecompose: eigh returns eigenvalues in ascending order
|
|
316
|
+
eigenvalues, eigenvectors = jnp.linalg.eigh(centered)
|
|
317
|
+
|
|
318
|
+
# Take the top n_components (largest eigenvalues = last ones)
|
|
319
|
+
top_eigenvalues = eigenvalues[-n_components:]
|
|
320
|
+
top_eigenvectors = eigenvectors[:, -n_components:]
|
|
321
|
+
|
|
322
|
+
# Clamp to non-negative for sqrt
|
|
323
|
+
top_eigenvalues = jnp.maximum(top_eigenvalues, EPSILON)
|
|
324
|
+
|
|
325
|
+
# Embedding: eigenvectors scaled by sqrt(eigenvalues)
|
|
326
|
+
embedding = top_eigenvectors * jnp.sqrt(top_eigenvalues)[None, :]
|
|
327
|
+
|
|
328
|
+
# Reverse so the largest component comes first
|
|
329
|
+
embedding = embedding[:, ::-1]
|
|
330
|
+
|
|
331
|
+
return embedding
|
|
332
|
+
|
|
333
|
+
def apply(
|
|
334
|
+
self,
|
|
335
|
+
data: dict[str, Any],
|
|
336
|
+
state: dict[str, Any],
|
|
337
|
+
metadata: dict | None,
|
|
338
|
+
random_params: dict | None = None,
|
|
339
|
+
stats: dict | None = None,
|
|
340
|
+
) -> tuple[dict, dict, dict | None]:
|
|
341
|
+
"""Apply PHATE dimensionality reduction.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
data: Dictionary containing:
|
|
345
|
+
- ``"features"``: High-dimensional features ``(n_samples, n_features)``
|
|
346
|
+
state: Operator state dictionary.
|
|
347
|
+
metadata: Optional metadata dictionary.
|
|
348
|
+
random_params: Optional random parameters (unused).
|
|
349
|
+
stats: Optional statistics dictionary (unused).
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Tuple of ``(output_data, state, metadata)`` where output_data contains:
|
|
353
|
+
|
|
354
|
+
- ``"features"``: Original high-dimensional features
|
|
355
|
+
- ``"embedding"``: Low-dimensional PHATE embedding
|
|
356
|
+
``(n_samples, n_components)``
|
|
357
|
+
- ``"potential_distances"``: Symmetric potential distance matrix
|
|
358
|
+
``(n_samples, n_samples)``
|
|
359
|
+
- ``"diffusion_operator"``: Row-stochastic diffusion matrix
|
|
360
|
+
``M^t`` ``(n_samples, n_samples)``
|
|
361
|
+
"""
|
|
362
|
+
del random_params, stats # Unused
|
|
363
|
+
|
|
364
|
+
features = data["features"]
|
|
365
|
+
n_samples = features.shape[0]
|
|
366
|
+
|
|
367
|
+
# Step 1: Pairwise distances (DRY: reuse graph_utils)
|
|
368
|
+
distances = compute_pairwise_distances(features, metric="euclidean")
|
|
369
|
+
|
|
370
|
+
# Mask diagonal for neighbor computation
|
|
371
|
+
distances_masked = distances + jnp.eye(n_samples) * DISTANCE_MASK_SENTINEL
|
|
372
|
+
|
|
373
|
+
# Step 2: Alpha-decay affinity kernel
|
|
374
|
+
affinity = self._build_alpha_decay_affinity(
|
|
375
|
+
distances_masked, self.config.n_neighbors, self.config.decay
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Step 3: Symmetrize (DRY: reuse graph_utils)
|
|
379
|
+
affinity_sym = symmetrize_graph(affinity)
|
|
380
|
+
|
|
381
|
+
# Step 4: Row-normalize to Markov matrix
|
|
382
|
+
markov = self._build_markov_matrix(affinity_sym)
|
|
383
|
+
|
|
384
|
+
# Step 5: Diffusion via eigendecomposition
|
|
385
|
+
diffusion_op = self._diffuse_eigendecomposition(markov, self.config.diffusion_t)
|
|
386
|
+
|
|
387
|
+
# Step 6: Potential distances
|
|
388
|
+
potential_distances = self._compute_potential_distances(diffusion_op, self.config.gamma)
|
|
389
|
+
|
|
390
|
+
# Step 7: Classical MDS embedding
|
|
391
|
+
embedding = self._classical_mds(potential_distances, self.config.n_components)
|
|
392
|
+
|
|
393
|
+
output_data = {
|
|
394
|
+
**data,
|
|
395
|
+
"embedding": embedding,
|
|
396
|
+
"potential_distances": potential_distances,
|
|
397
|
+
"diffusion_operator": diffusion_op,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return output_data, state, metadata
|