Pyramid construction¶
Terms¶
- Metanode
A synthetic vertex at a coarser resolution level that represents a group of source vertices co-located within a supervoxel bin. Its position is the centroid (or another aggregation) of the source vertices in the bin. Metanodes are the fundamental unit of spatial coarsening.
- Coarsening factor
Per-level isotropic vertex-reduction target —
coarsen_factor=2.0means each axis is binned 2× coarser, giving up to ~8× fewer vertices in 3D (exact reduction depends on the actual vertex distribution).- Sparsity factor
Per-level object-keep target —
sparsity_factor=3.0keeps every third object on average.1.0keeps all objects (the default).- Aggregation mode (
agg_mode) How per-vertex / per-object attributes are combined within a bin when producing the metanode’s attribute value. Options:
mean(default),sum,mode,count,min,max— seeconstants.VALID_AGGREGATIONS.- Coarsening method
How the writer reconciles object identity across levels —
per_object(default; OID-stable, metavertices may be shared) orcross_object_metanode(legacy aliasgrid_metanode; fresh OID space per level).- Cross-level link
An edge from a fine-level vertex to its coarse-level parent metanode. Materialised as a separate array family at each level (
links/<delta>/,cross_chunk_links/<delta>/) — see Links and cross-chunk links.
Introduction¶
Building a resolution pyramid means constructing one or more coarser representations of the base-level data. Coarsening operates independently on two axes:
Spatial coarsening — merge vertices in the same supervoxel bin into a single metanode (per the
coarsen_factor).Object thinning — drop a fraction of discrete objects (per the
sparsity_factor).
In 0.4, every coarsening pass can additionally emit cross-pyramid-
level links that record which fine vertex maps to which coarse
metanode. Cross-level links live alongside the per-level intra-chunk
edges under links/<delta>/ and cross_chunk_links/<delta>/; see
Links and cross-chunk links
for the on-disk layout and
examples/07_multiscale_links.ipynb
for a walkthrough.
This page documents the coarsening algorithm per geometry type, the
two public APIs (build_pyramid and coarsen_level), and the
cross-level link options.
Technical reference¶
Coarsening by geometry type¶
Point cloud¶
Pure spatial coarsening. For each bin at the coarser level, all vertices in the corresponding bin at the base level are merged into a single metanode:
metanode_position = mean(positions in bin) # centroid
metanode_attribute = agg_mode(attributes in bin)
There are no discrete objects in a point cloud; sparsity_factor is
ignored (always 1.0).
Polyline and streamline¶
Two-stage coarsening:
Spatial: for each object, the vertex sequence is downsampled by replacing each contiguous run of vertices within the same bin with one metanode at their centroid. Edge connectivity is preserved (consecutive metanodes are connected).
Object thinning (when
sparsity_factor > 1.0): a subset of objects is selected for the coarser level usingsparsity_strategy. Non-selected objects are not written.
After both stages, the per-level intra-chunk links/0/ and
cross_chunk_links/0/ arrays are recomputed at the coarser level
(metanode positions may fall in different chunks than the originals).
Graph and skeleton¶
For each bin, all vertices in the bin merge into one metanode. Edges
are remapped: an edge (u, v) at the base level becomes an edge
between the metanodes of u and v. Self-loops (both endpoints in
the same bin) are removed; duplicate edges deduplicated.
For skeletons, the tree structure is preserved across coarsening.
Multi-skeleton stores support sparsity_factor > 1.0 for thinning
when the store contains many independent skeletons.
Mesh¶
Mesh coarsening uses vertex merging:
Identify all vertices in each bin and replace them with one metanode.
Update face vertex indices to reference metanodes.
Remove degenerate faces (faces with two or more identical vertices after merging).
Remove duplicate faces.
This is conservative — it preserves topology at the cost of some geometric quality. For high-quality mesh decimation, pre-process with a dedicated mesh tool before writing.
build_pyramid API¶
The recommended entry point. Pass factors=[(coarsen, sparsity), ...]
where the i-th tuple produces level i+1 from level i:
from zarr_vectors.multiresolution.coarsen import build_pyramid
build_pyramid(
"scan.zarrvectors",
factors=[
(2.0, 1.0), # level 1: 2× coarsen, no sparsity
(2.0, 1.0), # level 2: 2× coarsen
(2.0, 3.0), # level 3: 2× coarsen + drop 2/3 objects
],
method="per_object", # OID-stable (default); see "Methods"
agg_mode="mean", # attribute aggregation
sparsity_strategy="random",
sparsity_seed=None,
cross_level_depth=1, # ±1 cross-level edges per pair
cross_level_storage="explicit", # write both +1 (fine) and -1 (coarse)
)
Each factor pair (coarsen, sparsity) opts out by passing 1.0 on
that axis. Passing the same factors with method="cross_object_metanode"
uses the legacy path.
Full signature (see
zarr_vectors/multiresolution/coarsen.py:build_pyramid):
def build_pyramid(
store_path: str | Path,
*,
factors: list[tuple[float, float]] | None = None,
method: str = "per_object",
level_configs: list[dict] | None = None, # legacy: explicit bin_ratio
target_volume_reduction: float = 8.0, # legacy: auto-plan mode
sparsity_weight: float = 0.0, # legacy: auto-plan mode
reduction_factor: int = 8, # legacy: heuristic threshold
max_levels: int = 10,
min_vertices: int = 8,
agg_mode: str = "mean",
sparsity_strategy: str = "random",
sparsity_seed: int | None = None,
cross_level_depth: int = 1,
cross_level_storage: str = "explicit",
) -> dict[str, Any]
The factors= interface is the recommended path. The
level_configs= / sparsity_weight= / auto-plan branches are kept
for backwards compatibility.
coarsen_level API¶
For one level at a time:
from zarr_vectors.multiresolution.coarsen import coarsen_level
coarsen_level(
"scan.zarrvectors",
source_level=0,
target_level=1,
coarsen_factor=2.0,
sparsity_factor=1.0,
method="per_object",
agg_mode="mean",
)
source_level does not have to be 0 — you can incrementally build
deeper pyramids by chaining coarsen_level calls. Cross-level
emission for individual coarsen_level calls is not automatic;
to materialise ±delta arrays, call build_pyramid(..., factors=...)
instead (which post-processes the whole pyramid in one pass via
_finalize_cross_level_for_store).
Methods: per_object vs cross_object_metanode¶
Method |
Behaviour |
OID stability |
Capabilities stamped |
|---|---|---|---|
|
Per-object pyramid; metavertices may be shared between objects |
Stable — each surviving object keeps its OID across levels |
|
|
Legacy grid-binning; merges vertices across object boundaries |
Fresh OID space per level |
(none additional) |
Choose per_object when downstream consumers need to track the
“same object” across resolution levels (e.g. drill-down navigation
in Neuroglancer, ID-preserving analytics). Choose
cross_object_metanode when you want the smallest possible coarse
representation and don’t need OID continuity.
Implementations:
_per_object_coarsen
and
_cross_object_metanode_coarsen.
Aggregation modes¶
Set globally via agg_mode= on build_pyramid or coarsen_level.
Applies to every per-vertex / per-object attribute:
|
Description |
Use case |
|---|---|---|
|
Mean of values in bin |
Continuous scalars (FA, intensity, concentration) |
|
Sum of values in bin |
Counts, densities |
|
Most frequent value |
Categorical / label attributes |
|
Number of source vertices in bin |
Density tracking |
|
Bin extrema |
Thresholds, peak activations |
Canonical token set:
zarr_vectors.constants.VALID_AGGREGATIONS.
Cross-level link emission (0.4+)¶
Two kwargs on build_pyramid control whether and how cross-pyramid-
level edges are materialised at each adjacent level pair:
build_pyramid(
"scan.zarrvectors",
factors=[(2.0, 1.0), (2.0, 1.0)],
cross_level_depth=2, # ±1, ±2 per applicable pair
cross_level_storage="explicit", # both +N and -N
)
cross_level_depth: int = 1 controls how far the cross-level
emission reaches:
Value |
Meaning |
|---|---|
|
Disabled (same as |
|
Materialise up to |
|
Walk all available pyramid levels |
For depth >= 2, parents are composed across coarsening steps —
grandparent[i] = parent_at_L1[parent_at_L0[i]] — so a single
edge goes from a level-0 vertex straight to its level-2 metanode
(instead of forcing readers to chain two +1 lookups).
cross_level_storage: Literal["none","implicit","explicit"]
controls direction:
Mode |
|
|
|---|---|---|
|
no |
no |
|
yes |
no (reconstruct on read by flipping |
|
yes |
yes |
implicit saves disk; explicit gives O(1) drill-down and drill-up
reads.
Algorithm. After all coarser levels have been written by the
normal coarsening loop, build_pyramid calls
_finalize_cross_level_for_store
which:
Walks every adjacent
(fine, coarse)level pair.Reconstructs the fine→parent map from the coarse level’s
cross_chunk_links/<delta=-1>/records (each record pairs a coarse metanode to one of its fine children).Builds the trivial edge list
[(i, parent[i]) for i in range(n_fine)].Partitions via
partition_cross_level_edgesinto chunk-aligned (links/+delta/<chunk>) and cross-chunk (cross_chunk_links/+delta/data) buckets.Writes the
+deltaarrays at the fine level; ifcross_level_storage="explicit", also writes the swapped-endpoint-deltaarrays at the coarse level.For
depth >= 2, composes parents as above and emits±2,±3, … in the same way.
Persistence. Root metadata gains cross_level_depth and
cross_level_storage, and the CAP_MULTISCALE_LINKS capability
token is stamped on format_capabilities whenever any delta != 0
array is emitted. Readers MAY use the capability to short-circuit
walks of stores that don’t carry cross-level edges at all.
See examples/07_multiscale_links.ipynb
for a walkthrough that builds a 3-level pyramid, inspects the
resulting <delta> subdirs, and reads both intra- and cross-level
edges.
Legacy automatic pyramid¶
build_pyramid with no factors= and no level_configs= falls back
to a legacy auto-planner driven by target_volume_reduction,
sparsity_weight, reduction_factor, max_levels, and
min_vertices:
build_pyramid("scan.zarrvectors") # legacy: auto-select levels
Convenient for exploratory use; for production pipelines prefer
explicit factors=.