From f8ccbd157666cd7c6bc4658fe970528f3498d15f Mon Sep 17 00:00:00 2001 From: keviny2 Date: Wed, 7 May 2025 14:33:13 -0700 Subject: [PATCH 1/3] Implement to_anndata() --- setup.cfg | 3 +- src/spatialexperiment/__init__.py | 1 - src/spatialexperiment/spatialexperiment.py | 48 +++++++++++++++++++++- src/spatialexperiment/spatialimage.py | 4 +- tests/test_to_anndata.py | 37 +++++++++++++++++ 5 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 tests/test_to_anndata.py diff --git a/setup.cfg b/setup.cfg index e0e090f..8fbcedc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,7 +69,8 @@ exclude = # PDF = ReportLab; RXP optional = pandas - requests + anndata + delayedarray # Add here test requirements (semicolon/line-separated) testing = diff --git a/src/spatialexperiment/__init__.py b/src/spatialexperiment/__init__.py index de0c505..8fd41e7 100644 --- a/src/spatialexperiment/__init__.py +++ b/src/spatialexperiment/__init__.py @@ -26,7 +26,6 @@ ) __all__ = [ - "ProxySpatialFeatureExperiment", "read_tenx_visium", "SpatialExperiment", "LoadedSpatialImage", diff --git a/src/spatialexperiment/spatialexperiment.py b/src/spatialexperiment/spatialexperiment.py index 31f2cf0..c6fa058 100644 --- a/src/spatialexperiment/spatialexperiment.py +++ b/src/spatialexperiment/spatialexperiment.py @@ -29,7 +29,7 @@ _validate_spatial_coords, _validate_spatial_coords_names, ) -from .spatialimage import VirtualSpatialImage, construct_spatial_image_class +from .spatialimage import VirtualSpatialImage, StoredSpatialImage, RemoteSpatialImage, LoadedSpatialImage, construct_spatial_image_class __author__ = "keviny2" __copyright__ = "keviny2" @@ -1009,6 +1009,52 @@ def mirror_img(self, sample_id=None, image_id=None, axis=("h", "v")): def to_spatial_experiment(): raise NotImplementedError() + ################################ + ######>> AnnData interop <<##### + ################################ + + def to_anndata(self, include_alternative_experiments: bool = False) -> "anndata.AnnData": + """Transform :py:class:`~SpatialExperiment`-like into a :py:class:`~anndata.AnnData` representation. + + Args: + include_alternative_experiments: + Whether to transform alternative experiments. + + Returns: + An ``AnnData`` representation of the experiment. + """ + obj, alt_exps = super().to_anndata(include_alternative_experiments=include_alternative_experiments) + + if 'spatial' in obj.uns: + raise ValueError("'spatial' key already exists in the metadata. Rename to something else.") + + obj.uns['spatial'] = {} + for _, row in self.img_data: + library_id = row['sample_id'] + '-' + row['image_id'] + obj.uns['spatial'][library_id] = {} + + spi = row['data'] + if isinstance(spi, LoadedSpatialImage): + img = spi.image + elif isinstance(spi, (StoredSpatialImage, RemoteSpatialImage)): + img = spi.img_source() + + obj.uns['spatial'][library_id]['images'] = { + 'hires': img + } # default to `hires` for now + + obj.uns['spatial'][library_id]['scalefactors'] = { + 'tissue_hires_scalef': row['scale_factor'] + } # default to `tissue_hires_scalef` for now + + obj.obsm['spatial'] = np.column_stack( + [self.spatial_coordinates[axis] + for axis in self.spatial_coords_names] + ) + + return obj, alt_exps + + ################################ #######>> combine ops <<######## ################################ diff --git a/src/spatialexperiment/spatialimage.py b/src/spatialexperiment/spatialimage.py index 852af7d..1e1900c 100644 --- a/src/spatialexperiment/spatialimage.py +++ b/src/spatialexperiment/spatialimage.py @@ -257,8 +257,8 @@ def __str__(self) -> str: ######>> img props <<####### ############################ - def get_image(self) -> Image.Image: - """Get the image as a PIL Image object.""" + def get_image(self) -> Union[Image.Image, np.ndarray]: + """Get the image as a PIL Image object or ndarray.""" return self._image diff --git a/tests/test_to_anndata.py b/tests/test_to_anndata.py new file mode 100644 index 0000000..583ac0d --- /dev/null +++ b/tests/test_to_anndata.py @@ -0,0 +1,37 @@ +from copy import deepcopy +from pathlib import Path +import pytest + +__author__ = "keviny2" +__copyright__ = "keviny2" +__license__ = "MIT" + + +def test_to_anndata(spe): + obj, alt_exps = spe.to_anndata() + + assert obj.shape == (500, 200) + + # check that uns has the correct components + assert 'spatial' in obj.uns + assert len(obj.uns['spatial']) == 3 + + library_id = list(obj.uns['spatial'])[0] + assert 'images' in obj.uns['spatial'][library_id] + assert 'scalefactors' in obj.uns['spatial'][library_id] + + assert 'hires' in obj.uns['spatial'][library_id]['images'] + assert isinstance(obj.uns['spatial'][library_id]['images']['hires'], Path) + + # check that obsm has the correct components + assert 'spatial' in obj.obsm + assert obj.obsm['spatial'].shape == (500, 2) + + +def test_to_anndata_spatial_key_exists(spe): + tspe = deepcopy(spe) + + tspe.metadata['spatial'] = "123" + + with pytest.raises(ValueError): + tspe.to_anndata() \ No newline at end of file From f74f8765853c90f4350e6c4ae9ea48d0a6fd2942 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 7 May 2025 21:34:24 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/spatialexperiment/spatialexperiment.py | 36 +++++++++++----------- tests/test_to_anndata.py | 4 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/spatialexperiment/spatialexperiment.py b/src/spatialexperiment/spatialexperiment.py index c6fa058..b94b942 100644 --- a/src/spatialexperiment/spatialexperiment.py +++ b/src/spatialexperiment/spatialexperiment.py @@ -29,7 +29,13 @@ _validate_spatial_coords, _validate_spatial_coords_names, ) -from .spatialimage import VirtualSpatialImage, StoredSpatialImage, RemoteSpatialImage, LoadedSpatialImage, construct_spatial_image_class +from .spatialimage import ( + VirtualSpatialImage, + StoredSpatialImage, + RemoteSpatialImage, + LoadedSpatialImage, + construct_spatial_image_class, +) __author__ = "keviny2" __copyright__ = "keviny2" @@ -1025,36 +1031,30 @@ def to_anndata(self, include_alternative_experiments: bool = False) -> "anndata. """ obj, alt_exps = super().to_anndata(include_alternative_experiments=include_alternative_experiments) - if 'spatial' in obj.uns: + if "spatial" in obj.uns: raise ValueError("'spatial' key already exists in the metadata. Rename to something else.") - obj.uns['spatial'] = {} + obj.uns["spatial"] = {} for _, row in self.img_data: - library_id = row['sample_id'] + '-' + row['image_id'] - obj.uns['spatial'][library_id] = {} + library_id = row["sample_id"] + "-" + row["image_id"] + obj.uns["spatial"][library_id] = {} - spi = row['data'] + spi = row["data"] if isinstance(spi, LoadedSpatialImage): - img = spi.image + img = spi.image elif isinstance(spi, (StoredSpatialImage, RemoteSpatialImage)): img = spi.img_source() - - obj.uns['spatial'][library_id]['images'] = { - 'hires': img - } # default to `hires` for now - obj.uns['spatial'][library_id]['scalefactors'] = { - 'tissue_hires_scalef': row['scale_factor'] + obj.uns["spatial"][library_id]["images"] = {"hires": img} # default to `hires` for now + + obj.uns["spatial"][library_id]["scalefactors"] = { + "tissue_hires_scalef": row["scale_factor"] } # default to `tissue_hires_scalef` for now - obj.obsm['spatial'] = np.column_stack( - [self.spatial_coordinates[axis] - for axis in self.spatial_coords_names] - ) + obj.obsm["spatial"] = np.column_stack([self.spatial_coordinates[axis] for axis in self.spatial_coords_names]) return obj, alt_exps - ################################ #######>> combine ops <<######## ################################ diff --git a/tests/test_to_anndata.py b/tests/test_to_anndata.py index 583ac0d..258f6a5 100644 --- a/tests/test_to_anndata.py +++ b/tests/test_to_anndata.py @@ -11,7 +11,7 @@ def test_to_anndata(spe): obj, alt_exps = spe.to_anndata() assert obj.shape == (500, 200) - + # check that uns has the correct components assert 'spatial' in obj.uns assert len(obj.uns['spatial']) == 3 @@ -34,4 +34,4 @@ def test_to_anndata_spatial_key_exists(spe): tspe.metadata['spatial'] = "123" with pytest.raises(ValueError): - tspe.to_anndata() \ No newline at end of file + tspe.to_anndata() From 4f64f4d957ff46f5c37743ef4ba8d57449c502a4 Mon Sep 17 00:00:00 2001 From: keviny2 Date: Wed, 7 May 2025 16:45:15 -0700 Subject: [PATCH 3/3] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b1d68..fd84ad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Version 0.0.9 +- Added `to_anndata()` in main `SpatialExperiment` class (PR #50) + +## Version 0.0.8 +- Set the expected column names for image data slot (PR #46) + ## Version 0.0.7 - Added `img_source` function in main SpatialExperiment class and child classes of VirtualSpatialExperiment (PR #36) - Added `remove_img` function (PR #34)