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.0 means 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.0 keeps every third object on average. 1.0 keeps 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 — see constants.VALID_AGGREGATIONS.

Coarsening method

How the writer reconciles object identity across levels — per_object (default; OID-stable, metavertices may be shared) or cross_object_metanode (legacy alias grid_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:

  1. 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).

  2. Object thinning (when sparsity_factor > 1.0): a subset of objects is selected for the coarser level using sparsity_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:

  1. Identify all vertices in each bin and replace them with one metanode.

  2. Update face vertex indices to reference metanodes.

  3. Remove degenerate faces (faces with two or more identical vertices after merging).

  4. 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 (default)

Per-object pyramid; metavertices may be shared between objects

Stable — each surviving object keeps its OID across levels

CAP_PRESERVED_OBJECT_IDS, CAP_SHARED_VERTEX_GROUPS

cross_object_metanode (alias: grid_metanode)

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:

agg_mode

Description

Use case

mean

Mean of values in bin

Continuous scalars (FA, intensity, concentration)

sum

Sum of values in bin

Counts, densities

mode

Most frequent value

Categorical / label attributes

count

Number of source vertices in bin

Density tracking

min / max

Bin extrema

Thresholds, peak activations

Canonical token set: zarr_vectors.constants.VALID_AGGREGATIONS.

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=.