API Reference
The public API is five objects: create_pyramid builds a write plan, Pyramid holds it, attach_geozarr_metadata adds geozarr convention attrs without building a pyramid, recommend_encoding returns the chunk/shard encoding for a flat dataset, and ZarrLayerVarConfig carries optional visualization hints. CoarseningMethod is the Literal["mean", "max", "min", "sum", "nearest"] alias accepted by create_pyramid(method=...), kept equal to topozarr_core.METHODS (the installed kernel's own list, which validation checks against) by a test; nearest decimates (corner-pick) for categorical data.
topozarr.coarsen.create_pyramid
create_pyramid(ds: Dataset, levels: int | None = None, *, factors: list[int] | None = None, x_dim: str = 'x', y_dim: str = 'y', method: CoarseningMethod = 'mean', target_chunk_bytes: int = DEFAULT_CHUNK_BYTES, chunks_per_shard: ChunksPerShard | None = DEFAULT_CHUNKS_PER_SHARD, layer_hints: dict[str, ZarrLayerVarConfig] | None = None) -> Pyramid
Build a multiscale Zarr pyramid plan from a georeferenced Dataset.
Exactly one of levels / factors must be given.
Parameters:
-
ds(Dataset) –Source dataset. Must have a CRS assigned via
ds.proj.assign_crs. -
levels(int | None, default:None) –Total number of resolution levels, including the original. Level
0is the original resolution; each subsequent level coarsens by 2× per spatial dimension (cumulative factors[1, 2, 4, ...]). -
factors(list[int] | None, default:None) –Explicit cumulative downsample factors per level, e.g.
[1, 4, 16]for a sparse 4×-spaced pyramid. Must start at 1, be strictly increasing, and have each entry integer-divide the next. Mutually exclusive withlevels. -
x_dim(str, default:'x') –Name of the x (longitude / easting) dimension.
-
y_dim(str, default:'y') –Name of the y (latitude / northing) dimension.
-
method(CoarseningMethod, default:'mean') –Spatial aggregation method for coarsening. Applied along whichever spatial dims a variable carries, so a variable over one of them (e.g. a per-column
profile(time, x)) is coarsened along that dim alone. Integer variables keep their dtype:meantruncates toward zero (unlikexarray.coarsen, which promotes to float).nearestdecimates (keeps the top-left cell of each window) — use it for categorical data such as class codes or masks, where averaging invents values. It ignores fill values, and a class present only away from window corners can vanish at coarse zoom. -
target_chunk_bytes(int, default:DEFAULT_CHUNK_BYTES) –Target uncompressed size per chunk (default ~500 KB).
-
chunks_per_shard(ChunksPerShard | None, default:DEFAULT_CHUNKS_PER_SHARD) –Number of chunks per shard along each spatial dimension (e.g.
4→ 4×4 = 16 chunks per shard, ~8 MB). Must be a power of 2 in the range 1–32. PassNoneto disable sharding.This also sets the shard byte budget. Spatial dims are sized first; when they cannot use the whole budget (a small raster, or a coarse pyramid level), the remainder widens non-spatial dims such as
timeorbandinstead of leaving them at a single element. Chunks along those dims stay at 1, so reads still fetch one element at a time. Editpyramid.encodingbefore writing to override. -
layer_hints(dict[str, ZarrLayerVarConfig] | None, default:None) –Optional per-variable colormap / color-range hints written into the
zarr-layerroot metadata key.
Returns:
-
Pyramid–A Pyramid write plan; call
-
Pyramid–pyramid.write(store)to compute and write all levels.
Raises:
-
ValueError–If
dshas no CRS,methodis not implemented by the installedtopozarr-core,chunks_per_shardis not a power of 2 in the range 1–32,x_dimory_dimis not a dimension ofds, no data variable has both spatial dimensions, a coordinate is 2-D over the spatial dims (curvilinear grids are not supported), a spatial variable has more than 4 dimensions (topozarr-core kernel limit), or a variable over a spatial dim has a non-numeric dtype.
Examples:
import xarray as xr
import xproj # registers the .proj accessor
from topozarr import create_pyramid
ds = xr.tutorial.open_dataset("air_temperature").drop_encoding()
ds = ds.proj.assign_crs(spatial_ref="EPSG:4326")
pyramid = create_pyramid(ds, levels=2, x_dim="lon", y_dim="lat")
pyramid.write("pyramid.zarr")
# sparse pyramid: native, 4x, 16x (skips the costly 2x level)
sparse = create_pyramid(ds, factors=[1, 4, 16], x_dim="lon", y_dim="lat")
sparse.write("sparse.zarr")
topozarr.pyramid.Pyramid
dataclass
Pyramid(source: Dataset, level_templates: dict[int, Dataset], encoding: dict[str, Any], attrs: dict[str, Any], x_dim: str, y_dim: str, method: CoarseningMethod, factors: list[int] = list(), fill_values: dict[str, float | int | None] = dict())
A write plan for a multiscale Zarr pyramid, returned by create_pyramid.
Attributes:
-
source(Dataset) –The original (level 0) dataset.
-
level_templates(dict[int, Dataset]) –Per-level datasets carrying real coordinates and attrs; spatial data variables are zero-cost placeholders with the correct shape/dtype (their data is computed during write).
-
encoding(dict[str, Any]) –Nested dict
{path: {var: {"chunks": ..., "shards": ...}}}. -
attrs(dict[str, Any]) –Root group metadata (multiscales / proj: / spatial: / zarr-layer).
as_datatree
Return a lazy DataTree with all pyramid levels coarsened via xarray.
Each level is produced by chaining xarray.coarsen operations on the
source dataset. If the source is Dask-backed, the returned tree is fully
lazy — suitable for writing on a Dask distributed cluster or with
icechunk. Use self.encoding (already shaped for DataTree.to_zarr)
to apply the recommended chunks and shards:
dt = pyramid.as_datatree()
dt.to_zarr(store, zarr_format=3, consolidated=False,
encoding=pyramid.encoding)
Deep levels of a Dask-backed source can outrun the chunk band that
recommend_encoding flexes to (see its Note); if to_zarr
raises on safe_chunks, pass safe_chunks=False.
Values match write exactly, source
dtype and _FillValue included, at the cost of an f8 intermediate
through each coarsen. The exception is an f8 source, where the two
differ by under 1 ULP on mean/sum (window summation order).
Raises:
-
NotImplementedError–If
methodhas noxarray.coarsenequivalent. Usewritefor those.
write
write(store: Any, *, mode: Literal['w', 'w-', 'a'] = 'w', max_workers: int | None = None, levels: list[int] | None = None, max_region_bytes: int = DEFAULT_MAX_REGION_BYTES, progress: bool = False, stats: bool = False, keep_levels_in_memory: bool | None = None) -> dict[str, Any] | None
Compute and write pyramid levels to a Zarr store.
Level 0 is streamed region by region from the source dataset; each
subsequent level is block-reduced from the previously written level,
streaming shard-sized regions through the Rust kernel on a thread
pool. Levels are written sequentially (each reads the previous one);
variables within a level are processed in parallel on a shared pool.
For bounded memory on large stores, open the source lazily (e.g.
xr.open_zarr(store, chunks=None)).
Parameters:
-
store(Any) –Anything zarr-python accepts — a local path,
ObjectStore, or icechunk session store. -
mode(Literal['w', 'w-', 'a'], default:'w') –Zarr open mode for the root group. Use
"a"when writing a subset of levels so the root group and any pre-existing levels are preserved;"w"with a levels subset raises if the store already holds data (truncation would delete the levels not being rewritten). -
max_workers(int | None, default:None) –Thread pool size for region processing.
Nonederives a default from the CPU count and available memory (peak memory is roughlymax_workers * 5 * region_bytes). -
levels(list[int] | None, default:None) –Subset of levels to write (e.g.
[1, 2]). Defaults to all levels. Each coarsened level reads its predecessor, so levelN > 0must have levelN - 1either in the subset or already present in the store. -
max_region_bytes(int, default:DEFAULT_MAX_REGION_BYTES) –Memory budget per level-0 copy region. Regions are widened to cover whole source chunks when that fits the budget, so each source chunk is read once.
-
progress(bool, default:False) –Show a tqdm progress bar over written regions (requires
tqdm). -
stats(bool, default:False) –Collect and return per-level timing stats: region shapes, worker count, wall time, and cumulative per-region read/reduce/write seconds (summed across threads).
With level pipelining active (
keep_levels_in_memory=Trueor auto-enabled), level N'sreduce_scaptures fused-reduce time (reducing level-N blocks into the level-N+1 buffer) rather than the reduce of level N itself (which is zero when reading from memory).read_s = block_s - reduce_sremains the pure source-read time at every level. -
keep_levels_in_memory(bool | None, default:None) –Control level pipelining.
None(default) auto-enables fusion when the higher levels fit in half the available RAM after accounting for the worker region budget.Trueforces fusion and raisesMemoryErrorif the budget is exceeded.Falsedisables fusion and always re-reads from the store.
Examples: Write all levels to a local store:
```python
pyramid.write("pyramid.zarr")
```
Rewrite the coarsened levels, preserving level 0:
```python
pyramid.write("pyramid.zarr", mode="a", levels=[1, 2])
```
topozarr.geozarr.attach_geozarr_metadata
attach_geozarr_metadata(ds: Dataset, *, x_dim: str = 'x', y_dim: str = 'y', crs: str | None = None, layer_hints: dict[str, ZarrLayerVarConfig] | None = None) -> Dataset
Return a copy of ds with geozarr convention attrs (proj + spatial).
No multiscale pyramid is built; the dataset stays a flat group. Write it
with ds.to_zarr(...). crs defaults to the dataset CRS (xproj).
topozarr.metadata.recommend_encoding
recommend_encoding(ds: Dataset, *, x_dim: str = 'x', y_dim: str = 'y', target_chunk_bytes: int = DEFAULT_CHUNK_BYTES, chunks_per_shard: ChunksPerShard | None = DEFAULT_CHUNKS_PER_SHARD) -> dict[str, dict[str, tuple[int, ...]]]
Recommended chunks / shards for writing ds as a flat group.
Same heuristic create_pyramid applies per level, exposed for
single-resolution datasets. Pass the result straight to
ds.to_zarr(..., encoding=...). No CRS is required — the encoding
depends only on shape and dtype.
Parameters:
-
ds(Dataset) –Source dataset. Chunking of a chunked source (dask or a zarr backend) is sniffed and the recommendation snapped to nest with it.
-
x_dim(str, default:'x') –Name of the x (longitude / easting) dimension.
-
y_dim(str, default:'y') –Name of the y (latitude / northing) dimension.
-
target_chunk_bytes(int, default:DEFAULT_CHUNK_BYTES) –Target uncompressed size per chunk (default ~500 KB).
-
chunks_per_shard(ChunksPerShard | None, default:DEFAULT_CHUNKS_PER_SHARD) –Number of chunks per shard along each spatial dimension (e.g.
4→ 4×4 = 16 chunks per shard, ~8 MB). Must be a power of 2 in the range 1–32. PassNoneto disable sharding.This also sets the shard byte budget: whatever the spatial dims leave unused widens non-spatial dims such as
timeorband. Chunks along those dims stay at 1.Treated as an upper bound on a chunked source: it is flexed down per spatial dim where that is what makes the shard divide the source chunk (see the note below).
Returns:
-
dict[str, dict[str, tuple[int, ...]]]–{var_name: {"chunks": (...), "shards": (...)}}for variables over at -
dict[str, dict[str, tuple[int, ...]]]–least one spatial dim (a variable with only one is sized along that dim
-
dict[str, dict[str, tuple[int, ...]]]–alone);
"shards"is omitted whenchunks_per_shardisNone. -
dict[str, dict[str, tuple[int, ...]]]–A 1-D coordinate along a spatial dim gets
"chunks"only, sized as a -
dict[str, dict[str, tuple[int, ...]]]–1-D array and never sharded, and is absent when the whole coordinate
-
dict[str, dict[str, tuple[int, ...]]]–already fits in one chunk. Variables over neither spatial dim are
-
dict[str, dict[str, tuple[int, ...]]]–absent and fall through to xarray's defaults.
Raises:
-
ValueError–If
chunks_per_shardis not a power of 2 in the range 1–32,x_dimory_dimis not a dimension ofds, or no data variable has both spatial dimensions.
Examples:
from topozarr import attach_geozarr_metadata, recommend_encoding
ds = attach_geozarr_metadata(ds, x_dim="lon", y_dim="lat")
ds.to_zarr(
"flat.zarr",
zarr_format=3,
consolidated=False,
encoding=recommend_encoding(ds, x_dim="lon", y_dim="lat"),
)
Note
For a dask-backed ds, xarray's safe_chunks check requires the
zarr write unit -- the shard, when sharding is on -- to divide the
dask block. The recommendation flexes chunks_per_shard down (never
up) until a shard does, so the snippet above writes a dask source as
is. A source chunk too small to divide into a chunk of usable size is
the exception: the recommendation keeps a read-aligned shard there, and
such a write still needs a rechunk or safe_chunks=False.