Source code for spatialvi.data._fields

"""Custom data fields for spatial transcriptomics."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

import numpy as np
from scvi.data.fields import ObsmField

if TYPE_CHECKING:
    from anndata import AnnData

logger = logging.getLogger(__name__)


[docs] class SpatialCoordinatesField(ObsmField): """Field for spatial coordinates. Parameters ---------- registry_key Key to register in the data registry. attr_key Key in adata.obsm for spatial coordinates. """ def __init__( self, registry_key: str, attr_key: str = "spatial", ): super().__init__( registry_key=registry_key, attr_key=attr_key, )
[docs] def validate_field(self, adata: AnnData) -> None: """Validate spatial coordinates. Parameters ---------- adata AnnData object to validate. """ super().validate_field(adata) if self.attr_key in adata.obsm: coords = adata.obsm[self.attr_key] if hasattr(coords, "values"): coords = coords.values if coords.ndim != 2: raise ValueError(f"Spatial coordinates must be 2D, got {coords.ndim}D") if coords.shape[1] not in (2, 3): raise ValueError(f"Spatial coordinates must have 2 or 3 dimensions, got {coords.shape[1]}")
[docs] class NeighborIndexField(ObsmField): """Field for spatial neighbor indices. Parameters ---------- registry_key Key to register in the data registry. attr_key Key in adata.obsm for neighbor indices. n_neighbors Expected number of neighbors. """ def __init__( self, registry_key: str, attr_key: str = "nn_index", n_neighbors: int | None = None, ): super().__init__( registry_key=registry_key, attr_key=attr_key, ) self.n_neighbors = n_neighbors
[docs] def validate_field(self, adata: AnnData) -> None: """Validate neighbor indices. Parameters ---------- adata AnnData object to validate. """ super().validate_field(adata) if self.attr_key in adata.obsm: indices = adata.obsm[self.attr_key] if indices.ndim != 2: raise ValueError(f"Neighbor indices must be 2D, got {indices.ndim}D") if indices.shape[0] != adata.n_obs: raise ValueError(f"Neighbor indices must have {adata.n_obs} rows, got {indices.shape[0]}") if self.n_neighbors is not None and indices.shape[1] != self.n_neighbors: logger.warning(f"Expected {self.n_neighbors} neighbors, got {indices.shape[1]}") # Check for valid indices if indices.max() >= adata.n_obs: raise ValueError(f"Neighbor index {indices.max()} out of bounds for {adata.n_obs} observations")
[docs] class NeighborDistanceField(ObsmField): """Field for spatial neighbor distances. Parameters ---------- registry_key Key to register in the data registry. attr_key Key in adata.obsm for neighbor distances. """ def __init__( self, registry_key: str, attr_key: str = "nn_dist", ): super().__init__( registry_key=registry_key, attr_key=attr_key, )
[docs] def validate_field(self, adata: AnnData) -> None: """Validate neighbor distances. Parameters ---------- adata AnnData object to validate. """ super().validate_field(adata) if self.attr_key in adata.obsm: distances = adata.obsm[self.attr_key] if distances.ndim != 2: raise ValueError(f"Neighbor distances must be 2D, got {distances.ndim}D") if (distances < 0).any(): raise ValueError("Neighbor distances must be non-negative")
[docs] class NicheCompositionField(ObsmField): """Field for neighborhood cell type composition. Parameters ---------- registry_key Key to register in the data registry. attr_key Key in adata.obsm for niche composition. n_cell_types Expected number of cell types. """ def __init__( self, registry_key: str, attr_key: str = "niche_composition", n_cell_types: int | None = None, ): super().__init__( registry_key=registry_key, attr_key=attr_key, ) self.n_cell_types = n_cell_types
[docs] def validate_field(self, adata: AnnData) -> None: """Validate niche composition. Parameters ---------- adata AnnData object to validate. """ super().validate_field(adata) if self.attr_key in adata.obsm: composition = adata.obsm[self.attr_key] if composition.ndim != 2: raise ValueError(f"Niche composition must be 2D, got {composition.ndim}D") if composition.shape[0] != adata.n_obs: raise ValueError(f"Niche composition must have {adata.n_obs} rows, got {composition.shape[0]}") # Check that composition sums to 1 row_sums = composition.sum(axis=1) if not np.allclose(row_sums, 1.0, atol=1e-4): logger.warning("Niche composition rows do not sum to 1. Consider normalizing.") if (composition < 0).any() or (composition > 1).any(): raise ValueError("Niche composition values must be between 0 and 1")