From d1ffae82557abb85d66cd836f000835afff68a54 Mon Sep 17 00:00:00 2001 From: Julia Dark <24235303+jp-dark@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:40:49 -0400 Subject: [PATCH] Update the MultiscaleImage API (#238) * Directly take the `CoordinateSpace` as a creation parameter. * Remove confusing `image_type` property/creation parameter. Use `data_axis_order` instead which uses the axis names the user provided. * Create the first resolution level when creating the `MultiscaleImage`. * Require the `add_new_level` to only add images smaller than the base (level=0) image. * Add a `set` method for adding images that exist outside of SOMA. * Add a property to check the number of channels in the image. * Remove `ImageProperties` class. --- python-spec/src/somacore/__init__.py | 1 - python-spec/src/somacore/spatial.py | 146 +++++++++++++++------------ 2 files changed, 84 insertions(+), 63 deletions(-) diff --git a/python-spec/src/somacore/__init__.py b/python-spec/src/somacore/__init__.py index 9afcd35..a814908 100644 --- a/python-spec/src/somacore/__init__.py +++ b/python-spec/src/somacore/__init__.py @@ -39,7 +39,6 @@ from .query import ExperimentAxisQuery from .scene import Scene from .spatial import GeometryDataFrame -from .spatial import ImageProperties from .spatial import MultiscaleImage from .spatial import PointCloudDataFrame from .spatial import SpatialRead diff --git a/python-spec/src/somacore/spatial.py b/python-spec/src/somacore/spatial.py index 0539592..0373107 100644 --- a/python-spec/src/somacore/spatial.py +++ b/python-spec/src/somacore/spatial.py @@ -14,7 +14,7 @@ ) import pyarrow as pa -from typing_extensions import Final, Protocol, Self +from typing_extensions import Final, Self from . import base from . import coordinates @@ -501,6 +501,7 @@ class MultiscaleImage( # type: ignore[misc] # __eq__ false positive match the expected following properties: * number of channels * axis order + * type Lifecycle: experimental """ @@ -528,23 +529,40 @@ def create( uri: str, *, type: pa.DataType, - reference_level_shape: Sequence[int], - axis_names: Sequence[str] = ("c", "y", "x"), - axis_types: Sequence[str] = ("channel", "height", "width"), + level_shape: Sequence[int], + level_key: str = "level0", + level_uri: Optional[str] = None, + coordinate_space: Union[Sequence[str], coordinates.CoordinateSpace] = ( + "x", + "y", + ), + data_axis_order: Optional[Sequence[str]] = None, + has_channel_axis: bool = True, platform_config: Optional[options.PlatformConfig] = None, context: Optional[Any] = None, ) -> Self: - """Creates a new collection of this type at the given URI. + """Creates a new MultiscaleImage with one initial level. Args: uri: The URI where the collection will be created. - reference_level_shape: The shape of the reference level for the multiscale - image. In most cases, this corresponds to the size of the image - at ``level=0``. - axis_names: The names of the axes of the image. - axis_types: The types of the axes of the image. Must be the same length as - ``axis_names``. Valid types are: ``channel``, ``height``, ``width``, - and ``depth``. + type: The Arrow type to store the image data in the array. + If the type is unsupported, an error will be raised. + level_shape: The shape of the multiscale image for ``level=0``. Must + include the channel dimension if there is one. + level_key: The name for the ``level=0`` image. Defaults to ``level0``. + level_uri: The URI for the ``level=0`` image. If the URI is an existing + SOMADenseNDArray it must match have the shape provided by + ``level_shape`` and type specified in ``type. If set to ``None``, the + ``level_key`` will be used to construct a default child URI. For more + on URIs see :meth:`collection.Collection.add_new_collction`. + coordinate_space: Either the coordinate space or the axis names for the + coordinate space the ``level=0`` image is defined on. This does not + include the channel dimension, only spatial dimensions. + data_axis_order: The order of the axes as stored on disk. Use + ``soma_channel`` to specify the location of a channel axis. If no + axis is provided, this defaults to the channel axis followed by the + coordinate space axes in reverse order (e.g. + ``("soma_channel", "y", "x")`` if ``coordinate_space=("x", "y")``). Returns: The newly created collection, opened for writing. @@ -560,12 +578,43 @@ def add_new_level( *, uri: Optional[str] = None, shape: Sequence[int], - ) -> data.DenseNDArray: + ) -> _DenseND: """Add a new level in the multi-scale image. Parameters are as in :meth:`data.DenseNDArray.create`. The provided shape will be used to compute the scale between images and must correspond to the image - size for the entire image. + size for the entire image. The image must be smaller than the ``level=0`` image. + + Lifecycle: experimental + """ + raise NotImplementedError() + + @abc.abstractmethod + def set( + self, + key: str, + value: _DenseND, + *, + use_relative_uri: Optional[bool] = None, + ) -> Self: + """Sets a new level in the multi-scale image to be an existing SOMA + :class:`data.DenseNDArray`. + + Args: + key: The string key to set. + value: The SOMA object to insert into the collection. + use_relative_uri: Determines whether to store the collection + entry with a relative URI (provided the storage engine + supports it). + If ``None`` (the default), will automatically determine whether + to use an absolute or relative URI based on their relative + location. + If ``True``, will always use a relative URI. If the new child + does not share a relative URI base, or use of relative URIs + is not possible at all, the collection should raise an error. + If ``False``, will always use an absolute URI. + + Returns: ``self``, to enable method chaining. Lifecycle: experimental """ @@ -583,6 +632,7 @@ def read_spatial_region( region_transform: Optional[coordinates.CoordinateTransform] = None, region_coord_space: Optional[coordinates.CoordinateSpace] = None, result_order: options.ResultOrderStr = _RO_AUTO, + data_axis_order: Optional[Sequence[str]] = None, platform_config: Optional[options.PlatformConfig] = None, ) -> "SpatialRead[pa.Tensor]": """Reads a user-defined region of space into a :class:`SpatialRead` with data @@ -607,8 +657,12 @@ def read_spatial_region( region_coord_space: An optional coordinate space for the region being read. The axis names must match the input axis names of the transform. Defaults to ``None``, coordinate space will be inferred from transform. - result_order: the order to return results, specified as a - :class:`~options.ResultOrder` or its string value. + data_axis_order: The order to return the data axes in. Use ``soma_channel`` + to specify the location of the channel coordinate. + result_order: The order data to return results, specified as a + :class:`~options.ResultOrder` or its string value. This is the result + order the data is read from disk. It may be permuted if + ``data_axis_order`` is not the default order. Returns: The data bounding the requested region as a :class:`SpatialRead` with @@ -620,26 +674,26 @@ def read_spatial_region( @property @abc.abstractmethod - def axis_names(self) -> Tuple[str, ...]: - """The name of the image axes. + def coordinate_space(self) -> coordinates.CoordinateSpace: + """Coordinate space for this multiscale image. Lifecycle: experimental """ raise NotImplementedError() - @property + @coordinate_space.setter @abc.abstractmethod - def coordinate_space(self) -> coordinates.CoordinateSpace: + def coordinate_space(self, value: coordinates.CoordinateSpace) -> None: """Coordinate space for this multiscale image. Lifecycle: experimental """ raise NotImplementedError() - @coordinate_space.setter + @property @abc.abstractmethod - def coordinate_space(self, value: coordinates.CoordinateSpace) -> None: - """Coordinate space for this multiscale image. + def data_axis_order(self) -> Tuple[str, ...]: + """The order of the axes for the images. Lifecycle: experimental """ @@ -668,10 +722,10 @@ def get_transform_to_level( @property @abc.abstractmethod - def image_type(self) -> str: - """The order of the axes as stored in the data model. + def has_channel_axis(self) -> bool: + """Returns if the images have an explicit channel axis. - Lifecycle: experimental + Lifecycle: experimental. """ raise NotImplementedError() @@ -685,19 +739,8 @@ def level_count(self) -> int: raise NotImplementedError() @abc.abstractmethod - def level_properties(self, level: Union[int, str]) -> "ImageProperties": - """The properties of an image at the specified level. - - Lifecycle: experimental - """ - raise NotImplementedError() - - @property - def reference_level(self) -> Optional[int]: - """The index of image level that is used as a reference level. - - This will return ``None`` if no current image level matches the size of the - reference level. + def level_shape(self, level: Union[int, str]) -> Tuple[int, ...]: + """The shape of the image at the specified level. Lifecycle: experimental """ @@ -705,35 +748,14 @@ def reference_level(self) -> Optional[int]: @property @abc.abstractmethod - def reference_level_properties(self) -> "ImageProperties": - """The image properties of the reference level. + def nchannels(self) -> int: + """The number of channels. Lifecycle: experimental """ raise NotImplementedError() -class ImageProperties(Protocol): - """Class requirements for level properties of images. - - Lifecycle: experimental - """ - - @property - def name(self) -> str: - """The key for the image. - - Lifecycle: experimental - """ - - @property - def shape(self) -> Tuple[int, ...]: - """Size of each axis of the image. - - Lifecycle: experimental - """ - - @dataclass class SpatialRead(Generic[_ReadData]): """Reader for spatial data.