Formats & Migration

Luxar has two distinct on-disk formats with independent version numbers. This page distinguishes them, documents the versioning policy, and explains how to migrate legacy inputs. For the exhaustive field-level specs, see Luxar zarr format (scenes) and GSplats zarr format (standalone splats).

Two on-disk formats

Format

Extension

What it holds

Version attr

Current version

Scene container

.luxar.zarr

The scene graph (points / lines / gsplats / mesh nodes, groups, transforms, dimensions)

format_version (with format_type="luxar_zarr"; 0.1 stores carry the legacy luxar_version instead)

v0.2

Standalone gsplats

.gsplats.zarr

A detached Gaussian-splat node-tree (leaf, additive ladder, kind=lod, kind=partition, nested)

format_version (with format_type="gsplats_zarr")

v3.4

The two version numbers are unrelated — the scene container being at v0.2 says nothing about the gsplats node-tree being at v3.4, and vice versa. A .gsplats.zarr is structurally identical to the gsplats node a scene already contains; it can be grafted into a scene with luxar gsplat convert or, from Python, scene.add_gsplats_from_file(...).

Versioning policy

What readers do with a version they do not know, and how long deprecated names keep working, is the Compatibility & Deprecation Policy.

Both version numbers and their supported ranges are single-sourced in format-contract/contract.yaml. That file is projected to Python (typing_utils/_format_contract.py) and TypeScript (luxar-viewer/src/types/format-contract.ts) by make gen-contract, and hatch run check-contract gates against drift, so the writer and the viewer always agree on the vocabulary.

The contract declares what each build can read:

  • Scene (.luxar.zarr): current 0.2; 0.1 and 0.2 are readable.

  • Gsplats (.gsplats.zarr): current 3.4; 3.0–3.4 are all readable.

There are no reserved future slots: a newer version is handled by the policy below, not by pre-declaring it.

One version-check rule, both formats, both languages

Every reader — LuxarScene.load and the validator in Python, the gsplats loaders (load_gsplats, inspect_gsplats, luxar gsplat doctor, the batch tile validator), and the viewer’s scene loader — applies the same rule, from luxar/typing_utils/format_version.py and its mirror luxar-viewer/src/data/format-version.ts. The two are pinned by one shared case table (same ids in both test suites):

Arm

Example on disk

Outcome

supported

scene 0.1 / 0.2; gsplats 3.0 … 3.4

Load silently.

newer-minor

scene 0.3; gsplats 3.5

Load, but warn (Python UserWarning; viewer console warning + toast). A minor bump is additive by policy, so the reader still makes sense of the store — it just cannot see what the newer writer added.

older-unsupported

scene 0.0; gsplats 2.0

Refuse. The error names the version, the supported set and the remedy: rebuild a scene with the current release; luxar gsplat migrate-format <in> <out> for a gsplats store.

newer-major

9.9

Refuse — upgrade Luxar / the viewer.

unparsable

abc, 0.2.1

Refuse — a version must be MAJOR.MINOR.

missing, with format_type

{format_type: "luxar_zarr"} and no version

Refuse — a 0.2+ header with its version stripped is corrupt, not legacy.

missing, without format_type

{type: "scene"} only

Tolerated — a hand-written / pre-header store.

legacy key

{luxar_version: "0.1"}

Supported — read exactly as format_version: "0.1".

In Python a refusal is luxar.typing_utils.format_version.UnsupportedFormatVersionError (a ValueError), or a ValidationError from validate_zarr_attributes; in the viewer the loader throws and the error overlay names the version and the fix.

Scene 0.1 → 0.2

