Skip to content

yaozarrs.write.v05#

This module provides convenience functions to write OME-Zarr v0.5 stores.

Overview#

The general pattern is:

  1. Decide what kind of OME model best matches your data (see Guide to OME-Zarr if you're new to OME Zarr), and construct your OME-Zarr metadata model using yaozarrs.v05 models:
    • Single Image (<=5D): use Image.
    • Multi-Well Plate: use Plate.
    • Collection of images (e.g. 5D images at multiple positions, or any other 6+D data): use bioformats2raw layout with Bf2Raw.
  2. Decide whether to use high level write functions (write_image, write_plate, etc...) or lower level prepare/Builder methods (prepare_image, PlateBuilder, etc...):

    • High Level write_* functions immediately write data and return a Path to the root of the written zarr group.

      Name Description
      write_image Write a single Image OME-Zarr v0.5 store.
      write_plate Write a multi-well Plate OME-Zarr v0.5 store.
      write_bioformats2raw Write a bioformats2raw-style OME-Zarr v0.5 store.
    • Low Level prepare_*/Builder methods prepare the zarr hierarchy, instantiate empty Arrays, and return a mapping of dataset-path to backend Array object, which you can then use to write data. For complex groups, they can write one dataset at a time, or in any order you like.

      Name Description
      prepare_image Prepare a single Image OME-Zarr v0.5 store for writing.
      LabelsBuilder Prepare a Labels OME-Zarr v0.5 store for writing.
      PlateBuilder Prepare a multi-well Plate OME-Zarr v0.5 store for writing.
      Bf2RawBuilder Prepare a bioformats2raw-style OME-Zarr v0.5 store for writing.
    Still confused: Which API should I use?
    • Use high level write_* functions for simple one-shot writes, where you can provide the full data arrays up front (either in-memory with numpy, or dask, etc...)
    • Use lower level prepare_*/Builder APIs when you need to customize how data is written, perhaps in a streaming, or slice-by-slice manner, or when you want to use the backend array writing API directly.

    Note: lower level builders are recommended for Plates and Bf2Raw collections

  3. Call the appropriate function with your metadata model and data arrays.

Tip

A key observation here is that there is generally a one-to-one mapping between a Dataset (nested inside the Image model), and an array node in the output zarr hierarchy. Most functions that accept arrays and/or (shape, dtype) pairs are expecting one per Dataset in the model.

Choosing an Array Writer Backend#

All functions in this module eventually write zarr arrays. You can control what backend is used to write arrays using the writer argument to each function, which takes a string literal (name of the backend), or a custom CreateArrayFunc function. If you want to use builtin writers backends, you must install with the appropriate extras:

You may also implement and pass in your own writer function. See the Custom Writers guide for details.


yaozarrs.write.v05 #

Writing utilities for OME-Zarr v0.5 format.

LabelsBuilder #

LabelsBuilder(
    dest: str | PathLike,
    *,
    writer: ZarrWriter = "auto",
    chunks: ShapeLike | Literal["auto"] | None = "auto",
    shards: ShapeLike | None = None,
    overwrite: bool = False,
    compression: CompressionName = "blosc-zstd",
)

Builder for labels groups within an Image.

The labels group structure includes: - A labels group with LabelsGroup metadata listing all label names - Each label as a separate LabelImage subgroup (e.g., cells/, nuclei/)

This builder supports two workflows:

  1. Immediate write (simpler): Use write_label() to write each label with its data immediately. The builder auto-generates and updates LabelsGroup metadata after each call.

  2. Prepare-only (flexible): Use add_label() to register all labels, then prepare() to create the hierarchy with empty arrays. Write data to the returned array handles yourself.

See Also
  • write_image : High-level function with labels parameter for writing everything at once.

Parameters:

Name Type Description Default
dest str | PathLike

Destination path for the labels Zarr group (typically image_path/labels).

required
writer 'zarr' | 'tensorstore' | 'auto' | CreateArrayFunc

Backend to use for writing arrays. Default is "auto".

'auto'
chunks tuple[int, ...] | 'auto' | None

Chunk shape for all arrays. Default is "auto".

'auto'
shards tuple[int, ...] | None

Shard shape for Zarr v3 sharding. Default is None (no sharding). When present, shard_shape must be divisible by chunk shape.

None
overwrite bool

If True, overwrite existing groups. Default is False. Note: existing directories that don't look like zarr groups will NOT be removed, an exception will be raised instead.

False
compression 'blosc-zstd' | 'blosc-lz4' | 'zstd' | 'none'

Compression codec. Default is "blosc-zstd".

'blosc-zstd'

Examples:

Immediate write workflow:

>>> import numpy as np
>>> from yaozarrs import v05
>>> from yaozarrs.write.v05 import LabelsBuilder
>>> def make_label_image():
...     return v05.LabelImage(
...         multiscales=[
...             v05.Multiscale(
...                 axes=[v05.SpaceAxis(name="y"), v05.SpaceAxis(name="x")],
...                 datasets=[
...                     v05.Dataset(
...                         path="0",
...                         coordinateTransformations=[
...                             v05.ScaleTransformation(scale=[1.0, 1.0])
...                         ],
...                     )
...                 ],
...             )
...         ],
...         image_label=v05.ImageLabel(),
...     )
>>> builder = LabelsBuilder("my_image.zarr/labels")
>>> builder.write_label(
...     "cells", make_label_image(), np.zeros((64, 64), dtype=np.uint32)
... )
<LabelsBuilder: 1 labels>
>>> builder.write_label(
...     "nuclei", make_label_image(), np.zeros((64, 64), dtype=np.uint32)
... )
<LabelsBuilder: 2 labels>

Prepare-only workflow:

>>> builder2 = LabelsBuilder("my_image2.zarr/labels")
>>> builder2.add_label(
...     "cells",
...     make_label_image(),
...     ((64, 64), np.uint32),  # shape, dtype spec
... )
<LabelsBuilder: 1 labels>
>>> path, arrays = builder2.prepare()
>>> arrays["cells/0"][:] = np.random.randint(0, 10, (64, 64), dtype=np.uint32)
Source code in src/yaozarrs/write/v05/_write.py
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
def __init__(
    self,
    dest: str | PathLike,
    *,
    writer: ZarrWriter = "auto",
    chunks: ShapeLike | Literal["auto"] | None = "auto",
    shards: ShapeLike | None = None,
    overwrite: bool = False,
    compression: CompressionName = "blosc-zstd",
) -> None:
    self._dest = Path(dest)
    self._writer: ZarrWriter = writer
    self._chunks: ShapeLike | Literal["auto"] | None = chunks
    self._shards = shards
    self._overwrite = overwrite
    self._compression: CompressionName = compression

    # For prepare-only workflow: {label_name: (LabelImage, specs)}
    self._labels: dict[str, tuple[LabelImage, ShapeAndDTypeOrPyramid]] = {}

    # For immediate write workflow
    self._initialized = False
    self._written_labels: list[str] = []

    # Load existing labels from labels/zarr.json if it exists
    self._preexisting_labels: list[str] = self._load_existing_labels()

root_path property #

root_path: Path

Path to the labels group.

write_label #

write_label(
    name: str,
    label_image: LabelImage,
    datasets: ArrayOrPyramid,
    *,
    progress: bool = False,
) -> Self

Write a label immediately with its data.

This method creates the label structure and writes data in one call. The labels group structure and LabelsGroup metadata are created/updated automatically. Use this for the "immediate write" workflow.

Parameters:

Name Type Description Default
name str

Label name (becomes the subgroup path, e.g., "cells", "nuclei").

required
label_image LabelImage

OME-Zarr LabelImage metadata model for this label.

required
datasets ArrayLike | Sequence[ArrayLike]

Data array(s) for each resolution level. For a single dataset, pass the array directly without wrapping in a list.

required
progress bool

Show progress bar for dask arrays. Default is False.

False

Returns:

Type Description
Self

The builder instance (for method chaining).

Raises:

Type Description
ValueError

If a label with this name was already written or added.

NotImplementedError

If the LabelImage has multiple multiscales.

Source code in src/yaozarrs/write/v05/_write.py
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
def write_label(
    self,
    name: str,
    label_image: LabelImage,
    datasets: ArrayOrPyramid,
    *,
    progress: bool = False,
) -> Self:
    """Write a label immediately with its data.

    This method creates the label structure and writes data in one call.
    The labels group structure and LabelsGroup metadata are created/updated
    automatically. Use this for the "immediate write" workflow.

    Parameters
    ----------
    name : str
        Label name (becomes the subgroup path, e.g., "cells", "nuclei").
    label_image : LabelImage
        OME-Zarr LabelImage metadata model for this label.
    datasets : ArrayLike | Sequence[ArrayLike]
        Data array(s) for each resolution level. For a single dataset,
        pass the array directly without wrapping in a list.
    progress : bool, optional
        Show progress bar for dask arrays. Default is False.

    Returns
    -------
    Self
        The builder instance (for method chaining).

    Raises
    ------
    ValueError
        If a label with this name was already written or added.
    NotImplementedError
        If the LabelImage has multiple multiscales.
    """
    self._validate_label_name(name)

    # Initialize labels group structure if needed
    self._ensure_initialized()

    # Update labels/zarr.json with this label
    self._update_labels_group(name)

    # Write the label using the existing write_image function
    # (LabelImage is a subclass of Image)
    write_image(
        self._dest / name,
        label_image,
        datasets,
        writer=self._writer,
        chunks=self._chunks,
        shards=self._shards,
        overwrite=self._overwrite,
        compression=self._compression,
        progress=progress,
    )

    return self

add_label #

add_label(
    name: str,
    label_image: LabelImage,
    datasets: ShapeAndDTypeOrPyramid,
) -> Self

Add a label for the prepare-only workflow.

Registers a label to be created when prepare() is called. Use this when you want to create the Zarr structure without writing data immediately. After calling prepare(), write data to the returned array handles.

Parameters:

Name Type Description Default
name str

Label name (becomes the subgroup path, e.g., "cells", "nuclei").

required
label_image LabelImage

OME-Zarr LabelImage metadata model for this label.

required
datasets ShapeAndDTypeOrPyramid

Shape/dtype spec(s) for each resolution level: - Single level: (shape, dtype) - Multiple levels: [(shape1, dtype1), (shape2, dtype2)]

required

Returns:

Type Description
Self

The builder instance (for method chaining).

Raises:

Type Description
ValueError

If a label with this name was already added or written, or if the number of specs doesn't match the metadata.

NotImplementedError

If the LabelImage has multiple multiscales.

Source code in src/yaozarrs/write/v05/_write.py
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
def add_label(
    self,
    name: str,
    label_image: LabelImage,
    datasets: ShapeAndDTypeOrPyramid,
) -> Self:
    """Add a label for the prepare-only workflow.

    Registers a label to be created when `prepare()` is called. Use this
    when you want to create the Zarr structure without writing data
    immediately. After calling `prepare()`, write data to the returned
    array handles.

    Parameters
    ----------
    name : str
        Label name (becomes the subgroup path, e.g., "cells", "nuclei").
    label_image : LabelImage
        OME-Zarr LabelImage metadata model for this label.
    datasets : ShapeAndDTypeOrPyramid
        Shape/dtype spec(s) for each resolution level:
        - Single level: `(shape, dtype)`
        - Multiple levels: `[(shape1, dtype1), (shape2, dtype2)]`

    Returns
    -------
    Self
        The builder instance (for method chaining).

    Raises
    ------
    ValueError
        If a label with this name was already added or written, or if the
        number of specs doesn't match the metadata.
    NotImplementedError
        If the LabelImage has multiple multiscales.
    """
    self._validate_label_name(name)
    _, specs_seq = _validate_and_normalize_datasets(
        label_image, datasets, f"Label '{name}': "
    )
    self._labels[name] = (label_image, specs_seq)
    return self

prepare #

prepare() -> tuple[Path, dict[str, Any]]

Create the Zarr hierarchy and return array handles.

Creates the complete labels group structure including LabelsGroup metadata, and empty arrays for all registered labels. Call this after registering all labels with add_label().

The returned arrays support numpy-style indexing for writing data: arrays["label_name/dataset"][:] = data.

Returns:

Type Description
tuple[Path, dict[str, Any]]

A tuple of (root_path, arrays) where arrays maps composite keys like "cells/0" (label name / dataset path) to array objects. The array type depends on the configured writer (zarr.Array or tensorstore.TensorStore).

Raises:

Type Description
ValueError

If no labels have been added with add_label().

FileExistsError

If destination exists and overwrite is False.

ImportError

If no suitable writer backend is installed.

Source code in src/yaozarrs/write/v05/_write.py
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
def prepare(self) -> tuple[Path, dict[str, Any]]:
    """Create the Zarr hierarchy and return array handles.

    Creates the complete labels group structure including LabelsGroup
    metadata, and empty arrays for all registered labels. Call this after
    registering all labels with `add_label()`.

    The returned arrays support numpy-style indexing for writing data:
    `arrays["label_name/dataset"][:] = data`.

    Returns
    -------
    tuple[Path, dict[str, Any]]
        A tuple of (root_path, arrays) where `arrays` maps composite keys
        like `"cells/0"` (label name / dataset path) to array objects. The
        array type depends on the configured writer (zarr.Array or
        tensorstore.TensorStore).

    Raises
    ------
    ValueError
        If no labels have been added with `add_label()`.
    FileExistsError
        If destination exists and `overwrite` is False.
    ImportError
        If no suitable writer backend is installed.
    """
    if not self._labels:  # pragma: no cover
        raise ValueError("No labels added. Use add_label() before prepare().")

    # Merge existing labels with new labels
    all_labels = list(self._preexisting_labels)
    all_labels.extend([new for new in self._labels if new not in all_labels])

    # Create or update labels/zarr.json with LabelsGroup metadata
    # If labels group already exists, just update the metadata file with new labels
    labels_group = LabelsGroup(labels=all_labels)
    if self._dest.exists() and (self._dest / "zarr.json").exists():
        _update_zarr3_group(self._dest, labels_group)
    else:
        # Create new group
        _create_zarr3_group(self._dest, labels_group, self._overwrite)

    # Create arrays for each label using prepare_image
    all_arrays: dict[str, Any] = {}
    for label_name, (label_image, datasets) in self._labels.items():
        _label_path, label_arrays = prepare_image(
            self._dest / label_name,
            label_image,
            datasets,
            chunks=self._chunks,
            shards=self._shards,
            writer=self._writer,
            overwrite=self._overwrite,
            compression=self._compression,
        )
        # Flatten into all_arrays with "label_name/dataset" keys
        for dataset_path, arr in label_arrays.items():
            all_arrays[f"{label_name}/{dataset_path}"] = arr

    return self._dest, all_arrays

PlateBuilder #

PlateBuilder(
    dest: str | PathLike,
    *,
    plate: Plate | None = None,
    extra_attributes: dict[str, Any] | None = None,
    writer: ZarrWriter = "auto",
    chunks: ShapeLike | Literal["auto"] | None = "auto",
    shards: ShapeLike | None = None,
    overwrite: bool = False,
    compression: CompressionName = "blosc-zstd",
)

Builder for OME-Zarr v0.5 Plate hierarchies with auto-generated metadata.

The Plate hierarchy includes: - A root Plate group with metadata (auto-generated from written wells) - Well subgroups (e.g., A/1/, B/2/, etc...) each containing Well metadata - Field subgroups (e.g., 0/, 1/) within each well, each an Image

This builder supports two workflows:

  1. Immediate write (simpler): Use write_well() to write each well with its field data immediately. The builder auto-generates and updates plate metadata (rows, columns, wells) after each call, similar to how Bf2RawBuilder auto-updates the series list.

  2. Prepare-only (flexible): Use add_well() to register all wells, then prepare() to create the hierarchy with empty arrays. Plate metadata is auto-generated from all registered wells.

See Also
  • write_plate : High-level function to write all wells at once.

Parameters:

Name Type Description Default
dest str | PathLike

Destination path for the Plate Zarr group.

required
plate Plate | None

Optional OME-Zarr Plate metadata model. If None (default), plate metadata (rows, columns, wells) is auto-generated from written/added wells. If provided, validates that written wells match the metadata.

None
writer 'zarr' | 'tensorstore' | 'auto' | CreateArrayFunc

Backend to use for writing arrays. Default is "auto".

'auto'
chunks tuple[int, ...] | 'auto' | None

Chunk shape for all arrays. Default is "auto".

'auto'
shards tuple[int, ...] | None

Shard shape for Zarr v3 sharding. Default is None (no sharding). When present, shard_shape must be divisible by chunk shape.

None
overwrite bool

If True, overwrite existing groups. Default is False. Note: existing directories that don't look like zarr groups will NOT be removed, an exception will be raised instead.

False
compression 'blosc-zstd' | 'blosc-lz4' | 'zstd' | 'none'

Compression codec. Default is "blosc-zstd".

'blosc-zstd'

Examples:

Auto-generation workflow (recommended):

>>> import numpy as np
>>> from pathlib import Path
>>> from yaozarrs import v05
>>> from yaozarrs.write.v05 import PlateBuilder
>>>
>>> def make_image():
...     return v05.Image(
...         multiscales=[
...             v05.Multiscale(
...                 axes=[v05.SpaceAxis(name="y"), v05.SpaceAxis(name="x")],
...                 datasets=[
...                     v05.Dataset(
...                         path="0",
...                         coordinateTransformations=[
...                             v05.ScaleTransformation(scale=[1.0, 1.0])
...                         ],
...                     )
...                 ],
...             )
...         ]
...     )
>>>
>>> # No plate metadata needed - it's auto-generated!
>>> builder = PlateBuilder("plate_auto.zarr")
>>> builder.write_well(
...     row="A",
...     col="1",
...     images={"0": (make_image(), np.zeros((32, 32), dtype=np.uint16))},
... )
<PlateBuilder: 1 wells>
>>> builder.write_well(
...     row="A",
...     col="2",
...     images={"0": (make_image(), np.zeros((32, 32), dtype=np.uint16))},
... )
<PlateBuilder: 2 wells>
>>> assert (builder.root_path / "zarr.json").exists()  # Plate metadata auto-updated

With explicit plate metadata:

>>> plate = v05.Plate(
...     plate=v05.PlateDef(
...         columns=[v05.Column(name="1")],
...         rows=[v05.Row(name="A")],
...         wells=[v05.PlateWell(path="A/1", rowIndex=0, columnIndex=0)],
...     )
... )
>>> builder2 = PlateBuilder("plate_explicit.zarr", plate=plate)
>>> builder2.write_well(
...     row="A",
...     col="1",
...     images={"0": (make_image(), np.zeros((32, 32), dtype=np.uint16))},
... )
<PlateBuilder: 1 wells>
Source code in src/yaozarrs/write/v05/_write.py
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
def __init__(
    self,
    dest: str | PathLike,
    *,
    plate: Plate | None = None,
    extra_attributes: dict[str, Any] | None = None,
    writer: ZarrWriter = "auto",
    chunks: ShapeLike | Literal["auto"] | None = "auto",
    shards: ShapeLike | None = None,
    overwrite: bool = False,
    compression: CompressionName = "blosc-zstd",
) -> None:
    self._dest = Path(dest)
    self._user_plate = plate  # Store user-provided plate (if any)
    self._extra_attributes = extra_attributes
    self._writer: ZarrWriter = writer
    self._chunks: ShapeLike | Literal["auto"] | None = chunks
    self._shards = shards
    self._overwrite = overwrite
    self._compression: CompressionName = compression

    # For prepare-only workflow: {well_path: {fov: (Image, specs)}}
    self._wells: dict[str, dict[str, ImageWithShapeSpecs]] = {}

    # For immediate write workflow
    self._initialized = False
    # Track written wells: {(row, col): {fov: (Image, datasets)}}
    self._written_wells_data: dict[
        tuple[str, str], dict[str, ImageWithDatasets]
    ] = {}

root_path property #

root_path: Path

Path to the root of the plate hierarchy.

write_well #

write_well(
    row: str,
    col: str,
    images: Mapping[str, ImageWithDatasets],
    *,
    progress: bool = False,
) -> Self

Write a well immediately with its images (fields of view) and data.

This method creates the well structure and writes all field data in one call. The plate structure and well metadata are created/updated automatically. Plate metadata (rows, columns, wells) is auto-generated from all written wells and rewritten after each call.

Parameters:

Name Type Description Default
row str

Row name like "A", "B", etc.

required
col str

Column name like "1", "2", etc.

required
images Mapping[str, ImageWithDatasets]

Mapping of {fov -> (image_model, datasets)} where: - fov: Field of view identifier like "0", "1", etc. - datasets can be: - Single array (for one dataset): {"0": (image, data)} - Sequence (for multiple datasets): {"0": (image, [data1, data2])}

required
progress bool

Show progress bar for dask arrays. Default is False.

False

Returns:

Type Description
Self

The builder instance (for method chaining).

Raises:

Type Description
ValueError

If row/col combination was already written or added, or if a user- provided Plate doesn't include this well.

NotImplementedError

If any Image has multiple multiscales.

Source code in src/yaozarrs/write/v05/_write.py
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
def write_well(
    self,
    row: str,
    col: str,
    images: Mapping[str, ImageWithDatasets],
    *,
    progress: bool = False,
) -> Self:
    """Write a well immediately with its `images` (fields of view) and data.

    This method creates the well structure and writes all field data in one
    call. The plate structure and well metadata are created/updated
    automatically. Plate metadata (rows, columns, wells) is auto-generated
    from all written wells and rewritten after each call.

    Parameters
    ----------
    row : str
        Row name like "A", "B", etc.
    col : str
        Column name like "1", "2", etc.
    images : Mapping[str, ImageWithDatasets]
        Mapping of `{fov -> (image_model, datasets)}` where:
        - fov: Field of view identifier like "0", "1", etc.
        - datasets can be:
          - Single array (for one dataset): `{"0": (image, data)}`
          - Sequence (for multiple datasets): `{"0": (image, [data1, data2])}`
    progress : bool, optional
        Show progress bar for dask arrays. Default is False.

    Returns
    -------
    Self
        The builder instance (for method chaining).

    Raises
    ------
    ValueError
        If row/col combination was already written or added, or if a user-
        provided Plate doesn't include this well.
    NotImplementedError
        If any Image has multiple multiscales.
    """
    # Validate well hasn't been used
    self._validate_well_coordinates(row, col)

    # Initialize plate structure if needed
    self._ensure_initialized()

    # Normalize fields (convert single arrays to sequences)
    normalized_fields: dict[str, tuple[Image, Sequence[ArrayLike]]] = {}
    for fov, (image_model, datasets) in images.items():
        _, datasets_seq = _validate_and_normalize_datasets(
            image_model, datasets, f"Well '{row}/{col}', field '{fov}': "
        )
        normalized_fields[fov] = (image_model, datasets_seq)

    # Track this well's data before writing
    self._written_wells_data[(row, col)] = cast(
        "dict[str, ImageWithDatasets]", normalized_fields
    )

    # Update plate metadata with the new well
    self._update_plate_metadata()

    # Generate Well metadata for this well and create well subgroup
    well_group_path = self._dest / f"{row}/{col}"
    well_metadata = self._generate_well_metadata(list(images))
    _create_zarr3_group(well_group_path, well_metadata, self._overwrite)

    # Write each field of view
    for fov, (image_model, datasets_seq) in normalized_fields.items():
        field_path = well_group_path / fov
        write_image(
            field_path,
            image_model,
            datasets_seq,
            writer=self._writer,
            chunks=self._chunks,
            shards=self._shards,
            overwrite=self._overwrite,
            compression=self._compression,
            progress=progress,
        )

    return self

add_well #

add_well(
    *,
    row: str,
    col: str,
    images: Mapping[str, ImageWithShapeSpecs],
) -> Self

Add a well for the prepare-only workflow.

Registers a well with its fields to be created when prepare() is called. Use this when you want to create the Zarr structure without writing data immediately. After calling prepare(), write data to the returned array handles.

Parameters:

Name Type Description Default
row str

Row name like "A", "B", etc.

required
col str

Column name like "1", "2", etc.

required
images Mapping[str, ImageWithShapeSpecs]

Mapping of {fov -> (image_model, specs)} where specs provide the dtype and shape for each resolution level: - Single level: (image, (shape, dtype)) - Multiple levels: (image, [(shape1, dtype1), (shape2, dtype2)])

required

Returns:

Type Description
Self

The builder instance (for method chaining).

Raises:

Type Description
ValueError

If row/col combination was already added/written, or if a user- provided Plate doesn't include this well, or if field specs don't match Image metadata.

NotImplementedError

If any Image has multiple multiscales.

Source code in src/yaozarrs/write/v05/_write.py
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
def add_well(
    self,
    *,
    row: str,
    col: str,
    images: Mapping[str, ImageWithShapeSpecs],
) -> Self:
    """Add a well for the prepare-only workflow.

    Registers a well with its fields to be created when `prepare()` is called.
    Use this when you want to create the Zarr structure without writing data
    immediately. After calling `prepare()`, write data to the returned array
    handles.

    Parameters
    ----------
    row : str
        Row name like "A", "B", etc.
    col : str
        Column name like "1", "2", etc.
    images : Mapping[str, ImageWithShapeSpecs]
        Mapping of `{fov -> (image_model, specs)}` where specs provide the
        dtype and shape for each resolution level:
        - Single level: `(image, (shape, dtype))`
        - Multiple levels: `(image, [(shape1, dtype1), (shape2, dtype2)])`

    Returns
    -------
    Self
        The builder instance (for method chaining).

    Raises
    ------
    ValueError
        If row/col combination was already added/written, or if a user-
        provided Plate doesn't include this well, or if field specs
        don't match Image metadata.
    NotImplementedError
        If any Image has multiple multiscales.
    """
    # Validate well hasn't been used
    self._validate_well_coordinates(row, col)

    # Validate and normalize all fields before accepting
    well_path = f"{row}/{col}"
    normalized_fields: dict[str, ImageWithShapeSpecs] = {}

    for fov, (image_model, specs) in images.items():
        _, specs_seq = _validate_and_normalize_datasets(
            image_model, specs, f"Well '{well_path}', field '{fov}': "
        )
        normalized_fields[fov] = (image_model, specs_seq)

    self._wells[well_path] = normalized_fields
    return self

prepare #

prepare() -> tuple[Path, dict[str, Any]]

Create the Zarr hierarchy and return array handles.

Creates the complete Plate structure including plate metadata (auto- generated from registered wells), well subgroups with Well metadata, and empty arrays for all registered fields. Call this after registering all wells with add_well().

The returned arrays support numpy-style indexing for writing data: arrays["well/field/dataset"][:] = data.

Returns:

Type Description
tuple[Path, dict[str, Any]]

A tuple of (root_path, arrays) where arrays maps composite keys like "A/1/0/0" (well_path / field / dataset_path) to array objects. The array type depends on the configured writer (zarr.Array or tensorstore.TensorStore).

Raises:

Type Description
ValueError

If no wells have been added with add_well().

FileExistsError

If destination exists and overwrite is False.

ImportError

If no suitable writer backend is installed.

Source code in src/yaozarrs/write/v05/_write.py
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
def prepare(self) -> tuple[Path, dict[str, Any]]:
    """Create the Zarr hierarchy and return array handles.

    Creates the complete Plate structure including plate metadata (auto-
    generated from registered wells), well subgroups with Well metadata,
    and empty arrays for all registered fields. Call this after registering
    all wells with `add_well()`.

    The returned arrays support numpy-style indexing for writing data:
    `arrays["well/field/dataset"][:] = data`.

    Returns
    -------
    tuple[Path, dict[str, Any]]
        A tuple of (root_path, arrays) where `arrays` maps composite keys
        like `"A/1/0/0"` (well_path / field / dataset_path) to array
        objects. The array type depends on the configured writer
        (zarr.Array or tensorstore.TensorStore).

    Raises
    ------
    ValueError
        If no wells have been added with `add_well()`.
    FileExistsError
        If destination exists and `overwrite` is False.
    ImportError
        If no suitable writer backend is installed.
    """
    if not self._wells:
        raise ValueError("No wells added. Use add_well() before prepare().")

    # Generate plate metadata from registered wells
    plate = _merge_plate_metadata(self._get_images_dict(), self._user_plate)

    # Create plate zarr.json
    _create_zarr3_group(
        self._dest,
        plate,
        self._overwrite,
        extra_attributes=self._extra_attributes,
    )

    # Create arrays for each well/field combination
    all_arrays: dict[str, Any] = {}

    for well_path, fields in self._wells.items():
        # Generate Well metadata and group
        well_metadata = self._generate_well_metadata(list(fields))
        well_group_path = self._dest / well_path
        _create_zarr3_group(well_group_path, well_metadata, self._overwrite)

        # Create arrays for each field
        for fov, (image_model, datasets) in fields.items():
            field_path = well_group_path / fov

            _field_path, field_arrays = prepare_image(
                field_path,
                image_model,
                datasets,
                chunks=self._chunks,
                shards=self._shards,
                writer=self._writer,
                overwrite=self._overwrite,
                compression=self._compression,
            )

            # Flatten into all_arrays with "well/field/dataset" keys
            for dataset_path, arr in field_arrays.items():
                composite_key = f"{well_path}/{fov}/{dataset_path}"
                all_arrays[composite_key] = arr

    return self._dest, all_arrays

Bf2RawBuilder #

Bf2RawBuilder(
    dest: str | PathLike,
    *,
    ome_xml: str | None = None,
    extra_attributes: dict[str, Any] | None = None,
    writer: ZarrWriter = "auto",
    chunks: ShapeLike | Literal["auto"] | None = "auto",
    shards: ShapeLike | None = None,
    overwrite: bool = False,
    compression: CompressionName = "blosc-zstd",
)

Builder for bioformats2raw layout hierarchies.

The bioformats2raw layout is a convention for storing multiple OME-Zarr images in a single hierarchy. It includes:

  • A root group with bioformats2raw.layout version attribute
  • An OME/ subgroup listing all series names
  • Each series as a separate Image subgroup (e.g., 0/, 1/)
  • Optional OME/METADATA.ome.xml with full OME-XML metadata

This builder supports two workflows:

  1. Immediate write (simpler): Use write_image() to write each series with its data immediately. The builder manages root structure and series list automatically.

  2. Prepare-only (flexible): Use add_series() to register all series, then prepare() to create the hierarchy with empty arrays. Write data to the returned arrays yourself.

See Also

Parameters:

Name Type Description Default
dest str | PathLike

Destination path for the root Zarr group.

required
ome_xml str | None

Original OME-XML string to store as OME/METADATA.ome.xml.

None
writer 'zarr' | 'tensorstore' | 'auto' | CreateArrayFunc

Backend to use for writing arrays. Default is "auto".

'auto'
chunks tuple[int, ...] | 'auto' | None

Chunk shape for all arrays. Default is "auto".

'auto'
shards tuple[int, ...] | None

Shard shape for Zarr v3 sharding. Default is None (no sharding). When present, shard_shape must be divisible by chunk shape.

None
overwrite bool

If True, overwrite existing groups. Default is False. Note: existing directories that don't look like zarr groups will NOT be removed, an exception will be raised instead.

False
compression 'blosc-zstd' | 'blosc-lz4' | 'zstd' | 'none'

Compression codec. Default is "blosc-zstd".

'blosc-zstd'

Examples:

Immediate write workflow:

>>> import numpy as np
>>> from pathlib import Path
>>> from yaozarrs import v05
>>> from yaozarrs.write.v05 import Bf2RawBuilder
>>> def make_image():
...     return v05.Image(
...         multiscales=[
...             v05.Multiscale(
...                 axes=[v05.SpaceAxis(name="y"), v05.SpaceAxis(name="x")],
...                 datasets=[
...                     v05.Dataset(
...                         path="0",
...                         coordinateTransformations=[
...                             v05.ScaleTransformation(scale=[1.0, 1.0])
...                         ],
...                     )
...                 ],
...             )
...         ]
...     )
>>> builder = Bf2RawBuilder("builder_immediate.zarr")
>>> builder.write_image("0", make_image(), np.zeros((32, 32), dtype=np.uint16))
<Bf2RawBuilder: 1 images>
>>> builder.write_image("1", make_image(), np.zeros((16, 16), dtype=np.uint16))
<Bf2RawBuilder: 2 images>
>>> assert (builder.root_path / "0" / "zarr.json").exists()

Prepare-only workflow:

>>> builder2 = Bf2RawBuilder("builder_prepare.zarr")
>>> builder2.add_series("0", make_image(), ((32, 32), np.uint16))  # shape, dtype
<Bf2RawBuilder: 1 images>
>>> builder2.add_series("1", make_image(), ((16, 16), np.uint16))
<Bf2RawBuilder: 2 images>
>>> path, arrays = builder2.prepare()
>>> arrays["0/0"][:] = np.zeros((32, 32), dtype=np.uint16)  # Write data yourself
>>> arrays["1/0"][:] = np.zeros((16, 16), dtype=np.uint16)
>>> assert path.exists()
Source code in src/yaozarrs/write/v05/_write.py
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
def __init__(
    self,
    dest: str | PathLike,
    *,
    ome_xml: str | None = None,
    extra_attributes: dict[str, Any] | None = None,
    writer: ZarrWriter = "auto",
    chunks: ShapeLike | Literal["auto"] | None = "auto",
    shards: ShapeLike | None = None,
    overwrite: bool = False,
    compression: CompressionName = "blosc-zstd",
) -> None:
    self._dest = Path(dest)
    self._ome_xml = ome_xml
    self._extra_attributes = extra_attributes
    self._writer: ZarrWriter = writer
    self._chunks: ShapeLike | Literal["auto"] | None = chunks
    self._shards = shards
    self._overwrite = overwrite
    self._compression: CompressionName = compression
    self._indent = 2

    # For prepare-only workflow: {series_name: (image, dataset_specs)}
    self._series: dict[str, ImageWithShapeSpecs] = {}

    # For immediate write workflow
    self._initialized = False
    self._written_series: list[str] = []

root_path property #

root_path: Path

Path to the root of the bioformats2raw hierarchy.

write_image #

write_image(
    name: str,
    image: Image,
    datasets: ArrayOrPyramid,
    *,
    progress: bool = False,
) -> Self

Write a series immediately with its data.

This method creates the series structure and writes data in one call. The root structure and OME metadata are created/updated automatically. Use this for the "immediate write" workflow.

Parameters:

Name Type Description Default
name str

Series name (becomes the subgroup path, e.g., "0", "1").

required
image Image

OME-Zarr Image metadata model for this series.

required
datasets ArrayLike | Sequence[ArrayLike]

Data array(s) for each resolution level. For a single dataset, pass the array directly without wrapping in a list.

required
progress bool

Show progress bar when writing dask arrays. Default is False.

False

Returns:

Type Description
Self

The builder instance (for method chaining).

Raises:

Type Description
ValueError

If a series with this name was already written or added.

NotImplementedError

If the Image has multiple multiscales.

Source code in src/yaozarrs/write/v05/_write.py
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
def write_image(
    self,
    name: str,
    image: Image,
    datasets: ArrayOrPyramid,
    *,
    progress: bool = False,
) -> Self:
    """Write a series immediately with its data.

    This method creates the series structure and writes data in one call.
    The root structure and OME metadata are created/updated automatically.
    Use this for the "immediate write" workflow.

    Parameters
    ----------
    name : str
        Series name (becomes the subgroup path, e.g., "0", "1").
    image : Image
        OME-Zarr Image metadata model for this series.
    datasets : ArrayLike | Sequence[ArrayLike]
        Data array(s) for each resolution level. For a single dataset,
        pass the array directly without wrapping in a list.
    progress : bool, optional
        Show progress bar when writing dask arrays. Default is False.

    Returns
    -------
    Self
        The builder instance (for method chaining).

    Raises
    ------
    ValueError
        If a series with this name was already written or added.
    NotImplementedError
        If the Image has multiple multiscales.
    """
    self._validate_series_name(name)

    # Initialize root structure if needed
    self._ensure_initialized()

    # Update OME/zarr.json with this series
    self._update_ome_series(name)

    # Write the series using the existing write_image function
    write_image(
        self._dest / name,
        image,
        datasets,
        writer=self._writer,
        chunks=self._chunks,
        shards=self._shards,
        overwrite=self._overwrite,
        compression=self._compression,
        progress=progress,
    )

    return self

add_series #

add_series(
    name: str,
    image: Image,
    datasets: ShapeAndDTypeOrPyramid,
) -> Self

Add a series for the prepare-only workflow.

Registers a series to be created when prepare() is called. Use this when you want to create the Zarr structure without writing data immediately. After calling prepare(), write data to the returned array handles.

Parameters:

Name Type Description Default
name str

Series name (becomes the subgroup path, e.g., "0", "1").

required
image Image

OME-Zarr Image metadata model for this series.

required
datasets ShapeAndDType | Sequence[ShapeAndDType]

Shape and dtype specification(s) for each resolution level, as (shape, dtype) tuples. For a single dataset, pass the tuple directly without wrapping in a list.

required

Returns:

Type Description
Self

The builder instance (for method chaining).

Raises:

Type Description
ValueError

If a series with this name was already added or written, or if the number of dataset specs doesn't match the metadata.

NotImplementedError

If the Image has multiple multiscales.

Source code in src/yaozarrs/write/v05/_write.py
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
def add_series(
    self,
    name: str,
    image: Image,
    datasets: ShapeAndDTypeOrPyramid,
) -> Self:
    """Add a series for the prepare-only workflow.

    Registers a series to be created when `prepare()` is called. Use this
    when you want to create the Zarr structure without writing data
    immediately. After calling `prepare()`, write data to the returned
    array handles.

    Parameters
    ----------
    name : str
        Series name (becomes the subgroup path, e.g., "0", "1").
    image : Image
        OME-Zarr Image metadata model for this series.
    datasets : ShapeAndDType | Sequence[ShapeAndDType]
        Shape and dtype specification(s) for each resolution level, as
        `(shape, dtype)` tuples. For a single dataset, pass the tuple
        directly without wrapping in a list.

    Returns
    -------
    Self
        The builder instance (for method chaining).

    Raises
    ------
    ValueError
        If a series with this name was already added or written, or if the
        number of dataset specs doesn't match the metadata.
    NotImplementedError
        If the Image has multiple multiscales.
    """
    self._validate_series_name(name)
    _, datasets_seq = _validate_and_normalize_datasets(
        image, datasets, f"Series '{name}': "
    )
    self._series[name] = (image, datasets_seq)
    return self

prepare #

prepare() -> tuple[Path, dict[str, Any]]

Create the Zarr hierarchy and return array handles.

Creates the complete bioformats2raw structure including root metadata, OME directory with series list, and empty arrays for all registered series. Call this after registering all series with add_series().

The returned arrays support numpy-style indexing for writing data: arrays["series/dataset"][:] = data.

Returns:

Type Description
tuple[Path, dict[str, Any]]

A tuple of (root_path, arrays) where arrays maps composite keys like "0/0" (series name / dataset path) to array objects. The array type depends on the configured writer (zarr.Array or tensorstore.TensorStore).

Raises:

Type Description
ValueError

If no series have been added with add_series().

FileExistsError

If destination exists and overwrite is False.

ImportError

If no suitable writer backend is installed.

Source code in src/yaozarrs/write/v05/_write.py
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
def prepare(self) -> tuple[Path, dict[str, Any]]:
    """Create the Zarr hierarchy and return array handles.

    Creates the complete bioformats2raw structure including root metadata,
    OME directory with series list, and empty arrays for all registered
    series. Call this after registering all series with `add_series()`.

    The returned arrays support numpy-style indexing for writing data:
    `arrays["series/dataset"][:] = data`.

    Returns
    -------
    tuple[Path, dict[str, Any]]
        A tuple of (root_path, arrays) where `arrays` maps composite keys
        like `"0/0"` (series name / dataset path) to array objects. The
        array type depends on the configured writer (zarr.Array or
        tensorstore.TensorStore).

    Raises
    ------
    ValueError
        If no series have been added with `add_series()`.
    FileExistsError
        If destination exists and `overwrite` is False.
    ImportError
        If no suitable writer backend is installed.
    """
    if not self._series:  # pragma: no cover
        raise ValueError("No series added. Use add_series() before prepare().")

    # Create root zarr.json with bioformats2raw.layout
    bf2raw = Bf2Raw(bioformats2raw_layout=3)  # type: ignore
    _create_zarr3_group(
        self._dest,
        bf2raw,
        self._overwrite,
        extra_attributes=self._extra_attributes,
    )

    # Create OME/zarr.json with series list
    ome_path = self._dest / "OME"
    series_model = Series(series=list(self._series))
    _create_zarr3_group(ome_path, series_model, self._overwrite)

    # Write METADATA.ome.xml if provided
    if self._ome_xml is not None:
        (ome_path / "METADATA.ome.xml").write_text(self._ome_xml)

    # Create arrays for each series using prepare_image
    all_arrays: dict[str, Any] = {}
    for series_name, (image_model, dataset_specs) in self._series.items():
        _root_path, series_arrays = prepare_image(
            self._dest / series_name,
            image_model,
            dataset_specs,
            chunks=self._chunks,
            shards=self._shards,
            writer=self._writer,
            overwrite=self._overwrite,
            compression=self._compression,
        )
        # Flatten into all_arrays with "series/dataset" keys
        for dataset_path, arr in series_arrays.items():
            all_arrays[f"{series_name}/{dataset_path}"] = arr

    return self._dest, all_arrays

write_image #

write_image(
    dest: str | PathLike,
    image: Image,
    datasets: ArrayOrPyramid,
    *,
    labels: Mapping[str, tuple[LabelImage, ArrayOrPyramid]]
    | None = None,
    extra_attributes: dict[str, Any] | None = None,
    writer: ZarrWriter = "auto",
    overwrite: bool = False,
    chunks: tuple[int, ...]
    | Literal["auto"]
    | None = "auto",
    shards: tuple[int, ...] | None = None,
    compression: CompressionName = "blosc-zstd",
    progress: bool = False,
) -> Path

Write an OME-Zarr v0.5 Image group with data.

This is the high-level function for writing a complete OME-Zarr image. It creates the Zarr group hierarchy, writes metadata, and stores data in a single call.

See Also

Parameters:

Name Type Description Default
dest str | PathLike

Destination path for the Zarr group. Will be created if it doesn't exist.

required
image Image

OME-Zarr Image metadata model. Must have exactly one multiscale, with one Dataset entry per array in datasets.

required
datasets ArrayOrPyramid

Data array(s) to write (numpy, dask, or any array with shape/dtype). - For a single dataset, pass the array directly: write_image(dest, image, data) - For multiple datasets (e.g., multiscale pyramid), pass a sequence: write_image(dest, image, [data0, data1, ...]) Must match the number and order of image.multiscales[0].datasets.

required
labels Mapping[str, tuple[LabelImage, ArrayOrPyramid]] | None

Optional label images to write alongside the image. Keys are label names (e.g., "cells", "nuclei"), values are (LabelImage, datasets) tuples. Labels will be written to dest/labels/{name}/. Default is None.

None
extra_attributes dict[str, Any] | None

Additional attributes to write alongside "ome" in zarr.json. For example, {"custom": {...}} will produce attributes: {"ome": {...}, "custom": {...}}.

None
writer 'zarr' | 'tensorstore' | 'auto' | CreateArrayFunc

Backend to use for writing arrays. "auto" prefers tensorstore if available, otherwise falls back to zarr-python. Pass a custom function matching the CreateArrayFunc protocol for custom backends.

'auto'
overwrite bool

If True, overwrite existing Zarr group at dest. Default is False.

False
chunks tuple[int, ...] | 'auto' | None

Chunk shape for storage. "auto" (default) calculates ~4MB chunks with non-spatial dims set to 1. None uses the full array shape (single chunk). Tuple values are clamped to the array shape.

'auto'
shards tuple[int, ...] | None

Shard shape for Zarr v3 sharding. Default is None (no sharding). When present, shard_shape must be divisible by chunk shape.

None
compression 'blosc-zstd' | 'blosc-lz4' | 'zstd' | 'none'

Compression codec. "blosc-zstd" (default) provides good compression with shuffle filter. "zstd" uses raw zstd without blosc container.

'blosc-zstd'
progress bool

Show progress bar when writing dask arrays. Default is False.

False

Returns:

Type Description
Path

Path to the created Zarr group.

Raises:

Type Description
NotImplementedError

If the Image model has multiple multiscales (not yet supported).

ValueError

If the number of datasets doesn't match the metadata.

FileExistsError

If dest exists and overwrite is False.

ImportError

If no suitable writer backend is installed.

Examples:

Write a simple 3D image (CYX) - single dataset:

>>> import numpy as np
>>> from yaozarrs import v05
>>> from yaozarrs.write.v05 import write_image
>>>
>>> data = np.zeros((2, 64, 64), dtype=np.uint16)
>>> image = v05.Image(
...     multiscales=[
...         v05.Multiscale(
...             axes=[
...                 v05.ChannelAxis(name="c"),
...                 v05.SpaceAxis(name="y", unit="micrometer"),
...                 v05.SpaceAxis(name="x", unit="micrometer"),
...             ],
...             datasets=[
...                 v05.Dataset(
...                     path="0",
...                     coordinateTransformations=[
...                         v05.ScaleTransformation(scale=[1.0, 0.5, 0.5])
...                     ],
...                 )
...             ],
...         )
...     ]
... )
>>> result = write_image("example.ome.zarr", image, data)
>>> assert result.exists()

Write a 3D image (CYX) with associated labels:

>>> # Create label images for segmentation
>>> cells_label = v05.LabelImage(
...     **image.model_dump(),
...     image_label={"colors": [{"label_value": 1, "rgba": [255, 0, 0, 255]}]},
... )
>>> cells_data = np.zeros((2, 64, 64), dtype=np.uint8)
>>> nuclei_label = v05.LabelImage(**image.model_dump(), image_label={})
>>> nuclei_data = np.zeros((2, 64, 64), dtype=np.uint8)
>>> result = write_image(
...     "example.ome.zarr",
...     image,
...     data,
...     labels={
...         "cells": (cells_label, cells_data),
...         "nuclei": (nuclei_label, nuclei_data),
...     },
...     overwrite=True,
... )
>>> assert (result / "labels" / "cells" / "0" / "zarr.json").exists()
Source code in src/yaozarrs/write/v05/_write.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
def write_image(
    dest: str | PathLike,
    image: Image,
    datasets: ArrayOrPyramid,
    *,
    labels: Mapping[str, tuple[LabelImage, ArrayOrPyramid]] | None = None,
    extra_attributes: dict[str, Any] | None = None,
    writer: ZarrWriter = "auto",
    overwrite: bool = False,
    chunks: tuple[int, ...] | Literal["auto"] | None = "auto",
    shards: tuple[int, ...] | None = None,
    compression: CompressionName = "blosc-zstd",
    progress: bool = False,
) -> Path:
    """Write an OME-Zarr v0.5 Image group with data.

    This is the high-level function for writing a complete OME-Zarr image.
    It creates the Zarr group hierarchy, writes metadata, and stores data
    in a single call.

    See Also
    --------
    - [`prepare_image`][yaozarrs.write.v05.prepare_image] : Create arrays without
      writing data (for custom write logic).
    - [`write_bioformats2raw`][yaozarrs.write.v05.write_bioformats2raw] : Write
      multi-series bioformats2raw layout.

    Parameters
    ----------
    dest : str | PathLike
        Destination path for the Zarr group. Will be created if it doesn't exist.
    image : Image
        OME-Zarr Image metadata model. Must have exactly one multiscale, with
        one Dataset entry per array in `datasets`.
    datasets : ArrayOrPyramid
        Data array(s) to write (numpy, dask, or any array with shape/dtype).
        - For a single dataset, pass the array directly:
          `write_image(dest, image, data)`
        - For multiple datasets (e.g., multiscale pyramid), pass a sequence:
          `write_image(dest, image, [data0, data1, ...])`
        Must match the number and order of `image.multiscales[0].datasets`.
    labels : Mapping[str, tuple[LabelImage, ArrayOrPyramid]] | None, optional
        Optional label images to write alongside the image. Keys are label names
        (e.g., "cells", "nuclei"), values are (LabelImage, datasets) tuples.
        Labels will be written to `dest/labels/{name}/`. Default is None.
    extra_attributes : dict[str, Any] | None, optional
        Additional attributes to write alongside "ome" in zarr.json.
        For example, `{"custom": {...}}` will produce
        `attributes: {"ome": {...}, "custom": {...}}`.
    writer : "zarr" | "tensorstore" | "auto" | CreateArrayFunc, optional
        Backend to use for writing arrays. "auto" prefers tensorstore if
        available, otherwise falls back to zarr-python. Pass a custom function
        matching the `CreateArrayFunc` protocol for custom backends.
    overwrite : bool, optional
        If True, overwrite existing Zarr group at `dest`. Default is False.
    chunks : tuple[int, ...] | "auto" | None, optional
        Chunk shape for storage. "auto" (default) calculates ~4MB chunks with
        non-spatial dims set to 1. None uses the full array shape (single chunk).
        Tuple values are clamped to the array shape.
    shards : tuple[int, ...] | None, optional
        Shard shape for Zarr v3 sharding. Default is None (no sharding).
        When present, shard_shape must be divisible by chunk shape.
    compression : "blosc-zstd" | "blosc-lz4" | "zstd" | "none", optional
        Compression codec. "blosc-zstd" (default) provides good compression with
        shuffle filter. "zstd" uses raw zstd without blosc container.
    progress : bool, optional
        Show progress bar when writing dask arrays. Default is False.


    Returns
    -------
    Path
        Path to the created Zarr group.

    Raises
    ------
    NotImplementedError
        If the Image model has multiple multiscales (not yet supported).
    ValueError
        If the number of datasets doesn't match the metadata.
    FileExistsError
        If `dest` exists and `overwrite` is False.
    ImportError
        If no suitable writer backend is installed.

    Examples
    --------
    Write a simple 3D image (CYX) - single dataset:

    >>> import numpy as np
    >>> from yaozarrs import v05
    >>> from yaozarrs.write.v05 import write_image
    >>>
    >>> data = np.zeros((2, 64, 64), dtype=np.uint16)
    >>> image = v05.Image(
    ...     multiscales=[
    ...         v05.Multiscale(
    ...             axes=[
    ...                 v05.ChannelAxis(name="c"),
    ...                 v05.SpaceAxis(name="y", unit="micrometer"),
    ...                 v05.SpaceAxis(name="x", unit="micrometer"),
    ...             ],
    ...             datasets=[
    ...                 v05.Dataset(
    ...                     path="0",
    ...                     coordinateTransformations=[
    ...                         v05.ScaleTransformation(scale=[1.0, 0.5, 0.5])
    ...                     ],
    ...                 )
    ...             ],
    ...         )
    ...     ]
    ... )
    >>> result = write_image("example.ome.zarr", image, data)
    >>> assert result.exists()

    Write a 3D image (CYX) with associated labels:

    >>> # Create label images for segmentation
    >>> cells_label = v05.LabelImage(
    ...     **image.model_dump(),
    ...     image_label={"colors": [{"label_value": 1, "rgba": [255, 0, 0, 255]}]},
    ... )
    >>> cells_data = np.zeros((2, 64, 64), dtype=np.uint8)
    >>> nuclei_label = v05.LabelImage(**image.model_dump(), image_label={})
    >>> nuclei_data = np.zeros((2, 64, 64), dtype=np.uint8)
    >>> result = write_image(
    ...     "example.ome.zarr",
    ...     image,
    ...     data,
    ...     labels={
    ...         "cells": (cells_label, cells_data),
    ...         "nuclei": (nuclei_label, nuclei_data),
    ...     },
    ...     overwrite=True,
    ... )
    >>> assert (result / "labels" / "cells" / "0" / "zarr.json").exists()
    """
    multiscale, datasets_seq = _validate_and_normalize_datasets(image, datasets)

    # Extract specs from arrays for prepare_image
    specs: list[ShapeAndDType] = [(arr.shape, arr.dtype) for arr in datasets_seq]

    # Create arrays using prepare_image
    dest_path, arrays = prepare_image(
        dest,
        image,
        specs,
        extra_attributes=extra_attributes,
        chunks=chunks,
        shards=shards,
        writer=writer,
        overwrite=overwrite,
        compression=compression,
    )

    # Write data to arrays
    for data_array, dataset_meta in zip(datasets_seq, multiscale.datasets):
        _write_to_array(arrays[dataset_meta.path], data_array, progress=progress)

    # Write labels if provided
    if labels:
        labels_builder = LabelsBuilder(
            dest_path / "labels",
            writer=writer,
            chunks=chunks,
            shards=shards,
            overwrite=overwrite,
            compression=compression,
        )
        for label_name, (label_image, label_datasets) in labels.items():
            labels_builder.write_label(
                label_name, label_image, label_datasets, progress=progress
            )

    return dest_path

write_plate #

write_plate(
    dest: str | PathLike,
    images: Mapping[
        tuple[str, str, str], ImageWithDatasets
    ],
    *,
    plate: Plate | dict[str, Any] | None = None,
    extra_attributes: dict[str, Any] | None = None,
    writer: ZarrWriter = "auto",
    overwrite: bool = False,
    chunks: tuple[int, ...]
    | Literal["auto"]
    | None = "auto",
    shards: tuple[int, ...] | None = None,
    compression: CompressionName = "blosc-zstd",
    progress: bool = False,
) -> Path

Write an OME-Zarr v0.5 Plate group with data.

This is the high-level function for writing a complete OME-Zarr plate. It creates the plate hierarchy (plate/wells/fields), writes metadata, and stores image data in a single call.

The plate structure::

dest/
├── zarr.json          # Plate metadata
├── A/
│   ├── 1/
│   │   ├── zarr.json  # Well metadata (auto-generated)
│   │   ├── 0/         # Field 0
│   │   │   ├── zarr.json  # Image metadata
│   │   │   └── 0/     # dataset arrays
│   │   └── 1/         # Field 1 (if multiple fields)
│   └── 2/
└── B/
    └── ...
See Also

Parameters:

Name Type Description Default
dest str | PathLike

Destination path for the Plate Zarr group.

required
images Mapping[tuple[str, str, str], ImageWithDatasets]

Mapping of {(row, col, fov) -> (image_model, [datasets, ...])}. Each tuple key specifies (row_name, column_name, field_of_view) like ("A", "1", "0"). Row and column names are auto-extracted from the keys.

required
plate Plate | dict[str, Any] | None

Optional plate metadata. Can be: - None (default): Auto-generate from images dict keys - dict: Merge with auto-generated metadata (user values take precedence) - Plate: Use as-is (must match images dict) Common dict keys: 'name', 'acquisitions', 'field_count'. Auto-generated: 'rows', 'columns', 'wells'.

None
extra_attributes dict[str, Any] | None

Additional attributes to write alongside "ome" in zarr.json.

None
writer 'zarr' | 'tensorstore' | 'auto' | CreateArrayFunc

Backend to use for writing arrays. Default is "auto".

'auto'
overwrite bool

If True, overwrite existing Zarr groups. Default is False.

False
chunks tuple[int, ...] | 'auto' | None

Chunk shape for all arrays. See write_image for details.

'auto'
shards tuple[int, ...] | None

Shard shape for Zarr v3 sharding. Default is None (no sharding). When present, shard_shape must be divisible by chunk shape.

None
compression 'blosc-zstd' | 'blosc-lz4' | 'zstd' | 'none'

Compression codec. Default is "blosc-zstd".

'blosc-zstd'
progress bool

Show progress bar when writing dask arrays. Default is False.

False

Returns:

Type Description
Path

Path to the created Plate Zarr group.

Raises:

Type Description
ValueError

If image keys don't match plate wells, or if datasets don't match metadata.

FileExistsError

If dest exists and overwrite is False.

ImportError

If no suitable writer backend is installed.

Examples:

Write a simple 2x2 plate with auto-generated metadata:

>>> import numpy as np
>>> from yaozarrs import v05
>>> from yaozarrs.write.v05 import write_plate
>>>
>>> # Create image metadata (same for all fields)
>>> def make_image():
...     return v05.Image(
...         multiscales=[
...             v05.Multiscale(
...                 axes=[
...                     v05.SpaceAxis(name="y", unit="micrometer"),
...                     v05.SpaceAxis(name="x", unit="micrometer"),
...                 ],
...                 datasets=[
...                     v05.Dataset(
...                         path="0",
...                         coordinateTransformations=[
...                             v05.ScaleTransformation(scale=[0.5, 0.5])
...                         ],
...                     )
...                 ],
...             )
...         ]
...     )
>>>
>>> # Rows, columns, and wells are auto-generated from the images dict!
>>> images = {
...     ("A", "1", "0"): (make_image(), [np.zeros((64, 64), dtype=np.uint16)]),
...     ("A", "2", "0"): (make_image(), [np.zeros((64, 64), dtype=np.uint16)]),
...     ("B", "1", "0"): (make_image(), [np.zeros((64, 64), dtype=np.uint16)]),
...     ("B", "2", "0"): (make_image(), [np.zeros((64, 64), dtype=np.uint16)]),
... }
>>>
>>> result = write_plate("my_plate1.ome.zarr", images)
>>> assert (result / "A" / "1" / "0" / "zarr.json").exists()
>>>
>>> # Or add custom metadata like a name
>>> result2 = write_plate(
...     "my_plate2.ome.zarr",
...     images,
...     plate={"name": "My Experiment"},
...     overwrite=True,
... )
Source code in src/yaozarrs/write/v05/_write.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
def write_plate(
    dest: str | PathLike,
    images: Mapping[tuple[str, str, str], ImageWithDatasets],
    *,
    plate: Plate | dict[str, Any] | None = None,
    extra_attributes: dict[str, Any] | None = None,
    writer: ZarrWriter = "auto",
    overwrite: bool = False,
    chunks: tuple[int, ...] | Literal["auto"] | None = "auto",
    shards: tuple[int, ...] | None = None,
    compression: CompressionName = "blosc-zstd",
    progress: bool = False,
) -> Path:
    """Write an OME-Zarr v0.5 Plate group with data.

    This is the high-level function for writing a complete OME-Zarr plate.
    It creates the plate hierarchy (plate/wells/fields), writes metadata,
    and stores image data in a single call.

    The plate structure::

        dest/
        ├── zarr.json          # Plate metadata
        ├── A/
        │   ├── 1/
        │   │   ├── zarr.json  # Well metadata (auto-generated)
        │   │   ├── 0/         # Field 0
        │   │   │   ├── zarr.json  # Image metadata
        │   │   │   └── 0/     # dataset arrays
        │   │   └── 1/         # Field 1 (if multiple fields)
        │   └── 2/
        └── B/
            └── ...

    See Also
    --------
    - [`PlateBuilder`][yaozarrs.write.v05.PlateBuilder]: Builder class for incremental
      well/field writing.
    - [`write_image`][yaozarrs.write.v05.write_image]: Write a single Image group.

    Parameters
    ----------
    dest : str | PathLike
        Destination path for the Plate Zarr group.
    images : Mapping[tuple[str, str, str], ImageWithDatasets]
        Mapping of `{(row, col, fov) -> (image_model, [datasets, ...])}`.
        Each tuple key specifies (row_name, column_name, field_of_view) like
        ("A", "1", "0"). Row and column names are auto-extracted from the keys.
    plate : Plate | dict[str, Any] | None, optional
        Optional plate metadata. Can be:
        - None (default): Auto-generate from images dict keys
        - dict: Merge with auto-generated metadata (user values take precedence)
        - Plate: Use as-is (must match images dict)
        Common dict keys: 'name', 'acquisitions', 'field_count'.
        Auto-generated: 'rows', 'columns', 'wells'.
    extra_attributes : dict[str, Any] | None, optional
        Additional attributes to write alongside "ome" in zarr.json.
    writer : "zarr" | "tensorstore" | "auto" | CreateArrayFunc, optional
        Backend to use for writing arrays. Default is "auto".
    overwrite : bool, optional
        If True, overwrite existing Zarr groups. Default is False.
    chunks : tuple[int, ...] | "auto" | None, optional
        Chunk shape for all arrays. See `write_image` for details.
    shards : tuple[int, ...] | None, optional
        Shard shape for Zarr v3 sharding. Default is None (no sharding).
        When present, shard_shape must be divisible by chunk shape.
    compression : "blosc-zstd" | "blosc-lz4" | "zstd" | "none", optional
        Compression codec. Default is "blosc-zstd".
    progress : bool, optional
        Show progress bar when writing dask arrays. Default is False.

    Returns
    -------
    Path
        Path to the created Plate Zarr group.

    Raises
    ------
    ValueError
        If image keys don't match plate wells, or if datasets don't match metadata.
    FileExistsError
        If `dest` exists and `overwrite` is False.
    ImportError
        If no suitable writer backend is installed.

    Examples
    --------
    Write a simple 2x2 plate with auto-generated metadata:

    >>> import numpy as np
    >>> from yaozarrs import v05
    >>> from yaozarrs.write.v05 import write_plate
    >>>
    >>> # Create image metadata (same for all fields)
    >>> def make_image():
    ...     return v05.Image(
    ...         multiscales=[
    ...             v05.Multiscale(
    ...                 axes=[
    ...                     v05.SpaceAxis(name="y", unit="micrometer"),
    ...                     v05.SpaceAxis(name="x", unit="micrometer"),
    ...                 ],
    ...                 datasets=[
    ...                     v05.Dataset(
    ...                         path="0",
    ...                         coordinateTransformations=[
    ...                             v05.ScaleTransformation(scale=[0.5, 0.5])
    ...                         ],
    ...                     )
    ...                 ],
    ...             )
    ...         ]
    ...     )
    >>>
    >>> # Rows, columns, and wells are auto-generated from the images dict!
    >>> images = {
    ...     ("A", "1", "0"): (make_image(), [np.zeros((64, 64), dtype=np.uint16)]),
    ...     ("A", "2", "0"): (make_image(), [np.zeros((64, 64), dtype=np.uint16)]),
    ...     ("B", "1", "0"): (make_image(), [np.zeros((64, 64), dtype=np.uint16)]),
    ...     ("B", "2", "0"): (make_image(), [np.zeros((64, 64), dtype=np.uint16)]),
    ... }
    >>>
    >>> result = write_plate("my_plate1.ome.zarr", images)
    >>> assert (result / "A" / "1" / "0" / "zarr.json").exists()
    >>>
    >>> # Or add custom metadata like a name
    >>> result2 = write_plate(
    ...     "my_plate2.ome.zarr",
    ...     images,
    ...     plate={"name": "My Experiment"},
    ...     overwrite=True,
    ... )
    """
    # Merge user-provided plate metadata with auto-generated
    plate_obj = _merge_plate_metadata(images, plate)

    # Use PlateBuilder to handle the writing
    builder = PlateBuilder(
        dest,
        plate=plate_obj,
        extra_attributes=extra_attributes,
        writer=writer,
        chunks=chunks,
        shards=shards,
        overwrite=overwrite,
        compression=compression,
    )

    # Group images by well: {(row, col): {fov: (Image, datasets)}}
    wells_data: dict[tuple[str, str], dict[str, ImageWithDatasets]] = {}
    for (row, col, fov), image_data in images.items():
        wells_data.setdefault((row, col), {})[fov] = image_data

    # Write each well with all its fields
    for (row, col), fields_data in wells_data.items():
        builder.write_well(row=row, col=col, images=fields_data, progress=progress)

    return builder.root_path

write_bioformats2raw #

write_bioformats2raw(
    dest: str | PathLike,
    images: Mapping[str, ImageWithDatasets],
    *,
    ome_xml: str | None = None,
    extra_attributes: dict[str, Any] | None = None,
    writer: ZarrWriter = "auto",
    overwrite: bool = False,
    chunks: tuple[int, ...]
    | Literal["auto"]
    | None = "auto",
    shards: tuple[int, ...] | None = None,
    compression: CompressionName = "blosc-zstd",
    progress: bool = False,
) -> Path

Write a bioformats2raw-layout OME-Zarr with multiple series.

The bioformats2raw layout is a convention for storing multiple images (series) in a single Zarr hierarchy. It includes a root group with bioformats2raw.layout version, an OME/ group with series metadata, and each series as a separate Image subgroup.

This is the high-level function for writing all series at once. For incremental writes, use Bf2RawBuilder directly.

Writes the following structure:

dest/
├── zarr.json              # root: attributes["ome"]["bioformats2raw.layout"]
├── 0/                     # first series (images["0"])
│   ├── zarr.json          # Image metadata (images["0"][0])
│   ├── 0/                 # first resolution level
│   │   ├── zarr.json      # array metadata
│   │   └── c/             # chunks directory
│   └── 1/                 # second resolution level (if multiscale)
│       ├── zarr.json
│       └── c/
├── 1/                     # second series (images["1"])
│   └── ...
└── OME/
    ├── zarr.json          # attributes["ome"]["series"] = ["0", "1", ...]
    └── METADATA.ome.xml   # optional OME-XML (if ome_xml provided)
See Also

Parameters:

Name Type Description Default
dest str | PathLike

Destination path for the root Zarr group.

required
images dict[str, ImageWithDatasets]

Mapping of {series_name -> (image_model, [datasets, ...])}. Each series name (e.g., "0", "1") becomes a subgroup in the root group, with the Image model defining the zarr.json and the datasets providing the data arrays.

required
ome_xml str | None

OME-XML string to store as OME/METADATA.ome.xml. Useful for preserving full metadata from converted files.

None
extra_attributes dict[str, Any] | None

Additional attributes to write alongside "ome" in zarr.json.

None
writer 'zarr' | 'tensorstore' | 'auto' | CreateArrayFunc

Backend to use for writing arrays.

'auto'
overwrite bool

If True, overwrite existing Zarr groups. Default is False.

False
chunks tuple[int, ...] | 'auto' | None

Chunk shape for all arrays. See write_image for details.

'auto'
shards tuple[int, ...] | None

Shard shape for Zarr v3 sharding. Default is None (no sharding). When present, shard_shape must be divisible by chunk shape.

None
compression 'blosc-zstd' | 'blosc-lz4' | 'zstd' | 'none'

Compression codec. Default is "blosc-zstd".

'blosc-zstd'
progress bool

Show progress bar when writing dask arrays. Default is False.

False

Returns:

Type Description
Path

Path to the root Zarr group.

Raises:

Type Description
FileExistsError

If dest exists and overwrite is False.

ValueError

If any series has mismatched datasets/metadata.

ImportError

If no suitable writer backend is installed.

Examples:

Write a multi-series OME-Zarr:

>>> import numpy as np
>>> from pathlib import Path
>>> from yaozarrs import v05
>>> from yaozarrs.write.v05 import write_bioformats2raw
>>>
>>> def make_image():
...     return v05.Image(
...         multiscales=[
...             v05.Multiscale(
...                 axes=[
...                     v05.SpaceAxis(name="y", unit="micrometer"),
...                     v05.SpaceAxis(name="x", unit="micrometer"),
...                 ],
...                 datasets=[
...                     v05.Dataset(
...                         path="0",
...                         coordinateTransformations=[
...                             v05.ScaleTransformation(scale=[0.5, 0.5])
...                         ],
...                     )
...                 ],
...             )
...         ]
...     )
>>> images = {
...     "0": (make_image(), [np.zeros((64, 64), dtype=np.uint16)]),
...     "1": (make_image(), [np.zeros((32, 32), dtype=np.uint16)]),
... }
>>> result = write_bioformats2raw("multi_series.zarr", images)
>>> (result / "OME" / "zarr.json").exists()
True
>>> (result / "0" / "zarr.json").exists()
True
Source code in src/yaozarrs/write/v05/_write.py
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
def write_bioformats2raw(
    dest: str | PathLike,
    # mapping of {series_name -> ( Image, [datasets, ...] )}
    images: Mapping[str, ImageWithDatasets],
    *,
    ome_xml: str | None = None,
    extra_attributes: dict[str, Any] | None = None,
    writer: ZarrWriter = "auto",
    overwrite: bool = False,
    chunks: tuple[int, ...] | Literal["auto"] | None = "auto",
    shards: tuple[int, ...] | None = None,
    compression: CompressionName = "blosc-zstd",
    progress: bool = False,
) -> Path:
    """Write a bioformats2raw-layout OME-Zarr with multiple series.

    The bioformats2raw layout is a convention for storing multiple images
    (series) in a single Zarr hierarchy. It includes a root group with
    `bioformats2raw.layout` version, an `OME/` group with series metadata,
    and each series as a separate Image subgroup.

    This is the high-level function for writing all series at once. For
    incremental writes, use [`Bf2RawBuilder`][yaozarrs.write.v05.Bf2RawBuilder]
    directly.

    Writes the following structure:

        dest/
        ├── zarr.json              # root: attributes["ome"]["bioformats2raw.layout"]
        ├── 0/                     # first series (images["0"])
        │   ├── zarr.json          # Image metadata (images["0"][0])
        │   ├── 0/                 # first resolution level
        │   │   ├── zarr.json      # array metadata
        │   │   └── c/             # chunks directory
        │   └── 1/                 # second resolution level (if multiscale)
        │       ├── zarr.json
        │       └── c/
        ├── 1/                     # second series (images["1"])
        │   └── ...
        └── OME/
            ├── zarr.json          # attributes["ome"]["series"] = ["0", "1", ...]
            └── METADATA.ome.xml   # optional OME-XML (if ome_xml provided)

    See Also
    --------
    - [`Bf2RawBuilder`][yaozarrs.write.v05.Bf2RawBuilder] : Builder class for
      incremental series writing.
    - [`write_image`][yaozarrs.write.v05.write_image] : Write a single Image group.

    Parameters
    ----------
    dest : str | PathLike
        Destination path for the root Zarr group.
    images : dict[str, ImageWithDatasets]
        Mapping of `{series_name -> (image_model, [datasets, ...])}`.
        Each series name (e.g., "0", "1") becomes a subgroup in the root group, with
        the Image model defining the zarr.json and the datasets providing the data
        arrays.
    ome_xml : str | None, optional
        OME-XML string to store as `OME/METADATA.ome.xml`.
        Useful for preserving full metadata from converted files.
    extra_attributes : dict[str, Any] | None, optional
        Additional attributes to write alongside "ome" in zarr.json.
    writer : "zarr" | "tensorstore" | "auto" | CreateArrayFunc, optional
        Backend to use for writing arrays.
    overwrite : bool, optional
        If True, overwrite existing Zarr groups. Default is False.
    chunks : tuple[int, ...] | "auto" | None, optional
        Chunk shape for all arrays. See `write_image` for details.
    shards : tuple[int, ...] | None, optional
        Shard shape for Zarr v3 sharding. Default is None (no sharding).
        When present, shard_shape must be divisible by chunk shape.
    compression : "blosc-zstd" | "blosc-lz4" | "zstd" | "none", optional
        Compression codec. Default is "blosc-zstd".
    progress : bool, optional
        Show progress bar when writing dask arrays. Default is False.

    Returns
    -------
    Path
        Path to the root Zarr group.

    Raises
    ------
    FileExistsError
        If `dest` exists and `overwrite` is False.
    ValueError
        If any series has mismatched datasets/metadata.
    ImportError
        If no suitable writer backend is installed.

    Examples
    --------
    Write a multi-series OME-Zarr:

    >>> import numpy as np
    >>> from pathlib import Path
    >>> from yaozarrs import v05
    >>> from yaozarrs.write.v05 import write_bioformats2raw
    >>>
    >>> def make_image():
    ...     return v05.Image(
    ...         multiscales=[
    ...             v05.Multiscale(
    ...                 axes=[
    ...                     v05.SpaceAxis(name="y", unit="micrometer"),
    ...                     v05.SpaceAxis(name="x", unit="micrometer"),
    ...                 ],
    ...                 datasets=[
    ...                     v05.Dataset(
    ...                         path="0",
    ...                         coordinateTransformations=[
    ...                             v05.ScaleTransformation(scale=[0.5, 0.5])
    ...                         ],
    ...                     )
    ...                 ],
    ...             )
    ...         ]
    ...     )
    >>> images = {
    ...     "0": (make_image(), [np.zeros((64, 64), dtype=np.uint16)]),
    ...     "1": (make_image(), [np.zeros((32, 32), dtype=np.uint16)]),
    ... }
    >>> result = write_bioformats2raw("multi_series.zarr", images)
    >>> (result / "OME" / "zarr.json").exists()
    True
    >>> (result / "0" / "zarr.json").exists()
    True
    """
    builder = Bf2RawBuilder(
        dest,
        ome_xml=ome_xml,
        extra_attributes=extra_attributes,
        writer=writer,
        chunks=chunks,
        shards=shards,
        overwrite=overwrite,
        compression=compression,
    )

    for series_name, (image_model, datasets) in images.items():
        builder.write_image(series_name, image_model, datasets, progress=progress)

    return builder.root_path

prepare_image #

prepare_image(
    dest: str | PathLike,
    image: Image,
    datasets: ShapeAndDTypeOrPyramid,
    *,
    extra_attributes: dict[str, Any] | None = None,
    chunks: tuple[int, ...]
    | Literal["auto"]
    | None = "auto",
    shards: tuple[int, ...] | None = None,
    writer: ZarrWriter = "auto",
    overwrite: bool = False,
    compression: CompressionName = "blosc-zstd",
) -> tuple[Path, dict[str, Any]]

Create OME-Zarr v0.5 Image structure and return array handles for writing.

This is a lower-level function that creates the Zarr group hierarchy and empty arrays, but does not write data. Use this when you need custom control over how data is written (e.g., chunk-by-chunk streaming, parallel writes).

To write data immediately, use write_image instead.

See Also

Parameters:

Name Type Description Default
dest str | PathLike

Destination path for the Zarr group.

required
image Image

OME-Zarr Image metadata model.

required
datasets ShapeAndDType | Sequence[ShapeAndDType]

Shape and dtype specification(s) for each dataset, as (shape, dtype) tuples. Can be:

  • Single (shape, dtype): For one dataset, no wrapping needed
  • Sequence of (shape, dtype): For multiple datasets (multiscale pyramid)

Must match the number and order of image.multiscales[0].datasets.

required
extra_attributes dict[str, Any] | None

Additional attributes to write alongside "ome" in zarr.json.

None
chunks tuple[int, ...] | 'auto' | None

Chunk shape. See write_image for details.

'auto'
shards tuple[int, ...] | None

Shard shape for Zarr v3 sharding. Default is None (no sharding). When present, shard_shape must be divisible by chunk shape.

None
writer 'zarr' | 'tensorstore' | 'auto' | CreateArrayFunc

Backend for creating arrays. When you specify "zarr" or "tensorstore", the return type is narrowed to the specific array type.

'auto'
overwrite bool

If True, overwrite existing Zarr group. Default is False.

False
compression 'blosc-zstd' | 'blosc-lz4' | 'zstd' | 'none'

Compression codec. Default is "blosc-zstd".

'blosc-zstd'

Returns:

Type Description
tuple[Path, dict[str, Array]]

A tuple of (path, arrays) where arrays maps dataset paths (e.g., "0") to array objects. The array type depends on the writer:

  • writer="zarr": Returns dict[str, zarr.Array]
  • writer="tensorstore": Returns dict[str, tensorstore.TensorStore]
  • writer="auto": Returns whichever is available

Raises:

Type Description
NotImplementedError

If the Image model has multiple multiscales.

ValueError

If the number of dataset specs doesn't match the metadata.

FileExistsError

If dest exists and overwrite is False.

ImportError

If no suitable writer backend is installed.

Examples:

Create arrays and write data in chunks - single dataset, no list needed:

>>> import numpy as np
>>> from yaozarrs import v05
>>> from yaozarrs.write.v05 import prepare_image
>>> image = v05.Image(
...     multiscales=[
...         v05.Multiscale(
...             axes=[
...                 v05.SpaceAxis(name="y", unit="micrometer"),
...                 v05.SpaceAxis(name="x", unit="micrometer"),
...             ],
...             datasets=[
...                 v05.Dataset(
...                     path="0",
...                     coordinateTransformations=[
...                         v05.ScaleTransformation(scale=[0.5, 0.5])
...                     ],
...                 )
...             ],
...         )
...     ]
... )
>>> # Prepare with just shape/dtype (no data yet) - no list wrapping!
>>> path, arrays = prepare_image("prepared.zarr", image, ((64, 64), "uint16"))
>>> arrays["0"][:] = np.zeros((64, 64), dtype=np.uint16)
>>> assert path.exists()
Source code in src/yaozarrs/write/v05/_write.py
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
def prepare_image(
    dest: str | PathLike,
    image: Image,
    datasets: ShapeAndDTypeOrPyramid,
    *,
    extra_attributes: dict[str, Any] | None = None,
    chunks: tuple[int, ...] | Literal["auto"] | None = "auto",
    shards: tuple[int, ...] | None = None,
    writer: ZarrWriter = "auto",
    overwrite: bool = False,
    compression: CompressionName = "blosc-zstd",
) -> tuple[Path, dict[str, Any]]:
    """Create OME-Zarr v0.5 Image structure and return array handles for writing.

    This is a lower-level function that creates the Zarr group hierarchy and
    empty arrays, but does not write data. Use this when you need custom control
    over how data is written (e.g., chunk-by-chunk streaming, parallel writes).

    To write data immediately, use `write_image` instead.

    See Also
    --------
    - [`write_image`][yaozarrs.write.v05.write_image] : High-level function that writes
      data immediately.
    - [`Bf2RawBuilder.prepare`][yaozarrs.write.v05.Bf2RawBuilder.prepare] : Prepare
      multiple series at once.

    Parameters
    ----------
    dest : str | PathLike
        Destination path for the Zarr group.
    image : Image
        OME-Zarr Image metadata model.
    datasets : ShapeAndDType | Sequence[ShapeAndDType]
        Shape and dtype specification(s) for each dataset, as `(shape, dtype)`
        tuples. Can be:

        - Single `(shape, dtype)`: For one dataset, no wrapping needed
        - Sequence of `(shape, dtype)`: For multiple datasets (multiscale pyramid)

        Must match the number and order of `image.multiscales[0].datasets`.
    extra_attributes : dict[str, Any] | None, optional
        Additional attributes to write alongside "ome" in zarr.json.
    chunks : tuple[int, ...] | "auto" | None, optional
        Chunk shape. See `write_image` for details.
    shards : tuple[int, ...] | None, optional
        Shard shape for Zarr v3 sharding. Default is None (no sharding).
        When present, shard_shape must be divisible by chunk shape.
    writer : "zarr" | "tensorstore" | "auto" | CreateArrayFunc, optional
        Backend for creating arrays. When you specify "zarr" or "tensorstore",
        the return type is narrowed to the specific array type.
    overwrite : bool, optional
        If True, overwrite existing Zarr group. Default is False.
    compression : "blosc-zstd" | "blosc-lz4" | "zstd" | "none", optional
        Compression codec. Default is "blosc-zstd".

    Returns
    -------
    tuple[Path, dict[str, Array]]
        A tuple of (path, arrays) where `arrays` maps dataset paths (e.g., "0")
        to array objects. The array type depends on the writer:

        - `writer="zarr"`: Returns `dict[str, zarr.Array]`
        - `writer="tensorstore"`: Returns `dict[str, tensorstore.TensorStore]`
        - `writer="auto"`: Returns whichever is available

    Raises
    ------
    NotImplementedError
        If the Image model has multiple multiscales.
    ValueError
        If the number of dataset specs doesn't match the metadata.
    FileExistsError
        If `dest` exists and `overwrite` is False.
    ImportError
        If no suitable writer backend is installed.

    Examples
    --------
    Create arrays and write data in chunks - single dataset, no list needed:

    >>> import numpy as np
    >>> from yaozarrs import v05
    >>> from yaozarrs.write.v05 import prepare_image
    >>> image = v05.Image(
    ...     multiscales=[
    ...         v05.Multiscale(
    ...             axes=[
    ...                 v05.SpaceAxis(name="y", unit="micrometer"),
    ...                 v05.SpaceAxis(name="x", unit="micrometer"),
    ...             ],
    ...             datasets=[
    ...                 v05.Dataset(
    ...                     path="0",
    ...                     coordinateTransformations=[
    ...                         v05.ScaleTransformation(scale=[0.5, 0.5])
    ...                     ],
    ...                 )
    ...             ],
    ...         )
    ...     ]
    ... )
    >>> # Prepare with just shape/dtype (no data yet) - no list wrapping!
    >>> path, arrays = prepare_image("prepared.zarr", image, ((64, 64), "uint16"))
    >>> arrays["0"][:] = np.zeros((64, 64), dtype=np.uint16)
    >>> assert path.exists()
    """
    if len(image.multiscales) != 1:
        raise NotImplementedError("Image must have exactly one multiscale")

    multiscale = image.multiscales[0]

    # Normalize to sequence: single (shape, dtype) tuple -> list
    datasets_seq: Sequence[ShapeAndDType]
    if (
        isinstance(datasets, tuple)
        and len(datasets) == 2
        and isinstance(datasets[0], tuple)  # shape is first element
    ):
        datasets_seq = [datasets]  # type: ignore[list-item]
    else:
        datasets_seq = datasets  # type: ignore[assignment]

    if len(datasets_seq) != len(multiscale.datasets):
        raise ValueError(
            f"Number of dataset specs ({len(datasets_seq)}) must match "
            f"number of datasets in metadata ({len(multiscale.datasets)})"
        )

    # Get create function
    create_func = _get_create_func(writer)

    # Create zarr group with Image metadata
    dest_path = Path(dest)
    _create_zarr3_group(dest_path, image, overwrite, extra_attributes=extra_attributes)

    dimension_names = [ax.name for ax in multiscale.axes]

    # Create arrays for each dataset
    arrays = {}
    for (shape, dtype_spec), dataset_meta in zip(datasets_seq, multiscale.datasets):
        # Convert dtype to np.dtype to ensure compatibility with all backends
        import numpy as np

        dtype = np.dtype(dtype_spec)
        arrays[dataset_meta.path] = create_func(
            path=dest_path / dataset_meta.path,
            shape=shape,
            dtype=dtype,
            chunks=_resolve_chunks(shape, dtype, chunks),
            shards=shards,
            dimension_names=dimension_names,
            overwrite=overwrite,
            compression=compression,
        )

    return dest_path, arrays

Custom Writers#

Advanced

This is an advanced feature, not needed for most users. You can skip this section if you are happy using the built-in zarr or tensorstore implementations.

CreateArrayFunc #

Protocol for custom array creation functions.

This is the type signature for functions that can be passed as the writer parameter to many functions in this module. Use this if you'd like to completely customize how arrays are created, e.g., using a different backend, or custom storage options. See _create_array_zarr and _create_array_tensorstore for (our internal) reference implementations.

__call__ #

__call__(
    path: Path,
    shape: tuple[int, ...],
    dtype: Any,
    chunks: tuple[int, ...],
    *,
    shards: tuple[int, ...] | None,
    overwrite: bool,
    compression: CompressionName,
    dimension_names: list[str] | None,
) -> Any

Create array structure without writing data.

Parameters:

Name Type Description Default
path Path

Path to create array

required
shape tuple[int, ...]

Array shape

required
dtype dtype

Data type

required
chunks tuple[int, ...]

Chunk shape (already resolved by yaozarrs)

required
shards tuple[int, ...] | None

Shard shape for Zarr v3 sharding, or None

required
dimension_names list[str] | None

Names for each dimension

required
overwrite bool

Whether to overwrite existing array

required
compression 'blosc-zstd' | 'blosc-lz4' | 'zstd' | 'none'

Compression codec to use

required

Returns:

Type Description
Any

Array object that supports numpy-style indexing for writing (e.g., zarr.Array or tensorstore.TensorStore).