Virtual chunk-index backends¶
The read path resolves a virtual index per run — a backend that answers,
per (granule, shard, config), exactly two questions: which chunks / element
ranges of the configured datasets intersect this shard (selection), and
what column arrays result from fetching + decoding them (addressing +
decode). Everything else — data-dependent filters like signal_conf_ph,
expression filters, aggregation — stays downstream and backend-invariant, so
any backend that returns the same columns produces byte-identical output
(issue #160).
The default is today's hierarchical read; a config without an index block
is untouched by this feature.
Config¶
data_source:
# index: absent → hierarchical: today's path, zero change
index:
backend: inline # compute the chunk map at read time
write_back: true # optional (default false): persist manifests
store: s3://sliderule-public-cors/zagg-index/ATL03/007/ # required with write_back
Keys other than backend are backend-specific: a key the named backend
does not accept (e.g. store under hierarchical, or on_miss under
inline) is a config error, not ignored.
Backends¶
| backend | ships in | mechanism |
|---|---|---|
hierarchical (default) |
zagg | coarse geolocation read + plan_read + h5coro hyperslices — the pre-existing path behind the protocol seam |
inline |
zagg | builds each dataset's chunk map at read time (pure-Python B-tree walk, metadata-only) and issues boundary-safe planned reads; optional write_back persists granule manifests to the store |
sidecar |
external (h5coro-hidefix) |
precomputed granule-keyed sidecar manifests fetched from the store; discovered via the entry-point group below |
inline never reads the store — it recomputes every granule every run. It
is the no-store-yet mode and (with write_back: true) the store-population
mode; consuming a populated store is the sidecar backend's job.
Requirements for inline¶
Selection still comes from the coarse spatial index, so inline requires the
hierarchical read surface: data_source.read_plan.spatial_index plus
levels/base_level (see the shipped atl03.yaml). Planned reads are
issued as-is — h5coro already inflates exactly the covering chunks — so
output is row-identical to hierarchical, with one exception the chunk map
makes detectable: a read that starts exactly on an interior chunk boundary
(which h5coro's B-tree start-edge intersection drops entirely) is shifted
one element early and trimmed, so inline survives shards the plain
hyperslice read fails on.
Write-back manifests¶
With write_back: true, after a granule's last group is read the accumulated
chunk maps are written to <store>/<granule_id>.parquet (the granule id is
the URL basename without extension, so it carries product + version and
reprocessing changes the key). store may be a local directory or an
s3://bucket/prefix URI; S3 writes use ambient credentials (the execution
role), never the granule-read credentials. A failed write is logged and the
read continues.
Coverage per visited group is deterministic — every dataset the config can touch (base-rate coordinates, variables, and filter datasets, plus the spatial-index level's coordinate and link arrays), built metadata-only up front, so a group that contributes no rows to this shard (or degrades to a full read) is still fully covered and concurrent shards of one granule write identical manifests. One row per HDF5 chunk:
| column | meaning |
|---|---|
dataset |
full HDF5 path |
chunk_idx |
row-major linear index over the chunk grid |
elem_start, elem_end |
half-open element range along the first axis |
byte_offset, nbytes |
stored (compressed) chunk extent in the file |
filter_mask |
HDF5 per-chunk filter mask (0 = all filters applied) |
chunk_offset |
per-dim dataspace offset, JSON list |
dtype |
byte-order-explicit numpy dtype string (np.dtype(...).str, e.g. <f4, |i1) |
shape, chunk_shape |
per-dataset dims, JSON lists |
gzip, shuffle |
filter-pipeline flags (booleans by contract — deflate level is irrelevant for decode) |
Contiguous (unchunked) datasets appear as a single pseudo-chunk. This is the
schema a sidecar consumer reconstructs its decode index from.
Registering an external backend¶
Backends resolve by name: zagg's built-ins live in a static dict merged with
the zagg.index_backends entry-point group (the same pattern as h5coro
registering itself as an xarray engine — zagg core never imports an external
backend's dependencies). Each entry point resolves directly to a
zagg.index.VirtualIndex subclass:
[project.entry-points."zagg.index_backends"]
sidecar = "my_package.backend:SidecarIndex"
A subclass implements read_group(h5obj, group, data_source, shard_key, grid,
arrow=False) (the same contract as the worker's group read: a carrier or
None), may override the per-granule finish_granule(h5obj, granule_url)
hook, and declares its config keys via the config_keys /
required_config_keys class attributes plus an optional
validate_index_config hook. A builtin name cannot be shadowed, and a broken
entry point is logged and skipped.