Skip to content

Remove image(s) from SPE #34

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 1, 2025
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ jobs:

# This uses the trusted publisher workflow so no token is required.
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@release/v1
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## [Unreleased]
- Added `remove_img` function (PR #34)
- Refactored `get_img_idx` for improved maintainability
- Disambiguated `get_img_data` between `_imgutils.py` and `SpatialExperiment.py`
- Moved `SpatialFeatureExperiment` into its own package
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,4 @@ For more detailed information about available methods and functionality, please
## Note

This project has been set up using [BiocSetup](https://github.com/biocpy/biocsetup)
and [PyScaffold](https://pyscaffold.org/).
and [PyScaffold](https://pyscaffold.org/).
4 changes: 2 additions & 2 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
furo
myst-nb
# Requirements file for ReadTheDocs, check .readthedocs.yml.
# To build the module reference correctly, make sure every external package
# under `install_requires` in `setup.cfg` is also listed here!
# sphinx_rtd_theme
myst-parser[linkify]
sphinx>=3.2.1
myst-nb
furo
sphinx-autodoc-typehints
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ python_requires = >=3.9
# For more information, check out https://semver.org/.
install_requires =
importlib-metadata; python_version<"3.8"
biocframe>=0.6.1
biocframe>=0.6.3
biocutils>=0.2
summarizedexperiment>=0.5
singlecellexperiment>=0.5.7
Expand Down
10 changes: 5 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
Setup file for SpatialExperiment.
Use setup.cfg to configure your project.
Setup file for SpatialExperiment.
Use setup.cfg to configure your project.

This file was generated with PyScaffold 4.6.
PyScaffold helps you to put up the scaffold of your new Python project.
Learn more under: https://pyscaffold.org/
This file was generated with PyScaffold 4.6.
PyScaffold helps you to put up the scaffold of your new Python project.
Learn more under: https://pyscaffold.org/
"""

from setuptools import setup
Expand Down
113 changes: 40 additions & 73 deletions src/spatialexperiment/SpatialExperiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,12 @@
check_assays_are_equal,
merge_assays,
merge_se_colnames,
relaxed_merge_assays
relaxed_merge_assays,
)
from summarizedexperiment._frameutils import _sanitize_frame
from summarizedexperiment.RangedSummarizedExperiment import GRangesOrGRangesList
from singlecellexperiment import SingleCellExperiment
from singlecellexperiment._combineutils import (
merge_generic,
relaxed_merge_generic,
relaxed_merge_numpy_generic
)
from singlecellexperiment._combineutils import merge_generic, relaxed_merge_generic, relaxed_merge_numpy_generic

from ._imgutils import get_img_idx
from ._validators import (
Expand Down Expand Up @@ -202,9 +198,7 @@ def __init__(
column_data = _sanitize_frame(column_data, num_rows=self.shape[1])

if not column_data.has_column("sample_id"):
column_data["sample_id"] = ["sample01"] * self.shape[
1
] # hard code default sample_id as "sample01"
column_data["sample_id"] = ["sample01"] * self.shape[1] # hard code default sample_id as "sample01"

spatial_coords = _sanitize_frame(spatial_coords, num_rows=self.shape[1])
img_data = _sanitize_frame(img_data, num_rows=0)
Expand All @@ -217,9 +211,7 @@ def __init__(
_validate_column_data(column_data=column_data)
_validate_img_data(img_data=img_data)
_validate_sample_ids(column_data=column_data, img_data=img_data)
_validate_spatial_coords(
spatial_coords=spatial_coords, column_data=column_data
)
_validate_spatial_coords(spatial_coords=spatial_coords, column_data=column_data)

#########################
######>> Copying <<######
Expand Down Expand Up @@ -323,14 +315,10 @@ def __repr__(self) -> str:
output += ", row_ranges=" + self._row_ranges.__repr__()

if self._alternative_experiments is not None:
output += ", alternative_experiments=" + ut.print_truncated_list(
self.alternative_experiment_names
)
output += ", alternative_experiments=" + ut.print_truncated_list(self.alternative_experiment_names)

if self._reduced_dims is not None:
output += ", reduced_dims=" + ut.print_truncated_list(
self.reduced_dim_names
)
output += ", reduced_dims=" + ut.print_truncated_list(self.reduced_dim_names)

if self._main_experiment_name is not None:
output += ", main_experiment_name=" + self._main_experiment_name
Expand Down Expand Up @@ -358,10 +346,14 @@ def __str__(self) -> str:

output += f"assays({len(self.assay_names)}): {ut.print_truncated_list(self.assay_names)}\n"

output += f"row_data columns({len(self._rows.column_names)}): {ut.print_truncated_list(self._rows.column_names)}\n"
output += (
f"row_data columns({len(self._rows.column_names)}): {ut.print_truncated_list(self._rows.column_names)}\n"
)
output += f"row_names({0 if self._row_names is None else len(self._row_names)}): {' ' if self._row_names is None else ut.print_truncated_list(self._row_names)}\n"

output += f"column_data columns({len(self._cols.column_names)}): {ut.print_truncated_list(self._cols.column_names)}\n"
output += (
f"column_data columns({len(self._cols.column_names)}): {ut.print_truncated_list(self._cols.column_names)}\n"
)
output += f"column_names({0 if self._column_names is None else len(self._column_names)}): {' ' if self._column_names is None else ut.print_truncated_list(self._column_names)}\n"

output += f"main_experiment_name: {' ' if self._main_experiment_name is None else self._main_experiment_name}\n"
Expand Down Expand Up @@ -434,9 +426,7 @@ def set_spatial_coords(
in_place: bool = False,
) -> "SpatialExperiment":
"""Alias for :py:meth:`~set_spatial_coordinates`."""
return self.set_spatial_coordinates(
spatial_coords=spatial_coords, in_place=in_place
)
return self.set_spatial_coordinates(spatial_coords=spatial_coords, in_place=in_place)

@property
def spatial_coords(self) -> BiocFrame:
Expand All @@ -458,9 +448,7 @@ def spatial_coordinates(self) -> BiocFrame:
return self.get_spatial_coordinates()

@spatial_coordinates.setter
def spatial_coordinates(
self, spatial_coords: Optional[Union[BiocFrame, np.ndarray]]
):
def spatial_coordinates(self, spatial_coords: Optional[Union[BiocFrame, np.ndarray]]):
"""Alias for :py:meth:`~set_spatial_coordinates`."""
warn(
"Setting property 'spatial_coords' is an in-place operation, use 'set_spatial_coordinates' instead.",
Expand Down Expand Up @@ -510,21 +498,15 @@ def set_spatial_coordinates_names(
new_spatial_coords = self._spatial_coords
else:
_validate_spatial_coords_names(spatial_coords_names, self._spatial_coords)
new_spatial_coords = self._spatial_coords.set_column_names(
spatial_coords_names
)
new_spatial_coords = self._spatial_coords.set_column_names(spatial_coords_names)

output = self._define_output(in_place)
output._spatial_coords = new_spatial_coords
return output

def set_spatial_coords_names(
self, spatial_coords_names: List[str], in_place: bool = False
) -> "SpatialExperiment":
def set_spatial_coords_names(self, spatial_coords_names: List[str], in_place: bool = False) -> "SpatialExperiment":
"""Alias for :py:meth:`~set_spatial_coordinates_names`."""
return self.set_spatial_coordinates_names(
spatial_coords_names=spatial_coords_names, in_place=in_place
)
return self.set_spatial_coordinates_names(spatial_coords_names=spatial_coords_names, in_place=in_place)

@property
def spatial_coords_names(self) -> List[str]:
Expand All @@ -538,9 +520,7 @@ def spatial_coords_names(self, spatial_coords_names: List[str]):
"Setting property 'spatial_coords_names' is an in-place operation, use 'set_spatial_coordinates_names' instead.",
UserWarning,
)
self.set_spatial_coordinates_names(
spatial_coords_names=spatial_coords_names, in_place=True
)
self.set_spatial_coordinates_names(spatial_coords_names=spatial_coords_names, in_place=True)

@property
def spatial_coordinates_names(self) -> List[str]:
Expand All @@ -554,9 +534,7 @@ def spatial_coordinates_names(self, spatial_coords_names: List[str]):
"Setting property 'spatial_coords_names' is an in-place operation, use 'set_spatial_coordinates_names' instead.",
UserWarning,
)
self.set_spatial_coordinates_names(
spatial_coords_names=spatial_coords_names, in_place=True
)
self.set_spatial_coordinates_names(spatial_coords_names=spatial_coords_names, in_place=True)

##############################
########>> img_data <<########
Expand All @@ -574,9 +552,7 @@ def get_img_data(self) -> BiocFrame:
"""Alias for :py:meth:`~get_image_data`."""
return self.get_image_data()

def set_image_data(
self, img_data: Optional[BiocFrame], in_place: bool = False
) -> "SpatialExperiment":
def set_image_data(self, img_data: Optional[BiocFrame], in_place: bool = False) -> "SpatialExperiment":
"""Set new image data.

Args:
Expand Down Expand Up @@ -605,9 +581,7 @@ def set_image_data(
output._img_data = img_data
return output

def set_img_data(
self, img_data: BiocFrame, in_place: bool = False
) -> "SpatialExperiment":
def set_img_data(self, img_data: BiocFrame, in_place: bool = False) -> "SpatialExperiment":
"""Alias for :py:meth:`~set_image_data`."""
return self.set_image_data(img_data=img_data, in_place=in_place)

Expand Down Expand Up @@ -669,9 +643,7 @@ def get_scale_factors(
_validate_id(sample_id)
_validate_id(image_id)

idxs = get_img_idx(
img_data=self.img_data, sample_id=sample_id, image_id=image_id
)
idxs = get_img_idx(img_data=self.img_data, sample_id=sample_id, image_id=image_id)

return self.img_data[idxs,]["scale_factor"]

Expand Down Expand Up @@ -734,19 +706,15 @@ def get_slice(
spe = super().get_slice(rows=rows, columns=columns)

slicer = self._generic_slice(rows=rows, columns=columns)
do_slice_cols = not (
isinstance(slicer.col_indices, slice) and slicer.col_indices == slice(None)
)
do_slice_cols = not (isinstance(slicer.col_indices, slice) and slicer.col_indices == slice(None))

new_spatial_coords = None

if do_slice_cols:
new_spatial_coords = self.spatial_coords[slicer.col_indices, :]

column_sample_ids = set(spe.column_data["sample_id"])
mask = [
sample_id in column_sample_ids for sample_id in self.img_data["sample_id"]
]
mask = [sample_id in column_sample_ids for sample_id in self.img_data["sample_id"]]

new_img_data = self.img_data[mask,]

Expand Down Expand Up @@ -822,11 +790,9 @@ def get_img(
if not self.img_data:
return None

idxs = get_img_idx(
img_data=self.img_data, sample_id=sample_id, image_id=image_id
)
indices = get_img_idx(img_data=self.img_data, sample_id=sample_id, image_id=image_id)

images = self.img_data[idxs,]["data"]
images = self.img_data[indices,]["data"]
return images[0] if len(images) == 1 else images

def add_img(
Expand Down Expand Up @@ -869,9 +835,7 @@ def add_img(
Raises:
ValueError: If the sample_id and image_id pair already exists.
"""
_validate_sample_image_ids(
img_data=self._img_data, new_sample_id=sample_id, new_image_id=image_id
)
_validate_sample_image_ids(img_data=self._img_data, new_sample_id=sample_id, new_image_id=image_id)

if isinstance(image_source, (str, Path)):
is_url = urlparse(str(image_source)).scheme in ("http", "https", "ftp")
Expand All @@ -897,12 +861,8 @@ def add_img(
output._img_data = new_img_data
return output

# TODO: implement rmv_img()
def rmv_img(
self,
sample_id: Union[str, bool, None] = None,
image_id: Union[str, bool, None] = None,
in_place: bool = False
def remove_img(
self, sample_id: Union[str, bool, None] = None, image_id: Union[str, bool, None] = None, in_place: bool = False
) -> "SpatialExperiment":
"""Remove an image entry.

Expand All @@ -921,17 +881,24 @@ def rmv_img(
Whether to modify the ``SpatialExperiment`` in place.
Defaults to False.
"""
raise NotImplementedError()
_validate_id(sample_id)
_validate_id(image_id)

indices = get_img_idx(img_data=self.img_data, sample_id=sample_id, image_id=image_id)

new_img_data = self._img_data.remove_rows(indices)

output = self._define_output(in_place=in_place)
output._img_data = new_img_data
return output

def img_source(
self,
sample_id: Union[str, bool, None] = None,
image_id: Union[str, bool, None] = None,
path=False,
):
raise NotImplementedError(
"This function is irrelevant because it is for `RemoteSpatialImages`"
)
raise NotImplementedError("This function is irrelevant because it is for `RemoteSpatialImages`")

def img_raster(self, sample_id=None, image_id=None):
# NOTE: this function seems redundant, might be an artifact of the different subclasses of SpatialImage in the R implementation? just call `get_img()` for now
Expand Down
24 changes: 6 additions & 18 deletions src/spatialexperiment/SpatialImage.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ def get_metadata(self) -> dict:
"""
return self._metadata

def set_metadata(
self, metadata: dict, in_place: bool = False
) -> "VirtualSpatialImage":
def set_metadata(self, metadata: dict, in_place: bool = False) -> "VirtualSpatialImage":
"""Set additional metadata.

Args:
Expand All @@ -67,9 +65,7 @@ def set_metadata(
or as a reference to the (in-place-modified) original.
"""
if not isinstance(metadata, dict):
raise TypeError(
f"`metadata` must be a dictionary, provided {type(metadata)}."
)
raise TypeError(f"`metadata` must be a dictionary, provided {type(metadata)}.")
output = self._define_output(in_place)
output._metadata = metadata
return output
Expand Down Expand Up @@ -150,9 +146,7 @@ def _sanitize_loaded_image(image):
class LoadedSpatialImage(VirtualSpatialImage):
"""Class for images loaded into memory."""

def __init__(
self, image: Union[Image.Image, np.ndarray], metadata: Optional[dict] = None
):
def __init__(self, image: Union[Image.Image, np.ndarray], metadata: Optional[dict] = None):
"""Initialize the object.

Args:
Expand Down Expand Up @@ -256,9 +250,7 @@ def get_image(self) -> Image.Image:

return self._image

def set_image(
self, image: Union[Image.Image, np.ndarray], in_place: bool = False
) -> "LoadedSpatialImage":
def set_image(self, image: Union[Image.Image, np.ndarray], in_place: bool = False) -> "LoadedSpatialImage":
"""Set new image.

Args:
Expand Down Expand Up @@ -410,9 +402,7 @@ def get_path(self) -> Path:
"""Get the path to the image file."""
return self._path

def set_path(
self, path: Union[str, Path], in_place: bool = False
) -> "StoredSpatialImage":
def set_path(self, path: Union[str, Path], in_place: bool = False) -> "StoredSpatialImage":
"""Update the path to the image file.

Args:
Expand Down Expand Up @@ -473,9 +463,7 @@ def _validate_url(url):
class RemoteSpatialImage(VirtualSpatialImage):
"""Class for remotely hosted images."""

def __init__(
self, url: str, metadata: Optional[dict] = None, validate: bool = True
):
def __init__(self, url: str, metadata: Optional[dict] = None, validate: bool = True):
"""Initialize the object.

Args:
Expand Down
1 change: 0 additions & 1 deletion src/spatialexperiment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,3 @@
"VirtualSpatialImage",
"construct_spatial_image_class",
]

Loading