Skip to content

Commit 780f39e

Browse files
authored
Remove image(s) from SPE (#34)
* Implement remove_img()
1 parent 7b50196 commit 780f39e

19 files changed

+236
-221
lines changed

.github/workflows/publish-pypi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@ jobs:
5252
5353
# This uses the trusted publisher workflow so no token is required.
5454
- name: Publish to PyPI
55-
uses: pypa/gh-action-pypi-publish@release/v1
55+
uses: pypa/gh-action-pypi-publish@release/v1

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Changelog
22

33
## [Unreleased]
4+
- Added `remove_img` function (PR #34)
45
- Refactored `get_img_idx` for improved maintainability
56
- Disambiguated `get_img_data` between `_imgutils.py` and `SpatialExperiment.py`
67
- Moved `SpatialFeatureExperiment` into its own package

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,4 @@ For more detailed information about available methods and functionality, please
100100
## Note
101101

102102
This project has been set up using [BiocSetup](https://github.com/biocpy/biocsetup)
103-
and [PyScaffold](https://pyscaffold.org/).
103+
and [PyScaffold](https://pyscaffold.org/).

docs/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
furo
2+
myst-nb
13
# Requirements file for ReadTheDocs, check .readthedocs.yml.
24
# To build the module reference correctly, make sure every external package
35
# under `install_requires` in `setup.cfg` is also listed here!
46
# sphinx_rtd_theme
57
myst-parser[linkify]
68
sphinx>=3.2.1
7-
myst-nb
8-
furo
99
sphinx-autodoc-typehints

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ python_requires = >=3.9
4949
# For more information, check out https://semver.org/.
5050
install_requires =
5151
importlib-metadata; python_version<"3.8"
52-
biocframe>=0.6.1
52+
biocframe>=0.6.3
5353
biocutils>=0.2
5454
summarizedexperiment>=0.5
5555
singlecellexperiment>=0.5.7

setup.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""
2-
Setup file for SpatialExperiment.
3-
Use setup.cfg to configure your project.
2+
Setup file for SpatialExperiment.
3+
Use setup.cfg to configure your project.
44
5-
This file was generated with PyScaffold 4.6.
6-
PyScaffold helps you to put up the scaffold of your new Python project.
7-
Learn more under: https://pyscaffold.org/
5+
This file was generated with PyScaffold 4.6.
6+
PyScaffold helps you to put up the scaffold of your new Python project.
7+
Learn more under: https://pyscaffold.org/
88
"""
99

1010
from setuptools import setup

src/spatialexperiment/SpatialExperiment.py

Lines changed: 40 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,12 @@
1212
check_assays_are_equal,
1313
merge_assays,
1414
merge_se_colnames,
15-
relaxed_merge_assays
15+
relaxed_merge_assays,
1616
)
1717
from summarizedexperiment._frameutils import _sanitize_frame
1818
from summarizedexperiment.RangedSummarizedExperiment import GRangesOrGRangesList
1919
from singlecellexperiment import SingleCellExperiment
20-
from singlecellexperiment._combineutils import (
21-
merge_generic,
22-
relaxed_merge_generic,
23-
relaxed_merge_numpy_generic
24-
)
20+
from singlecellexperiment._combineutils import merge_generic, relaxed_merge_generic, relaxed_merge_numpy_generic
2521

2622
from ._imgutils import get_img_idx
2723
from ._validators import (
@@ -202,9 +198,7 @@ def __init__(
202198
column_data = _sanitize_frame(column_data, num_rows=self.shape[1])
203199

204200
if not column_data.has_column("sample_id"):
205-
column_data["sample_id"] = ["sample01"] * self.shape[
206-
1
207-
] # hard code default sample_id as "sample01"
201+
column_data["sample_id"] = ["sample01"] * self.shape[1] # hard code default sample_id as "sample01"
208202

209203
spatial_coords = _sanitize_frame(spatial_coords, num_rows=self.shape[1])
210204
img_data = _sanitize_frame(img_data, num_rows=0)
@@ -217,9 +211,7 @@ def __init__(
217211
_validate_column_data(column_data=column_data)
218212
_validate_img_data(img_data=img_data)
219213
_validate_sample_ids(column_data=column_data, img_data=img_data)
220-
_validate_spatial_coords(
221-
spatial_coords=spatial_coords, column_data=column_data
222-
)
214+
_validate_spatial_coords(spatial_coords=spatial_coords, column_data=column_data)
223215

224216
#########################
225217
######>> Copying <<######
@@ -323,14 +315,10 @@ def __repr__(self) -> str:
323315
output += ", row_ranges=" + self._row_ranges.__repr__()
324316

325317
if self._alternative_experiments is not None:
326-
output += ", alternative_experiments=" + ut.print_truncated_list(
327-
self.alternative_experiment_names
328-
)
318+
output += ", alternative_experiments=" + ut.print_truncated_list(self.alternative_experiment_names)
329319

330320
if self._reduced_dims is not None:
331-
output += ", reduced_dims=" + ut.print_truncated_list(
332-
self.reduced_dim_names
333-
)
321+
output += ", reduced_dims=" + ut.print_truncated_list(self.reduced_dim_names)
334322

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

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

361-
output += f"row_data columns({len(self._rows.column_names)}): {ut.print_truncated_list(self._rows.column_names)}\n"
349+
output += (
350+
f"row_data columns({len(self._rows.column_names)}): {ut.print_truncated_list(self._rows.column_names)}\n"
351+
)
362352
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"
363353

364-
output += f"column_data columns({len(self._cols.column_names)}): {ut.print_truncated_list(self._cols.column_names)}\n"
354+
output += (
355+
f"column_data columns({len(self._cols.column_names)}): {ut.print_truncated_list(self._cols.column_names)}\n"
356+
)
365357
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"
366358

367359
output += f"main_experiment_name: {' ' if self._main_experiment_name is None else self._main_experiment_name}\n"
@@ -434,9 +426,7 @@ def set_spatial_coords(
434426
in_place: bool = False,
435427
) -> "SpatialExperiment":
436428
"""Alias for :py:meth:`~set_spatial_coordinates`."""
437-
return self.set_spatial_coordinates(
438-
spatial_coords=spatial_coords, in_place=in_place
439-
)
429+
return self.set_spatial_coordinates(spatial_coords=spatial_coords, in_place=in_place)
440430

441431
@property
442432
def spatial_coords(self) -> BiocFrame:
@@ -458,9 +448,7 @@ def spatial_coordinates(self) -> BiocFrame:
458448
return self.get_spatial_coordinates()
459449

460450
@spatial_coordinates.setter
461-
def spatial_coordinates(
462-
self, spatial_coords: Optional[Union[BiocFrame, np.ndarray]]
463-
):
451+
def spatial_coordinates(self, spatial_coords: Optional[Union[BiocFrame, np.ndarray]]):
464452
"""Alias for :py:meth:`~set_spatial_coordinates`."""
465453
warn(
466454
"Setting property 'spatial_coords' is an in-place operation, use 'set_spatial_coordinates' instead.",
@@ -510,21 +498,15 @@ def set_spatial_coordinates_names(
510498
new_spatial_coords = self._spatial_coords
511499
else:
512500
_validate_spatial_coords_names(spatial_coords_names, self._spatial_coords)
513-
new_spatial_coords = self._spatial_coords.set_column_names(
514-
spatial_coords_names
515-
)
501+
new_spatial_coords = self._spatial_coords.set_column_names(spatial_coords_names)
516502

517503
output = self._define_output(in_place)
518504
output._spatial_coords = new_spatial_coords
519505
return output
520506

521-
def set_spatial_coords_names(
522-
self, spatial_coords_names: List[str], in_place: bool = False
523-
) -> "SpatialExperiment":
507+
def set_spatial_coords_names(self, spatial_coords_names: List[str], in_place: bool = False) -> "SpatialExperiment":
524508
"""Alias for :py:meth:`~set_spatial_coordinates_names`."""
525-
return self.set_spatial_coordinates_names(
526-
spatial_coords_names=spatial_coords_names, in_place=in_place
527-
)
509+
return self.set_spatial_coordinates_names(spatial_coords_names=spatial_coords_names, in_place=in_place)
528510

529511
@property
530512
def spatial_coords_names(self) -> List[str]:
@@ -538,9 +520,7 @@ def spatial_coords_names(self, spatial_coords_names: List[str]):
538520
"Setting property 'spatial_coords_names' is an in-place operation, use 'set_spatial_coordinates_names' instead.",
539521
UserWarning,
540522
)
541-
self.set_spatial_coordinates_names(
542-
spatial_coords_names=spatial_coords_names, in_place=True
543-
)
523+
self.set_spatial_coordinates_names(spatial_coords_names=spatial_coords_names, in_place=True)
544524

545525
@property
546526
def spatial_coordinates_names(self) -> List[str]:
@@ -554,9 +534,7 @@ def spatial_coordinates_names(self, spatial_coords_names: List[str]):
554534
"Setting property 'spatial_coords_names' is an in-place operation, use 'set_spatial_coordinates_names' instead.",
555535
UserWarning,
556536
)
557-
self.set_spatial_coordinates_names(
558-
spatial_coords_names=spatial_coords_names, in_place=True
559-
)
537+
self.set_spatial_coordinates_names(spatial_coords_names=spatial_coords_names, in_place=True)
560538

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

577-
def set_image_data(
578-
self, img_data: Optional[BiocFrame], in_place: bool = False
579-
) -> "SpatialExperiment":
555+
def set_image_data(self, img_data: Optional[BiocFrame], in_place: bool = False) -> "SpatialExperiment":
580556
"""Set new image data.
581557
582558
Args:
@@ -605,9 +581,7 @@ def set_image_data(
605581
output._img_data = img_data
606582
return output
607583

608-
def set_img_data(
609-
self, img_data: BiocFrame, in_place: bool = False
610-
) -> "SpatialExperiment":
584+
def set_img_data(self, img_data: BiocFrame, in_place: bool = False) -> "SpatialExperiment":
611585
"""Alias for :py:meth:`~set_image_data`."""
612586
return self.set_image_data(img_data=img_data, in_place=in_place)
613587

@@ -669,9 +643,7 @@ def get_scale_factors(
669643
_validate_id(sample_id)
670644
_validate_id(image_id)
671645

672-
idxs = get_img_idx(
673-
img_data=self.img_data, sample_id=sample_id, image_id=image_id
674-
)
646+
idxs = get_img_idx(img_data=self.img_data, sample_id=sample_id, image_id=image_id)
675647

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

@@ -734,19 +706,15 @@ def get_slice(
734706
spe = super().get_slice(rows=rows, columns=columns)
735707

736708
slicer = self._generic_slice(rows=rows, columns=columns)
737-
do_slice_cols = not (
738-
isinstance(slicer.col_indices, slice) and slicer.col_indices == slice(None)
739-
)
709+
do_slice_cols = not (isinstance(slicer.col_indices, slice) and slicer.col_indices == slice(None))
740710

741711
new_spatial_coords = None
742712

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

746716
column_sample_ids = set(spe.column_data["sample_id"])
747-
mask = [
748-
sample_id in column_sample_ids for sample_id in self.img_data["sample_id"]
749-
]
717+
mask = [sample_id in column_sample_ids for sample_id in self.img_data["sample_id"]]
750718

751719
new_img_data = self.img_data[mask,]
752720

@@ -822,11 +790,9 @@ def get_img(
822790
if not self.img_data:
823791
return None
824792

825-
idxs = get_img_idx(
826-
img_data=self.img_data, sample_id=sample_id, image_id=image_id
827-
)
793+
indices = get_img_idx(img_data=self.img_data, sample_id=sample_id, image_id=image_id)
828794

829-
images = self.img_data[idxs,]["data"]
795+
images = self.img_data[indices,]["data"]
830796
return images[0] if len(images) == 1 else images
831797

832798
def add_img(
@@ -869,9 +835,7 @@ def add_img(
869835
Raises:
870836
ValueError: If the sample_id and image_id pair already exists.
871837
"""
872-
_validate_sample_image_ids(
873-
img_data=self._img_data, new_sample_id=sample_id, new_image_id=image_id
874-
)
838+
_validate_sample_image_ids(img_data=self._img_data, new_sample_id=sample_id, new_image_id=image_id)
875839

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

900-
# TODO: implement rmv_img()
901-
def rmv_img(
902-
self,
903-
sample_id: Union[str, bool, None] = None,
904-
image_id: Union[str, bool, None] = None,
905-
in_place: bool = False
864+
def remove_img(
865+
self, sample_id: Union[str, bool, None] = None, image_id: Union[str, bool, None] = None, in_place: bool = False
906866
) -> "SpatialExperiment":
907867
"""Remove an image entry.
908868
@@ -921,17 +881,24 @@ def rmv_img(
921881
Whether to modify the ``SpatialExperiment`` in place.
922882
Defaults to False.
923883
"""
924-
raise NotImplementedError()
884+
_validate_id(sample_id)
885+
_validate_id(image_id)
886+
887+
indices = get_img_idx(img_data=self.img_data, sample_id=sample_id, image_id=image_id)
888+
889+
new_img_data = self._img_data.remove_rows(indices)
890+
891+
output = self._define_output(in_place=in_place)
892+
output._img_data = new_img_data
893+
return output
925894

926895
def img_source(
927896
self,
928897
sample_id: Union[str, bool, None] = None,
929898
image_id: Union[str, bool, None] = None,
930899
path=False,
931900
):
932-
raise NotImplementedError(
933-
"This function is irrelevant because it is for `RemoteSpatialImages`"
934-
)
901+
raise NotImplementedError("This function is irrelevant because it is for `RemoteSpatialImages`")
935902

936903
def img_raster(self, sample_id=None, image_id=None):
937904
# 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

src/spatialexperiment/SpatialImage.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,7 @@ def get_metadata(self) -> dict:
5050
"""
5151
return self._metadata
5252

53-
def set_metadata(
54-
self, metadata: dict, in_place: bool = False
55-
) -> "VirtualSpatialImage":
53+
def set_metadata(self, metadata: dict, in_place: bool = False) -> "VirtualSpatialImage":
5654
"""Set additional metadata.
5755
5856
Args:
@@ -67,9 +65,7 @@ def set_metadata(
6765
or as a reference to the (in-place-modified) original.
6866
"""
6967
if not isinstance(metadata, dict):
70-
raise TypeError(
71-
f"`metadata` must be a dictionary, provided {type(metadata)}."
72-
)
68+
raise TypeError(f"`metadata` must be a dictionary, provided {type(metadata)}.")
7369
output = self._define_output(in_place)
7470
output._metadata = metadata
7571
return output
@@ -150,9 +146,7 @@ def _sanitize_loaded_image(image):
150146
class LoadedSpatialImage(VirtualSpatialImage):
151147
"""Class for images loaded into memory."""
152148

153-
def __init__(
154-
self, image: Union[Image.Image, np.ndarray], metadata: Optional[dict] = None
155-
):
149+
def __init__(self, image: Union[Image.Image, np.ndarray], metadata: Optional[dict] = None):
156150
"""Initialize the object.
157151
158152
Args:
@@ -256,9 +250,7 @@ def get_image(self) -> Image.Image:
256250

257251
return self._image
258252

259-
def set_image(
260-
self, image: Union[Image.Image, np.ndarray], in_place: bool = False
261-
) -> "LoadedSpatialImage":
253+
def set_image(self, image: Union[Image.Image, np.ndarray], in_place: bool = False) -> "LoadedSpatialImage":
262254
"""Set new image.
263255
264256
Args:
@@ -410,9 +402,7 @@ def get_path(self) -> Path:
410402
"""Get the path to the image file."""
411403
return self._path
412404

413-
def set_path(
414-
self, path: Union[str, Path], in_place: bool = False
415-
) -> "StoredSpatialImage":
405+
def set_path(self, path: Union[str, Path], in_place: bool = False) -> "StoredSpatialImage":
416406
"""Update the path to the image file.
417407
418408
Args:
@@ -473,9 +463,7 @@ def _validate_url(url):
473463
class RemoteSpatialImage(VirtualSpatialImage):
474464
"""Class for remotely hosted images."""
475465

476-
def __init__(
477-
self, url: str, metadata: Optional[dict] = None, validate: bool = True
478-
):
466+
def __init__(self, url: str, metadata: Optional[dict] = None, validate: bool = True):
479467
"""Initialize the object.
480468
481469
Args:

src/spatialexperiment/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,3 @@
3535
"VirtualSpatialImage",
3636
"construct_spatial_image_class",
3737
]
38-

0 commit comments

Comments
 (0)