From 049b675f9e2a9eba48b418d0a1c2006c02dc0d10 Mon Sep 17 00:00:00 2001 From: Intron7 Date: Tue, 7 Apr 2026 12:03:34 +0200 Subject: [PATCH] add squidpy backend --- docs/conf.py | 2 ++ pyproject.toml | 3 ++ src/rapids_singlecell/_compat.py | 5 ++++ src/rapids_singlecell/squidpy_backend.py | 30 +++++++++++++++++++ .../squidpy_gpu/_autocorr.py | 5 +++- src/rapids_singlecell/squidpy_gpu/_co_oc.py | 5 +++- src/rapids_singlecell/squidpy_gpu/_ligrec.py | 6 +++- tests/test_backend_conformance.py | 11 +++++++ 8 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/rapids_singlecell/squidpy_backend.py create mode 100644 tests/test_backend_conformance.py diff --git a/docs/conf.py b/docs/conf.py index e8569ca1..0931d88d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -79,6 +79,7 @@ "pylibraft", "dask", "cuvs", + "spatialdata", ] default_role = "literal" napoleon_google_docstring = False @@ -126,6 +127,7 @@ "statsmodels": ("https://www.statsmodels.org/stable/", None), "omnipath": ("https://omnipath.readthedocs.io/en/latest/", None), "dask": ("https://docs.dask.org/en/stable/", None), + "spatialdata": ("https://spatialdata.scverse.org/en/stable/", None), } # List of patterns, relative to source directory, that match files and diff --git a/pyproject.toml b/pyproject.toml index c38e1d00..b2069c07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,9 @@ dev = [ "pre-commit", ] +[project.entry-points."squidpy.backends"] +rapids_singlecell = "rapids_singlecell.squidpy_backend:RscSquidpyBackend" + [project.urls] Documentation = "https://rapids-singlecell.readthedocs.io" Source = "https://github.com/scverse/rapids_singlecell" diff --git a/src/rapids_singlecell/_compat.py b/src/rapids_singlecell/_compat.py index 984ae1fc..d331bc70 100644 --- a/src/rapids_singlecell/_compat.py +++ b/src/rapids_singlecell/_compat.py @@ -7,6 +7,11 @@ from scipy.sparse import csc_matrix as csc_matrix_cpu from scipy.sparse import csr_matrix as csr_matrix_cpu +try: + from spatialdata import SpatialData +except ImportError: + SpatialData = None + def _meta_dense(dtype): return cp.zeros([0], dtype=dtype) diff --git a/src/rapids_singlecell/squidpy_backend.py b/src/rapids_singlecell/squidpy_backend.py new file mode 100644 index 00000000..97e410b6 --- /dev/null +++ b/src/rapids_singlecell/squidpy_backend.py @@ -0,0 +1,30 @@ +"""Squidpy backend adapter for rapids_singlecell. + +The dispatch decorator introspects the real RSC function signatures +(lazily imported on first access), so no need to duplicate them here. +""" + +from __future__ import annotations + +import importlib + + +class RscSquidpyBackend: + """Backend adapter exposing rapids_singlecell GPU implementations to squidpy.""" + + name = "rapids_singlecell" + aliases = ["rapids-singlecell", "rsc", "cuda", "gpu"] + + # squidpy function name -> module that implements it + _functions = { + "spatial_autocorr": "rapids_singlecell.squidpy_gpu", + "co_occurrence": "rapids_singlecell.squidpy_gpu", + "ligrec": "rapids_singlecell.squidpy_gpu", + } + + def __getattr__(self, name: str): + if name in self._functions: + func = getattr(importlib.import_module(self._functions[name]), name) + setattr(self, name, func) # cache on instance + return func + raise AttributeError(f"{type(self).__name__!r} has no attribute {name!r}") diff --git a/src/rapids_singlecell/squidpy_gpu/_autocorr.py b/src/rapids_singlecell/squidpy_gpu/_autocorr.py index 76b2a3f3..5ea63c15 100644 --- a/src/rapids_singlecell/squidpy_gpu/_autocorr.py +++ b/src/rapids_singlecell/squidpy_gpu/_autocorr.py @@ -12,6 +12,7 @@ from scipy import sparse from statsmodels.stats.multitest import multipletests +from rapids_singlecell._compat import SpatialData from rapids_singlecell.preprocessing._utils import _sparse_to_dense from ._gearysc import _gearys_C_cupy @@ -49,7 +50,7 @@ def _to_cupy(vals, *, use_sparse: bool, dtype): def spatial_autocorr( - adata: AnnData, + adata: AnnData | SpatialData, *, connectivity_key: str = "spatial_connectivities", genes: str | Sequence[str] | None = None, @@ -118,6 +119,8 @@ def spatial_autocorr( DataFrame containing the autocorrelation scores, p-values, and corrected p-values for each gene. \ If `copy` is False, the results are stored in `adata.uns` and None is returned. """ + if SpatialData is not None and isinstance(adata, SpatialData): + adata = adata.table if genes is None: if "highly_variable" in adata.var: genes = adata[:, adata.var["highly_variable"]].var_names.values diff --git a/src/rapids_singlecell/squidpy_gpu/_co_oc.py b/src/rapids_singlecell/squidpy_gpu/_co_oc.py index e14c247e..78efb33b 100644 --- a/src/rapids_singlecell/squidpy_gpu/_co_oc.py +++ b/src/rapids_singlecell/squidpy_gpu/_co_oc.py @@ -6,6 +6,7 @@ import numpy as np from cuml.metrics import pairwise_distances +from rapids_singlecell._compat import SpatialData from rapids_singlecell._cuda import _cooc_cuda as _co from rapids_singlecell._utils import ( _calculate_blocks_per_pair, @@ -21,7 +22,7 @@ def co_occurrence( - adata: AnnData, + adata: AnnData | SpatialData, cluster_key: str, *, spatial_key: str = "spatial", @@ -65,6 +66,8 @@ def co_occurrence( computed at ``interval``. """ + if SpatialData is not None and isinstance(adata, SpatialData): + adata = adata.table _assert_categorical_obs(adata, key=cluster_key) _assert_spatial_basis(adata, key=spatial_key) spatial = cp.array(adata.obsm[spatial_key]).astype(np.float32) diff --git a/src/rapids_singlecell/squidpy_gpu/_ligrec.py b/src/rapids_singlecell/squidpy_gpu/_ligrec.py index f6cbadd2..80754e25 100644 --- a/src/rapids_singlecell/squidpy_gpu/_ligrec.py +++ b/src/rapids_singlecell/squidpy_gpu/_ligrec.py @@ -14,6 +14,8 @@ from cupyx.scipy.sparse import issparse as cpissparse from scipy.sparse import csc_matrix, issparse +from rapids_singlecell._compat import SpatialData + from ._utils import _assert_categorical_obs, _create_sparse_df SOURCE = "source" @@ -118,7 +120,7 @@ def _check_tuple_needles(needles, haystack, *, msg: str, reraise: bool = True): def ligrec( - adata: AnnData, + adata: AnnData | SpatialData, cluster_key: str, *, clusters: list | None = None, @@ -233,6 +235,8 @@ def ligrec( interacting components was 0 or it didn't pass the threshold percentage of \ cells being expressed within a given cluster. """ + if SpatialData is not None and isinstance(adata, SpatialData): + adata = adata.table # Get and Check interactions if interactions is None: interactions = _get_interactions( diff --git a/tests/test_backend_conformance.py b/tests/test_backend_conformance.py new file mode 100644 index 00000000..67bb0b33 --- /dev/null +++ b/tests/test_backend_conformance.py @@ -0,0 +1,11 @@ +"""Run squidpy's backend conformance suite against the RSC backend.""" + +from __future__ import annotations + +from squidpy.testing.backend_conformance import validate_backend + + +def test_conformance(): + results = validate_backend("rapids_singlecell") + for name, status in results.items(): + assert status == "PASSED", f"{name}: {status}"