"""Harreman module for metabolic exchange inference."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Literal
import torch
from torch import nn
if TYPE_CHECKING:
from torch import Tensor
logger = logging.getLogger(__name__)
[docs]
class HarremanModule(nn.Module):
"""Neural network module for Harreman metabolic exchange analysis.
This module computes spatial correlations between metabolic genes
and their neighborhood expression patterns using GPU acceleration.
Parameters
----------
n_genes
Number of metabolic genes.
n_neighbors
Number of spatial neighbors.
correlation_method
Method for computing correlations.
"""
def __init__(
self,
n_genes: int,
n_neighbors: int = 20,
correlation_method: Literal["pearson", "spearman"] = "pearson",
):
super().__init__()
self.n_genes = n_genes
self.n_neighbors = n_neighbors
self.correlation_method = correlation_method
[docs]
def compute_neighbor_expression(
self,
X: Tensor,
neighbor_indices: Tensor,
) -> Tensor:
"""Compute mean neighbor expression.
Parameters
----------
X
Gene expression tensor of shape (n_cells, n_genes).
neighbor_indices
Neighbor indices of shape (n_cells, n_neighbors).
Returns
-------
Mean neighbor expression of shape (n_cells, n_genes).
"""
# Gather neighbor expression
neighbor_expr = X[neighbor_indices.long()] # (n_cells, n_neighbors, n_genes)
return neighbor_expr.mean(dim=1)
[docs]
def compute_spatial_correlation(
self,
X: Tensor,
neighbor_mean: Tensor,
) -> Tensor:
"""Compute spatial correlation for each gene.
Parameters
----------
X
Gene expression tensor.
neighbor_mean
Mean neighbor expression.
Returns
-------
Correlation tensor of shape (n_genes,).
"""
if self.correlation_method == "spearman":
# Rank transform for Spearman
X_rank = self._rank_transform(X)
neighbor_rank = self._rank_transform(neighbor_mean)
return self._pearson_correlation(X_rank, neighbor_rank)
else:
return self._pearson_correlation(X, neighbor_mean)
def _rank_transform(self, X: Tensor) -> Tensor:
"""Convert to ranks along the cell dimension."""
sorted_indices = torch.argsort(X, dim=0)
ranks = torch.empty_like(X)
for g in range(X.shape[1]):
ranks[sorted_indices[:, g], g] = torch.arange(X.shape[0], dtype=X.dtype, device=X.device)
return ranks
def _pearson_correlation(self, X: Tensor, Y: Tensor) -> Tensor:
"""Compute Pearson correlation per gene."""
X_centered = X - X.mean(dim=0, keepdim=True)
Y_centered = Y - Y.mean(dim=0, keepdim=True)
numerator = (X_centered * Y_centered).sum(dim=0)
denominator = torch.sqrt((X_centered**2).sum(dim=0) * (Y_centered**2).sum(dim=0) + 1e-8)
return numerator / denominator
[docs]
def forward(
self,
X: Tensor,
neighbor_indices: Tensor,
) -> dict[str, Tensor]:
"""Run forward pass.
Parameters
----------
X
Gene expression tensor.
neighbor_indices
Neighbor indices.
Returns
-------
Dictionary with spatial_correlation and neighbor_expression.
"""
neighbor_mean = self.compute_neighbor_expression(X, neighbor_indices)
spatial_corr = self.compute_spatial_correlation(X, neighbor_mean)
return {
"spatial_correlation": spatial_corr,
"neighbor_expression": neighbor_mean,
}