Skip to content
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
7039c00
build: change supported dask versions
melonora Mar 30, 2026
63fbe9a
feat[config]: allow for persisting config
melonora Mar 30, 2026
b7e98b9
test: add config fixture
melonora Apr 7, 2026
558fe97
test: add tests for config
melonora Apr 7, 2026
8eebdb0
add raster write kwargs to api
melonora Apr 14, 2026
790be0c
add tests for raster API
melonora Apr 14, 2026
1a1c673
build: add zarrs-python for improved shard io
melonora Apr 14, 2026
471f72c
CI: change lowerbound dask version
melonora Apr 14, 2026
cd48574
build: correct zarrs
melonora Apr 14, 2026
0187fe9
build: change distributed version constraint
melonora Apr 14, 2026
b629de0
build: support dask and distributed >=2026.3.0
melonora Apr 14, 2026
c2b1375
CI: change lowerbound test version of dask
melonora Apr 14, 2026
bf5b910
fix: pre-commit error due to incorrect typehint
melonora Apr 14, 2026
49441fe
build: include zarrs as dependency
melonora Apr 14, 2026
6334fa8
make zarrs codec default
melonora Apr 14, 2026
73ca72a
config: change chunks and shards to accomodate for raster and table
melonora Apr 16, 2026
c6041bb
chore: adjust raster_write to new config fields
melonora Apr 16, 2026
278606a
docs: add docstring
melonora Apr 16, 2026
f6c0a37
change: add support for providing storage options as list
melonora Apr 16, 2026
36e2271
docs: make docstring for raster write kwargs more clear
melonora Apr 16, 2026
50cb6bb
tests: complete and refactor sharding tests
melonora Apr 16, 2026
6257096
docs: adjust docstring config
melonora Apr 16, 2026
5525fbf
fix: correct parsing chunks, shards argument
melonora Apr 16, 2026
44414c1
test: add testing for adjusting chunks with env variable
melonora Apr 16, 2026
e974647
test: write using settings raster_chunks
melonora Apr 16, 2026
bd6249e
feat: add raster_write_kwargs to write_element
melonora Apr 16, 2026
2600237
test: add test writing multiple elements with raster_kwargs
melonora Apr 16, 2026
bbb4bb6
fix: handle case of element name other than current element
melonora Apr 27, 2026
b95c710
fix: add contextmanager preventing settings leakage between tests
melonora Apr 27, 2026
03aafc9
build: remove sharding dependency group
melonora Apr 27, 2026
f44221a
Merge branch 'main' into support_sharding
LucaMarconato May 12, 2026
f77cff4
chore: use JSONdict
melonora May 20, 2026
d614519
Merge branch 'support_sharding' of github.com:melonora/spatialdata in…
melonora May 20, 2026
10ba46b
chore: use JSONDict
melonora May 20, 2026
c036d49
docs: add comment passing by create_raster_element_kwargs
melonora May 20, 2026
e65c0fb
docs: consistent arrow format and comment renaming chunks/shards
melonora May 20, 2026
26cb40b
refactor: create base_options before storage_options
melonora May 20, 2026
2642b74
build: update dependencies
melonora May 20, 2026
302fd1a
Merge branch 'main' into support_sharding
LucaMarconato May 21, 2026
1ce8d08
Merge branch 'main' into support_sharding
melonora May 21, 2026
7851d94
build: make zarrs optional and test dependency
melonora May 22, 2026
1cd0eb3
change: don't enforce zarrs codec globally
melonora May 22, 2026
a2b05ab
add zarrs context manager
melonora May 22, 2026
1f49846
revert: config settings restored
melonora May 22, 2026
60289cf
tests: add shards check
melonora Jun 26, 2026
9b7e111
chore: add zarrs warning comment
melonora Jun 26, 2026
533355f
docs: added docstring for create_raster_element_kwargs
melonora Jun 26, 2026
4915319
refactor: move normalization raster_write_kwargs
melonora Jun 26, 2026
edb245f
refactor: simplify create_raster_element_kwargs
melonora Jun 30, 2026
e2dd762
refactor: modify copy raster_write_kwargs
melonora Jun 30, 2026
26aa48c
chore: add comment on prefix raster_
melonora Jun 30, 2026
02d2a6d
docs: use docstring decorator for raster_write_kwargs
melonora Jun 30, 2026
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/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
fail-fast: false
matrix:
include:
- {os: windows-latest, python: "3.11", dask-version: "2025.12.0", name: "min dask"}
- {os: windows-latest, python: "3.11", dask-version: "2026.3.0", name: "min dask"}
- {os: windows-latest, python: "3.14", dask-version: "latest"}
- {os: ubuntu-latest, python: "3.11", dask-version: "latest"}
- {os: ubuntu-latest, python: "3.14", dask-version: "latest"}
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ dependencies = [
"annsel>=0.1.2",
"click",
"dask-image",
"dask>=2025.12.0,<2026.1.2",
"distributed<2026.1.2",
"dask>=2026.3.0",
"distributed>=2026.3.0",
Comment thread
melonora marked this conversation as resolved.
Outdated
"datashader",
"fsspec[s3,http]",
"geopandas>=0.14",
Expand All @@ -50,6 +50,7 @@ dependencies = [
"xarray>=2024.10.0",
"xarray-spatial>=0.3.5",
"zarr>=3.0.0",
"zarrs",
Comment thread
LucaMarconato marked this conversation as resolved.
Outdated
]
[project.optional-dependencies]
torch = [
Expand Down
4 changes: 4 additions & 0 deletions src/spatialdata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@
"settings",
]

import zarr

zarr.config.set({"codec_pipeline.path": "zarrs.ZarrsCodecPipeline"})
Comment thread
melonora marked this conversation as resolved.
Outdated


def __getattr__(name: str) -> Any:
if name in _submodules:
Expand Down
34 changes: 34 additions & 0 deletions src/spatialdata/_core/_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from collections.abc import Iterable
from typing import Any

from anndata import AnnData

Expand Down Expand Up @@ -164,3 +165,36 @@ def get_unique_name(name: str, attr: str, is_dataframe_column: bool = False) ->
setattr(sanitized, attr, new_dict)

return None if inplace else sanitized


def create_raster_element_kwargs(

Copy link
Copy Markdown
Collaborator

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 loose dict[str, Any].

If one sees a function that expects a RasterElementWriteOptions as 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.

raster_write_kwargs: dict[str, dict[str, Any] | list[dict[str, Any]]] | list[dict[str, Any]],
Comment thread
melonora marked this conversation as resolved.
Outdated
element_name: str,
element_names: set[str],
) -> dict[str, Any] | list[dict[str, Any]]:
Comment thread
melonora marked this conversation as resolved.
Outdated
element_raster_write_kwargs = None
if isinstance(raster_write_kwargs, dict) and (kwargs := raster_write_kwargs.get(element_name)):
element_raster_write_kwargs = kwargs

if not element_raster_write_kwargs:
Comment thread
melonora marked this conversation as resolved.
Outdated
if isinstance(raster_write_kwargs, dict):
for name in element_names:
raster_write_kwargs.pop(name, None)
Comment thread
melonora marked this conversation as resolved.
Outdated
if not raster_write_kwargs:
element_raster_write_kwargs = {}
elif isinstance(raster_write_kwargs, dict) and not all(
isinstance(x, (dict, list)) for x in raster_write_kwargs.values()
):
element_raster_write_kwargs = raster_write_kwargs
elif isinstance(raster_write_kwargs, list):
if not all(isinstance(x, dict) for x in raster_write_kwargs):
raise ValueError(
"If passing raster_write_kwargs as list, it is assumed to be the storage "
"options for each scale of a multiscale raster as a dictionary."
)
element_raster_write_kwargs = raster_write_kwargs
else:
raise ValueError(
f"Type of raster_write_kwargs should be either dict or list, got {type(raster_write_kwargs)}."
)
return element_raster_write_kwargs
64 changes: 63 additions & 1 deletion src/spatialdata/_core/spatialdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,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, dict[str, Any] | list[dict[str, Any]]] | list[dict[str, Any]] | None = None,
) -> None:
"""
Write the `SpatialData` object to a Zarr store.
Expand Down Expand Up @@ -1155,7 +1156,27 @@ 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
Comment thread
melonora marked this conversation as resolved.
Storage options for raster elements.These options are passed to the zarr storage backend for writing and
Comment thread
melonora marked this conversation as resolved.
Outdated
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would strongly recommend adding an example for users so that they have a recommendation on what to write, at least for Zarr v3. The tests already contain this information.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean as part of the docstring or actual doc? I would provide a follow up PR with specific docs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd put a quick example of how it's called right it in the docstring.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still unresolved. From the docstring alone I find it tricky to understand what to call. I suggest to add a link to the tests working with sharding -> no code duplication, no need to wait for an additional notebook.

- The available key–value pairs in these dictionaries depend on the Zarr format used for writing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it rather hard to know what is or isn't allowed here, as the argument type is very loose and correct parameters depend on what zarr will take. So this is hard for users in interactive mode to get right, and also hard for dependands in non-interactive mode to determine if they have well-formed storage options.

If we don't want (or need) to expose all zarr options to the user right now, we could for now just expose like

    def write_element(
        ...
        raster_write_args: Mapping[str, RasterWriteArgs] | Sequence[RasterWriteArgs] | RasterWriteArgs
        ...
    ):
        ...

    class RasterWriteArgs:
        shards: ShardsLike | None = None

    # or maybe this, if we want to allow raw dicts

    class RasterWriteARgs(TypedDict):
        shards: ShardsLike | None = None

Personally, I also don't like being able to pass in dict or list or dict of dicts, and would rather just have a single way to pass in those parameters.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some arguments that even vary based on whether you are writing zarr_format 2 versus zarr_format 3 which might even still change. This means we would have to keep on checking. We can, but that would be a decision to make.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is indeed super annoying, but I don't think we should be surfacing the complexities (and instabilities!) of our dependencies onto users of spatialdata. If it's annoying for us to deal with it, it'll be even worse for random users to figure it out

- For a full list of supported storage options, refer to:
https://zarr.readthedocs.io/en/stable/api/zarr/create/#zarr.create_array
"""
from spatialdata._core._utils import create_raster_element_kwargs
from spatialdata._io._utils import _resolve_zarr_store
from spatialdata._io.format import _parse_formats

