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,704 @@
|
|
|
1
|
+
"""Differentiable cell-cell communication analysis.
|
|
2
|
+
|
|
3
|
+
This module provides two complementary operators for analysing cell-cell
|
|
4
|
+
communication in single-cell data:
|
|
5
|
+
|
|
6
|
+
1. **DifferentiableLigandReceptor** -- ligand-receptor co-expression scoring
|
|
7
|
+
using fuzzy k-NN adjacency graphs and analytical z-score significance.
|
|
8
|
+
2. **DifferentiableCellCommunication** -- GNN-based communication analysis
|
|
9
|
+
using GATv2 graph attention on a spatial cell graph with per-edge
|
|
10
|
+
L-R expression features.
|
|
11
|
+
|
|
12
|
+
Key techniques:
|
|
13
|
+
- Soft adjacency weighting via fuzzy k-NN (L-R scoring)
|
|
14
|
+
- GATv2 message passing on spatial cell graphs (cell communication)
|
|
15
|
+
- Temperature-controlled smooth approximations throughout
|
|
16
|
+
|
|
17
|
+
Applications: CellChat/CellPhoneDB-style communication analysis, spatial
|
|
18
|
+
transcriptomics niche identification, pathway-level signaling inference.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import jax
|
|
26
|
+
import jax.numpy as jnp
|
|
27
|
+
from datarax.core.config import OperatorConfig
|
|
28
|
+
from flax import nnx
|
|
29
|
+
from jaxtyping import Array, Float, Int, PyTree
|
|
30
|
+
|
|
31
|
+
from diffbio.constants import DISTANCE_MASK_SENTINEL, EPSILON
|
|
32
|
+
from diffbio.core import soft_ops
|
|
33
|
+
from diffbio.core.base_operators import GraphOperator, TemperatureOperator
|
|
34
|
+
from diffbio.core.gnn_components import GATv2Layer
|
|
35
|
+
from diffbio.core.graph_utils import (
|
|
36
|
+
compute_fuzzy_membership,
|
|
37
|
+
compute_pairwise_distances,
|
|
38
|
+
symmetrize_graph,
|
|
39
|
+
)
|
|
40
|
+
from diffbio.utils.nn_utils import ensure_rngs
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class LRScoringConfig(OperatorConfig):
|
|
47
|
+
"""Configuration for ligand-receptor co-expression scoring.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
n_neighbors: Number of nearest neighbors for k-NN graph.
|
|
51
|
+
temperature: Temperature for soft p-value sigmoid.
|
|
52
|
+
learnable_temperature: Whether the temperature is a learnable parameter.
|
|
53
|
+
metric: Distance metric for k-NN, either ``"euclidean"`` or ``"cosine"``.
|
|
54
|
+
kh: Hill function half-maximal constant (CellChat default 0.5).
|
|
55
|
+
hill_n: Hill function cooperativity coefficient (CellChat default 1.0).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
n_neighbors: int = 15
|
|
59
|
+
temperature: float = 1.0
|
|
60
|
+
learnable_temperature: bool = False
|
|
61
|
+
metric: str = "euclidean"
|
|
62
|
+
kh: float = 0.5
|
|
63
|
+
hill_n: float = 1.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DifferentiableLigandReceptor(TemperatureOperator):
|
|
67
|
+
"""Differentiable ligand-receptor co-expression scoring operator.
|
|
68
|
+
|
|
69
|
+
Scores cell-cell communication by computing adjacency-weighted co-expression
|
|
70
|
+
of ligand-receptor gene pairs. For each pair, the score at each receiver
|
|
71
|
+
cell is the sum of sender ligand expression times receiver receptor
|
|
72
|
+
expression, weighted by a fuzzy k-NN adjacency graph.
|
|
73
|
+
|
|
74
|
+
Algorithm:
|
|
75
|
+
1. Build a symmetric fuzzy k-NN adjacency from the count matrix using
|
|
76
|
+
``compute_pairwise_distances``, ``compute_fuzzy_membership``, and
|
|
77
|
+
``symmetrize_graph``.
|
|
78
|
+
2. For each L-R pair (ligand_idx, receptor_idx):
|
|
79
|
+
- ``score_i = sum_j(adjacency[i,j] * L[j] * R[i])``
|
|
80
|
+
where L[j] is the sender's ligand expression and R[i] is the
|
|
81
|
+
receiver's receptor expression.
|
|
82
|
+
3. Compute analytical soft p-values via z-score comparison against
|
|
83
|
+
an expected null distribution.
|
|
84
|
+
|
|
85
|
+
Inherits from TemperatureOperator to get:
|
|
86
|
+
|
|
87
|
+
- _temperature property for temperature-controlled smoothing
|
|
88
|
+
- soft_max() for logsumexp-based smooth maximum
|
|
89
|
+
- soft_argmax() for soft position selection
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
config: LRScoringConfig with operator parameters.
|
|
93
|
+
rngs: Flax NNX random number generators.
|
|
94
|
+
name: Optional operator name.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
>>> config = LRScoringConfig(n_neighbors=15)
|
|
98
|
+
>>> op = DifferentiableLigandReceptor(config, rngs=nnx.Rngs(0))
|
|
99
|
+
>>> data = {"counts": counts, "lr_pairs": jnp.array([[0, 1]])}
|
|
100
|
+
>>> result, state, meta = op.apply(data, {}, None)
|
|
101
|
+
>>> result["lr_scores"].shape
|
|
102
|
+
(n_cells, 1)
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
config: LRScoringConfig,
|
|
108
|
+
*,
|
|
109
|
+
rngs: nnx.Rngs | None = None,
|
|
110
|
+
name: str | None = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Initialize the ligand-receptor scoring operator.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
config: L-R scoring configuration.
|
|
116
|
+
rngs: Random number generators for parameter initialization.
|
|
117
|
+
name: Optional operator name.
|
|
118
|
+
"""
|
|
119
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
120
|
+
|
|
121
|
+
def _build_adjacency(
|
|
122
|
+
self,
|
|
123
|
+
counts: Float[Array, "n_cells n_genes"],
|
|
124
|
+
) -> Float[Array, "n_cells n_cells"]:
|
|
125
|
+
"""Build symmetric fuzzy k-NN adjacency from expression counts.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
counts: Gene expression matrix.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Symmetric adjacency matrix of shape ``(n_cells, n_cells)``.
|
|
132
|
+
"""
|
|
133
|
+
# Mask self-distances with large sentinel before k-NN computation
|
|
134
|
+
n_cells = counts.shape[0]
|
|
135
|
+
distances = compute_pairwise_distances(counts, metric=self.config.metric)
|
|
136
|
+
distances = distances + jnp.eye(n_cells) * DISTANCE_MASK_SENTINEL
|
|
137
|
+
|
|
138
|
+
membership = compute_fuzzy_membership(distances, k=self.config.n_neighbors)
|
|
139
|
+
return symmetrize_graph(membership)
|
|
140
|
+
|
|
141
|
+
def _score_lr_pair(
|
|
142
|
+
self,
|
|
143
|
+
adjacency: Float[Array, "n_cells n_cells"],
|
|
144
|
+
ligand_expression: Float[Array, "n_cells"],
|
|
145
|
+
receptor_expression: Float[Array, "n_cells"],
|
|
146
|
+
) -> Float[Array, "n_cells"]:
|
|
147
|
+
"""Score L-R pair using Hill function (CellChat-style).
|
|
148
|
+
|
|
149
|
+
For each receiver cell *i* the score is a saturating Hill function
|
|
150
|
+
of the neighbor-averaged ligand signal times the receiver's own
|
|
151
|
+
receptor expression:
|
|
152
|
+
|
|
153
|
+
``P = (L*R)^n / (Kh^n + (L*R)^n)``
|
|
154
|
+
|
|
155
|
+
where ``Kh`` and ``n`` are configured via ``LRScoringConfig.kh``
|
|
156
|
+
and ``LRScoringConfig.hill_n``.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
adjacency: Symmetric adjacency matrix.
|
|
160
|
+
ligand_expression: Ligand gene expression per cell.
|
|
161
|
+
receptor_expression: Receptor gene expression per cell.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Per-cell interaction score of shape ``(n_cells,)``.
|
|
165
|
+
"""
|
|
166
|
+
# Neighbor-averaged ligand expression (sender perspective)
|
|
167
|
+
neighbor_ligand = adjacency @ ligand_expression # (n_cells,)
|
|
168
|
+
|
|
169
|
+
# L-R product per cell (receiver's receptor * sender's ligand)
|
|
170
|
+
lr_product = neighbor_ligand * receptor_expression
|
|
171
|
+
|
|
172
|
+
# Hill function for saturation (CellChat: Kh=0.5, n=1)
|
|
173
|
+
kh: float = self.config.kh
|
|
174
|
+
n: float = self.config.hill_n
|
|
175
|
+
score = lr_product**n / (kh**n + lr_product**n + EPSILON)
|
|
176
|
+
|
|
177
|
+
return score
|
|
178
|
+
|
|
179
|
+
def _compute_soft_pvalues(
|
|
180
|
+
self,
|
|
181
|
+
scores: Float[Array, "n_cells n_pairs"],
|
|
182
|
+
adjacency: Float[Array, "n_cells n_cells"],
|
|
183
|
+
counts: Float[Array, "n_cells n_genes"],
|
|
184
|
+
lr_pairs: Int[Array, "n_pairs 2"],
|
|
185
|
+
) -> Float[Array, "n_pairs"]:
|
|
186
|
+
"""Compute soft p-values for each L-R pair via analytical z-score.
|
|
187
|
+
|
|
188
|
+
Under a null where adjacency is independent of expression, the expected
|
|
189
|
+
score for each cell is ``E[score_i] = mean(L) * R[i] * sum_j(A[i,j])``.
|
|
190
|
+
The aggregate z-score is computed from the total observed vs. expected
|
|
191
|
+
score, and converted to a soft p-value via sigmoid.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
scores: Per-cell L-R scores of shape ``(n_cells, n_pairs)``.
|
|
195
|
+
adjacency: Adjacency matrix.
|
|
196
|
+
counts: Expression matrix for computing means.
|
|
197
|
+
lr_pairs: L-R pair indices.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Soft p-values per pair, shape ``(n_pairs,)``.
|
|
201
|
+
"""
|
|
202
|
+
row_sums = jnp.sum(adjacency, axis=1) # (n_cells,)
|
|
203
|
+
temperature = self._temperature
|
|
204
|
+
|
|
205
|
+
def _pvalue_for_pair(pair_idx: Int[Array, ""]) -> Float[Array, ""]:
|
|
206
|
+
ligand_idx = lr_pairs[pair_idx, 0]
|
|
207
|
+
receptor_idx = lr_pairs[pair_idx, 1]
|
|
208
|
+
|
|
209
|
+
ligand_expr = counts[:, ligand_idx]
|
|
210
|
+
receptor_expr = counts[:, receptor_idx]
|
|
211
|
+
|
|
212
|
+
# Observed total score
|
|
213
|
+
observed = jnp.sum(scores[:, pair_idx])
|
|
214
|
+
|
|
215
|
+
# Expected under null: E[score_i] = mean(L) * R[i] * degree_i
|
|
216
|
+
mean_ligand = jnp.mean(ligand_expr)
|
|
217
|
+
mean_receptor = jnp.mean(receptor_expr)
|
|
218
|
+
expected = jnp.sum(mean_ligand * receptor_expr * row_sums)
|
|
219
|
+
|
|
220
|
+
# Correct variance for Var(sum_j A[i,j]*L[j]*R[i]) under
|
|
221
|
+
# independence of L and R:
|
|
222
|
+
# Var(sum) = sum(A^2) * (Var(L)*Var(R)
|
|
223
|
+
# + Var(L)*E[R]^2 + Var(R)*E[L]^2)
|
|
224
|
+
var_ligand = jnp.var(ligand_expr) + EPSILON
|
|
225
|
+
var_receptor = jnp.var(receptor_expr) + EPSILON
|
|
226
|
+
sum_adj_sq = jnp.sum(adjacency**2)
|
|
227
|
+
std_approx = jnp.sqrt(
|
|
228
|
+
sum_adj_sq
|
|
229
|
+
* (
|
|
230
|
+
var_ligand * var_receptor
|
|
231
|
+
+ var_ligand * mean_receptor**2
|
|
232
|
+
+ var_receptor * mean_ligand**2
|
|
233
|
+
)
|
|
234
|
+
+ EPSILON
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
z_score = (observed - expected) / (std_approx + EPSILON)
|
|
238
|
+
|
|
239
|
+
# Soft p-value: high z-score -> low p-value
|
|
240
|
+
return soft_ops.less(z_score, 0.0, softness=temperature)
|
|
241
|
+
|
|
242
|
+
n_pairs = lr_pairs.shape[0]
|
|
243
|
+
pair_indices = jnp.arange(n_pairs)
|
|
244
|
+
return jax.vmap(_pvalue_for_pair)(pair_indices)
|
|
245
|
+
|
|
246
|
+
def apply(
|
|
247
|
+
self,
|
|
248
|
+
data: PyTree,
|
|
249
|
+
state: PyTree,
|
|
250
|
+
metadata: dict[str, Any] | None,
|
|
251
|
+
random_params: Any = None,
|
|
252
|
+
stats: dict[str, Any] | None = None,
|
|
253
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
254
|
+
"""Apply ligand-receptor co-expression scoring.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
data: Dictionary containing:
|
|
258
|
+
- ``"counts"``: Gene expression matrix ``(n_cells, n_genes)``
|
|
259
|
+
- ``"lr_pairs"``: L-R pair indices ``(n_pairs, 2)`` where each
|
|
260
|
+
row is ``[ligand_gene_idx, receptor_gene_idx]``
|
|
261
|
+
state: Element state (passed through unchanged).
|
|
262
|
+
metadata: Element metadata (passed through unchanged).
|
|
263
|
+
random_params: Not used (non-stochastic operator).
|
|
264
|
+
stats: Not used.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Tuple of (transformed_data, state, metadata):
|
|
268
|
+
- transformed_data contains:
|
|
269
|
+
|
|
270
|
+
- all original data keys
|
|
271
|
+
- ``"lr_scores"``: Per-cell interaction scores ``(n_cells, n_pairs)``
|
|
272
|
+
- ``"lr_pvalues"``: Soft p-values per pair ``(n_pairs,)``
|
|
273
|
+
- state is passed through unchanged
|
|
274
|
+
- metadata is passed through unchanged
|
|
275
|
+
"""
|
|
276
|
+
counts = data["counts"]
|
|
277
|
+
lr_pairs = data["lr_pairs"]
|
|
278
|
+
|
|
279
|
+
# Step 1: Build k-NN adjacency graph
|
|
280
|
+
adjacency = self._build_adjacency(counts)
|
|
281
|
+
|
|
282
|
+
# Step 2: Score each L-R pair across all cells
|
|
283
|
+
def _score_pair(pair: Int[Array, "2"]) -> Float[Array, "n_cells"]:
|
|
284
|
+
ligand_expr = counts[:, pair[0]]
|
|
285
|
+
receptor_expr = counts[:, pair[1]]
|
|
286
|
+
return self._score_lr_pair(adjacency, ligand_expr, receptor_expr)
|
|
287
|
+
|
|
288
|
+
lr_scores = jax.vmap(_score_pair)(lr_pairs).T # (n_cells, n_pairs)
|
|
289
|
+
|
|
290
|
+
# Step 3: Compute soft p-values
|
|
291
|
+
lr_pvalues = self._compute_soft_pvalues(lr_scores, adjacency, counts, lr_pairs)
|
|
292
|
+
|
|
293
|
+
transformed_data = {
|
|
294
|
+
**data,
|
|
295
|
+
"lr_scores": lr_scores,
|
|
296
|
+
"lr_pvalues": lr_pvalues,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return transformed_data, state, metadata
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# =============================================================================
|
|
303
|
+
# GNN-based cell-cell communication
|
|
304
|
+
# =============================================================================
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@dataclass(frozen=True)
|
|
308
|
+
class _CellCommunicationInputConfig:
|
|
309
|
+
"""Input-space sizing for cell communication analysis."""
|
|
310
|
+
|
|
311
|
+
n_genes: int = 2000
|
|
312
|
+
n_lr_pairs: int = 10
|
|
313
|
+
edge_features_dim: int = 8
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@dataclass(frozen=True)
|
|
317
|
+
class _CellCommunicationModelConfig:
|
|
318
|
+
"""Graph-attention architecture for communication inference."""
|
|
319
|
+
|
|
320
|
+
hidden_dim: int = 64
|
|
321
|
+
num_heads: int = 4
|
|
322
|
+
num_gnn_layers: int = 2
|
|
323
|
+
n_pathways: int = 20
|
|
324
|
+
dropout_rate: float = 0.1
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclass(frozen=True)
|
|
328
|
+
class CellCommunicationConfig(
|
|
329
|
+
_CellCommunicationInputConfig,
|
|
330
|
+
_CellCommunicationModelConfig,
|
|
331
|
+
OperatorConfig,
|
|
332
|
+
):
|
|
333
|
+
"""Configuration for GNN-based cell-cell communication analysis."""
|
|
334
|
+
|
|
335
|
+
def __post_init__(self) -> None:
|
|
336
|
+
"""Validate graph-attention communication configuration."""
|
|
337
|
+
if self.n_genes <= 0:
|
|
338
|
+
raise ValueError("n_genes must be positive")
|
|
339
|
+
if self.n_lr_pairs <= 0:
|
|
340
|
+
raise ValueError("n_lr_pairs must be positive")
|
|
341
|
+
if self.hidden_dim <= 0:
|
|
342
|
+
raise ValueError("hidden_dim must be positive")
|
|
343
|
+
if self.num_heads <= 0:
|
|
344
|
+
raise ValueError("num_heads must be positive")
|
|
345
|
+
if self.hidden_dim % self.num_heads != 0:
|
|
346
|
+
raise ValueError("hidden_dim must be divisible by num_heads")
|
|
347
|
+
if self.edge_features_dim <= 0:
|
|
348
|
+
raise ValueError("edge_features_dim must be positive")
|
|
349
|
+
if self.num_gnn_layers <= 0:
|
|
350
|
+
raise ValueError("num_gnn_layers must be positive")
|
|
351
|
+
if self.n_pathways <= 0:
|
|
352
|
+
raise ValueError("n_pathways must be positive")
|
|
353
|
+
if not 0.0 <= self.dropout_rate < 1.0:
|
|
354
|
+
raise ValueError("dropout_rate must be in [0, 1)")
|
|
355
|
+
super().__post_init__()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class SpatialAttentionGNN(nnx.Module):
|
|
359
|
+
"""Stacked GATv2 layers with residual connections for spatial cell graphs.
|
|
360
|
+
|
|
361
|
+
Each layer applies GATv2 attention followed by a LayerNorm and residual
|
|
362
|
+
connection. An input projection maps node features to the hidden dimension
|
|
363
|
+
before the first GATv2 layer.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
in_features: Dimension of input node features.
|
|
367
|
+
hidden_dim: Hidden dimension (must be divisible by num_heads).
|
|
368
|
+
num_heads: Number of attention heads per GATv2 layer.
|
|
369
|
+
edge_features_dim: Edge feature dimension.
|
|
370
|
+
num_layers: Number of GATv2 layers.
|
|
371
|
+
dropout_rate: Dropout rate.
|
|
372
|
+
rngs: Flax NNX random number generators.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
def __init__(
|
|
376
|
+
self,
|
|
377
|
+
in_features: int,
|
|
378
|
+
hidden_dim: int,
|
|
379
|
+
num_heads: int,
|
|
380
|
+
edge_features_dim: int,
|
|
381
|
+
num_layers: int,
|
|
382
|
+
dropout_rate: float,
|
|
383
|
+
*,
|
|
384
|
+
rngs: nnx.Rngs,
|
|
385
|
+
) -> None:
|
|
386
|
+
"""Initialize the spatial attention GNN.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
in_features: Input feature dimension.
|
|
390
|
+
hidden_dim: Hidden dimension.
|
|
391
|
+
num_heads: Number of attention heads.
|
|
392
|
+
edge_features_dim: Edge feature dimension.
|
|
393
|
+
num_layers: Number of GATv2 layers.
|
|
394
|
+
dropout_rate: Dropout rate.
|
|
395
|
+
rngs: Random number generators.
|
|
396
|
+
"""
|
|
397
|
+
super().__init__()
|
|
398
|
+
|
|
399
|
+
# Project input features to hidden dimension
|
|
400
|
+
self.input_proj = nnx.Linear(
|
|
401
|
+
in_features=in_features,
|
|
402
|
+
out_features=hidden_dim,
|
|
403
|
+
rngs=rngs,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
self.gat_layers = nnx.List(
|
|
407
|
+
[
|
|
408
|
+
GATv2Layer(
|
|
409
|
+
in_features=hidden_dim,
|
|
410
|
+
out_features=hidden_dim,
|
|
411
|
+
num_heads=num_heads,
|
|
412
|
+
edge_features=edge_features_dim,
|
|
413
|
+
dropout_rate=dropout_rate,
|
|
414
|
+
rngs=rngs,
|
|
415
|
+
)
|
|
416
|
+
for _ in range(num_layers)
|
|
417
|
+
]
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
self.layer_norms = nnx.List(
|
|
421
|
+
[nnx.LayerNorm(num_features=hidden_dim, rngs=rngs) for _ in range(num_layers)]
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def __call__(
|
|
425
|
+
self,
|
|
426
|
+
node_features: Float[Array, "n_nodes in_features"],
|
|
427
|
+
edge_index: Int[Array, "2 n_edges"],
|
|
428
|
+
edge_features: Float[Array, "n_edges edge_features_dim"],
|
|
429
|
+
*,
|
|
430
|
+
deterministic: bool = True,
|
|
431
|
+
) -> Float[Array, "n_nodes hidden_dim"]:
|
|
432
|
+
"""Run stacked GATv2 attention with residual connections.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
node_features: Input node features.
|
|
436
|
+
edge_index: Edge indices ``(source, target)`` of shape ``(2, n_edges)``.
|
|
437
|
+
edge_features: Per-edge features.
|
|
438
|
+
deterministic: If True, disable dropout.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Updated node embeddings of shape ``(n_nodes, hidden_dim)``.
|
|
442
|
+
"""
|
|
443
|
+
x = self.input_proj(node_features)
|
|
444
|
+
|
|
445
|
+
for gat_layer, norm in zip(self.gat_layers, self.layer_norms):
|
|
446
|
+
residual = x
|
|
447
|
+
x = gat_layer(x, edge_index, edge_features, deterministic=deterministic)
|
|
448
|
+
x = norm(x + residual)
|
|
449
|
+
|
|
450
|
+
return x
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class SignalingDecoder(nnx.Module):
|
|
454
|
+
"""Map node embeddings to pathway activities and communication scores.
|
|
455
|
+
|
|
456
|
+
Two heads:
|
|
457
|
+
- **Pathway head**: ``Linear(hidden_dim, n_pathways)`` produces per-node
|
|
458
|
+
pathway activity.
|
|
459
|
+
- **Communication head**: ``Linear(hidden_dim, n_lr_pairs)`` produces
|
|
460
|
+
per-node communication scores for each L-R pair.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
hidden_dim: Input embedding dimension.
|
|
464
|
+
n_pathways: Number of output pathways.
|
|
465
|
+
n_lr_pairs: Number of L-R pairs (communication score outputs).
|
|
466
|
+
rngs: Flax NNX random number generators.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
def __init__(
|
|
470
|
+
self,
|
|
471
|
+
hidden_dim: int,
|
|
472
|
+
n_pathways: int,
|
|
473
|
+
n_lr_pairs: int,
|
|
474
|
+
*,
|
|
475
|
+
rngs: nnx.Rngs,
|
|
476
|
+
) -> None:
|
|
477
|
+
"""Initialize the signaling decoder.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
hidden_dim: Hidden dimension.
|
|
481
|
+
n_pathways: Number of signaling pathways.
|
|
482
|
+
n_lr_pairs: Number of L-R pairs.
|
|
483
|
+
rngs: Random number generators.
|
|
484
|
+
"""
|
|
485
|
+
super().__init__()
|
|
486
|
+
|
|
487
|
+
self.pathway_head = nnx.Linear(
|
|
488
|
+
in_features=hidden_dim,
|
|
489
|
+
out_features=n_pathways,
|
|
490
|
+
rngs=rngs,
|
|
491
|
+
)
|
|
492
|
+
self.comm_head = nnx.Linear(
|
|
493
|
+
in_features=hidden_dim,
|
|
494
|
+
out_features=n_lr_pairs,
|
|
495
|
+
rngs=rngs,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
def __call__(
|
|
499
|
+
self,
|
|
500
|
+
node_embeddings: Float[Array, "n_nodes hidden_dim"],
|
|
501
|
+
) -> tuple[
|
|
502
|
+
Float[Array, "n_nodes n_pathways"],
|
|
503
|
+
Float[Array, "n_nodes n_lr_pairs"],
|
|
504
|
+
]:
|
|
505
|
+
"""Decode node embeddings into pathway activities and communication scores.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
node_embeddings: Node embedding matrix.
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Tuple of (signaling_activity, communication_scores).
|
|
512
|
+
"""
|
|
513
|
+
signaling_activity = self.pathway_head(node_embeddings)
|
|
514
|
+
communication_scores = self.comm_head(node_embeddings)
|
|
515
|
+
return signaling_activity, communication_scores
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class DifferentiableCellCommunication(GraphOperator):
|
|
519
|
+
"""GNN-based differentiable cell-cell communication analysis.
|
|
520
|
+
|
|
521
|
+
Analyses inter-cellular signaling by applying GATv2 graph attention on a
|
|
522
|
+
spatial cell graph whose edges carry ligand-receptor expression features.
|
|
523
|
+
|
|
524
|
+
Algorithm:
|
|
525
|
+
1. Build per-edge L-R expression features from ``counts`` and
|
|
526
|
+
``lr_pairs``: for each edge (i, j) the feature vector is the
|
|
527
|
+
concatenation of ``[L_expr[source], R_expr[target]]`` across all
|
|
528
|
+
L-R pairs, projected to ``edge_features_dim``.
|
|
529
|
+
2. Project per-node gene expression to initial node embeddings.
|
|
530
|
+
3. Apply stacked GATv2 layers (``SpatialAttentionGNN``) for message
|
|
531
|
+
passing on the spatial cell graph.
|
|
532
|
+
4. Decode node embeddings into per-node pathway activity and per-node
|
|
533
|
+
communication scores via ``SignalingDecoder``.
|
|
534
|
+
|
|
535
|
+
Inherits from GraphOperator to get:
|
|
536
|
+
|
|
537
|
+
- scatter_aggregate() for message aggregation
|
|
538
|
+
- global_pool() for graph-level pooling
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
config: CellCommunicationConfig with model parameters.
|
|
542
|
+
rngs: Flax NNX random number generators.
|
|
543
|
+
name: Optional operator name.
|
|
544
|
+
|
|
545
|
+
Example:
|
|
546
|
+
>>> config = CellCommunicationConfig(n_genes=50, n_lr_pairs=3, hidden_dim=32)
|
|
547
|
+
>>> op = DifferentiableCellCommunication(config, rngs=nnx.Rngs(0))
|
|
548
|
+
>>> data = {"counts": counts, "spatial_graph": graph, "lr_pairs": pairs}
|
|
549
|
+
>>> result, state, meta = op.apply(data, {}, None)
|
|
550
|
+
>>> result["communication_scores"].shape
|
|
551
|
+
(n_cells, 3)
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
def __init__(
|
|
555
|
+
self,
|
|
556
|
+
config: CellCommunicationConfig,
|
|
557
|
+
*,
|
|
558
|
+
rngs: nnx.Rngs | None = None,
|
|
559
|
+
name: str | None = None,
|
|
560
|
+
) -> None:
|
|
561
|
+
"""Initialize the cell communication operator.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
config: Cell communication configuration.
|
|
565
|
+
rngs: Random number generators for parameter initialization.
|
|
566
|
+
name: Optional operator name.
|
|
567
|
+
"""
|
|
568
|
+
super().__init__(config, rngs=rngs, name=name)
|
|
569
|
+
|
|
570
|
+
rngs = ensure_rngs(rngs)
|
|
571
|
+
|
|
572
|
+
# Node feature projection: n_genes -> hidden_dim
|
|
573
|
+
self.node_proj = nnx.Linear(
|
|
574
|
+
in_features=config.n_genes,
|
|
575
|
+
out_features=config.hidden_dim,
|
|
576
|
+
rngs=rngs,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Edge feature projection: raw LR features (2 * n_lr_pairs) -> edge_features_dim
|
|
580
|
+
self.edge_proj = nnx.Linear(
|
|
581
|
+
in_features=2 * config.n_lr_pairs,
|
|
582
|
+
out_features=config.edge_features_dim,
|
|
583
|
+
rngs=rngs,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# GATv2 stack
|
|
587
|
+
self.spatial_gnn = SpatialAttentionGNN(
|
|
588
|
+
in_features=config.hidden_dim,
|
|
589
|
+
hidden_dim=config.hidden_dim,
|
|
590
|
+
num_heads=config.num_heads,
|
|
591
|
+
edge_features_dim=config.edge_features_dim,
|
|
592
|
+
num_layers=config.num_gnn_layers,
|
|
593
|
+
dropout_rate=config.dropout_rate,
|
|
594
|
+
rngs=rngs,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Signaling decoder
|
|
598
|
+
self.decoder = SignalingDecoder(
|
|
599
|
+
hidden_dim=config.hidden_dim,
|
|
600
|
+
n_pathways=config.n_pathways,
|
|
601
|
+
n_lr_pairs=config.n_lr_pairs,
|
|
602
|
+
rngs=rngs,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
def _build_edge_features(
|
|
606
|
+
self,
|
|
607
|
+
counts: Float[Array, "n_cells n_genes"],
|
|
608
|
+
spatial_graph: Int[Array, "2 n_edges"],
|
|
609
|
+
lr_pairs: Int[Array, "n_pairs 2"],
|
|
610
|
+
) -> Float[Array, "n_edges edge_features_dim"]:
|
|
611
|
+
"""Compute per-edge L-R expression features.
|
|
612
|
+
|
|
613
|
+
For each edge (i, j) and each L-R pair (l, r) the raw feature is
|
|
614
|
+
``[counts[source, l], counts[target, r]]``. These are concatenated
|
|
615
|
+
across all pairs then projected to ``edge_features_dim``.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
counts: Gene expression matrix ``(n_cells, n_genes)``.
|
|
619
|
+
spatial_graph: Edge indices ``(source, target)`` ``(2, n_edges)``.
|
|
620
|
+
lr_pairs: L-R pair gene indices ``(n_pairs, 2)``.
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
Projected edge features ``(n_edges, edge_features_dim)``.
|
|
624
|
+
"""
|
|
625
|
+
sources = spatial_graph[0] # (n_edges,)
|
|
626
|
+
targets = spatial_graph[1] # (n_edges,)
|
|
627
|
+
|
|
628
|
+
# For each LR pair, gather ligand expression of source and receptor
|
|
629
|
+
# expression of target. Shape per pair: (n_edges, 2)
|
|
630
|
+
def _pair_features(pair: Int[Array, "2"]) -> Float[Array, "n_edges 2"]:
|
|
631
|
+
ligand_idx = pair[0]
|
|
632
|
+
receptor_idx = pair[1]
|
|
633
|
+
l_expr = counts[sources, ligand_idx] # (n_edges,)
|
|
634
|
+
r_expr = counts[targets, receptor_idx] # (n_edges,)
|
|
635
|
+
return jnp.stack([l_expr, r_expr], axis=-1)
|
|
636
|
+
|
|
637
|
+
# (n_pairs, n_edges, 2)
|
|
638
|
+
raw_features = jax.vmap(_pair_features)(lr_pairs)
|
|
639
|
+
# Reshape to (n_edges, 2*n_pairs)
|
|
640
|
+
n_edges = spatial_graph.shape[1]
|
|
641
|
+
raw_features = raw_features.transpose(1, 0, 2).reshape(n_edges, -1)
|
|
642
|
+
|
|
643
|
+
return self.edge_proj(raw_features)
|
|
644
|
+
|
|
645
|
+
def apply(
|
|
646
|
+
self,
|
|
647
|
+
data: PyTree,
|
|
648
|
+
state: PyTree,
|
|
649
|
+
metadata: dict[str, Any] | None,
|
|
650
|
+
random_params: Any = None,
|
|
651
|
+
stats: dict[str, Any] | None = None,
|
|
652
|
+
) -> tuple[PyTree, PyTree, dict[str, Any] | None]:
|
|
653
|
+
"""Apply GNN-based cell-cell communication analysis.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
data: Dictionary containing:
|
|
657
|
+
- ``"counts"``: Gene expression matrix ``(n_cells, n_genes)``
|
|
658
|
+
- ``"spatial_graph"``: Edge indices ``(2, n_edges)`` where
|
|
659
|
+
row 0 = source nodes, row 1 = target nodes
|
|
660
|
+
- ``"lr_pairs"``: L-R pair gene indices ``(n_pairs, 2)``
|
|
661
|
+
state: Element state (passed through unchanged).
|
|
662
|
+
metadata: Element metadata (passed through unchanged).
|
|
663
|
+
random_params: Not used (non-stochastic operator).
|
|
664
|
+
stats: Not used.
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
Tuple of (transformed_data, state, metadata):
|
|
668
|
+
- transformed_data contains all original keys plus:
|
|
669
|
+
|
|
670
|
+
- ``"communication_scores"``: ``(n_cells, n_pairs)``
|
|
671
|
+
- ``"signaling_activity"``: ``(n_cells, n_pathways)``
|
|
672
|
+
- ``"niche_embeddings"``: ``(n_cells, hidden_dim)``
|
|
673
|
+
- state is passed through unchanged
|
|
674
|
+
- metadata is passed through unchanged
|
|
675
|
+
"""
|
|
676
|
+
counts: Float[Array, "n_cells n_genes"] = data["counts"]
|
|
677
|
+
spatial_graph: Int[Array, "2 n_edges"] = data["spatial_graph"]
|
|
678
|
+
lr_pairs: Int[Array, "n_pairs 2"] = data["lr_pairs"]
|
|
679
|
+
|
|
680
|
+
# Step 1: Build per-edge L-R features
|
|
681
|
+
edge_features = self._build_edge_features(counts, spatial_graph, lr_pairs)
|
|
682
|
+
|
|
683
|
+
# Step 2: Project per-node gene expression to hidden dim
|
|
684
|
+
node_features = self.node_proj(counts) # (n_cells, hidden_dim)
|
|
685
|
+
|
|
686
|
+
# Step 3: GATv2 message passing
|
|
687
|
+
niche_embeddings = self.spatial_gnn(
|
|
688
|
+
node_features,
|
|
689
|
+
spatial_graph,
|
|
690
|
+
edge_features,
|
|
691
|
+
deterministic=True,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# Step 4: Decode to pathway activities and communication scores
|
|
695
|
+
signaling_activity, communication_scores = self.decoder(niche_embeddings)
|
|
696
|
+
|
|
697
|
+
transformed_data = {
|
|
698
|
+
**data,
|
|
699
|
+
"communication_scores": communication_scores,
|
|
700
|
+
"signaling_activity": signaling_activity,
|
|
701
|
+
"niche_embeddings": niche_embeddings,
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return transformed_data, state, metadata
|