Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
41189a5
init
selmanozleyen Jan 14, 2026
e058886
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 14, 2026
a0bfcd4
add cooccurance demo
selmanozleyen Jan 14, 2026
fa33967
Merge branch 'feat/device-settings' of https://github.com/selmanozley…
selmanozleyen Jan 14, 2026
5c67afd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 14, 2026
73c1b15
move import location
selmanozleyen Jan 14, 2026
4896c93
Merge branch 'feat/device-settings' of https://github.com/selmanozley…
selmanozleyen Jan 14, 2026
2474ac5
remove unused DeviceType
selmanozleyen Jan 14, 2026
20625c4
Merge branch 'main' into feat/device-settings
selmanozleyen Jan 15, 2026
1eca173
Merge branch 'main' into feat/device-settings
selmanozleyen Jan 23, 2026
464ae3c
save wip to test in the clusters
selmanozleyen Jan 26, 2026
037b6a8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 26, 2026
0820a0e
adjust deps but need prerelease of spatialdata for unpining dask
selmanozleyen Jan 26, 2026
70af5c7
fix the set_default_colors_for_categorical_obs
selmanozleyen Jan 26, 2026
74c70d9
add ligrec and cooccurence
selmanozleyen Jan 26, 2026
a1d2d10
reduce bloat code
selmanozleyen Jan 26, 2026
a331134
updates for the new dispatch approach
selmanozleyen Jan 26, 2026
0dffb8b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 26, 2026
de83264
use try except for compar
selmanozleyen Jan 26, 2026
1d7eb48
interphinx mapping
selmanozleyen Jan 26, 2026
17d34bd
rsc link refer workaround
selmanozleyen Jan 26, 2026
89be752
ad gpu notes
selmanozleyen Jan 26, 2026
5a7cb83
ok fix docs
selmanozleyen Jan 26, 2026
7478f62
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 26, 2026
74dc1ff
update docs
selmanozleyen Jan 26, 2026
f1b506e
refactor
selmanozleyen Jan 26, 2026
403f08f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 26, 2026
3aed99e
save wip
selmanozleyen Jan 27, 2026
ab5af39
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 27, 2026
a77f555
Merge branch 'main' into feat/device-settings
selmanozleyen Jan 27, 2026
b05f2bb
Merge branch 'main' into feat/device-settings
selmanozleyen Jan 27, 2026
c0aa5da
save wip
selmanozleyen Jan 27, 2026
8d03b5b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 27, 2026
08f7af6
remove kwargs from ligrec
selmanozleyen Jan 27, 2026
4885195
refactor and formatting
selmanozleyen Jan 27, 2026
ebb03c2
arg refactor
selmanozleyen Jan 27, 2026
eb58e0e
remove n_splits
selmanozleyen Jan 27, 2026
3568694
update parallelize args
selmanozleyen Jan 27, 2026
7b2aad3
apply suggestion
selmanozleyen Feb 2, 2026
4453d2b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 2, 2026
0482932
update docs
selmanozleyen Feb 2, 2026
8cd3829
Merge branch 'feat/device-settings' of https://github.com/selmanozley…
selmanozleyen Feb 2, 2026
f854794
fux docs
selmanozleyen Feb 2, 2026
26e8ddc
make gpu_availible an attr
selmanozleyen Feb 2, 2026
5034343
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 2, 2026
8aae2ee
revisions
selmanozleyen Feb 2, 2026
40f534a
Merge branch 'feat/device-settings' of https://github.com/selmanozley…
selmanozleyen Feb 2, 2026
25ebbb0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 2, 2026
52e0b4e
cleanup
selmanozleyen Feb 2, 2026
233ff07
Merge branch 'feat/device-settings' of https://github.com/selmanozley…
selmanozleyen Feb 2, 2026
0993068
Merge branch 'main' into feat/device-settings
selmanozleyen Feb 2, 2026
60f1eba
revert to old file
selmanozleyen Feb 2, 2026
bae11a6
Merge branch 'main' into feat/device-settings
selmanozleyen Feb 3, 2026
99d55e9
remove device and rely on context manager
selmanozleyen Feb 3, 2026
fb23269
Merge branch 'feat/device-settings' of https://github.com/selmanozley…
selmanozleyen Feb 3, 2026
22edd86
add device_kwargs
selmanozleyen Feb 3, 2026
5bc8ca4
pass args directly
selmanozleyen Feb 3, 2026
1f118da
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 4, 2026
97f5355
non-thread version of gpu dispatch
selmanozleyen Feb 4, 2026
b192fbe
gpu func cache
selmanozleyen Feb 4, 2026
6359577
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 4, 2026
9dca51b
context manager
selmanozleyen Feb 4, 2026
ac5421e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 4, 2026
06c001a
update
selmanozleyen Feb 4, 2026
b7ac76b
Merge branch 'feat/device-settings' of https://github.com/selmanozley…
selmanozleyen Feb 4, 2026
ba9e7e0
remove redundant test
selmanozleyen Feb 4, 2026
ae54591
redundant docstrings
selmanozleyen Feb 4, 2026
a137673
remove redundancy
selmanozleyen Feb 4, 2026
0ebb219
add uv.lock to gitignore
selmanozleyen Feb 4, 2026
a9517a9
remove device leftover
selmanozleyen Feb 4, 2026
4ebda67
spatial_neighbors doesnt match rsc much
selmanozleyen Feb 4, 2026
93b06dc
remove redundant test
selmanozleyen Feb 4, 2026
f19bdc7
change the order of filtering
selmanozleyen Feb 4, 2026
c0a10cb
make _SqSettings private
selmanozleyen Feb 4, 2026
0276e47
error when device_args is given
selmanozleyen Feb 4, 2026
53650c0
update tests
selmanozleyen Feb 4, 2026
49ed2fb
update regex patternf or test
selmanozleyen Feb 4, 2026
64fc33c
update gpu tests
selmanozleyen Feb 4, 2026
4f540ad
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 4, 2026
e3b331e
clean up test settings
selmanozleyen Feb 4, 2026
50ece87
simplify
selmanozleyen Feb 4, 2026
97fbb00
Merge branch 'main' into feat/device-settings
selmanozleyen Feb 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ optional-dependencies.docs = [
"sphinxcontrib-bibtex>=2.3",
"sphinxcontrib-spelling>=7.6.2",
]
optional-dependencies.gpu-cuda11 = [
"rapids-singlecell[rapids11]",
]
optional-dependencies.gpu-cuda12 = [
"rapids-singlecell[rapids12]",
]
optional-dependencies.leiden = [
"leidenalg",
"spatialleiden>=0.4",
Expand Down
3 changes: 2 additions & 1 deletion src/squidpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from importlib.metadata import PackageMetadata

from squidpy import datasets, experimental, gr, im, pl, read, tl
from squidpy.settings import settings

try:
md: PackageMetadata = metadata.metadata(__name__)
Expand All @@ -15,4 +16,4 @@

del metadata, md

__all__ = ["datasets", "experimental", "gr", "im", "pl", "read", "tl"]
__all__ = ["datasets", "experimental", "gr", "im", "pl", "read", "tl", "settings"]
35 changes: 35 additions & 0 deletions src/squidpy/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import xarray as xr
from spatialdata.models import Image2DModel, Labels2DModel

from squidpy.settings import DeviceType

__all__ = ["singledispatchmethod", "Signal", "SigQueue", "NDArray", "NDArrayA"]


Expand Down Expand Up @@ -387,3 +389,36 @@ def _ensure_dim_order(img_da: xr.DataArray, order: Literal["cyx", "yxc"] = "yxc"
img_da = img_da.expand_dims({"c": [0]})
# After possible expand, just transpose to target
return img_da.transpose(*tuple(order))


def resolve_device_arg(device: DeviceType | None) -> Literal["cpu", "gpu"]:
"""
Resolve per-call device argument to actual backend.

Parameters
----------
device
Per-call device setting. None uses ``settings.device``.

Returns
-------
Literal["cpu", "gpu"]
The resolved backend to use.

Raises
------
RuntimeError
If GPU is requested but rapids-singlecell is not installed.
"""
from squidpy.settings import settings

if device is None:
device = settings.device
if device == "cpu":
return "cpu"
if device == "gpu":
if not settings.gpu_available():
raise RuntimeError("GPU unavailable. Install with: pip install squidpy[gpu-cuda12] or squidpy[gpu-cuda11]")
return "gpu"
# if device == "auto"
return "gpu" if settings.gpu_available() else "cpu"
22 changes: 21 additions & 1 deletion src/squidpy/gr/_ppatterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from squidpy._constants._constants import SpatialAutocorr
from squidpy._constants._pkg_constants import Key
from squidpy._docs import d, inject_docs
from squidpy._utils import NDArrayA, Signal, SigQueue, _get_n_cores, parallelize
from squidpy._utils import NDArrayA, Signal, SigQueue, _get_n_cores, parallelize, resolve_device_arg
from squidpy.gr._utils import (
_assert_categorical_obs,
_assert_connectivity_key,
Expand Down Expand Up @@ -352,6 +352,7 @@ def co_occurrence(
n_jobs: int | None = None,
backend: str = "loky",
show_progress_bar: bool = True,
device: Literal["cpu", "gpu"] | None = None,
) -> tuple[NDArrayA, NDArrayA] | None:
"""
Compute co-occurrence probability of clusters.
Expand All @@ -369,6 +370,9 @@ def co_occurrence(
Number of splits in which to divide the spatial coordinates in
:attr:`anndata.AnnData.obsm` ``['{spatial_key}']``.
%(parallelize)s
device
Device to use for computation. If ``None``, uses :attr:`squidpy.settings.device`.
Set to ``"gpu"`` to use rapids-singlecell GPU acceleration.

Returns
-------
Expand All @@ -381,6 +385,22 @@ def co_occurrence(
- :attr:`anndata.AnnData.uns` ``['{cluster_key}_co_occurrence']['interval']`` - the distance thresholds
computed at ``interval``.
"""
effective_device = resolve_device_arg(device)

if effective_device == "gpu":
from rapids_singlecell.squidpy import co_occurrence as rsc_co_occurrence

return rsc_co_occurrence(
adata,
cluster_key=cluster_key,
spatial_key=spatial_key,
interval=interval,
copy=copy,
n_splits=n_splits,
n_jobs=n_jobs,
backend=backend,
show_progress_bar=show_progress_bar,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can generally be wrapped up in a decorator where we:

  1. check effective_device
  2. Subset the inbound arguments from squidpy to those of rapids' function (I imagine n_jobs doesn't apply, this can probably be done with python's inspect module)
  3. run the rapids function
  4. Optionally bring the data back to the CPU (I would guess people want this, or at least it should be behind a flag)


if isinstance(adata, SpatialData):
adata = adata.table
Expand Down
7 changes: 7 additions & 0 deletions src/squidpy/settings/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Squidpy settings and configuration."""

from __future__ import annotations

from squidpy.settings._settings import DeviceType, settings

__all__ = ["settings", "DeviceType"]
51 changes: 51 additions & 0 deletions src/squidpy/settings/_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

from contextvars import ContextVar
from typing import Literal, get_args

__all__ = ["settings", "DeviceType"]

DeviceType = Literal["auto", "cpu", "gpu"]

_device_var: ContextVar[DeviceType] = ContextVar("device", default="auto")


class _SqSettings:
"""Global settings for squidpy."""

@property
def device(self) -> DeviceType:
"""Current compute device setting."""
return _device_var.get()

@device.setter
def device(self, value: DeviceType) -> None:
valid = get_args(DeviceType)
if value not in valid:
raise ValueError(f"Invalid device {value!r}. Must be one of: {valid}")
if value == "gpu" and not self.gpu_available():
raise RuntimeError(
"Cannot set device='gpu': rapids-singlecell not installed. "
"Install with: pip install squidpy[gpu-cuda12] or squidpy[gpu-cuda11]"
)
_device_var.set(value)

@staticmethod
def gpu_available() -> bool:
"""
Check if GPU acceleration is available.

Returns
-------
bool
True if rapids-singlecell is installed and importable.
"""
try:
import rapids_singlecell # noqa: F401

return True
except ImportError:
return False


settings = _SqSettings()
63 changes: 63 additions & 0 deletions tests/test_gpu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Tests for GPU functionality (skipped in CI without GPU)."""

from __future__ import annotations

import pytest

from squidpy.settings import settings

# Skip all tests in this module if GPU is not available
pytestmark = pytest.mark.skipif(
not settings.gpu_available(),
reason="GPU tests require rapids-singlecell to be installed",
)


class TestGPUCoOccurrence:
"""Test GPU-accelerated co_occurrence function."""

def test_co_occurrence_gpu(self, adata):
"""Test co_occurrence with GPU device."""
import squidpy as sq

# Run with explicit GPU device
result = sq.gr.co_occurrence(
adata,
cluster_key="leiden",
copy=True,
device="gpu",
)

assert result is not None
arr, interval = result
assert arr.ndim == 3
assert arr.shape[1] == arr.shape[0] == adata.obs["leiden"].unique().shape[0]

def test_co_occurrence_gpu_vs_cpu(self, adata):
"""Test that GPU and CPU results are approximately equal."""
import numpy as np

import squidpy as sq

# Run on CPU
cpu_result = sq.gr.co_occurrence(
adata,
cluster_key="leiden",
copy=True,
device="cpu",
)

# Run on GPU
gpu_result = sq.gr.co_occurrence(
adata,
cluster_key="leiden",
copy=True,
device="gpu",
)

cpu_arr, cpu_interval = cpu_result
gpu_arr, gpu_interval = gpu_result

# Results should be close (allow for floating point differences)
np.testing.assert_allclose(cpu_interval, gpu_interval, rtol=1e-5)
np.testing.assert_allclose(cpu_arr, gpu_arr, rtol=1e-5)
35 changes: 35 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Tests for squidpy.settings module."""

from __future__ import annotations

import pytest

from squidpy.settings import settings


class TestSettings:
"""Test the settings module."""

def test_default_device(self):
"""Test that default device is 'auto'."""
# Reset to default
settings.device = "auto"
assert settings.device == "auto"

def test_set_device_cpu(self):
"""Test setting device to 'cpu'."""
settings.device = "cpu"
assert settings.device == "cpu"
settings.device = "auto" # reset

def test_set_device_invalid(self):
"""Test that invalid device raises ValueError."""
with pytest.raises(ValueError, match="Invalid device"):
settings.device = "invalid"

def test_set_device_gpu_without_rsc(self):
"""Test that setting device to 'gpu' without rapids-singlecell raises RuntimeError."""
# This will fail if rapids-singlecell is not installed
if not settings.gpu_available():
with pytest.raises(RuntimeError, match="rapids-singlecell not installed"):
settings.device = "gpu"