Skip to content

Commit d21e894

Browse files
authored
Merge pull request #16 from HiDiHlabs/dev
API updates and multimodal support
2 parents 4bb6c62 + 53d80b6 commit d21e894

File tree

12 files changed

+372
-143
lines changed

12 files changed

+372
-143
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
*.egg-info
2+
build
3+
__pycache__
4+
5+
# Sphinx
6+
generated
7+
jupyter_execute
8+
9+
*.ipynb

.pre-commit-config.yaml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,13 @@ repos:
1818
rev: v0.12.2
1919
hooks:
2020
- id: ruff
21-
args: [--fix]
22-
- repo: https://github.com/PyCQA/isort
23-
rev: 6.0.1
24-
hooks:
25-
- id: isort
26-
- repo: https://github.com/psf/black
27-
rev: 25.1.0
28-
hooks:
29-
- id: black
21+
- id: ruff-format
3022
- repo: https://github.com/pre-commit/mirrors-mypy
3123
rev: v1.16.1
3224
hooks:
3325
- id: mypy
26+
additional_dependencies:
27+
- numpy
3428
- repo: https://github.com/codespell-project/codespell
3529
rev: v2.4.1
3630
hooks:

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
# SpatialLeiden
22

33
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4-
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
54
[![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
6-
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
75
[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
86
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
7+
[![Docs](https://app.readthedocs.org/projects/spatialleiden/badge/?version=latest)](https://spatialleiden.readthedocs.io)
8+
[![PyPI](https://img.shields.io/pypi/v/spatialleiden)](https://pypi.org/project/spatialleiden)
9+
[![install with bioconda](https://img.shields.io/badge/install%20with-bioconda-brightgreen.svg?style=flat)](http://bioconda.github.io/recipes/spatialleiden/README.html)
10+
911

1012
``SpatialLeiden`` is an implementation of
1113
[Multiplex Leiden clustering](https://leidenalg.readthedocs.io/en/stable/multiplex.html)
1214
that can be used to cluster spatially resolved omics data.
1315

1416
``SpatialLeiden`` integrates with the [scverse](https://scverse.org/) by leveraging
15-
[scanpy](https://scanpy.readthedocs.io/) and [anndata](https://anndata.readthedocs.io/)
16-
but can also be used independently.
17+
[anndata](https://anndata.readthedocs.io/) but can also be used independently.
1718

1819
## Installation
1920

docs/source/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ Multiplex Leiden
1010
:nosignatures:
1111
:toctree: ./generated/
1212

13+
leiden
1314
spatialleiden
15+
spatialleiden_multimodal
1416
multiplex_leiden
1517

1618

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@
5050
intersphinx_mapping = dict(
5151
anndata=("https://anndata.readthedocs.io/en/stable/", None),
5252
matplotlib=("https://matplotlib.org/stable/", None),
53+
mudata=("https://mudata.readthedocs.io/en/stable/", None),
5354
numpy=("https://numpy.org/doc/stable/", None),
5455
python=("https://docs.python.org/3", None),
55-
scanpy=("https://scanpy.readthedocs.io/en/stable/", None),
5656
scipy=("https://docs.scipy.org/doc/scipy/", None),
5757
squidpy=("https://squidpy.readthedocs.io/en/stable/", None),
5858
)

docs/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SpatialLeiden is an implementation of
66
that can be used to cluster spatially resolved omics data.
77

88
SpatialLeiden integrates with the `scverse <https://scverse.org/>`_ by leveraging
9-
`scanpy <https://scanpy.readthedocs.io/>`_ and `anndata <https://anndata.readthedocs.io/>`_
9+
`anndata <https://anndata.readthedocs.io/>`_
1010
but can also be used independently.
1111

1212
Citations

docs/source/usage.md

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
file_format: mystnb
33
kernelspec:
44
name: python
5+
display_name: python
56
jupytext:
67
text_representation:
78
extension: .md
@@ -29,6 +30,13 @@ import anndata as ad
2930
with NamedTemporaryFile(suffix=".h5ad") as h5ad_file:
3031
urlretrieve("https://figshare.com/ndownloader/files/40038538", h5ad_file.name)
3132
adata = ad.read_h5ad(h5ad_file)
33+
34+
35+
# This is not recommended! Suppressing the warnings is only done because the code is run
36+
# when building the docs and would clutter the webpage
37+
import warnings
38+
39+
warnings.filterwarnings("ignore")
3240
```
3341

3442
First of all we are going to load the relevant packages that we will be working with as well as setting a random seed that we will use throughout this example to make the results reproducible.
@@ -38,10 +46,10 @@ import scanpy as sc
3846
import spatialleiden as sl
3947
import squidpy as sq
4048
41-
seed = 42
49+
random_state = 42
4250
```
4351

44-
The data set consists of 155 genes and ~5,500 cells including their annotation for cell type as well as domains.
52+
The data set consists of 155 genes and ~5,500 cells including their annotation for the cell type as well as domains.
4553

4654
+++
4755

@@ -51,14 +59,36 @@ We will do some standard preprocessing by log-transforming the data and then usi
5159

5260
```{code-cell} ipython3
5361
sc.pp.log1p(adata)
54-
sc.pp.pca(adata, random_state=seed)
62+
sc.pp.pca(adata, random_state=random_state)
5563
56-
sc.pp.neighbors(adata, random_state=seed)
64+
sc.pp.neighbors(adata, random_state=random_state)
5765
```
5866

59-
For SpatialLeiden we need an additional graph representing the connectivities in the topological space. Here we will use a kNN graph with 10 neighbors that we generate with {py:func}`squidpy.gr.spatial_neighbors`. Alternatives are Delaunay triangulation or regular grids in case of e.g. Visium data.
67+
### Building spatial neighbor graphs
68+
69+
For SpatialLeiden we need an additional graph representing the neighbors in space i.e.
70+
which cells are close/next to each other.
71+
72+
What kind of spatial neighbor graph is suitable for the analysis is dependent on the
73+
technology used to generate the data. Most of the neighborhood structures interesting
74+
for our use cases can be calculated using {py:func}`squidpy.gr.spatial_neighbors`.
6075

61-
We can use the calculated distances between neighboring points and transform them into connectivities using the {py:func}`spatialleiden.distance2connectivity` function.
76+
Generally, if the data is generated from a method with a regular lattice it is advisible
77+
to use this for the analysis;
78+
* isometric grid (hexagonal): for Visium with `squidpy.gr.spatial_neighbors(adata, coord_type="grid", n_neighs=6)`
79+
* square grid: for binned Stereo-seq and VisiumHD with `squidpy.gr.spatial_neighbors(adata, coord_type="grid", n_neighs=4)` (using 8 neighbors is also possible)
80+
81+
If your data does not originate from a regular lattice, there are various options to build your neighborhood graph.
82+
This applies to all imaging-based methodologies that are usually analysed after segmenting cells, but also technolgoies with regular lattices if you use cell segmentation (such as Stereo-seq or VisiumHD).
83+
* kNN: calculating the *k*-nearest neighbors per cell with `squidpy.gr.spatial_neighbors(adata, coord_type="generic", n_neighs=k)`
84+
* Delaunay triangulation: `squidpy.gr.spatial_neighbors(adata, coord_type="generic", delaunay=True)`
85+
* radius-based: with a threshold of *r* units `squidpy.gr.spatial_neighbors(adata, coord_type="generic", radius=r)`
86+
* other methods such as Gabriel graphs, ...
87+
88+
For the neighborhoods that are not based on regular grids we can, furthermore, scale the weight of each edge bsaed on the distance between the two cells (that's why it is not useful for the regular grid case as the neighbors will be equidistant).
89+
This can be achieved by calculating connectivities based on the distances using the {py:func}`spatialleiden.distance2connectivity` function.
90+
91+
Here, we will use a kNN graph with 10 neighbors.
6292

6393
```{code-cell} ipython3
6494
sq.gr.spatial_neighbors(adata, coord_type="generic", n_neighs=10)
@@ -68,16 +98,20 @@ adata.obsp["spatial_connectivities"] = sl.distance2connectivity(
6898
)
6999
```
70100

101+
### Finding clusters
102+
71103
Now, we can already run {py:func}`spatialleiden.spatialleiden` (which we will also compare to normal Leiden clustering).
72104

73-
The `layer_ratio` determines the weighting between the gene expression and the topological layer and is influenced by the graph structures (i.e. how many connections exist, the edge weights, etc.); the lower the value is the closer SpatialLeiden will be to normal Leiden clustering, while higher values lead to more spatially homogeneous clusters.
105+
The `layer_ratio` determines the weighting between the gene expression and the spatial layer and is influenced by the graph structures (i.e. how many connections exist, the edge weights, etc.); the lower the value is the closer SpatialLeiden will be to normal Leiden clustering, while higher values lead to more spatially homogeneous clusters.
74106

75107
The resolution has the same effect as in Leiden clustering (higher resolution will lead to more clusters) and can be defined for each of the layers (but for now is left at its default value).
76108

77109
```{code-cell} ipython3
78-
sc.tl.leiden(adata, directed=False, random_state=seed)
110+
sc.tl.leiden(adata, directed=False, random_state=random_state)
79111
80-
sl.spatialleiden(adata, layer_ratio=1.8, directed=(False, True), seed=seed)
112+
sl.spatialleiden(
113+
adata, layer_ratio=1.8, directed=(False, True), random_state=random_state
114+
)
81115
82116
sc.pl.embedding(adata, basis="spatial", color=["leiden", "spatialleiden"])
83117
```
@@ -98,8 +132,12 @@ n_clusters = adata.obs["domain"].nunique()
98132
latent_resolution, spatial_resolution = sl.search_resolution(
99133
adata,
100134
n_clusters,
101-
latent_kwargs={"seed": seed},
102-
spatial_kwargs={"layer_ratio": 1.8, "seed": seed, "directed": (False, True)},
135+
latent_kwargs={"random_state": random_state},
136+
spatial_kwargs={
137+
"layer_ratio": 1.8,
138+
"random_state": random_state,
139+
"directed": (False, True),
140+
},
103141
)
104142
105143
print(f"Latent resolution: {latent_resolution:.3f}")
@@ -108,6 +146,15 @@ print(f"Spatial resolution: {spatial_resolution:.3f}")
108146

109147
In our case we can compare the resulting clusters to the annotated ground truth regions. If we are not satisfied with the results, we can go back and tweak other parameters such as the underlying neighborhood graphs or the `layer_ratio` to achieve the desired granularity of our results.
110148

149+
```{code-cell} ipython3
150+
---
151+
tags: [hide-cell]
152+
---
153+
154+
# needed for scanpy v1.11 otherwise plotting fails because the number of clusters changed
155+
del adata.uns["spatialleiden_colors"]
156+
```
157+
111158
```{code-cell} ipython3
112159
sc.pl.embedding(adata, basis="spatial", color=["spatialleiden", "Region"])
113160
```

pyproject.toml

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,24 @@
11
[build-system]
2-
requires = ["setuptools>=61.0.0", "setuptools_scm[toml]>=6.2"]
2+
requires = ["setuptools>=77.0.3", "setuptools_scm>=8"]
33
build-backend = "setuptools.build_meta"
44

55

66
[project]
77
name = "spatialleiden"
8-
description = "Implementation of multiplex Leiden for analysis of spatial omics data."
8+
description = "Implementation of multiplex Leiden for analysis of (multimodal) spatial omics data."
99
readme = { file = "README.md", content-type = "text/markdown" }
10-
license = { file = "LICENSE" }
10+
license = "MIT"
11+
license-files = ["LICENSE"]
1112
requires-python = ">=3.10"
1213
dynamic = ["version"]
1314

1415
authors = [
1516
{ name = "Niklas Müller-Bötticher", email = "[email protected]" },
1617
{ name = "Shashwat Sahay", email = "[email protected]" },
1718
]
18-
dependencies = [
19-
"anndata",
20-
"igraph",
21-
"leidenalg~=0.10.2",
22-
"numpy>=1.21",
23-
"scanpy",
24-
"scipy>=1.9",
25-
]
19+
dependencies = ["igraph", "leidenalg~=0.10.2", "numpy>=1.21", "scipy>=1.9"]
2620
classifiers = [
2721
"Intended Audience :: Science/Research",
28-
"License :: OSI Approved :: MIT License",
2922
"Programming Language :: Python",
3023
"Programming Language :: Python :: 3",
3124
"Programming Language :: Python :: 3 :: Only",
@@ -35,8 +28,17 @@ classifiers = [
3528
]
3629

3730
[project.optional-dependencies]
38-
docs = ["sphinx", "sphinx-copybutton", "sphinx-rtd-theme", "squidpy", "myst-nb"]
39-
dev = ["spatialleiden[docs]", "pre-commit"]
31+
docs = [
32+
"sphinx",
33+
"sphinx-copybutton",
34+
"sphinx-rtd-theme",
35+
"anndata>=0.10",
36+
"scanpy",
37+
"squidpy",
38+
"dask<2025", # incompatibility of squidpy with dask >= 2025
39+
"myst-nb",
40+
]
41+
dev = ["spatialleiden[docs]", "mudata~=0.3", "pre-commit"]
4042

4143
[project.urls]
4244
Homepage = "https://github.com/HiDiHlabs/SpatialLeiden"
@@ -53,20 +55,21 @@ include = ["spatialleiden"]
5355
[tool.setuptools_scm]
5456

5557

56-
[tool.isort]
57-
profile = "black"
58-
59-
[tool.black]
60-
target-version = ["py310", "py311", "py312", "py313"]
61-
6258
[tool.ruff]
6359
target-version = "py310"
6460

61+
fix = true
62+
show-fixes = true
63+
64+
[tool.ruff.lint]
65+
extend-select = ["I"]
66+
6567
[tool.mypy]
6668
python_version = "3.10"
6769
ignore_missing_imports = true
6870
warn_no_return = false
6971
packages = "spatialleiden"
72+
plugins = "numpy.typing.mypy_plugin"
7073

7174
[tool.codespell]
7275
ignore-words-list = "coo"

spatialleiden/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
from importlib.metadata import PackageNotFoundError, version
22

3-
from ._multiplex_leiden import multiplex_leiden, spatialleiden
3+
from ._multiplex_leiden import (
4+
leiden,
5+
multiplex_leiden,
6+
spatialleiden,
7+
spatialleiden_multimodal,
8+
)
49
from ._resolution_search import (
510
search_resolution,
611
search_resolution_latent,
@@ -17,8 +22,10 @@
1722

1823

1924
__all__ = [
25+
"leiden",
2026
"multiplex_leiden",
2127
"spatialleiden",
28+
"spatialleiden_multimodal",
2229
"search_resolution",
2330
"search_resolution_latent",
2431
"search_resolution_spatial",

0 commit comments

Comments
 (0)