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,677 @@
|
|
|
1
|
+
"""Spatial domain identification and slice alignment operators.
|
|
2
|
+
|
|
3
|
+
This module provides two complementary operators for spatial transcriptomics:
|
|
4
|
+
|
|
5
|
+
1. **DifferentiableSpatialDomain** -- STAGATE-inspired graph attention autoencoder
|
|
6
|
+
that identifies spatial domains by combining gene expression with spatial
|
|
7
|
+
coordinates. Uses GATv2 attention with dual-graph pruning (alpha-weighted
|
|
8
|
+
combination of full and pruned adjacency) for encoding, followed by soft
|
|
9
|
+
domain assignment via learned prototypes.
|
|
10
|
+
|
|
11
|
+
2. **DifferentiablePASTEAlignment** -- PASTE-inspired fused Gromov-Wasserstein
|
|
12
|
+
optimal transport for aligning two spatial transcriptomics slices. Balances
|
|
13
|
+
expression dissimilarity with spatial structure preservation via entropy-
|
|
14
|
+
regularised Sinkhorn transport.
|
|
15
|
+
|
|
16
|
+
Key techniques:
|
|
17
|
+
- GATv2 graph attention on spatial k-NN graphs (STAGATE)
|
|
18
|
+
- Autoencoder reconstruction loss for representation learning (STAGATE)
|
|
19
|
+
- Fused expression + spatial Gromov-Wasserstein cost (PASTE)
|
|
20
|
+
- Sinkhorn optimal transport for differentiable alignment (PASTE)
|
|
21
|
+
|
|
22
|
+
References:
|
|
23
|
+
- Dong & Zhang, "STAGATE: Deciphering spatial domains from spatially resolved
|
|
24
|
+
transcriptomics with graph attention auto-encoder", Nature Communications 2022.
|
|
25
|
+
- Zeira et al., "Alignment and integration of spatial transcriptomics data",
|
|
26
|
+
Nature Methods 2022.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
import jax
|
|
34
|
+
import jax.numpy as jnp
|
|
35
|
+
from datarax.core.config import OperatorConfig
|
|
36
|
+
from flax import nnx
|
|
37
|
+
from jaxtyping import Array, Float, Int, PyTree
|
|
38
|
+
|
|
39
|
+
from diffbio.constants import DISTANCE_MASK_SENTINEL, EPSILON
|
|
40
|
+
from diffbio.core.base_operators import GraphOperator
|
|
41
|
+
from diffbio.core.gnn_components import GATv2Layer
|
|
42
|
+
from diffbio.core.graph_utils import compute_knn_graph, compute_pairwise_distances
|
|
43
|
+
from diffbio.core.optimal_transport import SinkhornLayer
|
|
44
|
+
from diffbio.utils.nn_utils import ensure_rngs
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# =============================================================================
|
|
50
|
+
# STAGATE-inspired spatial domain identification
|
|
51
|
+
# =============================================================================
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class SpatialDomainConfig(OperatorConfig):
|
|
56
|
+
"""Configuration for STAGATE-style spatial domain identification.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
n_genes: Number of input genes.
|
|
60
|
+
hidden_dim: Latent embedding dimension. Must be divisible by num_heads.
|
|
61
|
+
num_heads: Number of GATv2 attention heads.
|
|
62
|
+
n_domains: Number of spatial domains to identify.
|
|
63
|
+
alpha: Weight for pruned graph in dual-graph attention (STAGATE default 0.8).
|
|
64
|
+
At alpha=0, only the full k-NN graph is used. At alpha=1, only the
|
|
65
|
+
pruned (mutual k-NN) graph is used.
|
|
66
|
+
n_neighbors: Number of nearest neighbors for spatial k-NN graph.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
n_genes: int = 2000
|
|
70
|
+
hidden_dim: int = 64
|
|
71
|
+
num_heads: int = 4
|
|
72
|
+
n_domains: int = 7
|
|
73
|
+
alpha: float = 0.8
|
|
74
|
+
n_neighbors: int = 15
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _SpatialGATEncoder(nnx.Module):
|
|
78
|
+
"""GATv2-based encoder for spatial transcriptomics (STAGATE-style).
|
|
79
|
+
|
|
80
|
+
Encodes gene expression using dual-graph attention: a combination of
|
|
81
|
+
attention on the full k-NN graph and a pruned (mutual k-NN) graph,
|
|
82
|
+
weighted by alpha. This follows the STAGATE architecture where the
|
|
83
|
+
pruned graph encourages attention to spatially similar neighbors.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
n_genes: Input gene expression dimension.
|
|
87
|
+
hidden_dim: Output embedding dimension.
|
|
88
|
+
num_heads: Number of GATv2 attention heads.
|
|
89
|
+
rngs: Flax NNX random number generators.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
n_genes: int,
|
|
95
|
+
hidden_dim: int,
|
|
96
|
+
num_heads: int,
|
|
97
|
+
*,
|
|
98
|
+
rngs: nnx.Rngs,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Initialize the spatial GAT encoder.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
n_genes: Input feature dimension.
|
|
104
|
+
hidden_dim: Hidden / output dimension.
|
|
105
|
+
num_heads: Number of attention heads.
|
|
106
|
+
rngs: Random number generators.
|
|
107
|
+
"""
|
|
108
|
+
super().__init__()
|
|
109
|
+
|
|
110
|
+
# Input projection: n_genes -> hidden_dim
|
|
111
|
+
self.input_proj = nnx.Linear(
|
|
112
|
+
in_features=n_genes,
|
|
113
|
+
out_features=hidden_dim,
|
|
114
|
+
rngs=rngs,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# GATv2 layer for full k-NN graph
|
|
118
|
+
self.gat_full = GATv2Layer(
|
|
119
|
+
in_features=hidden_dim,
|
|
120
|
+
out_features=hidden_dim,
|
|
121
|
+
num_heads=num_heads,
|
|
122
|
+
edge_features=1,
|
|
123
|
+
dropout_rate=0.0,
|
|
124
|
+
rngs=rngs,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# GATv2 layer for pruned (mutual k-NN) graph
|
|
128
|
+
self.gat_pruned = GATv2Layer(
|
|
129
|
+
in_features=hidden_dim,
|
|
130
|
+
out_features=hidden_dim,
|
|
131
|
+
num_heads=num_heads,
|
|
132
|
+
edge_features=1,
|
|
133
|
+
dropout_rate=0.0,
|
|
134
|
+
rngs=rngs,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
self.layer_norm = nnx.LayerNorm(num_features=hidden_dim, rngs=rngs)
|
|
138
|
+
|
|
139
|
+
def __call__(
|
|
140
|
+
self,
|
|
141
|
+
node_features: Float[Array, "n_cells n_genes"],
|
|
142
|
+
full_edge_index: Int[Array, "2 n_full_edges"],
|
|
143
|
+
full_edge_weights: Float[Array, "n_full_edges 1"],
|
|
144
|
+
pruned_edge_index: Int[Array, "2 n_pruned_edges"],
|
|
145
|
+
pruned_edge_weights: Float[Array, "n_pruned_edges 1"],
|
|
146
|
+
alpha: float,
|
|
147
|
+
) -> Float[Array, "n_cells hidden_dim"]:
|
|
148
|
+
"""Encode gene expression using dual-graph GATv2 attention.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
node_features: Gene expression matrix.
|
|
152
|
+
full_edge_index: Edge indices for full k-NN graph (2, n_full_edges).
|
|
153
|
+
full_edge_weights: Edge weights for full graph.
|
|
154
|
+
pruned_edge_index: Edge indices for pruned mutual k-NN graph.
|
|
155
|
+
pruned_edge_weights: Edge weights for pruned graph.
|
|
156
|
+
alpha: Weight for pruned graph (0 = full only, 1 = pruned only).
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Spatial embeddings of shape (n_cells, hidden_dim).
|
|
160
|
+
"""
|
|
161
|
+
# Project input to hidden dim
|
|
162
|
+
h = self.input_proj(node_features)
|
|
163
|
+
|
|
164
|
+
# Dual-graph attention (STAGATE: (1-alpha)*full + alpha*pruned)
|
|
165
|
+
h_full = self.gat_full(h, full_edge_index, full_edge_weights, deterministic=True)
|
|
166
|
+
h_pruned = self.gat_pruned(h, pruned_edge_index, pruned_edge_weights, deterministic=True)
|
|
167
|
+
|
|
168
|
+
h = (1.0 - alpha) * h_full + alpha * h_pruned
|
|
169
|
+
|
|
170
|
+
# Apply ELU activation (following STAGATE) + LayerNorm
|
|
171
|
+
h = nnx.elu(h)
|
|
172
|
+
h = self.layer_norm(h)
|
|
173
|
+
|
|
174
|
+
return h
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class _ExpressionDecoder(nnx.Module):
|
|
178
|
+
"""Decoder that reconstructs gene expression from spatial embeddings.
|
|
179
|
+
|
|
180
|
+
Simple linear decoder mirroring the STAGATE architecture where the
|
|
181
|
+
decoder uses transposed weights for reconstruction.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
hidden_dim: Input embedding dimension.
|
|
185
|
+
n_genes: Output gene expression dimension.
|
|
186
|
+
rngs: Flax NNX random number generators.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def __init__(
|
|
190
|
+
self,
|
|
191
|
+
hidden_dim: int,
|
|
192
|
+
n_genes: int,
|
|
193
|
+
*,
|
|
194
|
+
rngs: nnx.Rngs,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Initialize the expression decoder.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
hidden_dim: Embedding dimension.
|
|
200
|
+
n_genes: Number of output genes.
|
|
201
|
+
rngs: Random number generators.
|
|
202
|
+
"""
|
|
203
|
+
super().__init__()
|
|
204
|
+
self.linear = nnx.Linear(
|
|
205
|
+
in_features=hidden_dim,
|
|
206
|
+
out_features=n_genes,
|
|
207
|
+
rngs=rngs,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def __call__(
|
|
211
|
+
self,
|
|
212
|
+
embeddings: Float[Array, "n_cells hidden_dim"],
|
|
213
|
+
) -> Float[Array, "n_cells n_genes"]:
|
|
214
|
+
"""Reconstruct gene expression from embeddings.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
embeddings: Spatial embeddings.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Reconstructed gene expression.
|
|
221
|
+
"""
|
|
222
|
+
return self.linear(embeddings)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class DifferentiableSpatialDomain(GraphOperator):
|
|
226
|
+
"""STAGATE-inspired differentiable spatial domain identification.
|
|
227
|
+
|
|
228
|
+
Identifies spatial domains by combining gene expression with spatial
|
|
229
|
+
coordinates through a graph attention autoencoder. The encoder uses
|
|
230
|
+
dual-graph GATv2 attention (full + pruned k-NN graphs), and soft domain
|
|
231
|
+
assignments are computed via learned prototypes with softmax.
|
|
232
|
+
|
|
233
|
+
Algorithm:
|
|
234
|
+
1. Build spatial k-NN graph from coordinates (full + pruned/mutual).
|
|
235
|
+
2. Apply GATv2 encoder: counts -> spatial embeddings (dual-graph
|
|
236
|
+
attention weighted by alpha).
|
|
237
|
+
3. Decoder: reconstruct gene expression from embeddings (autoencoder).
|
|
238
|
+
4. Soft domain assignment via softmax on learned domain prototypes.
|
|
239
|
+
|
|
240
|
+
Inherits from GraphOperator to get:
|
|
241
|
+
|
|
242
|
+
- scatter_aggregate() for message aggregation
|
|
243
|
+
- global_pool() for graph-level pooling
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
config: SpatialDomainConfig with model parameters.
|
|
247
|
+
rngs: Flax NNX random number generators.
|
|
248
|
+
name: Optional operator name.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def __init__(
|
|
252
|
+
self,
|
|
253
|
+
config: SpatialDomainConfig,
|
|
254
|
+
*,
|
|
255
|
+
rngs: nnx.Rngs | None = None,
|
|
256
|
+
name: str | None = None,
|
|
257
|
+
) -> None:
|
|
258
|
+
"""Initialize the spatial domain identification operator.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
config: Spatial domain configuration.
|
|
262
|
+
rngs: Random number generators for parameter initialization.
|
|
263
|
+
name: Optional operator name.
|
|
264
|
+
"""
|
|
265
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
266
|
+
|
|
267
|
+
rngs = ensure_rngs(rngs)
|
|
268
|
+
|
|
269
|
+
self.alpha = config.alpha
|
|
270
|
+
self.n_neighbors = config.n_neighbors
|
|
271
|
+
self.n_domains = config.n_domains
|
|
272
|
+
self.hidden_dim = config.hidden_dim
|
|
273
|
+
|
|
274
|
+
# Encoder: GATv2-based spatial graph attention
|
|
275
|
+
self.encoder = _SpatialGATEncoder(
|
|
276
|
+
n_genes=config.n_genes,
|
|
277
|
+
hidden_dim=config.hidden_dim,
|
|
278
|
+
num_heads=config.num_heads,
|
|
279
|
+
rngs=rngs,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Decoder: reconstruct gene expression
|
|
283
|
+
self.decoder = _ExpressionDecoder(
|
|
284
|
+
hidden_dim=config.hidden_dim,
|
|
285
|
+
n_genes=config.n_genes,
|
|
286
|
+
rngs=rngs,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Domain prototypes for soft assignment
|
|
290
|
+
key = rngs.params()
|
|
291
|
+
init_prototypes = jax.random.normal(key, (config.n_domains, config.hidden_dim)) * 0.1
|
|
292
|
+
self.domain_prototypes = nnx.Param(init_prototypes)
|
|
293
|
+
|
|
294
|
+
def _build_spatial_graphs(
|
|
295
|
+
self,
|
|
296
|
+
spatial_coords: Float[Array, "n_cells 2"],
|
|
297
|
+
) -> tuple[
|
|
298
|
+
Int[Array, "2 n_full_edges"],
|
|
299
|
+
Float[Array, "n_full_edges 1"],
|
|
300
|
+
Int[Array, "2 n_pruned_edges"],
|
|
301
|
+
Float[Array, "n_pruned_edges 1"],
|
|
302
|
+
]:
|
|
303
|
+
"""Build full and pruned (mutual) k-NN graphs from spatial coordinates.
|
|
304
|
+
|
|
305
|
+
The full graph connects each cell to its k nearest spatial neighbors.
|
|
306
|
+
The pruned graph keeps only mutual neighbors (edges present in both
|
|
307
|
+
directions), following the STAGATE strategy.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
spatial_coords: Spatial coordinates of shape (n_cells, 2).
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Tuple of (full_edge_index, full_edge_weights,
|
|
314
|
+
pruned_edge_index, pruned_edge_weights).
|
|
315
|
+
"""
|
|
316
|
+
n_cells = spatial_coords.shape[0]
|
|
317
|
+
|
|
318
|
+
# Compute pairwise spatial distances
|
|
319
|
+
distances = compute_pairwise_distances(spatial_coords, metric="euclidean")
|
|
320
|
+
# Mask self-distances
|
|
321
|
+
distances = distances + jnp.eye(n_cells) * DISTANCE_MASK_SENTINEL
|
|
322
|
+
|
|
323
|
+
# Full k-NN graph
|
|
324
|
+
edge_indices, edge_weights = compute_knn_graph(distances, k=self.n_neighbors)
|
|
325
|
+
# edge_indices: (n_edges, 2), edge_weights: (n_edges,)
|
|
326
|
+
full_edge_index = edge_indices.T # (2, n_edges)
|
|
327
|
+
full_edge_weights = edge_weights[:, None] # (n_edges, 1)
|
|
328
|
+
|
|
329
|
+
# Pruned (mutual) k-NN graph: keep only mutual edges
|
|
330
|
+
# Build adjacency indicator for fast mutual check
|
|
331
|
+
adj_indicator = jnp.zeros((n_cells, n_cells))
|
|
332
|
+
adj_indicator = adj_indicator.at[edge_indices[:, 0], edge_indices[:, 1]].set(1.0)
|
|
333
|
+
|
|
334
|
+
# Edge is mutual if both (i,j) and (j,i) are in the k-NN graph
|
|
335
|
+
mutual_mask = (
|
|
336
|
+
adj_indicator[edge_indices[:, 0], edge_indices[:, 1]]
|
|
337
|
+
* adj_indicator[edge_indices[:, 1], edge_indices[:, 0]]
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Use the same edges but weight by mutual membership
|
|
341
|
+
# (soft pruning for differentiability)
|
|
342
|
+
pruned_edge_index = full_edge_index
|
|
343
|
+
pruned_edge_weights = full_edge_weights * mutual_mask[:, None]
|
|
344
|
+
|
|
345
|
+
return full_edge_index, full_edge_weights, pruned_edge_index, pruned_edge_weights
|
|
346
|
+
|
|
347
|
+
def _compute_domain_assignments(
|
|
348
|
+
self,
|
|
349
|
+
embeddings: Float[Array, "n_cells hidden_dim"],
|
|
350
|
+
) -> Float[Array, "n_cells n_domains"]:
|
|
351
|
+
"""Compute soft domain assignments via learned prototypes.
|
|
352
|
+
|
|
353
|
+
Distance from each cell embedding to each domain prototype is computed,
|
|
354
|
+
then converted to assignment probabilities via softmax over negative
|
|
355
|
+
squared distances.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
embeddings: Spatial embeddings.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Soft domain assignment probabilities.
|
|
362
|
+
"""
|
|
363
|
+
prototypes = self.domain_prototypes[...] # (n_domains, hidden_dim)
|
|
364
|
+
|
|
365
|
+
# Squared distances: ||embedding - prototype||^2
|
|
366
|
+
# Using expansion: ||e||^2 + ||p||^2 - 2*e.p
|
|
367
|
+
emb_sq = jnp.sum(embeddings**2, axis=-1, keepdims=True) # (n, 1)
|
|
368
|
+
proto_sq = jnp.sum(prototypes**2, axis=-1) # (d,)
|
|
369
|
+
dot = jnp.einsum("nf,df->nd", embeddings, prototypes) # (n, d)
|
|
370
|
+
distances_sq = emb_sq + proto_sq - 2.0 * dot
|
|
371
|
+
|
|
372
|
+
# Soft assignment via softmax over negative distances
|
|
373
|
+
return jax.nn.softmax(-distances_sq, axis=-1)
|
|
374
|
+
|
|
375
|
+
def apply(
|
|
376
|
+
self,
|
|
377
|
+
data: PyTree,
|
|
378
|
+
state: PyTree,
|
|
379
|
+
metadata: dict[str, Any] | None,
|
|
380
|
+
random_params: Any = None,
|
|
381
|
+
stats: dict[str, Any] | None = None,
|
|
382
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
383
|
+
"""Apply spatial domain identification to spatial transcriptomics data.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
data: Dictionary containing:
|
|
387
|
+
- ``"counts"``: Gene expression matrix ``(n_cells, n_genes)``
|
|
388
|
+
- ``"spatial_coords"``: Spatial coordinates ``(n_cells, 2)``
|
|
389
|
+
state: Element state (passed through unchanged).
|
|
390
|
+
metadata: Element metadata (passed through unchanged).
|
|
391
|
+
random_params: Not used (non-stochastic operator).
|
|
392
|
+
stats: Not used.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Tuple of (transformed_data, state, metadata):
|
|
396
|
+
- transformed_data contains all original keys plus:
|
|
397
|
+
|
|
398
|
+
- ``"domain_assignments"``: Soft domain probabilities
|
|
399
|
+
``(n_cells, n_domains)``
|
|
400
|
+
- ``"spatial_embeddings"``: Latent embeddings
|
|
401
|
+
``(n_cells, hidden_dim)``
|
|
402
|
+
- state is passed through unchanged
|
|
403
|
+
- metadata is passed through unchanged
|
|
404
|
+
"""
|
|
405
|
+
counts: Float[Array, "n_cells n_genes"] = data["counts"]
|
|
406
|
+
spatial_coords: Float[Array, "n_cells 2"] = data["spatial_coords"]
|
|
407
|
+
|
|
408
|
+
# Step 1: Build spatial k-NN graphs (full + pruned)
|
|
409
|
+
(
|
|
410
|
+
full_edge_index,
|
|
411
|
+
full_edge_weights,
|
|
412
|
+
pruned_edge_index,
|
|
413
|
+
pruned_edge_weights,
|
|
414
|
+
) = self._build_spatial_graphs(spatial_coords)
|
|
415
|
+
|
|
416
|
+
# Step 2: Encode via dual-graph GATv2 attention
|
|
417
|
+
embeddings = self.encoder(
|
|
418
|
+
counts,
|
|
419
|
+
full_edge_index,
|
|
420
|
+
full_edge_weights,
|
|
421
|
+
pruned_edge_index,
|
|
422
|
+
pruned_edge_weights,
|
|
423
|
+
self.alpha,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Step 3: Decode (autoencoder reconstruction -- loss can be computed externally)
|
|
427
|
+
_reconstructed = self.decoder(embeddings) # noqa: F841
|
|
428
|
+
|
|
429
|
+
# Step 4: Soft domain assignment via prototypes
|
|
430
|
+
domain_assignments = self._compute_domain_assignments(embeddings)
|
|
431
|
+
|
|
432
|
+
transformed_data = {
|
|
433
|
+
**data,
|
|
434
|
+
"domain_assignments": domain_assignments,
|
|
435
|
+
"spatial_embeddings": embeddings,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return transformed_data, state, metadata
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# =============================================================================
|
|
442
|
+
# PASTE-inspired slice alignment
|
|
443
|
+
# =============================================================================
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@dataclass(frozen=True)
|
|
447
|
+
class PASTEAlignmentConfig(OperatorConfig):
|
|
448
|
+
"""Configuration for PASTE-style spatial transcriptomics slice alignment.
|
|
449
|
+
|
|
450
|
+
Attributes:
|
|
451
|
+
alpha: Balance between expression dissimilarity (linear term) and
|
|
452
|
+
spatial Gromov-Wasserstein cost (quadratic term). 0 = pure expression
|
|
453
|
+
matching, 1 = pure spatial structure matching. PASTE default: 0.1.
|
|
454
|
+
sinkhorn_epsilon: Entropy regularisation strength for the Sinkhorn
|
|
455
|
+
optimal transport solver.
|
|
456
|
+
sinkhorn_iters: Number of Sinkhorn iterations.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
alpha: float = 0.1
|
|
460
|
+
sinkhorn_epsilon: float = 0.1
|
|
461
|
+
sinkhorn_iters: int = 100
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class DifferentiablePASTEAlignment(GraphOperator):
|
|
465
|
+
"""PASTE-inspired differentiable spatial transcriptomics slice alignment.
|
|
466
|
+
|
|
467
|
+
Aligns two spatial transcriptomics slices by computing a fused cost that
|
|
468
|
+
balances expression dissimilarity with spatial structure (Gromov-Wasserstein)
|
|
469
|
+
and solving for the optimal transport plan via differentiable Sinkhorn.
|
|
470
|
+
|
|
471
|
+
Algorithm:
|
|
472
|
+
1. Compute expression dissimilarity between slices (Euclidean distance).
|
|
473
|
+
2. Compute intra-slice spatial distance matrices.
|
|
474
|
+
3. Compute Gromov-Wasserstein spatial cost that penalizes distortion
|
|
475
|
+
of pairwise spatial relationships.
|
|
476
|
+
4. Fuse costs: alpha * expression_cost + (1 - alpha) * spatial_GW_cost.
|
|
477
|
+
5. Solve OT via SinkhornLayer for the differentiable transport plan.
|
|
478
|
+
6. Align slice 2 coordinates using the transport plan.
|
|
479
|
+
|
|
480
|
+
Inherits from GraphOperator to get:
|
|
481
|
+
|
|
482
|
+
- scatter_aggregate() for message aggregation
|
|
483
|
+
- global_pool() for graph-level pooling
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
config: PASTEAlignmentConfig with alignment parameters.
|
|
487
|
+
rngs: Flax NNX random number generators.
|
|
488
|
+
name: Optional operator name.
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
def __init__(
|
|
492
|
+
self,
|
|
493
|
+
config: PASTEAlignmentConfig,
|
|
494
|
+
*,
|
|
495
|
+
rngs: nnx.Rngs | None = None,
|
|
496
|
+
name: str | None = None,
|
|
497
|
+
) -> None:
|
|
498
|
+
"""Initialize the PASTE alignment operator.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
config: PASTE alignment configuration.
|
|
502
|
+
rngs: Random number generators.
|
|
503
|
+
name: Optional operator name.
|
|
504
|
+
"""
|
|
505
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
506
|
+
|
|
507
|
+
rngs = ensure_rngs(rngs)
|
|
508
|
+
|
|
509
|
+
self.alpha_cost = config.alpha
|
|
510
|
+
self.sinkhorn = SinkhornLayer(
|
|
511
|
+
epsilon=config.sinkhorn_epsilon,
|
|
512
|
+
num_iters=config.sinkhorn_iters,
|
|
513
|
+
rngs=rngs,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
def _compute_expression_cost(
|
|
517
|
+
self,
|
|
518
|
+
counts1: Float[Array, "n1 g"],
|
|
519
|
+
counts2: Float[Array, "n2 g"],
|
|
520
|
+
) -> Float[Array, "n1 n2"]:
|
|
521
|
+
"""Compute pairwise expression dissimilarity between two slices.
|
|
522
|
+
|
|
523
|
+
Uses squared Euclidean distance, normalized by number of genes
|
|
524
|
+
for numerical stability.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
counts1: Expression matrix for slice 1.
|
|
528
|
+
counts2: Expression matrix for slice 2.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
Expression cost matrix of shape (n1, n2).
|
|
532
|
+
"""
|
|
533
|
+
n_genes = counts1.shape[1]
|
|
534
|
+
# ||c1_i - c2_j||^2 = ||c1_i||^2 + ||c2_j||^2 - 2 * c1_i . c2_j
|
|
535
|
+
sq1 = jnp.sum(counts1**2, axis=-1, keepdims=True) # (n1, 1)
|
|
536
|
+
sq2 = jnp.sum(counts2**2, axis=-1) # (n2,)
|
|
537
|
+
dot = jnp.dot(counts1, counts2.T) # (n1, n2)
|
|
538
|
+
cost = sq1 + sq2 - 2.0 * dot
|
|
539
|
+
# Normalize by number of genes for stability
|
|
540
|
+
return jnp.maximum(cost, 0.0) / (n_genes + EPSILON)
|
|
541
|
+
|
|
542
|
+
def _compute_spatial_distances(
|
|
543
|
+
self,
|
|
544
|
+
coords: Float[Array, "n 2"],
|
|
545
|
+
) -> Float[Array, "n n"]:
|
|
546
|
+
"""Compute intra-slice pairwise spatial distance matrix.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
coords: Spatial coordinates of shape (n, 2).
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
Distance matrix of shape (n, n).
|
|
553
|
+
"""
|
|
554
|
+
return compute_pairwise_distances(coords, metric="euclidean")
|
|
555
|
+
|
|
556
|
+
def _compute_gromov_wasserstein_cost(
|
|
557
|
+
self,
|
|
558
|
+
dist_a: Float[Array, "n1 n1"],
|
|
559
|
+
dist_b: Float[Array, "n2 n2"],
|
|
560
|
+
transport_plan: Float[Array, "n1 n2"],
|
|
561
|
+
) -> Float[Array, "n1 n2"]:
|
|
562
|
+
"""Compute the Gromov-Wasserstein gradient term for the fused cost.
|
|
563
|
+
|
|
564
|
+
The GW cost measures how well the transport plan preserves pairwise
|
|
565
|
+
spatial relationships::
|
|
566
|
+
|
|
567
|
+
L(D_A, D_B, T) = sum |D_A[i,k] - D_B[j,l]|^2 * T[i,j] * T[k,l]
|
|
568
|
+
|
|
569
|
+
This computes the gradient of the GW cost with respect to T, which
|
|
570
|
+
gives the linear cost matrix for the next Sinkhorn iteration.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
dist_a: Spatial distance matrix for slice 1.
|
|
574
|
+
dist_b: Spatial distance matrix for slice 2.
|
|
575
|
+
transport_plan: Current transport plan estimate.
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
GW cost gradient matrix of shape (n1, n2).
|
|
579
|
+
"""
|
|
580
|
+
# Square loss GW: sum_{ijkl} (D_A[i,k] - D_B[j,l])^2 * T[k,l]
|
|
581
|
+
# Gradient w.r.t. T[i,j] = 2 * (D_A^2 @ T @ 1 + 1 @ T @ D_B^2 - 2 * D_A @ T @ D_B)
|
|
582
|
+
# Simplified constant parts + linear in T:
|
|
583
|
+
da_sq = dist_a**2
|
|
584
|
+
db_sq = dist_b**2
|
|
585
|
+
|
|
586
|
+
# Term 1: D_A^2 @ T @ ones_n2 (broadcast) -> (n1, n2) contribution
|
|
587
|
+
term1 = da_sq @ transport_plan # (n1, n2)
|
|
588
|
+
|
|
589
|
+
# Term 2: ones_n1^T @ T @ D_B^2 (broadcast) -> (n1, n2) contribution
|
|
590
|
+
term2 = transport_plan @ db_sq # (n1, n2)
|
|
591
|
+
|
|
592
|
+
# Term 3: D_A @ T @ D_B^T (cross term)
|
|
593
|
+
cross = dist_a @ transport_plan @ dist_b # (n1, n2)
|
|
594
|
+
|
|
595
|
+
gw_cost = term1 + term2 - 2.0 * cross
|
|
596
|
+
return gw_cost
|
|
597
|
+
|
|
598
|
+
def apply(
|
|
599
|
+
self,
|
|
600
|
+
data: PyTree,
|
|
601
|
+
state: PyTree,
|
|
602
|
+
metadata: dict[str, Any] | None,
|
|
603
|
+
random_params: Any = None,
|
|
604
|
+
stats: dict[str, Any] | None = None,
|
|
605
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
606
|
+
"""Apply PASTE-style alignment between two spatial transcriptomics slices.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
data: Dictionary containing:
|
|
610
|
+
- ``"slice1_counts"``: Expression matrix for slice 1 ``(n1, g)``
|
|
611
|
+
- ``"slice2_counts"``: Expression matrix for slice 2 ``(n2, g)``
|
|
612
|
+
- ``"slice1_coords"``: Spatial coordinates for slice 1 ``(n1, 2)``
|
|
613
|
+
- ``"slice2_coords"``: Spatial coordinates for slice 2 ``(n2, 2)``
|
|
614
|
+
state: Element state (passed through unchanged).
|
|
615
|
+
metadata: Element metadata (passed through unchanged).
|
|
616
|
+
random_params: Not used (non-stochastic operator).
|
|
617
|
+
stats: Not used.
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Tuple of (transformed_data, state, metadata):
|
|
621
|
+
- transformed_data contains all original keys plus:
|
|
622
|
+
|
|
623
|
+
- ``"transport_plan"``: OT plan ``(n1, n2)``
|
|
624
|
+
- ``"aligned_coords"``: Aligned slice 2 coordinates ``(n2, 2)``
|
|
625
|
+
- state is passed through unchanged
|
|
626
|
+
- metadata is passed through unchanged
|
|
627
|
+
"""
|
|
628
|
+
counts1: Float[Array, "n1 g"] = data["slice1_counts"]
|
|
629
|
+
counts2: Float[Array, "n2 g"] = data["slice2_counts"]
|
|
630
|
+
coords1: Float[Array, "n1 2"] = data["slice1_coords"]
|
|
631
|
+
coords2: Float[Array, "n2 2"] = data["slice2_coords"]
|
|
632
|
+
|
|
633
|
+
n1 = counts1.shape[0]
|
|
634
|
+
n2 = counts2.shape[0]
|
|
635
|
+
|
|
636
|
+
# Step 1: Expression dissimilarity cost
|
|
637
|
+
expression_cost = self._compute_expression_cost(counts1, counts2)
|
|
638
|
+
|
|
639
|
+
# Step 2: Intra-slice spatial distances
|
|
640
|
+
dist_a = self._compute_spatial_distances(coords1)
|
|
641
|
+
dist_b = self._compute_spatial_distances(coords2)
|
|
642
|
+
|
|
643
|
+
# Step 3: Initial transport plan (uniform) for GW cost estimation
|
|
644
|
+
init_plan = jnp.ones((n1, n2)) / (n1 * n2)
|
|
645
|
+
|
|
646
|
+
# Step 4: Compute Gromov-Wasserstein spatial cost
|
|
647
|
+
gw_cost = self._compute_gromov_wasserstein_cost(dist_a, dist_b, init_plan)
|
|
648
|
+
|
|
649
|
+
# Normalize costs to comparable scales
|
|
650
|
+
expr_max = jnp.max(expression_cost) + EPSILON
|
|
651
|
+
gw_max = jnp.max(gw_cost) + EPSILON
|
|
652
|
+
expression_cost_norm = expression_cost / expr_max
|
|
653
|
+
gw_cost_norm = gw_cost / gw_max
|
|
654
|
+
|
|
655
|
+
# Step 5: Fused cost = (1-alpha) * expression + alpha * GW_spatial
|
|
656
|
+
fused_cost = (1.0 - self.alpha_cost) * expression_cost_norm + self.alpha_cost * gw_cost_norm
|
|
657
|
+
|
|
658
|
+
# Step 6: Solve OT via Sinkhorn
|
|
659
|
+
a = jnp.ones(n1) / n1 # uniform source marginal
|
|
660
|
+
b = jnp.ones(n2) / n2 # uniform target marginal
|
|
661
|
+
transport_plan = self.sinkhorn(fused_cost, a, b)
|
|
662
|
+
|
|
663
|
+
# Step 7: Align slice 2 coordinates using the transport plan
|
|
664
|
+
# Normalized plan rows: T_norm[i, :] = T[i, :] / sum_j T[i, j]
|
|
665
|
+
plan_col_normalized = transport_plan / (
|
|
666
|
+
jnp.sum(transport_plan, axis=0, keepdims=True) + EPSILON
|
|
667
|
+
)
|
|
668
|
+
# aligned_coords[j] = sum_i plan_col_norm[i, j] * coords1[i]
|
|
669
|
+
aligned_coords = plan_col_normalized.T @ coords1
|
|
670
|
+
|
|
671
|
+
transformed_data = {
|
|
672
|
+
**data,
|
|
673
|
+
"transport_plan": transport_plan,
|
|
674
|
+
"aligned_coords": aligned_coords,
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return transformed_data, state, metadata
|