0.2 changed only the root header: format_version + format_type: "luxar_zarr" replace luxar_version, and luxar_software_version records the writing release (provenance only — excluded from content_hash by both the compile-time hasher and luxar optimize’s streaming twin, so a restamp under a newer release never invalidates a viewer cache). Node schema, spatial index and encodings are identical. Nothing needs migrating: every 0.1 store, including the published Zenodo records and the datasets/** stores, loads through the legacy key arm above. Recompiling a scene at 0.2 does change its content_hash (the header keys fold into the digest) — expected, one-time.

Two stores, two keys, one fact: a standalone .gsplats.zarr still records the writing release as luxar_gsplats_version. That asymmetry is deliberate — renaming a 3.4 header key without bumping the gsplats format would make “3.4” mean two shapes, and every published gsplats record carries the old key. It is scheduled to unify on luxar_software_version at the next gsplats bump.

Supported legacy inputs & migration

luxar gsplat migrate-format upgrades an older .gsplats.zarr layout to the current v3.4 node-tree. It auto-detects the input shape:

Legacy layout

How it is recognized

Migrates to

v1.0 (flat single-LOD)

root format_version attr = "1.0"

v3.4 leaf

v1.1 (additive ladder)

root format_version attr = "1.1"

v3.4 leaf + additive ladder

pre-v2.0 substitutive directory

a directory whose manifest.json has lod_kind: "substitutive"

v3.4 kind=lod group

v2.0 (matrix)

root format_version attr = "2.0"

v3.4 tree

v3.0 / v3.1 with legacy LOD attrs

a current node tree whose kind=lod groups still carry the pre-v3.2 pixel_size selector attrs

v3.4 (selector: "screen-area" + derived per-child coverage_fraction)

luxar gsplat migrate-format legacy.gsplats.zarr v3.gsplats.zarr             # single file (AUTO encoding)
luxar gsplat migrate-format old_pyr/ v3.gsplats.zarr                        # substitutive directory
luxar gsplat migrate-format legacy.gsplats.zarr v3.gsplats.zarr --lossless  # preserve float32 Cholesky exactly

By default the output adopts the AUTO encoding policy (certified uint8→uint16 quantization of the Cholesky factors). Pass --lossless to keep the factors in float32 for archival fidelity.

migrate-format vs reencode

Both write a new copy and never mutate the input, but they solve different problems:

luxar gsplat migrate-format

luxar gsplat reencode

Input

Legacy layout (v1.0 … v3.1)

Current-format dataset

Output

Current v3.4 layout

Same structure, re-quantized

Changes

Layout and encoding (AUTO; --lossless for float32)

The on-disk Cholesky encoding, and under auto/memory the centers too (per-axis uint16 fixed-point — on ordinary spatial data centers are bit-exact only under precision; a gridded axis such as a stacked sigma=0 time axis keeps uint16 but has its grid snapped onto the data’s own spacing, and a non-gridded axis whose grid would displace splats past their own σ for more than 0.1% of the splats falls back to float32 — so a gridded axis is exact in every mode, while a smaller degenerate population on a non-gridded axis is quantized away with no warning)

Encoding choices

float32 vs AUTO (certified u8→u16 ladder) via --lossless

Full ladder: -e memory (uint8) / precision (float32) / auto

Structure

May restructure legacy tree

Structure-preserving (leaf/lod/partition/nested; the fitting/provenance/pipeline groups carry over for directory stores)

luxar gsplat reencode fit.gsplats.zarr fit_u8.gsplats.zarr -e memory      # uint8 (smallest)
luxar gsplat reencode fit.gsplats.zarr fit_f32.gsplats.zarr -e precision  # float32 (exact/archival)

Decode is always to float32, so the viewer, GPU, and WASM paths are unaffected by the on-disk encoding choice.

Both are distinct from the two export commands, which produce entirely different artifacts:

  • luxar export — a scene (.luxar.zarr) → a standalone offline viewer folder.

  • luxar gsplat export — a .gsplats.zarr → a classical INRIA PLY file.

LOD terminology map

The level-of-detail algorithms and their on-disk semantics keep their original names in the API and data model. The CLI --recipe flag uses intent-first names for the same concepts. Both vocabularies are current — the API terms are not deprecated; they name the algorithm and the kind=lod semantics, while the recipe names describe what you get.

API / data-model term

CLI --recipe name

Meaning

additive

stream

One leaf + a progressive prefix-sum ladder (fast first paint)

substitutive / pyramid

levels

Coarse→fine replacement levels (zoom across scales)

partitioned

tiles

Spatial BSP tiles, culled and streamed per tile

multiscale

overview

Instant coarse overview level + fine tiles on zoom

mosaic

adaptive

Tiles where each tile picks its own detail level

(The pre-2026-07 recipe names — additive, substitutive, pyramid, partitioned, multiscale, mosaic — still error on the CLI with a pointer to the new name; stored batch manifests translate silently.)

See also