Skill Tracer
Skills › Scientific Agent Skills › zarr-python › chunking_and_compression.md

chunking_and_compression.md

108 lines · skills/zarr-python/references/chunking_and_compression.md at 5f2bc03

Open on GitHub
1# Chunking and compression
2
3Targets Zarr-Python 3.4.0. These examples are local synthetic roundtrips, not benchmarks.
4

Chunking and compression

Targets Zarr-Python 3.4.0. These examples are local synthetic roundtrips, not benchmarks.

Raw lines 1 to 4
5## Match layout to queries and memory
6
7A chunk is the unit decoded for a read; a shard packs multiple chunks into one stored
8object. Count the chunks touched by representative slices and benchmark latency,
9throughput, object requests, and peak memory. Approximately 1 MiB uncompressed per chunk
10is a useful starting experiment for Blosc, not a universal minimum or optimum. For
11float32, `(512, 512)` is 1 MiB. Larger cloud objects can amortize requests; sharding lets
12small read chunks coexist with larger stored objects. There is no universal 5–100 MB
13cloud optimum or fixed codec ranking.
15For row reads, `(small_rows, all_columns)` often reduces touched chunks; for column
16reads, reverse that geometry. For a full `[:, y, x]` trace, long time-axis chunks can
17help; for `[t, :, :]` images, short time-axis chunks can help. State the selection before
18claiming a layout is faster. Budget concurrent decoded chunks, output buffers, codec
19scratch memory, and compressed shard buffers; do not assume only one chunk is resident.

Match layout to queries and memory

A chunk is the unit decoded for a read; a shard packs multiple chunks into one stored object. Count the chunks touched by representative slices and benchmark latency, throughput, object requests, and peak memory. Approximately 1 MiB uncompressed per chunk is a useful starting experiment for Blosc, not a universal minimum or optimum. For float32, (512, 512) is 1 MiB. Larger cloud objects can amortize requests; sharding lets small read chunks coexist with larger stored objects. There is no universal 5–100 MB cloud optimum or fixed codec ranking.

For row reads, (small_rows, all_columns) often reduces touched chunks; for column reads, reverse that geometry. For a full [:, y, x] trace, long time-axis chunks can help; for [t, :, :] images, short time-axis chunks can help. State the selection before claiming a layout is faster. Budget concurrent decoded chunks, output buffers, codec scratch memory, and compressed shard buffers; do not assume only one chunk is resident.

Raw lines 5 to 20
21## Sharding
23```python
24import numpy as np
25import zarr
27expected = np.arange(256, dtype="i4").reshape(16, 16)
28z = zarr.create_array("sharded.zarr", data=expected, chunks=(2, 2), shards=(8, 8))
29assert z.chunks == (2, 2) and z.shards == (8, 8)
30z.oindex[[1, 3], [2, 4]] = [[1000, 1001], [1002, 1003]]
31expected[np.ix_([1, 3], [2, 4])] = [[1000, 1001], [1002, 1003]]
32np.testing.assert_array_equal(zarr.open_array("sharded.zarr", mode="r")[:], expected)
33```
35Shard dimensions must be multiples of chunk dimensions. Assign a single writer to each
36shard even when workers update different inner chunks. Partial writes may rewrite a
37shard; backend byte-range reads and coalescing affect performance. Measure with the
38actual backend rather than promising fewer requests for every selection.

Sharding

import numpy as np
import zarr

expected = np.arange(256, dtype="i4").reshape(16, 16)
z = zarr.create_array("sharded.zarr", data=expected, chunks=(2, 2), shards=(8, 8))
assert z.chunks == (2, 2) and z.shards == (8, 8)
z.oindex[[1, 3], [2, 4]] = [[1000, 1001], [1002, 1003]]
expected[np.ix_([1, 3], [2, 4])] = [[1000, 1001], [1002, 1003]]
np.testing.assert_array_equal(zarr.open_array("sharded.zarr", mode="r")[:], expected)

Shard dimensions must be multiples of chunk dimensions. Assign a single writer to each shard even when workers update different inner chunks. Partial writes may rewrite a shard; backend byte-range reads and coalescing affect performance. Measure with the actual backend rather than promising fewer requests for every selection.

Raw lines 21 to 39
40## Rectilinear chunks are experimental
42Nested per-axis lengths require an explicit opt-in. They are not ordinary Dask regular
43chunks; use `dask_array.chunksize` for a regular grid, not its nested `.chunks` tuple.
44An unsharded rectilinear array has no single uniform `Array.chunks` shape.
46```python
47import numpy as np
48import zarr
50with zarr.config.set({"array.rectilinear_chunks": True}):
51 rect = zarr.create_array(
52 "rectilinear.zarr", shape=(6, 10), dtype="f4",
53 chunks=([1, 2, 3], [5, 5]),
54 )
55 rect[:] = np.arange(60, dtype="f4").reshape(6, 10)
56 np.testing.assert_array_equal(rect[1:4], np.arange(60).reshape(6, 10)[1:4])
57```
59Validate consumer compatibility before choosing this extension. Resize uses the grid's
60own rules: explicit lengths preserve rectilinear identity even if initially uniform.

