diff --git a/src/datasource/zarr/README.md b/src/datasource/zarr/README.md index 1ff114f760..ae75ddd274 100644 --- a/src/datasource/zarr/README.md +++ b/src/datasource/zarr/README.md @@ -54,6 +54,14 @@ Dimension names may be specified using an `_ARRAY_DIMENSIONS` attribute, ## Zarr v3 +Supported chunk grids: + +- regular +- regular_bounded (a regular grid with an `origin` and a `lower_bound`, under which chunk keys + may be negative and the first chunk may be partly filled). A bare array is displayed in its own + coordinates, starting at `lower_bound`. Inside an OME-Zarr multiscale group each level's + `translation` refers to its first element, as OME-NGFF defines the array index space. + Supported codecs: - crc32c (not validated) diff --git a/src/datasource/zarr/frontend.ts b/src/datasource/zarr/frontend.ts index 9197cdebe5..b244835a94 100644 --- a/src/datasource/zarr/frontend.ts +++ b/src/datasource/zarr/frontend.ts @@ -119,18 +119,25 @@ export class MultiscaleVolumeChunkSource extends GenericMultiscaleVolumeChunkSou return transposeNestedArrays( this.multiscale.scales.map((scale) => { const { metadata } = scale; - const { rank, codecs, shape } = metadata; + const { rank, codecs, shape, chunkGridOrigin, lowerBound } = metadata; const readChunkShape = codecs.layoutInfo[0].readChunkShape; const { physicalToLogicalDimension } = metadata.codecs.layoutInfo[0]; const permutedChunkShape = new Uint32Array(rank); - const permutedDataShape = new Float32Array(rank); + const permutedLowerBound = new Float32Array(rank); + const permutedUpperBound = new Float32Array(rank); const orderTransform = new Float32Array((rank + 1) ** 2); orderTransform[(rank + 1) ** 2 - 1] = 1; + // Chunk coordinates are relative to the chunk grid origin; the index + // space the scale transform maps starts at the first element. for (let i = 0; i < rank; ++i) { const decodedDim = physicalToLogicalDimension[rank - 1 - i]; permutedChunkShape[i] = readChunkShape[decodedDim]; - permutedDataShape[i] = shape[decodedDim]; + permutedLowerBound[i] = + lowerBound[decodedDim] - chunkGridOrigin[decodedDim]; + permutedUpperBound[i] = permutedLowerBound[i] + shape[decodedDim]; orderTransform[decodedDim + i * (rank + 1)] = 1; + orderTransform[(rank + 1) * rank + i] = + chunkGridOrigin[i] - lowerBound[i]; } const transform = new Float32Array((rank + 1) ** 2); matrix.multiply( @@ -148,7 +155,8 @@ export class MultiscaleVolumeChunkSource extends GenericMultiscaleVolumeChunkSou rank, chunkToMultiscaleTransform: transform, dataType: metadata.dataType, - upperVoxelBound: permutedDataShape, + lowerVoxelBound: permutedLowerBound, + upperVoxelBound: permutedUpperBound, volumeType: this.volumeType, chunkDataSizes: [permutedChunkShape], volumeSourceOptions, @@ -275,23 +283,28 @@ function getMultiscaleInfoForSingleArray( units: Array.from(unitsAndScales, (x) => x.unit), boundingBoxes: [ makeIdentityTransformedBoundingBox({ - lowerBounds: new Float64Array(metadata.rank), - upperBounds: Float64Array.from(metadata.shape), + lowerBounds: Float64Array.from(metadata.lowerBound), + upperBounds: Float64Array.from( + metadata.shape, + (n, i) => metadata.lowerBound[i] + n, + ), }), ], }); - const transform = matrix.createIdentity(Float64Array, metadata.rank + 1); return { coordinateSpace: modelSpace, dataType: metadata.dataType, scales: [ { url, - transform, + transform: matrix.createHomogeneousTranslationMatrix( + Float64Array, + metadata.lowerBound, + ), metadata, }, ], - baseTransform: transform, + baseTransform: matrix.createIdentity(Float64Array, metadata.rank + 1), }; } diff --git a/src/datasource/zarr/metadata/index.ts b/src/datasource/zarr/metadata/index.ts index 0cfae9bb96..b22912e582 100644 --- a/src/datasource/zarr/metadata/index.ts +++ b/src/datasource/zarr/metadata/index.ts @@ -39,6 +39,10 @@ export interface ArrayMetadata { userAttributes: Record; dimensionSeparator: DimensionSeparator; chunkKeyEncoding: ChunkKeyEncoding; + // Array coordinate where chunk 0 starts, and coordinate of the first + // element; all zero except under the `regular_bounded` chunk grid. + chunkGridOrigin: number[]; + lowerBound: number[]; codecs: CodecChainSpec; } diff --git a/src/datasource/zarr/metadata/parse.ts b/src/datasource/zarr/metadata/parse.ts index c77e710772..3d69996c01 100644 --- a/src/datasource/zarr/metadata/parse.ts +++ b/src/datasource/zarr/metadata/parse.ts @@ -62,6 +62,20 @@ export function parseChunkShape(obj: unknown, rank: number): number[] { }); } +enum ChunkGrid { + REGULAR = 0, + REGULAR_BOUNDED = 1, +} + +function verifyInteger(value: unknown): number { + if (!Number.isInteger(value)) { + throw new Error( + `Expected integer, but received: ${JSON.stringify(value)}.`, + ); + } + return value as number; +} + export function parseDimensionSeparator(value: unknown): "/" | "." { if (value !== "." && value !== "/") { throw new Error( @@ -225,18 +239,40 @@ export function parseV3Metadata( verifyEnumString(x, DataType, /^[a-z0-9]+$/), ); - const { configuration: chunkShape } = verifyObjectProperty( - obj, - "chunk_grid", - (chunkGrid) => - parseNameAndConfiguration( - chunkGrid, - (name) => verifyConstant(name, "regular"), - (configuration) => - verifyObjectProperty(configuration, "chunk_shape", (chunks) => - parseChunkShape(chunks, rank), + const { + configuration: { chunkShape, chunkGridOrigin, lowerBound }, + } = verifyObjectProperty(obj, "chunk_grid", (chunkGrid) => + parseNameAndConfiguration( + chunkGrid, + (name) => + verifyEnumString(name, ChunkGrid, /^(regular|regular_bounded)$/), + (configuration, name) => { + const chunkShape = verifyObjectProperty( + configuration, + "chunk_shape", + (chunks) => parseChunkShape(chunks, rank), + ); + if (name === ChunkGrid.REGULAR) { + const zeros = new Array(rank).fill(0); + return { chunkShape, chunkGridOrigin: zeros, lowerBound: zeros }; + } + const parseCoordinates = (x: unknown) => + parseFixedLengthArray(new Array(rank), x, verifyInteger); + return { + chunkShape, + chunkGridOrigin: verifyObjectProperty( + configuration, + "origin", + parseCoordinates, ), - ), + lowerBound: verifyObjectProperty( + configuration, + "lower_bound", + parseCoordinates, + ), + }; + }, + ), ); const { userAttributes, dimensionUnits } = verifyObjectProperty( @@ -291,6 +327,8 @@ export function parseV3Metadata( dimensionUnits, chunkKeyEncoding, dimensionSeparator, + chunkGridOrigin, + lowerBound, userAttributes, codecs, }; @@ -436,6 +474,8 @@ export function parseV2Metadata( userAttributes: attrs, dimensionSeparator, chunkKeyEncoding: ChunkKeyEncoding.V2, + chunkGridOrigin: new Array(rank).fill(0), + lowerBound: new Array(rank).fill(0), codecs: codecChainSpec, }; } catch (e) { diff --git a/src/datasource/zarr/metadata/regular_bounded.spec.ts b/src/datasource/zarr/metadata/regular_bounded.spec.ts new file mode 100644 index 0000000000..07c50087e8 --- /dev/null +++ b/src/datasource/zarr/metadata/regular_bounded.spec.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright 2026 Google Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from "vitest"; +import "#src/datasource/zarr/codec/bytes/resolve.js"; +import type { ArrayMetadata } from "#src/datasource/zarr/metadata/index.js"; +import { parseV3Metadata } from "#src/datasource/zarr/metadata/parse.js"; + +function parseArray(chunkGrid: unknown): ArrayMetadata { + const metadata = parseV3Metadata( + { + zarr_format: 3, + node_type: "array", + shape: [6, 7], + data_type: "uint16", + chunk_grid: chunkGrid, + chunk_key_encoding: { name: "default" }, + fill_value: 0, + codecs: [{ name: "bytes", configuration: { endian: "little" } }], + }, + "array", + ); + if (metadata.nodeType !== "array") throw new Error("expected an array"); + return metadata; +} + +describe("regular_bounded chunk grid", () => { + it("parses the origin and lower bound", () => { + const metadata = parseArray({ + name: "regular_bounded", + configuration: { + chunk_shape: [4, 5], + origin: [1, 0], + lower_bound: [-3, -2], + }, + }); + expect(metadata.chunkShape).toEqual([4, 5]); + expect(metadata.chunkGridOrigin).toEqual([1, 0]); + expect(metadata.lowerBound).toEqual([-3, -2]); + }); + + it("leaves a regular grid at zero", () => { + const metadata = parseArray({ + name: "regular", + configuration: { chunk_shape: [4, 5] }, + }); + expect(metadata.chunkGridOrigin).toEqual([0, 0]); + expect(metadata.lowerBound).toEqual([0, 0]); + }); + + it("requires one integer per dimension for both fields", () => { + const grid = (configuration: object) => ({ + name: "regular_bounded", + configuration: { chunk_shape: [4, 5], ...configuration }, + }); + expect(() => parseArray(grid({ origin: [0, 0] }))).toThrow(/lower_bound/); + expect(() => + parseArray(grid({ origin: [0], lower_bound: [0, 0] })), + ).toThrow(/length 2/); + expect(() => + parseArray(grid({ origin: [0, 0], lower_bound: [0, 0.5] })), + ).toThrow(/integer/); + }); + + it("refuses an unknown chunk grid", () => { + expect(() => + parseArray({ + name: "regular_offset", + configuration: { chunk_shape: [4, 5] }, + }), + ).toThrow(/Invalid enum/); + }); +}); diff --git a/testdata/datasource/zarr/ome_zarr/generate_regular_bounded_0.5.py b/testdata/datasource/zarr/ome_zarr/generate_regular_bounded_0.5.py new file mode 100644 index 0000000000..345a64c180 --- /dev/null +++ b/testdata/datasource/zarr/ome_zarr/generate_regular_bounded_0.5.py @@ -0,0 +1,84 @@ +"""Writes `regular_bounded_0.5`: the `simple_0.5` image grown in front by 3 rows and 2 columns. + +Requires a zarr-python with `Array.prepend` (zarr-developers/zarr-python#4516). Both levels are +grown with `prepend`, and each level's `translation` moves to its new first element, as OME-NGFF +defines the array index space, so the original elements keep their physical positions. +""" + +import json +import pathlib +import shutil + +import numpy as np +import zarr + +root = pathlib.Path(__file__).parent / "regular_bounded_0.5" +shutil.rmtree(root, ignore_errors=True) +root.mkdir() + + +def level(path, data): + a = zarr.create_array( + store=str(root / path), + shape=data.shape, + chunks=(5, 5), + dtype="uint16", + fill_value=0, + compressors=None, + dimension_names=["y", "x"], + ) + a[:] = data + return a + + +# Level 0: 10 x 10 at 4 x 30 nm, then rows [-3, 0) and columns [-2, 0) in front. +s0 = level("scale0/image", np.arange(100, dtype="uint16").reshape(10, 10)) +s0.prepend(np.arange(30, dtype="uint16").reshape(3, 10) + 1000, axis=0) +s0.prepend(np.arange(26, dtype="uint16").reshape(13, 2) + 2000, axis=1) + +# Level 1: 2 x 2 bins on even level-0 coordinates: rows [-4, 10), columns [-2, 10). +grown = np.zeros((14, 12), dtype="float64") +grown[1:] = s0[:] +binned = grown.reshape(7, 2, 6, 2).mean(axis=(1, 3)).astype("uint16") +s1 = level("scale1/image", binned[2:, 1:]) +s1.prepend(binned[:2, 1:], axis=0) +s1.prepend(binned[:, :1], axis=1) + +# Translations are voxel centres of each level's first element: +# level 0 row -3 -> 4 * -3 = -12 nm, column -2 -> 30 * -2 = -60 nm; +# level 1 rows {-4, -3} -> -14 nm, columns {-2, -1} -> -45 nm. +ome = { + "version": "0.5", + "multiscales": [ + { + "name": "image", + "axes": [ + {"name": "y", "type": "space", "unit": "nanometer"}, + {"name": "x", "type": "space", "unit": "nanometer"}, + ], + "datasets": [ + { + "path": "scale0/image", + "coordinateTransformations": [ + {"type": "scale", "scale": [4, 30]}, + {"type": "translation", "translation": [-12, -60]}, + ], + }, + { + "path": "scale1/image", + "coordinateTransformations": [ + {"type": "scale", "scale": [8, 60]}, + {"type": "translation", "translation": [-14, -45]}, + ], + }, + ], + } + ], +} +for group in (root, root / "scale0", root / "scale1"): + attrs = {"ome": ome} if group == root else {} + (group / "zarr.json").write_text( + json.dumps( + {"zarr_format": 3, "node_type": "group", "attributes": attrs}, indent=2 + ) + ) diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/-1/-1 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/-1/-1 new file mode 100644 index 0000000000..8e18d62796 Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/-1/-1 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/-1/0 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/-1/0 new file mode 100644 index 0000000000..e695834cc9 Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/-1/0 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/-1/1 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/-1/1 new file mode 100644 index 0000000000..9afbbbe5d7 Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/-1/1 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/0/-1 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/0/-1 new file mode 100644 index 0000000000..54d3348d51 Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/0/-1 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/0/0 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/0/0 new file mode 100644 index 0000000000..ea25fa73da Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/0/0 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/0/1 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/0/1 new file mode 100644 index 0000000000..99f96e0ba7 Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/0/1 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/1/-1 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/1/-1 new file mode 100644 index 0000000000..34c7713357 Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/1/-1 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/1/0 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/1/0 new file mode 100644 index 0000000000..0c3d4f045f Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/1/0 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/1/1 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/1/1 new file mode 100644 index 0000000000..3f52816fe0 Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/c/1/1 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/zarr.json b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/zarr.json new file mode 100644 index 0000000000..342ecd9bba --- /dev/null +++ b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/zarr.json @@ -0,0 +1,47 @@ +{ + "shape": [ + 13, + 12 + ], + "data_type": "uint16", + "chunk_grid": { + "name": "regular_bounded", + "configuration": { + "chunk_shape": [ + 5, + 5 + ], + "origin": [ + 0, + 0 + ], + "lower_bound": [ + -3, + -2 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": {}, + "dimension_names": [ + "y", + "x" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/zarr.json b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/zarr.json new file mode 100644 index 0000000000..051870a8e5 --- /dev/null +++ b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/zarr.json @@ -0,0 +1,5 @@ +{ + "zarr_format": 3, + "node_type": "group", + "attributes": {} +} \ No newline at end of file diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/-1/-1 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/-1/-1 new file mode 100644 index 0000000000..47883e7f4d Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/-1/-1 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/-1/0 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/-1/0 new file mode 100644 index 0000000000..e4e460c673 Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/-1/0 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/0/-1 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/0/-1 new file mode 100644 index 0000000000..4f5035a6b3 Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/0/-1 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/0/0 b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/0/0 new file mode 100644 index 0000000000..d8f039ace3 Binary files /dev/null and b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/c/0/0 differ diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/zarr.json b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/zarr.json new file mode 100644 index 0000000000..c70682f1d4 --- /dev/null +++ b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/zarr.json @@ -0,0 +1,47 @@ +{ + "shape": [ + 7, + 6 + ], + "data_type": "uint16", + "chunk_grid": { + "name": "regular_bounded", + "configuration": { + "chunk_shape": [ + 5, + 5 + ], + "origin": [ + 0, + 0 + ], + "lower_bound": [ + -2, + -1 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": {}, + "dimension_names": [ + "y", + "x" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/zarr.json b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/zarr.json new file mode 100644 index 0000000000..051870a8e5 --- /dev/null +++ b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/zarr.json @@ -0,0 +1,5 @@ +{ + "zarr_format": 3, + "node_type": "group", + "attributes": {} +} \ No newline at end of file diff --git a/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/zarr.json b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/zarr.json new file mode 100644 index 0000000000..3d1620988b --- /dev/null +++ b/testdata/datasource/zarr/ome_zarr/regular_bounded_0.5/zarr.json @@ -0,0 +1,66 @@ +{ + "zarr_format": 3, + "node_type": "group", + "attributes": { + "ome": { + "version": "0.5", + "multiscales": [ + { + "name": "image", + "axes": [ + { + "name": "y", + "type": "space", + "unit": "nanometer" + }, + { + "name": "x", + "type": "space", + "unit": "nanometer" + } + ], + "datasets": [ + { + "path": "scale0/image", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 4, + 30 + ] + }, + { + "type": "translation", + "translation": [ + -12, + -60 + ] + } + ] + }, + { + "path": "scale1/image", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 8, + 60 + ] + }, + { + "type": "translation", + "translation": [ + -14, + -45 + ] + } + ] + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/-1/-1 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/-1/-1 new file mode 100644 index 0000000000..df77d40c30 Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/-1/-1 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/-1/0 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/-1/0 new file mode 100644 index 0000000000..1b807ba68b Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/-1/0 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/-1/1 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/-1/1 new file mode 100644 index 0000000000..5d26c14511 Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/-1/1 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/0/-1 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/0/-1 new file mode 100644 index 0000000000..59f9272f15 Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/0/-1 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/0/0 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/0/0 new file mode 100644 index 0000000000..7abc66d5c8 Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/0/0 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/0/1 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/0/1 new file mode 100644 index 0000000000..83836c01fb Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/0/1 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/1/-1 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/1/-1 new file mode 100644 index 0000000000..1a663dbdc2 Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/1/-1 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/1/0 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/1/0 new file mode 100644 index 0000000000..d969e65c85 Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/1/0 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/1/1 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/1/1 new file mode 100644 index 0000000000..015e4c72ff Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/c/1/1 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/zarr.json b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/zarr.json new file mode 100644 index 0000000000..d07af7c355 --- /dev/null +++ b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded/zarr.json @@ -0,0 +1,43 @@ +{ + "shape": [ + 9, + 9 + ], + "data_type": "uint16", + "chunk_grid": { + "name": "regular_bounded", + "configuration": { + "chunk_shape": [ + 4, + 5 + ], + "origin": [ + 0, + 0 + ], + "lower_bound": [ + -3, + -2 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/-1/0 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/-1/0 new file mode 100644 index 0000000000..0e79de19f5 Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/-1/0 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/-1/1 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/-1/1 new file mode 100644 index 0000000000..59d563b714 Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/-1/1 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/0/0 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/0/0 new file mode 100644 index 0000000000..f067223587 Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/0/0 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/0/1 b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/0/1 new file mode 100644 index 0000000000..9fd4d41284 Binary files /dev/null and b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/c/0/1 differ diff --git a/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/zarr.json b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/zarr.json new file mode 100644 index 0000000000..3e26baa863 --- /dev/null +++ b/testdata/datasource/zarr/zarr_v3/examples/regular_bounded_origin/zarr.json @@ -0,0 +1,43 @@ +{ + "shape": [ + 3, + 4 + ], + "data_type": "uint16", + "chunk_grid": { + "name": "regular_bounded", + "configuration": { + "chunk_shape": [ + 2, + 3 + ], + "origin": [ + 1, + -1 + ], + "lower_bound": [ + -1, + 0 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": {}, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/testdata/datasource/zarr/zarr_v3/generate_regular_bounded.py b/testdata/datasource/zarr/zarr_v3/generate_regular_bounded.py new file mode 100644 index 0000000000..51dd1d18e9 --- /dev/null +++ b/testdata/datasource/zarr/zarr_v3/generate_regular_bounded.py @@ -0,0 +1,47 @@ +"""Writes `examples/regular_bounded`: a 6 x 7 array grown in front by 3 rows and 2 columns. + +Requires a zarr-python with `Array.prepend` (zarr-developers/zarr-python#4516). +""" + +import json +import pathlib +import shutil + +import numpy as np +import zarr + +out = pathlib.Path(__file__).parent / "examples" / "regular_bounded" +shutil.rmtree(out, ignore_errors=True) +z = zarr.create_array( + store=str(out), + shape=(6, 7), + chunks=(4, 5), + dtype="uint16", + fill_value=0, + compressors=None, +) +z[:] = np.arange(42, dtype="uint16").reshape(6, 7) + 100 +z.prepend(np.arange(21, dtype="uint16").reshape(3, 7) + 1, axis=0) +z.prepend(np.full((9, 2), 900, dtype="uint16"), axis=1) + +# `examples/regular_bounded_origin`: chunk 0 starts at (1, -1), the first element at (-1, 0). + +out = pathlib.Path(__file__).parent / "examples" / "regular_bounded_origin" +shutil.rmtree(out, ignore_errors=True) +zarr.create_array( + store=str(out), + shape=(3, 4), + chunks=(2, 3), + dtype="uint16", + fill_value=0, + compressors=None, +) +meta = json.loads((out / "zarr.json").read_text()) +meta["chunk_grid"] = { + "name": "regular_bounded", + "configuration": {"chunk_shape": [2, 3], "origin": [1, -1], "lower_bound": [-1, 0]}, +} +(out / "zarr.json").write_text(json.dumps(meta, indent=2)) +zarr.open_array(str(out), mode="r+")[:] = ( + np.arange(12, dtype="uint16").reshape(3, 4) + 1 +) diff --git a/tests/datasource/metadata_snapshots/zarr/ome_zarr_regular_bounded_0.5.snapshot b/tests/datasource/metadata_snapshots/zarr/ome_zarr_regular_bounded_0.5.snapshot new file mode 100644 index 0000000000..5f413f1c74 --- /dev/null +++ b/tests/datasource/metadata_snapshots/zarr/ome_zarr_regular_bounded_0.5.snapshot @@ -0,0 +1,164 @@ +{ + "canonicalUrl": "http://localhost:*/datasource/zarr/ome_zarr/regular_bounded_0.5/|zarr3:", + "modelTransform": { + "inputSpace": { + "bounds": { + "lowerBounds": Float64Array [ + -3.5, + -2.5, + ], + "upperBounds": Float64Array [ + 9.5, + 9.5, + ], + "voxelCenterAtIntegerCoordinates": [ + true, + true, + ], + }, + "coordinateArrays": [ + , + , + ], + "names": [ + "y", + "x", + ], + "scales": Float64Array [ + 4e-9, + 3.0000000000000004e-8, + ], + "units": [ + "m", + "m", + ], + "valid": true, + }, + }, + "subsources": [ + { + "default": true, + "id": "default", + "subsource": { + "volume": { + "dataType": "UINT16", + "rank": 2, + "sources": [ + [ + { + "chunkSource": { + "parameters": { + "url": "http://localhost:*/datasource/zarr/ome_zarr/regular_bounded_0.5/scale0/image/", + }, + "spec": { + "baseVoxelOffset": [ + 0, + 0, + ], + "chunkDataSize": [ + 5, + 5, + ], + "compressedSegmentationBlockSize": undefined, + "dataType": "UINT16", + "lowerVoxelBound": [ + -2, + -3, + ], + "upperVoxelBound": [ + 10, + 10, + ], + }, + }, + "chunkToMultiscaleTransform": [ + [ + 0, + 1, + -0.5, + ], + [ + 1, + 0, + -0.5, + ], + [ + 0, + 0, + 1, + ], + ], + }, + { + "chunkSource": { + "parameters": { + "url": "http://localhost:*/datasource/zarr/ome_zarr/regular_bounded_0.5/scale1/image/", + }, + "spec": { + "baseVoxelOffset": [ + 0, + 0, + ], + "chunkDataSize": [ + 5, + 5, + ], + "compressedSegmentationBlockSize": undefined, + "dataType": "UINT16", + "lowerVoxelBound": [ + -1, + -2, + ], + "upperVoxelBound": [ + 5, + 5, + ], + }, + }, + "chunkToMultiscaleTransform": [ + [ + 0, + 2, + -0.5, + ], + [ + 2, + 0, + -0.5, + ], + [ + 0, + 0, + 1, + ], + ], + }, + ], + ], + "volumeType": "IMAGE", + }, + }, + }, + { + "default": true, + "id": "bounds", + "subsource": { + "staticAnnotations": [ + { + "description": "Data Bounds", + "id": "data-bounds", + "pointA": [ + -3.5, + -2.5, + ], + "pointB": [ + 9.5, + 9.5, + ], + "type": "axis_aligned_bounding_box", + }, + ], + }, + }, + ], +} \ No newline at end of file diff --git a/tests/datasource/metadata_snapshots/zarr/zarr_v3_examples_regular_bounded.snapshot b/tests/datasource/metadata_snapshots/zarr/zarr_v3_examples_regular_bounded.snapshot new file mode 100644 index 0000000000..47560b876f --- /dev/null +++ b/tests/datasource/metadata_snapshots/zarr/zarr_v3_examples_regular_bounded.snapshot @@ -0,0 +1,120 @@ +{ + "canonicalUrl": "http://localhost:*/datasource/zarr/zarr_v3/examples/regular_bounded/|zarr3:", + "modelTransform": { + "inputSpace": { + "bounds": { + "lowerBounds": Float64Array [ + -3, + -2, + ], + "upperBounds": Float64Array [ + 6, + 7, + ], + "voxelCenterAtIntegerCoordinates": [ + false, + false, + ], + }, + "coordinateArrays": [ + , + , + ], + "names": [ + "dim_0", + "dim_1", + ], + "scales": Float64Array [ + 1, + 1, + ], + "units": [ + "", + "", + ], + "valid": true, + }, + }, + "subsources": [ + { + "default": true, + "id": "default", + "subsource": { + "volume": { + "dataType": "UINT16", + "rank": 2, + "sources": [ + [ + { + "chunkSource": { + "parameters": { + "url": "http://localhost:*/datasource/zarr/zarr_v3/examples/regular_bounded/", + }, + "spec": { + "baseVoxelOffset": [ + 0, + 0, + ], + "chunkDataSize": [ + 5, + 4, + ], + "compressedSegmentationBlockSize": undefined, + "dataType": "UINT16", + "lowerVoxelBound": [ + -2, + -3, + ], + "upperVoxelBound": [ + 7, + 6, + ], + }, + }, + "chunkToMultiscaleTransform": [ + [ + 0, + 1, + 0, + ], + [ + 1, + 0, + 0, + ], + [ + 0, + 0, + 1, + ], + ], + }, + ], + ], + "volumeType": "IMAGE", + }, + }, + }, + { + "default": true, + "id": "bounds", + "subsource": { + "staticAnnotations": [ + { + "description": "Data Bounds", + "id": "data-bounds", + "pointA": [ + -3, + -2, + ], + "pointB": [ + 6, + 7, + ], + "type": "axis_aligned_bounding_box", + }, + ], + }, + }, + ], +} \ No newline at end of file diff --git a/tests/datasource/metadata_snapshots/zarr/zarr_v3_examples_regular_bounded_origin.snapshot b/tests/datasource/metadata_snapshots/zarr/zarr_v3_examples_regular_bounded_origin.snapshot new file mode 100644 index 0000000000..9882085e22 --- /dev/null +++ b/tests/datasource/metadata_snapshots/zarr/zarr_v3_examples_regular_bounded_origin.snapshot @@ -0,0 +1,120 @@ +{ + "canonicalUrl": "http://localhost:*/datasource/zarr/zarr_v3/examples/regular_bounded_origin/|zarr3:", + "modelTransform": { + "inputSpace": { + "bounds": { + "lowerBounds": Float64Array [ + -1, + 0, + ], + "upperBounds": Float64Array [ + 2, + 4, + ], + "voxelCenterAtIntegerCoordinates": [ + false, + false, + ], + }, + "coordinateArrays": [ + , + , + ], + "names": [ + "dim_0", + "dim_1", + ], + "scales": Float64Array [ + 1, + 1, + ], + "units": [ + "", + "", + ], + "valid": true, + }, + }, + "subsources": [ + { + "default": true, + "id": "default", + "subsource": { + "volume": { + "dataType": "UINT16", + "rank": 2, + "sources": [ + [ + { + "chunkSource": { + "parameters": { + "url": "http://localhost:*/datasource/zarr/zarr_v3/examples/regular_bounded_origin/", + }, + "spec": { + "baseVoxelOffset": [ + 0, + 0, + ], + "chunkDataSize": [ + 3, + 2, + ], + "compressedSegmentationBlockSize": undefined, + "dataType": "UINT16", + "lowerVoxelBound": [ + 1, + -2, + ], + "upperVoxelBound": [ + 5, + 1, + ], + }, + }, + "chunkToMultiscaleTransform": [ + [ + 0, + 1, + 1, + ], + [ + 1, + 0, + -1, + ], + [ + 0, + 0, + 1, + ], + ], + }, + ], + ], + "volumeType": "IMAGE", + }, + }, + }, + { + "default": true, + "id": "bounds", + "subsource": { + "staticAnnotations": [ + { + "description": "Data Bounds", + "id": "data-bounds", + "pointA": [ + -1, + 0, + ], + "pointB": [ + 2, + 4, + ], + "type": "axis_aligned_bounding_box", + }, + ], + }, + }, + ], +} \ No newline at end of file diff --git a/tests/datasource/zarr.browser_test.ts b/tests/datasource/zarr.browser_test.ts index b3ebcb33fd..810bd7e112 100644 --- a/tests/datasource/zarr.browser_test.ts +++ b/tests/datasource/zarr.browser_test.ts @@ -24,10 +24,13 @@ import { datasourceMetadataSnapshotTests } from "#tests/datasource/metadata_snap datasourceMetadataSnapshotTests("zarr", [ "zarr_v3/examples/single_res", "zarr_v3/examples/transpose", + "zarr_v3/examples/regular_bounded", + "zarr_v3/examples/regular_bounded_origin", "ome_zarr/simple_0.4", "ome_zarr/simple_0.5", "ome_zarr/simple_0.5.zip", "ome_zarr/simple_0.5.ocdbt", + "ome_zarr/regular_bounded_0.5", ]); datasourceMetadataSnapshotTests(