Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 93
Support sharding through config and raster_write_kwargs#1106
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
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
7039c0063fbe9ab7e98b9558fe978eebdb0790be0c1a1c673471f72ccd485740187fe9b629de0c2b1375bf5b91049441fe6334fa873ca72ac6041bb278606af6c0a3736e227150cb6bb62570965525fbf44414c1e974647bd6249e2600237bbb4bb6b95c71003aafc9f44221af77cff4d61451910ba46bc036d49e65c0fb26cb40b2642b74302fd1a1ce8d087851d941cd0eb3a2b05ab1f4984660289cf9b7e111533355f4915319edb245fe2dd76226aa48c02d2a6dFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -16,6 +16,7 @@ | ||
| from dask.dataframe import DataFrame as DaskDataFrame | ||
| from dask.dataframe import Scalar | ||
| from geopandas import GeoDataFrame | ||
| from ome_zarr.types import JSONDict | ||
| from shapely import MultiPolygon, Polygon | ||
| from upath import UPath | ||
| from xarray import DataArray, DataTree | ||
| @@ -29,9 +30,10 @@ | ||
| raise_validation_errors, | ||
| validate_table_attr_keys, | ||
| ) | ||
| from spatialdata._docs import docstring_parameter | ||
| from spatialdata._logging import logger | ||
| from spatialdata._types import ArrayLike, Raster_T | ||
| from spatialdata._utils import _deprecation_alias | ||
| from spatialdata._utils import _deprecation_alias, zarrs_context | ||
| from spatialdata.models import ( | ||
| Image2DModel, | ||
| Image3DModel, | ||
| @@ -57,6 +59,27 @@ | ||
| SpatialDataFormatType, | ||
| ) | ||
| RASTER_WRITE_KWARGS_DOCS = """\ | ||
| Storage options for raster elements. These options are passed to the zarr storage backend for writing and | ||
| can be provided in several formats: | ||
| 1. Single dictionary | ||
| A dictionary containing all storage options applied globally. | ||
| 2. Dictionary per raster element | ||
| A dictionary where: | ||
| - Keys = names of raster elements | ||
| - Values = storage options for each element | ||
| - For single-scale data: a dictionary | ||
| - For multiscale data: a list of dictionaries (one per scale) | ||
| 3. List of dictionaries (multiscale only) | ||
| A list where each dictionary defines the storage options for one scale of a multiscale raster element. | ||
| Important Notes | ||
| - The available key–value pairs in these dictionaries depend on the Zarr format used for writing. | ||
| - For a full list of supported storage options, refer to: | ||
| https://zarr.readthedocs.io/en/stable/api/zarr/create/#zarr.create_array | ||
| """ | ||
| class SpatialData: | ||
| """ | ||
| @@ -1105,6 +1128,7 @@ def _validate_all_elements(self) -> None: | ||
| validate_table_attr_keys(element, location=element_path) | ||
| @_deprecation_alias(format="sdata_formats", version="0.7.0") | ||
| @docstring_parameter(raster_write_kwargs=RASTER_WRITE_KWARGS_DOCS) | ||
| def write( | ||
| self, | ||
| file_path: str | Path, | ||
| @@ -1113,6 +1137,7 @@ def write( | ||
| update_sdata_path: bool = True, | ||
| sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None, | ||
| shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None, | ||
| raster_write_kwargs: dict[str, JSONDict | list[JSONDict]] | list[JSONDict] | None = None, | ||
| raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None, | ||
| ) -> None: | ||
| """ | ||
| @@ -1161,6 +1186,8 @@ def write( | ||
| shapes_geometry_encoding | ||
| Whether to use the WKB or geoarrow encoding for GeoParquet. See :meth:`geopandas.GeoDataFrame.to_parquet` | ||
| for details. If None, uses the value from :attr:`spatialdata.settings.shapes_geometry_encoding`. | ||
| raster_write_kwargs | ||
melonora marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| {RASTER_WRITE_KWARGS_DOCS} | ||
| raster_compressor | ||
| A lenght-1 dictionary with as key the type of compression to use for images and labels and as value the | ||
| compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are | ||
| @@ -1193,6 +1220,7 @@ def write( | ||
| overwrite=False, | ||
| parsed_formats=parsed, | ||
| shapes_geometry_encoding=shapes_geometry_encoding, | ||
| raster_write_kwargs=raster_write_kwargs, | ||
| raster_compressor=raster_compressor, | ||
| ) | ||
| @@ -1211,6 +1239,7 @@ def _write_element( | ||
| overwrite: bool, | ||
| parsed_formats: dict[str, SpatialDataFormatType] | None = None, | ||
| shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None, | ||
| raster_write_kwargs: dict[str, JSONDict | list[JSONDict] | Any] | list[JSONDict] | None = None, | ||
| raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None, | ||
| ) -> None: | ||
| from spatialdata._io.io_zarr import _get_groups_for_element | ||
| @@ -1244,51 +1273,63 @@ def _write_element( | ||
| validate_element(element) | ||
| if element_type == "images": | ||
| write_image( | ||
| image=element, | ||
| group=element_group, | ||
| name=element_name, | ||
| element_format=parsed_formats["raster"], | ||
| raster_compressor=raster_compressor, | ||
| ) | ||
| elif element_type == "labels": | ||
| write_labels( | ||
| labels=element, | ||
| group=root_group, | ||
| name=element_name, | ||
| element_format=parsed_formats["raster"], | ||
| raster_compressor=raster_compressor, | ||
| ) | ||
| elif element_type == "points": | ||
| write_points( | ||
| points=element, | ||
| group=element_group, | ||
| element_format=parsed_formats["points"], | ||
| ) | ||
| elif element_type == "shapes": | ||
| write_shapes( | ||
| shapes=element, | ||
| group=element_group, | ||
| element_format=parsed_formats["shapes"], | ||
| geometry_encoding=shapes_geometry_encoding, | ||
| ) | ||
| elif element_type == "tables": | ||
| write_table( | ||
| table=element, | ||
| group=element_type_group, | ||
| name=element_name, | ||
| element_format=parsed_formats["tables"], | ||
| ) | ||
| else: | ||
| raise ValueError(f"Unknown element type: {element_type}") | ||
| element_raster_write_kwargs = None | ||
| if element_type in ("images", "labels") and raster_write_kwargs: | ||
| from spatialdata._core._utils import create_raster_element_kwargs | ||
| element_names = set(self.images.keys()).union(self.labels.keys()) | ||
| element_raster_write_kwargs = create_raster_element_kwargs(raster_write_kwargs, element_name, element_names) | ||
| with zarrs_context(): | ||
| if element_type == "images": | ||
| write_image( | ||
| image=element, | ||
| group=element_group, | ||
| name=element_name, | ||
| element_format=parsed_formats["raster"], | ||
| storage_options=element_raster_write_kwargs, | ||
| raster_compressor=raster_compressor, | ||
| ) | ||
| elif element_type == "labels": | ||
| write_labels( | ||
| labels=element, | ||
| group=root_group, | ||
| name=element_name, | ||
| element_format=parsed_formats["raster"], | ||
| storage_options=element_raster_write_kwargs, | ||
| raster_compressor=raster_compressor, | ||
| ) | ||
| elif element_type == "points": | ||
| write_points( | ||
| points=element, | ||
| group=element_group, | ||
| element_format=parsed_formats["points"], | ||
| ) | ||
| elif element_type == "shapes": | ||
| write_shapes( | ||
| shapes=element, | ||
| group=element_group, | ||
| element_format=parsed_formats["shapes"], | ||
| geometry_encoding=shapes_geometry_encoding, | ||
| ) | ||
| elif element_type == "tables": | ||
| write_table( | ||
| table=element, | ||
| group=element_type_group, | ||
| name=element_name, | ||
| element_format=parsed_formats["tables"], | ||
| ) | ||
| else: | ||
| raise ValueError(f"Unknown element type: {element_type}") | ||
| @docstring_parameter(raster_write_kwargs=RASTER_WRITE_KWARGS_DOCS) | ||
| def write_element( | ||
| self, | ||
| element_name: str | list[str], | ||
| overwrite: bool = False, | ||
| sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None, | ||
| shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None, | ||
| raster_write_kwargs: dict[str, JSONDict | list[JSONDict] | Any] | list[JSONDict] | None = None, | ||
| raster_compressor: dict[Literal["lz4", "zstd"], int] | None = None, | ||
| ) -> None: | ||
| """ | ||
| @@ -1308,6 +1349,8 @@ def write_element( | ||
| shapes_geometry_encoding | ||
| Whether to use the WKB or geoarrow encoding for GeoParquet. See :meth:`geopandas.GeoDataFrame.to_parquet` | ||
| for details. If None, uses the value from :attr:`spatialdata.settings.shapes_geometry_encoding`. | ||
| raster_write_kwargs | ||
melonora marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| {RASTER_WRITE_KWARGS_DOCS} | ||
| raster_compressor | ||
| A lenght-1 dictionary with as key the type of compression to use for images and labels and as value the | ||
| compression level which should be inclusive between 0 and 9. For compression, `lz4` and `zstd` are | ||
| @@ -1331,6 +1374,7 @@ def write_element( | ||
| overwrite=overwrite, | ||
| sdata_formats=sdata_formats, | ||
| shapes_geometry_encoding=shapes_geometry_encoding, | ||
| raster_write_kwargs=raster_write_kwargs, | ||
| raster_compressor=raster_compressor, | ||
| ) | ||
| return | ||
| @@ -1367,6 +1411,7 @@ def write_element( | ||
| overwrite=overwrite, | ||
| parsed_formats=parsed_formats, | ||
| shapes_geometry_encoding=shapes_geometry_encoding, | ||
| raster_write_kwargs=raster_write_kwargs, | ||
| raster_compressor=raster_compressor, | ||
| ) | ||
| # After every write, metadata should be consolidated, otherwise this can lead to IO problems like when deleting. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -148,13 +148,13 @@ def _prepare_storage_options( | ||
| return None | ||
| if isinstance(storage_options, dict): | ||
| prepared = dict(storage_options) | ||
| if "chunks" in prepared: | ||
| if "chunks" in prepared and prepared["chunks"] is not None: | ||
| prepared["chunks"] = _normalize_explicit_chunks(prepared["chunks"]) | ||
| return prepared | ||
| prepared_options = [dict(options) for options in storage_options] | ||
| for options in prepared_options: | ||
| if "chunks" in options: | ||
| if "chunks" in options and options["chunks"] is not None: | ||
| options["chunks"] = _normalize_explicit_chunks(options["chunks"]) | ||
| return prepared_options | ||
| @@ -284,6 +284,19 @@ def _write_raster( | ||
| raster_format | ||
| The format used to write the raster data. | ||
| storage_options | ||
melonora marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Storage options for raster elements, which have been extracted from potentially mixed kwargs dict by | ||
| `create_raster_element_kwargs`. These options are passed to the zarr storage backend for writing and can be | ||
| provided in several formats: | ||
| 1. Single dictionary | ||
| A dictionary containing all storage options applied to the raster, either single or multiscale. | ||
| 2. List of dictionaries (multiscale only) | ||
| A list where each dictionary defines the storage options for one scale of the multiscale raster element. | ||
| Important Notes | ||
| - The available key–value pairs in these dictionaries depend on the Zarr format used for writing. | ||
| - For a full list of supported storage options, refer to: | ||
| https://zarr.readthedocs.io/en/stable/api/zarr/create/#zarr.create_array | ||
| Additional options for writing the raster data, like chunks and compression. | ||
| raster_compressor | ||
| Compression settings as a len-1 dictionary with a single key-value {compression: compression level} pair | ||
| @@ -292,6 +305,10 @@ def _write_raster( | ||
| metadata | ||
| Additional metadata for the raster element | ||
| """ | ||
| from dataclasses import asdict | ||
| from spatialdata import settings | ||
| if raster_type not in ["image", "labels"]: | ||
| raise ValueError(f"{raster_type} is not a valid raster type. Must be 'image' or 'labels'.") | ||
| # "name" and "label_metadata" are only used for labels. "name" is written in write_multiscale_ngff() but ignored in | ||
| @@ -308,6 +325,14 @@ def _write_raster( | ||
| for c in channels: | ||
| metadata["metadata"]["omero"]["channels"].append({"label": c}) # type: ignore[union-attr, index, call-overload] | ||
| # Prefixing with raster_ to account for anndata chunks / shards that will be supported in the future. | ||
| base_options = {k.split("_")[1]: v for k, v in asdict(settings).items() if k in ("raster_chunks", "raster_shards")} | ||
| if isinstance(storage_options, list): | ||
| storage_options = [{**base_options, **x} for x in storage_options] | ||
| else: | ||
| storage_options = {**base_options, **(storage_options or {})} | ||
| if isinstance(raster_data, DataArray): | ||
| _write_raster_dataarray( | ||
| raster_type, | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to register my core argument from our discussion yesterday, I believe this function should rather be a constructor (or factory function) of a class with a name like
RasterElementWriteOptions, rather than being a free-standing function that returns a loosedict[str, Any].If one sees a function that expects a
RasterElementWriteOptionsas an argument, then it is very natural to go find that class and a way to construct it, and everything that is or isn't accepted inside that class is immediately clear from the constructor signature and/or class field declarations. If we use these loose dictionaries, then the expectations for such dicts will only exist in comments or internal knowledge. It is also a lot harder to spot API breakages.