Skip to content
This repository was archived by the owner on Aug 27, 2025. It is now read-only.

Commit 21e64c6

Browse files
add spot technologies support
1 parent fa925ee commit 21e64c6

5 files changed

Lines changed: 72 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## [0.1.x] - tbd
2+
3+
### Added
4+
- Support spot-based technologies like Visium
5+
- Faster table conversion (using native CSR matrix arguments)
6+
17
## [0.1.4] - 2024-01-11
28

39
### Fix

spatialdata_xenium_explorer/_constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ class Versions:
2424
CELL_CATEGORIES = [1, 0]
2525

2626

27+
class ShapesConstants:
28+
RADIUS = "radius"
29+
DEFAULT_POINT_RADIUS = 100
30+
31+
2732
def cell_categories_attrs() -> dict:
2833
return {
2934
"major_version": Versions.CELL_CATEGORIES[0],

spatialdata_xenium_explorer/converter.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
from spatialdata import SpatialData
99

1010
from . import (
11+
utils,
1112
write_cell_categories,
1213
write_gene_counts,
1314
write_image,
1415
write_polygons,
1516
write_transcripts,
1617
)
1718
from ._constants import FileNames, experiment_dict
18-
from .utils import explorer_file_path, get_element, get_spatial_image, to_intrinsic
1919

2020
log = logging.getLogger(__name__)
2121

@@ -28,6 +28,7 @@ def write(
2828
points_key: str | None = None,
2929
gene_column: str | None = None,
3030
pixelsize: float = 0.2125,
31+
spot: bool = False,
3132
layer: str | None = None,
3233
polygon_max_vertices: int = 13,
3334
lazy: bool = True,
@@ -64,6 +65,7 @@ def write(
6465
points_key: Name of the transcripts (key of `sdata.points`). This argument doesn't need to be provided if there is only one points key.
6566
gene_column: Column name of the points dataframe containing the gene names.
6667
pixelsize: Number of microns in a pixel. Invalid value can lead to inconsistent scales in the Explorer.
68+
spot: Whether the technology is based on spots
6769
layer: Layer of `sdata.table` where the gene counts are saved. If `None`, uses `sdata.table.X`.
6870
polygon_max_vertices: Maximum number of vertices for the cell polygons. A higher value will display smoother cells.
6971
lazy: If `True`, will not load the full images in memory (except if the image memory is below `ram_threshold_gb`).
@@ -73,7 +75,7 @@ def write(
7375
path: Path = Path(path)
7476
_check_explorer_directory(path)
7577

76-
image_key, image = get_spatial_image(sdata, image_key, return_key=True)
78+
image_key, image = utils.get_spatial_image(sdata, image_key, return_key=True)
7779

7880
### Saving cell categories and gene counts
7981
if sdata.table is not None:
@@ -94,22 +96,27 @@ def write(
9496
write_cell_categories(path, adata)
9597

9698
### Saving cell boundaries
97-
shapes_key, geo_df = get_element(sdata, "shapes", shapes_key, return_key=True)
99+
shapes_key, geo_df = utils.get_element(sdata, "shapes", shapes_key, return_key=True)
98100

99101
if _should_save(mode, "b") and geo_df is not None:
100-
geo_df = to_intrinsic(sdata, geo_df, image_key)
102+
geo_df = utils.to_intrinsic(sdata, geo_df, image_key)
101103

102104
if sdata.table is not None:
103105
geo_df = geo_df.loc[adata.obs[adata.uns["spatialdata_attrs"]["instance_key"]]]
104106

107+
geo_df = utils._standardize_shapes(geo_df)
108+
105109
write_polygons(path, geo_df.geometry, polygon_max_vertices, pixelsize=pixelsize)
106110

107111
### Saving transcripts
108-
df = get_element(sdata, "points", points_key)
112+
if spot and sdata.table is not None:
113+
df, gene_column = utils._spot_transcripts_origin(adata)
114+
else:
115+
df = utils.get_element(sdata, "points", points_key)
116+
df = utils.to_intrinsic(sdata, df, image_key)
109117

110118
if _should_save(mode, "t") and df is not None:
111119
if gene_column is not None:
112-
df = to_intrinsic(sdata, df, image_key)
113120
write_transcripts(path, df, gene_column, pixelsize=pixelsize)
114121
else:
115122
log.warn("The argument 'gene_column' has to be provided to save the transcripts")
@@ -172,7 +179,7 @@ def write_metadata(
172179
is_dir: If `False`, then `path` is a path to a single file, not to the Xenium Explorer directory.
173180
pixelsize: Number of microns in a pixel. Invalid value can lead to inconsistent scales in the Explorer.
174181
"""
175-
path = explorer_file_path(path, FileNames.METADATA, is_dir)
182+
path = utils.explorer_file_path(path, FileNames.METADATA, is_dir)
176183

177184
with open(path, "w") as f:
178185
metadata = experiment_dict(image_key, shapes_key, n_obs, pixelsize)

spatialdata_xenium_explorer/core/points.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def write_transcripts(
112112
GRIDS_ATTRS["grid_number_objects"].append([])
113113
GRIDS_ATTRS["grid_keys"].append([])
114114

115-
n_tiles_x, n_tiles_y = ceil(xmax / tile_size), ceil(ymax / tile_size)
115+
n_tiles_x, n_tiles_y = max(1, ceil(xmax / tile_size)), max(1, ceil(ymax / tile_size))
116116

117117
for tx in range(n_tiles_x):
118118
for ty in range(n_tiles_y):
@@ -179,7 +179,7 @@ def write_transcripts(
179179
chunks=chunks,
180180
)
181181

182-
if n_tiles_x * n_tiles_y == 1:
182+
if n_tiles_x * n_tiles_y == 1 and level > 0:
183183
GRIDS_ATTRS["number_levels"] = level + 1
184184
break
185185

spatialdata_xenium_explorer/utils.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@
44
from pathlib import Path
55

66
import dask.array as da
7+
import dask.dataframe as dd
78
import dask_image.ndinterp
9+
import geopandas as gpd
810
import numpy as np
11+
import pandas as pd
912
import xarray as xr
13+
from anndata import AnnData
1014
from multiscale_spatial_image import MultiscaleSpatialImage
15+
from shapely.geometry import MultiPolygon, Point, Polygon
1116
from spatial_image import SpatialImage
1217
from spatialdata import SpatialData
1318
from spatialdata.models import SpatialElement
1419
from spatialdata.transformations import Identity, get_transformation, set_transformation
1520

21+
from ._constants import ShapesConstants
22+
1623
log = logging.getLogger(__name__)
1724

1825

@@ -213,3 +220,41 @@ def scale_dtype(arr: np.ndarray, dtype: np.dtype) -> np.ndarray:
213220

214221
factor = np.iinfo(dtype).max / np.iinfo(arr.dtype).max
215222
return (arr * factor).astype(dtype)
223+
224+
225+
def _standardize_shapes(geo_df: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
226+
if geo_df.geometry.map(lambda geom: isinstance(geom, Polygon)).all():
227+
return geo_df
228+
229+
if geo_df.geometry.map(lambda geom: isinstance(geom, Point)).all():
230+
if not ShapesConstants.RADIUS in geo_df:
231+
log.warn(
232+
f"GeoDataFrame contains only Point objects, but no column '{ShapesConstants.RADIUS}' was found. Using default {ShapesConstants.RADIUS}={ShapesConstants.DEFAULT_POINT_RADIUS}"
233+
)
234+
geo_df[ShapesConstants.RADIUS] = ShapesConstants.DEFAULT_POINT_RADIUS
235+
236+
geo_df.geometry = geo_df.apply(lambda row: row.geometry.buffer(row.radius), axis=1)
237+
return geo_df
238+
239+
if geo_df.geometry.map(lambda geom: isinstance(geom, MultiPolygon)).all():
240+
log.warn(
241+
"GeoDataFrame contains only MultiPolygon objects. For each MultiPolygon, only the Polygon with the largest area will be shown"
242+
)
243+
244+
geo_df.geometry = geo_df.geometry.map(
245+
lambda multi_polygon: max(multi_polygon.geoms, key=lambda geom: geom.area)
246+
)
247+
return geo_df
248+
249+
raise ValueError(
250+
"The provided shapes contain unsupported types, or contain a mix of multiple types. Supported types: Polygon, Point, MultiPolygon."
251+
)
252+
253+
254+
def _spot_transcripts_origin(adata: AnnData) -> tuple[dd.DataFrame, str]:
255+
gene_column = "gene"
256+
df = pd.DataFrame(
257+
{"x": [0] * adata.n_vars, "y": [0] * adata.n_vars, gene_column: adata.var_names}
258+
)
259+
df = dd.from_pandas(df, chunksize=10_000)
260+
return df, gene_column

0 commit comments

Comments
 (0)