Expand All @@ -1173,6 +1194,13 @@ def write(
store.close()

for element_type, element_name, element in self.gen_elements():
element_raster_write_kwargs = None
Comment thread
melonora marked this conversation as resolved.
Outdated
if element_type in ("images", "labels") and raster_write_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
)

self._write_element(
element=element,
zarr_container_path=file_path,
Expand All @@ -1181,6 +1209,7 @@ def write(
overwrite=False,
parsed_formats=parsed,
shapes_geometry_encoding=shapes_geometry_encoding,
element_raster_write_kwargs=element_raster_write_kwargs,
)

if self.path != file_path and update_sdata_path:
Expand All @@ -1198,6 +1227,7 @@ def _write_element(
overwrite: bool,
parsed_formats: dict[str, SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
element_raster_write_kwargs: dict[str, Any] | list[dict[str, Any]] | None = None,
) -> None:
from spatialdata._io.io_zarr import _get_groups_for_element

Expand Down Expand Up @@ -1236,13 +1266,15 @@ def _write_element(
group=element_group,
name=element_name,
element_format=parsed_formats["raster"],
storage_options=element_raster_write_kwargs,
)
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,
)
elif element_type == "points":
write_points(
Expand Down Expand Up @@ -1273,6 +1305,9 @@ def write_element(
overwrite: bool = False,
sdata_formats: SpatialDataFormatType | list[SpatialDataFormatType] | None = None,
shapes_geometry_encoding: Literal["WKB", "geoarrow"] | None = None,
raster_write_kwargs: dict[str, dict[str, Any] | list[dict[str, Any]] | Any]
| list[dict[str, Any]]
| None = None,
) -> None:
"""
Write a single element, or a list of elements, to the Zarr store used for backing.
Expand All @@ -1291,12 +1326,32 @@ 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
Comment thread
melonora marked this conversation as resolved.
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

Notes
-----
If you pass a list of names, the elements will be written one by one. If an error occurs during the writing of
an element, the writing of the remaining elements will not be attempted.
"""
from spatialdata._core._utils import create_raster_element_kwargs
from spatialdata._io.format import _parse_formats

parsed_formats = _parse_formats(formats=sdata_formats)
Expand All @@ -1309,6 +1364,7 @@ def write_element(
overwrite=overwrite,
sdata_formats=sdata_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
raster_write_kwargs=raster_write_kwargs,
)
return

Expand Down Expand Up @@ -1336,6 +1392,11 @@ def write_element(

self._check_element_not_on_disk_with_different_type(element_type=element_type, element_name=element_name)

element_raster_write_kwargs = None
if element_type in ("images", "labels") and raster_write_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)

self._write_element(
element=element,
zarr_container_path=self.path,
Expand All @@ -1344,6 +1405,7 @@ def write_element(
overwrite=overwrite,
parsed_formats=parsed_formats,
shapes_geometry_encoding=shapes_geometry_encoding,
element_raster_write_kwargs=element_raster_write_kwargs,
)
# After every write, metadata should be consolidated, otherwise this can lead to IO problems like when deleting.
if self.has_consolidated_metadata():
Expand Down
39 changes: 36 additions & 3 deletions src/spatialdata/_io/io_raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -283,12 +283,27 @@ def _write_raster(
raster_format
The format used to write the raster data.
storage_options
Comment thread
melonora marked this conversation as resolved.
Additional options for writing the raster data, like chunks and compression.
Storage options for raster elements.These options are passed to the zarr storage backend for writing and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the docstring in write(). I wonder if we can simplify this docstring by importing some subdosctrings from the other one (like the Important Notes part). But maybe not worth overengineering.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not overengineer for now, also dependent on the function the options can be slightly different.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docstring is long and mostly overlapping with the versions found in write() and write_element(). It's really not ideal to have the versions dangling around.

Comment thread
melonora marked this conversation as resolved.
Outdated
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
label_metadata
Label metadata which can only be defined when writing 'labels'.
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
Expand All @@ -305,6 +320,24 @@ def _write_raster(
for c in channels:
metadata["metadata"]["omero"]["channels"].append({"label": c}) # type: ignore[union-attr, index, call-overload]

if isinstance(storage_options, dict):
storage_options = {
**{k.split("_")[1]: v for k, v in asdict(settings).items() if k in ("raster_chunks", "raster_shards")},
Comment thread
melonora marked this conversation as resolved.
Outdated
**storage_options,
}
elif isinstance(storage_options, list):
storage_options = [
{
**{k.split("_")[1]: v for k, v in asdict(settings).items() if k in ("raster_chunks", "raster_shards")},
**x,
}
for x in storage_options
]
elif not storage_options:
storage_options = {
k.split("_")[1]: v for k, v in asdict(settings).items() if k in ("raster_chunks", "raster_shards")
}
Comment thread
melonora marked this conversation as resolved.
Outdated

if isinstance(raster_data, DataArray):
_write_raster_dataarray(
raster_type,
Expand Down
Loading
Loading