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,493 @@
|
|
|
1
|
+
"""Differentiable spatial gene detection operators.
|
|
2
|
+
|
|
3
|
+
This module provides Gaussian process-based approaches to spatial gene detection
|
|
4
|
+
inspired by SpatialDE for identifying spatially variable genes in spatial
|
|
5
|
+
transcriptomics data.
|
|
6
|
+
|
|
7
|
+
SpatialDE decomposes expression variability into spatial and non-spatial components
|
|
8
|
+
using GP regression with RBF kernels. The Fraction of Spatial Variance (FSV)
|
|
9
|
+
quantifies how much variance is explained by spatial structure.
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
Svensson et al. (2018) "SpatialDE: identification of spatially variable genes"
|
|
13
|
+
https://www.nature.com/articles/nmeth.4636
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import jax
|
|
21
|
+
import jax.numpy as jnp
|
|
22
|
+
from datarax.core.config import OperatorConfig
|
|
23
|
+
from flax import nnx
|
|
24
|
+
from jaxtyping import Array, Float
|
|
25
|
+
|
|
26
|
+
from diffbio.core import soft_ops
|
|
27
|
+
from diffbio.core.base_operators import TemperatureOperator
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class _SpatialKernelConfig:
|
|
34
|
+
"""Gaussian-process kernel configuration."""
|
|
35
|
+
|
|
36
|
+
lengthscale: float = 1.0
|
|
37
|
+
variance: float = 1.0
|
|
38
|
+
noise_variance: float = 0.1
|
|
39
|
+
n_inducing_points: int = 100
|
|
40
|
+
learnable_kernel: bool = True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class _SpatialDetectionConfig:
|
|
45
|
+
"""Spatial gene classification configuration."""
|
|
46
|
+
|
|
47
|
+
n_genes: int = 2000
|
|
48
|
+
hidden_dims: tuple[int, ...] | list[int] = (64, 32)
|
|
49
|
+
temperature: float = 1.0
|
|
50
|
+
pvalue_threshold: float = 0.05
|
|
51
|
+
compute_field_ops: bool = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class SpatialGeneDetectorConfig(
|
|
56
|
+
_SpatialKernelConfig,
|
|
57
|
+
_SpatialDetectionConfig,
|
|
58
|
+
OperatorConfig,
|
|
59
|
+
):
|
|
60
|
+
"""Configuration for spatial gene detection."""
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
"""Validate the spatial gene detector configuration."""
|
|
64
|
+
super().__post_init__()
|
|
65
|
+
|
|
66
|
+
hidden_dims = tuple(self.hidden_dims)
|
|
67
|
+
object.__setattr__(self, "hidden_dims", hidden_dims)
|
|
68
|
+
|
|
69
|
+
if self.n_genes <= 0:
|
|
70
|
+
raise ValueError("n_genes must be positive.")
|
|
71
|
+
if self.lengthscale <= 0.0:
|
|
72
|
+
raise ValueError("lengthscale must be positive.")
|
|
73
|
+
if self.variance <= 0.0:
|
|
74
|
+
raise ValueError("variance must be positive.")
|
|
75
|
+
if self.noise_variance <= 0.0:
|
|
76
|
+
raise ValueError("noise_variance must be positive.")
|
|
77
|
+
if self.n_inducing_points <= 0:
|
|
78
|
+
raise ValueError("n_inducing_points must be positive.")
|
|
79
|
+
if not hidden_dims or any(hidden_dim <= 0 for hidden_dim in hidden_dims):
|
|
80
|
+
raise ValueError("hidden_dims must contain only positive integers.")
|
|
81
|
+
if self.temperature <= 0.0:
|
|
82
|
+
raise ValueError("temperature must be positive.")
|
|
83
|
+
if not 0.0 <= self.pvalue_threshold <= 1.0:
|
|
84
|
+
raise ValueError("pvalue_threshold must be between 0.0 and 1.0.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class _FixedKernelState:
|
|
89
|
+
"""Static kernel parameters for non-learnable mode."""
|
|
90
|
+
|
|
91
|
+
log_lengthscale: float
|
|
92
|
+
log_variance: float
|
|
93
|
+
log_noise_variance: float
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _LearnableKernelState(nnx.Module):
|
|
97
|
+
"""Learnable kernel parameters stored in log-space."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, config: SpatialGeneDetectorConfig) -> None:
|
|
100
|
+
self.log_lengthscale = nnx.Param(jnp.log(jnp.array(config.lengthscale)))
|
|
101
|
+
self.log_variance = nnx.Param(jnp.log(jnp.array(config.variance)))
|
|
102
|
+
self.log_noise_variance = nnx.Param(jnp.log(jnp.array(config.noise_variance)))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class _SpatialSmoothingNetwork(nnx.Module):
|
|
106
|
+
"""Neural approximation to the GP posterior mean."""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
hidden_dims: tuple[int, ...],
|
|
112
|
+
n_genes: int,
|
|
113
|
+
rngs: nnx.Rngs,
|
|
114
|
+
) -> None:
|
|
115
|
+
smoothing_layers = []
|
|
116
|
+
prev_dim = 2
|
|
117
|
+
for hidden_dim in hidden_dims:
|
|
118
|
+
smoothing_layers.append(nnx.Linear(prev_dim, hidden_dim, rngs=rngs))
|
|
119
|
+
prev_dim = hidden_dim
|
|
120
|
+
self.layers = nnx.List(smoothing_layers)
|
|
121
|
+
self.output = nnx.Linear(prev_dim, n_genes, rngs=rngs)
|
|
122
|
+
|
|
123
|
+
def __call__(self, coords: Float[Array, "n_spots 2"]) -> Float[Array, "n_spots n_genes"]:
|
|
124
|
+
"""Predict a spatial deviation field from normalized coordinates."""
|
|
125
|
+
hidden = coords
|
|
126
|
+
for layer in self.layers:
|
|
127
|
+
hidden = nnx.relu(layer(hidden))
|
|
128
|
+
return self.output(hidden)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class DifferentiableSpatialGeneDetector(TemperatureOperator):
|
|
132
|
+
"""SpatialDE-style differentiable spatial gene detection.
|
|
133
|
+
|
|
134
|
+
This operator identifies spatially variable genes using a differentiable
|
|
135
|
+
Gaussian process approach. It computes a spatial variance score for each
|
|
136
|
+
gene and provides soft assignments for spatial vs non-spatial genes.
|
|
137
|
+
|
|
138
|
+
The model decomposes gene expression as:
|
|
139
|
+
y = f(x) + epsilon
|
|
140
|
+
where f(x) ~ GP(0, K) is the spatial component and epsilon ~ N(0, sigma^2)
|
|
141
|
+
is the non-spatial noise.
|
|
142
|
+
|
|
143
|
+
The Fraction of Spatial Variance (FSV) is:
|
|
144
|
+
FSV = sigma^2_s / (sigma^2_s + sigma^2_e)
|
|
145
|
+
|
|
146
|
+
Input data structure:
|
|
147
|
+
- spatial_coords: Float[Array, "n_spots 2"] - Spatial coordinates
|
|
148
|
+
- expression: Float[Array, "n_spots n_genes"] - Gene expression
|
|
149
|
+
- total_counts: Float[Array, "n_spots"] - Total counts per spot
|
|
150
|
+
|
|
151
|
+
Output data structure (adds):
|
|
152
|
+
- spatial_variance: Float[Array, "n_genes"] - Spatial variance per gene
|
|
153
|
+
- spatial_pvalues: Float[Array, "n_genes"] - P-values for spatial patterns
|
|
154
|
+
- is_spatial: Float[Array, "n_genes"] - Soft spatial gene indicator
|
|
155
|
+
- smoothed_expression: Float[Array, "n_spots n_genes"] - GP smoothed expression
|
|
156
|
+
- fsv: Float[Array, "n_genes"] - Fraction of Spatial Variance
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
```python
|
|
160
|
+
config = SpatialGeneDetectorConfig(n_genes=2000)
|
|
161
|
+
detector = DifferentiableSpatialGeneDetector(config, rngs=nnx.Rngs(42))
|
|
162
|
+
result, state, meta = detector.apply(data, {}, None)
|
|
163
|
+
spatial_genes = result["is_spatial"] > 0.5
|
|
164
|
+
```
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
config: SpatialGeneDetectorConfig,
|
|
170
|
+
*,
|
|
171
|
+
rngs: nnx.Rngs,
|
|
172
|
+
name: str | None = None,
|
|
173
|
+
):
|
|
174
|
+
"""Initialize the spatial gene detector.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
config: Detector configuration.
|
|
178
|
+
rngs: Random number generators.
|
|
179
|
+
name: Optional name for the operator.
|
|
180
|
+
"""
|
|
181
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
182
|
+
|
|
183
|
+
# Kernel parameters (learnable in log-space for positivity)
|
|
184
|
+
if config.learnable_kernel:
|
|
185
|
+
self.kernel_state = _LearnableKernelState(config)
|
|
186
|
+
else:
|
|
187
|
+
self.kernel_state = nnx.static(
|
|
188
|
+
_FixedKernelState(
|
|
189
|
+
log_lengthscale=float(jnp.log(jnp.array(config.lengthscale))),
|
|
190
|
+
log_variance=float(jnp.log(jnp.array(config.variance))),
|
|
191
|
+
log_noise_variance=float(jnp.log(jnp.array(config.noise_variance))),
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Smoothing network for expression (neural approximation to GP mean)
|
|
196
|
+
self.smoothing_network = _SpatialSmoothingNetwork(
|
|
197
|
+
hidden_dims=tuple(config.hidden_dims),
|
|
198
|
+
n_genes=config.n_genes,
|
|
199
|
+
rngs=rngs,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def lengthscale(self) -> Float[Array, ""] | float:
|
|
204
|
+
"""Get the characteristic length for RBF kernel."""
|
|
205
|
+
kernel_state = self.kernel_state
|
|
206
|
+
if isinstance(kernel_state, _LearnableKernelState):
|
|
207
|
+
return jnp.exp(kernel_state.log_lengthscale[...])
|
|
208
|
+
return jnp.exp(jnp.asarray(kernel_state.log_lengthscale))
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def variance(self) -> Float[Array, ""] | float:
|
|
212
|
+
"""Get the signal variance parameter."""
|
|
213
|
+
kernel_state = self.kernel_state
|
|
214
|
+
if isinstance(kernel_state, _LearnableKernelState):
|
|
215
|
+
return jnp.exp(kernel_state.log_variance[...])
|
|
216
|
+
return jnp.exp(jnp.asarray(kernel_state.log_variance))
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def noise_variance(self) -> Float[Array, ""] | float:
|
|
220
|
+
"""Get current noise variance (sigma^2_e)."""
|
|
221
|
+
kernel_state = self.kernel_state
|
|
222
|
+
if isinstance(kernel_state, _LearnableKernelState):
|
|
223
|
+
return jnp.exp(kernel_state.log_noise_variance[...])
|
|
224
|
+
return jnp.exp(jnp.asarray(kernel_state.log_noise_variance))
|
|
225
|
+
|
|
226
|
+
def compute_kernel(
|
|
227
|
+
self,
|
|
228
|
+
X1: Float[Array, "n1 2"],
|
|
229
|
+
X2: Float[Array, "n2 2"],
|
|
230
|
+
) -> Float[Array, "n1 n2"]:
|
|
231
|
+
"""Compute squared exponential (RBF) kernel matrix.
|
|
232
|
+
|
|
233
|
+
K(x1, x2) = variance * exp(-||x1 - x2||^2 / (2 * lengthscale^2))
|
|
234
|
+
|
|
235
|
+
This is the standard kernel used in SpatialDE for modeling
|
|
236
|
+
spatial covariance.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
X1: First set of spatial coordinates.
|
|
240
|
+
X2: Second set of spatial coordinates.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Kernel matrix.
|
|
244
|
+
"""
|
|
245
|
+
# Compute squared Euclidean distances
|
|
246
|
+
sq_dist = jnp.sum(
|
|
247
|
+
(X1[:, None, :] - X2[None, :, :]) ** 2,
|
|
248
|
+
axis=-1,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Squared exponential (RBF) kernel
|
|
252
|
+
lengthscale = self.lengthscale
|
|
253
|
+
variance = self.variance
|
|
254
|
+
|
|
255
|
+
K = variance * jnp.exp(-sq_dist / (2 * lengthscale**2))
|
|
256
|
+
return K
|
|
257
|
+
|
|
258
|
+
def compute_spatial_variance(
|
|
259
|
+
self,
|
|
260
|
+
coords: Float[Array, "n_spots 2"],
|
|
261
|
+
expression: Float[Array, "n_spots n_genes"],
|
|
262
|
+
) -> tuple[Float[Array, "n_genes"], Float[Array, "n_genes"]]:
|
|
263
|
+
"""Compute spatial variance and FSV for each gene.
|
|
264
|
+
|
|
265
|
+
Uses neural network approximation to GP posterior mean for efficiency.
|
|
266
|
+
Computes variance decomposition: total = spatial + residual.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
coords: Spatial coordinates.
|
|
270
|
+
expression: Normalized gene expression.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Tuple of (spatial_variance, fsv) per gene.
|
|
274
|
+
"""
|
|
275
|
+
# Compute smoothed expression (approximate GP mean)
|
|
276
|
+
smoothed = self._smooth_expression(coords, expression)
|
|
277
|
+
|
|
278
|
+
# Center expression
|
|
279
|
+
expression_centered = expression - jnp.mean(expression, axis=0, keepdims=True)
|
|
280
|
+
|
|
281
|
+
# Total variance per gene
|
|
282
|
+
total_var = jnp.var(expression_centered, axis=0)
|
|
283
|
+
|
|
284
|
+
# Residual after spatial smoothing
|
|
285
|
+
residual = expression - smoothed
|
|
286
|
+
|
|
287
|
+
# Residual variance (non-spatial component)
|
|
288
|
+
residual_var = jnp.var(residual, axis=0)
|
|
289
|
+
|
|
290
|
+
# Spatial variance = total - residual (variance explained by space)
|
|
291
|
+
spatial_var = jnp.maximum(total_var - residual_var, 0.0)
|
|
292
|
+
|
|
293
|
+
# Fraction of Spatial Variance (FSV)
|
|
294
|
+
fsv = spatial_var / (total_var + 1e-8)
|
|
295
|
+
|
|
296
|
+
return spatial_var, fsv
|
|
297
|
+
|
|
298
|
+
def _smooth_expression(
|
|
299
|
+
self,
|
|
300
|
+
coords: Float[Array, "n_spots 2"],
|
|
301
|
+
expression: Float[Array, "n_spots n_genes"],
|
|
302
|
+
) -> Float[Array, "n_spots n_genes"]:
|
|
303
|
+
"""Compute smoothed expression using neural network.
|
|
304
|
+
|
|
305
|
+
This provides a differentiable approximation to the GP posterior mean.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
coords: Spatial coordinates.
|
|
309
|
+
expression: Gene expression.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Smoothed expression.
|
|
313
|
+
"""
|
|
314
|
+
# Normalize coordinates for stable training
|
|
315
|
+
coords_norm = (coords - jnp.mean(coords, axis=0)) / (jnp.std(coords, axis=0) + 1e-6)
|
|
316
|
+
|
|
317
|
+
# Apply smoothing network
|
|
318
|
+
deviation = self.smoothing_network(coords_norm)
|
|
319
|
+
|
|
320
|
+
# Smoothed = mean + learned spatial deviation
|
|
321
|
+
smoothed = jnp.mean(expression, axis=0, keepdims=True) + deviation
|
|
322
|
+
|
|
323
|
+
return smoothed
|
|
324
|
+
|
|
325
|
+
def compute_pvalues(
|
|
326
|
+
self,
|
|
327
|
+
fsv: Float[Array, "n_genes"],
|
|
328
|
+
n_spots: int,
|
|
329
|
+
) -> Float[Array, "n_genes"]:
|
|
330
|
+
"""Compute differentiable pseudo-p-values for spatial patterns.
|
|
331
|
+
|
|
332
|
+
Uses a soft approximation to the likelihood ratio test.
|
|
333
|
+
In SpatialDE, p-values come from comparing the spatial model
|
|
334
|
+
to a null model without spatial structure.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
fsv: Fraction of Spatial Variance per gene.
|
|
338
|
+
n_spots: Number of spatial locations.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Soft p-values (lower = more spatially variable).
|
|
342
|
+
"""
|
|
343
|
+
# Approximate likelihood ratio statistic
|
|
344
|
+
# Higher FSV -> larger LR statistic -> smaller p-value
|
|
345
|
+
# Scale by n_spots to approximate degrees of freedom effect
|
|
346
|
+
lr_stat = fsv * n_spots
|
|
347
|
+
|
|
348
|
+
# Transform to pseudo-pvalue using sigmoid
|
|
349
|
+
# This gives a differentiable approximation to the chi-squared CDF
|
|
350
|
+
pvalues = nnx.sigmoid(-lr_stat + 2.0)
|
|
351
|
+
|
|
352
|
+
return pvalues
|
|
353
|
+
|
|
354
|
+
def apply(
|
|
355
|
+
self,
|
|
356
|
+
data: dict[str, Array],
|
|
357
|
+
state: dict[str, Any],
|
|
358
|
+
metadata: dict[str, Any] | None,
|
|
359
|
+
random_params: Any = None, # noqa: ARG002
|
|
360
|
+
stats: dict[str, Any] | None = None, # noqa: ARG002
|
|
361
|
+
) -> tuple[dict[str, Array], dict[str, Any], dict[str, Any] | None]:
|
|
362
|
+
"""Apply spatial gene detection.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
data: Input data containing:
|
|
366
|
+
- spatial_coords: Float[Array, "n_spots 2"]
|
|
367
|
+
- expression: Float[Array, "n_spots n_genes"]
|
|
368
|
+
- total_counts: Float[Array, "n_spots"] (optional)
|
|
369
|
+
state: Element state (passed through).
|
|
370
|
+
metadata: Element metadata (passed through).
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Tuple of (output_data, state, metadata).
|
|
374
|
+
"""
|
|
375
|
+
coords = data["spatial_coords"]
|
|
376
|
+
expression = data["expression"]
|
|
377
|
+
n_spots = coords.shape[0]
|
|
378
|
+
|
|
379
|
+
# Normalize expression if total counts provided
|
|
380
|
+
if "total_counts" in data:
|
|
381
|
+
total_counts = data["total_counts"]
|
|
382
|
+
expression_norm = expression / (total_counts[:, None] + 1e-6)
|
|
383
|
+
expression_norm = expression_norm * soft_ops.median(total_counts, softness=0.1)
|
|
384
|
+
else:
|
|
385
|
+
expression_norm = expression
|
|
386
|
+
|
|
387
|
+
# Compute smoothed expression
|
|
388
|
+
smoothed = self._smooth_expression(coords, expression_norm)
|
|
389
|
+
|
|
390
|
+
# Compute spatial variance and FSV
|
|
391
|
+
spatial_variance, fsv = self.compute_spatial_variance(coords, expression_norm)
|
|
392
|
+
|
|
393
|
+
# Compute p-values
|
|
394
|
+
pvalues = self.compute_pvalues(fsv, n_spots)
|
|
395
|
+
|
|
396
|
+
# Soft spatial classification using temperature-controlled sigmoid
|
|
397
|
+
threshold = self.config.pvalue_threshold
|
|
398
|
+
temp = self._temperature
|
|
399
|
+
is_spatial = soft_ops.less(pvalues, threshold, softness=temp)
|
|
400
|
+
|
|
401
|
+
# Build output
|
|
402
|
+
output_data = {
|
|
403
|
+
**data,
|
|
404
|
+
"spatial_variance": spatial_variance,
|
|
405
|
+
"fsv": fsv,
|
|
406
|
+
"spatial_pvalues": pvalues,
|
|
407
|
+
"is_spatial": is_spatial,
|
|
408
|
+
"smoothed_expression": smoothed,
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# Optionally compute spatial field operations (gradient, laplacian)
|
|
412
|
+
if self.config.compute_field_ops:
|
|
413
|
+
field_ops = _compute_spatial_field_ops(coords, smoothed)
|
|
414
|
+
output_data.update(field_ops)
|
|
415
|
+
|
|
416
|
+
return output_data, state, metadata
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _compute_spatial_field_ops(
|
|
420
|
+
coords: Float[Array, "n_spots 2"],
|
|
421
|
+
smoothed: Float[Array, "n_spots n_genes"],
|
|
422
|
+
) -> dict[str, Array]:
|
|
423
|
+
"""Compute spatial gradient and Laplacian of smoothed expression.
|
|
424
|
+
|
|
425
|
+
Uses opifex's autodiff-based field operations to compute per-gene
|
|
426
|
+
spatial gradients and Laplacians at each spot location. Vectorized
|
|
427
|
+
over genes via ``jax.vmap`` — no Python for-loops.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
coords: Spatial coordinates (n_spots, 2).
|
|
431
|
+
smoothed: Smoothed expression (n_spots, n_genes).
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Dict with:
|
|
435
|
+
- ``expression_gradient``: Per-gene spatial gradient magnitude (n_genes,).
|
|
436
|
+
- ``expression_laplacian``: Per-gene mean Laplacian (n_genes,).
|
|
437
|
+
"""
|
|
438
|
+
from opifex.core.physics import compute_gradient, compute_laplacian # noqa: PLC0415
|
|
439
|
+
|
|
440
|
+
def _gene_field(x: Array, gene_expr: Array) -> Array:
|
|
441
|
+
"""RBF-interpolated scalar field for a single gene."""
|
|
442
|
+
dists = jnp.sum((x - coords) ** 2, axis=-1)
|
|
443
|
+
weights = jnp.exp(-dists / 2.0)
|
|
444
|
+
weights = weights / (jnp.sum(weights, axis=-1, keepdims=True) + 1e-8)
|
|
445
|
+
return jnp.sum(weights * gene_expr, axis=-1)
|
|
446
|
+
|
|
447
|
+
def _per_gene_metrics(gene_expr: Array) -> tuple[Array, Array]:
|
|
448
|
+
"""Compute gradient magnitude and Laplacian for one gene."""
|
|
449
|
+
field_fn = lambda x: _gene_field(x, gene_expr) # noqa: E731
|
|
450
|
+
grads = compute_gradient(field_fn, coords)
|
|
451
|
+
grad_mag = jnp.mean(jnp.sqrt(jnp.sum(grads**2, axis=-1) + 1e-8))
|
|
452
|
+
lap = compute_laplacian(field_fn, coords)
|
|
453
|
+
lap_mean = jnp.mean(jnp.abs(lap))
|
|
454
|
+
return grad_mag, lap_mean
|
|
455
|
+
|
|
456
|
+
# Vectorize over genes (columns of smoothed)
|
|
457
|
+
gradient_mags, laplacian_means = jax.vmap(_per_gene_metrics)(
|
|
458
|
+
smoothed.T
|
|
459
|
+
) # smoothed.T is (n_genes, n_spots)
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
"expression_gradient": gradient_mags,
|
|
463
|
+
"expression_laplacian": laplacian_means,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def create_spatial_gene_detector(
|
|
468
|
+
n_genes: int = 2000,
|
|
469
|
+
n_inducing_points: int = 100,
|
|
470
|
+
lengthscale: float = 1.0,
|
|
471
|
+
variance: float = 1.0,
|
|
472
|
+
seed: int = 42,
|
|
473
|
+
) -> DifferentiableSpatialGeneDetector:
|
|
474
|
+
"""Factory function to create a spatial gene detector.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
n_genes: Number of genes to analyze.
|
|
478
|
+
n_inducing_points: Number of inducing points for sparse GP.
|
|
479
|
+
lengthscale: Initial kernel lengthscale.
|
|
480
|
+
variance: Initial signal variance.
|
|
481
|
+
seed: Random seed.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Configured DifferentiableSpatialGeneDetector instance.
|
|
485
|
+
"""
|
|
486
|
+
config = SpatialGeneDetectorConfig(
|
|
487
|
+
n_genes=n_genes,
|
|
488
|
+
n_inducing_points=n_inducing_points,
|
|
489
|
+
lengthscale=lengthscale,
|
|
490
|
+
variance=variance,
|
|
491
|
+
)
|
|
492
|
+
rngs = nnx.Rngs(seed)
|
|
493
|
+
return DifferentiableSpatialGeneDetector(config, rngs=rngs)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Differentiable normalization and embedding operators.
|
|
2
|
+
|
|
3
|
+
This module provides operators for:
|
|
4
|
+
|
|
5
|
+
- VAE-based count normalization (scVI-style)
|
|
6
|
+
- Sequence embedding with learned representations
|
|
7
|
+
- Differentiable dimensionality reduction (UMAP, PHATE)
|
|
8
|
+
|
|
9
|
+
All operators maintain gradient flow for end-to-end training.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from diffbio.operators.normalization.embedding import (
|
|
13
|
+
SequenceEmbedding,
|
|
14
|
+
SequenceEmbeddingConfig,
|
|
15
|
+
)
|
|
16
|
+
from diffbio.operators.normalization.phate import (
|
|
17
|
+
DifferentiablePHATE,
|
|
18
|
+
PHATEConfig,
|
|
19
|
+
)
|
|
20
|
+
from diffbio.operators.normalization.umap import (
|
|
21
|
+
DifferentiableUMAP,
|
|
22
|
+
UMAPConfig,
|
|
23
|
+
)
|
|
24
|
+
from diffbio.operators.normalization.vae_normalizer import (
|
|
25
|
+
VAENormalizer,
|
|
26
|
+
VAENormalizerConfig,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# VAE Normalization
|
|
31
|
+
"VAENormalizerConfig",
|
|
32
|
+
"VAENormalizer",
|
|
33
|
+
# Sequence Embedding
|
|
34
|
+
"SequenceEmbeddingConfig",
|
|
35
|
+
"SequenceEmbedding",
|
|
36
|
+
# UMAP Dimensionality Reduction
|
|
37
|
+
"UMAPConfig",
|
|
38
|
+
"DifferentiableUMAP",
|
|
39
|
+
# PHATE Dimensionality Reduction
|
|
40
|
+
"PHATEConfig",
|
|
41
|
+
"DifferentiablePHATE",
|
|
42
|
+
]
|