Skip to content

Commit

Permalink
Update the MultiscaleImage API (#238)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
jp-dark authored Oct 31, 2024
1 parent ae05538 commit d1ffae8
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 63 deletions.
1 change: 0 additions & 1 deletion python-spec/src/somacore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
146 changes: 84 additions & 62 deletions python-spec/src/somacore/spatial.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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
"""
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
"""
Expand Down Expand Up @@ -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()

Expand All @@ -685,55 +739,23 @@ 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
"""
raise NotImplementedError()

@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.
Expand Down

0 comments on commit d1ffae8

Please sign in to comment.