Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions docs/api/scanpy_gpu.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

These functions offer accelerated near drop-in replacements for common tools provided by [`scanpy`](https://scanpy.readthedocs.io/en/stable/api/index.html) {cite}`Wolf2018`.

## Scanpy backend

With Scanpy versions that support computational backends, RAPIDS-singlecell is
available as the `rapids-singlecell` backend with the aliases `cuda`, `rapids`,
and `rapids_singlecell`.

```python
import scanpy as sc

sc.settings.backend = "cuda"
```

The backend exposes RAPIDS-singlecell's `pp` and `tl` functions, plus {func}`rapids_singlecell.get.aggregate`, for Scanpy's backend dispatcher.
The data must already be on the GPU: move it with {func}`~rapids_singlecell.get.anndata_to_GPU` before calling the accelerated functions (see {doc}`/usage_principles`). Scanpy's `copy` argument, and `subset`/`inplace` of {func}`~rapids_singlecell.pp.highly_variable_genes`, behave as in Scanpy; other arguments that RAPIDS-singlecell does not support are dropped with a warning.

Backend calls use RAPIDS-singlecell's native defaults and return types. In particular, {func}`~rapids_singlecell.pp.normalize_total` and {func}`~rapids_singlecell.pp.normalize_pearson_residuals` return the normalized GPU matrix directly with `inplace=False`, rather than Scanpy's result dictionary. {func}`~rapids_singlecell.pp.calculate_qc_metrics` defaults to `inplace=False` through the backend, matching Scanpy.

## Preprocessing `pp`
Filtering of highly-variable genes, batch-effect correction, per-cell normalization.

Expand Down
18 changes: 18 additions & 0 deletions docs/api/squidpy_gpu.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@
{mod}`squidpy.gr` is a tool for the analysis of spatial molecular data {cite}`Palla2022`.
{mod}`rapids_singlecell.gr` accelerates some of these functions.

## Squidpy backend

With Squidpy versions that support computational backends, RAPIDS-singlecell is
available as the `rapids-singlecell` backend with the aliases `cuda`, `rapids`,
and `rapids_singlecell`.

```python
import squidpy as sq

sq.settings.backend = "cuda"
```

The backend exposes RAPIDS-singlecell's {mod}`rapids_singlecell.gr` functions for Squidpy's backend dispatcher, including the flavor-specific {func}`~rapids_singlecell.gr.calculate_niche_neighborhood`, {func}`~rapids_singlecell.gr.calculate_niche_utag` and {func}`~rapids_singlecell.gr.calculate_niche_cellcharter`.

Like their Squidpy counterparts, {func}`~rapids_singlecell.gr.spatial_autocorr`, {func}`~rapids_singlecell.gr.co_occurrence`, {func}`~rapids_singlecell.gr.ligrec` and the `calculate_niche_*` functions accept a {class}`~spatialdata.SpatialData` together with `table_key`. `flavor="spatialleiden"` of {func}`squidpy.gr.calculate_niche` has no GPU implementation and raises `NotImplementedError` on this backend; `calculate_niche_spatialleiden` is not exposed, so Squidpy runs it on the CPU.

Niche calls use RAPIDS-singlecell's native defaults and validation. The deprecated {func}`~rapids_singlecell.gr.calculate_niche` supplies `n_neighbors=15` and `resolutions=(0.5,)` when omitted and emits its native deprecation warning; prefer the flavor-specific functions. The backend translates Squidpy's `data`, `copy`, and `rng` arguments to the native names.

```{eval-rst}
.. module:: rapids_singlecell.gr
.. currentmodule:: rapids_singlecell
Expand Down
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"pylibraft",
"dask",
"cuvs",
"spatialdata",
]
default_role = "literal"
napoleon_google_docstring = False
Expand Down Expand Up @@ -128,6 +129,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
Expand Down
15 changes: 15 additions & 0 deletions docs/release-notes/0.18.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
### 0.18.0 {small}`the-future`

```{rubric} Bug fixes
```
* Fix {func}`~rapids_singlecell.gr.ligrec` raising ``KeyError`` when the cluster column has integer categories {pr}`628` {smaller}`S Dicks`

```{rubric} Features
```
* Register RAPIDS-singlecell as a computational backend for Scanpy and Squidpy through the ``scanpy.backends`` and ``squidpy.backends`` entry points. Select it with ``sc.settings.backend = "cuda"`` / ``sq.settings.backend = "cuda"`` (aliases ``rapids`` and ``rapids_singlecell``) or per call with ``backend="cuda"``. All {mod}`~rapids_singlecell.pp`, {mod}`~rapids_singlecell.tl` and {mod}`~rapids_singlecell.gr` functions plus {func}`~rapids_singlecell.get.aggregate` are exposed, including the flavor-specific niche functions. Backend calls use RAPIDS-singlecell's native defaults and return types {pr}`628` {smaller}`S Dicks`
* {func}`~rapids_singlecell.gr.spatial_autocorr`, {func}`~rapids_singlecell.gr.co_occurrence`, {func}`~rapids_singlecell.gr.ligrec` and the ``calculate_niche_*`` functions accept a {class}`~spatialdata.SpatialData` together with ``table_key``, following {mod}`squidpy` {pr}`628` {smaller}`S Dicks`

```{rubric} Misc
```
* Add native ``copy`` support to {func}`~rapids_singlecell.pp.filter_cells`, {func}`~rapids_singlecell.pp.filter_genes`, {func}`~rapids_singlecell.pp.regress_out`, {func}`~rapids_singlecell.tl.diffmap`, {func}`~rapids_singlecell.tl.draw_graph` and {func}`~rapids_singlecell.tl.rank_genes_groups`, and ``subset``/``inplace`` support to {func}`~rapids_singlecell.pp.highly_variable_genes` {pr}`628` {smaller}`S Dicks`
* Scanpy compat: {func}`~rapids_singlecell.pp.calculate_qc_metrics` accepts ``inplace``; with ``inplace=False`` it returns the per-cell and per-gene metrics as two DataFrames instead of writing them to ``adata`` {pr}`628` {smaller}`S Dicks`
4 changes: 4 additions & 0 deletions docs/release-notes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

# Release notes

## Version 0.18.0
```{include} /release-notes/0.18.0.md
```

## Version 0.17.0
```{include} /release-notes/0.17.0.md
```
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ dev = [
"pre-commit",
]

[project.entry-points."scanpy.backends"]
rapids-singlecell = "rapids_singlecell._backends.scanpy"

[project.entry-points."squidpy.backends"]
rapids-singlecell = "rapids_singlecell._backends.squidpy"

[project.urls]
Documentation = "https://rapids-singlecell.readthedocs.io"
Source = "https://github.com/scverse/rapids_singlecell"
Expand Down
1 change: 1 addition & 0 deletions src/rapids_singlecell/_backends/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from __future__ import annotations
85 changes: 85 additions & 0 deletions src/rapids_singlecell/_backends/scanpy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Scanpy backend exports with RAPIDS-singlecell's native semantics."""

from __future__ import annotations

from functools import partial

from rapids_singlecell.get import aggregate
from rapids_singlecell.preprocessing import (
bbknn,
filter_cells,
filter_genes,
filter_highly_variable,
flag_gene_family,
harmony_integrate,
highly_variable_genes,
log1p,
neighbors,
normalize_pearson_residuals,
normalize_total,
pca,
regress_out,
scale,
scrublet,
scrublet_simulate_doublets,
sqrt,
)
from rapids_singlecell.preprocessing import (
calculate_qc_metrics as _calculate_qc_metrics,
)
from rapids_singlecell.tools import (
diffmap,
draw_graph,
embedding_density,
ingest,
kmeans,
leiden,
louvain,
rank_genes_groups,
rank_genes_groups_logreg,
score_genes,
score_genes_cell_cycle,
tsne,
umap,
)

name = "rapids-singlecell"
aliases = ["cuda", "rapids", "rapids_singlecell"]

calculate_qc_metrics = partial(_calculate_qc_metrics, inplace=False)


__all__ = [
"aggregate",
"bbknn",
"calculate_qc_metrics",
"diffmap",
"draw_graph",
"embedding_density",
"filter_cells",
"filter_genes",
"filter_highly_variable",
"flag_gene_family",
"harmony_integrate",
"highly_variable_genes",
"ingest",
"kmeans",
"leiden",
"log1p",
"louvain",
"neighbors",
"normalize_pearson_residuals",
"normalize_total",
"pca",
"rank_genes_groups",
"rank_genes_groups_logreg",
"regress_out",
"scale",
"score_genes",
"score_genes_cell_cycle",
"scrublet",
"scrublet_simulate_doublets",
"sqrt",
"tsne",
"umap",
]
87 changes: 87 additions & 0 deletions src/rapids_singlecell/_backends/squidpy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Squidpy backend using native defaults and small niche argument bridges."""

from __future__ import annotations

import inspect
import warnings
from functools import wraps

from rapids_singlecell._utils._random import _seed_from_rng
from rapids_singlecell.squidpy_gpu import calculate_niche as _calculate_niche
from rapids_singlecell.squidpy_gpu import calculate_niche_cellcharter as _cellcharter
from rapids_singlecell.squidpy_gpu import calculate_niche_neighborhood as _neighborhood
from rapids_singlecell.squidpy_gpu import calculate_niche_utag as _utag
from rapids_singlecell.squidpy_gpu import co_occurrence, spatial_autocorr
from rapids_singlecell.squidpy_gpu import ligrec as _ligrec

name = "rapids-singlecell"
aliases = ["cuda", "rapids", "rapids_singlecell"]

SQUIDPY_LIGREC_CPU_ONLY = {
"seed": None,
"n_jobs": None,
"show_progress_bar": True,
"numba_parallel": None,
}


@wraps(_ligrec)
def ligrec(*args, **kwargs):
ignored = []
for key, default in SQUIDPY_LIGREC_CPU_ONLY.items():
if key in kwargs and kwargs.pop(key) != default:
ignored.append(key)
if ignored:
warnings.warn(
f"Parameters {', '.join(ignored)} have no effect on the "
"rapids-singlecell backend.",
UserWarning,
stacklevel=2,
)
return _ligrec(*args, **kwargs)


def _niche_adapter(func, *, flavor_api: bool = False):
"""Expose native niche signatures with Squidpy's argument names."""
names = {"adata": "data"}
defaults = {}
if flavor_api:
names.update(inplace="copy", random_state="rng")
defaults.update(copy=False, rng=None)

@wraps(func)
def adapted(data, **kwargs):
if kwargs.get("flavor") == "spatialleiden":
raise NotImplementedError("Use `backend='cpu'` for flavor='spatialleiden'.")
if flavor_api:
if "copy" in kwargs:
kwargs["inplace"] = not kwargs.pop("copy")
if (rng := kwargs.pop("rng", None)) is not None:
kwargs["random_state"] = _seed_from_rng(rng, allow_none=False)
return func(data, **kwargs)

signature = inspect.signature(func)
parameters = []
for parameter in signature.parameters.values():
name = names.get(parameter.name, parameter.name)
parameters.append(
parameter.replace(name=name, default=defaults.get(name, parameter.default))
)
adapted.__signature__ = signature.replace(parameters=parameters)
return adapted


calculate_niche = _niche_adapter(_calculate_niche)
calculate_niche_neighborhood = _niche_adapter(_neighborhood, flavor_api=True)
calculate_niche_utag = _niche_adapter(_utag, flavor_api=True)
calculate_niche_cellcharter = _niche_adapter(_cellcharter, flavor_api=True)

__all__ = [
"calculate_niche",
"calculate_niche_cellcharter",
"calculate_niche_neighborhood",
"calculate_niche_utag",
"co_occurrence",
"ligrec",
"spatial_autocorr",
]
33 changes: 30 additions & 3 deletions src/rapids_singlecell/preprocessing/_hvg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from typing import TYPE_CHECKING, Literal

import numpy as np
from anndata import AnnData
from cupyx.scipy.sparse import issparse

from rapids_singlecell._settings import Default, resolve_default
from rapids_singlecell.get import _get_obs_rep, _set_obs_rep
from rapids_singlecell.preprocessing._utils import _sanitize_column

from ._cutoffs import _Cutoffs
Expand All @@ -17,7 +20,7 @@
from ._seurat_v3 import _highly_variable_genes_seurat_v3

if TYPE_CHECKING:
from anndata import AnnData
import pandas as pd

flavors = Literal[
"seurat",
Expand Down Expand Up @@ -47,7 +50,9 @@ def highly_variable_genes(
chunksize: int = 1000,
n_samples: int = 10000,
batch_key: str | None = None,
) -> None:
subset: bool = False,
inplace: bool = True,
) -> pd.DataFrame | None:
"""\
Annotate highly variable genes :cite:p:`Satija2015,Zheng2017,Stuart2019,Lause2021,Andrews2019`.

Expand Down Expand Up @@ -117,10 +122,15 @@ def highly_variable_genes(
of enrichment of zeros for each gene (only for `flavor='poisson_gene_selection'`).
batch_key
If specified, highly-variable genes are selected within each batch separately and merged.
subset
Restrict the result to highly variable genes.
inplace
Write metrics to `adata.var`. If `False`, return the metrics as a DataFrame.

Returns
-------
updates `adata.var` with the following fields:
Returns the metrics if `inplace=False`; otherwise updates `adata.var`
with the following fields and subsets `adata` if requested:

`highly_variable` : bool
boolean indicator of highly-variable genes
Expand Down Expand Up @@ -148,6 +158,17 @@ def highly_variable_genes(
"""
flavor = resolve_default(flavor)

if not inplace:
adata = AnnData(
X=adata.X,
obs=adata.obs.copy(),
var=adata.var[[]].copy(),
layers=adata.layers,
)
X = _get_obs_rep(adata, layer=layer)
if issparse(X) and not X.has_canonical_format:
_set_obs_rep(adata, X.copy(), layer=layer)

if batch_key is not None:
_sanitize_column(adata, batch_key)

Expand Down Expand Up @@ -221,3 +242,9 @@ def highly_variable_genes(
adata.var["highly_variable_intersection"] = df[
"highly_variable_intersection"
]

if not inplace:
return adata.var.loc[adata.var["highly_variable"]] if subset else adata.var
if subset:
adata._inplace_subset_var(adata.var["highly_variable"])
return None
Loading
Loading