Rectilinear chunks are experimental

Nested per-axis lengths require an explicit opt-in. They are not ordinary Dask regular chunks; use dask_array.chunksize for a regular grid, not its nested .chunks tuple. An unsharded rectilinear array has no single uniform Array.chunks shape.

import numpy as np
import zarr

with zarr.config.set({"array.rectilinear_chunks": True}):
    rect = zarr.create_array(
        "rectilinear.zarr", shape=(6, 10), dtype="f4",
        chunks=([1, 2, 3], [5, 5]),
    )
    rect[:] = np.arange(60, dtype="f4").reshape(6, 10)
    np.testing.assert_array_equal(rect[1:4], np.arange(60).reshape(6, 10)[1:4])

Validate consumer compatibility before choosing this extension. Resize uses the grid's own rules: explicit lengths preserve rectilinear identity even if initially uniform.

Raw lines 40 to 61
62## Codec selection
64Format-3 numeric defaults use `BytesCodec` plus `ZstdCodec(level=0)` in this release.
65`compressors="auto"` selects defaults. `compressors=None` removes compression while
66retaining serialization. A format-3 codec must implement the relevant Zarr codec
67interface; plain format-2 `numcodecs.Blosc` is not accepted as a format-3 compressor.
69```python
70import numpy as np
71import zarr
72from zarr.codecs import BloscCodec, GzipCodec, ZstdCodec
74values = np.linspace(-1, 1, 64, dtype="f4").reshape(8, 8)
75codecs = [
76 BloscCodec(cname="zstd", clevel=5, shuffle="bitshuffle"),
77 BloscCodec(cname="lz4", clevel=1),
78 GzipCodec(level=6), ZstdCodec(level=3), None,
80for codec in codecs:
81 arr = zarr.create_array(None, data=values, chunks=(4, 4), compressors=codec)
82 np.testing.assert_array_equal(arr[:], values)
83 print(arr.compressors)
84```
86These codecs are lossless for the tested numeric data. Benchmark speed and stored bytes;
87Gzip is not inherently the best compression ratio. Available Blosc backends depend on
88the build; inspect `numcodecs.blosc.list_compressors()` instead of assuming Snappy exists.
89Lossy quantization/casting needs a declared error budget and downstream scientific
90validation; byte checksums alone cannot validate the resulting measurements.
92For format 2, use NumCodecs:
94```python
95import numpy as np
96import zarr
97from numcodecs import Blosc
99legacy = zarr.create_array(
100 "format2.zarr", data=np.arange(12, dtype="i4"), chunks=(4,), zarr_format=2,
101 compressors=Blosc(cname="zstd", clevel=5, shuffle=Blosc.BITSHUFFLE),
103np.testing.assert_array_equal(zarr.open_array("format2.zarr", mode="r")[:], np.arange(12))
104```
106Sources: [performance](https://zarr.readthedocs.io/en/stable/user-guide/performance/),
107[rectilinear example](https://zarr.readthedocs.io/en/stable/user-guide/examples/rectilinear_chunks/),
108[released array implementation](https://github.com/zarr-developers/zarr-python/blob/v3.4.0/src/zarr/core/array.py).

Codec selection

Format-3 numeric defaults use BytesCodec plus ZstdCodec(level=0) in this release. compressors="auto" selects defaults. compressors=None removes compression while retaining serialization. A format-3 codec must implement the relevant Zarr codec interface; plain format-2 numcodecs.Blosc is not accepted as a format-3 compressor.

import numpy as np
import zarr
from zarr.codecs import BloscCodec, GzipCodec, ZstdCodec

values = np.linspace(-1, 1, 64, dtype="f4").reshape(8, 8)
codecs = [
    BloscCodec(cname="zstd", clevel=5, shuffle="bitshuffle"),
    BloscCodec(cname="lz4", clevel=1),
    GzipCodec(level=6), ZstdCodec(level=3), None,
]
for codec in codecs:
    arr = zarr.create_array(None, data=values, chunks=(4, 4), compressors=codec)
    np.testing.assert_array_equal(arr[:], values)
    print(arr.compressors)

These codecs are lossless for the tested numeric data. Benchmark speed and stored bytes; Gzip is not inherently the best compression ratio. Available Blosc backends depend on the build; inspect numcodecs.blosc.list_compressors() instead of assuming Snappy exists. Lossy quantization/casting needs a declared error budget and downstream scientific validation; byte checksums alone cannot validate the resulting measurements.

For format 2, use NumCodecs:

import numpy as np
import zarr
from numcodecs import Blosc

legacy = zarr.create_array(
    "format2.zarr", data=np.arange(12, dtype="i4"), chunks=(4,), zarr_format=2,
    compressors=Blosc(cname="zstd", clevel=5, shuffle=Blosc.BITSHUFFLE),
)
np.testing.assert_array_equal(zarr.open_array("format2.zarr", mode="r")[:], np.arange(12))

Sources: performance, rectilinear example, released array implementation.

Raw lines 62 to 108