Gaussian Splatting Package

The gsplats package provides tools for fitting and rendering Gaussian splats to images.

Gaussian splatting subsystem: fitting, calibration, LOD, tiling, and lifting.

Public entry points for turning a volume into oriented Gaussian splats (fit_gaussian_splats / GaussianSplatFitter), calibrating the splat count K (calibrate), building level-of-detail topologies (make_additive_lod, make_substitutive_lod, make_lod_pyramid), tiling large volumes, seeding, and lifting points/lines to splats. The heavy dependencies (torch, scipy) are optional and loaded lazily: the package imports without them, and the entry points that need them fail only when first used (the callable exports raise a clear install hint; an unexpected internal import error propagates loudly).

The docstrings on the fallback definitions below describe what each real symbol does; when the gsplats extra is absent these names resolve to stubs that raise ImportError (via _raise_gsplats_import_error()) on first use rather than at import time. GSplatData and its two LOD record types are the exception: they are pure NumPy and are always the real classes.

The scope of that exception is CONSTRUCTING, SAVING and LOADING a GSplatData — a hand-built AdditiveSubLOD ladder or SubstitutiveLevel stack included — plus the purely geometric translate / transform / center_at_centroid, and grafting the result into a scene with add_gsplats / add_gsplats_from_data / add_gsplats_from_file. That covers core scene authoring: a core-only install can build, write and read back a .gsplats.zarr.

Most content editing is also core-only. An edit whose source ladder carries authored LOD stamps must load the extra to recompute them, and raises a bare ModuleNotFoundError rather than the friendly install hint because the exception bypasses the stub guard. Which root is missing depends on the route:

  • 'scipy', reached through luxar.gsplats.lod (whose lod/additive.py imports scipy.sparse) when authored ladder stats are recomputed after the intensity ops (scale_intensity / normalize_intensity / clamp_intensity / affine_intensity), a filter / filter_by that actually removes splats, a slice_by that crops, the heuristic cull methods cumulative / amplitude_percentile / combined (hence a bare cull(), whose auto resolves to cumulative), embed_dimension, and a strict additive_prefix view.

  • 'torch', imported earlier still — the rendering-based cull methods error_budget / redundancy, and therefore an auto handed a target or a shape.

Building a ladder with add_gsplats_from_data(..., additive_lod={...}) also needs 'scipy' regardless of source stamps. Unlike add_points(..., additive_lod=...), that route goes through lod.

The stamp-driven rewrites in the first bullet work core-only on unstamped data. An operation that removes nothing (an all-passing filter, a cull whose retention keeps every splat) short-circuits before either import regardless of stamps.

Main API

luxar.gsplats.fit_gaussian_splats(V: ndarray, seeds: ndarray | int | float | GSplatData | None = None, norm_percentile: float = 0.0, floor: str | float | None = 'auto', norm_range: tuple[float, float] | None = None, downscale: int | Sequence[int] | None = None, init_sigma_vox: float | None = None, n_iters: int = 1000, lr: float = 0.01, loss_type: str = 'l1', asymmetric_penalty: float | None = 1.0, l1_amp: float | None = None, l1_diag: float | None = None, sigma_min_diag: Sequence[float] | float | None = 0.28867513459481287, sigma_max_diag: Sequence[float] | float | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, truncate: float = 2.75, device: str | None = None, seed_method: str = 'auto', verbose: bool = True, max_abs_error: float | None = None, rel_l2_target: float | None = None, gradient_clip: float | None = None, scheduler_type: str = 'plateau', patience: int = 15, lr_reduction_factor: float = 0.9, early_stop_patience: int | None = 300, enable_dynamic_ops: bool = True, dynamic_config: DynamicOpsConfig | None = None, dynamic_ops_verbose: bool = False, napari_movie: bool = False, movie_every: int = 1, movie_max_frames: int | None = None, use_metal: bool = True, use_cuda: bool = True, cull_retention: float | None = 0.95, voxel_footprint_correction: bool | float = False, boundary_penalty: float | None = None, clip_to_bounds: bool = False, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real', sort_splats_enabled: bool = True, sort_splats_interval: int = 1000, iter_callback: Any | None = None, iter_callback_every: int = 25, seed_amps_background_relative: bool = False, source_dtype: str | None = None, source_shape: Sequence[int] | None = None, source_stored_bytes: int | None = None, **seed_kwargs: Any) → GSplatData[source]

Fit n-dimensional oriented Gaussian splats to reconstruct input image/volume.

Uses standard PyTorch Adam (fused on CUDA when available) combined with fixed-pool splat relocation: the least informative splats are periodically moved to regions of high reconstruction residual, which keeps optimizer tensor shapes constant and avoids per-splat state management.

The optimization uses: - Standard PyTorch Adam with gradient-dilution-compensated learning rates - Center position (bounded to image domain via sigmoid) - Non-negative amplitude (via softplus activation) - Covariance matrix Σ = L @ L^T where L is the Cholesky factor - Efficient rendering via batched triangular solve

Parameters:
  • V (np.ndarray) – Input n-dimensional image/volume to reconstruct. Will be normalized to [0,1].

  • seeds (np.ndarray (N, d), int, float, GSplatData, or None) –

    Initial seed center positions, count, compression ratio, or full warm-start dataset.

    • If np.ndarray: Explicit seed centers in voxel coordinates

    • If int: Exact number (keeps highest intensity if more detected)

    • If float (0 < seeds <= 1.0): Compression ratio - the ratio of floats used to represent Gaussian splats over total image floats. For example, seeds=0.1 targets a representation using 10% of the original storage. The number of splats is computed as: n = ratio × total_voxels / floats_per_splat where floats_per_splat = d + d×(d+1)/2 + 1 (center + Cholesky + amp).

    • If GSplatData: Explicit seeds or a warm start (centers + Cholesky + amplitudes carried over directly). Both generate_seeds() output and a previously fitted result go through this door — they differ in amplitude scale, so declare it with seed_amps_background_relative (below).

    • If None: Auto-generated using dimension-aware intelligent defaults: * Universal scales: (0.5, 1.0, 2.0, 4.0, 8.0, 16.0) for comprehensive detection * Volume-proportional density: ~1% of voxels as seeds * Inclusive threshold: percentile_thresh=70 for broad feature coverage

  • norm_percentile (float, default 0.0) – Normalization method for handling outliers and noise: - 0.0: Full min-max range (maximum dynamic range, sensitive to outliers) - >0: Percentile clipping (e.g., 1.0 uses 1%-99% range, robust to outliers) Higher values provide more outlier robustness but may clip important data.

  • floor (str, float, or None, default "auto") –

    Background floor / DC-offset suppression, applied before normalization (subtracts a constant pedestal that a localized-Gaussian basis cannot represent efficiently). Raises the effective image_min so sub-floor intensity clips to 0.

    • ”auto”: histogram-mode estimate (capped at the median; a no-op on clean data with no pedestal).

    • ”pN” (e.g. “p10”): the Nth percentile of non-zero intensities.

    • float: a fixed intensity value.

    • ”none” / 0 / None: disabled (today’s hard-min normalization).

    Orthogonal to norm_percentile unless the floor overtakes its percentile-derived high endpoint; then image_max expands to the data maximum and bright-outlier clipping is dropped to preserve signal.

  • norm_range (tuple of float, or None, default None) – Explicit (image_min, image_max) for normalization, replacing the pair norm_percentile would derive from V itself. Tiled fitting passes a range resolved against the WHOLE volume (resolve_volume_norm_range()) so every tile maps a given physical intensity to the same normalized value, and is therefore held to the same absolute convergence tolerance and thresholds. Must be finite with image_max > image_min. Because such a range is a bounded-sample estimate, a voxel above image_max is left unclipped when norm_percentile == 0 (and the auto amp_max rises with it), rather than flattening the brightest structure. Leave None for a whole-volume fit — the array already IS the volume.

  • downscale (int, sequence of int, or None, default None) – Downsample the volume by integer factor(s) before fitting. Useful for band-limited data where high-frequency voxels contain only noise. A Gaussian anti-alias filter (sigma = factor/2) is applied before decimation. - If int: Isotropic downscale (e.g., downscale=4 reduces all axes by 4x). - If sequence: Per-axis factors (e.g., downscale=(1, 4, 4) for anisotropic). - If None: No downscaling (default). Fitted splat parameters are automatically rescaled to original coordinates.

  • init_sigma_vox (float or None, default None) – Initial isotropic standard deviation for Gaussian splats (in voxels). If None, uses scale-informed initialization from seeding methods. If no scale info available, auto-computes based on image size (~5% of smallest dimension, min 1.5).

  • n_iters (int, default 1000) – Maximum number of optimization iterations. Default is generous to allow max_abs_error convergence criterion to work effectively.

  • lr (float, default 0.01) – Learning rate for Adam optimizer.

  • loss_type (str, default "l1") – Loss function: “l1” (default; robust to outliers, preserves sharp features; in the loss-comparison study, Supp. Doc. 5, L1 beats MSE on held-out PSNR on 11 of 17 microscopy volumes and never trails it by more than 0.28 dB), “mse” (the global unregularized MSE minimizer maximizes training PSNR, but finite-iteration regularized fits do not reach it), or “poisson” (natural for count/photon data; stops in the fewest iterations on most confocal volumes, 8-15x shorter wall time than MSE where MSE runs to the iteration cap, at a held-out cost vs L1 of up to ~2.9 dB).

  • asymmetric_penalty (float, default 1.0) – Over-prediction penalty factor for asymmetric loss. Multiplies loss for regions where pred > target by this factor. Set to None to disable asymmetric loss. Default 1.0 (symmetric); progressive fitting uses 10.0 for residual passes to prevent locked-in overshoot.

  • l1_amp (float, default None (auto: 0.1 * lr)) – L1 regularization coefficient on splat amplitudes for sparsity. If None, automatically set to 10% of learning rate for consistent sparsity pressure that scales with optimization strength.

  • l1_diag (float, default None (auto: 0.01 * lr)) – L1 regularization coefficient on diagonal elements of Cholesky factors. Encourages smaller, more isotropic splats. If None, automatically set to 1% of learning rate for mild shape regularization.

  • sigma_min_diag (Sequence[float] | float, optional) – Minimum diagonal values for Cholesky factor L along each axis. A single float is broadcast across all dimensions. Defaults to sqrt(1/12) ≈ 0.289 (1-voxel box footprint) to allow single-voxel splats while preventing degeneracy.

  • sigma_max_diag (Sequence[float] | float, optional) –

    Maximum diagonal values for Cholesky factor L along each axis.

    • If Sequence[float]: Per-axis absolute bounds (one per dimension).

    • If float: Fraction of volume extent per axis. Each dimension gets shape[i] * fraction independently. E.g., sigma_max_diag=1/16 on a (50, 200, 300) volume gives [3.125, 12.5, 18.75].

  • amp_max (float or None, default None (auto: 1.0)) – Maximum amplitude constraint for splats. Prevents amplitude explosion during optimization, especially with aggressive compression (few splats). Since the image is normalized to [0, 1], a value of 1.0 matches the max possible intensity. If None, automatically set to 1.0. Set to higher values (e.g., 2.0) for more flexibility, or lower (e.g., 0.5) for tighter control.

  • max_eccentricity (float or None, default 10.0) – Maximum ratio of longest to shortest axis for splat covariance. Limits anisotropy by constraining diagonal elements of Cholesky factor L so that max(diag)/min(diag) <= sqrt(max_eccentricity). For example, 2.0 means the longest axis can be at most sqrt(2) ≈ 1.41x the shortest axis.

  • truncate (float, default 2.75) – Truncation radius in standard deviations for rendering efficiency.

  • device (str, optional) – PyTorch device (“auto”, “cpu”, “cuda”, “mps”). Auto-detects for None or "auto".

  • seed_method (str, default "auto" (RECOMMENDED)) –

    Method for generating seeds when seeds=None:

    • ”auto” (DEFAULT, RECOMMENDED): Fast edges + grid combination. Provides good convergence by capturing boundaries (edges) and spatial coverage (grid). Decomposition is excluded by default for speed. Budget allocation: ~60% edges, ~40% grid.

    • ”decomposition”: Multi-scale decomposition for blob-like features (slow). Captures global structure but may miss boundaries and fine details.

    • ”grid”: Uniform grid seeding for spatial coverage. Fast and simple, good for uniform textures.

    • ”edges”: Edge-based seeding with anisotropic shapes. Good for images with clear boundaries and structure.

    • Comma-separated combinations (e.g., “decomposition,edges,grid”).

    This parameter is only used when seeds=None. If seeds are provided explicitly, this parameter is ignored.

  • **seed_kwargs – Additional keyword arguments for seed generation (e.g., num_scales, percentile_thresh, scale_voxels, etc.). Only used when seeds=None. See seed generation functions for available options.

  • verbose (bool, default True) – Whether to print optimization progress.

  • max_abs_error (float or None, default None (auto: 0.01)) – Maximum absolute error threshold for convergence. If specified, optimization stops when max(|prediction - target|) < max_abs_error. If None, automatically set to 0.01 (1% of normalized [0,1] range) for sensible convergence behavior. Auto-threshold usage is logged.

  • rel_l2_target (float or None, default None) – Relative L2 error threshold for convergence. If specified, optimization stops when ||pred - target||₂ / ||target||₂ < rel_l2_target. This is an additional (OR) criterion alongside max_abs_error — either being satisfied triggers convergence. Provides a smoother, more stable convergence signal than max_abs_error. If None, this criterion is disabled.

  • gradient_clip (float or None, default None) – Maximum gradient norm for clipping. None disables clipping (default for MSE loss where gradients are inherently well-scaled by error magnitude).

  • scheduler_type (str, default "plateau") – Type of learning rate scheduler (“plateau” or “exponential”).

  • patience (int, default 15) – Scheduler patience: iterations without loss improvement before LR reduction.

  • lr_reduction_factor (float, default 0.9) – LR multiplier on plateau (new_lr = lr × lr_reduction_factor). Examples: 0.5=halve LR, 0.1=reduce to 10%, 0.9=moderate reduction.

  • early_stop_patience (Optional[int], default 300) – Early stopping: stop if no loss improvement for N iterations. None disables early stopping (runs until convergence or iteration limit). Example: 300 stops if no improvement for 300 consecutive iterations.

  • enable_dynamic_ops (bool, default True) – Enable dynamic operations (seeding and pruning).

  • dynamic_config (DynamicOpsConfig, optional) – Configuration for dynamic operations. Uses defaults if None.

  • dynamic_ops_verbose (bool, default False) – Enable detailed console logging for dynamic operations. Shows residual analysis, seeding attempts and pruning operations.

  • napari_movie (bool, default False) – Record optimization movie for napari visualization. Enable this to create a time-series visualization of optimization progress.

  • movie_every (int, default 1) – Record movie frame every N iterations.

  • movie_max_frames (int, default None (infinite)) – Maximum number of movie frames to store in memory. Older frames are automatically removed when this limit is exceeded, preventing memory exhaustion during long optimizations.

  • use_metal (bool, default True) – Enable Metal acceleration on Apple Silicon (macOS + MPS device). Provides substantial speedup for 3D volumes (chip-dependent). Automatically disabled if not available.

  • use_cuda (bool, default True) – Enable custom CUDA kernels on NVIDIA GPUs. Provides substantial speedup for 2D-8D volumes (often orders of magnitude, GPU-dependent). Automatically disabled if not available.

  • cull_retention (float or None, default 0.95) – Post-fit cumulative culling. Keeps the top splats that account for this fraction of the total amplitude (0–1). At 0.95, roughly 5% of splats are removed — those that collectively contribute only 5% of the total signal. Set to None to disable.

  • voxel_footprint_correction (bool | float, default False) – Post-fit correction to inflate splat covariances by the voxel footprint. This ensures that upsampling doesn’t invent detail beyond what the original discrete data can represent. The correction adds sigma^2 to covariance diagonals: Sigma_new = Sigma_original + sigma^2 * I_d - False: Disabled (default) - True: Enable with 1-voxel box footprint (sigma ≈ 0.289 voxels) - float: Custom sigma in voxel units (e.g., 0.5 for half-voxel blur, 1.0 for 1-voxel blur) Works for any dimension d.

  • boundary_penalty (float or None, default None) – Weight for boundary containment penalty during optimization. Adds a differentiable penalty for splats whose effective support (truncate * sqrt(Sigma_ii)) extends beyond the volume bounds. The penalty is: boundary_penalty * mean(overflow^2). - None: Disabled (default) - float > 0: Enable with this weight (e.g., 0.1 for mild, 1.0 for strong)

  • clip_to_bounds (bool, default False) – Post-fit hard clipping to guarantee no splat extends beyond the volume bounds. Scales down rows of the Cholesky factor L so that truncate * sqrt(Sigma_ii) <= distance_to_nearest_edge for each dimension. Preserves splat orientation but shrinks to fit within bounds.

  • voxel_size (Sequence[float] | float, optional) – Physical voxel spacing per axis (e.g., (5.0, 1.0, 1.0) for Z-anisotropic microscopy). A scalar means isotropic spacing. Affects: - max_eccentricity: evaluated in physical space - Auto init_sigma: based on physical dimensions - Output coordinates: converted to physical space (see output_space) If None (default), all voxels are treated as unit-spaced.

  • output_space (str, default "real") –

    Coordinate system for output Gaussians:

    • "real": Physical coordinates (centers and Cholesky scaled by voxel_size). When voxel_size is None, identical to "voxel".

    • "voxel": Raw voxel indices (no conversion).

  • seed_amps_background_relative (bool, default False) –

    Which intensity convention the amplitudes of a seeds=GSplatData carry. Ignored for every other kind of seeds.

    • False (default): RAW-IMAGE-SAMPLED — the amplitudes were sampled off the original volume (up to a fixed seeding scale factor), background pedestal included. This is what generate_seeds() returns, i.e. the explicit-seeding workflow (fit_gaussian_splats(V, seeds=generate_seeds(V))). They are rescaled as (a - image_min) / intensity_range, so an active floor is subtracted exactly once.

    • True: BACKGROUND-RELATIVE — the amplitudes already have the pedestal removed. This is what a previous fit returns (the fit’s output amplitudes are the normalized ones times intensity_range, with image_min never added back), hence also what a .gsplats.zarr WRITTEN BY a fit (gsplat fit / gsplat lod) carries. They are rescaled as a / intensity_range.

    A store that was IMPORTED (gsplat import maps PLY/SPZ opacity into roughly [0, 1]) or intensity-rescaled (gsplat transform --normalize-intensity / --scale-intensity) carries neither convention exactly, so its warm start is approximate either way.

    Getting this wrong is silent: declaring False on a fit’s output makes an active floor be subtracted twice, initializing every seed dimmer than the floor to exactly 0; declaring True on raw amplitudes starts every seed too bright by floor / intensity_range (#1172).

  • source_dtype (str or None, default None) – Element type of the volume as it was ACQUIRED (e.g. "uint16"), when that differs from V.dtype. Recorded in stats["source_dtype"] / stats["source_bytes"] — the honest denominator of a compression ratio. Pass it when the volume has already been cast to float before reaching here (as luxar.io.volume.load_volume does), otherwise the recorded source size describes the float working copy and overstates compression by the cast’s inflation factor. If None, V.dtype is used.

  • source_shape (sequence of int or None, default None) – Grid of the ACQUISITION this fit represents, when the caller preprocessed before fitting. Most producers do: a demo that pulls one channel out of a 5D OME-Zarr, downscales it and normalizes it hands over an array that is no longer the data anyone means by “the source”, so measuring V would quote compression against the working copy. Pair it with source_dtype — a declared grid with the cast’s dtype is still the wrong denominator. Recorded with stats["source_declared"] = True so a reader can tell a stated grid from a measured one. If None, V’s own shape is used, which is correct whenever nothing was preprocessed.

Returns:

Dataclass containing all fitting results: - centers: np.ndarray, shape (N, d) - Center positions (physical or voxel, see output_space) - amplitudes: np.ndarray, shape (N,) - Non-negative amplitudes rescaled to original intensity - cholesky_factors: np.ndarray, shape (N, d*(d+1)//2) - Packed lower-triangular Cholesky factors - stats: Dict[str, Any] - Optimization statistics (time, iterations, convergence, etc.)

All arrays represent the BEST state encountered during optimization (lowest loss). Note: Gaussian splatting cannot represent uniform DC components - only variations.

Return type:

GSplatData

Notes

The optimization uses standard PyTorch Adam combined with fixed-pool splat relocation: - Gradient-dilution-compensated learning rates for dimensional consistency - Periodic relocation of low-importance splats to high-residual regions - Optimizer state reset for relocated splats; all others untouched - Early stopping and adaptive learning-rate scheduling

luxar.gsplats.fit_progressive_gaussian_splats(V: ndarray, max_splats: int = 50000, max_splats_per_pass: int = 5000, iters_per_pass: int = 1000, psnr_patience: float = 0.5, max_passes: int | None = None, asymmetric_penalty: float | None = 10.0, enable_dynamic_ops: bool = False, cull_retention: float | None = 0.98, on_pass_complete: Callable[[int, AdditiveSubLOD, float], None] | None = None, device: str | None = None, verbose: bool = True, truncate: float = 2.75, residual_pass_min_iters: int = 500, **kwargs: Any) → GSplatData[source]

Fit Gaussian splats progressively via iterative residual decomposition.

Each pass fits up to max_splats_per_pass splats to the current residual (clamp(V - render(accumulated), min=0)). Passes continue until max_splats is reached, PSNR improvement drops below psnr_patience dB, or the residual becomes negligible.

Parameters:
  • V (np.ndarray) – Input volume to approximate (any dimensionality).

  • max_splats (int) – Maximum total number of splats across all passes.

  • max_splats_per_pass (int) – Maximum number of splats to fit per pass. The actual count may be lower due to post-fit culling and adaptive reduction when previous passes show high culling rates.

  • iters_per_pass (int) – Optimization iterations per pass.

  • psnr_patience (float) – Stop if ΔPSNR between consecutive passes < this value (in dB).

  • max_passes (int, optional) – Maximum number of passes. If None, continues until max_splats is reached or PSNR patience triggers.

  • asymmetric_penalty (float, optional) – Asymmetric loss penalty factor (default 10.0).

  • enable_dynamic_ops (bool, default False) – Whether to enable dynamic splat relocation within each pass. Off by default for progressive fitting: each pass seeds directly at the residual peaks, so relocation shows no measured quality benefit. Set to True to opt in.

  • cull_retention (float or None, default 0.98) – Post-fit cumulative culling on the final accumulated result. Keeps the top splats that account for this fraction of total amplitude (0–1). Set to None to disable.

  • on_pass_complete (callable, optional) – Callback invoked after each pass: on_pass_complete(pass_index, lod_data, cumulative_psnr).

  • device (str, optional) – Device for fitting and rendering (auto-detected if None).

  • verbose (bool) – Whether to print progress information.

  • truncate (float) – Truncation radius in standard deviations for rendering.

  • residual_pass_min_iters (int, default 500) – Minimum optimizer iterations for residual passes (pass 1+). Pass 0 always honours iters_per_pass directly. The default of 500 is the historical floor that protects fit quality when callers supply a small iters_per_pass (the decayed value can otherwise drop below what residual passes need to converge). Lower this only for tests that need short runtime — production callers should leave it at the default.

  • **kwargs – Additional keyword arguments passed through to fit_gaussian_splats. norm_range (a whole-volume intensity scale, as tiled fitting supplies) applies to pass 0 only: passes 1+ fit a residual that is by construction a small fraction of that range, and normalizing it against the range would put it under the absolute convergence tolerance and end the pass immediately. Residual passes keep their own per-pass scale.

Returns:

Single-LOD result containing all splats from all passes. The per-pass intermediate LODs are surfaced through on_pass_complete and the stats dict (stats['n_passes'], stats['pass_psnrs']); to build a streamable LOD ladder, hand the result to luxar.gsplats.lod.make_additive_lod().

Return type:

GSplatData

Notes

GPU utilization: Each pass fits only max_splats_per_pass splats, which may under-saturate the GPU compared to a single large fit. When using tiled fitting on a cluster (luxar gsplat batch-fit submit), combine --progressive with --parallel to run multiple tiles concurrently on the same GPU and fill the utilization gap.

class luxar.gsplats.GSplatData(centers: np.ndarray | None = None, amplitudes: np.ndarray | None = None, cholesky_factors: np.ndarray | None = None, colors: np.ndarray | None = None, label_ids: np.ndarray | None = None, label_vocabulary: Mapping[int, str] | None = None, stats: Dict[str, Any] | None = None, *, additive_sublods: List[AdditiveSubLOD] | None = None, substitutive_levels: List[SubstitutiveLevel] | None = None, truncation_radius: float = 2.75, _node: GSplatNode | None = None)[source]

Bases: RenderMixin, IOAdapterMixin, FilteringMixin, CullingMixin, LODViewsMixin, CompositionMixin, TransformsMixin, IntensityMixin

Container for Gaussian splat data with always-LOD structure.

Every GSplatData holds one or more LOD levels (AdditiveSubLOD instances). A single-LOD dataset is simply additive_sublods=[one_lod].

Construction styles:

# Convenience constructor (wraps into single LOD internally):
GSplatData(centers=c, amplitudes=a, cholesky_factors=cf)

# Explicit LOD construction:
GSplatData.from_additive_sublods([lod0, lod1, lod2])

Top-level centers, amplitudes, cholesky_factors, colors, and label_ids are the concatenation of all additive LODs, computed once at construction time. The object is conceptually immutable — all operations return new instances.

The behaviour is split across the domain mixins in luxar.gsplats._data (render / io / filtering / culling / lod views / composition / transforms / intensity); this class holds construction, the truncation radius and the repr.

additive_sublods

Additive sub-LODs of the default substitutive level. Always >= 1.

Type:

List[AdditiveSubLOD]

centers

Cached concatenation of all LOD centers.

Type:

np.ndarray, shape (N_total, d)

amplitudes

Cached concatenation of all LOD amplitudes.

Type:

np.ndarray, shape (N_total,)

cholesky_factors

Cached concatenation of all LOD packed lower-triangular factors L of the covariance (Σ = L·Lᵀ), using the convenience-constructor convention: isotropic std σ uses [σ, 0, σ, 0, 0, σ], not 1/sigma.

Type:

np.ndarray, shape (N_total, tril)

colors

Cached concatenation of all LOD colors (None if no LOD has colors). The optional 4th column is per-splat opacity alpha in [0, 1].

Type:

Optional[np.ndarray], shape (N_total, 3) or (N_total, 4)

label_ids

Cached concatenation of exact categorical ids.

Type:

Optional[np.ndarray], shape (N_total,)

label_vocabulary

Shared id-to-name vocabulary for the categorical channel.

Type:

Optional[Dict[int, str]]

stats

Top-level statistics (overall quality, timing, etc.).

Type:

Dict[str, Any]

__init__(centers: np.ndarray | None = None, amplitudes: np.ndarray | None = None, cholesky_factors: np.ndarray | None = None, colors: np.ndarray | None = None, label_ids: np.ndarray | None = None, label_vocabulary: Mapping[int, str] | None = None, stats: Dict[str, Any] | None = None, *, additive_sublods: List[AdditiveSubLOD] | None = None, substitutive_levels: List[SubstitutiveLevel] | None = None, truncation_radius: float = 2.75, _node: GSplatNode | None = None) → None[source]

Build the single in-memory ground truth: a matrix-shaped node tree.

The historical substitutive_levels / additive_sublods matrix API is preserved as derived finest-first views over self._node (which is stored coarsest-first, matching disk). _node is the internal fast path (used by from_tree()) that stores a pre-built node verbatim.

__len__() → int

Return number of splats.

additive_prefix(level: int) → GSplatData

Return a new GSplatData with LODs 0 through level (inclusive).

The returned object’s arrays are read-only zero-copy views of this one’s (the class is conceptually immutable); mutating them raises rather than silently corrupting the source.

A STRICT prefix holds fewer splats than the object the INHERITED top-level measured scores were taken on, so the view does not carry them (#1600): a view is a reduction like any other. The full prefix (level == n - 1) IS the input content and keeps everything. Only the view’s own top-level dict is scrubbed — each rung’s stats (its own ladder PSNR, its e(k)) is a statement about that rung, which the prefix still holds unchanged.

Parameters:

level – Maximum LOD level to include (0 <= level < n_additive_sublods).

Returns:

New GSplatData with level + 1 LODs.

Raises:

IndexError – If level is out of range.

additive_sublod(level: int) → AdditiveSubLOD

Return the AdditiveSubLOD at the given level.

Parameters:

level – LOD level index (0 = coarsest).

affine_intensity(scale: float = 1.0, offset: float = 0.0) → GSplatData

Apply affine transform to amplitudes: new_amp = scale * amp + offset.

Parameters:
  • scale – Multiplicative factor.

  • offset – Additive offset.

Returns:

New GSplatData with transformed amplitudes.

at_substitutive(level: int) → GSplatData

Return a single-substitutive-level view as a new GSplatData.

The returned object has n_substitutive == 1 and its lone substitutive level carries the additive ladder of self’s level level. Useful for operating one substitutive level at a time (e.g., data.at_substitutive(s).flattened()).

A COARSER level (level > 0) is a different, MERGED splat set, so the view does not inherit the top-level measured reconstruction scores (#1600) — this is the chokepoint through which lod --recipe overview builds its merged coarse cap (as at_substitutive(n - 1).flattened()) and published the input fit’s psnr_db on it. Level 0 is the finest content itself and keeps them. (_view_of_level itself does not scrub: _map_substitutive walks every level through it and discards the view’s top-level stats, so only the callers that know the index can tell a reduction from a rebuild step.)

The level’s OWN stamps are untouched — its level_stats Q / w and each rung’s e(k) are measured on this level’s content, not inherited from the finest, and this method is a plain accessor on the scene-authoring path (lod_dispatch.py builds every coarse child of a kind=lod group with at_substitutive(s) and copies those numbers onto it).

Parameters:

level – Substitutive level index (0 = finest).

center_at_centroid() → GSplatData

Center the splats at their center of mass (amplitude-weighted centroid).

The centroid is the amplitude-weighted average of splat centers (the center of mass of the represented density). Only the spatial (non-degenerate) axes are re-origined: a zero-variance categorical axis (a per-timepoint time axis, a channel axis) keeps its original coordinates, because centering it would push integer timepoints to fractional offsets and misalign the viewer’s slice navigator. For pure spatial data (no degenerate axis) every axis is centered, as before.

Returns:

New GSplatData with its spatial centroid at the origin.

Example

>>> # Center splats at origin for easier viewing
>>> centered = data.center_at_centroid()
clamp_intensity(min: float | None = None, max: float | None = None) → GSplatData

Clamp amplitudes to a range.

Parameters:
  • min – Lower bound (None = no lower bound).

  • max – Upper bound (None = no upper bound).

Returns:

New GSplatData with clamped amplitudes.

classmethod combine_as_new_dimension(datasets: list[GSplatData], values: np.ndarray | Sequence[float | np.ndarray] | None = None, sigma: float = 0.0, *, part_provenance: Sequence[Dict[str, Any]] | None = None) → GSplatData

Combine datasets by embedding each into a new dimension, then concatenating.

Each dataset is promoted from D-dimensional to (D+1)-dimensional by appending a coordinate in the new dimension, then all are concatenated into a single dataset.

This is useful for combining per-timepoint 3D fits into a single 4D dataset, per-slice 2D fits into 3D, or any similar stacking operation.

Parameters:
  • datasets – List of GSplatData, all with the same ndim.

  • values – Coordinate for each dataset in the new dimension. If None, uses 0.0, 1.0, 2.0, … (one per dataset). If scalar-per-dataset, all splats in that dataset get the same coordinate. Can also be a list of per-splat arrays if different splats within a dataset need different coordinates.

  • sigma – Standard deviation in the new dimension. Use 0.0 for discrete dimensions (e.g., time frames) where splats should not extend across the new axis. Use a positive value for continuous dimensions where splats should have Gaussian extent.

  • part_provenance – Optional caller-supplied component-fit records, one entry per dataset in the same order as values. This requires one scalar coordinate per dataset.

Returns:

Single GSplatData with ndim+1 dimensions containing all splats.

Raises:

ValueError – If datasets is empty, lengths mismatch, ndims differ, or categorical labels are present on only some inputs or use different vocabularies.

Example

>>> # Combine 3D timepoints into 4D
>>> combined = GSplatData.combine_as_new_dimension(
...     [t0_3d, t1_3d, t2_3d], sigma=0.0
... )
>>> combined.ndim  # 4
>>> combined.n_splats  # sum of all timepoints
classmethod concatenate(datasets: list['GSplatData']) → GSplatData

Concatenate multiple GSplatData objects into one.

All datasets must share the same dimensionality, truncation radius, and number of substitutive levels. The full 2-D LOD matrix is preserved: merging is done per (substitutive, additive) cell, so concatenating pyramids yields a pyramid (no level is silently dropped). To merge across a mismatched substitutive hierarchy, flattened() the inputs first.

Colors: if all have colors, concatenate; if all None, None; if mixed, fill missing with white (1,1,1).

Parameters:

datasets – List of GSplatData (same ndim, truncation_radius, and n_substitutive required).

Returns:

New GSplatData with all splats concatenated per LOD cell.

Raises:

ValueError – On empty input list, or mismatched ndim / truncation_radius / n_substitutive across datasets; also when categorical labels are present on only some inputs or use different vocabularies.

cull(target: np.ndarray | None = None, *, method: str = 'auto', shape: tuple[int, ...] | None = None, truncate: float | None = None, error_percentile: float = 99.0, error_tolerance: float = 1.0, redundancy_threshold: float = 0.01, max_binary_search_iters: int = 8, device: str | None = None, intensity_floor: float = 1e-05, retention: float = 0.95, amplitude_percentile: float = 5.0, volume_percentile: float = 95.0, verbose: bool = False) → GSplatData

Cull splats that contribute negligibly to the reconstruction.

This is the unified entry point for all splat removal strategies, from fast heuristics to principled contribution-based methods. The method parameter selects which strategy to use.

Methods (ordered from cheapest to most principled)

“cumulative” — Keep the top splats that account for a target fraction of the total amplitude. Fast (no rendering), but blind to spatial overlap: a low-amplitude splat covering a unique region will be removed even though it is the sole contributor there.

>>> data.cull(method="cumulative", retention=0.95)

“amplitude_percentile” — Remove splats in the bottom X percentile of amplitude. Same limitation as cumulative: ignores spatial context.

>>> data.cull(method="amplitude_percentile", amplitude_percentile=10)

“combined” — Remove splats that have low amplitude OR unusually large volume (artifacts). Useful as a quick cleanup pass.

>>> data.cull(method="combined", amplitude_percentile=5, volume_percentile=95)

“redundancy” — Render the full reconstruction and measure each splat’s maximum fractional contribution g_j(x) / V_pred(x). If a splat never contributes more than redundancy_threshold of the local signal, it is redundant. Does not need the target volume but requires GPU rendering.

>>> data.cull(method="redundancy", shape=(128,128,128), redundancy_threshold=0.02)

“error_budget” — The most principled mode. Requires the original target volume. Computes the residual R = target - V_pred and derives an error budget from it. A splat is safe to remove when the worst-case error increase from its removal is below the budget. Robust to pre-existing noise and accounts for spatial redundancy.

>>> data.cull(target_volume, method="error_budget", error_percentile=99)

“auto” (default) — Selects automatically: "error_budget" if target is provided, "redundancy" if shape is provided, "cumulative" otherwise.

Joint compounding check (error_budget and redundancy only)

After identifying individual candidates, verifies that their joint removal does not exceed the budget. If it does, a binary search tightens the per-splat threshold until the joint constraint holds, guaranteeing that the combined removal is safe.

param target:

Original target volume. If provided and method="auto", selects error-budget mode.

param method:

Culling strategy. One of "auto", "error_budget", "redundancy", "cumulative", "amplitude_percentile", "combined".

param shape:

Volume shape for rendering (error_budget / redundancy). Defaults to target.shape when target is provided.

param truncate:

Truncation radius in standard deviations. Defaults to self.truncation_radius.

param error_percentile:

error_budget only. Percentile of |residual| for the budget (0–100).

param error_tolerance:

error_budget only. Multiplier on the budget.

param redundancy_threshold:

redundancy only. Max fractional contribution (0–1) below which a splat is redundant.

param max_binary_search_iters:

error_budget / redundancy only. Max iterations for the joint compounding binary search.

param device:

Device for GPU computation. Auto-detected for None or "auto".

param intensity_floor:

Min intensity threshold for AABB computation.

param retention:

cumulative only. Fraction of total amplitude to retain (0–1).

param amplitude_percentile:

amplitude_percentile / combined only. Bottom percentile to remove (0–100).

param volume_percentile:

combined only. Remove splats above this volume percentile (0–100).

param verbose:

Print progress information.

returns:

New GSplatData with culled splats removed. Stats include culled, culling_method, n_original, n_culled.

property default_substitutive: int

Index of the data-model default substitutive level (finest = 0).

Fixed in the finest-first matrix view; not settable. Distinct from the on-disk default_level (the viewer’s coarsest-first progressive-load hint).

eccentricities(axes: Sequence[int] | None = None) → ndarray

Per-splat eccentricity: max marginal sigma / min marginal sigma.

1.0 = isotropic. Higher values = more elongated. By default the ratio is taken over the auto-detected non-degenerate (spatial) axes — for pure 3D data this is all axes (unchanged), but on a timelapse it ignores the ~zero-variance time axis (which would otherwise force the degenerate 1.0 fallback for every splat).

Returns:

shape (N,) float array. Returns 1.0 for degenerate splats.

embed_dimension(values: np.ndarray | float, sigma: float = 0.0) → GSplatData

Add a new dimension to the splat data.

Appends a column to centers and embeds Cholesky factors into the higher-dimensional space.

Parameters:
  • values – Coordinate for the new dimension. Scalar (same for all) or (N,) array (per-splat).

  • sigma – Standard deviation in the new dimension (default 0.0 for discrete dimensions like time).

Returns:

New GSplatData with ndim+1 dimensions.

Example

>>> data_4d = data_3d.embed_dimension(5.0, sigma=0.0)
>>> data_4d = data_3d.embed_dimension(time_values, sigma=0.5)
filter(mask: np.ndarray) → GSplatData

Return new GSplatData with only the splats where mask is True.

Parameters:

mask – Boolean array of shape (N,).

Returns:

New GSplatData with filtered arrays.

Removing any splat drops the inherited measured reconstruction scores and the inherited reduction record (see _CONTENT_SCOPED_STATS_KEYS and _CONTENT_SCOPED_OP_RECORD_KEYS): this is the single chokepoint every mask-based rewrite goes through — filter_by, slice_by and every cull strategy — so scrubbing here covers all of them, and the record each of those stamps AFTERWARDS (culled, n_original, filter_criteria, …) describes THIS operation and lands on a clean dict.

Example

>>> filtered = data.filter(data.volumes() < 100)
>>> filtered = data.filter((data.amplitudes > 0.1) & (data.eccentricities() < 5))
filter_by(*, bbox: list[tuple[float, float]] | None = None, volume_min: float | None = None, volume_max: float | None = None, volume_normalized: bool = False, volume_percentile: bool = False, scale_min: float | None = None, scale_max: float | None = None, scale_normalized: bool = False, scale_percentile: bool = False, amplitude_min: float | None = None, amplitude_max: float | None = None, amplitude_normalized: bool = False, amplitude_percentile: bool = False, eccentricity_min: float | None = None, eccentricity_max: float | None = None, eccentricity_percentile: bool = False, mass_min: float | None = None, mass_max: float | None = None, mass_normalized: bool = False, mass_percentile: bool = False, sigma_axis: int | None = None, sigma_min: float | None = None, sigma_max: float | None = None, sigma_percentile: bool = False, isolation_max: float | None = None, isolation_percentile: bool = False, min_neighbors: int | None = None, neighbor_radius: float | None = None, spatial_dims: Sequence[int] | None = None, truncate: float | None = None) → GSplatData

Filter splats by multiple criteria (AND logic).

All criteria are optional. Only specified criteria are applied. Multiple criteria combine with AND — a splat must satisfy all active criteria to be kept.

Parameters:
  • bbox – Bounding box per dimension as [(min0, max0), (min1, max1), …]. Length must equal ndim. Filters by center position.

  • volume_min – Minimum volume (characteristic length * truncate).

  • volume_max – Maximum volume.

  • volume_normalized – If True, interpret volume thresholds as 0-1 mapped to the dataset’s [min, max] volume range.

  • amplitude_min – Minimum amplitude.

  • amplitude_max – Maximum amplitude.

  • amplitude_normalized – If True, interpret amplitude thresholds as 0-1 mapped to the dataset’s [min, max] amplitude range.

  • eccentricity_min – Minimum eccentricity (1.0 = isotropic).

  • eccentricity_max – Maximum eccentricity.

  • mass_min – Minimum mass (amplitude * volume).

  • mass_max – Maximum mass.

  • mass_normalized – If True, interpret mass thresholds as 0-1 mapped to the dataset’s [min, max] mass range.

  • sigma_axis – Axis index for per-axis sigma filtering.

  • sigma_min – Minimum marginal sigma on sigma_axis.

  • sigma_max – Maximum marginal sigma on sigma_axis.

  • scale_min/scale_max – Characteristic size (geometric-mean marginal sigma over the spatial/spatial_dims axes; see scale()). The recommended “remove large diffuse background” knob — cleaner than volume on nD timelapses.

  • isolation_max – Remove splats whose nearest-neighbour distance (over the spatial axes, grouped by the non-spatial axes) EXCEEDS this — i.e. spatially isolated noise splats.

  • neighbor_radius (min_neighbors /) – Remove splats with fewer than min_neighbors other splats within neighbor_radius.

  • spatial_dims – Override the axes used for scale / eccentricity / isolation (default: auto-detected non-degenerate axes).

  • *_percentile – For volume/scale/amplitude/mass/sigma/eccentricity/ isolation — interpret the corresponding min/max as a percentile in [0,100] of that attribute (robust on heavy-tailed data).

  • truncate – Sigma truncation factor for volume computation. Defaults to self.truncation_radius.

Returns:

New GSplatData with only splats that pass all criteria.

Raises:

ValueError – If bbox length doesn’t match ndim, sigma_axis is out of range, or sigma_min/sigma_max given without sigma_axis.

Examples

>>> # Keep splats with amplitude >= 0.1 and eccentricity <= 5
>>> filtered = data.filter_by(amplitude_min=0.1, eccentricity_max=5.0)
>>>
>>> # Spatial crop to a bounding box (3D)
>>> filtered = data.filter_by(bbox=[(0, 50), (0, 50), (0, 50)])
>>>
>>> # Remove top 10% largest volumes (normalized)
>>> filtered = data.filter_by(volume_max=0.9, volume_normalized=True)
flattened() → GSplatData

Collapse all LODs into a single LOD.

The returned object’s arrays are read-only zero-copy views of this one’s, honouring the immutability contract: mutating them raises rather than silently corrupting the source.

Returns:

New GSplatData with n_additive_sublods == 1 containing all splats.

classmethod from_additive_sublods(additive_sublods: List['AdditiveSubLOD'], stats: Dict[str, Any] | None = None) → GSplatData

Construct a GSplatData from a list of additive sub-LODs.

The result has n_substitutive == 1 (single substitutive level) whose additive ladder is the given list.

Parameters:
  • additive_sublods – List of AdditiveSubLOD (at least one).

  • stats – Optional top-level statistics.

classmethod from_default_selection(node: GSplatNode, *, stats: Dict[str, Any] | None = None) → GSplatData

Materialize the tree selection rendered by default.

Matrix-shaped nodes are preserved verbatim, including their additive and substitutive ladders. Nested trees become one flat dataset containing every partition part and only each LOD group’s default (finest) child. Call flattened() on the result when the caller requires one rung.

classmethod from_substitutive_levels(substitutive_levels: List['SubstitutiveLevel'], stats: Dict[str, Any] | None = None) → GSplatData

Construct a 2-D GSplatData from a list of substitutive levels.

Each SubstitutiveLevel carries its own additive ladder (one or more AdditiveSubLOD). The resulting GSplatData has n_substitutive == len(substitutive_levels) and represents the full [N, M_i] matrix of splat sets. The accessors (.centers/.additive_sublods/…) always return the FINEST level (index 0) — the data-model default is fixed, not settable (see __init__).

Parameters:
  • substitutive_levels – Ordered list, finest at index 0.

  • stats – Optional top-level statistics.

Returns:

New GSplatData with the given substitutive × additive matrix.

classmethod from_tree(node: GSplatNode, stats: Dict[str, Any] | None = None) → GSplatData

Construct a GSplatData from a matrix-shaped tree node.

Accepts a bare GSplatLeaf or a GSplatLodGroup of leaves (the inverse of tree). Genuinely nested trees (partitions, or lod groups with non-leaf children) have no flat GSplatData equivalent and raise — they must be consumed through the tree directly.

classmethod load(path: str | Path, include_stats: bool = False) → GSplatData

Load splats from .gsplats.zarr format.

Parameters:
  • path – Path to .gsplats.zarr directory

  • include_stats – Whether to include fitting/provenance metadata

Returns:

GSplatData with decoded arrays

Example

>>> data = GSplatData.load("fitted.gsplats.zarr")
>>> aprint(data.centers.shape)
lod_psnrs() → list[float]

Extract cumulative PSNR from each LOD’s stats.

Returns:

List of PSNR values (one per LOD). NaN if not available.

marginal_sigmas() → ndarray

Per-dimension standard deviation: sqrt(Sigma_ii).

For lower-triangular L: Sigma[i,i] = sum_j L[i,j]^2.

Returns:

shape (N, d) float array.

masses() → ndarray

Per-splat mass: amplitude * volume.

Returns:

shape (N,) float array.

classmethod merge_with_channel_colors(gsplats_per_channel: list['GSplatData'], channel_colors: list[tuple[float, float, float]]) → GSplatData

Merge multiple GSplatData objects, assigning a fixed color per channel.

This is useful for multi-channel visualization where each channel was fitted separately and should be displayed with a distinct color.

Parameters:
  • gsplats_per_channel – List of GSplatData objects, one per channel. All must have the same dimensionality.

  • channel_colors – List of RGB color tuples (one per channel). Each tuple should have values in [0, 1] range, e.g., (1.0, 0.0, 0.5).

Returns:

New GSplatData with all splats merged and colors assigned.

Raises:

ValueError – If lists have different lengths or dimensionalities don’t match.

Example

>>> # Fit each channel separately
>>> gsplats_ch0 = fit_gaussian_splats(volume_ch0, ...)
>>> gsplats_ch1 = fit_gaussian_splats(volume_ch1, ...)
>>>
>>> # Merge with magenta for ch0, cyan for ch1
>>> merged = GSplatData.merge_with_channel_colors(
...     [gsplats_ch0, gsplats_ch1],
...     channel_colors=[(1.0, 0.0, 0.5), (0.0, 1.0, 0.5)],
... )
>>>
>>> # Add to scene
>>> scene.add_gsplats_from_data("multichannel", merged)
property n_additive_sublods: int

Number of LOD levels.

property n_splats: int

Number of splats.

property n_substitutive: int

Number of substitutive levels (always >= 1).

property ndim: int

Number of spatial dimensions.

nearest_neighbor_distances(spatial_axes: Sequence[int] | None = None, group_axes: Sequence[int] | None = None, k: int = 1) → ndarray

Distance from each splat to its k-th nearest neighbour.

Computed over the spatial axes and grouped by the non-spatial axes (so a timelapse’s timepoints never count as neighbours). Large distance = spatially isolated (a noise-splat signature). Returns shape (N,); +inf where a group has <= k splats (no neighbour exists).

neighbor_counts(radius: float, spatial_axes: Sequence[int] | None = None, group_axes: Sequence[int] | None = None) → ndarray

Number of OTHER splats within radius (Euclidean, spatial axes), grouped by the non-spatial axes. Returns shape (N,) int64. Low count = spatially isolated.

normalize_intensity(target_max: float = 1.0) → GSplatData

Normalize amplitudes so the maximum equals target_max.

Parameters:

target_max – Desired maximum amplitude (default 1.0).

Returns:

New GSplatData. Returns copy if all amplitudes are zero.

static partition_from_regions(regions: List[GSplatData], *, recipe: str | None = None, recipe_params: Any | None = None, bsp_tree: Dict[str, Any] | None = None, region_labels: Sequence[int] | None = None) → GSplatNode

Assemble a kind=partition tree from pre-decomposed spatial regions.

Unlike to_spatial_partition() (which BSP-splits a flat splat set), this keeps the given spatial decomposition: each region becomes one partition part, preserving the exact tile/box boundaries the fitter already produced. Used by tiled / content-aware fitting, where the regions are the per-tile (apodized) or per-box (core-kept) splats — both sum correctly as additive partition parts, so the partitioned render equals the flat concatenation with no double-count.

With recipe (one of PER_PART_RECIPES: stream → tiles topology, levels → adaptive) each part is given its OWN LOD via build_part_lod() (clamped to the part’s splat count), so the output is a partition whose every child carries a ladder/lod-group — the fit-time equivalent of a per-part gsplat lod pass (which cannot run on a partition). Without a recipe each part is a bare leaf (the historical behaviour).

Empty regions (0 splats) are dropped. With a single non-empty region the bare part node is returned (no 1-part partition wrapper); with none, raises. Returns a tree node (write with write_gsplats_tree or embed in a scene) — a partition has no flat-matrix GSplatData equivalent.

bsp_tree is the decomposition’s serialized split planes — the producer’s, since this method is handed a decomposition rather than computing one (contrast to_spatial_partition(), which splits and so knows its own planes). Supplying it is what lets the viewer order the parts back-to-front EXACTLY instead of guessing from part centroids, which is not a valid painter’s order and pops at the seams as the camera orbits (#1555). Its leaf labels are read in region_labels space (default: the positions of regions), and it is pruned to the regions that survived the empty filter — so a caller passes the labels of the regions it is handing over and does not have to pre-compensate for drops itself.

principal_radii(anisotropy: bool = True) → ndarray

Per-splat element radius (world units) at the truncation boundary.

Used by gsplat filter (eccentricity / volume). The Gaussian is truncated at truncation_radius sigmas, so the radius is truncation_radius * semi_axis.

  • anisotropy=True → the largest principal semi-axis sqrt(lambda_max(Sigma)) (worst-case projected radius; orientation-independent — the splat’s biggest reach in any direction).

  • anisotropy=False → the isotropic-equivalent geometric-mean semi-axis det(Sigma)^(1/2d) (== sqrt(volumes())).

Returns:

shape (N,) float array.

render_to_volume(shape: tuple[int, ...], device: str | None = None, truncate: float | None = None, intensity_floor: float = 1e-05, chunk_size: int | None = None) → ndarray

Render Gaussian splats to a volume using GPU-accelerated rendering.

This is a convenience method that automatically selects the fastest available backend (CUDA, MPS, or CPU) and uses the optimized PyTorch renderer.

Parameters:
  • shape (tuple[int, ]) – Output volume shape (e.g., (128, 128, 128) for 3D).

  • device (str, optional) – Device to use for rendering. If None or "auto", auto-detects the best device. Options: "cuda", "mps", "cpu", "auto".

  • truncate (float, optional) – Truncation radius in standard deviations. Gaussians are evaluated within this radius from their centers. Defaults to self.truncation_radius.

  • intensity_floor (float, default 1e-5) – Minimum intensity threshold for amplitude-aware culling. Splats with contributions below this threshold are culled early for performance.

  • chunk_size (int, optional) – Chunk size for memory management when processing large volumes. If None, automatically calculated based on available memory.

Returns:

Rendered volume with the specified shape.

Return type:

np.ndarray

Examples

>>> # Render to 128³ volume
>>> volume = gsplat_data.render_to_volume(shape=(128, 128, 128))
>>>
>>> # Force CPU rendering
>>> volume = gsplat_data.render_to_volume(shape=(128, 128, 128), device="cpu")
>>>
>>> # Use larger truncation radius
>>> volume = gsplat_data.render_to_volume(shape=(128, 128, 128), truncate=4.0)

Notes

  • For 8K splats on 128³ volume: substantially faster than NumPy implementation (often orders of magnitude on GPU; varies by hardware)

  • Automatically chunks large volumes to prevent out-of-memory errors

  • Uses specialized fast paths for 2D/3D rendering

reweight_amplitude(multiplier: np.ndarray) → GSplatData

Return a copy with per-splat amplitudes multiplied by multiplier.

The per-splat counterpart of scale_intensity (which is scalar-only). multiplier must be shape (n_splats,) and operates on this (matrix / default-level) view; it preserves the additive ladder. A global multiplier is not meaningful across substitutive levels — callers with a pyramid should reweight per-level (see soft_scale_filter).

save(path: str | Path, ordering: Literal['morton', 'hilbert', 'none']='hilbert', encoding_mode: 'EncodingMode' | None = None, include_fitting_info: bool = True, include_provenance: bool = False, description: str | None = None, compress: Optional[Literal['zip', 'tar.gz']]=None, compressor: Any = <object object>, zip_deflate: bool = False, barrier_dims: Sequence[int] | None = None, root_attrs: dict | None = None, amplitude_bits: Literal['auto', 8, 16]=16) → None

Save splats to .gsplats.zarr format.

Parameters:
  • path – Output path (should end with .gsplats.zarr or .gsplats.zarr.zip/.tar.gz if compress is used)

  • ordering – Spatial ordering method (“morton”, “hilbert”, or “none”)

  • encoding_mode – Encoding mode (AUTO, PRECISION, or MEMORY), defaults to AUTO

  • amplitude_bits – AUTO amplitude quantization tier. 16 preserves the historical default; 8 opts into uint8 geometric-log codes; "auto" uses 8 bits only for 8-bit integer sources recorded in stats["source_dtype"] and otherwise uses 16.

  • include_fitting_info – Whether to include fitting statistics

  • include_provenance – Whether to include provenance info from stats

  • description – Optional user description

  • compress – Optional compression format (“zip” or “tar.gz”). Creates compressed archive.

  • zip_deflate – Use DEFLATE compression for the outer zip (default: STORED). Useful when metadata overhead matters, e.g. for Git LFS storage.

  • barrier_dims – Explicit categorical/barrier center columns for chunk ordering (e.g. a stacked-time axis). None (default) derives the barrier from the coarsen_dims complement in stats, else per-leaf auto-detect — see write_gsplats_tree.

  • root_attrs – Extra attrs seeded onto the root at LOWEST precedence (structural attrs still win). A structure-only rebuild passes the SOURCE root’s authored appearance here so it is not dropped — see luxar.gsplats.io.load_gsplats.read_authored_appearance.

For a multi-substitutive dataset the per-level coverage_fraction LOD switch thresholds are derived automatically as screen-area fractions (selector="screen-area"): full detail while the object occupies at least half the screen, one level coarser per halving of occupied area — so there is no per-dataset threshold knob. See core.group.lod.group.coverage_fractions.

Colors are written via the shared COLOR helper, which auto-detects SDR vs HDR (values > 1) — there is no explicit color_mode knob.

Example

>>> result = fit_gaussian_splats(image, n_iters=1000)
>>> result.save("fitted.gsplats.zarr", encoding_mode=EncodingMode.MEMORY)
>>> # With compression for storage/git-lfs
>>> result.save("fitted.gsplats.zarr.zip", compress="zip")
scale(axes: Sequence[int] | None = None) → ndarray

Per-splat characteristic size (world units): geometric mean of the marginal sigmas over axes.

Unlike volumes() (det(Σ)^(1/d) over ALL dims, which collapses on a zero-variance time axis), scale defaults to the auto-detected non-degenerate (spatial) axes, so it is the meaningful “size” metric for nD timelapses. Large scale = diffuse / low-frequency (background).

Returns:

shape (N,) float array.

scale_intensity(factor: float) → GSplatData

Scale all splat amplitudes by a multiplicative factor.

This effectively brightens (factor > 1) or dims (factor < 1) the entire representation.

Parameters:

factor – Multiplicative scaling factor for amplitudes

Returns:

New GSplatData with scaled amplitudes

Example

>>> # Reduce brightness by 10x
>>> dimmed = data.scale_intensity(0.1)
>>> # Brighten by 2x
>>> brightened = data.scale_intensity(2.0)
slice_by(slices: list[slice]) → GSplatData

Slice splats by coordinate ranges per dimension (numpy-style).

Each slice specifies a [start, stop] range for that dimension’s center coordinate. None in start/stop means unbounded.

Parameters:

slices – One slice per dimension. slice(lo, hi) keeps splats with center in [lo, hi]. slice(None, None) keeps all.

Returns:

New GSplatData with only splats inside all ranges.

Raises:

ValueError – If number of slices doesn’t match ndim.

Examples

>>> # Keep x in [0,50], all y, z in [10,90]
>>> sliced = data.slice_by([slice(0, 50), slice(None, None), slice(10, 90)])
>>>
>>> # Open-ended: x >= 50
>>> sliced = data.slice_by([slice(50, None), slice(None, None), slice(None, None)])
soft_scale_filter(*, highpass: float | None = None, lowpass: float | None = None, width: float = 1.0, spatial_dims: Sequence[int] | None = None) → GSplatData

Soft “frequency” filter: attenuate amplitude by a smooth function of each splat’s characteristic scale() — a gentler alternative to a hard scale cut (no popping, splat count unchanged).

  • highpass: suppress splats with scale ABOVE the cutoff (removes large diffuse / low-frequency background). Multiplier → 0 for very large scales, → 1 for small.

  • lowpass: suppress splats with scale BELOW the cutoff (removes fine detail / high-frequency). Multiplier → 0 for very small scales, → 1 for large.

Both may be combined (a band-pass). width is the transition softness in octaves (log2 scale); larger = gentler roll-off.

The cutoff is in the same world units as scale().

property substitutive_levels: List['SubstitutiveLevel']

Finest-first substitutive × additive matrix view, derived from the node.

Reconstructed on access from self._node (the single ground truth). Index 0 is the finest level — the historical matrix convention — independent of the node’s coarsest-first storage order.

to_spatial_partition(*, max_elements: int, rule: Literal['median', 'midpoint', 'sah'] = 'median') → GSplatPartition

Spatially partition the splats into a kind=partition tree node.

Recursively BSP-splits the splat centers so each part holds at most max_elements splats, using the shared splitters in luxar.core.group.partition (the same machinery the scene uses). Returns a GSplatPartition (a tree node, not a GSplatData — a partition has no flat-matrix equivalent); write it with write_gsplats_tree (one self-contained kind=partition file) or embed it in a scene. Each part gets its own position_bounds at write time so the viewer can frustum-cull per part.

A multi-LOD input is flattened to its default substitutive level first (BSP partitions a single splat set), matching partition().

transform(matrix: np.ndarray) → GSplatData

Apply affine transformation to all splats.

Transforms centers and covariance matrices. Amplitudes and colors are unchanged.

Parameters:

matrix – Either (d, d) for linear-only transform or (d+1, d+1) for full affine (last row must be [0..0, 1]).

Returns:

New GSplatData with transformed geometry.

Raises:
  • ValueError – If matrix shape is invalid.

  • np.linalg.LinAlgError – If transform produces non-positive-definite covariance.

Example

>>> scaled = data.transform(np.eye(3) * 2.0)
>>> M = np.eye(4); M[:3, 3] = [10, 20, 30]
>>> transformed = data.transform(M)
translate(offset: np.ndarray) → GSplatData

Translate all splat centers by an offset vector.

Parameters:

offset – Translation vector (shape: (d,) where d is spatial dimensions)

Returns:

New GSplatData with translated centers (all other data unchanged)

Example

>>> # Shift all splats by [10, 20, 30]
>>> translated = data.translate(np.array([10, 20, 30]))
property tree: GSplatNode

This dataset as a luxar.gsplats.tree node subtree.

The tree is the single in-memory ground truth (this just returns the stored node), behind the v3.0 .gsplats.zarr format and the scene gsplat-node subtree. For the matrix shape it is one of: a single GSplatLeaf (one substitutive level) or a GSplatLodGroup of leaves (multiple levels, coarsest first). Per-level provenance rides in each leaf’s meta. The view-driven coverage_fraction thresholds are derived at serialize time (see save() / the writer), not stored here.

volumes() → ndarray

Per-splat characteristic length: det(Σ)^(1/d).

This is the geometric mean of the eigenvalues (not a true volume). For lower-triangular L: det(L) = product of diagonal elements, det(Sigma) = det(L)^2.

Returns:

shape (N,) float array.

with_colors(colors: np.ndarray | tuple[float, ...]) → GSplatData

Return a new GSplatData with replaced colors, preserving LODs.

Parameters:

colors – Either an (N, 3) RGB / (N, 4) RGBA array of per-splat colors, or a single (r, g, b) / (r, g, b, a) tuple/array to broadcast to all splats. The alpha channel is per-splat opacity in [0, 1].

Returns:

New GSplatData with the specified colors.

with_label_ids(label_ids: np.ndarray | Sequence[int], label_vocabulary: dict[int, str]) → GSplatData

Attach exact categorical ids to a flat/additive gsplat dataset.

without_label_ids() → GSplatData

Remove categorical ids and vocabulary while preserving all LODs.

property truncation_radius: float

Gaussian truncation radius in standard deviations (from first LOD).

__repr__() → str[source]

Summary representation (avoids dumping full arrays).

property GSplatData.additive_sublods: List['AdditiveSubLOD']

The finest level’s additive ladder (the “primary” sub-LODs).

Returns a fresh list (the AdditiveSubLOD elements are shared) so a caller mutating it cannot corrupt the ground-truth node or desync the cached centers/n_splats — matching the pre-refactor defensive copy and the sibling substitutive_levels view’s semantics.

Tiled Fitting

Fit large volumes tile-by-tile. The uniform grid (luxar gsplat fit --tiling uniform) blends overlapping tiles with half-Hann (cosine) ramps for seamless stitching; content tiling (--tiling content) fits halo-padded boxes without a window and keeps each box’s core (see Content Planning below).

The Hann ramps conserve the reconstructed intensity (splat mass) across an overlap, but they do not preserve each splat’s amplitude: shared structure is represented by two tapered splat sets. A scalar colormap evaluated per splat can therefore reveal the crossfade even when an intensity residual is seamless. Use a display window that saturates the structure of interest, or content tiling when per-splat amplitude must remain comparable across spatial parts.

luxar.gsplats.fit_tiled_gaussian_splats(volume: Any, tile_size: int | Sequence[int] = 256, overlap: int | Sequence[int] = 32, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real', verbose: bool = True, progressive: bool = False, max_splats_per_pass: int = 5000, psnr_patience: float = 0.5, max_passes: int | None = None, cull_retention: float | None = 0.95, partition: bool = False, recipe: str | None = None, recipe_params: Any | None = None, fold_tile_slivers: bool = True, tile_seed_counts: Sequence[int] | None = None, source_shape: Sequence[int] | None = None, source_dtype: str | None = None, source_stored_bytes: int | None = None, **fit_kwargs: Any) → Any

Fit Gaussian splats to a large volume using tiled decomposition.

Splits the volume into overlapping tiles with cosine apodization (Hann window), fits each tile independently, and merges results. The background floor (floor in fit_kwargs, default "auto") is resolved once against the whole volume and subtracted from each tile before windowing; on the floor-subtracted data the Hann partition-of-unity property guarantees seamless blending. When per-tile denoising is active (_denoise_h / _denoise_params), the level is resolved on the denoised basis, matching the non-tiled path (#1178) — with the one documented exception that the default "auto" spec on a volume above the denoise probe’s budget keeps its raw-basis level, since the histogram-mode shift is not measurable on a bounded crop.

When progressive=True, each tile is fitted in several residual passes; the progressive fitter flattens its passes before returning, so the tiles are concatenated exactly as in the single-pass case. (Tile results that do carry additive sub-LODs are merged level by level.)

Parameters:
  • volume (np.ndarray or zarr.Array) – Full volume. Can be a lazy zarr array for out-of-core processing — only one tile at a time is materialized in memory.

  • tile_size (int or tuple of int, default 256) – Tile size per axis in voxels. Scalar is broadcast to all axes.

  • overlap (int or tuple of int, default 32) – Overlap width per axis in voxels. Scalar is broadcast.

  • voxel_size (float or sequence of float, optional) – Physical voxel spacing, forwarded to per-tile fitting.

  • output_space (str, default "real") – Coordinate space for output centers ("real" or "voxel").

  • verbose (bool, default True) – Print per-tile progress with arbol.

  • progressive (bool, default False) – Optimize each tile in several residual passes. The fitter flattens those passes into one splat set before the tiles are merged.

  • max_splats_per_pass (int, default 5000) – Maximum splats per progressive pass (ignored if progressive=False).

  • psnr_patience (float, default 0.5) – Stop progressive passes if ΔPSNR < this value in dB.

  • max_passes (int, optional) – Maximum number of progressive passes (None = unlimited).

  • fold_tile_slivers (bool, default True) –

    Fold a trailing tile whose unique coverage is smaller than the overlap into its predecessor, instead of emitting an overlap-dominated sliver.

    This CHANGES OUTPUT versus pre-#2838, where the default was False. There is now exactly ONE uniform grid: this function, the fit --tiling uniform sequential and -j N paths, the fit --tile k/M worker, and every batch-fit producer all build it. On (108, 1352, 532) at tile_size=512, overlap=32 that is 3 tiles, where the unfolded grid had 6 — so the same call on the same volume now fits different regions with different per-tile budgets, and the result differs from a pre-#2838 one by more than a content_hash. Pass False to reproduce a historical grid.

    A folded tile also EXCEEDS tile_size: it spans up to tile_size + overlap - 1 voxels on the folded axis, so peak per-tile memory is that much above what tile_size alone suggests (256/32 -> 287, 1.41x the voxels in 3D; 256/64 -> 319, 1.93x; 24/8 -> 31, 2.16x). Size tile_size for that worst case.

  • tile_seed_counts (sequence of int, optional) – Exact per-tile integer seed counts in grid order. When provided, this overrides seeds for each tile; its length must match the resolved grid. This is the CLI handoff for occupancy-weighted whole-volume budgets. A 0 entry means “this tile holds no signal”: the tile is NOT fitted and contributes a 0-splat placeholder, mirroring the --allow-empty-tile batch worker (the inner fitter rejects a non-positive integer seeds).

  • cull_retention (float or None, default 0.95) – Post-fit cumulative culling on the merged result. Keeps the top splats that account for this fraction of total amplitude (0–1). Per-tile culling is disabled automatically; only the merged result is culled. Set to None to disable.

  • source_shape (sequence of int, optional) – Grid of the ACQUISITION, when volume is already a preprocessed copy of it — a caller that decimated before tiling must declare it, or the merged result records the working copy as its source and the compression ratio is quoted against a grid the data never had. None measures volume itself, which is right whenever nothing was preprocessed.

  • source_dtype (str, optional) – Element type the volume was STORED in, for the same reason as in fit_gaussian_splats(). Applied to the MERGED result rather than forwarded to the tiles: a tile would use it to describe its own crop.

  • **fit_kwargs – All other keyword arguments forwarded to the per-tile fitting function (e.g. seeds, n_iters, preset, device, residual_pass_min_iters when progressive=True). seeds is handed to EVERY tile as-is, so an integer here is a per-tile count, not a whole-volume budget: N tiles fit ~N x seeds splats — unless tile_seed_counts overrides it per tile, which is what the CLI normally passes. The CLI’s --seeds IS a whole-volume budget, divided before this call into one exact occupancy-weighted count per tile (luxar.cli.gsplat_ops.fitting.fit_utils._weighted_uniform_seed_counts → tile_seed_counts), falling back to the equal share luxar.cli.gsplat_ops.fitting.fit_utils.split_seeds_across_tiles where no weighting ran; a direct Python caller that wants the same semantics divides itself.

Returns:

Merged result with all splats in global coordinates. Progressive fitting changes the optimization schedule, not the result’s LOD structure. The merged reconstruction is also scored against the whole volume. Metrics land in stats for a flat result and in the root node’s in-memory meta["fit_stats"] for a tree, ready for the CLI writer to persist at the store root.

Return type:

GSplatData

Notes

Merged quality metrics: the per-tile scores describe crops of an apodized decomposition and do not compose, so the merged reconstruction is rendered once against volume and scored. Scoring materializes the whole volume, so separate host-reference and render-device peaks are bounded by half the memory actually free, each held under a 24 GiB ceiling. Concurrent local workers divide the default host allowance across the run and the default device allowance across the workers on their card. LUXAR_TILED_QUALITY_MAX_GB overrides both budgets (0 declines outright). Over budget, or on a failure, it says so even when verbose=False. A partition is scored by rendering each surviving tile-part and summing the volumes in place, matching how the viewer composes the parts without flattening or copying the full splat set.

GPU utilization with progressive: When progressive=True, each per-pass fit uses fewer splats (max_splats_per_pass), which may under-saturate the GPU. For batch/Slurm jobs, combine --progressive with --parallel to run multiple tiles concurrently on the same GPU and improve throughput.

luxar.gsplats.fit_tile(volume: Any, spec: TileSpec, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real', progressive: bool = False, max_splats_per_pass: int = 5000, psnr_patience: float = 0.5, max_passes: int | None = None, tile_data: ndarray | None = None, **fit_kwargs: Any) → GSplatData[source]

Fit Gaussian splats on a single tile of a larger volume.

Extracts the tile subvolume, optionally denoises it, subtracts the background floor (resolved against the whole volume, never the tile, and on the denoised basis when denoising is active), applies cosine apodization, fits splats, and translates centers to global volume coordinates. This is the atomic unit for tiled fitting — each call is independent and Slurm-ready.

Parameters:
  • volume (np.ndarray or zarr.Array) – Full volume (or lazy zarr array). Only the tile’s slice is materialized into memory via volume[spec.slices].

  • spec (TileSpec) – Tile specification from compute_tile_specs(). Contains the per-face overlap sizes used for cosine window construction.

  • voxel_size (float or sequence of float, optional) – Physical voxel spacing. Passed through to the per-tile fitter (both fit_gaussian_splats() and the progressive fitter) and used for correct center translation when output_space="real".

  • output_space (str, default "real") – Coordinate space for output centers ("real" or "voxel").

  • progressive (bool, default False) – If True, use progressive fitting (multiple passes on residuals) instead of standard single-pass fitting. The passes are an optimization schedule: each tile still returns one flat splat set.

  • max_splats_per_pass (int, default 5000) – Maximum splats per progressive pass (ignored if progressive=False).

  • psnr_patience (float, default 0.5) – Stop progressive passes if ΔPSNR < this value in dB.

  • max_passes (int, optional) – Maximum number of progressive passes (None = unlimited).

  • **fit_kwargs – All other keyword arguments forwarded to the fitting function. seeds here is per tile: an integer is the count for THIS tile alone, and 0 is a skip, not an error — the caller has budgeted this tile nothing, so a 0-splat _skipped_tile_result() comes back instead of the fitter’s “seeds as int must be positive”. The CLI’s --seeds is a whole-volume budget, divided before reaching this function: normally into one exact occupancy-weighted count per tile (tile_seed_counts on fit_tiled(), --tile-seed-count on a worker, both from luxar.cli.gsplat_ops.fitting.fit_utils._weighted_uniform_seed_counts), and by the equal-share fallback luxar.cli.gsplat_ops.fitting.fit_utils.split_seeds_across_tiles where no weighting ran (a hand-run --tile k/M). A direct Python caller does that division itself if it wants the same semantics. floor (default "auto") is intercepted here: a spec string is resolved once against the whole volume via resolve_volume_floor_denoised() (so independent tile workers agree on one level), with the “would erase all signal” guard applied. With _denoise_h / _denoise_params in fit_kwargs that resolution happens on the DENOISED basis — the tile is denoised before the level is subtracted, so a raw-basis level would remove a different pedestal than the non-tiled path does (#1178) — wherever that shift is measurable: always within the denoise probe’s budget, and above it for a pNN spec only (see resolve_volume_floor_denoised()). A numeric value is taken at face value — the caller is expected to have guarded it (as fit_tiled() does with guard_numeric=True; the single-tile CLI worker deliberately does NOT, so one level resolved by its parent applies unchanged to a dim timepoint). The level is subtracted from the tile — after any denoising, before apodization; the two do not commute the other way — and the inner fit then runs with floor="none" and the applied level is recorded in result.stats["floor"] (with image_min/image_max shifted back into the input volume’s units).

Returns:

Fit result with centers in global volume coordinates (a single flat LOD; progressive=True changes how each tile is optimized, not the structure of the result).

Return type:

GSplatData

Raises:

ValueError – If seeds in fit_kwargs is an explicit np.ndarray (not supported with tiled fitting; use int, float, or None instead).

luxar.gsplats.fit_tiled(volume: Any, tile_size: int | Sequence[int] = 256, overlap: int | Sequence[int] = 32, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real', verbose: bool = True, progressive: bool = False, max_splats_per_pass: int = 5000, psnr_patience: float = 0.5, max_passes: int | None = None, cull_retention: float | None = 0.95, partition: bool = False, recipe: str | None = None, recipe_params: Any | None = None, fold_tile_slivers: bool = True, tile_seed_counts: Sequence[int] | None = None, source_shape: Sequence[int] | None = None, source_dtype: str | None = None, source_stored_bytes: int | None = None, **fit_kwargs: Any) → Any[source]

Fit Gaussian splats to a large volume using tiled decomposition.

Splits the volume into overlapping tiles with cosine apodization (Hann window), fits each tile independently, and merges results. The background floor (floor in fit_kwargs, default "auto") is resolved once against the whole volume and subtracted from each tile before windowing; on the floor-subtracted data the Hann partition-of-unity property guarantees seamless blending. When per-tile denoising is active (_denoise_h / _denoise_params), the level is resolved on the denoised basis, matching the non-tiled path (#1178) — with the one documented exception that the default "auto" spec on a volume above the denoise probe’s budget keeps its raw-basis level, since the histogram-mode shift is not measurable on a bounded crop.

When progressive=True, each tile is fitted in several residual passes; the progressive fitter flattens its passes before returning, so the tiles are concatenated exactly as in the single-pass case. (Tile results that do carry additive sub-LODs are merged level by level.)

Parameters:
  • volume (np.ndarray or zarr.Array) – Full volume. Can be a lazy zarr array for out-of-core processing — only one tile at a time is materialized in memory.

  • tile_size (int or tuple of int, default 256) – Tile size per axis in voxels. Scalar is broadcast to all axes.

  • overlap (int or tuple of int, default 32) – Overlap width per axis in voxels. Scalar is broadcast.

  • voxel_size (float or sequence of float, optional) – Physical voxel spacing, forwarded to per-tile fitting.

  • output_space (str, default "real") – Coordinate space for output centers ("real" or "voxel").

  • verbose (bool, default True) – Print per-tile progress with arbol.

  • progressive (bool, default False) – Optimize each tile in several residual passes. The fitter flattens those passes into one splat set before the tiles are merged.

  • max_splats_per_pass (int, default 5000) – Maximum splats per progressive pass (ignored if progressive=False).

  • psnr_patience (float, default 0.5) – Stop progressive passes if ΔPSNR < this value in dB.

  • max_passes (int, optional) – Maximum number of progressive passes (None = unlimited).

  • fold_tile_slivers (bool, default True) –

    Fold a trailing tile whose unique coverage is smaller than the overlap into its predecessor, instead of emitting an overlap-dominated sliver.

    This CHANGES OUTPUT versus pre-#2838, where the default was False. There is now exactly ONE uniform grid: this function, the fit --tiling uniform sequential and -j N paths, the fit --tile k/M worker, and every batch-fit producer all build it. On (108, 1352, 532) at tile_size=512, overlap=32 that is 3 tiles, where the unfolded grid had 6 — so the same call on the same volume now fits different regions with different per-tile budgets, and the result differs from a pre-#2838 one by more than a content_hash. Pass False to reproduce a historical grid.

    A folded tile also EXCEEDS tile_size: it spans up to tile_size + overlap - 1 voxels on the folded axis, so peak per-tile memory is that much above what tile_size alone suggests (256/32 -> 287, 1.41x the voxels in 3D; 256/64 -> 319, 1.93x; 24/8 -> 31, 2.16x). Size tile_size for that worst case.

  • tile_seed_counts (sequence of int, optional) – Exact per-tile integer seed counts in grid order. When provided, this overrides seeds for each tile; its length must match the resolved grid. This is the CLI handoff for occupancy-weighted whole-volume budgets. A 0 entry means “this tile holds no signal”: the tile is NOT fitted and contributes a 0-splat placeholder, mirroring the --allow-empty-tile batch worker (the inner fitter rejects a non-positive integer seeds).

  • cull_retention (float or None, default 0.95) – Post-fit cumulative culling on the merged result. Keeps the top splats that account for this fraction of total amplitude (0–1). Per-tile culling is disabled automatically; only the merged result is culled. Set to None to disable.

  • source_shape (sequence of int, optional) – Grid of the ACQUISITION, when volume is already a preprocessed copy of it — a caller that decimated before tiling must declare it, or the merged result records the working copy as its source and the compression ratio is quoted against a grid the data never had. None measures volume itself, which is right whenever nothing was preprocessed.

  • source_dtype (str, optional) – Element type the volume was STORED in, for the same reason as in fit_gaussian_splats(). Applied to the MERGED result rather than forwarded to the tiles: a tile would use it to describe its own crop.

  • **fit_kwargs – All other keyword arguments forwarded to the per-tile fitting function (e.g. seeds, n_iters, preset, device, residual_pass_min_iters when progressive=True). seeds is handed to EVERY tile as-is, so an integer here is a per-tile count, not a whole-volume budget: N tiles fit ~N x seeds splats — unless tile_seed_counts overrides it per tile, which is what the CLI normally passes. The CLI’s --seeds IS a whole-volume budget, divided before this call into one exact occupancy-weighted count per tile (luxar.cli.gsplat_ops.fitting.fit_utils._weighted_uniform_seed_counts → tile_seed_counts), falling back to the equal share luxar.cli.gsplat_ops.fitting.fit_utils.split_seeds_across_tiles where no weighting ran; a direct Python caller that wants the same semantics divides itself.

Returns:

Merged result with all splats in global coordinates. Progressive fitting changes the optimization schedule, not the result’s LOD structure. The merged reconstruction is also scored against the whole volume. Metrics land in stats for a flat result and in the root node’s in-memory meta["fit_stats"] for a tree, ready for the CLI writer to persist at the store root.

Return type:

GSplatData

Notes

Merged quality metrics: the per-tile scores describe crops of an apodized decomposition and do not compose, so the merged reconstruction is rendered once against volume and scored. Scoring materializes the whole volume, so separate host-reference and render-device peaks are bounded by half the memory actually free, each held under a 24 GiB ceiling. Concurrent local workers divide the default host allowance across the run and the default device allowance across the workers on their card. LUXAR_TILED_QUALITY_MAX_GB overrides both budgets (0 declines outright). Over budget, or on a failure, it says so even when verbose=False. A partition is scored by rendering each surviving tile-part and summing the volumes in place, matching how the viewer composes the parts without flattening or copying the full splat set.

GPU utilization with progressive: When progressive=True, each per-pass fit uses fewer splats (max_splats_per_pass), which may under-saturate the GPU. For batch/Slurm jobs, combine --progressive with --parallel to run multiple tiles concurrently on the same GPU and improve throughput.

Whole-volume quality scoring shared by merged gsplat fit paths.

luxar.gsplats.merged_quality.announce_unscored_merge(reason: str) → None[source]

Explain why a merged result carries no whole-volume quality metrics.

Parameters:

reason (str) – The reason scoring was unavailable.

luxar.gsplats.merged_quality.collect_part_provenance(datasets: Sequence[GSplatData], *, values: Sequence[float], fit_reference: dict[str, Any] | None) → list[dict[str, Any]][source]

Collect JSON-safe per-fit stamps for caller-defined component coordinates.

The caller supplies the reference classification when it knows what each fit was scored against; None records the contract’s unknown-reference case. The returned records describe the component fits; they are not a scalar quality claim for the transformed union. Existing component provenance is retained recursively when composed datasets are collected again.

luxar.gsplats.merged_quality.resolve_merged_reference(volume: Any | None, expected_shape: tuple[int, ...], *, grid_name: str, missing_reason: str) → tuple[Any | None, str | None][source]

Validate that a merged-quality reference matches its fitting grid.

luxar.gsplats.merged_quality.stamp_merged_quality(merged: GSplatData | Sequence[GSplatData], volume: Any, *, volume_shape: tuple[int, ...], grid_scale: Sequence[float] | None, device: str | None, verbose: bool, image_min: float | None, stats: dict[str, Any] | None = None) → None[source]

Score the MERGED reconstruction against the whole volume, in place.

Each tile already scores itself, but those numbers are about crops of an apodized decomposition: the tiles overlap, so their errors do not compose into the merged one, and none of them can speak for the archive that actually ships. Without this a tiled archive carries no PSNR at all — which is exactly what a published dataset is asked for.

The reference is shifted onto the merged fit’s background-relative basis using the explicitly resolved image_min. The tiles reconstruct V - image_min, not the raw acquisition, so leaving the pedestal in the reference would make tiled and non-tiled fits publish different metrics for the same signal (#1173). Under --denoise that parity ends, and not in this path’s favor: the tiles reconstruct denoised data while the reference here keeps its noise, so the score is capped by that noise, whereas --tiling none denoises the whole volume up front and scores against its own smoothed copy. Neither number is wrong, but they are not the same measurement — a gap between them under --denoise is not a tiling artifact. A lazy source is materialized here — during the fit it is only ever read tile-by-tile — which is what the budget below bounds.

luxar.gsplats.merged_quality.summarize_part_provenance(value: Any, *, shared_source: bool = False) → list[dict[str, Any]] | None[source]

Collapse component records into one coordinate-free source summary.

shared_source is for spatial partition records whose repeated source sizes describe the same parent volume. Independent merge inputs use the default additive policy.

Tile geometry and cosine apodization for large-volume fitting.

Pure NumPy module with no fitting dependencies. Computes overlapping tile specifications and Hann (raised cosine) apodization windows that satisfy the partition-of-unity property: overlapping windows sum to 1.0.

class luxar.gsplats.tiling.TileSpec(index: int, grid_index: tuple[int, ...], slices: tuple[slice, ...], origin: tuple[float, ...], shape: tuple[int, ...], border_low: tuple[bool, ...], border_high: tuple[bool, ...], overlap_low: tuple[int, ...], overlap_high: tuple[int, ...])[source]

Bases: object

Specification for a single tile within a larger volume.

index

Flat index in [0, N) where N is total tile count. Used for --tile N/M CLI addressing.

Type:

int

grid_index

Position in the tile grid, e.g. (iz, iy, ix) for 3D.

Type:

tuple of int

slices

Index slices into the full volume to extract this tile.

Type:

tuple of slice

origin

Global coordinate offset of this tile’s [0, 0, ...] corner. Equal to the start of each slice, as float for voxel_size compatibility.

Type:

tuple of float

shape

Expected tile shape after slicing (may be smaller at volume edges).

Type:

tuple of int

border_low

Per-axis: True if the tile is the first on this axis (no taper on low side).

Type:

tuple of bool

border_high

Per-axis: True if the tile is the last on this axis (no taper on high side).

Type:

tuple of bool

overlap_low

Per-axis: actual overlap in voxels with the preceding tile on low side. Zero for the first tile on each axis.

Type:

tuple of int

overlap_high

Per-axis: actual overlap in voxels with the following tile on high side. Zero for the last tile on each axis.

Type:

tuple of int

__init__(index: int, grid_index: tuple[int, ...], slices: tuple[slice, ...], origin: tuple[float, ...], shape: tuple[int, ...], border_low: tuple[bool, ...], border_high: tuple[bool, ...], overlap_low: tuple[int, ...], overlap_high: tuple[int, ...]) → None
luxar.gsplats.tiling.compute_tile_specs(volume_shape: tuple[int, ...], tile_size: int | Sequence[int], overlap: int | Sequence[int], *, fold_slivers: bool = True) → list[TileSpec][source]

Compute a deterministic grid of overlapping tiles covering a volume.

The grid uses a stride of tile_size - overlap per axis. Edge tiles are clamped to the volume boundary and may be smaller than tile_size. With fold_slivers=True, a trailing tile whose unique coverage is smaller than the overlap is folded into its predecessor instead of creating an overlap-dominated sliver. That predecessor then spans up to tile_size + overlap - 1 voxels on the folded axis — a folded tile is the one case where a tile is BIGGER than tile_size, so size it for peak memory accordingly: 256/32 reaches 287 (1.41x the voxels of a full tile in 3D), 256/64 reaches 319 (1.93x), 24/8 reaches 31 (2.16x). Each tile stores its actual overlap with neighbors (which may differ from the overlap parameter at volume edges) to ensure correct windowing.

Parameters:
  • volume_shape (tuple of int) – Shape of the full volume, e.g. (500, 2048, 2048).

  • tile_size (int or sequence of int) – Tile size per axis. Scalar is broadcast to all axes.

  • overlap (int or sequence of int) – Overlap width per axis. Scalar is broadcast to all axes. Must satisfy 0 <= overlap <= tile_size // 2 on each axis. Overlaps larger than half the tile size cause triple tile overlap, which breaks the Hann partition-of-unity guarantee.

  • fold_slivers (bool, default True) – Fold a trailing sliver into its predecessor. Defaults to the FOLDED grid (#2838): it is the grid every Luxar producer builds — the sequential and -j N fit --tiling uniform paths, a hand-run fit --tile k/M, and batch-fit — so pairing this function with fit_tile() by hand reproduces exactly the grid those commands fit and merge. Pass False only to rebuild the historical unfolded grid of a store written before #2838 (a legacy batch-fit manifest records which one it planned).

Returns:

Tile specifications in row-major order. The list is deterministic: identical inputs always produce identical output (critical for Slurm).

Return type:

list of TileSpec

Raises:

ValueError – If overlap > tile_size // 2, tile_size <= 0, or overlap < 0 on any axis.

luxar.gsplats.tiling.resolve_grid_scale(ndim: int, *, downscale_factors: Sequence[int] | None = None, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real') → tuple[float, ...] | None[source]

Combine the two factors that separate the tile grid’s frame from the splats’.

compute_tile_specs() works in VOXELS of the array that was tiled, but the fitted splats need not live in that frame (issue #1587), for two independent and MULTIPLICATIVE reasons:

  • --downscale: the grid is computed on the decimated shape while each worker rescales its splats back to full resolution, so a tile origin o lands at o * f.

  • voxel_size with output_space="real": the fit emits physical coordinates, so a tile origin o lands at o * voxel_size.

Both at once (a downscaled parallel fit with a voxel_size from --config) gives o * f * voxel_size, which is why one combined factor is resolved here rather than each caller applying its own.

Parameters:
  • ndim (int) – Number of dimensions of the tiled array (the length of the result).

  • downscale_factors (sequence of int, optional) – Per-axis --downscale factors the grid was decimated by, or None when the grid is at full resolution. A scalar is broadcast.

  • voxel_size (float or sequence of float, optional) – Physical voxel spacing the fit was given. A scalar is broadcast; None means unit spacing.

  • output_space (str, default "real") – The fit’s output space, "real" or "voxel". The voxel_size term applies only for "real" — with "voxel" the centers stay in voxel coordinates and multiplying by the spacing would move the planes off the parts.

Returns:

Per-axis factor for grid_bsp_tree()’s scale, or None when every factor is 1 (the two frames already agree).

Return type:

tuple of float or None

Raises:

ValueError – On an output_space outside ("real", "voxel"), or a voxel_size that is neither a scalar nor a length-ndim sequence. Both are refused rather than absorbed: an unrecognised output_space would silently DROP the voxel_size term (the exact #1587 mismatch this function exists to close), and a wrong-length spacing would broadcast a partial answer.

luxar.gsplats.tiling.grid_bsp_tree(specs: Sequence[TileSpec], *, scale: Sequence[float] | None = None) → dict | None[source]

Split-plane tree over a uniform tile grid, in the serialized bsp_tree form.

Lets the viewer order uniform-tiled partition parts back-to-front by painter’s algorithm instead of by part centroid, which is not a valid order and flips discretely as the camera moves (the seam popping of issue #1555).

APPROXIMATE, unlike a content plan’s tree. A content box crops its splats to the core box, so those parts are exactly disjoint; a uniform tile keeps every splat of the apodized tile, overlap band included, so neighbouring tiles genuinely share space and no exact part order exists. compute_tile_specs() caps the halo at 2 * overlap <= tile_size, so at most two tiles meet on any axis and the honest cut is the MIDPLANE of their shared band. Misordering is then confined to that band rather than whole tiles swapping — the same second-order residual as splats whose own Gaussian straddles a seam.

Leaves carry TileSpec.index (the flat, row-major tile index) VERBATIM, so a caller can prune with the same keep-set it uses for the fitted regions (prune_serialized_bsp_tree()). The labels are explicit rather than DFS-implied because the median split below does not visit tiles in flat order.

Parameters:
  • specs (sequence of TileSpec) – A full grid as returned by compute_tile_specs().

  • scale (sequence of float, optional) –

    Per-axis factor mapping the specs’ VOXEL frame onto the frame the SPLATS live in. None (the default) means the two frames agree. Every entry must be strictly positive: 0 would collapse the planes onto the origin and a negative factor would mirror the ordering the tree encodes. Use resolve_grid_scale() to build it.

    Two independent terms can put the splats in a different frame from the grid, and they COMPOSE (issue #1587):

    • --downscale. The parallel tiled path deliberately computes its grid on the POST-downscale shape — that is how the parent and its fit --tile i/M workers agree on the tile count M — while each worker rescales its own splats back to full resolution before writing.

    • voxel_size with output_space="real". The specs are voxel coordinates, but a fit asked for real-space output emits centers in physical units (see fit_tile(), which offsets a tile by origin * voxel_size).

    Without a factor here, every plane of the resulting kind=partition would be a factor too small and would no longer lie between the parts it separates, so the viewer’s back-to-front part ordering (#1555) would be computed against nonsense. Passing the resolved factors maps a tile spanning [origin[d], origin[d] + shape[d]) to [origin[d] * f[d], (origin[d] + shape[d]) * f[d]), which is exactly the convention rescale_centers() (and the origin * voxel_size offset) applies to the centers.

Returns:

The serialized tree, or None when specs is empty or the grid subdivides an axis beyond the third. Those axes are stacked time/channel barriers and are never displayed.

Return type:

dict or None

Raises:

ValueError – If scale is given with a length other than the grid’s ndim, or with a non-positive entry.

luxar.gsplats.tiling.cosine_window(spec: TileSpec) → ndarray[source]

Build an nD cosine (Hann) apodization window for a tile.

The window is a separable product of 1D half-cosine ramps. Boundary faces (first/last tile on an axis) stay at 1.0. Interior faces are tapered over the actual overlap with the neighboring tile.

Two overlapping windows from adjacent tiles sum to exactly 1.0 in the overlap zone (Hann partition-of-unity property).

Parameters:

spec (TileSpec) – Tile specification with shape, border flags, and actual overlap sizes.

Returns:

Window array of tile shape with values in (0, 1].

Return type:

np.ndarray, dtype float32

Lifting Points and Lines to Splats

Convert existing Points/Lines geometry into Gaussian splats.

luxar.gsplats.lift_points_to_gsplats(positions: NDArray, radii: NDArray | float, colors: NDArray | Sequence[float] | None = None, opacity: float = 1.0, *, radius_scale: float = 1.0, truncation_radius: float = 3.0, _uniform_colors: bool | None = None) → GSplatData[source]

Lift a point cloud to a single-level GSplatData of isotropic Gaussians.

Each point i becomes a Gaussian with centre positions[i], isotropic covariance sigma_i^2 I where sigma_i = 2 * radii[i] * radius_scale / truncation_radius, and peak amplitude opacity / (uRIF * sigma_i) (see the module docstring for the calibration). The result is a flat (single substitutive level, single additive sub-LOD) GSplatData ready to feed to luxar.gsplats.lod.substitutive.make_substitutive_lod().

Parameters:
  • positions (array, shape (N, d)) – Point centres (any spatial dimensionality d).

  • radii (array (N,) or float) – Per-point world radius (the 1% iso-contour radius), before radius_scale.

  • colors (array (N, 3), uniform RGB(A), or None) – Per-point RGB (float32, 0..1 or HDR). A uniform colour — an RGB(A) list/tuple or a (1, c) row — is broadcast to all N points, ALPHA INCLUDED: gsplats carry per-splat alpha end to end (GSplatData.colors is (N, 3) or (N, 4), and every shader scales intensity by it), so a uniform (r, g, b, a) keeps rendering like the node it coarsens. Per-element (N, 4) RGBA is refused — the substitutive merge is untested on a VARYING alpha, and only the uniform case is trivially exact. None leaves colours unset.

  • opacity (float) – Node opacity baked into the lifted amplitude (peak match).

  • radius_scale (float) – Mirrors the shader radiusScale dtype normalisation (e.g. 1/255 for uint8 radii). Default 1.0.

  • truncation_radius (float) – Gaussian truncation T in sigmas. Defaults to LIFT_TRUNCATION_RADIUS (3.0) — NOT the codebase-wide DEFAULT_TRUNCATION_RADIUS; see that constant for why.

  • _uniform_colors (bool or None) – PRIVATE. None (the default) means “nobody has classified colors yet” and this function resolves it itself. Supplying a bool means the CALLER already resolved uniformity, and its verdict is final: no re-classification happens here, and the bool alone decides whether a 4th (alpha) column is admitted. Only lift_lines_to_gsplats() supplies it — it classifies per VERTEX and then interpolates per bead, and re-classifying the bead array would misread a one-bead (1, 4) result as the uniform form and let a genuine per-element RGBA through with an invented (averaged) alpha. Supplying a bool therefore also ASSERTS that colors is already one row per element: the expansion is skipped along with the classification, and an unexpanded (1, c) row would reach the zero-radius mask below (which indexes per element).

Returns:

A flat dataset with n_splats == N valid splats (zero-radius points, e.g. from nD slicing, are dropped).

Return type:

GSplatData

luxar.gsplats.lift_lines_to_gsplats(vertices: NDArray, widths: NDArray | float, line_type: str = 'polyline', indices: NDArray | None = None, colors: NDArray | Sequence[float] | None = None, opacity: float = 1.0, *, scalars: NDArray | None = None, colormap: str | NDArray | None = None, radius_scale: float = 1.0, truncation_radius: float = 3.0, bead_spacing_factor: float = 1.0) → GSplatData[source]

Lift a line set to a flat GSplatData of isotropic bead Gaussians.

Each segment is sampled into a string of overlapping isotropic “bead” Gaussians spaced bead_spacing_factor * σ_perp along it (σ_perp = 2 w / T per the C0 calibration — same constant as the point lift). Beads are used instead of one elongated anisotropic Gaussian per segment because the gsplat ray-integral is view-dependent for anisotropic covariances (a single elongated Gaussian is ~L/(4w) brighter end-on than broadside); isotropic beads are view-independent and sum to a smooth tube.

Bead amplitude conserves the line’s centreline brightness: each bead’s amplitude is divided by the per-segment Gaussian-comb sum evaluated at the segment midpoint (the sum of all the segment’s beads’ unit peaks there), so a long segment’s tube and a short segment’s single bead both peak at opacity — the asymptotic √(2π) only applies in the long-segment limit.

scalars + colormap (per-vertex scalar field): the scalar is interpolated per bead and then mapped through the colormap LUT (interpolate-then-LUT, matching the line shader) — pass these instead of pre-baked colors so non-linear colormaps get correct mid-segment colours.

Parameters otherwise mirror lift_points_to_gsplats() plus line_type / indices (how vertices form edges) and bead_spacing_factor — colors included, so a uniform RGB(A) list/tuple or (1, c) row is broadcast to every vertex, alpha included, before the per-bead interpolation (interpolating a constant alpha yields that same constant, so the beads stay uniformly transparent). Uniformity is decided ONCE, per vertex, and forwarded to the inner point lift: the bead array must never be re-classified, or a line set that collapses to a single bead would present a per-element RGBA as a (1, 4) “uniform” row and slip an averaged alpha into the coarse levels.

Lift Points into isotropic Gaussian splats (point -> gsplat).

This is the bridge that lets the mature gsplat substitutive LOD pipeline (luxar.gsplats.lod.substitutive.make_substitutive_lod()) coarsen a point cloud: each point becomes one isotropic Gaussian, the pipeline synthesises fewer-but-larger representatives, and those become the coarse levels of a points LOD ladder (the finest level stays the original Points node).

The seed formulas below were calibrated against the viewer shaders (point materials/point/shader-glsl.ts and gsplat materials/gsplat/shader-glsl.ts) so a single lifted Gaussian renders like the point it came from:

  • Footprint match. A point’s super-Gaussian sprite truncates to zero at its 1% iso-contour (rho = 1, the sprite edge); a Gaussian truncated at T sigmas has its visible edge at T * sigma. The point’s screen radius is R * pointSizeFactor / (2 z) and the gsplat’s is T * f * sigma_world / z with pointSizeFactor / f = 4 (2 resY/tan vs resY/(2 tan)), so matching the two screen radii gives, view-independently:

    sigma_world = (pointSizeFactor / (2 f)) * R / T = 2 R / T
    

    where R = radius * radius_scale is the point’s world radius and T is the gsplat truncation radius (default 3.0). At T = 3 the gsplat Gaussian matches the point super-Gaussian profile to 0.45% relative L2 (the kernels coincide exactly at T* = sqrt(2 ln 100) ≈ 3.035).

  • Brightness match. A single isotropic gsplat’s peak screen intensity is a * sigma_world * uRayIntegralFactor (the ray-integral boost vAmplitude2D = a * sigmaRay * uRIF with sigmaRay = sigma_world for an isotropic covariance, and uInvOneMinusC * (1 - uShiftC) = 1 at the centre). A point’s peak alpha is opacity. Equating:

    a_lift = opacity / (uRayIntegralFactor(T) * sigma_world)
    

The lift is strictly isotropic on purpose: sigmaRay equals sigma_world only for isotropic covariances, so anisotropy would make brightness view-dependent and break the seam match. (Lines lift to a string of isotropic beads for the very same reason — never one elongated anisotropic Gaussian — see lift_lines_to_gsplats() below.) The substitutive merge would silently re-introduce anisotropy — Morton bins chunk a bead string into elongated representatives whose aspect grows ~K× per level — so coarse_substitutive_levels() caps each coarse splat’s aspect at max_aspect (default 3, mass-preserving; see _cap_aspect()) and uses per-bin mass-preserving amplitudes, keeping every level’s brightness and hue view-coherent with the finest one.

Sharpness/beta is intentionally NOT used: the point kernel is a truncated super-Gaussian, and an (untruncated) moment-match to beta = 2 overspreads it badly; sigma = 2 R / T is the right footprint-preserving choice for all sharpness. The single-point seam mismatch for non-default sharpness is hidden in practice because the finest LOD level is the real Points node and coarse levels merge many points (per-point shape washes out).

nD note: a point radius is a single isotropic spatial scalar, so the lift assigns sigma = 2 R / T to every axis of positions. For a 3D cloud that is exactly right. For an nD scene where a non-spatial axis (e.g. a continuous time coordinate filled in via dim_order) is part of positions, the lifted Gaussian gains a spurious extent along that axis — the coarse gsplat levels then blur across it. Use extend_to_all for such axes (the common case), or restrict positions to the spatial subspace, until a dim-aware lift lands.

Fitting Pipeline

The modular fitting pipeline for Gaussian splat optimization.

Fitting pipeline components for Gaussian splatting.

This package contains the Gaussian splat fitting pipeline components: - Configuration dataclasses - Input validation and preprocessing - Model and optimizer initialization - Loss function creation - Optimization loop logic - Result finalization - Visualization helpers

class luxar.gsplats.fitting.FitConfig(V: np.ndarray, seeds: np.ndarray | int | float | 'GSplatData' | None, norm_percentile: float, init_sigma_vox: float | None, sigma_min_diag: Sequence[float] | None, sigma_max_diag: Sequence[float] | float | None, truncate: float, n_iters: int, lr: float, max_abs_error: float | None, rel_l2_target: float | None, gradient_clip: float | None, loss_type: str, asymmetric_penalty: float | None, l1_amp: float | None, l1_diag: float | None, scheduler_type: str, patience: int, lr_reduction_factor: float, early_stop_patience: int | None, enable_dynamic_ops: bool, dynamic_config: DynamicOpsConfig, dynamic_ops_verbose: bool, napari_movie: bool, movie_every: int, movie_max_frames: int | None, device: torch.device, verbose: bool, floor: str | float | None = 'auto', norm_range: tuple[float, float] | None = None, use_metal: bool = True, metal_intensity_floor: float = 1e-05, use_cuda: bool = True, cuda_intensity_floor: float = 1e-05, seed_method: str = 'auto', seed_kwargs: Dict[str, Any] | None = None, seed_amps_background_relative: bool = False, init_L: np.ndarray | None = None, init_amps: np.ndarray | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, voxel_footprint_correction: bool | float = False, clip_to_bounds: bool = False, voxel_size: np.ndarray | None = None, output_space: str = 'real', boundary_penalty: float | None = None, sort_splats_enabled: bool = True, sort_splats_interval: int = 1000, downscale: tuple[int, ...] | None = None, iter_callback: IterCallback | None = None, iter_callback_every: int = 25, source_dtype: str | None = None, source_itemsize: int | None = None, source_shape: list[int] | None = None, source_stored_bytes: int | None = None)[source]

Bases: object

Configuration for Gaussian splat fitting.

Contains all parameters and settings needed for the fitting process.

V: np.ndarray
seeds: np.ndarray | int | float | 'GSplatData' | None
norm_percentile: float
init_sigma_vox: float | None
sigma_min_diag: Sequence[float] | None
sigma_max_diag: Sequence[float] | float | None
truncate: float
n_iters: int
lr: float
max_abs_error: float | None
rel_l2_target: float | None
gradient_clip: float | None
loss_type: str
asymmetric_penalty: float | None
l1_amp: float | None
l1_diag: float | None
scheduler_type: str
patience: int
lr_reduction_factor: float
early_stop_patience: int | None
enable_dynamic_ops: bool
dynamic_config: DynamicOpsConfig
dynamic_ops_verbose: bool
napari_movie: bool
movie_every: int
movie_max_frames: int | None
device: torch.device
verbose: bool
floor: str | float | None = 'auto'
norm_range: tuple[float, float] | None = None
use_metal: bool = True
metal_intensity_floor: float = 1e-05
use_cuda: bool = True
cuda_intensity_floor: float = 1e-05
seed_method: str = 'auto'
seed_kwargs: Dict[str, Any] | None = None
seed_amps_background_relative: bool = False
init_L: np.ndarray | None = None
init_amps: np.ndarray | None = None
amp_max: float | None = None
max_eccentricity: float | None = 10.0
voxel_footprint_correction: bool | float = False
clip_to_bounds: bool = False
voxel_size: np.ndarray | None = None
output_space: str = 'real'
boundary_penalty: float | None = None
sort_splats_enabled: bool = True
sort_splats_interval: int = 1000
downscale: tuple[int, ...] | None = None
iter_callback: IterCallback | None = None
iter_callback_every: int = 25
source_dtype: str | None = None
source_itemsize: int | None = None
source_shape: list[int] | None = None

Declared grid of the ACQUISITION, when the caller preprocessed before fitting. None means the array handed in IS the source.

source_stored_bytes: int | None = None

Bytes the acquisition OCCUPIES on disk (compressed), as opposed to the decoded source_bytes. Enables the second, apples-to-apples ratio.

__init__(V: np.ndarray, seeds: np.ndarray | int | float | 'GSplatData' | None, norm_percentile: float, init_sigma_vox: float | None, sigma_min_diag: Sequence[float] | None, sigma_max_diag: Sequence[float] | float | None, truncate: float, n_iters: int, lr: float, max_abs_error: float | None, rel_l2_target: float | None, gradient_clip: float | None, loss_type: str, asymmetric_penalty: float | None, l1_amp: float | None, l1_diag: float | None, scheduler_type: str, patience: int, lr_reduction_factor: float, early_stop_patience: int | None, enable_dynamic_ops: bool, dynamic_config: DynamicOpsConfig, dynamic_ops_verbose: bool, napari_movie: bool, movie_every: int, movie_max_frames: int | None, device: torch.device, verbose: bool, floor: str | float | None = 'auto', norm_range: tuple[float, float] | None = None, use_metal: bool = True, metal_intensity_floor: float = 1e-05, use_cuda: bool = True, cuda_intensity_floor: float = 1e-05, seed_method: str = 'auto', seed_kwargs: Dict[str, Any] | None = None, seed_amps_background_relative: bool = False, init_L: np.ndarray | None = None, init_amps: np.ndarray | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, voxel_footprint_correction: bool | float = False, clip_to_bounds: bool = False, voxel_size: np.ndarray | None = None, output_space: str = 'real', boundary_penalty: float | None = None, sort_splats_enabled: bool = True, sort_splats_interval: int = 1000, downscale: tuple[int, ...] | None = None, iter_callback: IterCallback | None = None, iter_callback_every: int = 25, source_dtype: str | None = None, source_itemsize: int | None = None, source_shape: list[int] | None = None, source_stored_bytes: int | None = None) → None
class luxar.gsplats.fitting.FitParameters(V: np.ndarray, seeds: np.ndarray | int | float | 'GSplatData' | None = None, norm_percentile: float = 0.0, floor: str | float | None = 'auto', norm_range: tuple[float, float] | None=None, downscale: int | Sequence[int] | None = None, init_sigma_vox: float | None = None, n_iters: int = 1000, lr: float = 0.01, loss_type: str = 'l1', asymmetric_penalty: float | None = 1.0, l1_amp: float | None = None, l1_diag: float | None = None, sigma_min_diag: Sequence[float] | float | None = 0.28867513459481287, sigma_max_diag: Sequence[float] | float | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, truncate: float = 2.75, seed_method: str = 'auto', verbose: bool = True, max_abs_error: float | None = None, rel_l2_target: float | None = None, gradient_clip: float | None = None, napari_movie: bool = False, movie_every: int = 1, movie_max_frames: int | None = None, scheduler_type: str = 'plateau', patience: int = 15, lr_reduction_factor: float = 0.9, early_stop_patience: int | None = 300, dynamic_ops_verbose: bool = False, voxel_footprint_correction: bool | float = False, boundary_penalty: float | None = None, clip_to_bounds: bool = False, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real', sort_splats_enabled: bool = True, sort_splats_interval: int = 1000, iter_callback: Any | None = None, iter_callback_every: int = 25, seed_amps_background_relative: bool = False, source_dtype: str | None = None, source_shape: Sequence[int] | None = None, source_stored_bytes: int | None = None, seed_kwargs: Dict[str, Any]=<factory>)[source]

Bases: object

Raw parameters threaded through the internal fitting pipeline.

fit_gaussian_splats remains the explicit public API. This bundle removes the duplicate parameter signatures from GaussianSplatFitter.fit and prepare_fit_config while preserving their existing values until the validation boundary normalizes them into FitConfig.

V: np.ndarray
seeds: np.ndarray | int | float | 'GSplatData' | None = None
norm_percentile: float = 0.0
floor: str | float | None = 'auto'
norm_range: tuple[float, float] | None = None
downscale: int | Sequence[int] | None = None
init_sigma_vox: float | None = None
n_iters: int = 1000
lr: float = 0.01
loss_type: str = 'l1'
asymmetric_penalty: float | None = 1.0
l1_amp: float | None = None
l1_diag: float | None = None
sigma_min_diag: Sequence[float] | float | None = 0.28867513459481287
sigma_max_diag: Sequence[float] | float | None = None
amp_max: float | None = None
max_eccentricity: float | None = 10.0
truncate: float = 2.75
seed_method: str = 'auto'
verbose: bool = True
max_abs_error: float | None = None
rel_l2_target: float | None = None
gradient_clip: float | None = None
napari_movie: bool = False
movie_every: int = 1
movie_max_frames: int | None = None
scheduler_type: str = 'plateau'
patience: int = 15
lr_reduction_factor: float = 0.9
early_stop_patience: int | None = 300
dynamic_ops_verbose: bool = False
voxel_footprint_correction: bool | float = False
boundary_penalty: float | None = None
clip_to_bounds: bool = False
voxel_size: Sequence[float] | float | None = None
output_space: str = 'real'
sort_splats_enabled: bool = True
sort_splats_interval: int = 1000
iter_callback: Any | None = None
iter_callback_every: int = 25
seed_amps_background_relative: bool = False
source_dtype: str | None = None
source_shape: Sequence[int] | None = None
source_stored_bytes: int | None = None
seed_kwargs: Dict[str, Any]
__init__(V: np.ndarray, seeds: np.ndarray | int | float | 'GSplatData' | None = None, norm_percentile: float = 0.0, floor: str | float | None = 'auto', norm_range: tuple[float, float] | None=None, downscale: int | Sequence[int] | None = None, init_sigma_vox: float | None = None, n_iters: int = 1000, lr: float = 0.01, loss_type: str = 'l1', asymmetric_penalty: float | None = 1.0, l1_amp: float | None = None, l1_diag: float | None = None, sigma_min_diag: Sequence[float] | float | None = 0.28867513459481287, sigma_max_diag: Sequence[float] | float | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, truncate: float = 2.75, seed_method: str = 'auto', verbose: bool = True, max_abs_error: float | None = None, rel_l2_target: float | None = None, gradient_clip: float | None = None, napari_movie: bool = False, movie_every: int = 1, movie_max_frames: int | None = None, scheduler_type: str = 'plateau', patience: int = 15, lr_reduction_factor: float = 0.9, early_stop_patience: int | None = 300, dynamic_ops_verbose: bool = False, voxel_footprint_correction: bool | float = False, boundary_penalty: float | None = None, clip_to_bounds: bool = False, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real', sort_splats_enabled: bool = True, sort_splats_interval: int = 1000, iter_callback: Any | None = None, iter_callback_every: int = 25, seed_amps_background_relative: bool = False, source_dtype: str | None = None, source_shape: Sequence[int] | None = None, source_stored_bytes: int | None = None, seed_kwargs: Dict[str, Any]=<factory>) → None
class luxar.gsplats.fitting.PreprocessedData(V_normalized: ndarray, V_tensor: torch.Tensor, seed_centers: ndarray, image_min: float, image_max: float, intensity_range: float, d: int, N: int, max_abs_error: float, rel_l2_target: float | None = None, floor: float | None = None, floor_strategy: str | None = None, l1_amp: float | None = None, l1_diag: float | None = None, init_L: ndarray | None = None, init_amps: ndarray | None = None, downscale_factors: tuple[int, ...] | None = None)[source]

Bases: object

Data that has been preprocessed and is ready for optimization.

Contains normalized data, seed centers, and preprocessing metadata.

V_normalized: ndarray
V_tensor: torch.Tensor
seed_centers: ndarray
image_min: float
image_max: float
intensity_range: float
d: int
N: int
max_abs_error: float
rel_l2_target: float | None = None
floor: float | None = None
floor_strategy: str | None = None
l1_amp: float | None = None
l1_diag: float | None = None
init_L: ndarray | None = None
init_amps: ndarray | None = None
downscale_factors: tuple[int, ...] | None = None
__init__(V_normalized: ndarray, V_tensor: torch.Tensor, seed_centers: ndarray, image_min: float, image_max: float, intensity_range: float, d: int, N: int, max_abs_error: float, rel_l2_target: float | None = None, floor: float | None = None, floor_strategy: str | None = None, l1_amp: float | None = None, l1_diag: float | None = None, init_L: ndarray | None = None, init_amps: ndarray | None = None, downscale_factors: tuple[int, ...] | None = None) → None
class luxar.gsplats.fitting.OptimizationResults(centers: torch.Tensor, Ls: torch.Tensor, amps: torch.Tensor, converged_early: bool, early_stopped: bool, actual_iters: int, best_iteration: int, best_loss: float, best_max_abs_error: float, best_rel_l2: float, movie_frames: Dict[str, ~typing.Any] | None, start_time: float, end_time: float, relocation_statistics: Dict[str, int]=<factory>)[source]

Bases: object

Results from the optimization process.

Contains final parameters, optimization statistics, and metadata.

centers: torch.Tensor
Ls: torch.Tensor
amps: torch.Tensor
converged_early: bool
early_stopped: bool
actual_iters: int
best_iteration: int
best_loss: float
best_max_abs_error: float
best_rel_l2: float
movie_frames: Dict[str, Any] | None
start_time: float
end_time: float
relocation_statistics: Dict[str, int]
__init__(centers: torch.Tensor, Ls: torch.Tensor, amps: torch.Tensor, converged_early: bool, early_stopped: bool, actual_iters: int, best_iteration: int, best_loss: float, best_max_abs_error: float, best_rel_l2: float, movie_frames: Dict[str, ~typing.Any] | None, start_time: float, end_time: float, relocation_statistics: Dict[str, int]=<factory>) → None
luxar.gsplats.fitting.prepare_fit_config(fitter: GaussianSplatFitter, parameters: FitParameters) → FitConfig[source]

Validate input parameters and prepare configuration for fitting.

Parameters:
  • fitter (GaussianSplatFitter) – The fitter instance (for device and dynamic ops config)

  • parameters (FitParameters) – Raw fit parameters from the public entry point.

Returns:

Validated and prepared configuration

Return type:

FitConfig

Raises:

ValueError – If any parameters are invalid

luxar.gsplats.fitting.preprocess_data(config: FitConfig) → PreprocessedData[source]

Preprocess input data for optimization.

Performs normalization, seed generation, and gradient dilution compensation.

Parameters:

config (FitConfig) – Configuration containing input data and parameters (not mutated)

Returns:

Preprocessed data ready for optimization

Return type:

PreprocessedData

luxar.gsplats.fitting.initialize_optimization(config: FitConfig, preprocessed_data: PreprocessedData) → ModelComponents[source]

Initialize model, optimizer, and scheduler.

Parameters:
  • config (FitConfig) – Configuration for the fitting process

  • preprocessed_data (PreprocessedData) – Preprocessed data ready for optimization

Returns:

Components needed for optimization

Return type:

ModelComponents

luxar.gsplats.fitting.create_loss_function(config: FitConfig, preprocessed_data: PreprocessedData, model: GaussianSplatModel) → Callable[[torch.Tensor], torch.Tensor][source]

Create loss function based on configuration.

Parameters:
  • config (FitConfig) – Configuration containing loss type and parameters

  • preprocessed_data (PreprocessedData) – Preprocessed data containing target tensor and computed L1 values

  • model (GaussianSplatModel) – Model for accessing parameters (needed for L1 regularization)

Returns:

Loss function that takes prediction tensor and returns loss

Return type:

Callable[[torch.Tensor], torch.Tensor]

luxar.gsplats.fitting.run_optimization_loop(components: ModelComponents, loss_fn: Callable[[torch.Tensor], torch.Tensor], config: FitConfig, preprocessed_data: PreprocessedData) → OptimizationResults[source]

Run the main optimization loop.

Parameters:
  • components (ModelComponents) – Model, optimizer, and scheduler

  • loss_fn (Callable) – Loss function that takes prediction and returns loss

  • config (FitConfig) – Configuration for optimization

  • preprocessed_data (PreprocessedData) – Preprocessed data including target tensor

Returns:

Results from optimization including best state and statistics

Return type:

OptimizationResults

luxar.gsplats.fitting.finalize_results(optimization_results: OptimizationResults, config: FitConfig, preprocessed_data: PreprocessedData) → GSplatData[source]

Finalize optimization results and return as GSplatData.

Parameters:
  • optimization_results (OptimizationResults) – Results from optimization loop

  • config (FitConfig) – Configuration used for fitting

  • preprocessed_data (PreprocessedData) – Preprocessed data with normalization metadata

Returns:

Dataclass containing centers, amplitudes, cholesky_factors, and stats

Return type:

GSplatData

luxar.gsplats.fitting.display_compression_analysis(V: ndarray, result: GSplatData) → None[source]

Calculate and display compression ratio analysis.

Compares the storage requirements of the original image vs the Gaussian splat representation.

Parameters:
  • V (np.ndarray) – Original input image/volume

  • result (GSplatData) – Fitted Gaussian splat result

luxar.gsplats.fitting.show_optimization_movie(movie_frames: Dict[str, Any], shape: tuple) → None[source]

Display napari viewer with optimization movie showing target, reconstruction, and residual over time.

Parameters:
  • movie_frames (dict) – Dictionary containing movie frame data

  • shape (tuple) – Shape of the original data

Fitting Configuration

Configuration dataclasses for Gaussian splat fitting pipeline.

class luxar.gsplats.fitting.config.OptimConfig(n_iters: int = 1000, lr: float = 0.01, gradient_clip: float | None = None, scheduler_type: str = 'plateau', patience: int = 15, lr_reduction_factor: float = 0.9, early_stop_patience: int | None = 300, sort_splats_enabled: bool = True, sort_splats_interval: int = 1000)[source]

Optimization hyperparameters for fit_gaussian_splats().

A declarative bundle. fit_gaussian_splats takes these as FLAT keyword arguments — there is no optim= parameter — so a config is applied by unpacking it:

from dataclasses import asdict

from luxar.gsplats import fit_gaussian_splats
from luxar.gsplats.fitting.config import OptimConfig

cfg = OptimConfig(n_iters=2000, lr=0.01, early_stop_patience=500)
result = fit_gaussian_splats(volume, **asdict(cfg))

Every field defaults to exactly what fit_gaussian_splats defaults to, so unpacking a default-constructed config is a no-op rather than a silent change of behaviour. test_no_default_disagrees_with_the_entry_point enforces that for every field of all three configs.

n_iters: int = 1000
lr: float = 0.01
gradient_clip: float | None = None
scheduler_type: str = 'plateau'
patience: int = 15
lr_reduction_factor: float = 0.9
early_stop_patience: int | None = 300
sort_splats_enabled: bool = True
sort_splats_interval: int = 1000
__init__(n_iters: int = 1000, lr: float = 0.01, gradient_clip: float | None = None, scheduler_type: str = 'plateau', patience: int = 15, lr_reduction_factor: float = 0.9, early_stop_patience: int | None = 300, sort_splats_enabled: bool = True, sort_splats_interval: int = 1000) → None
class luxar.gsplats.fitting.config.LossConfig(loss_type: str = 'l1', asymmetric_penalty: float | None = 1.0, l1_amp: float | None = None, l1_diag: float | None = None)[source]

Loss function configuration for fit_gaussian_splats().

Default loss is “l1”: in the loss-comparison study (Supp. Doc. 5), L1 beats MSE on held-out PSNR on 11 of 17 microscopy volumes and never trails it by more than 0.28 dB. Pass loss_type="mse" or loss_type="poisson" to override.

Applied by unpacking — there is no loss= parameter:

from dataclasses import asdict

from luxar.gsplats import fit_gaussian_splats
from luxar.gsplats.fitting.config import LossConfig

cfg = LossConfig(loss_type="poisson", asymmetric_penalty=5.0)
result = fit_gaussian_splats(volume, **asdict(cfg))
loss_type: str = 'l1'
asymmetric_penalty: float | None = 1.0
l1_amp: float | None = None
l1_diag: float | None = None
__init__(loss_type: str = 'l1', asymmetric_penalty: float | None = 1.0, l1_amp: float | None = None, l1_diag: float | None = None) → None
class luxar.gsplats.fitting.config.ConstraintConfig(sigma_min_diag: Sequence[float] | float | None = 0.28867513459481287, sigma_max_diag: Sequence[float] | float | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, truncate: float = 2.75, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real', boundary_penalty: float | None = None, clip_to_bounds: bool = False)[source]

Constraint configuration for fit_gaussian_splats().

Applied by unpacking — there is no constraints= parameter:

from dataclasses import asdict

from luxar.gsplats import fit_gaussian_splats
from luxar.gsplats.fitting.config import ConstraintConfig

cfg = ConstraintConfig(amp_max=2.0, max_eccentricity=5.0)
result = fit_gaussian_splats(volume, **asdict(cfg))
sigma_min_diag: Sequence[float] | float | None = 0.28867513459481287
sigma_max_diag: Sequence[float] | float | None = None
amp_max: float | None = None
max_eccentricity: float | None = 10.0
truncate: float = 2.75
voxel_size: Sequence[float] | float | None = None
output_space: str = 'real'
boundary_penalty: float | None = None
clip_to_bounds: bool = False
__init__(sigma_min_diag: Sequence[float] | float | None = 0.28867513459481287, sigma_max_diag: Sequence[float] | float | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, truncate: float = 2.75, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real', boundary_penalty: float | None = None, clip_to_bounds: bool = False) → None
class luxar.gsplats.fitting.config.FitParameters(V: np.ndarray, seeds: np.ndarray | int | float | 'GSplatData' | None = None, norm_percentile: float = 0.0, floor: str | float | None = 'auto', norm_range: tuple[float, float] | None=None, downscale: int | Sequence[int] | None = None, init_sigma_vox: float | None = None, n_iters: int = 1000, lr: float = 0.01, loss_type: str = 'l1', asymmetric_penalty: float | None = 1.0, l1_amp: float | None = None, l1_diag: float | None = None, sigma_min_diag: Sequence[float] | float | None = 0.28867513459481287, sigma_max_diag: Sequence[float] | float | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, truncate: float = 2.75, seed_method: str = 'auto', verbose: bool = True, max_abs_error: float | None = None, rel_l2_target: float | None = None, gradient_clip: float | None = None, napari_movie: bool = False, movie_every: int = 1, movie_max_frames: int | None = None, scheduler_type: str = 'plateau', patience: int = 15, lr_reduction_factor: float = 0.9, early_stop_patience: int | None = 300, dynamic_ops_verbose: bool = False, voxel_footprint_correction: bool | float = False, boundary_penalty: float | None = None, clip_to_bounds: bool = False, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real', sort_splats_enabled: bool = True, sort_splats_interval: int = 1000, iter_callback: Any | None = None, iter_callback_every: int = 25, seed_amps_background_relative: bool = False, source_dtype: str | None = None, source_shape: Sequence[int] | None = None, source_stored_bytes: int | None = None, seed_kwargs: Dict[str, Any]=<factory>)[source]

Raw parameters threaded through the internal fitting pipeline.

fit_gaussian_splats remains the explicit public API. This bundle removes the duplicate parameter signatures from GaussianSplatFitter.fit and prepare_fit_config while preserving their existing values until the validation boundary normalizes them into FitConfig.

V: np.ndarray
seeds: np.ndarray | int | float | 'GSplatData' | None = None
norm_percentile: float = 0.0
floor: str | float | None = 'auto'
norm_range: tuple[float, float] | None = None
downscale: int | Sequence[int] | None = None
init_sigma_vox: float | None = None
n_iters: int = 1000
lr: float = 0.01
loss_type: str = 'l1'
asymmetric_penalty: float | None = 1.0
l1_amp: float | None = None
l1_diag: float | None = None
sigma_min_diag: Sequence[float] | float | None = 0.28867513459481287
sigma_max_diag: Sequence[float] | float | None = None
amp_max: float | None = None
max_eccentricity: float | None = 10.0
truncate: float = 2.75
seed_method: str = 'auto'
verbose: bool = True
max_abs_error: float | None = None
rel_l2_target: float | None = None
gradient_clip: float | None = None
napari_movie: bool = False
movie_every: int = 1
movie_max_frames: int | None = None
scheduler_type: str = 'plateau'
patience: int = 15
lr_reduction_factor: float = 0.9
early_stop_patience: int | None = 300
dynamic_ops_verbose: bool = False
voxel_footprint_correction: bool | float = False
boundary_penalty: float | None = None
clip_to_bounds: bool = False
voxel_size: Sequence[float] | float | None = None
output_space: str = 'real'
sort_splats_enabled: bool = True
sort_splats_interval: int = 1000
iter_callback: Any | None = None
iter_callback_every: int = 25
seed_amps_background_relative: bool = False
source_dtype: str | None = None
source_shape: Sequence[int] | None = None
source_stored_bytes: int | None = None
seed_kwargs: Dict[str, Any]
__init__(V: np.ndarray, seeds: np.ndarray | int | float | 'GSplatData' | None = None, norm_percentile: float = 0.0, floor: str | float | None = 'auto', norm_range: tuple[float, float] | None=None, downscale: int | Sequence[int] | None = None, init_sigma_vox: float | None = None, n_iters: int = 1000, lr: float = 0.01, loss_type: str = 'l1', asymmetric_penalty: float | None = 1.0, l1_amp: float | None = None, l1_diag: float | None = None, sigma_min_diag: Sequence[float] | float | None = 0.28867513459481287, sigma_max_diag: Sequence[float] | float | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, truncate: float = 2.75, seed_method: str = 'auto', verbose: bool = True, max_abs_error: float | None = None, rel_l2_target: float | None = None, gradient_clip: float | None = None, napari_movie: bool = False, movie_every: int = 1, movie_max_frames: int | None = None, scheduler_type: str = 'plateau', patience: int = 15, lr_reduction_factor: float = 0.9, early_stop_patience: int | None = 300, dynamic_ops_verbose: bool = False, voxel_footprint_correction: bool | float = False, boundary_penalty: float | None = None, clip_to_bounds: bool = False, voxel_size: Sequence[float] | float | None = None, output_space: str = 'real', sort_splats_enabled: bool = True, sort_splats_interval: int = 1000, iter_callback: Any | None = None, iter_callback_every: int = 25, seed_amps_background_relative: bool = False, source_dtype: str | None = None, source_shape: Sequence[int] | None = None, source_stored_bytes: int | None = None, seed_kwargs: Dict[str, Any]=<factory>) → None
class luxar.gsplats.fitting.config.FitConfig(V: np.ndarray, seeds: np.ndarray | int | float | 'GSplatData' | None, norm_percentile: float, init_sigma_vox: float | None, sigma_min_diag: Sequence[float] | None, sigma_max_diag: Sequence[float] | float | None, truncate: float, n_iters: int, lr: float, max_abs_error: float | None, rel_l2_target: float | None, gradient_clip: float | None, loss_type: str, asymmetric_penalty: float | None, l1_amp: float | None, l1_diag: float | None, scheduler_type: str, patience: int, lr_reduction_factor: float, early_stop_patience: int | None, enable_dynamic_ops: bool, dynamic_config: DynamicOpsConfig, dynamic_ops_verbose: bool, napari_movie: bool, movie_every: int, movie_max_frames: int | None, device: torch.device, verbose: bool, floor: str | float | None = 'auto', norm_range: tuple[float, float] | None = None, use_metal: bool = True, metal_intensity_floor: float = 1e-05, use_cuda: bool = True, cuda_intensity_floor: float = 1e-05, seed_method: str = 'auto', seed_kwargs: Dict[str, Any] | None = None, seed_amps_background_relative: bool = False, init_L: np.ndarray | None = None, init_amps: np.ndarray | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, voxel_footprint_correction: bool | float = False, clip_to_bounds: bool = False, voxel_size: np.ndarray | None = None, output_space: str = 'real', boundary_penalty: float | None = None, sort_splats_enabled: bool = True, sort_splats_interval: int = 1000, downscale: tuple[int, ...] | None = None, iter_callback: IterCallback | None = None, iter_callback_every: int = 25, source_dtype: str | None = None, source_itemsize: int | None = None, source_shape: list[int] | None = None, source_stored_bytes: int | None = None)[source]

Configuration for Gaussian splat fitting.

Contains all parameters and settings needed for the fitting process.

V: np.ndarray
seeds: np.ndarray | int | float | 'GSplatData' | None
norm_percentile: float
init_sigma_vox: float | None
sigma_min_diag: Sequence[float] | None
sigma_max_diag: Sequence[float] | float | None
truncate: float
n_iters: int
lr: float
max_abs_error: float | None
rel_l2_target: float | None
gradient_clip: float | None
loss_type: str
asymmetric_penalty: float | None
l1_amp: float | None
l1_diag: float | None
scheduler_type: str
patience: int
lr_reduction_factor: float
early_stop_patience: int | None
enable_dynamic_ops: bool
dynamic_config: DynamicOpsConfig
dynamic_ops_verbose: bool
napari_movie: bool
movie_every: int
movie_max_frames: int | None
device: torch.device
verbose: bool
floor: str | float | None = 'auto'
norm_range: tuple[float, float] | None = None
use_metal: bool = True
metal_intensity_floor: float = 1e-05
use_cuda: bool = True
cuda_intensity_floor: float = 1e-05
seed_method: str = 'auto'
seed_kwargs: Dict[str, Any] | None = None
seed_amps_background_relative: bool = False
init_L: np.ndarray | None = None
init_amps: np.ndarray | None = None
amp_max: float | None = None
max_eccentricity: float | None = 10.0
voxel_footprint_correction: bool | float = False
clip_to_bounds: bool = False
voxel_size: np.ndarray | None = None
output_space: str = 'real'
boundary_penalty: float | None = None
sort_splats_enabled: bool = True
sort_splats_interval: int = 1000
downscale: tuple[int, ...] | None = None
iter_callback: IterCallback | None = None
iter_callback_every: int = 25
source_dtype: str | None = None
source_itemsize: int | None = None
source_shape: list[int] | None = None

Declared grid of the ACQUISITION, when the caller preprocessed before fitting. None means the array handed in IS the source.

source_stored_bytes: int | None = None

Bytes the acquisition OCCUPIES on disk (compressed), as opposed to the decoded source_bytes. Enables the second, apples-to-apples ratio.

__init__(V: np.ndarray, seeds: np.ndarray | int | float | 'GSplatData' | None, norm_percentile: float, init_sigma_vox: float | None, sigma_min_diag: Sequence[float] | None, sigma_max_diag: Sequence[float] | float | None, truncate: float, n_iters: int, lr: float, max_abs_error: float | None, rel_l2_target: float | None, gradient_clip: float | None, loss_type: str, asymmetric_penalty: float | None, l1_amp: float | None, l1_diag: float | None, scheduler_type: str, patience: int, lr_reduction_factor: float, early_stop_patience: int | None, enable_dynamic_ops: bool, dynamic_config: DynamicOpsConfig, dynamic_ops_verbose: bool, napari_movie: bool, movie_every: int, movie_max_frames: int | None, device: torch.device, verbose: bool, floor: str | float | None = 'auto', norm_range: tuple[float, float] | None = None, use_metal: bool = True, metal_intensity_floor: float = 1e-05, use_cuda: bool = True, cuda_intensity_floor: float = 1e-05, seed_method: str = 'auto', seed_kwargs: Dict[str, Any] | None = None, seed_amps_background_relative: bool = False, init_L: np.ndarray | None = None, init_amps: np.ndarray | None = None, amp_max: float | None = None, max_eccentricity: float | None = 10.0, voxel_footprint_correction: bool | float = False, clip_to_bounds: bool = False, voxel_size: np.ndarray | None = None, output_space: str = 'real', boundary_penalty: float | None = None, sort_splats_enabled: bool = True, sort_splats_interval: int = 1000, downscale: tuple[int, ...] | None = None, iter_callback: IterCallback | None = None, iter_callback_every: int = 25, source_dtype: str | None = None, source_itemsize: int | None = None, source_shape: list[int] | None = None, source_stored_bytes: int | None = None) → None
class luxar.gsplats.fitting.config.PreprocessedData(V_normalized: ndarray, V_tensor: torch.Tensor, seed_centers: ndarray, image_min: float, image_max: float, intensity_range: float, d: int, N: int, max_abs_error: float, rel_l2_target: float | None = None, floor: float | None = None, floor_strategy: str | None = None, l1_amp: float | None = None, l1_diag: float | None = None, init_L: ndarray | None = None, init_amps: ndarray | None = None, downscale_factors: tuple[int, ...] | None = None)[source]

Data that has been preprocessed and is ready for optimization.

Contains normalized data, seed centers, and preprocessing metadata.

V_normalized: ndarray
V_tensor: torch.Tensor
seed_centers: ndarray
image_min: float
image_max: float
intensity_range: float
d: int
N: int
max_abs_error: float
rel_l2_target: float | None = None
floor: float | None = None
floor_strategy: str | None = None
l1_amp: float | None = None
l1_diag: float | None = None
init_L: ndarray | None = None
init_amps: ndarray | None = None
downscale_factors: tuple[int, ...] | None = None
__init__(V_normalized: ndarray, V_tensor: torch.Tensor, seed_centers: ndarray, image_min: float, image_max: float, intensity_range: float, d: int, N: int, max_abs_error: float, rel_l2_target: float | None = None, floor: float | None = None, floor_strategy: str | None = None, l1_amp: float | None = None, l1_diag: float | None = None, init_L: ndarray | None = None, init_amps: ndarray | None = None, downscale_factors: tuple[int, ...] | None = None) → None
class luxar.gsplats.fitting.config.OptimizationResults(centers: torch.Tensor, Ls: torch.Tensor, amps: torch.Tensor, converged_early: bool, early_stopped: bool, actual_iters: int, best_iteration: int, best_loss: float, best_max_abs_error: float, best_rel_l2: float, movie_frames: Dict[str, ~typing.Any] | None, start_time: float, end_time: float, relocation_statistics: Dict[str, int]=<factory>)[source]

Results from the optimization process.

Contains final parameters, optimization statistics, and metadata.

centers: torch.Tensor
Ls: torch.Tensor
amps: torch.Tensor
converged_early: bool
early_stopped: bool
actual_iters: int
best_iteration: int
best_loss: float
best_max_abs_error: float
best_rel_l2: float
movie_frames: Dict[str, Any] | None
start_time: float
end_time: float
relocation_statistics: Dict[str, int]
__init__(centers: torch.Tensor, Ls: torch.Tensor, amps: torch.Tensor, converged_early: bool, early_stopped: bool, actual_iters: int, best_iteration: int, best_loss: float, best_max_abs_error: float, best_rel_l2: float, movie_frames: Dict[str, ~typing.Any] | None, start_time: float, end_time: float, relocation_statistics: Dict[str, int]=<factory>) → None
class luxar.gsplats.fitting.config.ModelComponents(model: Any, optimizer: torch.optim.Optimizer, scheduler: Any)[source]

Components needed during optimization.

Contains model, optimizer, and scheduler.

model: Any
optimizer: torch.optim.Optimizer
scheduler: Any
__init__(model: Any, optimizer: torch.optim.Optimizer, scheduler: Any) → None

Fitting Stages

Input validation and configuration preparation for Gaussian splat fitting.

luxar.gsplats.fitting.validation.DEFAULT_SIGMA_MIN_DIAG = 0.28867513459481287

Re-exported from luxar.typing_utils.constants, where it now lives.

It moved because this module imports FitConfig from gsplats.fitting.config, so the config module could not name its own default from here without a circular import — which is how ConstraintConfig came to default to None while the fitter defaulted to this value (audit A3-01). Kept bound here so the existing import sites do not move.

luxar.gsplats.fitting.validation.prepare_fit_config(fitter: GaussianSplatFitter, parameters: FitParameters) → FitConfig[source]

Validate input parameters and prepare configuration for fitting.

Parameters:
  • fitter (GaussianSplatFitter) – The fitter instance (for device and dynamic ops config)

  • parameters (FitParameters) – Raw fit parameters from the public entry point.

Returns:

Validated and prepared configuration

Return type:

FitConfig

Raises:

ValueError – If any parameters are invalid

Data preprocessing for Gaussian splat fitting.

Handles normalization, seed generation, and gradient dilution compensation.

luxar.gsplats.fitting.preprocessing.preprocess_data(config: FitConfig) → PreprocessedData[source]

Preprocess input data for optimization.

Performs normalization, seed generation, and gradient dilution compensation.

Parameters:

config (FitConfig) – Configuration containing input data and parameters (not mutated)

Returns:

Preprocessed data ready for optimization

Return type:

PreprocessedData

luxar.gsplats.fitting.preprocessing.resolve_volume_norm_range(volume: Any, norm_percentile: float, *, subtract: float | None = None, verbose: bool = False) → tuple[float, float][source]

Resolve the normalization range against a whole volume.

The intensity-scale counterpart of resolve_volume_floor(), and it exists for the same reason. A tiled fit hands each worker one tile; if the tile is normalized by its OWN min/max then each tile is stretched to fill [0, 1] by a different factor. Output amplitudes are rescaled by that same factor afterwards, so the physical amplitude of a linear fit largely cancels out — what does NOT cancel is everything the optimiser expresses as an absolute quantity in the normalized range: the convergence tolerance (max_abs_error, 1% of it by default), seeding and culling thresholds, and any amp_max. A dim tile is therefore resolved to a much finer physical accuracy than a bright one, and the two tiles’ splats are not mutually comparable. Sharing one range makes a tiled fit behave like the whole-volume fit it is meant to approximate.

The flip side is deliberate: a tile far dimmer than the volume maximum is now held to the same ABSOLUTE tolerance as the rest of the volume, so it converges earlier instead of resolving its own noise at full contrast.

Parameters:
  • volume (np.ndarray or zarr.Array) – Full volume (may be a lazy zarr array; only a bounded sample is read, via the same budget and block layout as resolve_volume_floor()).

  • norm_percentile (float) – 0 for full min-max; otherwise the low/high percentile pair, exactly as _normalize_data() interprets it.

  • subtract (float, optional) – A level already subtracted from the tile before fitting (the resolved floor). The returned range is shifted to match, since the fit sees post-subtraction data. Clamped at 0 like the tile’s own clip.

  • verbose (bool, default False) – Print the resolved range via arbol.

Returns:

(image_min, image_max) to hand to every tile of this volume.

Return type:

tuple[float, float]

Notes

Determinism matters as much as it does for the floor: the sample is a pure function of volume.shape and the fixed budget, so independent workers (--tile k/M, -j N) resolve the SAME range for the volume they are HANDED, without coordinating. Batch-fit instead resolves one range across its bounded plan-time (t, c) samples, records it in the manifest, and forwards it to every task, so spatial and temporal children share the same normalization scale.

luxar.gsplats.fitting.preprocessing.resolve_volume_norm_range_denoised(volume: Any, norm_percentile: float, *, denoise_h: float | None, denoise_params: dict[str, Any] | None, subtract: float | None = None, probe_cache: dict[str, Any] | None = None, verbose: bool = False) → tuple[float, float][source]

Resolve the shared normalization range on the data tiles will fit.

With denoising disabled this is exactly resolve_volume_norm_range(). Otherwise, when the whole volume fits the bounded probe budget, the raw whole-volume range is shifted by the denoise-induced endpoint change measured on the deterministic shape-preserving probe used for floor correction. When the volume fits the probe budget, the probe is the whole volume and the result exactly matches resolving after a full denoise, as the non-tiled path does. Above that budget the raw range is kept: a bounded max-shift did not converge in measurement and is not worth an NLM pass.

luxar.gsplats.fitting.preprocessing.resolve_volume_floor(volume: Any, floor: str | float | None, *, guard_numeric: bool = False, sample_budget: int | None = None, verbose: bool = False) → float | None[source]

Resolve a floor spec against a whole volume, without loading it all.

The whole-volume counterpart of _resolve_floor() for tiled fitting: the returned level is a property of the volume, never of any tile, so independent workers (--tile k/M, -j N, batch-fit) all subtract one identical pedestal.

Parameters:
  • volume (np.ndarray or zarr.Array) – Full volume (may be a lazy zarr array; only a bounded sample is read).

  • floor (str, float, or None) – Floor spec (see _resolve_floor()). A numeric spec (float or numeric string) short-circuits and is echoed back without touching the volume — unless guard_numeric is set; "none"/None/0 return None.

  • guard_numeric (bool, default False) – Also apply the “floor >= max would erase all signal” guard to a numeric spec (one bounded sample read). Pass True where a USER-supplied spec is first turned into a level; leave False for levels already resolved and guarded upstream (e.g. the concrete level the parent hands each tile worker), preserving the read-free short-circuit.

  • sample_budget (int, optional) – Override the bounded sample voxel budget. None uses FLOOR_SAMPLE_BUDGET_VOXELS.

  • verbose (bool, default False) – Print the resolved level via arbol.

Returns:

The concrete background level to subtract, or None (disabled, nothing to subtract, or the guard below refused the level).

Return type:

float or None

Notes

  • Memory bound: at most FLOOR_SAMPLE_BUDGET_VOXELS voxels are sampled, as evenly spaced contiguous slab blocks along the volume’s longest axis. If one full cross-section exceeds the budget, it is deterministically center-cropped along the remaining axes until it fits. A volume within the budget is read whole.

  • Determinism: the sample is a pure function of volume.shape and the fixed budget, so two independent processes given the same volume and spec always resolve the same level.

  • A negative resolved level (dark-frame-corrected / deconvolved data with a negative background) is returned like any other: floor suppression means “put the background at 0”, so a background sitting at -2 is shifted up by V - (-2) — exactly what the non-tiled path’s image_min = max(resolved_floor, image_min) does when resolved_floor is negative.

  • The “floor >= max would erase all signal” guard is applied against the sampled max: such a level is refused with an aprint warning and None is returned. For numeric specs the guard runs only with guard_numeric=True.

luxar.gsplats.fitting.preprocessing.resolve_volume_floor_with_strategy(volume: Any, floor: str | float | None, *, guard_numeric: bool = False, sample_budget: int | None = None, verbose: bool = False) → tuple[float | None, str | None][source]

Resolve specimen level and branch from the same bounded sample.

luxar.gsplats.fitting.preprocessing.resolve_volume_floor_denoised_with_strategy(volume: Any, floor: str | float | None, *, denoise_h: float | None = None, denoise_params: dict[str, Any] | None = None, guard_numeric: bool = False, sample_budget: int | None = None, probe_cache: dict[str, Any] | None = None, verbose: bool = False) → tuple[float | None, str | None][source]

Resolve a denoised-basis floor and its specimen estimator branch.

The tiled paths denoise each tile and then subtract a global level, while the non-tiled path denoises the whole volume and estimates the level from THAT. Denoising collapses the noise tail and shifts the histogram mode, so resolving on the raw volume and subtracting from denoised tiles removes a measurably different pedestal than --tiling none does on the same input (#1178). Estimating on denoised data is the better default — the mode estimator is more reliable once the tail is collapsed — so this function keeps resolve_volume_floor()’s whole-volume basis (one global level, the #1174 invariant) and applies the denoise-induced CORRECTION measured on a small bounded probe, wherever that shift can actually be measured: on a volume within the probe budget always, and above it only for a pNN spec. See the Notes for the two regimes and the measurements behind them.

Parameters:
  • volume (np.ndarray or zarr.Array) – Full volume (may be lazy; only bounded samples are read).

  • floor (str, float, or None) – Floor spec, exactly as resolve_volume_floor() interprets it. Only a VOLUME-DERIVED spec ("auto" / "pNN") is ever corrected; a numeric spec or "none" is a user absolute and passes through untouched. Above the probe budget only "pNN" is corrected.

  • denoise_h (float, optional) – NLM filtering strength the tiles will be denoised with. None (denoise off) delegates to resolve_volume_floor() verbatim.

  • denoise_params (dict, optional) – The remaining denoise_volume_array keyword arguments (patch_size, search_distance, backend, device, use_2d, norm_range), passed verbatim so the probe is smoothed exactly as the tiles are. None delegates like denoise_h=None.

  • guard_numeric (bool, default False) – Forwarded to resolve_volume_floor() (see there).

  • sample_budget (int, optional) – Override the bounded raw floor-sample voxel budget. None uses FLOOR_SAMPLE_BUDGET_VOXELS.

  • verbose (bool, default False) – Print the raw level, the correction and the final level. Forwarded to resolve_volume_floor() on the paths that delegate to it.

Returns:

The concrete level every tile should subtract from its DENOISED data, or None (disabled, or a guard refused the level).

Return type:

float or None

Notes

  • Two regimes, one measured rule. The correction is applied where it is demonstrably right, and not applied where it is not:

    1. The probe covers the WHOLE volume (total <= DENOISE_PROBE_BUDGET_VOXELS). The corrected level then IS the denoised-whole-volume estimate, bit for bit — the same estimator over the same values — so it is applied for any volume-derived spec. This is what makes tiled/non-tiled parity exact on small volumes.

    2. Above the budget the probe is a handful of cubic centre crops, and whether its shift transfers depends on the ESTIMATOR. A pNN percentile shift does; the auto histogram-mode shift does not, and is therefore not applied at all — the raw-basis level is kept (exactly the pre-#1178 behaviour) and one note says so. No probe is denoised in that case, so the skip costs nothing.

  • What was measured (synthetic 24x64x64 stacks with a known pedestal, six background families x six seeds, production denoise params including the whole-volume norm_range, probe at 4.7% of the volume; error = |level - reference| against the reference --tiling none computes, _resolve_floor(denoise_whole(volume), spec)):

    • pNN (p10): mean error 4.386 raw -> 1.408 corrected, closer on 28/36 volumes, and the worst family (Poisson) goes from a mean 11.656 to 1.644 (worst single volume 2.091). Applied.

    • auto: mean error 1.309 raw -> 0.830 corrected, but closer on only 21/36 volumes — the sign is close to a coin flip. It wins big on the two families whose true shift is large (gamma-skewed and masked pedestals, ~2.6 -> ~0.6) and loses on the four whose true shift is ~0.2-0.6 (flat Gaussian 0.248 -> 0.473, vignetted 0.649 -> 1.149), because a crop-measured mode shift carries ~1 unit of noise regardless. Two independent reviewers measured the same aggregate as net WORSE on their volumes. Not applied above the budget.

  • The deciding measurement is the probe-size sweep (12 volumes, probe at 2.3 / 4.7 / 18.8 / 37.5% of the volume). p10’s corrected error falls monotonically — 1.03, 0.98, 0.59, 0.38 units, closer than raw on 9/12 then 12/12 — so the percentile shift is a real property of the data that a bigger probe measures better. auto’s does not move: 0.91, 0.75, 0.72, 0.90, closer than raw on 8/12 even with 37.5% of the volume in the probe, and its WORST case gets worse (2.6 -> 4.3). The mode shift is a property of the LOCAL background level, which varies spatially, so no affordable probe converges on it — and a real light-sheet stack sits at ~0.03%, far below anything measured here.

  • Cost: one extra denoise pass over at most DENOISE_PROBE_BUDGET_VOXELS voxels per CALL, and none at all for auto above the budget (regime 2 is decided from volume.shape, before anything is read). That is once per resolution, not once per tile — but every worker resolves its own level, so a -j N run or an M-way --tile k/M fleet pays it once per worker, and for a volume within the probe budget the probe IS the whole volume (M whole-volume denoise passes for M workers).

  • Determinism: the probe is a pure function of volume.shape and the budget, so independent workers (--tile k/M, -j N) that share a volume, h and params all reach the same corrected level — provided they also share a denoise BACKEND. backend="auto" resolves to skimage on a CPU-only host and to the torch/CUDA kernel on a GPU host, and the two do NOT agree closely enough for the estimators to be indifferent: on four synthetic pedestals their outputs differed by a mean of ~0.34-0.48 and by up to 26-34 INTENSITY units at individual voxels, and the resolved auto level came out different in 4 of 4 configurations (by 0.001-0.043 units; a reviewer measured 0.005-0.085 on other data). A level difference across backends is therefore the norm, not a corner case: pin --denoise-backend for a fleet spanning heterogeneous hosts.

  • The “level >= sampled max would erase all signal” guard is re-applied to the corrected level against the raw floor sample’s max — the same basis, and the same bounded read, resolve_volume_floor() judges on. The probe’s own denoised max is deliberately NOT used, and the reason is NOT that it would catch less: NLM shrinks the range, so the denoised max is a strictly TIGHTER bound and would veto a SUPERSET of levels (measured on a light-sheet crop: raw max 288.5 vs denoised max 263.0, and a level between the two erases every denoised tile while passing the raw-max guard). It is not used because a centre-cropped, smoothed probe may legitimately see no signal at all — a masked or zero-padded middle — and a veto there would silently drop floor suppression for a whole run, which is worse than the level being a little generous. The cost of that choice is the gap: a level between the denoised and raw maxima is not caught. A background-mode level does not land there in practice.

  • Degrades, never crashes: an unreadable probe, a failing denoise_volume_array (no torch, an unavailable backend, a raising kernel), a degenerate estimate or a probe carrying non-finite values each print an honest note and return the RAW-basis level, i.e. exactly today’s behaviour. See _denoise_probe_correction().

luxar.gsplats.fitting.preprocessing.resolve_volume_floor_denoised(volume: Any, floor: str | float | None, *, denoise_h: float | None = None, denoise_params: dict[str, Any] | None = None, guard_numeric: bool = False, sample_budget: int | None = None, probe_cache: dict[str, Any] | None = None, verbose: bool = False) → float | None[source]

Resolve a denoised-basis floor while discarding provenance.

Model and optimizer initialization for Gaussian splat fitting.

luxar.gsplats.fitting.initialization.apply_sigma_min_diag_floor(sigma_diag: ndarray, sigma_min_diag: Sequence[float] | ndarray) → ndarray[source]

Apply the fitter’s gradient-safety floor to Cholesky diagonals.

luxar.gsplats.fitting.initialization.resolve_fit_initial_sigma_diag(config: FitConfig, preprocessed_data: PreprocessedData) → ndarray[source]

Return the uniform voxel-space sigma vector used for fresh seeds.

Callers use this only when preprocessed_data.init_L is absent; supplied covariances may contain a different initialization for every splat.

luxar.gsplats.fitting.initialization.initialize_optimization(config: FitConfig, preprocessed_data: PreprocessedData) → ModelComponents[source]

Initialize model, optimizer, and scheduler.

Parameters:
  • config (FitConfig) – Configuration for the fitting process

  • preprocessed_data (PreprocessedData) – Preprocessed data ready for optimization

Returns:

Components needed for optimization

Return type:

ModelComponents

Loss function creation for Gaussian splat fitting.

Speed optimisations (empirically validated)

  1. Poisson deviance dedup (−4.9%): The per-element deviance Pc - Vc + xlogy(Vc, Vc/Pc) is computed once and reused for both the base loss and the asymmetric over-prediction penalty. Previously it was computed twice (once for sum, once for the masked over-prediction sum).

  2. torch.compile on CUDA (−14.4% in benchmarked cases): CUDA loss kernels are compiled opportunistically with a safe eager fallback. CPU and MPS use eager PyTorch to avoid runtime C++ toolchain requirements.

luxar.gsplats.fitting.losses.create_loss_function(config: FitConfig, preprocessed_data: PreprocessedData, model: GaussianSplatModel) → Callable[[torch.Tensor], torch.Tensor][source]

Create loss function based on configuration.

Parameters:
  • config (FitConfig) – Configuration containing loss type and parameters

  • preprocessed_data (PreprocessedData) – Preprocessed data containing target tensor and computed L1 values

  • model (GaussianSplatModel) – Model for accessing parameters (needed for L1 regularization)

Returns:

Loss function that takes prediction tensor and returns loss

Return type:

Callable[[torch.Tensor], torch.Tensor]

Optimization loop logic for Gaussian splat fitting.

Speed optimisations (empirically validated)

  1. Eval frequency (−21.5%): The eval forward pass (for convergence checking and best-state metrics) runs every 25 iterations instead of every iteration. Training loss is used for scheduler and best-loss tracking on non-eval iters.

  2. GPU sync elimination (−1.9%): Best-loss is tracked as a GPU tensor (avoids loss.item() which forces CPU↔GPU sync every iteration).

luxar.gsplats.fitting.optimization.run_optimization_loop(components: ModelComponents, loss_fn: Callable[[torch.Tensor], torch.Tensor], config: FitConfig, preprocessed_data: PreprocessedData) → OptimizationResults[source]

Run the main optimization loop.

Parameters:
  • components (ModelComponents) – Model, optimizer, and scheduler

  • loss_fn (Callable) – Loss function that takes prediction and returns loss

  • config (FitConfig) – Configuration for optimization

  • preprocessed_data (PreprocessedData) – Preprocessed data including target tensor

Returns:

Results from optimization including best state and statistics

Return type:

OptimizationResults

Result finalization for Gaussian splat fitting.

luxar.gsplats.fitting.results.SOURCE_GRID_VOLUME_KEYS = ('source_shape', 'source_dtype', 'source_voxels', 'source_bytes', 'source_stored_bytes', 'source_declared', 'fitted_shape', 'fitted_voxels', 'occupancy')

Source-grid stamps that describe the VOLUME and so belong to a whole fit, however many times the fitter was invoked to produce it.

Deliberately excludes voxels_per_splat: that one is a ratio against the splat count of the invocation that produced it, so a multi-pass fitter copying it verbatim would report the first pass’s density for the whole result. It has to be recomputed against the final count.

luxar.gsplats.fitting.results.lift_source_grid_stats(dest: dict[str, Any], passes: Sequence[Any]) → None[source]

Copy the source-grid stamps from a multi-pass fit’s FIRST pass onto dest.

Every pass of a progressive fit sees the same volume (later ones fit its residual), so the first pass’s record of that volume describes the fit as a whole. Left in the per-pass stats it never reaches _FITTING_INFO_KEYS, and the dataset cannot say what it is a representation of.

passes are the accumulated sub-LODs, in order; an empty list is a no-op.

luxar.gsplats.fitting.results.lift_normalization_stats(dest: dict[str, Any], passes: Sequence[Any], applied_floor: float | None, *, floor_strategy: str | None = None) → None[source]

Record a multi-pass fit’s normalization provenance on dest (#1175).

A progressive fit subtracts the pedestal from the volume ONCE up front and then runs every pass with floor="none", so no pass’s own stats knows the level — the whole fit used to ship no record of the background it removed. applied_floor is that up-front level (None when suppression was disabled or refused).

The bounds come from the FIRST pass only: it is the one that sees the volume itself, while later passes normalize their own residual by its own extent, so no single intensity_range describes them all. They were measured on the already-subtracted array, so the level is added back — the block is in the input volume’s own units on every writer path, matching the single-pass fitter where image_min IS the applied level.

floor is the effective baseline the single-pass fitter would record: the greater of the resolved floor and the configured low normalization endpoint. Pass 0 receives the same bounds shifted onto the already-subtracted basis, so adding that baseline back yields the same image_min, image_max and intensity_range as a flat fit.

luxar.gsplats.fitting.results.stamp_voxels_per_splat(stats: dict[str, Any], n_splats: int) → None[source]

Quote density against the splats actually DELIVERED.

Called after any post-fit cull rather than beside the other source-grid stamps: the pre-cull count would overstate how much of the volume each surviving splat stands for, and it is the surviving ones that ship. A no-op without a fitted grid to divide, or with nothing left to divide by.

luxar.gsplats.fitting.results.finalize_results(optimization_results: OptimizationResults, config: FitConfig, preprocessed_data: PreprocessedData) → GSplatData[source]

Finalize optimization results and return as GSplatData.

Parameters:
  • optimization_results (OptimizationResults) – Results from optimization loop

  • config (FitConfig) – Configuration used for fitting

  • preprocessed_data (PreprocessedData) – Preprocessed data with normalization metadata

Returns:

Dataclass containing centers, amplitudes, cholesky_factors, and stats

Return type:

GSplatData

Dynamic Operations

Fixed-Pool Splat Relocation Operations

This package implements fixed-pool splat relocation for adaptive Gaussian splatting. Instead of adding/removing splats, weak splats are relocated to high-residual regions. This enables use of standard PyTorch Adam optimizer for much faster optimization.

class luxar.gsplats.fitting.dynamic_ops.DynamicOpsConfig(step_every: int = 50, k_max_residuals: int = 40, nms_radius_vox: float = 2.0, enable_tiled_seeding: bool = True, num_tiles_per_dim: int | None = None, seed: int | None = 42, relocation_percentile: float = 1.0, max_relocations_per_step: int | None = 64, init_sigma_vox: float = 0.5, min_contribution_threshold: float = 0.01, enable_coverage_check: bool = False, relocation_cooldown_steps: int = 1, min_splats_to_keep: int = 10)[source]

Configuration for fixed-pool splat relocation operations.

This class contains all parameters for the splat relocation algorithm: 1. Residual Peak Analysis: Find strongest error locations 2. Weak Splat Identification: Find splats with low importance (amplitude x volume) 3. Relocation: Move weak splats to high-residual peaks

Key features: - Fixed splat pool (no topology changes) enables fast standard optimizer - Relocation preserves total splat count while redistributing coverage - NMS ensures relocated splats don’t crowd each other - Convergence-based guards prevent unnecessary operations

step_every: int = 50
k_max_residuals: int = 40
nms_radius_vox: float = 2.0
enable_tiled_seeding: bool = True
num_tiles_per_dim: int | None = None
seed: int | None = 42
relocation_percentile: float = 1.0
max_relocations_per_step: int | None = 64
init_sigma_vox: float = 0.5
min_contribution_threshold: float = 0.01
enable_coverage_check: bool = False
relocation_cooldown_steps: int = 1
min_splats_to_keep: int = 10
__init__(step_every: int = 50, k_max_residuals: int = 40, nms_radius_vox: float = 2.0, enable_tiled_seeding: bool = True, num_tiles_per_dim: int | None = None, seed: int | None = 42, relocation_percentile: float = 1.0, max_relocations_per_step: int | None = 64, init_sigma_vox: float = 0.5, min_contribution_threshold: float = 0.01, enable_coverage_check: bool = False, relocation_cooldown_steps: int = 1, min_splats_to_keep: int = 10) → None
class luxar.gsplats.fitting.dynamic_ops.RecentlyRelocatedTracker(n_splats: int, cooldown_steps: int = 3, device: str = 'cpu')[source]

Track recently relocated splats to avoid immediate re-selection.

This prevents the critical bug where the same weak splats get relocated repeatedly while many splats remain untouched.

The cooldown mechanism ensures that after a splat is relocated, it won’t be selected for relocation again until it has had time to be optimized at its new location.

Performance: Uses GPU tensors for bulk filtering operations.

__init__(n_splats: int, cooldown_steps: int = 3, device: str = 'cpu')[source]

Initialize tracker.

Parameters:
  • n_splats – Total number of splats in the model

  • cooldown_steps – Number of dynamic ops steps to wait before allowing a splat to be relocated again. Default is 3 steps.

  • device – Device to store tensors on

mark_relocated_batch(splat_indices: torch.Tensor) → None[source]

Mark multiple splats as recently relocated (vectorized).

Parameters:

splat_indices – Tensor of splat indices that were relocated

filter_eligible_splats(candidate_indices: torch.Tensor) → torch.Tensor[source]

Filter candidates to only those eligible for relocation (vectorized).

Parameters:

candidate_indices – Tensor of candidate splat indices to check

Returns:

Tensor of indices that are eligible for relocation (on same device)

advance_step() → None[source]

Advance to the next dynamic ops step.

get_statistics() → Dict[str, int][source]

Get statistics about relocations.

Returns:

  • total_relocations: Total number of relocations performed

  • unique_splats: Number of unique splats that have been relocated

  • currently_on_cooldown: Number of splats currently in cooldown

Return type:

Dictionary with statistics

luxar.gsplats.fitting.dynamic_ops.apply_dynamic_operations(model: Any, V_target: torch.Tensor, V_pred: torch.Tensor, cfg: DynamicOpsConfig, max_abs_error_threshold: float, optimizer: torch.optim.Optimizer | None = None, relocation_tracker: RecentlyRelocatedTracker | None = None, verbose: bool = False) → bool[source]

Apply fixed-pool splat relocation for adaptive Gaussian splatting.

Instead of adding/removing splats, this relocates weak splats to high-residual regions. This preserves the total splat count and works with standard PyTorch Adam optimizer (no per-splat optimizer needed).

Algorithm: 1. Find residual peaks (high-error locations needing coverage) 2. Identify weak splats (low importance = amplitude × volume) 3. Filter out recently relocated splats (cooldown mechanism) 4. Match weak splats to peaks (avoiding already-covered locations) 5. Relocate matched splats and reset their optimizer state

Parameters:
  • model – GaussianSplatModel with current splat parameters

  • V_target – Target tensor to reconstruct

  • V_pred – Current prediction tensor from model

  • cfg – Dynamic operations configuration

  • max_abs_error_threshold – Convergence threshold

  • optimizer – Optional optimizer (for state reset). If provided, optimizer state (momentum, variance) will be reset for relocated splats.

  • relocation_tracker – Optional tracker for cooldown mechanism. If provided, prevents immediate re-relocation of recently moved splats.

  • verbose – Whether to print detailed progress information

Returns:

True if any splats were relocated

Return type:

bool

Optimization

Per-splat Adam optimizer with gradient dilution compensation.

Optimizer utilities for Gaussian splatting.

luxar.gsplats.optim.create_optimizer_and_scheduler(model: Any, lr: float = 0.001, scheduler_type: str | None = 'plateau', betas: Tuple[float, float] = (0.9, 0.999), eps: float = 1e-08, weight_decay: float = 0.0, amsgrad: bool = False, patience: int = 10, factor: float = 0.5, threshold: float = 0.001, cooldown: int = 0, min_lr: float = 1e-08, gamma: float = 0.95, **extra_kwargs: Any) → Tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LRScheduler | None][source]

Create optimizer and scheduler for Gaussian splat fitting.

Uses standard PyTorch Adam with gradient dilution compensation for consistent optimization across different dimensionalities.

Parameters:
  • model – GaussianSplatModel

  • lr – Base learning rate (automatically compensated for gradient dilution)

  • scheduler_type – ‘plateau’, ‘exponential’, or None

  • args (# Scheduler)

  • betas – Adam beta parameters

  • eps – Adam epsilon

  • weight_decay – L2 penalty

  • amsgrad – Whether to use AMSGrad

  • args

  • patience – Plateau scheduler patience

  • factor – LR reduction factor

  • threshold – Improvement threshold

  • cooldown – Cooldown period

  • min_lr – Minimum learning rate

  • gamma – Exponential decay rate

Returns:

(optimizer, scheduler)

Return type:

tuple

Models

Rendering models for 2D and 3D Gaussian splats.

Gaussian splat models, rendering, and numerical utilities.

Re-exports key public symbols for convenience:

from luxar.gsplats.models import GaussianSplatModel, render_gaussians
class luxar.gsplats.models.GaussianSplatModel(*args: Any, **kwargs: Any)[source]

Bases: Module

PyTorch model for n-dimensional oriented Gaussian splats with full covariance matrices.

This model represents a collection of oriented Gaussian functions (splats) that can be optimized to reconstruct images or volumes. Each splat is parameterized by:

  1. Center position: Constrained to image domain via sigmoid parameterization

  2. Covariance matrix: Represented via Cholesky decomposition L where Σ = L @ L^T

  3. Amplitude: Non-negative scalar via softplus activation

Mathematical formulation:

Each splat k contributes: a_k * exp(-0.5 * (x-μ_k)^T @ Σ_k^{-1} @ (x-μ_k))

Computational optimizations:
  • Avoids explicit matrix inversion by solving triangular system L @ y = (x-μ)

  • Uses AABB truncation for efficient rendering

  • Batched operations for multiple splats

Parameters:
  • shape (Sequence[int]) – Dimensions of the target image/volume to reconstruct.

  • centers0 (np.ndarray, shape (N, d)) – Initial center positions in voxel coordinates.

  • L0 (np.ndarray, shape (N, d, d)) – Initial lower-triangular Cholesky factors.

  • amps0 (np.ndarray, shape (N,)) – Initial amplitude values.

  • sigma_min_diag (Sequence[float]) – Minimum diagonal values for Cholesky factor (prevents degeneracy).

  • sigma_max_diag (Sequence[float], optional) – Maximum diagonal values for Cholesky factor (prevents over-smoothing).

  • amp_max (float, optional) – Maximum amplitude value. Prevents amplitude explosion during optimization, especially with aggressive compression (few splats). Since images are normalized to [0, 1], a value of 1.0 matches the max possible intensity.

  • max_eccentricity (float, optional) – Maximum allowed eccentricity (ratio of largest to smallest eigenvalue of the covariance matrix Σ = L @ L^T). This bounds the actual shape elongation of the Gaussian splats. For example, max_eccentricity=4.0 means the longest axis can be at most 2x the shortest (since eccentricity is the variance ratio, axis ratio = sqrt(eccentricity)).

  • truncate (float, default DEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations for computational efficiency.

  • device (str or torch.device, optional) – PyTorch device for computations. Explicit values override auto-detection.

  • use_cuda (bool, default True) – Allow CUDA during auto-detection when device is not provided.

  • use_metal (bool, default True) – Allow MPS/Metal during auto-detection when device is not provided.

sigma_min_diag: torch.Tensor
__init__(shape: Sequence[int], centers0: np.ndarray, L0: np.ndarray, amps0: np.ndarray, sigma_min_diag: Sequence[float], sigma_max_diag: Sequence[float] | None = None, amp_max: float | None = None, max_eccentricity: float | None = None, truncate: float = 2.75, voxel_size: np.ndarray | None = None, device: str | torch.device | None = None, use_cuda: bool = True, use_metal: bool = True) → None[source]
voxel_size: torch.Tensor | None
sigma_max_diag: torch.Tensor | None
current_params() → Tuple[torch.Tensor, torch.Tensor, torch.Tensor][source]

Extract current parameter values from the model’s learnable parameters.

Applies all transformations to convert raw parameters to their final forms: - Centers: sigmoid transformation to ensure bounds - Cholesky factors: reconstruction from diagonal/off-diagonal components - Amplitudes: softplus transformation to ensure non-negativity

Returns:

  • centers (torch.Tensor, shape (N, d)) – Center coordinates in voxel units, bounded within image domain.

  • L (torch.Tensor, shape (N, d, d)) – Lower-triangular Cholesky factors where covariance Σ = L @ L^T.

  • amps (torch.Tensor, shape (N,)) – Non-negative amplitude values for each splat.

replace_with(centers: torch.Tensor, Ls: torch.Tensor, amps: torch.Tensor) → None

Hard-replace the whole parameter set in-place.

Warning

Reassigning nn.Parameter attributes invalidates any optimizer state (Adam moments, momentum buffers, etc.) registered against the previous parameter tensors. Callers that intend to keep training after a replace_with must rebuild the optimizer — initialize_optimization is the canonical entry point.

The current best-state restore path (optimization.py::_restore_best_state) is safe because it runs purely under torch.no_grad() and does NOT call optimizer.step() afterwards: it only re-evaluates the loss so the reported metrics match the restored parameters.

prune_(keep_mask: torch.Tensor) → None

Keep only indices where keep_mask is True.

append_(centers_new: torch.Tensor, Ls_new: torch.Tensor, amps_new: torch.Tensor) → None

Append new splats to the tail.

n_splats() → int[source]

Return current number of splats.

forward() → torch.Tensor[source]

Render all splats using AABB truncation at ‘truncate’ sigmas. Avoids explicit Sigma^{-1} by solving L y = (x-mu) and using ||y||^2. Standard Gaussian falloff: exp(-0.5 * ||y||^2).

luxar.gsplats.models.render_gaussians(shape: Sequence[int], centers: torch.Tensor, Ls: torch.Tensor, amps: torch.Tensor, truncate: float = 2.75, intensity_floor: float | None = 1e-05, chunk_size: int | None = None) → torch.Tensor[source]

Fast vectorized renderer with 2D/3D fast-paths. Falls back to the generic nD implementation for d != 2 and d != 3.

Renders shifted Gaussians: a * scale * max(0, exp(-0.5 * ||y||^2) - C) where y = L^{-1}(x - mu), C = exp(-0.5 * T^2), scale = 1/(1-C). The shift ensures C^0 continuity at the truncation boundary.

Parameters:
  • shape (Sequence[int]) – Output shape of the rendered image/volume.

  • centers (torch.Tensor, shape (N, d)) – Center positions in voxel coordinates.

  • Ls (torch.Tensor, shape (N, d, d)) – Lower-triangular Cholesky factors.

  • amps (torch.Tensor, shape (N,)) – Splat amplitudes.

  • truncate (float, default DEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations.

  • intensity_floor (float or None, default 1e-5) – Minimum intensity threshold for amplitude-aware culling. Pass None (or a non-positive value) to disable culling entirely.

  • chunk_size (int, optional) – Chunk size for memory management.

Returns:

Rendered image/volume.

Return type:

torch.Tensor

luxar.gsplats.models.render_gaussians_numpy(shape: Sequence[int], result: GSplatData, truncate: float = 2.75, chunk_size: int | None = None) → ndarray[source]

CPU NumPy output wrapper around torch renderer (no grads).

Takes a GSplatData and renders it to an image/volume.

Parameters:
  • shape (Sequence[int]) – Output image/volume shape.

  • result (GSplatData) – Fitted Gaussian splat result containing centers, amplitudes, and cholesky_factors.

  • truncate (float, default DEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations.

  • chunk_size (int, optional) – Chunk size for memory management.

Returns:

Rendered image/volume.

Return type:

np.ndarray

luxar.gsplats.models.render_gaussians_pytorch(shape: Sequence[int], result: GSplatData, truncate: float = 2.75, device: str = 'cpu', chunk_size: int | None = None) → torch.Tensor[source]

PyTorch wrapper for rendering gaussians.

Takes a GSplatData and renders it to a tensor on specified device.

Parameters:
  • shape (Sequence[int]) – Output image/volume shape.

  • result (GSplatData) – Fitted Gaussian splat result containing centers, amplitudes, and cholesky_factors.

  • truncate (float, default DEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations.

  • device (str, default "cpu") – PyTorch device for computation.

  • chunk_size (int, optional) – Chunk size for memory management.

Returns:

Rendered image/volume on specified device.

Return type:

torch.Tensor

luxar.gsplats.models.stable_inverse_softplus(y: ndarray, beta: float = 1.0) → ndarray[source]

Compute numerically stable inverse of softplus function.

The softplus function is softplus(x) = (1/beta) * log(1 + exp(beta*x)). This function computes its inverse: x such that softplus(x) = y.

Uses expm1 for numerical stability when computing exp(beta*y) - 1, which avoids catastrophic cancellation for small y values.

Parameters:
  • y (np.ndarray) – Input values (must be positive since softplus range is (0, inf)).

  • beta (float, default 1.0) – Softplus scaling parameter. Higher values make function steeper.

Returns:

Inverse softplus values with same shape as input.

Return type:

np.ndarray

Notes

Mathematical relationship:

softplus(x) = (1/beta) * log(1 + exp(beta*x))
inverse_softplus(y) = (1/beta) * log(exp(beta*y) - 1)
                    = (1/beta) * log(expm1(beta*y))  # numerically stable
luxar.gsplats.models.stable_inverse_softplus_torch(y: torch.Tensor, beta: float = 1.0) → torch.Tensor[source]

Compute numerically stable inverse of softplus function (PyTorch version).

This is a GPU-compatible version that avoids CPU-GPU transfers. Runs entirely on the same device as the input tensor.

The softplus function is softplus(x) = (1/beta) * log(1 + exp(beta*x)). This function computes its inverse: x such that softplus(x) = y.

Parameters:
  • y (torch.Tensor) – Input values (must be positive since softplus range is (0, inf)).

  • beta (float, default 1.0) – Softplus scaling parameter. Higher values make function steeper.

Returns:

Inverse softplus values with same shape, dtype, and device as input.

Return type:

torch.Tensor

Notes

Mathematical relationship:

softplus(x) = (1/beta) * log(1 + exp(beta*x))
inverse_softplus(y) = (1/beta) * log(exp(beta*y) - 1)
                    = (1/beta) * log(expm1(beta*y))  # numerically stable
luxar.gsplats.models.solve_lower_triangular(L: torch.Tensor, B: torch.Tensor) → torch.Tensor[source]

Solve lower triangular linear system L @ X = B for X.

This function provides cross-version compatibility for PyTorch’s triangular solve functionality, preferring the newer torch.linalg.solve_triangular when available, falling back to torch.triangular_solve for older versions.

Parameters:
  • L (torch.Tensor, shape (d, d) or (N, d, d)) – Lower triangular coefficient matrix. Upper triangular elements are ignored. For batched operation, first dimension is batch size.

  • B (torch.Tensor, shape (d, P) or (N, d, P)) – Right-hand side matrix with P solution vectors in columns. Must have compatible batch dimensions with L.

Returns:

Solution matrix X such that L @ X = B.

Return type:

torch.Tensor, shape (d, P) or (N, d, P)

Notes

This solver is numerically stable and efficient for lower triangular systems, commonly arising from Cholesky decomposition. The operation is performed via forward substitution.

Gaussian Splat Models

Gaussian splat model and rendering functions.

class luxar.gsplats.models.gsplats.GaussianSplatModel(*args: Any, **kwargs: Any)[source]

Bases: Module

PyTorch model for n-dimensional oriented Gaussian splats with full covariance matrices.

This model represents a collection of oriented Gaussian functions (splats) that can be optimized to reconstruct images or volumes. Each splat is parameterized by:

  1. Center position: Constrained to image domain via sigmoid parameterization

  2. Covariance matrix: Represented via Cholesky decomposition L where Σ = L @ L^T

  3. Amplitude: Non-negative scalar via softplus activation

Mathematical formulation:

Each splat k contributes: a_k * exp(-0.5 * (x-μ_k)^T @ Σ_k^{-1} @ (x-μ_k))

Computational optimizations:
  • Avoids explicit matrix inversion by solving triangular system L @ y = (x-μ)

  • Uses AABB truncation for efficient rendering

  • Batched operations for multiple splats

Parameters:
  • shape (Sequence[int]) – Dimensions of the target image/volume to reconstruct.

  • centers0 (np.ndarray, shape (N, d)) – Initial center positions in voxel coordinates.

  • L0 (np.ndarray, shape (N, d, d)) – Initial lower-triangular Cholesky factors.

  • amps0 (np.ndarray, shape (N,)) – Initial amplitude values.

  • sigma_min_diag (Sequence[float]) – Minimum diagonal values for Cholesky factor (prevents degeneracy).

  • sigma_max_diag (Sequence[float], optional) – Maximum diagonal values for Cholesky factor (prevents over-smoothing).

  • amp_max (float, optional) – Maximum amplitude value. Prevents amplitude explosion during optimization, especially with aggressive compression (few splats). Since images are normalized to [0, 1], a value of 1.0 matches the max possible intensity.

  • max_eccentricity (float, optional) – Maximum allowed eccentricity (ratio of largest to smallest eigenvalue of the covariance matrix Σ = L @ L^T). This bounds the actual shape elongation of the Gaussian splats. For example, max_eccentricity=4.0 means the longest axis can be at most 2x the shortest (since eccentricity is the variance ratio, axis ratio = sqrt(eccentricity)).

  • truncate (float, default DEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations for computational efficiency.

  • device (str or torch.device, optional) – PyTorch device for computations. Explicit values override auto-detection.

  • use_cuda (bool, default True) – Allow CUDA during auto-detection when device is not provided.

  • use_metal (bool, default True) – Allow MPS/Metal during auto-detection when device is not provided.

sigma_min_diag: torch.Tensor
__init__(shape: Sequence[int], centers0: np.ndarray, L0: np.ndarray, amps0: np.ndarray, sigma_min_diag: Sequence[float], sigma_max_diag: Sequence[float] | None = None, amp_max: float | None = None, max_eccentricity: float | None = None, truncate: float = 2.75, voxel_size: np.ndarray | None = None, device: str | torch.device | None = None, use_cuda: bool = True, use_metal: bool = True) → None[source]
voxel_size: torch.Tensor | None
amp_max: float | None
max_eccentricity: float | None
sigma_max_diag: torch.Tensor | None
current_params() → Tuple[torch.Tensor, torch.Tensor, torch.Tensor][source]

Extract current parameter values from the model’s learnable parameters.

Applies all transformations to convert raw parameters to their final forms: - Centers: sigmoid transformation to ensure bounds - Cholesky factors: reconstruction from diagonal/off-diagonal components - Amplitudes: softplus transformation to ensure non-negativity

Returns:

  • centers (torch.Tensor, shape (N, d)) – Center coordinates in voxel units, bounded within image domain.

  • L (torch.Tensor, shape (N, d, d)) – Lower-triangular Cholesky factors where covariance Σ = L @ L^T.

  • amps (torch.Tensor, shape (N,)) – Non-negative amplitude values for each splat.

replace_with(centers: torch.Tensor, Ls: torch.Tensor, amps: torch.Tensor) → None

Hard-replace the whole parameter set in-place.

Warning

Reassigning nn.Parameter attributes invalidates any optimizer state (Adam moments, momentum buffers, etc.) registered against the previous parameter tensors. Callers that intend to keep training after a replace_with must rebuild the optimizer — initialize_optimization is the canonical entry point.

The current best-state restore path (optimization.py::_restore_best_state) is safe because it runs purely under torch.no_grad() and does NOT call optimizer.step() afterwards: it only re-evaluates the loss so the reported metrics match the restored parameters.

prune_(keep_mask: torch.Tensor) → None

Keep only indices where keep_mask is True.

append_(centers_new: torch.Tensor, Ls_new: torch.Tensor, amps_new: torch.Tensor) → None

Append new splats to the tail.

n_splats() → int[source]

Return current number of splats.

forward() → torch.Tensor[source]

Render all splats using AABB truncation at ‘truncate’ sigmas. Avoids explicit Sigma^{-1} by solving L y = (x-mu) and using ||y||^2. Standard Gaussian falloff: exp(-0.5 * ||y||^2).

Model Utilities

Multiscale Decomposition

Hierarchical multiscale Gaussian splat decomposition.

Multi-scale image decomposition for efficient Gaussian splatting.

This package provides tools for decomposing n-dimensional images into non-negative multi-scale components, enabling efficient hierarchical Gaussian splat fitting.

Main Functions

decompose_image : Decompose image into multi-scale components MultiScaleDecomposer : PyTorch model for decomposition decomposition_loss : Loss function for optimization

Examples

>>> from luxar.gsplats.multiscale import decompose_image
>>> import numpy as np
>>>
>>> # Decompose a 2D image
>>> V = np.random.rand(256, 256)
>>> scales_list, stats = decompose_image(V, scales=[1, 2, 4])
>>>
>>> # Use scale components
>>> V_full = scales_list[0]      # Full resolution
>>> V_half = scales_list[1]      # Half resolution
>>> V_quarter = scales_list[2]   # Quarter resolution
luxar.gsplats.multiscale.decompose_image(V: ndarray, scales: List[int] = [1, 2, 4, 8, 16, 32], n_iters: int = 500, lr: float = 0.01, energy_weight: float = 0.01, alpha: float = 1.5, loss_type: str = 'l1', asymmetric_penalty: float | None = 10.0, init_method: str = 'coarse', max_abs_error_threshold: float | None = None, interpolation: str = 'cubic', napari_movie: bool = False, movie_every: int = 1, movie_max_frames: int | None = None, device: str | None = None, verbose: bool = True) → Tuple[List[ndarray], Dict[str, Any]][source]

Decompose n-dimensional image into multi-scale non-negative components.

Optimizes a decomposition V = Σₖ upsample(Vₖ) where each Vₖ represents features at a different scale, with energy preferentially distributed toward coarse scales.

Parameters:
  • V (np.ndarray) – Input n-dimensional image to decompose

  • scales (List[int], default [1, 2, 4, 8]) – Scale factors. Scale 1 = full res, scale 2 = half res, etc. Scales larger than min image dim are filtered with a warning. The actual scales used are returned in stats[‘scales’].

  • n_iters (int, default 500) – Number of optimization iterations

  • lr (float, default 0.01) – Learning rate for Adam optimizer

  • energy_weight (float, default 0.01) – Weight for hierarchical energy penalty (higher = more energy to coarse)

  • alpha (float, default 1.5) – Growth factor for energy penalties (higher = stronger coarse preference)

  • loss_type (str, default "l1") – Type of reconstruction loss: “l1” (Mean Absolute Error, default), “mse” (Mean Squared Error), or “poisson” (Poisson Deviance)

  • asymmetric_penalty (Optional[float], default 10.0) – Over-prediction penalty factor. Multiplies reconstruction loss for regions where pred > target by this factor. Set to None to disable asymmetric loss.

  • init_method (str, default "coarse") – Initialization: “coarse” (energy toward coarse - BEST), “pyramid” (Gaussian pyramid), “uniform” (equal split), or “finest” (all in finest). Coarse gives best convergence and quality.

  • max_abs_error_threshold (float, optional) – Convergence threshold for max absolute error. Stops early when max|reconstruction - target| < threshold. None uses 1% of image range.

  • interpolation (str, default 'cubic') –

    Interpolation method for upsampling scale components. Three modes available: - ‘nearest’: Nearest-neighbor (fastest, blocky output) - ‘linear’: Linear interpolation (fast, smooth) - ‘cubic’: Cubic interpolation (highest quality, practical for 3D)

    Implementation details: - 2D: Uses PyTorch’s ‘bicubic’ interpolation - 3D+: Uses Keys cubic convolution (vectorized, 27-43× faster) - Keys cubic uses separable filters for efficient nD processing

    Note: Cubic interpolation can produce small negative values (undershoot) due to the negative lobes in the cubic kernel. These are automatically clamped to zero to maintain non-negativity constraint.

  • napari_movie (bool, default False) – Enable recording of optimization progress for napari movie visualization

  • movie_every (int, default 1) – Record a movie frame every N iterations (only if napari_movie=True)

  • movie_max_frames (Optional[int], default None) – Maximum number of frames to store. If None, no limit (can use lots of memory). Oldest frames are discarded when limit is reached.

  • device (str, optional) – PyTorch device (‘cpu’, ‘cuda’, ‘mps’). Auto-detects if None.

  • verbose (bool, default True) – Print optimization progress

Returns:

  • scales_list (List[np.ndarray]) – List of K non-negative images at each scale. scales_list[i] has shape (s₀/rᵢ, s₁/rᵢ, …, sₙ₋₁/rᵢ)

  • stats (dict) – Optimization statistics: - ‘history’: List of per-iteration loss components - ‘final_error’: Final reconstruction MSE - ‘best_error’: Best reconstruction MSE achieved - ‘best_max_abs_error’: Best maximum absolute error achieved - ‘converged’: Boolean indicating if convergence criterion was met - ‘best_iteration’: Iteration where best result was achieved - ‘actual_iters’: Number of iterations run (less if converged early) - ‘energy_distribution’: Fraction of total energy per scale - ‘scales’: Scale factors used - ‘time_seconds’: Total optimization time - ‘movie_frames’: Dictionary with movie data (if napari_movie=True), or None - ‘interpolation’: Interpolation mode used (‘nearest’, ‘linear’, or ‘cubic’)

Examples

>>> import numpy as np
>>> from luxar.gsplats.multiscale import decompose_image
>>>
>>> # 2D example
>>> V = np.random.rand(256, 256)
>>> scales_list, stats = decompose_image(V, scales=[1, 2, 4])
>>> aprint([s.shape for s in scales_list])
[(256, 256), (128, 128), (64, 64)]
>>>
>>> # 3D example
>>> V = np.random.rand(128, 128, 128)
>>> scales_list, stats = decompose_image(V, scales=[1, 2, 4, 8])
>>> aprint(f"Energy distribution: {stats['energy_distribution']}")
Energy distribution: [0.62, 0.23, 0.11, 0.04]

Notes

  • Uses Gaussian pyramid initialization for stable convergence

  • All output images are guaranteed non-negative

  • Reconstruction: V ≈ Σₖ upsample(scales_list[k])

  • Higher alpha values push more energy to coarse scales

class luxar.gsplats.multiscale.MultiScaleDecomposer(*args: Any, **kwargs: Any)[source]

Bases: Module

PyTorch model for multi-scale image decomposition.

Decomposes an n-dimensional image V into K non-negative scale components:

V = Σₖ upsample(Vₖ)

where Vₖ are images at different resolutions (scale factors).

Parameters:
  • shape (Tuple[int, ]) – Shape of the target image (n-dimensional)

  • scales (List[int], default [1, 2, 4, 8]) – Scale factors for decomposition. Scale 1 = full resolution, scale 2 = half resolution, etc.

  • interpolation (str, default 'cubic') –

    Interpolation method for upsampling: ‘nearest’, ‘linear’, or ‘cubic’. - ‘nearest’: Fastest, blocky output - ‘linear’: Fast, smooth output - ‘cubic’: Highest quality (default), now practical for 3D

    Implementation details: - 2D: ‘nearest’, ‘bilinear’, or ‘bicubic’ (PyTorch) - 3D+: ‘nearest’, ‘trilinear’, or Keys cubic convolution (vectorized)

    Keys cubic convolution uses separable filters with vectorized operations for efficient nD interpolation (27-43× faster than torch-interpol). Note: Cubic interpolation may produce small negative values (undershoot) which are automatically clamped to zero.

raw_images

Learnable parameters for each scale (unconstrained, applied softplus)

Type:

nn.ParameterList

__init__(shape: Tuple[int, ...], scales: List[int] = [1, 2, 4, 8], interpolation: str = 'cubic') → None[source]
forward() → Tuple[List[torch.Tensor], List[torch.Tensor], torch.Tensor][source]

Forward pass: apply non-negativity and upsample all scales.

Returns:

  • scales_list (List[torch.Tensor]) – List of K non-negative images at each scale

  • upsampled_list (List[torch.Tensor]) – List of K upsampled images (all at full resolution)

  • reconstruction (torch.Tensor) – Sum of all upsampled scales (final reconstruction)

initialize_from_pyramid(target: torch.Tensor) → None

Initialize parameters from Gaussian pyramid decomposition.

This provides a sensible starting point where the decomposition already approximately represents the target image. Energy is distributed across scales from coarse to fine.

Uses negative propagation: if a coarse scale overshoots (causing negative values in the remainder), those negatives are propagated to finer scales, which compensate by reducing their values in those regions. This ensures energy conservation without information loss.

Parameters:

target (torch.Tensor) – Target image to decompose (shape must match self.shape)

initialize_finest_scale(target: torch.Tensor) → None

Initialize with all energy in the finest (highest resolution) scale.

This creates a “trivial” starting point where all energy is in the finest scale and must be redistributed during optimization. All other scales start at near-zero values.

Parameters:

target (torch.Tensor) – Target image to decompose (shape must match self.shape)

initialize_uniform(target: torch.Tensor) → None

Initialize with energy split uniformly across all scales.

Each scale gets 1/K of the total energy (when upsampled to full resolution), where K is the number of scales. This provides a balanced starting point between pyramid and finest initialization.

Parameters:

target (torch.Tensor) – Target image to decompose (shape must match self.shape)

initialize_coarse(target: torch.Tensor) → None

Initialize with energy weighted toward coarse scales.

Energy is distributed proportionally to scale factor: coarser scales (larger scale factors) get more energy. For scales [1, 2, 4, 8], the distribution is [1x, 2x, 4x, 8x], so scale 8 gets 8 times more energy than scale 1. This strongly biases the initialization toward coarse scales.

Parameters:

target (torch.Tensor) – Target image to decompose (shape must match self.shape)

initialize_zero(target: torch.Tensor) → None

Initialize all scales to zero (or near-zero).

This creates a “worst case” starting point where all scales start at effectively zero and must be learned from scratch. Useful for understanding the importance of initialization and as a baseline comparison.

Parameters:

target (torch.Tensor) – Target image to decompose (shape must match self.shape)

luxar.gsplats.multiscale.decomposition_loss(model: MultiScaleDecomposer, target: torch.Tensor, energy_weight: float = 0.001, alpha: float = 1.5, loss_type: str = 'l1', asymmetric_penalty: float | None = 10.0) → Tuple[torch.Tensor, Dict[str, float]][source]

Compute multi-scale decomposition loss.

Combines reconstruction fidelity with hierarchical energy penalties to encourage energy distribution toward coarse scales.

Loss = L_reconstruction + λ_energy × L_energy

where:

L_reconstruction = loss_fn(Σₖ upsample(Vₖ), V) with optional asymmetric penalty L_energy = Σₖ (αᵏ × ∫Vₖ) / ∫V

Parameters:
  • model (MultiScaleDecomposer) – Decomposition model

  • target (torch.Tensor) – Target image to reconstruct

  • energy_weight (float, default 0.001) – Weight for energy penalty (higher pushes more energy to coarse scales)

  • alpha (float, default 1.5) – Growth factor for energy penalties (αᵏ grows exponentially with scale index)

  • loss_type (str, default "l1") – Type of reconstruction loss: “l1” (Mean Absolute Error, default), “mse” (Mean Squared Error), or “poisson” (Poisson Deviance)

  • asymmetric_penalty (Optional[float], default 10.0) – Over-prediction penalty factor. Multiplies reconstruction loss for regions where pred > target by this factor. Set to None to disable asymmetric loss.

Returns:

  • total_loss (torch.Tensor) – Combined loss (scalar)

  • stats (dict) – Dictionary with per-component losses and diagnostics: - ‘recon_loss’: Reconstruction loss - ‘energy_loss’: Hierarchical energy penalty (normalized) - ‘total_loss’: Combined loss - ‘energy_scale_i’: Energy fraction at scale i

luxar.gsplats.multiscale.show_optimization_movie(movie_frames: Dict[str, Any], shape: Tuple[int, ...], interpolation: str = 'cubic') → None[source]

Display napari viewer with optimization movie (target, reconstruction, residual, and all scales over time).

Parameters:
  • movie_frames (dict) – Dictionary containing movie frame data: - ‘target’: List of target frames - ‘reconstruction’: List of reconstruction frames - ‘residual’: List of residual frames - ‘scales’: List of lists of scale components (one list per frame) - ‘iterations’: List of iteration numbers

  • shape (tuple) – Shape of the original data

  • interpolation (str, default 'cubic') – Upsampling method: ‘nearest’, ‘linear’, or ‘cubic’. Should match the interpolation used during optimization.

luxar.gsplats.multiscale.upsample_for_visualization(img: ndarray, target_shape: Tuple[int, ...], interpolation: str = 'cubic') → ndarray[source]

Upsample numpy array to target shape for visualization purposes.

Uses the same interpolation methods as the optimization to ensure visual consistency between optimization and visualization.

Parameters:
  • img (np.ndarray) – Input image of any dimensionality

  • target_shape (Tuple[int, ]) – Target shape to upsample to

  • interpolation (str, default 'cubic') – Interpolation method: ‘nearest’, ‘linear’, or ‘cubic’. Should match the interpolation used during optimization.

Returns:

Upsampled image of shape target_shape

Return type:

np.ndarray

Examples

>>> import numpy as np
>>> from luxar.gsplats.multiscale import upsample_for_visualization
>>> img = np.random.rand(64, 64)
>>> upsampled = upsample_for_visualization(img, (256, 256), 'cubic')
>>> upsampled.shape
(256, 256)

I/O Operations

Save and load Gaussian splat results.

I/O operations for Gaussian splat persistence.

This package provides functions for saving and loading fitted Gaussian splat data in a dedicated zarr format (.gsplats.zarr).

Main functions: - save_gsplats() - Save GSplatData to .gsplats.zarr - load_gsplats() - Load GSplatData from .gsplats.zarr - load_default_gsplats() - Materialize the tree’s default-rendered selection - inspect_gsplats_zarr() - Inspect .gsplats.zarr metadata - format_gsplats_info() - Format inspection info as string

Spatial ordering utilities: - sort_splats_spatial() - Sort splats using Morton or Hilbert curves - compute_chunk_bounds_gsplats() - Compute chunk bounding boxes with extent

luxar.gsplats.io.save_gsplats(path: str | Path, centers: ndarray, amplitudes: ndarray, cholesky_factors: ndarray, colors: ndarray | None = None, label_ids: ndarray | None = None, label_vocabulary: Dict[int, str] | None=None, ordering: Literal['morton', 'hilbert', 'none']='hilbert', encoding_mode: EncodingMode = EncodingMode.AUTO, fitting_info: Dict[str, ~typing.Any] | None=None, fitting_config: Dict[str, ~typing.Any] | None=None, provenance_info: Dict[str, ~typing.Any] | None=None, description: str | None = None, compress: Literal['zip', 'tar.gz'] | None=None, compressor: Any | None = <width-aware default compressor>, zip_deflate: bool = False, truncation_radius: float = 2.75, amplitude_bits: Literal['auto', 8, 16]=16) → None[source]

Save a single Gaussian-splat set to .gsplats.zarr (a leaf node).

Thin convenience wrapper: builds a single-leaf GSplatNode and hands it to write_gsplats_tree(). Colors are written via the shared COLOR helper, which auto-detects SDR vs HDR (values > 1) — there is no explicit color_mode knob; amplitudes use the canonical POSITIVE_SCALAR encoding.

Empty input (n_splats == 0) raises — the shared writer validates against empty splat sets, matching the scene writer’s no-empty policy.

luxar.gsplats.io.load_gsplats(path: str | Path, include_stats: bool = False) → GSplatData[source]

Load Gaussian splats from .gsplats.zarr format.

Supports both uncompressed (.gsplats.zarr) and compressed formats (.gsplats.zarr.zip, .gsplats.zarr.tar.gz). Compressed archives are automatically extracted to a temporary directory.

Arrays are automatically decoded from their stored encoding (quantization, broadcasting, etc.) to float32.

Only matrix-shaped node trees map to a GSplatData — a leaf, or a kind=lod group whose children are all leaves (the substitutive × additive matrix). A genuinely nested tree (a kind=partition root, or a lod group with non-leaf children) has no flat GSplatData equivalent and raises ValueError; use load_default_gsplats to materialize the default-rendered selection, or consume the node tree directly with load_gsplat_node.

Parameters:
  • path – Path to .gsplats.zarr directory or compressed archive

  • include_stats – Whether to include fitting/provenance metadata in stats

Returns:

GSplatData with decoded arrays and optional stats

Raises:
  • FileNotFoundError – If path doesn’t exist

  • ValueError – If the format is invalid/incompatible, or the file is a non-matrix (partition/nested) tree.

luxar.gsplats.io.load_default_gsplats(path: str | Path, include_stats: bool = False) → GSplatData[source]

Load the splats selected by the tree’s default rendering semantics.

Matrix-shaped inputs retain their existing substitutive/additive structure. For a partition or nested tree, all partition children, the default (finest) child of each substitutive LOD group, and every additive sub-LOD are materialized as one flat in-memory dataset. Root stats are deliberately retained unchanged when requested because this helper is read-only; a writer that changes topology must scrub structure-scoped metadata itself.

Utilities

Matrix packing/unpacking utilities for covariance matrices.

Utilities for Gaussian splat fitting.

This package provides utility functions for: - Lower-triangular matrix operations (pack/unpack Cholesky factors) - Cholesky factor validation for Gaussian splats - Cholesky dimension permutation and embedding for cross-dimensional scenes - Gradient dilution compensation for higher-dimensional optimization - Triangle matrix size calculations

luxar.gsplats.utils.is_mps_available() → bool[source]

Return True when PyTorch’s MPS backend is available.

Some older or CPU-only PyTorch builds do not expose torch.backends.mps. Centralizing the check keeps device auto-detection robust across builds.

luxar.gsplats.utils.resolve_torch_device(device: str | torch.device | None = None, *, use_cuda: bool = True, use_metal: bool = True) → torch.device[source]

Resolve an explicit or auto-selected PyTorch device.

Explicit device names always win except "auto", which is equivalent to None. Auto-selection prefers CUDA over MPS/Metal, and both accelerator classes honor their corresponding opt-in flags before falling back to CPU.

luxar.gsplats.utils.tril_size(d: int) → int[source]

Calculate number of elements in lower-triangular portion of d×d matrix.

This includes all elements on and below the main diagonal, which is the standard storage requirement for Cholesky decomposition.

Parameters:

d (int) – Dimension of square matrix.

Returns:

Number of lower-triangular elements: d*(d+1)/2.

Return type:

int

Examples

>>> tril_size(3)
6  # Elements: (0,0), (1,0), (1,1), (2,0), (2,1), (2,2)
luxar.gsplats.utils.calculate_gradient_dilution_factor(d: int) → float[source]

Calculate gradient dilution compensation factor for higher dimensions.

Gradient dilution occurs because higher dimensions have more parameters per splat, spreading gradients thinner. This function computes the compensation factor.

Parameters:

d (int) – Dimensionality

Returns:

Gradient dilution compensation factor (multiply base learning rate by this)

Return type:

float

Notes

Background: In Gaussian splat fitting, each splat has d center params + d(d+1)/2 Cholesky params. As dimensionality increases, the same loss gradient gets distributed across more parameters, causing each parameter to receive smaller gradient updates. This effect is called “gradient dilution.”

Formula rationale:

For 2D/3D: Simple linear scaling by parameter count ratio.

  • 2D: 2 + 3 = 5 params per splat (baseline)

  • 3D: 3 + 6 = 9 params, factor = 9/5 = 1.8

For 4D+: Two additional effects compound:

  1. Parameter dilution (params_current / params_2d): More parameters need updates

  2. Spatial complexity (d^0.8): Higher-dimensional spaces have exponentially more “room” for splats to move, requiring larger position updates to achieve equivalent progress in fitting. The 0.8 exponent was empirically determined through testing on 4D-8D synthetic datasets, balancing convergence speed vs. stability.

Empirical validation:

  • Without compensation: 4D+ fitting converges 3-10x slower than 2D/3D

  • With d^0.8 factor: Convergence rates across dimensions within 2x of each other

  • The 0.8 exponent is a compromise: d^1.0 caused instability in 6D+, d^0.5 was insufficient for 4D-5D

Example factors:

  • 2D: 1.0 (baseline)

  • 3D: 1.8

  • 4D: 3.0 * 3.5 / 5 = 2.1 (d^0.8 approx 3.0, params = 4+10 = 14)

  • 6D: 4.2 * 5.2 / 5 = 4.4 (d^0.8 approx 4.2, params = 6+21 = 27)

luxar.gsplats.utils.pack_tril(L: ndarray) → ndarray[source]

Pack lower-triangular portion of matrices into compact vector representation.

Extracts and concatenates lower-triangular elements (including diagonal) from a batch of square matrices. This is commonly used for efficient storage and transmission of Cholesky factors.

Parameters:

L (np.ndarray, shape (N, d, d)) – Batch of square matrices. Only elements where i >= j are used (on and below main diagonal). Upper triangular elements are ignored.

Returns:

Packed vectors containing lower-triangular elements in row-major order. For each matrix, elements are ordered as: [L[0,0], L[1,0], L[1,1], L[2,0], L[2,1], L[2,2], …]

Return type:

np.ndarray, shape (N, d*(d+1)//2)

Examples

>>> L = np.array([[[1, 0], [2, 3]]])  # Shape (1, 2, 2)
>>> pack_tril(L)
array([[1, 2, 3]])  # Shape (1, 3): [L00, L10, L11]
luxar.gsplats.utils.unpack_tril(v: ndarray, d: int) → ndarray[source]

Unpack compact vector representation into lower-triangular matrices.

Inverse operation of pack_tril(). Reconstructs square matrices from their packed lower-triangular representations, filling upper triangle with zeros.

Parameters:
  • v (np.ndarray, shape (N, d*(d+1)//2)) – Packed vectors containing lower-triangular elements in row-major order.

  • d (int) – Dimension of square matrices to reconstruct.

Returns:

Batch of lower-triangular matrices with zeros above diagonal and packed elements on/below diagonal.

Return type:

np.ndarray, shape (N, d, d)

Examples

>>> v = np.array([[1, 2, 3]])  # Shape (1, 3)
>>> unpack_tril(v, 2)
array([[[1, 0],
        [2, 3]]])  # Shape (1, 2, 2)
luxar.gsplats.utils.validate_cholesky_shape(cholesky_factors: ndarray, ndim: int, n_splats: int | None = None, allow_uniform: bool = True) → Tuple[bool, int][source]

Validate shape of packed Cholesky factors for Gaussian splats.

Packed Cholesky factors should be either: - Per-splat: shape (N, k) where k = d*(d+1)//2 - Uniform: shape (k,) when allow_uniform=True

Parameters:
  • cholesky_factors (np.ndarray) – Packed Cholesky factors array to validate.

  • ndim (int) – Number of dimensions (d). Determines expected packed size k = d*(d+1)//2.

  • n_splats (int, optional) – Expected number of splats. If provided, validates first dimension matches. Ignored if cholesky_factors is uniform (1D).

  • allow_uniform (bool, default True) – Whether to allow uniform Cholesky factors (shape (k,)) for all splats.

Returns:

  • is_uniform (bool) – True if cholesky_factors is uniform (shape (k,)), False if per-splat.

  • actual_n_splats (int) – Actual number of splats inferred from shape. For uniform, returns 0.

Raises:

ValueError – If shape is invalid for the given ndim and n_splats.

Examples

>>> # Valid per-splat for 2D (k=3)
>>> chol = np.random.rand(100, 3)
>>> is_uniform, n = validate_cholesky_shape(chol, ndim=2, n_splats=100)
>>> is_uniform, n
(False, 100)
>>> # Valid uniform for 3D (k=6)
>>> chol = np.random.rand(6)
>>> is_uniform, n = validate_cholesky_shape(chol, ndim=3)
>>> is_uniform, n
(True, 0)
>>> # Invalid shape raises
>>> chol = np.random.rand(100, 5)  # Wrong k for 2D
>>> validate_cholesky_shape(chol, ndim=2)
Traceback (most recent call last):
    ...
ValueError: Cholesky factors have wrong packed size...
luxar.gsplats.utils.permute_cholesky_packed(packed: ndarray, d: int, perm: Sequence[int]) → ndarray[source]

Permute dimensions of packed Cholesky factors.

Given packed Cholesky factors L where Sigma = L @ L^T, reorder the dimensions according to the permutation. The new Cholesky L’ satisfies Sigma’[i,j] = Sigma[perm[i], perm[j]].

Parameters:
  • packed (np.ndarray, shape (N, k) where k = d*(d+1)//2) – Packed lower-triangular Cholesky factors.

  • d (int) – Number of dimensions.

  • perm (sequence of int) – Permutation of dimension indices. perm[new_i] = old_i. E.g., [2, 0, 1] means new dim 0 was old dim 2.

Returns:

Packed Cholesky factors with permuted dimensions.

Return type:

np.ndarray, shape (N, k)

Examples

>>> # Reverse 2D dimensions: swap X and Y
>>> packed = np.array([[1.0, 0.5, 2.0]])  # L00, L10, L11
>>> permute_cholesky_packed(packed, 2, [1, 0])
luxar.gsplats.utils.embed_cholesky_packed(packed: ndarray, d_src: int, d_dst: int, dim_mapping: List[int], fill_sigma: Dict[int, float] | None = None) → ndarray[source]

Embed lower-dimensional packed Cholesky factors into higher dimensions.

Takes d_src-dimensional Cholesky factors and embeds them into a d_dst-dimensional space (d_dst >= d_src). Mapped dimensions carry over the original covariance; unmapped dimensions get independent Gaussian variance (diagonal only, no cross-terms).

Parameters:
  • packed (np.ndarray, shape (N, k_src) where k_src = d_src*(d_src+1)//2) – Packed Cholesky factors in the source dimensionality.

  • d_src (int) – Source dimensionality.

  • d_dst (int) – Target dimensionality (must be >= d_src).

  • dim_mapping (list of int, length d_src) – Maps source dimension i to target dimension dim_mapping[i]. E.g., [1, 2, 3] maps src dims 0,1,2 to dst dims 1,2,3.

  • fill_sigma (dict of {target_dim_index: sigma_value}, optional) – Standard deviations for unmapped target dimensions. Unmapped dims not in fill_sigma default to 1.0.

Returns:

Packed Cholesky factors in the target dimensionality.

Return type:

np.ndarray, shape (N, k_dst) where k_dst = d_dst*(d_dst+1)//2

Examples

>>> # Embed 2D into 3D: src dims [0,1] → dst dims [0,1], new dim 2 has sigma=0.5
>>> packed_2d = np.array([[1.0, 0.0, 1.0]])  # isotropic 2D
>>> embed_cholesky_packed(packed_2d, 2, 3, [0, 1], fill_sigma={2: 0.5})
luxar.gsplats.utils.diag_indices(d: int) → ndarray[source]

Packed-vector positions of the diagonal elements of a d×d tril matrix.

For row-major lower-triangular packing [L00, L10, L11, L20, L21, L22, ...] the diagonal element (i, i) lives at packed position (i+1)*(i+2)//2 - 1.

Parameters:

d (int) – Dimension of the square matrix.

Returns:

Integer positions of the diagonal elements within the packed vector.

Return type:

np.ndarray, shape (d,)

Examples

>>> diag_indices(3)
array([0, 2, 5])
luxar.gsplats.utils.offdiag_indices(d: int) → ndarray[source]

Packed-vector positions of the off-diagonal (strictly lower) elements.

Complement of diag_indices() within range(tril_size(d)), preserving the row-major lower-triangular order. Empty for d == 1.

Parameters:

d (int) – Dimension of the square matrix.

Returns:

Integer positions of the off-diagonal elements within the packed vector.

Return type:

np.ndarray, shape (d*(d-1)//2,)

Examples

>>> offdiag_indices(3)
array([1, 3, 4])
luxar.gsplats.utils.split_tril(packed: ndarray, d: int) → Tuple[ndarray, ndarray][source]

Split packed Cholesky factors into diagonal and off-diagonal parts.

The diagonal of a Cholesky factor is positive and scale-like while the off-diagonal is signed and zero-centred; splitting them lets each be encoded/quantised independently on disk. Operates on the last axis, so it accepts per-splat (N, k), broadcast (1, k) and uniform (k,) inputs alike.

Parameters:
  • packed (np.ndarray, shape (..., k) where k = d*(d+1)//2) – Packed lower-triangular Cholesky factors (row-major).

  • d (int) – Number of dimensions.

Returns:

  • diag (np.ndarray, shape (..., d)) – Diagonal elements in dimension order.

  • offdiag (np.ndarray, shape (..., d*(d-1)//2)) – Off-diagonal elements in row-major lower-triangular order (empty trailing axis when d == 1).

See also

merge_tril

inverse operation.

luxar.gsplats.utils.merge_tril(diag: ndarray, offdiag: ndarray, d: int) → ndarray[source]

Recombine diagonal and off-diagonal parts into packed Cholesky factors.

Inverse of split_tril(). Scatters the two column groups back to their row-major lower-triangular positions. Operates on the last axis.

Parameters:
  • diag (np.ndarray, shape (..., d)) – Diagonal elements (as returned by split_tril()).

  • offdiag (np.ndarray, shape (..., d*(d-1)//2)) – Off-diagonal elements (as returned by split_tril()).

  • d (int) – Number of dimensions.

Returns:

Packed lower-triangular Cholesky factors (row-major).

Return type:

np.ndarray, shape (..., d*(d+1)//2)

luxar.gsplats.utils.trils.tril_size(d: int) → int[source]

Calculate number of elements in lower-triangular portion of d×d matrix.

This includes all elements on and below the main diagonal, which is the standard storage requirement for Cholesky decomposition.

Parameters:

d (int) – Dimension of square matrix.

Returns:

Number of lower-triangular elements: d*(d+1)/2.

Return type:

int

Examples

>>> tril_size(3)
6  # Elements: (0,0), (1,0), (1,1), (2,0), (2,1), (2,2)
luxar.gsplats.utils.trils.calculate_gradient_dilution_factor(d: int) → float[source]

Calculate gradient dilution compensation factor for higher dimensions.

Gradient dilution occurs because higher dimensions have more parameters per splat, spreading gradients thinner. This function computes the compensation factor.

Parameters:

d (int) – Dimensionality

Returns:

Gradient dilution compensation factor (multiply base learning rate by this)

Return type:

float

Notes

Background: In Gaussian splat fitting, each splat has d center params + d(d+1)/2 Cholesky params. As dimensionality increases, the same loss gradient gets distributed across more parameters, causing each parameter to receive smaller gradient updates. This effect is called “gradient dilution.”

Formula rationale:

For 2D/3D: Simple linear scaling by parameter count ratio.

  • 2D: 2 + 3 = 5 params per splat (baseline)

  • 3D: 3 + 6 = 9 params, factor = 9/5 = 1.8

For 4D+: Two additional effects compound:

  1. Parameter dilution (params_current / params_2d): More parameters need updates

  2. Spatial complexity (d^0.8): Higher-dimensional spaces have exponentially more “room” for splats to move, requiring larger position updates to achieve equivalent progress in fitting. The 0.8 exponent was empirically determined through testing on 4D-8D synthetic datasets, balancing convergence speed vs. stability.

Empirical validation:

  • Without compensation: 4D+ fitting converges 3-10x slower than 2D/3D

  • With d^0.8 factor: Convergence rates across dimensions within 2x of each other

  • The 0.8 exponent is a compromise: d^1.0 caused instability in 6D+, d^0.5 was insufficient for 4D-5D

Example factors:

  • 2D: 1.0 (baseline)

  • 3D: 1.8

  • 4D: 3.0 * 3.5 / 5 = 2.1 (d^0.8 approx 3.0, params = 4+10 = 14)

  • 6D: 4.2 * 5.2 / 5 = 4.4 (d^0.8 approx 4.2, params = 6+21 = 27)

luxar.gsplats.utils.trils.pack_tril(L: ndarray) → ndarray[source]

Pack lower-triangular portion of matrices into compact vector representation.

Extracts and concatenates lower-triangular elements (including diagonal) from a batch of square matrices. This is commonly used for efficient storage and transmission of Cholesky factors.

Parameters:

L (np.ndarray, shape (N, d, d)) – Batch of square matrices. Only elements where i >= j are used (on and below main diagonal). Upper triangular elements are ignored.

Returns:

Packed vectors containing lower-triangular elements in row-major order. For each matrix, elements are ordered as: [L[0,0], L[1,0], L[1,1], L[2,0], L[2,1], L[2,2], …]

Return type:

np.ndarray, shape (N, d*(d+1)//2)

Examples

>>> L = np.array([[[1, 0], [2, 3]]])  # Shape (1, 2, 2)
>>> pack_tril(L)
array([[1, 2, 3]])  # Shape (1, 3): [L00, L10, L11]
luxar.gsplats.utils.trils.unpack_tril(v: ndarray, d: int) → ndarray[source]

Unpack compact vector representation into lower-triangular matrices.

Inverse operation of pack_tril(). Reconstructs square matrices from their packed lower-triangular representations, filling upper triangle with zeros.

Parameters:
  • v (np.ndarray, shape (N, d*(d+1)//2)) – Packed vectors containing lower-triangular elements in row-major order.

  • d (int) – Dimension of square matrices to reconstruct.

Returns:

Batch of lower-triangular matrices with zeros above diagonal and packed elements on/below diagonal.

Return type:

np.ndarray, shape (N, d, d)

Examples

>>> v = np.array([[1, 2, 3]])  # Shape (1, 3)
>>> unpack_tril(v, 2)
array([[[1, 0],
        [2, 3]]])  # Shape (1, 2, 2)
luxar.gsplats.utils.trils.diag_indices(d: int) → ndarray[source]

Packed-vector positions of the diagonal elements of a d×d tril matrix.

For row-major lower-triangular packing [L00, L10, L11, L20, L21, L22, ...] the diagonal element (i, i) lives at packed position (i+1)*(i+2)//2 - 1.

Parameters:

d (int) – Dimension of the square matrix.

Returns:

Integer positions of the diagonal elements within the packed vector.

Return type:

np.ndarray, shape (d,)

Examples

>>> diag_indices(3)
array([0, 2, 5])
luxar.gsplats.utils.trils.offdiag_indices(d: int) → ndarray[source]

Packed-vector positions of the off-diagonal (strictly lower) elements.

Complement of diag_indices() within range(tril_size(d)), preserving the row-major lower-triangular order. Empty for d == 1.

Parameters:

d (int) – Dimension of the square matrix.

Returns:

Integer positions of the off-diagonal elements within the packed vector.

Return type:

np.ndarray, shape (d*(d-1)//2,)

Examples

>>> offdiag_indices(3)
array([1, 3, 4])
luxar.gsplats.utils.trils.split_tril(packed: ndarray, d: int) → Tuple[ndarray, ndarray][source]

Split packed Cholesky factors into diagonal and off-diagonal parts.

The diagonal of a Cholesky factor is positive and scale-like while the off-diagonal is signed and zero-centred; splitting them lets each be encoded/quantised independently on disk. Operates on the last axis, so it accepts per-splat (N, k), broadcast (1, k) and uniform (k,) inputs alike.

Parameters:
  • packed (np.ndarray, shape (..., k) where k = d*(d+1)//2) – Packed lower-triangular Cholesky factors (row-major).

  • d (int) – Number of dimensions.

Returns:

  • diag (np.ndarray, shape (..., d)) – Diagonal elements in dimension order.

  • offdiag (np.ndarray, shape (..., d*(d-1)//2)) – Off-diagonal elements in row-major lower-triangular order (empty trailing axis when d == 1).

See also

merge_tril

inverse operation.

luxar.gsplats.utils.trils.merge_tril(diag: ndarray, offdiag: ndarray, d: int) → ndarray[source]

Recombine diagonal and off-diagonal parts into packed Cholesky factors.

Inverse of split_tril(). Scatters the two column groups back to their row-major lower-triangular positions. Operates on the last axis.

Parameters:
  • diag (np.ndarray, shape (..., d)) – Diagonal elements (as returned by split_tril()).

  • offdiag (np.ndarray, shape (..., d*(d-1)//2)) – Off-diagonal elements (as returned by split_tril()).

  • d (int) – Number of dimensions.

Returns:

Packed lower-triangular Cholesky factors (row-major).

Return type:

np.ndarray, shape (..., d*(d+1)//2)

luxar.gsplats.utils.trils.recombine_cholesky(decode: Callable[[str], ndarray | None]) → ndarray | None[source]

Recombine on-disk Cholesky factors into the packed (N, k) form.

Single source of truth for the read side of the v3.1 split layout, shared by every reader (the scene reader and the gsplat-tree decoder) so the version handling, corruption invariant, and error message live in ONE place.

decode(name) returns the named array decoded to a NumPy array, or None when that array is absent from the store. Two layouts are handled:

  • v3.1 split: cholesky_factors_diag (N, d) + cholesky_factors_offdiag (N, k-d) → merged via merge_tril().

  • v3.0 single: cholesky_factors (N, k) → returned as-is (the fallback taken when no diagonal array is present).

The off-diagonal array is legitimately absent ONLY for 1D gsplats (no off-diagonal terms); for d > 1 its absence means a corrupt or partially-written store and raises ValueError rather than silently dropping every splat’s off-diagonal covariance. Returns None when no Cholesky array is present at all (matching the legacy single-array reader).

Parameters:

decode (Callable[[str], Optional[np.ndarray]]) – Resolves an array name to its decoded values, or None if absent.

Returns:

Packed lower-triangular Cholesky factors, or None if no Cholesky array exists in the store.

Return type:

np.ndarray or None

luxar.gsplats.utils.trils.validate_cholesky_shape(cholesky_factors: ndarray, ndim: int, n_splats: int | None = None, allow_uniform: bool = True) → Tuple[bool, int][source]

Validate shape of packed Cholesky factors for Gaussian splats.

Packed Cholesky factors should be either: - Per-splat: shape (N, k) where k = d*(d+1)//2 - Uniform: shape (k,) when allow_uniform=True

Parameters:
  • cholesky_factors (np.ndarray) – Packed Cholesky factors array to validate.

  • ndim (int) – Number of dimensions (d). Determines expected packed size k = d*(d+1)//2.

  • n_splats (int, optional) – Expected number of splats. If provided, validates first dimension matches. Ignored if cholesky_factors is uniform (1D).

  • allow_uniform (bool, default True) – Whether to allow uniform Cholesky factors (shape (k,)) for all splats.

Returns:

  • is_uniform (bool) – True if cholesky_factors is uniform (shape (k,)), False if per-splat.

  • actual_n_splats (int) – Actual number of splats inferred from shape. For uniform, returns 0.

Raises:

ValueError – If shape is invalid for the given ndim and n_splats.

Examples

>>> # Valid per-splat for 2D (k=3)
>>> chol = np.random.rand(100, 3)
>>> is_uniform, n = validate_cholesky_shape(chol, ndim=2, n_splats=100)
>>> is_uniform, n
(False, 100)
>>> # Valid uniform for 3D (k=6)
>>> chol = np.random.rand(6)
>>> is_uniform, n = validate_cholesky_shape(chol, ndim=3)
>>> is_uniform, n
(True, 0)
>>> # Invalid shape raises
>>> chol = np.random.rand(100, 5)  # Wrong k for 2D
>>> validate_cholesky_shape(chol, ndim=2)
Traceback (most recent call last):
    ...
ValueError: Cholesky factors have wrong packed size...
luxar.gsplats.utils.trils.permute_cholesky_packed(packed: ndarray, d: int, perm: Sequence[int]) → ndarray[source]

Permute dimensions of packed Cholesky factors.

Given packed Cholesky factors L where Sigma = L @ L^T, reorder the dimensions according to the permutation. The new Cholesky L’ satisfies Sigma’[i,j] = Sigma[perm[i], perm[j]].

Parameters:
  • packed (np.ndarray, shape (N, k) where k = d*(d+1)//2) – Packed lower-triangular Cholesky factors.

  • d (int) – Number of dimensions.

  • perm (sequence of int) – Permutation of dimension indices. perm[new_i] = old_i. E.g., [2, 0, 1] means new dim 0 was old dim 2.

Returns:

Packed Cholesky factors with permuted dimensions.

Return type:

np.ndarray, shape (N, k)

Examples

>>> # Reverse 2D dimensions: swap X and Y
>>> packed = np.array([[1.0, 0.5, 2.0]])  # L00, L10, L11
>>> permute_cholesky_packed(packed, 2, [1, 0])
luxar.gsplats.utils.trils.embed_cholesky_packed(packed: ndarray, d_src: int, d_dst: int, dim_mapping: List[int], fill_sigma: Dict[int, float] | None = None) → ndarray[source]

Embed lower-dimensional packed Cholesky factors into higher dimensions.

Takes d_src-dimensional Cholesky factors and embeds them into a d_dst-dimensional space (d_dst >= d_src). Mapped dimensions carry over the original covariance; unmapped dimensions get independent Gaussian variance (diagonal only, no cross-terms).

Parameters:
  • packed (np.ndarray, shape (N, k_src) where k_src = d_src*(d_src+1)//2) – Packed Cholesky factors in the source dimensionality.

  • d_src (int) – Source dimensionality.

  • d_dst (int) – Target dimensionality (must be >= d_src).

  • dim_mapping (list of int, length d_src) – Maps source dimension i to target dimension dim_mapping[i]. E.g., [1, 2, 3] maps src dims 0,1,2 to dst dims 1,2,3.

  • fill_sigma (dict of {target_dim_index: sigma_value}, optional) – Standard deviations for unmapped target dimensions. Unmapped dims not in fill_sigma default to 1.0.

Returns:

Packed Cholesky factors in the target dimensionality.

Return type:

np.ndarray, shape (N, k_dst) where k_dst = d_dst*(d_dst+1)//2

Examples

>>> # Embed 2D into 3D: src dims [0,1] → dst dims [0,1], new dim 2 has sigma=0.5
>>> packed_2d = np.array([[1.0, 0.0, 1.0]])  # isotropic 2D
>>> embed_cholesky_packed(packed_2d, 2, 3, [0, 1], fill_sigma={2: 0.5})

Seed Generation

Strategies for generating initial seed points for splat fitting.

Seed generation methods for Gaussian splatting.

This package provides unified seeding for Gaussian splat fitting. All methods return GSplatData with scale-informed Gaussian shapes.

Seeding Methods:

  1. seed_from_decomposition: Scale-hierarchical detection via image decomposition. Each seed’s sigma equals the decomposition scale factor. Best for blob-like features.

  2. seed_from_grid: Uniform grid seeding for spatial coverage. Isotropic Gaussians with user-defined or auto-computed sigma.

  3. seed_from_edges: Edge-based seeding with isotropic shapes. Uses Sobel gradients for edge detection and Poisson disk sampling.

Unified Entry Point:

Use generate_seeds() for a unified interface to all methods. The default method is “auto” which uses fast edges + grid combination (decomposition excluded for speed). Use method=”decomposition,edges,grid” to include all.

Examples

>>> from luxar.gsplats.seeds import generate_seeds
>>>
>>> # Automatic method selection (recommended) - fast edges + grid
>>> seeds = generate_seeds(image)
>>>
>>> # Single method
>>> seeds = generate_seeds(image, method="decomposition")
>>>
>>> # Use with fitter
>>> from luxar.gsplats import fit_gaussian_splats
>>> result = fit_gaussian_splats(image, seeds=seeds)
luxar.gsplats.seeds.generate_seeds(V: ndarray, method: str = 'auto', **kwargs: Any) → GSplatData[source]

Generate seed Gaussian splats using specified method(s).

All seeding methods return GSplatData with scale-informed Gaussian shapes, allowing the fitter to use full geometry (centers, sigmas, amplitudes).

Parameters:
  • V (np.ndarray) – Input n-dimensional image/volume to analyze.

  • method (str, default "auto") – Seed generation method(s) to use. Options: - “auto”: Fast edges + grid combination (default, recommended) - “decomposition”: Multi-scale decomposition for blob-like features (slow) - “grid”: Uniform grid for spatial coverage - “edges”: Edge-based boundary detection with Sobel gradients - “peaks”: Local maxima after Gaussian blur (ideal for sparse residuals) - “decomposition,edges,grid”: Include all methods (comma-separated)

  • **kwargs –

    Method-specific parameters. Common parameters are routed to all applicable methods, while method-specific parameters are routed only to their respective methods.

    Common Parameters (apply to multiple methods):

    min_distancefloat, default=2.0

    Minimum Euclidean distance (in voxels) between seed centers. Used for deduplication when combining multiple methods.

    target_seedsint or None, optional

    Target number of seeds for “auto” mode. If None, auto-estimated.

    devicestr or None, optional

    PyTorch device for GPU acceleration. Options: - None (default): CPU using scipy.ndimage - ‘cpu’: Force CPU - ‘cuda’: NVIDIA GPU (if available) - ‘mps’: Apple Metal (if available) - ‘auto’: Auto-detect best device

    GPU acceleration provides substantial speedup for large volumes (>100³) — often orders of magnitude depending on GPU and problem size. Applied to all selected seeding methods.

    Decomposition Parameters (method=”decomposition”):

    scaleslist[int], default=[1, 2, 4, 8, 16, 32, 64]

    Scale factors for decomposition.

    ignore_finest_kint, default=1

    Number of finest scales to ignore for peak detection.

    threshold_relfloat, default=0.1

    Relative threshold for peak detection (0.0-1.0).

    peaks_per_scaleint or None, optional

    Maximum number of peaks per scale.

    decompose_kwargsdict, optional

    Additional kwargs passed to decompose_image().

    verbosebool, default=False

    Print progress information.

    Grid Parameters (method=”grid”):

    spacingfloat or Sequence[float] or None, optional

    Grid spacing in voxels. None = auto (~5% of smallest dimension).

    jitterfloat, default=0.0

    Jitter fraction (0.0-0.5) for random offset.

    sigmafloat or None, optional

    Gaussian sigma. None = spacing / 2.

    exclude_belowfloat or None, optional

    Absolute intensity threshold.

    exclude_below_percentilefloat or None, optional

    Percentile intensity threshold (0-100).

    Edge Parameters (method=”edges”):

    n_seedsint or None, optional

    Target number of edge seeds.

    edge_threshold_relfloat, default=0.1

    Relative edge threshold.

Returns:

Gaussian splat seeds with: - centers: Peak/centroid positions - amplitudes: Peak intensities - cholesky_factors: Scale-informed Cholesky factors - Standard Gaussian profile (no sharpness parameter)

Return type:

GSplatData

Examples

Basic usage with default settings (auto combination):

>>> from luxar.gsplats.seeds import generate_seeds
>>> seeds = generate_seeds(image)  # Returns GSplatData
>>> print(f"Generated {len(seeds.centers)} seed splats")

Using decomposition method:

>>> seeds = generate_seeds(image, method="decomposition")

Using grid method:

>>> seeds = generate_seeds(image, method="grid", spacing=10.0)

Combining methods:

>>> seeds = generate_seeds(image, method="decomposition,grid")

Use with fit_gaussian_splats:

>>> from luxar.gsplats import fit_gaussian_splats
>>> seeds = generate_seeds(image, method="auto")
>>> result = fit_gaussian_splats(image, seeds=seeds)

Notes

  • Default method is “auto” (fast edges + grid combination)

  • “decomposition” is best for blob-like features but slow

  • “grid” provides uniform spatial coverage

  • “edges” captures boundaries with isotropic shapes (orientation learned during fitting)

  • Use “decomposition,edges,grid” to include all methods

  • The fitter will refine all parameters during optimization

luxar.gsplats.seeds.seed_from_decomposition(V: ndarray, scales: List[int] | None = None, ignore_finest_k: int = 1, peaks_per_scale: int | None = None, min_distance: float = 2.0, threshold_rel: float = 0.1, decompose_kwargs: Dict[str, Any] | None = None, verbose: bool = False, device: str | None = None) → GSplatData[source]

Generate seed Gaussian splats using multi-scale decomposition.

This method decomposes the input image into multiple scales using decompose_image(), finds local maxima in each scale (excluding the finest k scales), and returns GSplatData with isotropic Gaussians where sigma = scale_factor.

Parameters:
  • V (np.ndarray) – Input n-dimensional image/volume. Shape: (s_0, s_1, …, s_{n-1}).

  • scales (List[int], default [1, 2, 4, 8, 16, 32, 64]) – Scale factors for decomposition. Scale 1 = full resolution, scale 2 = half resolution, etc. Each seed’s sigma is set to the scale at which it was detected.

  • ignore_finest_k (int, default 1) – Number of finest scales to ignore for peak detection. Setting k=1 ignores the full-resolution scale to suppress noise.

  • peaks_per_scale (int or None, optional) – Maximum number of peaks to extract per scale. If None, extract all.

  • min_distance (float, default 2.0) – Minimum Euclidean distance between seeds (in voxels).

  • threshold_rel (float, default 0.1) – Relative threshold for peak detection (0.0 to 1.0).

  • decompose_kwargs (dict or None, optional) – Additional keyword arguments passed to decompose_image().

  • verbose (bool, default False) – Print progress information.

  • device (str, optional) –

    PyTorch device for GPU acceleration. Options: - None (default): CPU using scipy.ndimage - ‘cpu’: Force CPU - ‘cuda’: NVIDIA GPU (if available) - ‘mps’: Apple Metal (if available) - ‘auto’: Auto-detect best device

    Forwarded to decompose_image(); GPU acceleration provides substantial speedup for the decomposition on large volumes (>100³), with the magnitude depending on GPU and problem size. Peak detection itself always runs on the CPU.

Returns:

Gaussian splat seeds with: - centers: Peak positions in full resolution coordinates - amplitudes: Peak intensities - cholesky_factors: Isotropic Cholesky factors where sigma = scale_factor - Standard Gaussian profile (no sharpness parameter)

Return type:

GSplatData

Notes

The sigma for each seed equals the decomposition scale_factor at which it was detected. Features at scale=4 will have sigma=4 voxels.

luxar.gsplats.seeds.seed_from_edges(V: ndarray, n_seeds: int | None = None, min_distance: float = 2.0, edge_threshold_rel: float = 0.1, device: str | None = None) → GSplatData[source]

Generate seed Gaussian splats along edges with isotropic shapes.

This method detects edges using nD Sobel gradients and samples points along edges using weighted Poisson disk sampling. Seeds are initialized with isotropic Gaussians (σ=1.0).

Parameters:
  • V (np.ndarray) – Input n-dimensional image/volume. Shape: (s_0, s_1, …, s_{n-1}).

  • n_seeds (int or None, optional) – Target number of seeds. If None, auto-estimates based on image size.

  • min_distance (float, default 2.0) – Minimum distance between seeds in voxels.

  • edge_threshold_rel (float, default 0.1) – Relative edge threshold (0.0-1.0). Fraction of max edge response.

  • device (str, optional) –

    PyTorch device for GPU acceleration. Options: - None (default): CPU using scipy.ndimage - ‘cpu’: Force CPU - ‘cuda’: NVIDIA GPU (if available) - ‘mps’: Apple Metal (if available) - ‘auto’: Auto-detect best device

    GPU acceleration provides substantial speedup for large volumes (>100³) — often orders of magnitude depending on GPU and problem size. Small volumes (<50³) automatically use CPU due to overhead.

Returns:

Gaussian splat seeds with: - centers: Edge point positions - amplitudes: Intensity values at each point - cholesky_factors: Isotropic Cholesky factors (σ=1.0) - Standard Gaussian profile (no sharpness parameter)

Return type:

GSplatData

Notes

  • Seeds are placed along edges (high gradient magnitude)

  • Gaussian shapes are isotropic (σ=1.0) for all seeds

  • The optimizer will adjust shapes during fitting

  • Previous versions used structure tensor for anisotropic initialization, but empirical testing showed no benefit in practice

Examples

>>> from luxar.gsplats.seeds import generate_seeds
>>> import numpy as np
>>>
>>> # Create test image with edges
>>> image = np.zeros((100, 100))
>>> image[40:60, 40:60] = 1.0  # Square
>>>
>>> # Edge-based seeding (CPU)
>>> seeds = generate_seeds(image, method="edges")
>>>
>>> # Edge-based seeding with GPU acceleration
>>> seeds = generate_seeds(image, method="edges", device="cuda")
>>>
>>> # Custom parameters
>>> seeds = generate_seeds(
...     image, method="edges",
...     edge_threshold_rel=0.2,  # Higher threshold
...     n_seeds=1000,            # Target seed count
...     device="auto",           # Auto-detect GPU
... )
luxar.gsplats.seeds.seed_from_grid(V: ndarray, spacing: float | Sequence[float] | None = None, jitter: float = 0.0, sigma: float | None = None, exclude_below: float | None = None, exclude_below_percentile: float | None = None, device: str | None = None) → GSplatData[source]

Generate seed Gaussian splats on a uniform grid.

This method creates seeds at regular grid positions throughout the image. It provides uniform spatial coverage, useful as a baseline or for filling gaps left by other seeding methods.

Parameters:
  • V (np.ndarray) – Input n-dimensional image/volume. Shape: (s_0, s_1, …, s_{n-1}).

  • spacing (float or Sequence[float] or None, optional) –

    Grid spacing in voxels. Can be:

    • float: Same spacing for all dimensions

    • Sequence[float]: Per-dimension spacing

    • None: Auto-compute with aspect-ratio-aware spacing (default). Spacing is proportional to each dimension’s size, respecting anisotropy. Example: 1000x1000x10 image gives [136, 136, 1.4] spacing (not [29, 29, 29])

  • jitter (float, default 0.0) – Jitter fraction (0.0 to 0.5). Random offset applied to each grid point as a fraction of spacing. 0.0 = no jitter, 0.5 = up to half spacing.

  • sigma (float or None, optional) – Gaussian sigma (standard deviation) for all seeds. If None, defaults to mean(spacing) / 2 (ensures ~60% overlap at midpoints between adjacent grid points).

  • exclude_below (float or None, optional) – Absolute intensity threshold. Grid points where V < threshold are excluded. Mutually exclusive with exclude_below_percentile.

  • exclude_below_percentile (float or None, optional) – Percentile threshold (0-100). Grid points below this percentile of V are excluded. Mutually exclusive with exclude_below.

  • device (str, optional) –

    PyTorch device for GPU acceleration. Options: - None (default): CPU using scipy.ndimage - ‘cpu’: Force CPU - ‘cuda’: NVIDIA GPU (if available) - ‘mps’: Apple Metal (if available) - ‘auto’: Auto-detect best device

    GPU acceleration provides substantial speedup for amplitude interpolation on large volumes (>100³); the magnitude depends on GPU and problem size.

Returns:

Gaussian splat seeds with: - centers: Grid point positions (possibly jittered) - amplitudes: Intensity values at each grid point - cholesky_factors: Isotropic Cholesky factors (sigma * I) - Standard Gaussian profile (no sharpness parameter)

Return type:

GSplatData

Notes

  • Grid seeding provides uniform spatial coverage

  • Default spacing is aspect-ratio-aware: respects image anisotropy (e.g., thin Z slices in microscopy get denser Z spacing)

  • Jitter helps avoid aliasing artifacts

  • Intensity filtering removes seeds in low-signal regions

  • This method is fast and produces many seeds; combine with other methods using the “auto” mode in generate_seeds()

Examples

>>> from luxar.gsplats.seeds import generate_seeds
>>> import numpy as np
>>>
>>> # Create test image
>>> image = np.random.rand(100, 100) + 0.5
>>>
>>> # Basic grid seeding (CPU)
>>> seeds = generate_seeds(image, method="grid")
>>>
>>> # Grid seeding with GPU acceleration
>>> seeds = generate_seeds(image, method="grid", device="cuda")
>>>
>>> # Custom spacing with jitter
>>> seeds = generate_seeds(image, method="grid", spacing=10.0, jitter=0.25)
>>>
>>> # Exclude low-intensity regions
>>> seeds = generate_seeds(image, method="grid", exclude_below_percentile=25.0)
luxar.gsplats.seeds.seed_from_peaks(V: ndarray, n_seeds: int | None = None, init_sigma: float | None = None, device: str | None = None) → GSplatData[source]

Generate seeds at non-zero voxels, weighted by intensity.

Samples n_seeds locations from the non-zero voxels of V, with probability proportional to voxel intensity. Brighter voxels are more likely to receive a seed. Unless overridden, init_sigma is auto-scaled to roughly half the expected inter-seed spacing (with a floor of 1.5 voxels) so that splats have enough support to generate useful gradients without massively overlapping.

This method is ideal for sparse residuals in progressive fitting where the signal has varying shape (peaks, plateaus, edges) and peak-detection would miss non-extremal structures.

Parameters:
  • V (np.ndarray) – Input volume (any dimensionality). Zero voxels are ignored.

  • n_seeds (int, optional) – Number of seeds to generate. If None or larger than the number of non-zero voxels, returns one seed per non-zero voxel.

  • init_sigma (float, optional) – Initial Gaussian sigma for seed splats. If None, auto-scaled based on expected inter-seed spacing: (non_zero_voxels / n_seeds)^(1/d) / 2. This ensures splats are large enough for gradients but don’t massively overlap and overshoot.

  • device (str, optional) – PyTorch device (‘cuda’, ‘mps’, ‘cpu’, or None for auto).

Returns:

Seeds with centers at sampled non-zero voxels, amplitudes from V, and isotropic Cholesky factors at init_sigma.

Return type:

GSplatData

CLAHE Enhancement

Contrast-Limited Adaptive Histogram Equalization for nD data.

CLAHE (Contrast Limited Adaptive Histogram Equalization) for nD volumes.

This subpackage provides a PyTorch-based implementation of CLAHE that works on arbitrary-dimensional tensors. CLAHE is particularly useful for: - Enhancing local contrast in images/volumes with varying background - Preprocessing for feature detection in heterogeneous data - Creating perceptually-balanced sampling distributions

Key Features: - nD support: works on 1D, 2D, 3D, and higher-dimensional tensors - Contrast limiting: prevents noise amplification in uniform regions - Tile-based processing: adapts to local intensity distributions - PyTorch native: GPU-accelerated, differentiable operations

Example

>>> import torch
>>> from luxar.gsplats.clahe import apply_clahe
>>>
>>> # 2D image with varying background
>>> image = torch.randn(256, 256)
>>>
>>> # Apply CLAHE with default parameters
>>> enhanced = apply_clahe(image, tile_size=16, clip_limit=2.0)
>>>
>>> # Result has locally-equalized contrast
>>> assert enhanced.shape == image.shape
luxar.gsplats.clahe.apply_clahe(V: torch.Tensor, tile_size: int = 16, clip_limit: float = 2.0, nbins: int = 256) → torch.Tensor[source]

Apply CLAHE (Contrast Limited Adaptive Histogram Equalization) to nD volume.

CLAHE [CLAHE1994] enhances local contrast by performing histogram equalization on small tiles, then applying contrast limiting to prevent noise amplification.

Algorithm:

  1. Divide volume into non-overlapping tiles of size tile_size^d

  2. For each tile:

    1. Compute local histogram (nbins bins)

    2. Apply contrast limiting (clip histogram peaks)

    3. Compute CDF mapping (local histogram equalization)

    4. Transform tile intensities

  3. Result: Volume with locally-equalized contrast

Parameters:
  • V (torch.Tensor) – Input tensor of any dimensionality (1D, 2D, 3D, nD)

  • tile_size (int, default 16) – Size of tiles in voxels. Tiles are tile_size^d hypercubes. - Too small (< 8): Overfits to noise, over-amplifies uniform regions - Too large (> 32): Loses local adaptation, approaches global equalization - Recommended: ~2× typical feature diameter

  • clip_limit (float, default 2.0) – Contrast limiting factor (range: 1.0-4.0) - Low (1.0-2.0): Conservative, closer to original distribution, less noise - High (3.0-4.0): Aggressive equalization, more noise amplification - Formula: max_histogram_height = clip_limit × (n_pixels_per_tile / nbins)

  • nbins (int, default 256) – Number of histogram bins for equalization - Too few (< 64): Coarse equalization, loses detail - Too many (> 512): Computational cost, no benefit - Standard: 256 for 8-16 bit images

Returns:

CLAHE-equalized volume with same shape and device as input. Intensity range preserved (same min/max as input).

Return type:

torch.Tensor

Examples

>>> import torch
>>> from luxar.gsplats.clahe import apply_clahe
>>>
>>> # 2D image with heterogeneous background
>>> image = torch.randn(256, 256)
>>> enhanced = apply_clahe(image, tile_size=16, clip_limit=2.0)
>>>
>>> # 3D volume
>>> volume = torch.randn(128, 128, 128)
>>> enhanced_3d = apply_clahe(volume, tile_size=16, clip_limit=2.0)
>>>
>>> # Higher dimensions
>>> data_4d = torch.randn(64, 64, 64, 64)
>>> enhanced_4d = apply_clahe(data_4d, tile_size=8, clip_limit=1.5)

Notes

  • Output preserves input dtype and device

  • Tiles at boundaries may be smaller than tile_size

  • No interpolation between tiles (for speed and simplicity)

  • For sampling applications, discontinuities are acceptable

  • For visualization, consider adding bilinear/trilinear interpolation

References

[CLAHE1994]

Zuiderveld, K. (1994). “Contrast Limited Adaptive Histogram Equalization.” Graphics Gems IV, Academic Press.

Batch Fitting

Scheduler-agnostic batch orchestration for fitting a whole nD dataset across its axes — locally across multiple GPUs (batch-fit run) or on a Slurm cluster (batch-fit submit).

HPC batch fitting orchestration for large OME-Zarr datasets.

Provides Slurm array job generation, environment capture, time estimation, status tracking, and post-batch merge orchestration.

Culling

Gaussian splat culling strategies for reducing splat count while preserving quality.

Contribution-based Gaussian splat culling.

This module provides principled removal of splats that contribute negligibly to the reconstruction, going beyond simple amplitude thresholds by evaluating each splat’s actual impact on the rendered volume.

Two modes are available

Error-budget mode (target provided):

Uses the original volume that was fitted. Computes the residual R = target - V_pred and derives an error budget tau from the existing reconstruction error. For each splat, measures the maximum error increase from removal: max(0, |R+g_j| - |R|). A splat is safe to remove when this increase is below tau. This formulation is robust to pre-existing high-error voxels — a splat near a noisy region can still be culled if it contributes negligibly. This is the most principled mode, but requires the target volume.

Redundancy mode (no target):

Works from the splats alone — no target volume needed. For each splat, measures the maximum fractional contribution: g_j(x) / V_pred(x) within the splat’s support. If a splat never contributes more than a small fraction of the total signal at any point, it is redundant — other splats already cover its region. A redundancy_threshold (e.g. 0.01) means “remove splats that contribute less than 1% of the local signal everywhere.”

Both modes include a joint compounding check that verifies the joint removal of all candidates does not exceed the budget. If it does, a binary search tightens the per-splat threshold until the joint constraint holds.

Why two modes?

Error-budget mode is strictly more powerful: it accounts for the actual fitting error and can detect splats in regions where the reconstruction is already poor (removing them doesn’t make things worse). Redundancy mode cannot make this distinction because it has no reference to compare against.

However, the target volume is often unavailable — splats may have been pre-computed, transferred, or the original data discarded. Redundancy mode provides a useful fallback that still captures the key idea: spatially redundant splats can be removed without degrading the reconstruction.

class luxar.gsplats.culling.CullResult(keep_mask: ndarray, n_culled: int, error_budget: float, phase1_candidates: int, phase2_iterations: int, max_joint_error: float, mode: str = 'error_budget')[source]

Result of contribution-based culling.

keep_mask

Boolean mask — True for splats to keep.

Type:

np.ndarray, shape (N,)

n_culled

Number of splats removed.

Type:

int

error_budget

The error budget tau used for the final decision. In error-budget mode this is percentile(|R|) * tolerance; in redundancy mode it equals the redundancy_threshold.

Type:

float

phase1_candidates

Number of individually safe candidates before the joint compounding check.

Type:

int

phase2_iterations

Number of binary-search iterations in the joint compounding check.

Type:

int

max_joint_error

The max |R_joint| (error-budget) or max fractional contribution (redundancy) after removing the final set of culled splats.

Type:

float

mode

"error_budget" or "redundancy" — which mode was used.

Type:

str

__init__(keep_mask: ndarray, n_culled: int, error_budget: float, phase1_candidates: int, phase2_iterations: int, max_joint_error: float, mode: str = 'error_budget') → None
luxar.gsplats.culling.compute_per_splat_deletion_error(centers: torch.Tensor, Ls: torch.Tensor, amps: torch.Tensor, residual: torch.Tensor, shape: Sequence[int], truncate: float = 2.75, intensity_floor: float = 1e-05, chunk_size: int | None = None) → torch.Tensor[source]

Compute per-splat maximum deletion error (error-budget mode).

Convenience wrapper around _compute_per_splat_error() with fractional=False. See that function for details.

luxar.gsplats.culling.cull_by_contribution(centers: torch.Tensor, Ls: torch.Tensor, amps: torch.Tensor, target: torch.Tensor | None, shape: Sequence[int], truncate: float = 2.75, error_percentile: float = 99.0, error_tolerance: float = 1.0, redundancy_threshold: float = 0.01, max_binary_search_iters: int = 8, intensity_floor: float = 1e-05, chunk_size: int | None = None, verbose: bool = False) → CullResult[source]

Cull splats that contribute negligibly to the reconstruction.

This function supports two modes, selected automatically based on whether a target volume is provided:

Error-budget mode (target is not None)

Computes the residual R = target - V_pred and establishes an error budget tau = percentile(|R|, error_percentile) * error_tolerance. A splat is safe to remove when the worst-case error increase max_x max(0, |R(x)+g_j(x)| - |R(x)|) <= tau — i.e., removing it does not degrade any voxel’s error by more than the budget. This formulation is robust to pre-existing high-error voxels: a splat in a noisy region can still be culled if it contributes negligibly to the reconstruction there. This is the most principled approach: it uses the actual reconstruction quality to set the threshold, and it can detect splats in high-error regions where removal is harmless.

Redundancy mode (target is None)

Works from the splats alone — no target volume needed. Renders the full reconstruction V_pred = sum g_i and measures each splat’s maximum fractional contribution: max_x  g_j(x) / V_pred(x). A splat is safe to remove when its fractional contribution is everywhere below redundancy_threshold — other splats already cover its region. This mode is useful when the original volume is unavailable (e.g., pre-computed splat datasets), but it cannot account for fitting error and may be slightly more conservative.

Both modes include a joint compounding check: after identifying individual candidates, the function verifies that their joint removal does not exceed the budget. If it does (because overlapping candidates compound), the threshold is tightened via binary search.

Parameters:
  • centers (torch.Tensor, shape (N, d)) – Splat center positions (GPU tensor).

  • Ls (torch.Tensor, shape (N, d, d)) – Lower-triangular Cholesky factors (GPU tensor).

  • amps (torch.Tensor, shape (N,)) – Splat amplitudes (GPU tensor).

  • target (torch.Tensor or None) – Target volume to compare against. If provided, error-budget mode is used. If None, redundancy mode is used.

  • shape (Sequence[int]) – Volume shape for rendering.

  • truncate (float) – Truncation radius in standard deviations.

  • error_percentile (float) – Error-budget mode only. Percentile of |residual| used to set the budget (0–100). Higher = more conservative.

  • error_tolerance (float) – Error-budget mode only. Multiplier on the budget.

  • redundancy_threshold (float) – Redundancy mode only. Maximum fractional contribution below which a splat is considered redundant (0–1). E.g. 0.01 means “remove splats contributing < 1% of the local signal everywhere.”

  • max_binary_search_iters (int) – Maximum binary-search iterations for the joint compounding check.

  • intensity_floor (float) – Minimum intensity threshold for AABB computation.

  • chunk_size (int, optional) – Chunk size for memory management.

  • verbose (bool) – Print progress information via arbol.

Returns:

Culling result with keep_mask, diagnostics, and metadata.

Return type:

CullResult

Examples

Error-budget mode (target available):

>>> result = cull_by_contribution(centers, Ls, amps, target, shape)

Redundancy mode (no target):

>>> result = cull_by_contribution(
...     centers, Ls, amps, None, shape,
...     redundancy_threshold=0.02,
... )

Quality Metrics

PSNR, SSIM, and MSE metrics for evaluating reconstruction quality.

Quality metrics for Gaussian splat reconstructions.

All functions operate on PyTorch tensors and stay on the input device, avoiding unnecessary GPU-CPU transfers. Only scalar results are moved to CPU (via .item()).

luxar.gsplats.metrics.compute_ssim(pred: torch.Tensor, target: torch.Tensor, window_size: int = 11, data_range: float | None = None) → float[source]

Compute Structural Similarity Index (SSIM) on the input device.

For 2-D and 3-D tensors a true n-D SSIM is computed via F.conv{2,3}d. For higher-dimensional tensors the SSIM is averaged over all 3-D sub-volumes along the leading dimensions.

Large volumes are automatically split into overlapping tiles to avoid GPU out-of-memory errors. The tiling threshold is based on estimated peak memory vs. available GPU memory.

Parameters:
  • pred (torch.Tensor) – Predicted and reference tensors (same shape, >= 2-D).

  • target (torch.Tensor) – Predicted and reference tensors (same shape, >= 2-D).

  • window_size (int) – Side length of the Gaussian weighting window (must be odd).

  • data_range (float, optional) – Dynamic range of the data. If None, computed as target.max() - target.min().

Returns:

Mean SSIM in [−1, 1] (typically [0, 1] for non-negative data).

Return type:

float

luxar.gsplats.metrics.compute_psnr(pred: torch.Tensor, target: torch.Tensor, data_range: float | None = None) → float[source]

Compute Peak Signal-to-Noise Ratio in dB.

PSNR = 10 * log10(data_range² / MSE).

Parameters:
  • pred (torch.Tensor) – Same shape.

  • target (torch.Tensor) – Same shape.

  • data_range (float, optional) – If None, uses target.max() - target.min().

Returns:

PSNR in dB. Returns float('inf') when MSE is zero.

Return type:

float

luxar.gsplats.metrics.otsu_threshold(target: torch.Tensor, bins: int = 256) → float[source]

Otsu’s between-class-variance threshold, on the input device.

Reimplemented here rather than delegating to skimage.filters because scikit-image lives in the demos extra. Calibration now shares this dependency-free implementation, so the definition of foreground does not depend on which extras happened to be installed.

Follows scikit-image’s formulation exactly (cumulative class weights and means over histogram bin centres, threshold taken at the argmax of the between-class variance) so the two are numerically interchangeable.

Returns target.min() for a constant volume, which selects nothing under the strict > that compute_foreground_psnr() applies.

luxar.gsplats.metrics.compute_foreground_psnr(pred: torch.Tensor, target: torch.Tensor, data_range: float | None = None, threshold: float | None = None) → Tuple[float, float, float][source]

PSNR restricted to foreground voxels of target.

Global PSNR is dominated by background on sparse volumes – a light-sheet stack that is 97.8% empty scores well for reconstructing the emptiness – so the foreground number is the one that says whether the signal survived the fit. Reported alongside, never instead of, the global figure.

The error is averaged over foreground voxels only, but data_range is taken from the whole target, matching luxar.gsplats.calibration.metrics.held_out_psnr_foreground() so the two are comparable. Using the foreground’s own range instead would shrink the reference and silently inflate the result.

Parameters:
  • pred (torch.Tensor) – Same shape. Foreground is defined on target, never on pred: a fit that hallucinates signal must be scored against where the signal actually is.

  • target (torch.Tensor) – Same shape. Foreground is defined on target, never on pred: a fit that hallucinates signal must be scored against where the signal actually is.

  • data_range (float, optional) – Defaults to target.max() - target.min() over the whole volume.

  • threshold (float, optional) – Foreground is target > threshold. Defaults to Otsu.

Returns:

fraction is the share of voxels counted as foreground – report it, because a PSNR over 0.01% of the volume means something very different from one over 40%. psnr_db is nan when the foreground is empty.

Return type:

(psnr_db, threshold, fraction)

luxar.gsplats.metrics.compute_quality_metrics(pred: torch.Tensor, target: torch.Tensor, data_range: float | None = None, ssim_window_size: int = 11) → Dict[str, float][source]

Compute a suite of quality metrics between predicted and target volumes.

All heavy computation stays on the input device; only scalar results are returned.

Parameters:
  • pred (torch.Tensor) – Predicted and reference tensors (same shape, >= 2-D).

  • target (torch.Tensor) – Predicted and reference tensors (same shape, >= 2-D).

  • data_range (float, optional) – Dynamic range. If None, computed from target.

  • ssim_window_size (int) – SSIM window size.

Returns:

Keys: mse, psnr_db, ssim, rel_l2, max_abs_error, foreground_psnr_db, foreground_threshold, foreground_fraction. The foreground trio is the honest score on sparse data – see compute_foreground_psnr().

Return type:

dict

Calibration (Blind-Spot Cross-Validation)

Noise2Self model selection for Gaussian splat fits: sweep splat count K, fit each at against a 5%-donut-median-filled volume, and report the held-out PSNR peak (K*) plus the dataset’s noise-floor PSNR ceiling. Used by the luxar gsplat cal CLI command.

Blind-spot cross-validation for Gaussian-splat model selection.

Implements the manuscript’s calibration protocol (Supp. Doc. 2, splat_count_vs_quality):

  1. Mask 5% of voxels with a deterministic Bernoulli draw (seed=42).

  2. Replace masked voxels with the median of the unmasked voxels in their 3^D donut neighbourhood (centre excluded) — Noise2Self self-supervision.

  3. Fit a Gaussian-splat model on the donut-filled volume at each K in a sweep; the optimiser never sees the original noisy values at masked positions.

  4. Evaluate held-out PSNR against the original (pre-fill) values at the masked positions.

  5. The K that maximises held-out PSNR is the principled splat budget — capacity beyond K* memorises noise rather than signal.

As a free byproduct, an ensemble noise-floor estimator (Laplacian + Haar HH + background MAD) places each dataset in absolute terms.

The protocol is purely additive: fit_gaussian_splats is called unchanged at each K; this package owns mask generation, donut fill, held-out evaluation, K-grid construction, peak detection, and noise-floor estimation.

This module is a package split by phase (masking / metrics / content / noise_floor / curve_analysis / result / driver); it re-exports the full public surface so luxar.gsplats.calibration.X keeps resolving as before.

class luxar.gsplats.calibration.CalibrationResult(k_values_requested: ~typing.List[int], k_values_effective: ~typing.List[int], held_out_psnr_db: ~typing.List[float], train_psnr_db: ~typing.List[float], held_out_mse: ~typing.List[float], full_psnr_db: ~typing.List[float], full_ssim: ~typing.List[float], held_out_peak: ~luxar.gsplats.calibration.curve_analysis.HeldOutPeak, noise_floor: ~luxar.gsplats.calibration.noise_floor.NoiseFloor, fit_times_seconds: ~typing.List[float], splat_paths: ~typing.List[str] | None, mask_seed: int, mask_fraction: float, donut_radius: int, fit_config: ~typing.Dict[str, ~typing.Any], volume_shape: ~typing.List[int], volume_dtype: str, timestamp: str, held_out_psnr_fg_db: ~typing.List[float] = <factory>, held_out_psnr_fg_weighted_db: ~typing.List[float] = <factory>, foreground_mask_fraction: float = nan, foreground_otsu_threshold: float = nan, fg_bg_ratio: float = 1.0, held_out_gain_db: ~typing.List[float] = <factory>, predict_zero_baseline_mse: float = nan, k_star_metric: str = 'psnr_minmax', held_out_peak_selected: ~luxar.gsplats.calibration.curve_analysis.HeldOutPeak | None = None, calibration_region: ~typing.Dict[str, ~typing.Any] | None = None, original_volume_shape: ~typing.List[int] | None = None, splat_density: ~typing.Dict[str, ~typing.Any] | None = None, rd_model: ~typing.Dict[str, ~typing.Any] | None = None, not_converged: bool = False, exponent_fit: ~typing.Dict[str, ~typing.Any] | None = None)[source]

Bases: object

Output of calibrate(). Serialisable to JSON.

k_values_requested: List[int]

K values passed to the sweep.

k_values_effective: List[int]

Post-cull splat counts actually realised at each K.

held_out_psnr_db: List[float]

PSNR at masked positions, against the original pre-fill values.

train_psnr_db: List[float]

PSNR at unmasked positions, against the original values.

held_out_mse: List[float]

MSE at masked positions, against the original values.

full_psnr_db: List[float]

PSNR over the whole volume against the original — for cross-run comparison.

full_ssim: List[float]

SSIM over the whole volume against the original.

held_out_peak: HeldOutPeak

The recommended K* and curve type.

noise_floor: NoiseFloor

Ensemble noise-floor estimate for the input volume.

fit_times_seconds: List[float]

Wall-clock time per fit, in seconds.

splat_paths: List[str] | None

Per-K .gsplats.zarr paths when --keep-fits is set; else None.

mask_seed: int
mask_fraction: float
donut_radius: int
fit_config: Dict[str, Any]

Fit kwargs that were applied (sans the per-K seeds value).

volume_shape: List[int]
volume_dtype: str
timestamp: str
held_out_psnr_fg_db: List[float]

Foreground-restricted held-out PSNR (background-domination removed).

held_out_psnr_fg_weighted_db: List[float]

Held-out PSNR with controlled foreground/background total weight.

foreground_mask_fraction: float = nan

Fraction selected by the smoothed Otsu foreground mask.

__init__(k_values_requested: ~typing.List[int], k_values_effective: ~typing.List[int], held_out_psnr_db: ~typing.List[float], train_psnr_db: ~typing.List[float], held_out_mse: ~typing.List[float], full_psnr_db: ~typing.List[float], full_ssim: ~typing.List[float], held_out_peak: ~luxar.gsplats.calibration.curve_analysis.HeldOutPeak, noise_floor: ~luxar.gsplats.calibration.noise_floor.NoiseFloor, fit_times_seconds: ~typing.List[float], splat_paths: ~typing.List[str] | None, mask_seed: int, mask_fraction: float, donut_radius: int, fit_config: ~typing.Dict[str, ~typing.Any], volume_shape: ~typing.List[int], volume_dtype: str, timestamp: str, held_out_psnr_fg_db: ~typing.List[float] = <factory>, held_out_psnr_fg_weighted_db: ~typing.List[float] = <factory>, foreground_mask_fraction: float = nan, foreground_otsu_threshold: float = nan, fg_bg_ratio: float = 1.0, held_out_gain_db: ~typing.List[float] = <factory>, predict_zero_baseline_mse: float = nan, k_star_metric: str = 'psnr_minmax', held_out_peak_selected: ~luxar.gsplats.calibration.curve_analysis.HeldOutPeak | None = None, calibration_region: ~typing.Dict[str, ~typing.Any] | None = None, original_volume_shape: ~typing.List[int] | None = None, splat_density: ~typing.Dict[str, ~typing.Any] | None = None, rd_model: ~typing.Dict[str, ~typing.Any] | None = None, not_converged: bool = False, exponent_fit: ~typing.Dict[str, ~typing.Any] | None = None) → None
foreground_otsu_threshold: float = nan

Otsu cut on the lightly smoothed, floor-subtracted calibration volume.

fg_bg_ratio: float = 1.0

background total-weight ratio for the weighted metric.

Type:

Foreground

held_out_gain_db: List[float]

dB the fit beats the predict-zero baseline (plateaus meaningfully).

predict_zero_baseline_mse: float = nan

MSE of the trivial all-zeros reconstruction at masked voxels.

k_star_metric: str = 'psnr_minmax'

Metric used for held_out_peak_selected.

held_out_peak_selected: HeldOutPeak | None = None

K* under k_star_metric.

None when the metric is the default psnr_minmax, or when the selected curve was undefined and K* fell back to min-max PSNR.

calibration_region: Dict[str, Any] | None = None

Provenance when an auto-selected sub-region was calibrated (else None).

original_volume_shape: List[int] | None = None

Shape of the full input before any region crop (disambiguates cropped PSNR).

splat_density: Dict[str, Any] | None = None

Transferable SplatDensity (as dict) for the planner.

rd_model: Dict[str, Any] | None = None

Parametric RDModel (as dict) of held-out error vs K.

not_converged: bool = False

True when the held-out curve was still climbing at K_max (RD model).

exponent_fit: Dict[str, Any] | None = None

Multi-scale ExponentFit (as dict) when cal --fit-exponent ran; its alpha is also written into splat_density.saturation_exponent.

to_json(path: Path) → None[source]

Serialise to JSON. Non-finite floats become null.

classmethod from_json(path: Path) → CalibrationResult[source]

Load from JSON. null floats become nan.

class luxar.gsplats.calibration.ExponentFit(alpha: float, intercept: float, r_squared: float, n_points: int, n_distinct: int, scales: List[int], n_features: List[int], k_star: List[int])[source]

Bases: object

Multi-scale fit of the saturation exponent alpha in K ~ features^alpha.

The single-scale calibration assumes the empirical default alpha=0.44; this measures it by calibrating K* at several region scales (each a different feature count) and regressing log K* on log n_features. The slope is alpha; the per-scale (n_features, k_star) points and the fit r_squared are kept for reporting / provenance.

alpha: float

Fitted sub-linear exponent (the regression slope in log-log space).

intercept: float

Log-space intercept log C (so K = exp(intercept)·features^alpha).

__init__(alpha: float, intercept: float, r_squared: float, n_points: int, n_distinct: int, scales: List[int], n_features: List[int], k_star: List[int]) → None
r_squared: float

Goodness-of-fit of the log-log regression (1 = perfect power law). NaN when it cannot be assessed: fewer than 3 distinct feature counts (a line through 2 points is trivially perfect), or zero K* variance (a flat/degenerate fit). A NaN here means “treat the exponent as provisional”.

n_points: int

Total scales measured (before collapsing duplicate feature counts).

n_distinct: int

Distinct feature counts actually regressed (the meaningful sample size). < 3 ⇒ r_squared is NaN (under-determined).

scales: List[int]

Region edge lengths (voxels), one per distinct regressed point.

n_features: List[int]

Feature count of each distinct regressed point (shared-threshold count).

k_star: List[int]

Effective K* at each distinct regressed point.

class luxar.gsplats.calibration.FloorEstimate(level: float, strategy: str)[source]

Bases: object

Resolved floor level and the estimator branch that produced it.

level: float
strategy: str
__init__(level: float, strategy: str) → None
class luxar.gsplats.calibration.HeldOutPeak(k_star: int, type: Literal['peak', 'plateau', 'signal_limited'], confidence_db: float, k_knee: int = 0, knee_idx: int = -1, drop_after_peak_db: float = 0.0, tail_rise_db: float = 0.0, plateau_spread_db: float = 0.0, total_rise_db: float = 0.0, still_climbing: bool = False, knee_margin_db: float = 0.3)[source]

Bases: object

Detected K* and qualitative shape of the held-out PSNR curve.

k_star: int

The recommended splat count.

type: Literal['peak', 'plateau', 'signal_limited']

peak — clear interior maximum; plateau — flat top, smallest K within 0.3 dB returned; signal_limited — monotone-rising through the largest tested K (no peak in sampled range).

confidence_db: float

margin to the second-best K in dB. For plateau: spread across the in-tolerance plateau. For signal_limited: total dB rise across the sweep.

Type:

For peak

k_knee: int = 0

The point of diminishing returns, independent of the budget anchor k_star and of the regime label: the interior argmax for a clear peak, otherwise the smallest K within knee_margin_db of the maximum. For peak and plateau this equals k_star; for signal_limited it is the (earlier) knee while k_star remains the last/max K used for the splat budget. Defaults to 0; callers that predate this field should fall back to k_star (from_json does this).

knee_idx: int = -1

Positional index of k_knee in the input k_values (−1 if unset).

drop_after_peak_db: float = 0.0

Held-out PSNR at the last K minus the peak (≤ 0; its magnitude is the post-peak overfitting drop for a peak curve).

tail_rise_db: float = 0.0

Mean per-step held-out rise over the trailing run of adjacent finite K (the still-climbing discriminator; ≥ 0.1 dB drives signal_limited).

plateau_spread_db: float = 0.0

Peak minus the smallest held-out value among K within knee_margin_db of the maximum (how flat the in-tolerance top is).

total_rise_db: float = 0.0

Peak minus the first finite held-out value (total climb across the sweep).

still_climbing: bool = False

argmax at the last K, total rise across the sweep ≥ knee_margin_db (0.3 dB), the trailing tail still rising ≥ 0.1 dB/step on average, and the final step ≥ 0.05 dB. k_star is then the last K (budget anchor); k_knee may still be an earlier K when one is already within knee_margin_db of the maximum, and equals the last K only when no earlier K is that close.

Type:

True when the curve is signal-limited

knee_margin_db: float = 0.3

The dB tolerance used to locate the knee / plateau onset.

__init__(k_star: int, type: Literal['peak', 'plateau', 'signal_limited'], confidence_db: float, k_knee: int = 0, knee_idx: int = -1, drop_after_peak_db: float = 0.0, tail_rise_db: float = 0.0, plateau_spread_db: float = 0.0, total_rise_db: float = 0.0, still_climbing: bool = False, knee_margin_db: float = 0.3) → None
class luxar.gsplats.calibration.NoiseFloor(sigma_hat: float, sigma_laplacian: float, sigma_haar: float, sigma_background: float, psnr_max_db: float)[source]

Bases: object

Ensemble noise-floor estimate for a [0, 1]-normalised volume.

All sigma_* fields are noise standard deviations in the volume’s intensity units. psnr_max_db is the corresponding PSNR ceiling assuming a data_range = 1.0.

sigma_hat: float

Ensemble estimate (median of available high-pass estimators).

sigma_laplacian: float

Discrete Laplacian MAD (Immerkaer 1996, kernel-norm = 2D(2D+1)).

sigma_haar: float

Haar HH-subband MAD over slice-pairs (Donoho & Johnstone 1994).

sigma_background: float

MAD of voxels in the bottom 10% intensity percentile.

psnr_max_db: float

-20 log10(sigma_hat) for [0,1] data; +inf when sigma_hat == 0.

__init__(sigma_hat: float, sigma_laplacian: float, sigma_haar: float, sigma_background: float, psnr_max_db: float) → None
class luxar.gsplats.calibration.RDModel(floor: float, a: float, beta: float, rmse: float, n_points: int, converged_fraction: float)[source]

Bases: object

Parametric fit of held-out error vs K: error ≈ floor + a·K^-beta.

Lets us extrapolate the sweep cheaply and, crucially, flag when a curve is still climbing at K_max (converged_fraction < 1) — the cheap detector that would have caught the original signal_limited false alarm without an expensive dense high-K sweep.

__init__(floor: float, a: float, beta: float, rmse: float, n_points: int, converged_fraction: float) → None
floor: float
a: float
beta: float
rmse: float
n_points: int
converged_fraction: float

Fraction of the achievable error drop realised by K_max (1 = converged).

predict_error(k: float) → float[source]
k_for_error(target_error: float) → float[source]

Invert the model: smallest K reaching target_error (inf if < floor).

class luxar.gsplats.calibration.RegionSelection(origin: List[int], size: List[int], strategy: str, n_features: int, score: float)[source]

Bases: object

Provenance of an auto-selected calibration sub-region.

origin: List[int]

Top-left corner of the crop in the original volume’s coordinates.

size: List[int]

Crop shape actually used (clamped per-axis to the volume).

strategy: str

densest | median | whole.

__init__(origin: List[int], size: List[int], strategy: str, n_features: int, score: float) → None
n_features: int

Feature count inside the chosen crop.

score: float

Feature density (features / voxel) used to rank candidate windows.

class luxar.gsplats.calibration.SplatDensity(feature_method: str, n_features_reference: int, k_star_reference: int, saturation_exponent: float, saturation_cap: int, splats_per_feature: float, feature_threshold: float = 0.0)[source]

Bases: object

Transferable splat budget derived from one calibration.

The investigation found splats-to-saturate scales sub-linearly with feature content (K ~ features^alpha, alpha≈0.44; n_peaks the best predictor). This packages K* + the reference feature count + the exponent so any tile can get a budget via predict_k() without re-calibrating — the cal→planner interface. Assumes tiles of roughly the reference scale (“calibrate at the scale you fit at”).

feature_method: str
n_features_reference: int
k_star_reference: int
saturation_exponent: float

Sub-linear exponent alpha in K ~ features^alpha (default 0.44).

saturation_cap: int

Effective K beyond which the reference region overfits / plateaus.

splats_per_feature: float

Linear reference density k_star / n_features (for reporting).

feature_threshold: float = 0.0

Absolute intensity threshold used to count n_features_reference. The planner must scan at this same absolute level so its per-box counts are on the same scale as the reference (threshold-relative-to-local-max does not compose across regions, especially with hot outliers).

predict_k(n_features: int) → int[source]

Predict the splat budget for a region with n_features features.

__init__(feature_method: str, n_features_reference: int, k_star_reference: int, saturation_exponent: float, saturation_cap: int, splats_per_feature: float, feature_threshold: float = 0.0) → None
luxar.gsplats.calibration.build_k_grid(explicit: Sequence[int] | None = None, n_points: int = 10, k_min: int = 1000, k_max: int = 512000, progression: str = 'exp', power: int = 2) → List[int][source]

Construct a sweep grid of splat counts.

When explicit is provided it takes precedence; otherwise n_points values are placed between k_min and k_max according to progression:

  • "exp" — log-spaced (geometric). Default. Matches the manuscript’s {1K, 2K, ..., 512K} at n_points=10, k_min=1000, k_max=512000.

  • "power" — polynomial: K_i = k_min + (k_max - k_min) * (i/(N-1))**power. Denser at low K when power > 1.

Duplicates from rounding are removed but the sequence is kept monotonic. Endpoints are guaranteed to be exactly k_min and k_max.

luxar.gsplats.calibration.calibrate(V: ndarray, k_grid: Sequence[int], *, fit_kwargs: Dict[str, Any] | None = None, mask_seed: int = 42, mask_fraction: float = 0.05, donut_radius: int = 1, keep_fits: Path | None = None, progress_callback: Callable[[int, int, str], None] | None = None, k_star_metric: str = 'psnr_minmax', fg_bg_ratio: float = 1.0, feature_method: str = 'peaks', saturation_exponent: float = 0.44, compute_rd_model: bool = True) → CalibrationResult[source]

Run a blind-spot CV sweep over k_grid on volume V.

Pipeline (one execution per call):

  1. Generate a deterministic CV mask, donut-fill V to make V_filled.

  2. Fit a Gaussian-splat model with fit_gaussian_splats at each K in k_grid, using V_filled as the target. fit_kwargs are forwarded verbatim except seeds (overridden per-K).

  3. Render each fit back to volume; compute held-out / train / full PSNR (and full SSIM) against the original V.

  4. Estimate the noise floor on V.

  5. Detect K* via find_k_star().

Parameters:
  • V (np.ndarray) – Input volume (ndim >= 2). Will be passed verbatim to the fitter, which handles its own normalisation.

  • k_grid (sequence of int) – Splat counts to evaluate.

  • fit_kwargs (dict, optional) – Forwarded to fit_gaussian_splats (preset / config / device / cull_retention / verbose / …). The seeds key is always overridden per-K.

  • mask_seed – Mask construction parameters; the defaults match the manuscript.

  • mask_fraction – Mask construction parameters; the defaults match the manuscript.

  • donut_radius – Mask construction parameters; the defaults match the manuscript.

  • keep_fits (Path, optional) – Directory to persist the per-K .gsplats.zarr outputs. If None, the fitted splats are not saved (memory only during the run).

  • progress_callback (callable, optional) – Invoked as (i, n, msg) before each fit and after metrics.

Return type:

CalibrationResult

luxar.gsplats.calibration.calibrate_saturation_exponent(V: ndarray, scales: Sequence[int], *, k_grid: Sequence[int], fit_kwargs: Dict[str, Any] | None = None, feature_method: str = 'peaks', region_strategy: str = 'densest', k_star_metric: str = 'psnr_minmax', fg_bg_ratio: float = 1.0, mask_seed: int = 42, mask_fraction: float = 0.05, progress_callback: Callable[[int, int, str], None] | None = None) → ExponentFit | None[source]

Measure alpha in K ~ features^alpha by calibrating at several scales.

For each edge length in scales a content-rich sub-region of that size is selected (select_calibration_region()) and calibrated (calibrate(), RD model skipped) to obtain its K*. Feature counts are taken at a single shared absolute level derived from the full volume (_robust_feature_level()) so the per-scale counts compose — a per-crop relative threshold would make the slope (alpha) inconsistent. K* is detected with the same k_star_metric the caller uses for the main sweep, so the regressed K* and the reported anchor are the same definition. The points are regressed in log-log space by fit_saturation_exponent().

Returns the ExponentFit, or None when fewer than two scales yield distinct feature counts (e.g. every scale collapsed to the whole volume). Runtime is roughly len(scales) × a single calibrate() sweep.

luxar.gsplats.calibration.count_features(V: ndarray, method: str = 'peaks', *, threshold_abs: float | None = None, **kwargs: Any) → int[source]

Estimate the feature content of a volume — the predictor of splat need.

The empirical investigation found local-maxima count (peaks) the best predictor of how many splats a region needs (better than intensity-sum or foreground-count), so it is the default. edges (summed Sobel gradient magnitude, thresholded) suits non-punctate structure (filaments, membranes); intensity is a robust foreground-voxel count.

Parameters:
  • V (np.ndarray) – Input volume.

  • method ({"peaks", "edges", "intensity"}, default "peaks") – Feature estimator. Pluggable so non-nuclear data can choose edges.

  • threshold_abs (float, optional) – Absolute detection level (the value feature_threshold() returns). When given, every method counts at this shared level instead of a per-volume relative one — so counts on different crops compose (required when ranking sliding windows; a per-crop relative threshold lets a faint-noise window out-score a real one). peaks/edges threshold the blurred / gradient field at it; intensity counts V > thr (strict >, matching foreground_mask_otsu()).

  • **kwargs – Forwarded to the underlying estimator (e.g. radius, threshold_rel for peaks).

Returns:

A non-negative feature count.

Return type:

int

luxar.gsplats.calibration.cv_mask(shape: Tuple[int, ...], fraction: float = 0.05, seed: int = 42) → ndarray[source]

Deterministic Bernoulli boolean mask for blind-spot cross-validation.

Defaults match Batson & Royer (2019) and the Luxar manuscript: 5% of voxels are held out with seed 42.

Parameters:
  • shape (tuple of int) – Output array shape.

  • fraction (float, default 0.05) – Probability of any voxel being marked True (held out).

  • seed (int, default 42) – RNG seed for reproducibility.

Returns:

True at held-out positions, False elsewhere.

Return type:

np.ndarray of bool, shape ``shape``

luxar.gsplats.calibration.donut_median_fill(V: ndarray, mask: ndarray, radius: int = 1) → ndarray[source]

Replace masked voxels with the median of their unmasked donut neighbours.

The donut is the (2r+1)^D cube around each masked voxel with the centre excluded — 26 neighbours in 3D when r=1. Neighbours that are themselves held out are excluded from the median, so the filled volume is a function of the unmasked voxels only: perturbing the values at masked positions leaves the output unchanged everywhere. This is what makes the blind-spot argument hold — the fitter never sees a held-out value, directly or through a neighbour’s fill. (At a 5 % Bernoulli mask with a 26-neighbour donut, ~74 % of masked voxels have at least one masked neighbour, so the exclusion is not a corner case.)

Operates on arrays of arbitrary dimension (2D, 3D, 4D, …). Edge voxels use mode='reflect' padding (applied to V and mask alike, so a reflected masked voxel stays excluded). If every donor at radius is masked, the neighbourhood expands one shell at a time until an unmasked donor is found. Genuine NaN donors remain distinct from held-out donors and propagate through the median as they did before masked-neighbour exclusion was added.

Donors are gathered and sorted in bounded chunks. The working donor buffer targets 16 MiB (or one donor column when that alone is larger), in the input dtype, in addition to the reflected copies of V and mask for the current radius.

Parameters:
  • V (np.ndarray) – Volume to fill.

  • mask (np.ndarray of bool, same shape as ``V``) – True at positions to replace (held out).

  • radius (int, default 1) – Donut half-width. Default 1 → 3^D neighbourhood, matching the manuscript.

Returns:

Copy of V with masked voxels replaced by donut medians of their unmasked neighbours. Unmasked voxels are unchanged.

Return type:

np.ndarray, same shape and dtype as ``V``

Raises:

ValueError – If the shapes differ, radius is less than one, or every voxel is held out so no fill donor exists.

luxar.gsplats.calibration.estimate_floor(V: ndarray, method: str = 'mode') → float[source]

Estimate the background pedestal / DC offset to subtract before fitting.

A constant background is the worst case for a localized Gaussian-splat basis, so subtracting it before normalisation is the single highest-leverage preprocessing step on real microscopy.

Parameters:
  • V (np.ndarray) – Input volume (any shape / dtype convertible to float).

  • method ({"mode", "specimen", "percentile"}) – "mode" (default): histogram mode of the low-intensity bulk (the pedestal peak), capped at the median so an image that is mostly signal can never have real signal subtracted. On clean data with no pedestal mode ≈ min(V) → effectively a no-op → backward-compatible. "percentile": the 10th intensity percentile (cheaper; matches the _background_mad() threshold). "specimen": opt-in bimodal-background mode. Otsu first excludes the bright signal class, then splits the remaining background band; when both populations are compact and separated, the upper population’s mode is returned. Otherwise it falls back to mode.

Notes

Exact-zero voxels (masked / out-of-FOV padding) are excluded so padding does not dominate the histogram. This function materializes V; use luxar.gsplats.fitting.preprocessing.resolve_volume_floor() for a lazy whole volume.

luxar.gsplats.calibration.estimate_floor_result(V: ndarray, method: str = 'mode') → FloorEstimate[source]

Estimate a floor and report which estimator branch produced it.

luxar.gsplats.calibration.estimate_noise_floor(V: ndarray) → NoiseFloor[source]

Three-estimator ensemble noise-floor estimate.

Returns the median of the (Laplacian, Haar, background) estimators that finite-valued — robust to one outlier on the low side (typical when the dark tail is quantised, e.g. acto3d_heart_nuclei in the manuscript).

The PSNR ceiling assumes data_range = 1.0 (the [0, 1] normalisation enforced by fit_gaussian_splats). When sigma_hat is exactly zero (saturation at float32 precision), the ceiling is +inf; callers can clamp to a conservative finite value.

luxar.gsplats.calibration.feature_threshold(V: ndarray, method: str = 'peaks', threshold_rel: float = 0.1) → float[source]

The exact absolute intensity level count_features() thresholds at.

Single source of truth for the cal→planner contract: the calibration records this so the planner’s scan_content counts on the identical scale (the detectors threshold relative to a blurred / gradient / Otsu level, NOT the raw max, so a naive 0.1*max drifts — badly with hot outliers).

  • peaks → threshold_rel * max(soft_blur(V)) (matches count_local_maxima)

  • edges → threshold_rel * max(|∇V|)

  • intensity → the Otsu cut

luxar.gsplats.calibration.find_k_star(k_values: Sequence[int], held_out_psnr_values: Sequence[float]) → HeldOutPeak[source]

Detect the held-out PSNR peak via the manuscript’s hybrid rule.

Hybrid rule (splat_count_vs_quality §4.2):

  1. Peak: the argmax is strictly interior AND both mean(pre-argmax) and mean(post-argmax) are at least 0.1 dB below the peak. Return the argmax.

  2. Signal-limited: the argmax is the last K, the curve rose by ≥ 0.3 dB across the sweep, AND it is still climbing at the top (mean rise over the trailing run of adjacent finite steps ≥ 0.1 dB AND the single final step ≥ 0.05 dB). Return the last K. The tail checks stop a flat-topped plateau (whose noisy max lands on the last K) from being misread as signal-limited.

  3. Plateau: otherwise. Return the smallest K within 0.3 dB of the maximum — the onset of diminishing returns.

The returned HeldOutPeak reports both k_star (the budget anchor above — max K for signal_limited) and k_knee (the point of diminishing returns: the argmax for a clear peak, else the knee), plus supporting per-regime metadata. k_knee equals k_star for peak and plateau and is the earlier knee for signal_limited; it is the field to use when a single “reasonable operating point” is wanted regardless of regime.

luxar.gsplats.calibration.fit_rd_model(k_values: Sequence[float], error_values: Sequence[float]) → RDModel | None[source]

Fit error ≈ floor + a·K^-beta (least-squares). None if <3 finite points or the fit fails. error_values should be held-out MSE.

luxar.gsplats.calibration.fit_saturation_exponent(points: Sequence[Tuple[int, float, float]]) → ExponentFit | None[source]

Least-squares fit of alpha in K ~ features^alpha (log-log regression).

points is a sequence of (scale, n_features, k_star) triples (one per calibrated region scale). Duplicate feature counts (scales that clamped to the same crop) are collapsed to one point. Returns None when fewer than two distinct feature counts remain (no spread to fit a slope).

r_squared is set to NaN when it cannot be meaningfully assessed — fewer than three distinct points (a 2-point line is always perfect), or zero K* variance (a degenerate flat fit, alpha≈0) — so a caller’s goodness-of-fit gate is not fooled by a structural R²==1.0.

luxar.gsplats.calibration.foreground_mask_otsu(V: ndarray) → ndarray[source]

Boolean foreground mask via Otsu’s threshold (V > thr).

luxar.gsplats.calibration.foreground_mask_otsu_smoothed(V: ndarray) → Tuple[ndarray, float][source]

Foreground mask for weighted calibration scoring.

Applies one light separable tent blur, then computes Otsu on the blurred field. The input is expected floor-subtracted; the returned threshold is on the smoothed scale and is recorded with the metric for reproducibility.

luxar.gsplats.calibration.held_out_gain_db(held_mse: float, baseline_mse: float) → float[source]

dB improvement of the fit over the predict-zero baseline.

10 * log10(baseline_mse / held_mse). 0 dB means “no better than predicting zeros”. Because the baseline is constant across K, this curve differs from min–max PSNR only by a constant and selects the same K*.

luxar.gsplats.calibration.held_out_psnr(V_hat: ndarray, V_original: ndarray, mask: ndarray, data_range: float | None = None) → float[source]

PSNR of reconstruction at masked voxels vs the original (pre-fill) values.

Parameters:
  • V_hat (np.ndarray) – Reconstructed volume from the splat fit.

  • V_original (np.ndarray) – The unmodified original volume (NOT the donut-filled one).

  • mask (np.ndarray of bool) – Held-out mask. Must broadcast to the volume shape.

  • data_range (float, optional) – Dynamic range for PSNR. If None, uses V_original.max() - V_original.min() over the whole volume.

Returns:

PSNR in dB. +inf when MSE is zero, nan when mask is empty.

Return type:

float

luxar.gsplats.calibration.held_out_psnr_fg_weighted(V_hat: ndarray, V_original: ndarray, held_mask: ndarray, foreground_mask: ndarray, fg_bg_ratio: float = 1.0, data_range: float | None = None) → float[source]

Held-out PSNR with controlled foreground/background total weight.

Foreground voxels receive unit weight. Background voxels receive n_fg / (fg_bg_ratio * n_bg) using counts from the held-out subset, so fg_bg_ratio=1 gives the two strata exactly equal total weight. Returns nan when either held-out stratum is empty.

luxar.gsplats.calibration.held_out_psnr_foreground(V_hat: ndarray, V_original: ndarray, held_mask: ndarray, foreground_mask: ndarray, data_range: float | None = None) → float[source]

Held-out PSNR restricted to voxels that are both held out AND foreground.

Strips the background-domination from held_out_psnr() so the curve reflects how well actual signal (not empty space) is reconstructed. Returns nan when the held-out∩foreground set is empty.

luxar.gsplats.calibration.predict_zero_baseline_mse(V_original: ndarray, mask: ndarray) → float[source]

MSE of the trivial all-zeros reconstruction at masked voxels.

This is the “free” error floor any fit must beat. On sparse data it is small (most masked voxels are background ~0), which is exactly why the raw held-out PSNR looks deceptively high.

luxar.gsplats.calibration.select_calibration_region(V: ndarray, region_size: int = 256, strategy: str = 'densest', feature: str = 'peaks', **feature_kwargs: Any) → Tuple[ndarray, RegionSelection][source]

Pick a content-rich sub-region to calibrate at the fitting scale.

The manuscript calibrates on crops ≤~20 M voxels; on a large sparse volume the held-out metric is background-dominated and the absolute K wrong-scale. This slides non-overlapping region_size windows, scores each by feature density (count_features()), and returns the chosen crop + provenance.

strategy="densest" picks the highest-density window (worst case for splat budget); "median" picks the median-density window (representative, avoids the single brightest outlier). For volumes no larger than region_size on every axis the whole volume is returned (strategy="whole").

PDF report for luxar gsplat cal results.

Produces a multi-page matplotlib PDF mirroring the per-dataset figures of manuscript/supp_doc/splat_count_vs_quality/:

  • Page 1 — Rate-distortion: PSNR / SSIM / fit time / train-vs-held-out gap, all vs K, with K* annotated and the noise-floor PSNR ceiling overlaid.

  • Page 2 — Blind-spot cross-validation: train + held-out PSNR with the overfitting region shaded.

  • Page 3 — Slice montages (only when per-K fits were persisted via --keep-fits): target / reconstruction at K_min / K* / K_max plus a per-pixel error map. Skipped gracefully otherwise.

This module is loaded only when --pdf is set so matplotlib does not inflate the cold-start cost of every CLI invocation.

luxar.gsplats.calibration_report.render_calibration_report(result: CalibrationResult, volume: ndarray, output_path: Path, splat_paths: List[str] | None = None) → None[source]

Generate a multi-page PDF calibration report.

Parameters:
  • result – Output of luxar.gsplats.calibration.calibrate().

  • volume – The original (pre-mask) input volume — used as the “target” panel in the slice montage.

  • output_path – Destination .pdf.

  • splat_paths – Optional list of per-K .gsplats.zarr paths in the same order as result.k_values_requested. When provided, the third page of the PDF includes reconstruction slice montages at K_min, K*, K_max. When None, page 3 is replaced with a placeholder.

Raises:

ImportError – If matplotlib is not installed. The CLI handler catches this and prints a hint to install the optional dependency.

Level of Detail (LOD)

Post-fit LOD construction for streaming and view-dependent rendering. Used by the luxar gsplat lod --recipe ... CLI command (recipes flat / stream / levels / tiles / overview / adaptive).

  • stream — same N splats, reordered into a prefix-monotone additive ladder (make_additive_lod). Loading the first k splats is the best L² approximation at that budget.

  • levels — synthesise M < N representative splats per coarser level via Gaussian mixture reduction (make_substitutive_lod).

  • tiles / overview / adaptive — spatial-partition topologies for large datasets (per-tile streaming ladders, an optional coarse overview cap, or per-tile level swaps).

Levels-of-Detail (LOD) post-processing for fitted Gaussian-splat datasets.

Two LOD axes are implemented:

  • Additive — same N splats, ordered so that the prefix sum at any k splats is the best L^2 approximation of the full scene. Implemented in luxar.gsplats.lod.additive. Entry point: make_additive_lod() returns a matrix-shaped GSplatData where the selected substitutive level’s additive_prefix(k) is a valid additive prefix.

  • Substitutive — synthesise M < N representative splats per coarser level via mixture reduction (supp doc substitutive_lod.tex). Implemented in luxar.gsplats.lod.substitutive. Entry point: make_substitutive_lod() returns a matrix-shaped GSplatData with n_substitutive = levels + 1 and a single additive sub-LOD per substitutive level.

Both operators are pure post-processes on a fitted GSplatData; fitting (single-pass or progressive) returns a single flattened dataset, and an LOD hierarchy is built only on demand.

The convenience function make_lod_pyramid() (in luxar.gsplats.lod.pyramid) chains the two: substitutive reduction first (outer axis), then an additive ladder inside each substitutive level (inner axis). Saved to disk, the result is a single v3.4 node-tree .gsplats.zarr carrying the full 2-D pyramid.

The luxar.gsplats.lod.recipes module composes these builders into named, scale-ordered representation topologies (flat / stream / levels / tiles / overview / adaptive) — the luxar gsplat lod --recipe CLI is a thin wrapper over build_recipe().

class luxar.gsplats.lod.RecipeParams(n_lods: int = 4, additive_method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto', reveal_center: Sequence[float] | None = None, spatial_dims: Sequence[int] | None = None, breakpoints: str | Sequence[int] | Sequence[float] = 'equal-count', truncation_sigmas: float | None = None, max_n_dense: int = 2000, max_elements: int | None = None, partition_rule: Literal['median', 'midpoint', 'sah'] = 'median', compression_factor: int = 4, levels: int = 3, substitutive_method: str = 'auto', lloyd_iterations: int = 5, candidate_bins_k: int = 12, coverage_inflation: float = 3.0, conserve_mass: bool = True, additive_ladders: bool = True, refine: str = 'none', refine_iters: int | None = None, volume: ndarray | None = None, image_min: float | None = None, volume_axes: tuple | None = None, coarsen_dims: tuple | None = None, quality_stamps: bool = True, quality_max_pair_splats: int = 2000000, device: str = 'auto', seed: int | None = None)[source]

Bases: object

Parameters for build_recipe(), with library-faithful defaults.

The defaults here mirror the historical subcommands. The CLI applies its own scale-derived defaults (e.g. --parts → max_elements) before calling build_recipe(); passing max_elements=None falls back to DEFAULT_MAX_ELEMENTS so the builders are usable standalone too.

n_lods: int = 4
additive_method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto'
reveal_center: Sequence[float] | None = None
spatial_dims: Sequence[int] | None = None
breakpoints: str | Sequence[int] | Sequence[float] = 'equal-count'
truncation_sigmas: float | None = None
max_n_dense: int = 2000
max_elements: int | None = None
partition_rule: Literal['median', 'midpoint', 'sah'] = 'median'
compression_factor: int = 4
levels: int = 3
substitutive_method: str = 'auto'
lloyd_iterations: int = 5
candidate_bins_k: int = 12
coverage_inflation: float = 3.0
conserve_mass: bool = True
additive_ladders: bool = True
refine: str = 'none'
refine_iters: int | None = None
volume: ndarray | None = None
image_min: float | None = None
volume_axes: tuple | None = None
coarsen_dims: tuple | None = None
quality_stamps: bool = True
quality_max_pair_splats: int = 2000000
device: str = 'auto'
seed: int | None = None
property effective_max_elements: int

max_elements with the DEFAULT_MAX_ELEMENTS fallback.

__init__(n_lods: int = 4, additive_method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto', reveal_center: Sequence[float] | None = None, spatial_dims: Sequence[int] | None = None, breakpoints: str | Sequence[int] | Sequence[float] = 'equal-count', truncation_sigmas: float | None = None, max_n_dense: int = 2000, max_elements: int | None = None, partition_rule: Literal['median', 'midpoint', 'sah'] = 'median', compression_factor: int = 4, levels: int = 3, substitutive_method: str = 'auto', lloyd_iterations: int = 5, candidate_bins_k: int = 12, coverage_inflation: float = 3.0, conserve_mass: bool = True, additive_ladders: bool = True, refine: str = 'none', refine_iters: int | None = None, volume: ndarray | None = None, image_min: float | None = None, volume_axes: tuple | None = None, coarsen_dims: tuple | None = None, quality_stamps: bool = True, quality_max_pair_splats: int = 2000000, device: str = 'auto', seed: int | None = None) → None
luxar.gsplats.lod.build_recipe(data: GSplatData, recipe: Literal['flat', 'stream', 'levels', 'tiles', 'overview', 'adaptive'], params: RecipeParams) → GSplatData | GSplatLeaf | GSplatLodGroup | GSplatPartition[source]

Build recipe from data.

Returns a GSplatData for the matrix recipes (flat/stream/levels) and a GSplatNode for the composed recipes (tiles/overview/adaptive) — see RecipeResult.

luxar.gsplats.lod.compute_additive_order(data: GSplatData, method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto', *, truncation_sigmas: float | None = None, max_n_dense: int = 2000, seed: int | None = None, reveal_center: Sequence[float] | None = None, spatial_dims: Sequence[int] | None = None, slice_dims: Sequence[int] | None = None) → ndarray[source]

Compute an additive ordering permutation for the splats in data.

Parameters:
  • data (GSplatData) – Fitted (single- or multi-LOD) gsplat dataset. Operates on the flattened concatenation across LODs.

  • method (str) – One of auto, greedy, self_energy, mass, amplitude, spectral, random, radial. auto (the default) resolves to greedy at small N and self_energy above _AUTO_ADDITIVE_MAX_N — see resolve_additive_method(). See module docstring for details.

  • truncation_sigmas (float, optional) – Mahalanobis cutoff used for sparse-Gram pruning. Defaults to the dataset’s own truncation_radius — the support the splats were fitted at and are rendered at. Only relevant for greedy and spectral.

  • max_n_dense (int) – For greedy, build a dense Gram and use scan-greedy when $N leq$ this threshold. Above it, build a sparse Gram and use lazy-greedy. Default 2000 (per supp doc §4.3).

  • seed (int, optional) – Random seed for method='random'.

  • reveal_center (sequence of float, optional) – Centre of the shells for method='radial'. Defaults to the spatial bounding-box centre — NOT the scene origin, so a dataset far from the origin still grows from its own middle. One coordinate per spatial axis.

  • spatial_dims (sequence of int, optional) – Centre columns the radial distance is measured over. Defaults to the non-degenerate (real-extent) axes, which excludes a stacked time or channel axis. Ignored by every other method.

  • slice_dims (sequence of int, optional) – RAW, pre-dim_order centre columns whose distinct combinations the viewer SLICES (a hidden time / channel axis) — NOT the scene’s post-dim_order dimension positions, which are a different frame of reference. When given, the ordering chosen by method is re-emitted round-robin across those slices by interleave_order_across_slices(), so every prefix carries an equal ABSOLUTE budget per slice instead of a global contribution-ordered prefix that starves the sparse ones. Composes with EVERY method — it is a modifier, not a method. None (the default) leaves the order untouched; there is no default column set, since a standalone GSplatData has no display information to derive one from.

Returns:

order[k] is the original index of the splat at rank $k$.

Return type:

np.ndarray of shape (N,), dtype int64

luxar.gsplats.lod.decimate(data: GSplatData, *, target: int | float, method: Literal['merge', 'prefix', 'auto'] = 'auto', prefix_method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto', device: str | None = 'auto', seed: int | None = None, coarsen_dims: Sequence[int] | None = None, lloyd_iterations: int = 5, verbose: bool = False) → GSplatData[source]

Reduce data to target splats and return a flat dataset.

Parameters:
  • data – Source dataset. A multi-level input is reduced from its finest content (the same convention make_substitutive_lod() uses).

  • target – Absolute count (int) or fraction of the input (float in (0, 1]). See resolve_target_count().

  • method – "merge", "prefix", or "auto" (the measured rule — see the module docstring). Labeled inputs constrain "auto" to "prefix" as the conservative default; explicit "merge" coarsens independently within exact label groups.

  • prefix_method – Ordering for method="prefix", passed to compute_additive_order() (auto / self_energy / mass / greedy / radial / …).

  • device – Device for the clustering pass (merge only).

  • seed – Seed for the random ordering (prefix only; every other ordering, and the clustering, is deterministic).

  • coarsen_dims – Center-column indices merging may combine over; the rest are hard barriers (merge only). Default: all dims. A merge stamps the RESOLVED set on the result (the writer turns it into the chunk-ordering barrier); a prefix ignores the argument (a UserWarning, so the notice survives verbose=False) and keeps the input’s stamp, having coarsened nothing. Under the luxar CLI that warning renders as an arbol line like any other output. The request is range-validated for BOTH families, before the family is chosen — under method="auto" which one runs depends on the kept fraction, and an argument may not be a hard error on one path and silently accepted on the other.

  • lloyd_iterations – Lloyd refinement passes (merge only).

  • verbose – Narrate the reduction.

Returns:

A flat GSplatData with <= target splats, and close to it. Returns the input unchanged when target resolves to the full count. merge can land slightly under the request — the clustering drops degenerate (empty / non-positive-mass) clusters, so a 165,340 ask on the 1.65M-splat reference dataset yields 165,276. A request below the number of coordinate and/or label barrier groups lands OVER: every group keeps at least one representative rather than whole timepoints, channels, or classes being deleted to hit a count (the reduction says so on the console).

Raises:

ValueError – on an out-of-range target, an unknown method, or a coarsen_dims index outside [0, data.ndim).

luxar.gsplats.lod.interleave_order_across_slices(data: GSplatData, order: ndarray, slice_dims: Sequence[int]) → ndarray[source]

Re-emit order round-robin across slices, so every prefix is slice-even.

A node the viewer SLICES (any non-displayed dimension) shows one hidden coordinate at a time, but an additive rung is sized against the WHOLE node. A contribution-ordered prefix therefore concentrates wherever the signal is and the sparse coordinates get almost nothing: the NEXRAD supercell’s 82-scan stack (817,989 splats; per-scan min 562, p05 774, median 10,499, max 19,237) shipped an absolute breakpoints="stream:20000" first rung whose 5th-percentile scan held 4 splats — an empty screen during playback, and two failures in scripts/check_demo_ladders.py (#2485).

Grouping the elements of order by their distinct combination of the slice_dims centre columns and emitting one per group per pass turns that global budget into an EQUAL ABSOLUTE per-slice budget. Precisely: after $R$ completed passes the prefix holds $min(n_i, R)$ elements of every slice $i$, so a slice SMALLER than the budget is carried WHOLE and a large one is capped — which is exactly the shape check_demo_ladders.py’s absolute first-paint arm asks for. Ties inside a pass are broken by position in order, so the pass order is stable.

That guarantee has a PRECONDITION worth stating: a rung must be at least as large as the slice count, or it cannot reach every slice at all. A rung smaller than $S$ does not even complete its first pass, and since within-pass ties break by position in order the coordinates left with NOTHING are the faintest ones — measured on 500 slices of 40 splats with breakpoints=[200], 300 coordinates got zero. Nowhere near a hazard for the demo this was built for (204,497 against 82 slices), but a ladder whose first rung is smaller than its hidden-axis cardinality is not made even by this.

Two properties worth relying on:

  • Deterministic — no seed, and no distributional argument. Sizing the ladder by hand cannot get here: on the stack above the 250-element floor needs ~32% of the sparsest scan, and a uniform method="random" permutation only makes the per-slice share proportional IN EXPECTATION — measured over seeds 0-7, n_lods=3 cleared the floor 5 times in 8 (p05 243-277 against a floor of 250) and n_lods=4 never did (186-205).

  • Idempotent — re-interleaving an already-interleaved order returns it unchanged, because the within-group order (and hence every within-group rank) is untouched. Both the authoring door and make_additive_lod()’s Gram branch could in principle apply it.

The within-slice order is still whatever the base method produced, so under the default method="auto" each coordinate keeps painting bright-core- first rather than evenly thin.

slice_dims indexes RAW, PRE-dim_order centre columns, and that is the likeliest way to get this wrong. The ladder is built in core/group/gsplats_pipeline/from_data.py ABOVE the apply_dim_order_* pass that lod_dispatch runs, so these are the columns of the array the caller handed in — NOT the scene’s post-dim_order dimension positions. They coincide for the NEXRAD supercell only because its dim_order leaves time last in both frames. An author who reads off the scene’s Dimensions list instead gets a different column, and per the next paragraph that is a near-no-op. A high-cardinality diagnostic below warns about this likely pre-/post-dim_order mixup without rejecting legitimate small slices.

The columns must also be genuinely DISCRETE — a stacked time/channel axis, where coordinates repeat. Pointed at a continuous one, nearly every key is distinct, nearly every rank is 0, and the lexsort reproduces order: functionally a no-op. NOT a bit-identical one, though, so this must not be used as an equality assertion — real centres do collide, and each collision demotes one element a pass later, which shifts the whole tail behind it. Measured on 8 cached NEXRAD frames (5,937 splats, 5,907 distinct values in column 0), slice_dims=[0] left the first 20 positions untouched and moved 5,901 of 5,937 overall; one deliberate collision among 2,000 float32 samples moved 48. Reachable by composition and not only by typo: a lod_group=dict(coarsen_dims=[0, 1, 2, 3]) — coarsening OVER the stacked axis — turned 3 exact time coordinates into 35 fractional ones on the coarse level, and resolve_additive_axis_gsplats() applies one slice_dims to every substitutive level, giving a slice-even finest level and silently uneven coarse ones. The default Auto coarsening (hidden axis as a hard barrier) is safe.

Parameters:
  • data (GSplatData) – The dataset order indexes; only its centers are read.

  • order (np.ndarray) – A length-N integer permutation, as returned by compute_additive_order(). Validated for shape, dtype and range; duplicate entries are the caller’s responsibility (see _validate_interleave_order()).

  • slice_dims (sequence of int) – RAW, pre-dim_order centre columns whose distinct combinations define a slice. No default — see _validate_slice_dims().

Returns:

A permutation of the same elements, slice-even at every prefix.

Return type:

np.ndarray of shape (N,), dtype int64

luxar.gsplats.lod.make_additive_lod(data: GSplatData, n_lods: int = 4, *, method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto', breakpoints: str | Sequence[int] | Sequence[float] = 'equal-count', truncation_sigmas: float | None = None, max_n_dense: int = 2000, seed: int | None = None, substitutive_level: int | None = None, reveal_center: Sequence[float] | None = None, spatial_dims: Sequence[int] | None = None, slice_dims: Sequence[int] | None = None) → GSplatData[source]

Permute and split a fitted gsplat dataset into a multi-LOD ladder.

The result is a GSplatData with n_lods (or as resolved by breakpoints) AdditiveSubLOD levels on the selected substitutive level. additive_prefix(k) returns the valid additive prefix of size \(\sum_{\ell \leq k} N_\ell\) for that level.

Parameters:
  • data (GSplatData) – Fitted gsplat dataset. May be multi-substitutive: the substitutive_level argument (default = default substitutive level) selects which level receives the new additive ladder. Other substitutive levels are carried over verbatim.

  • n_lods (int) – Number of LOD levels when breakpoints='equal-count'. Ignored when breakpoints is a list or 'stream:<c>' (those determine the level count themselves).

  • method (str) – Ordering method (see compute_additive_order()).

  • breakpoints (:py:class:``’equal-count’:py:class:``, :py:class:``’stream:<c>’:py:class:``, or list of int / float) –

    • 'equal-count': n_lods levels of (nearly-)equal size.

    • 'stream:<c>': geometric streaming ladder — cumulative cuts [c, 2c, 4c, …, N] sized so the first chunk is c splats (bandwidth-derived via streaming_chunk_splats()), then doubling. Resolved against each call’s own N (per part / per substitutive level), silently clamped for small N (never raises, unlike explicit counts), capped at DEFAULT_STREAM_MAX_LEVELS levels.

    • 'equi-energy:<n>': n rungs at EQUAL shares of cumulative self-energy along the ordering — the first rung is the few heaviest splats, later rungs are fatter in count for the same light — with any increment above DEFAULT_MAX_ADDITIVE_COMMIT split into capped steps (luxar.utils.lod_breakpoints.equi_energy_cuts()). Pair with a contribution-first method (self_energy, the large-N default) — under random the rungs are still equal in energy but there is no ordering to front-load.

    • list[int]: explicit cumulative splat counts per level.

    • list[float] in $(0, 1]$: cumulative energy fractions; the smallest $k$ at which the cumulative-utility curve crosses each fraction is used as the cutpoint. For greedy / spectral orderings that build a Gram matrix the curve is the residual-energy curve; for score-ordered methods (self_energy / mass / amplitude / random) it is the O(N) self-energy cumulative, so cuts land where the viewer’s own $e(k)$ quality stamp reads the requested fraction.

  • truncation_sigmas (float, optional) – $sigma$ multiplier for sparse-Gram pruning. Defaults to the dataset’s own truncation_radius.

  • max_n_dense (int) – Threshold below which greedy uses a dense Gram + scan-greedy.

  • seed (int, optional) – Random seed for method='random'.

  • substitutive_level (int, optional) – Index of the substitutive level to build the ladder for. Defaults to data.default_substitutive.

  • reveal_center (sequence of float, optional) – method='radial' only — centre of the concentric shells. Defaults to the spatial bounding-box centre (NOT the scene origin, so a dataset far from the origin still reveals from its own middle).

  • spatial_dims (sequence of int, optional) – method='radial' only — the centre columns the shell distance is measured over. Defaults to the non-degenerate axes, so a stacked time/channel axis cannot become a shell dimension.

  • slice_dims (sequence of int, optional) – RAW, pre-dim_order centre columns the viewer SLICES (a hidden time / channel axis) — NOT the scene’s post-dim_order dimension positions. When given, the ordering is re-emitted round-robin across those slices (see interleave_order_across_slices()), so every rung carries an equal ABSOLUTE budget per slice rather than a global prefix that starves the sparse ones (#2485). A modifier: it composes with every method, and the energy_fraction_cum stamps are computed from the interleaved order, so the viewer’s committed e(k) describes the prefix actually written — NODE-GLOBALLY, which is the only granularity that stamp has: e(k) is one number per rung and the viewer’s 1/max(e, 0.1) compensation is applied uniformly across hidden coordinates, so a small slice that slice-evenness has already loaded COMPLETELY is still brightened (measured, rung 0 stamps e = 0.408 interleaved against 0.684 plain — a 2.45x boost on an already-complete slice). This is NOT confined to a kind=lod group: applyLodFade has a second caller in the viewer’s scene/density-guard.ts, driven by the projected-density tracker over the whole scene graph for any blendable data mesh, and both the guard and the compensation default ON — so a BARE multi-additive leaf is in scope too once the guard steps it. Small in practice; on the NEXRAD node it is a 1.108x brightening (that store stamps e(0) = 0.9024), applied to the 12 of 82 scans this already loads whole as much as to the rest. Note also that interleaving FLATTENS the cumulative-energy curve, so energy-fraction breakpoints resolve to materially larger first rungs — measured on 1,580 splats over 4 slices, [0.5, 0.9, 0.99, 1.0] cuts at [222, 749, 1116, 1580] plain and [521, 1128, 1492, 1580] interleaved. The requested fractions are still delivered and the rungs are still slice-even; it is the first-paint COST that moves. None (the default) leaves the order untouched.

Returns:

A matrix-shaped GSplatData with the same n_substitutive as data; the selected level’s additive sub-LODs form the new ladder, other substitutive levels are passed through unchanged.

Return type:

GSplatData

luxar.gsplats.lod.make_lod_pyramid(data: GSplatData, *, compression_factor: int = 4, levels: int = 3, substitutive_method: Literal['auto', 'kmeans', 'kmeans_lloyd', 'greedy', 'greedy_lloyd'] = 'auto', lloyd_iterations: int = 5, candidate_bins_k: int = 12, color_weight: float = 0.0, coverage_inflation: float = 3.0, conserve_mass: bool = True, refine: str = 'none', refine_iters: int | None = None, volume: ndarray | None = None, volume_axes: Sequence[int] | None = None, device: str | torch.device | None = 'auto', coarsen_dims: Sequence[int] | None = None, n_additive_lods: int = 4, additive_method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto', additive_reveal_center: Sequence[float] | None = None, additive_spatial_dims: Sequence[int] | None = None, breakpoints: str | Sequence[int] | Sequence[float] = 'equal-count', truncation_sigmas: float | None = None, max_n_dense: int = 2000, seed: int | None = None, verbose: bool = False, quality_stamps: bool = False, quality_max_pair_splats: int = 2000000) → GSplatData[source]

Build the full 2-D LOD pyramid (substitutive × additive) in one call.

The pipeline runs make_substitutive_lod() first (outer axis) and then calls make_additive_lod() on each substitutive level (inner axis). The result is a single matrix-shaped GSplatData with n_substitutive = levels + 1 and M_i = n_additive_lods (or as resolved by breakpoints) per level.

Parameters:
  • data – Source fitted gsplat dataset.

  • compression_factor – Substitutive axis parameters (passed to make_substitutive_lod()).

  • levels – Substitutive axis parameters (passed to make_substitutive_lod()).

  • substitutive_method – Substitutive axis algorithm parameters (coverage_inflation is the anti-grid inter-spread widening; refine="l2" post-optimizes each level under the closed-form mixture L²; refine="volume" warm-start re-fits each level against the source volume — see make_substitutive_lod()).

  • lloyd_iterations – Substitutive axis algorithm parameters (coverage_inflation is the anti-grid inter-spread widening; refine="l2" post-optimizes each level under the closed-form mixture L²; refine="volume" warm-start re-fits each level against the source volume — see make_substitutive_lod()).

  • candidate_bins_k – Substitutive axis algorithm parameters (coverage_inflation is the anti-grid inter-spread widening; refine="l2" post-optimizes each level under the closed-form mixture L²; refine="volume" warm-start re-fits each level against the source volume — see make_substitutive_lod()).

  • color_weight – Substitutive axis algorithm parameters (coverage_inflation is the anti-grid inter-spread widening; refine="l2" post-optimizes each level under the closed-form mixture L²; refine="volume" warm-start re-fits each level against the source volume — see make_substitutive_lod()).

  • coverage_inflation – Substitutive axis algorithm parameters (coverage_inflation is the anti-grid inter-spread widening; refine="l2" post-optimizes each level under the closed-form mixture L²; refine="volume" warm-start re-fits each level against the source volume — see make_substitutive_lod()).

  • refine – Substitutive axis algorithm parameters (coverage_inflation is the anti-grid inter-spread widening; refine="l2" post-optimizes each level under the closed-form mixture L²; refine="volume" warm-start re-fits each level against the source volume — see make_substitutive_lod()).

  • refine_iters – Substitutive axis algorithm parameters (coverage_inflation is the anti-grid inter-spread widening; refine="l2" post-optimizes each level under the closed-form mixture L²; refine="volume" warm-start re-fits each level against the source volume — see make_substitutive_lod()).

  • volume – Substitutive axis algorithm parameters (coverage_inflation is the anti-grid inter-spread widening; refine="l2" post-optimizes each level under the closed-form mixture L²; refine="volume" warm-start re-fits each level against the source volume — see make_substitutive_lod()).

  • volume_axes – Substitutive axis algorithm parameters (coverage_inflation is the anti-grid inter-spread widening; refine="l2" post-optimizes each level under the closed-form mixture L²; refine="volume" warm-start re-fits each level against the source volume — see make_substitutive_lod()).

  • device – Substitutive axis algorithm parameters (coverage_inflation is the anti-grid inter-spread widening; refine="l2" post-optimizes each level under the closed-form mixture L²; refine="volume" warm-start re-fits each level against the source volume — see make_substitutive_lod()).

  • n_additive_lods – Additive axis parameters (passed to make_additive_lod()).

  • additive_method – Additive axis parameters (passed to make_additive_lod()).

  • breakpoints – Additive axis parameters (passed to make_additive_lod()).

  • truncation_sigmas – Additive axis algorithmic knobs. truncation_sigmas=None (the default) means the dataset’s own truncation_radius — resolved once here and passed as a concrete value to every level’s ladder.

  • max_n_dense – Additive axis algorithmic knobs. truncation_sigmas=None (the default) means the dataset’s own truncation_radius — resolved once here and passed as a concrete value to every level’s ladder.

  • seed – Optional shared seed (per-axis offsets are added internally).

  • verbose – Per-step Arbol logging from substitutive reduction.

  • quality_stamps – Opt-in measured Q·e quality stamps per substitutive level (passed to make_substitutive_lod(); see luxar.gsplats.lod.quality).

  • quality_max_pair_splats – Opt-in measured Q·e quality stamps per substitutive level (passed to make_substitutive_lod(); see luxar.gsplats.lod.quality).

Returns:

A matrix-shaped dataset with the full [levels+1, n_additive_lods] pyramid.

Return type:

GSplatData

luxar.gsplats.lod.make_substitutive_lod(data: GSplatData, *, compression_factor: int = 4, levels: int = 3, method: Literal['auto', 'kmeans', 'kmeans_lloyd', 'greedy', 'greedy_lloyd'] = 'auto', lloyd_iterations: int = 5, candidate_bins_k: int = 12, color_weight: float = 0.0, coverage_inflation: float = 3.0, conserve_mass: bool = True, amplitude: Literal['l2', 'mass'] = 'l2', refine: Literal['none', 'l2', 'volume'] = 'none', refine_iters: int | None = None, volume: ndarray | None = None, volume_axes: Sequence[int] | None = None, image_min: float | None = None, volume_box: Sequence[tuple[float, float]] | None = None, device: str | torch.device | None = 'auto', seed: int | None = None, coarsen_dims: Sequence[int] | None = None, verbose: bool = False, quality_stamps: bool = False, quality_max_pair_splats: int = 2000000) → GSplatData[source]

Build a substitutive-LOD hierarchy.

Parameters:
  • data – Source dataset. If multi-substitutive, only its default substitutive level is reduced (additive sub-LODs at that level are flattened first).

  • compression_factor – Per-level branching factor $K$. Each level ℓ has ceil(N / K^ℓ) splats.

  • levels – Number of coarser levels to produce. The returned object has levels + 1 substitutive levels (the original at index 0).

  • method – Partition algorithm, or "auto" (default). "auto" resolves per level: "greedy" when the level’s input has <= 5000 splats (highest quality, and fast there) and "kmeans_lloyd" above (greedy is ~50-100x slower at large N). See module docstring for the individual methods.

  • lloyd_iterations – Maximum number of cost-increment Lloyd passes per level (only for "kmeans_lloyd" / "greedy_lloyd"). The loop exits early as soon as a pass fails to improve the projection energy.

  • candidate_bins_k – Number of Morton-curve neighbours whose current bins are the move candidates for each splat during Lloyd refinement. Tighter k → faster, slightly worse quality.

  • color_weight – Opt-in chromatic penalty in the partition cost. 0 (default) keeps the historical spatial/intensity-only partition byte-for-byte. Values above zero apply an exp(-color_weight * distance²) affinity, but the distance is pair-to-pair for greedy and member-to-centroid for Lloyd, so the useful scale is method-specific: roughly 0.1 to 1 for greedy and 1 to 10 for Lloyd spans a soft-to-strong hue preference. Because method="auto" may switch per level, pin an explicit method when consistent chromatic strength matters. RGB is normalized by brightness; pure black maps to neutral chromaticity, and alpha is deliberately excluded while representative alpha is composed in optical-depth space. This expert knob is API-only today.

  • coverage_inflation – Inflation factor β >= 1 applied to each representative’s inter-center spread (Σ_out = intra + β·inter) with a mass-preserving amplitude rescale. Pure moment matching gives the balanced bins σ ≈ pitch/√12 — too narrow for neighbouring representatives to sum flat, which renders as a strong periodic grid ripple along the shared Morton-cell boundaries. The default β=3 widens exactly the inter term to σ ≈ pitch/2 (flat-sum threshold) and is the exact fixed point of the level recurrence, so the calibration holds at every level. 1.0 disables (historical pure-moment-matching behaviour). Trade-off: coarse levels look slightly smoother; each splat’s integral (X-ray projection) is preserved exactly. With refine="l2" the inflation is demoted from final answer to optimizer seed: the refit takes over the exact flat-sum calibration.

  • conserve_mass – Rescale each reduced level’s amplitudes by one global factor so its total mass over the coarsened dims equals its fine input’s (per barrier group under coarsen_dims). The per-bin L²-optimal amplitude is not mass-preserving (3–17 % loss per level measured, content-dependent), and that mass is the DC an additive render integrates — uncorrected it shows as a brightness pop at every LOD switch. Default True; False restores the raw per-bin amplitudes. The rescale is skipped (with a warning) when the implied factor falls outside [0.1, 10] — a numerically degenerate coarsened-dim mass, where “conserving” it would blow the amplitudes up instead.

  • amplitude – Per-bin merged-amplitude rule. "l2" (default) is the L²-optimal projection amplitude — the right choice for fitted volumetric gsplats. "mass" makes every bin exactly mass-preserving (a = Σ member a·|det L| / |det L_out|, on the final inflated covariance): per-bin colored light is then conserved together with the bin-mass-weighted mean colors, which is what the lifted points/lines LOD path uses to keep brightness/hue coherent across levels (the beads are a stroke stand-in, not a density to L²-fit). Under "mass" the global conserve_mass rescale is a no-op by construction (kept as a safety net). Exactness note: with barrier groups the conserved per-bin quantity is the full-determinant mass; the sliced (coarsened-dims-only) mass coincides when member barrier widths are equal within a bin — true for lifted isotropic beads.

  • refine – Post-merge per-level refinement. "l2" Adam-optimizes each merged level’s (mu, Σ, a) against that level’s fine input under the closed-form mixture L² (sparse pair lists, trusted checkpoints, total mass pinned to the fine mixture’s — see _substitutive.refine). Never worse than the merge in the trusted metric; substantially higher fidelity (prototype: rel-L² 0.089 vs 0.151 on flat fields, peak preservation 0.99 vs 0.91 on isolated blobs). "volume" warm-start re-fits each merged level against the source volume itself (a full fit_gaussian_splats() pass seeded by the merge) — the highest-fidelity option (+5–12 dB over the merge on real microscopy, see volume_refit); requires volume. With barrier dims (coarsen_dims set) each barrier group is re-fitted against its OWN slice of the volume, in the coarsened dims only — see volume_regions for why the barrier axis is sliced away rather than held still. Each level keeps whichever of {merge seed, re-fit} renders closer to the volume, so it is never worse than the merge. The volume has no label channel, so categorical groups sharing the same coordinate barriers each run a separate full re-fit against the same crop; this can multiply work by the class count, and the guard may discard those re-fits. "none" (default) keeps the merge output.

  • refine_iters – Adam steps per refined level (refine="l2") / fit iterations per re-fitted level (refine="volume"). None (default) resolves to the engine’s own config default — 120 for l2 (L2RefineConfig), 300 for volume (VolumeRefitConfig).

  • volume – The source volume (full resolution, same voxel coordinate frame as the splats) that refine="volume" fits against. Required for — and only meaningful with — that mode. Only ever sliced, never coerced whole, so a lazy store (a zarr array) stays lazy: a 253-timepoint 407x2048x2048 uint16 timelapse is 431 GB while one timepoint is 3.4 GB.

  • volume_axes – volume_axes[i] is the volume axis holding center dim i. None (default) means the identity, which is what a whole-volume 3D re-fit has always assumed. A stacked timelapse needs it: Luxar puts spatial dims first and the stacked axis LAST, while the source array is typically (t, z, y, x) with time FIRST.

  • image_min – Normalization level removed by the input fit. When omitted, it is read from data.stats; per-part recipe callers pass it explicitly because converting a bare tree node to GSplatData has no top-level stats.

  • volume_box – Per-coarsened-dim (low, high) bounds restricting the re-fit to one spatial tile, for the per-part (adaptive) caller. The re-fit then sees only that tile’s crop, and a re-fit that moves a centre out of the tile is rejected in favour of the merge — the viewer frustum-culls by part bounds, so an escapee would silently stop being drawn.

  • device – "auto" (default), "cpu", "cuda", "mps", or a torch.device.

  • seed – Seeds the L2-refine minibatch pair sampler when refine="l2" (a local torch.Generator; global torch RNG untouched). Otherwise accepted for API stability only — the Morton warm start and the synchronous Lloyd pass are deterministic.

  • coarsen_dims – Center-column indices that coarsening is allowed to cluster/merge over. The complementary dims become hard grouping boundaries: splats are partitioned by their exact coordinate in those barrier dims and each group is reduced independently, so a coarse splat never blends across a barrier value (e.g. a categorical coloring axis, time, or channel). label_ids are also exact barriers whenever present. None (default) coarsens over all center dims (the historical behavior). Passing all dims is equivalent to None. Because every non-empty group keeps >= 1 representative, the coarsest level has at least as many splats as there are combined barrier groups.

  • verbose – Per-level Arbol logging.

  • quality_stamps – Measure each level’s approximation quality against the finest content (closed-form mixture L², lod/quality.py) and stamp quality + reference_energy into every level’s stats — the Q of the viewer’s committed quality Q·e(k). reference_energy is the FINEST content’s total self-energy (constant across the group), so partition-of-lod aggregation weighs every tile by its region’s content regardless of which level the tile displays. Default False at this primitive layer (the measurement costs seconds per level); the RECIPE/CLI pipeline enables it by default — stamped artifacts are its product, speed-sensitive library callers opt in.

  • quality_max_pair_splats – Pair-term subsampling threshold for the quality measurement (see mixture_quality()).

Returns:

A matrix-shaped dataset with n_substitutive = levels + 1 and a single additive sub-LOD per substitutive level (the finest at substitutive_levels[0]).

Return type:

GSplatData

Raises:

ValueError – If compression_factor < 2, levels < 1, or method is not recognised.

Additive Levels-of-Detail for Gaussian splats.

Implements the additive-LOD algorithms from the supplementary document additive_lod (luxar-paper/supp_doc/additive_lod): given a fitted GSplatData, compute a permutation that orders the splats so the prefix sum approximates the full scene at every intermediate $k$, and split the ordered set into n_lods levels.

Algorithms (additive_lod §3-4)

  • random — uniform permutation; baseline.

  • amplitude — sort by peak amplitude $a_i$, descending.

  • mass — sort by integral mass $m_i propto a_i,|Sigma_i|^{1/2}$.

  • self_energy — sort by $L^2$ self-energy $|phi_i|^2 propto a_i^2,|Sigma_i|^{1/2}$.

  • spectral — sort by $|u_1[i]|$, leading eigenvector of the Gram matrix.

  • greedy — submodular greedy / matching pursuit. $(1-1/e)$ optimal at every prefix simultaneously (Nemhauser–Wolsey–Fisher 1978); empirically $geq 99.9%$ of the exhaustive optimum on dense-overlap instances.

Modifiers (compose with EVERY method above)

  • slice_dims — interleave_order_across_slices() re-emits the chosen ordering round-robin across the distinct coordinates of the named centre columns, so every PREFIX is slice-even. On a node the viewer SLICES (a hidden time/channel dimension) a rung is sized against the whole node but only one coordinate is ever on screen, so a global contribution-ordered prefix starves the sparse coordinates; this gives each of them an equal ABSOLUTE budget instead. Deterministic (no seed) and idempotent. Not an ordering method — it is applied after one, and the within-coordinate order stays whatever the method produced.

The greedy path uses a sparse Gram matrix built via Mahalanobis truncation at the dataset’s own truncation_radius (the support it was fitted and is rendered at) + k-d-tree pruning (Algorithm 4.4 in the supp doc), keeping memory at $O(mathrm{nnz}(mathbf{G}))$. At $N leq 2000$ a dense Gram + scan-greedy is faster than the heap-based lazy greedy due to Python overhead (supp doc §4.3); we switch automatically.

luxar.gsplats.lod.additive.resolve_additive_method(method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'], n: int) → Literal['greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'][source]

Resolve method for n splats, handling the "auto" sentinel.

auto → greedy when n <= _AUTO_ADDITIVE_MAX_N (high quality and affordable at small N), else self_energy (avoids the O(nnz) sparse-Gram build that greedy/spectral need, which blows up with overlap density on large inputs). A concrete method passes through unchanged.

luxar.gsplats.lod.additive.resolve_truncation_sigmas(truncation_sigmas: float | None, data: GSplatData) → float[source]

Resolve the σ multiplier used for Gaussian truncation, honouring the data.

None (the default everywhere on the LOD path) means “use the dataset’s own truncation_radius” — the support the splats were fitted at and are rendered at. The ladder used to hard-code 3.0, so a dataset fitted at the canonical DEFAULT_TRUNCATION_RADIUS (2.75) was pruned at a support it never had. getattr with that same constant as fallback mirrors the defensive read in GSplatData.principal_radii (gsplats/_data/metrics.py): it covers the (structural) case of a data-like object that exposes no radius at all, and — since getattr swallows any AttributeError, including one raised inside the property (truncation_radius → additive_sublods[0]) — a mis-wired object too, which prunes at the constant rather than failing.

An explicit value is checked here, locally: this σ is a CPU pruning cutoff (which pairs enter the sparse Gram), not a render uniform, so it carries no float32/shader bounds — any finite positive value is meaningful. Only a degenerate cutoff is rejected, because it poisons the per-splat truncation radii that feed the k-d-tree pair search and the Gram entries (0 → all zero, negative → negative, NaN/inf → NaN/inf radii). A legitimately tiny σ is accepted on purpose: it merely yields a diagonal-only Gram, degrading the greedy ordering toward the score-only one rather than being an error.

luxar.gsplats.lod.additive.BreakpointSpec

Breakpoint specification for the additive ladder. String forms: "equal-count" (n_lods equal levels) and "stream:<c>" (geometric cumulative cuts [c, 2c, 4c, …, N] — a bandwidth-derived first chunk that doubles; resolved per-N inside _resolve_breakpoints(), so the same spec adapts to every part/level size). List forms: list[int] explicit cumulative counts; list[float] cumulative energy fractions in (0, 1].

The cut geometry itself lives in luxar.utils.lod_breakpoints so all three geometries derive identical cuts from an identical spec; the names below are re-exported here because they are part of this module’s public surface.

alias of str | Sequence[int] | Sequence[float]

luxar.gsplats.lod.additive.clamp_counts_breakpoints(breakpoints: str | Sequence[int] | Sequence[float], n: int) → str | Sequence[int] | Sequence[float][source]

Clamp explicit counts: breakpoints to a part/level of n splats.

Per-part and per-level ladders (BSP parts, pyramid levels) have differing N; a fixed counts: list whose largest cut exceeds a small part would otherwise abort the whole build via _resolve_breakpoints’s strict “largest breakpoint exceeds N” check (which is the RIGHT behavior for a direct whole-dataset build, where the user knows N). This helper keeps the cuts below n and lets _resolve_breakpoints append the final n; non-count specs (strings, energy fractions) pass through unchanged — they are already size-adaptive.

luxar.gsplats.lod.additive.validate_counts_breakpoints(breakpoints: str | Sequence[int] | Sequence[float], n: int) → None[source]

Strictly validate explicit counts: breakpoints against the FULL n.

The whole-dataset companion of clamp_counts_breakpoints(): clamping is right for an individual part/level whose N the user cannot know, but the spec itself must still be sane for the dataset as a whole — a largest count exceeding the full N is a typo (e.g. counts:1000000 on a 50 k dataset) and must abort loudly, exactly like a direct whole-dataset make_additive_lod() build does via _resolve_breakpoints. Callers that clamp per part/level call this ONCE up front with the union / finest-level size. Non-count specs pass through (validated downstream).

luxar.gsplats.lod.additive.interleave_order_across_slices(data: GSplatData, order: ndarray, slice_dims: Sequence[int]) → ndarray[source]

Re-emit order round-robin across slices, so every prefix is slice-even.

A node the viewer SLICES (any non-displayed dimension) shows one hidden coordinate at a time, but an additive rung is sized against the WHOLE node. A contribution-ordered prefix therefore concentrates wherever the signal is and the sparse coordinates get almost nothing: the NEXRAD supercell’s 82-scan stack (817,989 splats; per-scan min 562, p05 774, median 10,499, max 19,237) shipped an absolute breakpoints="stream:20000" first rung whose 5th-percentile scan held 4 splats — an empty screen during playback, and two failures in scripts/check_demo_ladders.py (#2485).

Grouping the elements of order by their distinct combination of the slice_dims centre columns and emitting one per group per pass turns that global budget into an EQUAL ABSOLUTE per-slice budget. Precisely: after $R$ completed passes the prefix holds $min(n_i, R)$ elements of every slice $i$, so a slice SMALLER than the budget is carried WHOLE and a large one is capped — which is exactly the shape check_demo_ladders.py’s absolute first-paint arm asks for. Ties inside a pass are broken by position in order, so the pass order is stable.

That guarantee has a PRECONDITION worth stating: a rung must be at least as large as the slice count, or it cannot reach every slice at all. A rung smaller than $S$ does not even complete its first pass, and since within-pass ties break by position in order the coordinates left with NOTHING are the faintest ones — measured on 500 slices of 40 splats with breakpoints=[200], 300 coordinates got zero. Nowhere near a hazard for the demo this was built for (204,497 against 82 slices), but a ladder whose first rung is smaller than its hidden-axis cardinality is not made even by this.

Two properties worth relying on:

  • Deterministic — no seed, and no distributional argument. Sizing the ladder by hand cannot get here: on the stack above the 250-element floor needs ~32% of the sparsest scan, and a uniform method="random" permutation only makes the per-slice share proportional IN EXPECTATION — measured over seeds 0-7, n_lods=3 cleared the floor 5 times in 8 (p05 243-277 against a floor of 250) and n_lods=4 never did (186-205).

  • Idempotent — re-interleaving an already-interleaved order returns it unchanged, because the within-group order (and hence every within-group rank) is untouched. Both the authoring door and make_additive_lod()’s Gram branch could in principle apply it.

The within-slice order is still whatever the base method produced, so under the default method="auto" each coordinate keeps painting bright-core- first rather than evenly thin.

slice_dims indexes RAW, PRE-dim_order centre columns, and that is the likeliest way to get this wrong. The ladder is built in core/group/gsplats_pipeline/from_data.py ABOVE the apply_dim_order_* pass that lod_dispatch runs, so these are the columns of the array the caller handed in — NOT the scene’s post-dim_order dimension positions. They coincide for the NEXRAD supercell only because its dim_order leaves time last in both frames. An author who reads off the scene’s Dimensions list instead gets a different column, and per the next paragraph that is a near-no-op. A high-cardinality diagnostic below warns about this likely pre-/post-dim_order mixup without rejecting legitimate small slices.

The columns must also be genuinely DISCRETE — a stacked time/channel axis, where coordinates repeat. Pointed at a continuous one, nearly every key is distinct, nearly every rank is 0, and the lexsort reproduces order: functionally a no-op. NOT a bit-identical one, though, so this must not be used as an equality assertion — real centres do collide, and each collision demotes one element a pass later, which shifts the whole tail behind it. Measured on 8 cached NEXRAD frames (5,937 splats, 5,907 distinct values in column 0), slice_dims=[0] left the first 20 positions untouched and moved 5,901 of 5,937 overall; one deliberate collision among 2,000 float32 samples moved 48. Reachable by composition and not only by typo: a lod_group=dict(coarsen_dims=[0, 1, 2, 3]) — coarsening OVER the stacked axis — turned 3 exact time coordinates into 35 fractional ones on the coarse level, and resolve_additive_axis_gsplats() applies one slice_dims to every substitutive level, giving a slice-even finest level and silently uneven coarse ones. The default Auto coarsening (hidden axis as a hard barrier) is safe.

Parameters:
  • data (GSplatData) – The dataset order indexes; only its centers are read.

  • order (np.ndarray) – A length-N integer permutation, as returned by compute_additive_order(). Validated for shape, dtype and range; duplicate entries are the caller’s responsibility (see _validate_interleave_order()).

  • slice_dims (sequence of int) – RAW, pre-dim_order centre columns whose distinct combinations define a slice. No default — see _validate_slice_dims().

Returns:

A permutation of the same elements, slice-even at every prefix.

Return type:

np.ndarray of shape (N,), dtype int64

luxar.gsplats.lod.additive.compute_additive_order(data: GSplatData, method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto', *, truncation_sigmas: float | None = None, max_n_dense: int = 2000, seed: int | None = None, reveal_center: Sequence[float] | None = None, spatial_dims: Sequence[int] | None = None, slice_dims: Sequence[int] | None = None) → ndarray[source]

Compute an additive ordering permutation for the splats in data.

Parameters:
  • data (GSplatData) – Fitted (single- or multi-LOD) gsplat dataset. Operates on the flattened concatenation across LODs.

  • method (str) – One of auto, greedy, self_energy, mass, amplitude, spectral, random, radial. auto (the default) resolves to greedy at small N and self_energy above _AUTO_ADDITIVE_MAX_N — see resolve_additive_method(). See module docstring for details.

  • truncation_sigmas (float, optional) – Mahalanobis cutoff used for sparse-Gram pruning. Defaults to the dataset’s own truncation_radius — the support the splats were fitted at and are rendered at. Only relevant for greedy and spectral.

  • max_n_dense (int) – For greedy, build a dense Gram and use scan-greedy when $N leq$ this threshold. Above it, build a sparse Gram and use lazy-greedy. Default 2000 (per supp doc §4.3).

  • seed (int, optional) – Random seed for method='random'.

  • reveal_center (sequence of float, optional) – Centre of the shells for method='radial'. Defaults to the spatial bounding-box centre — NOT the scene origin, so a dataset far from the origin still grows from its own middle. One coordinate per spatial axis.

  • spatial_dims (sequence of int, optional) – Centre columns the radial distance is measured over. Defaults to the non-degenerate (real-extent) axes, which excludes a stacked time or channel axis. Ignored by every other method.

  • slice_dims (sequence of int, optional) – RAW, pre-dim_order centre columns whose distinct combinations the viewer SLICES (a hidden time / channel axis) — NOT the scene’s post-dim_order dimension positions, which are a different frame of reference. When given, the ordering chosen by method is re-emitted round-robin across those slices by interleave_order_across_slices(), so every prefix carries an equal ABSOLUTE budget per slice instead of a global contribution-ordered prefix that starves the sparse ones. Composes with EVERY method — it is a modifier, not a method. None (the default) leaves the order untouched; there is no default column set, since a standalone GSplatData has no display information to derive one from.

Returns:

order[k] is the original index of the splat at rank $k$.

Return type:

np.ndarray of shape (N,), dtype int64

luxar.gsplats.lod.additive.additive_rung_count(n: int, n_lods: int = 4, breakpoints: str | Sequence[int] | Sequence[float] = 'equal-count') → int | None[source]

How many rungs would this spec leave on a leaf of n splats? (#1632)

A cheap, ordering-free query. The exactness comes from SHARING the cut resolver make_additive_lod() uses (_resolve_breakpoints()): the answer is counted off the very cuts that build would consume, so for equal-count / stream: / explicit counts it is the number of AdditiveSubLOD objects that build would EMIT, not a re-derivation free to drift. The loop below also MIRRORS that build loop’s if end <= prev: continue de-duplication — but as a mirror only, so the two cannot diverge if a future cut resolver ever emits a duplicate. It is not a live filter and is not what makes the count exact: every non-energy path of _resolve_breakpoints() returns strictly-increasing positive cuts, so neither loop can skip one today. Pinned against make_additive_lod(...).n_additive_sublods per breakpoint kind in tests/test_additive.py.

It exists because the callers that must decide whether a ladder will exist cannot afford to build one. The live one is the file/graft door’s partition-vs-ladder gate (_reject_a_partition_beside_a_stored_ladder(), via the spec-level wrapper resolve_additive_rungs()): partition= and a multi-rung ladder are mutually exclusive, and that gate runs before graft_gsplat_node builds a kind=partition wrapper it would otherwise strand. Presence of the additive_lod= kwarg is not the question — {"n_lods": 1} resolves to one rung and partitions perfectly well, while {"method": "radial"} carries no n_lods to read and falls to the n_lods=4 default — four rungs on any leaf of >= 4 splats, and n on a smaller one, since equal-count cuts clamp to the leaf’s own size.

Returns None for UNKNOWN, never raises:

  • kind == "energy-fractions" or "equi-energy" — those cuts need the ordering and the energy curve, i.e. exactly the expensive half this query exists to avoid.

  • anything _resolve_breakpoints() would reject (a non-positive n_lods, an unknown breakpoints string, a mixed list, a counts list exceeding n, …). Swallowing the fault is deliberate: this is a QUERY, and the real build must stay the thing that reports it, at its own site, with its own message. None says nothing about the INPUT, only that this spec is unreadable here, so a caller that cannot act on it should fall back to what it already knows rather than assume “no ladder”.

Note the converse, for the gate: a fault this COUNT cannot see — a bad method, a stray substitutive_level key, anything past the cut resolver — makes no difference to the number, so a caller refusing on the count MASKS it rather than letting the builder report it. Same trade in the other direction, and an acceptable one where the caller’s own conflict is the more fundamental fault and nothing is written either way.

n <= 0 returns 1, mirroring make_additive_lod()’s empty-leaf branch, which emits exactly one sub-LOD labelled lod_method="none".

luxar.gsplats.lod.additive.make_additive_lod(data: GSplatData, n_lods: int = 4, *, method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto', breakpoints: str | Sequence[int] | Sequence[float] = 'equal-count', truncation_sigmas: float | None = None, max_n_dense: int = 2000, seed: int | None = None, substitutive_level: int | None = None, reveal_center: Sequence[float] | None = None, spatial_dims: Sequence[int] | None = None, slice_dims: Sequence[int] | None = None) → GSplatData[source]

Permute and split a fitted gsplat dataset into a multi-LOD ladder.

The result is a GSplatData with n_lods (or as resolved by breakpoints) AdditiveSubLOD levels on the selected substitutive level. additive_prefix(k) returns the valid additive prefix of size \(\sum_{\ell \leq k} N_\ell\) for that level.

Parameters:
  • data (GSplatData) – Fitted gsplat dataset. May be multi-substitutive: the substitutive_level argument (default = default substitutive level) selects which level receives the new additive ladder. Other substitutive levels are carried over verbatim.

  • n_lods (int) – Number of LOD levels when breakpoints='equal-count'. Ignored when breakpoints is a list or 'stream:<c>' (those determine the level count themselves).

  • method (str) – Ordering method (see compute_additive_order()).

  • breakpoints (:py:class:``’equal-count’:py:class:``, :py:class:``’stream:<c>’:py:class:``, or list of int / float) –

    • 'equal-count': n_lods levels of (nearly-)equal size.

    • 'stream:<c>': geometric streaming ladder — cumulative cuts [c, 2c, 4c, …, N] sized so the first chunk is c splats (bandwidth-derived via streaming_chunk_splats()), then doubling. Resolved against each call’s own N (per part / per substitutive level), silently clamped for small N (never raises, unlike explicit counts), capped at DEFAULT_STREAM_MAX_LEVELS levels.

    • 'equi-energy:<n>': n rungs at EQUAL shares of cumulative self-energy along the ordering — the first rung is the few heaviest splats, later rungs are fatter in count for the same light — with any increment above DEFAULT_MAX_ADDITIVE_COMMIT split into capped steps (luxar.utils.lod_breakpoints.equi_energy_cuts()). Pair with a contribution-first method (self_energy, the large-N default) — under random the rungs are still equal in energy but there is no ordering to front-load.

    • list[int]: explicit cumulative splat counts per level.

    • list[float] in $(0, 1]$: cumulative energy fractions; the smallest $k$ at which the cumulative-utility curve crosses each fraction is used as the cutpoint. For greedy / spectral orderings that build a Gram matrix the curve is the residual-energy curve; for score-ordered methods (self_energy / mass / amplitude / random) it is the O(N) self-energy cumulative, so cuts land where the viewer’s own $e(k)$ quality stamp reads the requested fraction.

  • truncation_sigmas (float, optional) – $sigma$ multiplier for sparse-Gram pruning. Defaults to the dataset’s own truncation_radius.

  • max_n_dense (int) – Threshold below which greedy uses a dense Gram + scan-greedy.

  • seed (int, optional) – Random seed for method='random'.

  • substitutive_level (int, optional) – Index of the substitutive level to build the ladder for. Defaults to data.default_substitutive.

  • reveal_center (sequence of float, optional) – method='radial' only — centre of the concentric shells. Defaults to the spatial bounding-box centre (NOT the scene origin, so a dataset far from the origin still reveals from its own middle).

  • spatial_dims (sequence of int, optional) – method='radial' only — the centre columns the shell distance is measured over. Defaults to the non-degenerate axes, so a stacked time/channel axis cannot become a shell dimension.

  • slice_dims (sequence of int, optional) – RAW, pre-dim_order centre columns the viewer SLICES (a hidden time / channel axis) — NOT the scene’s post-dim_order dimension positions. When given, the ordering is re-emitted round-robin across those slices (see interleave_order_across_slices()), so every rung carries an equal ABSOLUTE budget per slice rather than a global prefix that starves the sparse ones (#2485). A modifier: it composes with every method, and the energy_fraction_cum stamps are computed from the interleaved order, so the viewer’s committed e(k) describes the prefix actually written — NODE-GLOBALLY, which is the only granularity that stamp has: e(k) is one number per rung and the viewer’s 1/max(e, 0.1) compensation is applied uniformly across hidden coordinates, so a small slice that slice-evenness has already loaded COMPLETELY is still brightened (measured, rung 0 stamps e = 0.408 interleaved against 0.684 plain — a 2.45x boost on an already-complete slice). This is NOT confined to a kind=lod group: applyLodFade has a second caller in the viewer’s scene/density-guard.ts, driven by the projected-density tracker over the whole scene graph for any blendable data mesh, and both the guard and the compensation default ON — so a BARE multi-additive leaf is in scope too once the guard steps it. Small in practice; on the NEXRAD node it is a 1.108x brightening (that store stamps e(0) = 0.9024), applied to the 12 of 82 scans this already loads whole as much as to the rest. Note also that interleaving FLATTENS the cumulative-energy curve, so energy-fraction breakpoints resolve to materially larger first rungs — measured on 1,580 splats over 4 slices, [0.5, 0.9, 0.99, 1.0] cuts at [222, 749, 1116, 1580] plain and [521, 1128, 1492, 1580] interleaved. The requested fractions are still delivered and the rungs are still slice-even; it is the first-paint COST that moves. None (the default) leaves the order untouched.

Returns:

A matrix-shaped GSplatData with the same n_substitutive as data; the selected level’s additive sub-LODs form the new ladder, other substitutive levels are passed through unchanged.

Return type:

GSplatData

Substitutive Levels-of-Detail for Gaussian splat datasets.

Substitutive LOD is the second of the two LOD axes for Gaussian splats (complementing the additive axis in luxar.gsplats.lod.additive). Each level synthesises $Mlev = N/KK$ representative splats that replace the finer level — real geometry / memory compression rather than just a streaming order. The math derives from the supplementary document luxar-paper/supp_doc/substitutive_lod/substitutive_lod.tex; in particular Algorithm 4.3 (cost-increment Lloyd) and the \(L^2\)-optimal $K$-wise merge (Prop. 2.2).

Public API

make_substitutive_lod()

Build a level-by-level hierarchy [level_0=data, level_1, ..., level_L] by iterating the partition-and-merge operator $mathcal{R}_K$.

Algorithms (selected via the method argument):

  • "auto" (default): resolved per level from the level’s input count — "greedy" at or below 5000 splats (highest quality and fast there), "kmeans_lloyd" above (greedy is ~50-100x slower at large N). For a large dataset the coarse early levels use kmeans_lloyd and the small later levels switch to greedy.

  • "kmeans_lloyd" (large-N workhorse): a Morton (Z-order) space-filling-curve warm start → cost-increment Lloyd refinement. The warm start sorts splats along the curve and chunks the sorted sequence into M = N/K contiguous, balanced, spatially coherent bins in O(N log N); Lloyd then reassigns splats to the template they best project onto. Per supp doc Experiment C, the refined ladder dominates amplitude culling on real anisotropic data.

  • "kmeans": warm start only, no Lloyd refinement — the raw Morton-chunk partition. Fast and already high quality; the _lloyd variant typically adds a few dB of PSNR.

  • "greedy": bottom-up Runnalls-style merging using closed-form pairwise merge cost. Quality-leading at small $N$ and small $K$. Implemented as a lazy-deletion priority queue with incremental neighbour updates and batched pair-cost evaluation — ~$mathcal{O}(N k log(N k))$, a constant-factor heavier than the Morton warm start but usable well beyond the former $mathcal{O}(N^2)$ full re-scan.

  • "greedy_lloyd": greedy warm start + Lloyd refinement.

The method names retain their kmeans prefix for API stability; the warm start is now the O(N log N) Morton partition rather than a global k-means++ (whose O(M·N) = O(N²/K) initialisation was intractable once M = N/K reached tens of thousands — the substitutive regime). Both the warm start and the vectorised Lloyd pass avoid Python per-splat / per-bin loops: every per-bin quantity is a segment reduction (torch.Tensor.index_add_()) keyed by the bin assignment, running on PyTorch (CUDA / MPS / CPU; device='auto'). Per-bin merge math (moment matching, $L^2$-optimal amplitude, residual energy) lives in luxar.gsplats.lod._kernels and is shared with the additive axis.

Coverage inflation (coverage_inflation, default 3.0): every method finishes with a merge whose covariance is the bin’s moment match (intra + inter spread). For balanced spatial bins of pitch d the moment-matched σ is ≈ d/√12 ≈ 0.29 d — well below the σ ≳ d/2 a lattice of Gaussians needs to sum flat — and, because the Morton warm start quantises bin boundaries onto a global dyadic grid, the coverage dips align into coherent axis-aligned planes: a very visible grid pattern at every coarse level. The fix widens the inter-center term only (Σ_out = intra + β·inter; β=3 turns d²/12 into (d/2)²) with a mass-preserving amplitude rescale, and is the exact fixed point of the level recurrence so it stays calibrated at every depth. Set coverage_inflation=1.0 for the historical pure moment match.

L2 refinement (refine="l2", opt-in): after each merge, the level is Adam-optimized against its fine input under the closed-form mixture L² (_substitutive.refine) — the merge (with its β=3 inflation) becomes the optimizer seed, and the refit takes over the exact calibration. The refit is never worse than the merge in its trusted metric, keeps total mass pinned to the fine mixture’s (no brightness pop across levels), and freezes barrier dims under coarsen_dims grouping. Exact label_ids are an additional categorical barrier: representatives never cross class ids.

The returned value is a single GSplatData with n_substitutive = levels + 1 and M_i = 1 per substitutive level (one additive sub-LOD each). Saved to disk, this becomes a single v3.4 node-tree .gsplats.zarr (a kind=lod group with one child per level — see luxar.gsplats.tree).

luxar.gsplats.lod.substitutive.RefineName

Post-merge per-level refinement of the substitutive reduction. "l2" Adam-optimizes each merged level against its fine input under the closed-form mixture L² (see _substitutive.refine). "volume" warm-start re-fits each merged level against the source volume itself (see volume_refit; requires the volume argument).

alias of Literal[‘none’, ‘l2’, ‘volume’]

luxar.gsplats.lod.substitutive.resolved_merge_coarsen_dims(coarsen_dims: Sequence[int] | None, ndim: int | None) → list[int][source]

The dims a substitutive reduction ACTUALLY coarsens over, spelled EXPLICITLY.

The single resolution shared by every path that WRITES the coarsen_dims stamp (make_substitutive_lod() here, decimate()’s merge family, and the batch-fit merge per-part record) — one function so a fourth one cannot quietly publish the same choice a second way.

Three substitutive producers write NO stamp at all and are therefore not reached by this: lod --recipe adaptive / --recipe overview and fit --recipe levels build their pipeline/ group out of their INPUT’s stats rather than out of the recipe they ran, so the reduction’s own choice — an explicit --coarsen-dims included — never lands on disk, and an absent key reads exactly like the null below. Routing those through here means plumbing a composed recipe’s parameters into its record, which is a separate change tracked on #1600.

Always a non-empty literal list, never None — including for the coarsen-everything case (the coarsen_dims=None default, and a request naming every dim, which _normalise_coarsen_dims() collapses to the same thing). An empty explicit request is invalid: its empty complement would claim every axis as a barrier. The two valid coarsen-everything spellings are NOT interchangeable on disk: _barrier_from_coarsen_dims() cannot tell a written null from an absent key, so both read as “no provenance” and fall through to detect_barrier_dims auto-detection — a GUESS about the result’s coordinates, not “no barrier”.

What that guess costs depends on the data, and it was measured rather than asserted (#1600 review). Auto-detection re-imposes the very barrier this merge blended over exactly when the reduction leaves the stacked axis’ grid INTACT: on 200 4D splats over three timepoints spaced 1000 apart against a spatial extent of 100, no cluster ever spans two timepoints, the coordinates stay integral, and the fallback hands back [3]. On a fine grid (step 1) the merge averages those coordinates away, the axis stops looking integral, and the fallback finds nothing — but only on the levels it actually merged, so a ladder came out with a per-level MIXTURE ([[], [], [3]]: the finest level is the unreduced input and keeps its integral grid). [0, …, d-1] asserts the empty complement outright on either grid and on every level, i.e. the no-barrier layout the reduction actually earned.

ndim is only read to EXPAND a None request, so a caller that always names its dims may pass None for it rather than a stand-in width — a made-up width is the one thing this must not appear to assert. The two Nones together are a caller bug, not a coarsen-everything answer, and raise instead of returning the empty list (whose complement is every axis a barrier — the splat-dropping direction).

luxar.gsplats.lod.substitutive.make_substitutive_lod(data: GSplatData, *, compression_factor: int = 4, levels: int = 3, method: Literal['auto', 'kmeans', 'kmeans_lloyd', 'greedy', 'greedy_lloyd'] = 'auto', lloyd_iterations: int = 5, candidate_bins_k: int = 12, color_weight: float = 0.0, coverage_inflation: float = 3.0, conserve_mass: bool = True, amplitude: Literal['l2', 'mass'] = 'l2', refine: Literal['none', 'l2', 'volume'] = 'none', refine_iters: int | None = None, volume: ndarray | None = None, volume_axes: Sequence[int] | None = None, image_min: float | None = None, volume_box: Sequence[tuple[float, float]] | None = None, device: str | torch.device | None = 'auto', seed: int | None = None, coarsen_dims: Sequence[int] | None = None, verbose: bool = False, quality_stamps: bool = False, quality_max_pair_splats: int = 2000000) → GSplatData[source]

Build a substitutive-LOD hierarchy.

Parameters:
  • data – Source dataset. If multi-substitutive, only its default substitutive level is reduced (additive sub-LODs at that level are flattened first).

  • compression_factor – Per-level branching factor $K$. Each level ℓ has ceil(N / K^ℓ) splats.

  • levels – Number of coarser levels to produce. The returned object has levels + 1 substitutive levels (the original at index 0).

  • method – Partition algorithm, or "auto" (default). "auto" resolves per level: "greedy" when the level’s input has <= 5000 splats (highest quality, and fast there) and "kmeans_lloyd" above (greedy is ~50-100x slower at large N). See module docstring for the individual methods.

  • lloyd_iterations – Maximum number of cost-increment Lloyd passes per level (only for "kmeans_lloyd" / "greedy_lloyd"). The loop exits early as soon as a pass fails to improve the projection energy.

  • candidate_bins_k – Number of Morton-curve neighbours whose current bins are the move candidates for each splat during Lloyd refinement. Tighter k → faster, slightly worse quality.

  • color_weight – Opt-in chromatic penalty in the partition cost. 0 (default) keeps the historical spatial/intensity-only partition byte-for-byte. Values above zero apply an exp(-color_weight * distance²) affinity, but the distance is pair-to-pair for greedy and member-to-centroid for Lloyd, so the useful scale is method-specific: roughly 0.1 to 1 for greedy and 1 to 10 for Lloyd spans a soft-to-strong hue preference. Because method="auto" may switch per level, pin an explicit method when consistent chromatic strength matters. RGB is normalized by brightness; pure black maps to neutral chromaticity, and alpha is deliberately excluded while representative alpha is composed in optical-depth space. This expert knob is API-only today.

  • coverage_inflation – Inflation factor β >= 1 applied to each representative’s inter-center spread (Σ_out = intra + β·inter) with a mass-preserving amplitude rescale. Pure moment matching gives the balanced bins σ ≈ pitch/√12 — too narrow for neighbouring representatives to sum flat, which renders as a strong periodic grid ripple along the shared Morton-cell boundaries. The default β=3 widens exactly the inter term to σ ≈ pitch/2 (flat-sum threshold) and is the exact fixed point of the level recurrence, so the calibration holds at every level. 1.0 disables (historical pure-moment-matching behaviour). Trade-off: coarse levels look slightly smoother; each splat’s integral (X-ray projection) is preserved exactly. With refine="l2" the inflation is demoted from final answer to optimizer seed: the refit takes over the exact flat-sum calibration.

  • conserve_mass – Rescale each reduced level’s amplitudes by one global factor so its total mass over the coarsened dims equals its fine input’s (per barrier group under coarsen_dims). The per-bin L²-optimal amplitude is not mass-preserving (3–17 % loss per level measured, content-dependent), and that mass is the DC an additive render integrates — uncorrected it shows as a brightness pop at every LOD switch. Default True; False restores the raw per-bin amplitudes. The rescale is skipped (with a warning) when the implied factor falls outside [0.1, 10] — a numerically degenerate coarsened-dim mass, where “conserving” it would blow the amplitudes up instead.

  • amplitude – Per-bin merged-amplitude rule. "l2" (default) is the L²-optimal projection amplitude — the right choice for fitted volumetric gsplats. "mass" makes every bin exactly mass-preserving (a = Σ member a·|det L| / |det L_out|, on the final inflated covariance): per-bin colored light is then conserved together with the bin-mass-weighted mean colors, which is what the lifted points/lines LOD path uses to keep brightness/hue coherent across levels (the beads are a stroke stand-in, not a density to L²-fit). Under "mass" the global conserve_mass rescale is a no-op by construction (kept as a safety net). Exactness note: with barrier groups the conserved per-bin quantity is the full-determinant mass; the sliced (coarsened-dims-only) mass coincides when member barrier widths are equal within a bin — true for lifted isotropic beads.

  • refine – Post-merge per-level refinement. "l2" Adam-optimizes each merged level’s (mu, Σ, a) against that level’s fine input under the closed-form mixture L² (sparse pair lists, trusted checkpoints, total mass pinned to the fine mixture’s — see _substitutive.refine). Never worse than the merge in the trusted metric; substantially higher fidelity (prototype: rel-L² 0.089 vs 0.151 on flat fields, peak preservation 0.99 vs 0.91 on isolated blobs). "volume" warm-start re-fits each merged level against the source volume itself (a full fit_gaussian_splats() pass seeded by the merge) — the highest-fidelity option (+5–12 dB over the merge on real microscopy, see volume_refit); requires volume. With barrier dims (coarsen_dims set) each barrier group is re-fitted against its OWN slice of the volume, in the coarsened dims only — see volume_regions for why the barrier axis is sliced away rather than held still. Each level keeps whichever of {merge seed, re-fit} renders closer to the volume, so it is never worse than the merge. The volume has no label channel, so categorical groups sharing the same coordinate barriers each run a separate full re-fit against the same crop; this can multiply work by the class count, and the guard may discard those re-fits. "none" (default) keeps the merge output.

  • refine_iters – Adam steps per refined level (refine="l2") / fit iterations per re-fitted level (refine="volume"). None (default) resolves to the engine’s own config default — 120 for l2 (L2RefineConfig), 300 for volume (VolumeRefitConfig).

  • volume – The source volume (full resolution, same voxel coordinate frame as the splats) that refine="volume" fits against. Required for — and only meaningful with — that mode. Only ever sliced, never coerced whole, so a lazy store (a zarr array) stays lazy: a 253-timepoint 407x2048x2048 uint16 timelapse is 431 GB while one timepoint is 3.4 GB.

  • volume_axes – volume_axes[i] is the volume axis holding center dim i. None (default) means the identity, which is what a whole-volume 3D re-fit has always assumed. A stacked timelapse needs it: Luxar puts spatial dims first and the stacked axis LAST, while the source array is typically (t, z, y, x) with time FIRST.

  • image_min – Normalization level removed by the input fit. When omitted, it is read from data.stats; per-part recipe callers pass it explicitly because converting a bare tree node to GSplatData has no top-level stats.

  • volume_box – Per-coarsened-dim (low, high) bounds restricting the re-fit to one spatial tile, for the per-part (adaptive) caller. The re-fit then sees only that tile’s crop, and a re-fit that moves a centre out of the tile is rejected in favour of the merge — the viewer frustum-culls by part bounds, so an escapee would silently stop being drawn.

  • device – "auto" (default), "cpu", "cuda", "mps", or a torch.device.

  • seed – Seeds the L2-refine minibatch pair sampler when refine="l2" (a local torch.Generator; global torch RNG untouched). Otherwise accepted for API stability only — the Morton warm start and the synchronous Lloyd pass are deterministic.

  • coarsen_dims – Center-column indices that coarsening is allowed to cluster/merge over. The complementary dims become hard grouping boundaries: splats are partitioned by their exact coordinate in those barrier dims and each group is reduced independently, so a coarse splat never blends across a barrier value (e.g. a categorical coloring axis, time, or channel). label_ids are also exact barriers whenever present. None (default) coarsens over all center dims (the historical behavior). Passing all dims is equivalent to None. Because every non-empty group keeps >= 1 representative, the coarsest level has at least as many splats as there are combined barrier groups.

  • verbose – Per-level Arbol logging.

  • quality_stamps – Measure each level’s approximation quality against the finest content (closed-form mixture L², lod/quality.py) and stamp quality + reference_energy into every level’s stats — the Q of the viewer’s committed quality Q·e(k). reference_energy is the FINEST content’s total self-energy (constant across the group), so partition-of-lod aggregation weighs every tile by its region’s content regardless of which level the tile displays. Default False at this primitive layer (the measurement costs seconds per level); the RECIPE/CLI pipeline enables it by default — stamped artifacts are its product, speed-sensitive library callers opt in.

  • quality_max_pair_splats – Pair-term subsampling threshold for the quality measurement (see mixture_quality()).

Returns:

A matrix-shaped dataset with n_substitutive = levels + 1 and a single additive sub-LOD per substitutive level (the finest at substitutive_levels[0]).

Return type:

GSplatData

Raises:

ValueError – If compression_factor < 2, levels < 1, or method is not recognised.

luxar.gsplats.lod.substitutive.merge_to_count(data: GSplatData, *, n_target: int, method: Literal['auto', 'kmeans', 'kmeans_lloyd', 'greedy', 'greedy_lloyd'] = 'auto', lloyd_iterations: int = 5, candidate_bins_k: int = 12, color_weight: float = 0.0, coverage_inflation: float = 3.0, device: str | torch.device | None = 'auto', coarsen_dims: Sequence[int] | None = None) → GSplatData[source]

Merge data into n_target representatives — ONE flat level.

A single application of the partition-and-merge operator that make_substitutive_lod() iterates, exposed for callers who want a SIZE rather than a ladder. make_substitutive_lod reduces by an INTEGER per-level factor, so the counts it can land on are quantised (N/2, N/3, …) and an arbitrary request falls between two of them; here the count is the input. Everything else is shared with the ladder path — the same merge math, the same barrier-dim grouping, and the same per-group mass conservation, so the result keeps the input’s brightness instead of dimming it.

Parameters:
  • data – Source dataset (reduced from its finest content).

  • n_target – Number of representatives to produce. A request at or above the input count returns the finest content unreduced.

  • method – Partition algorithm or "auto" — see make_substitutive_lod().

  • lloyd_iterations – Lloyd refinement passes.

  • candidate_bins_k – Lloyd move-candidate neighbours per splat.

  • color_weight – Opt-in chromatic partition penalty; see make_substitutive_lod().

  • coverage_inflation – Inter-center spread inflation β (see make_substitutive_lod()).

  • device – Torch device ("auto" resolves; MPS downgrades to CPU).

  • coarsen_dims – Center-column indices merging may combine over; the rest are hard barriers. label_ids are also exact barriers whenever present. Default: all center dims.

Returns:

A flat GSplatData with at most n_target splats. It can land slightly under: the merge culls degenerate (empty / non-positive-mass) clusters, and the barrier grouping keeps at least one representative per group, which can push the count up instead.

Raises:

ValueError – If n_target < 1 or method is not recognised.

Representation recipes — assemble a fitted gsplat set into a topology.

Intent-first vocabulary (every recipe carries streaming additive ladders by default — see RecipeParams.additive_ladders), ordered by dataset scale:

  • flat — a single bare leaf. No LOD, no tiles. Tiny data / debugging.

  • stream — one leaf whose splats are ordered into a progressive (prefix-sum, “additive”) ladder: any prefix is the best preview, so the viewer paints fast and refines. Small-to-medium single-load data.

  • levels — classic coarse→fine level-of-detail: each coarser level has ~``N/K^ℓ`` merged (“substitutive”) representative splats that REPLACE the finer level, each level itself stream-laddered. Zooming across scales.

  • tiles — a spatial BSP kind=partition: off-screen tiles are culled, each visible tile streams its own ladder. Large scenes, single scale.

  • overview — one cheap coarse level for the instant far view + a tiles fine branch for close-up (unbalanced by design: detail only where you look). Huge scenes with a “see everything first” need.

  • adaptive — tiles where EVERY tile carries its own levels group: each tile culls AND picks its own detail level by its on-screen size. The most locally adaptive; the largest scenes.

These functions are pure (GSplatData in, a result out) with no Typer/IO — the CLI wrapper lives in luxar.cli.lod. They only compose the existing math builders (make_additive_lod(), make_substitutive_lod(), make_lod_pyramid(), GSplatData.to_spatial_partition()); the mechanism vocabulary (“additive” prefix ladders, “substitutive” merged levels) lives at that layer, while recipes name user intent.

Renamed (old → new): additive``→``stream, substitutive and pyramid``→``levels, partitioned``→``tiles, multiscale``→``overview, mosaic``→``adaptive. LEGACY_RECIPE_NAMES maps old spellings; the CLI rejects them with a pointer, stored batch manifests translate silently.

Two return shapes (see RecipeResult):

  • the matrix recipes (flat/stream/levels) return a GSplatData, written via GSplatData.save().

  • the composed recipes (tiles/overview/adaptive) return a GSplatNode tree, written via write_gsplats_tree().

luxar.gsplats.lod.recipes.RecipeName

The recipe vocabulary, ordered by dataset scale.

alias of Literal[‘flat’, ‘stream’, ‘levels’, ‘tiles’, ‘overview’, ‘adaptive’]

luxar.gsplats.lod.recipes.RECIPE_NAMES: tuple[str, ...] = ('flat', 'stream', 'levels', 'tiles', 'overview', 'adaptive')

Tuple form of RecipeName for CLI choices / validation.

luxar.gsplats.lod.recipes.LEGACY_RECIPE_NAMES: dict[str, str] = {'additive': 'stream', 'mosaic': 'adaptive', 'multiscale': 'overview', 'partitioned': 'tiles', 'pyramid': 'levels', 'substitutive': 'levels'}

Old → new recipe spellings (renamed 2026-07-03). The CLI rejects old names with a did-you-mean pointer; stored batch manifests translate silently via canonical_recipe_name().

luxar.gsplats.lod.recipes.canonical_recipe_name(name: str) → str[source]

Translate a legacy recipe spelling to the current one (identity for current names; unknown names pass through for the caller to reject).

luxar.gsplats.lod.recipes.MATRIX_RECIPES: frozenset[str] = frozenset({'flat', 'levels', 'stream'})

Recipes whose result is a flat GSplatData (written via .save).

luxar.gsplats.lod.recipes.COMPOSED_RECIPES: frozenset[str] = frozenset({'adaptive', 'overview', 'tiles'})

Recipes whose result is a non-matrix GSplatNode tree.

luxar.gsplats.lod.recipes.RecipeResult

A recipe builds either a flat dataset or a node-tree (see module docstring).

alias of GSplatData | GSplatLeaf | GSplatLodGroup | GSplatPartition

class luxar.gsplats.lod.recipes.RecipeParams(n_lods: int = 4, additive_method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto', reveal_center: Sequence[float] | None = None, spatial_dims: Sequence[int] | None = None, breakpoints: str | Sequence[int] | Sequence[float] = 'equal-count', truncation_sigmas: float | None = None, max_n_dense: int = 2000, max_elements: int | None = None, partition_rule: Literal['median', 'midpoint', 'sah'] = 'median', compression_factor: int = 4, levels: int = 3, substitutive_method: str = 'auto', lloyd_iterations: int = 5, candidate_bins_k: int = 12, coverage_inflation: float = 3.0, conserve_mass: bool = True, additive_ladders: bool = True, refine: str = 'none', refine_iters: int | None = None, volume: ndarray | None = None, image_min: float | None = None, volume_axes: tuple | None = None, coarsen_dims: tuple | None = None, quality_stamps: bool = True, quality_max_pair_splats: int = 2000000, device: str = 'auto', seed: int | None = None)[source]

Parameters for build_recipe(), with library-faithful defaults.

The defaults here mirror the historical subcommands. The CLI applies its own scale-derived defaults (e.g. --parts → max_elements) before calling build_recipe(); passing max_elements=None falls back to DEFAULT_MAX_ELEMENTS so the builders are usable standalone too.

n_lods: int = 4
additive_method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto'
reveal_center: Sequence[float] | None = None
spatial_dims: Sequence[int] | None = None
breakpoints: str | Sequence[int] | Sequence[float] = 'equal-count'
truncation_sigmas: float | None = None
max_n_dense: int = 2000
max_elements: int | None = None
partition_rule: Literal['median', 'midpoint', 'sah'] = 'median'
compression_factor: int = 4
levels: int = 3
substitutive_method: str = 'auto'
lloyd_iterations: int = 5
candidate_bins_k: int = 12
coverage_inflation: float = 3.0
conserve_mass: bool = True
additive_ladders: bool = True
refine: str = 'none'
refine_iters: int | None = None
volume: ndarray | None = None
image_min: float | None = None
volume_axes: tuple | None = None
coarsen_dims: tuple | None = None
quality_stamps: bool = True
quality_max_pair_splats: int = 2000000
device: str = 'auto'
seed: int | None = None
property effective_max_elements: int

max_elements with the DEFAULT_MAX_ELEMENTS fallback.

__init__(n_lods: int = 4, additive_method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto', reveal_center: Sequence[float] | None = None, spatial_dims: Sequence[int] | None = None, breakpoints: str | Sequence[int] | Sequence[float] = 'equal-count', truncation_sigmas: float | None = None, max_n_dense: int = 2000, max_elements: int | None = None, partition_rule: Literal['median', 'midpoint', 'sah'] = 'median', compression_factor: int = 4, levels: int = 3, substitutive_method: str = 'auto', lloyd_iterations: int = 5, candidate_bins_k: int = 12, coverage_inflation: float = 3.0, conserve_mass: bool = True, additive_ladders: bool = True, refine: str = 'none', refine_iters: int | None = None, volume: ndarray | None = None, image_min: float | None = None, volume_axes: tuple | None = None, coarsen_dims: tuple | None = None, quality_stamps: bool = True, quality_max_pair_splats: int = 2000000, device: str = 'auto', seed: int | None = None) → None
luxar.gsplats.lod.recipes.build_flat(data: GSplatData, params: RecipeParams) → GSplatData[source]

Collapse to a single leaf (no LOD, no partition).

luxar.gsplats.lod.recipes.build_stream(data: GSplatData, params: RecipeParams) → GSplatData[source]

Reorder into a single additive (prefix-sum) ladder.

luxar.gsplats.lod.recipes.build_levels(data: GSplatData, params: RecipeParams) → GSplatData[source]

Coarse→fine replacement levels (the substitutive reduction).

By default every substitutive level also carries an additive ladder (params.additive_ladders; streaming-friendly first paint per level) — the project convention is additive LODs everywhere unless explicitly disabled (--no-additive), which emits bare per-level leaves.

luxar.gsplats.lod.recipes.build_levels_matrix(data: GSplatData, params: RecipeParams) → GSplatData[source]

Build the balanced substitutive × additive matrix.

luxar.gsplats.lod.recipes.build_tiles(data: GSplatData, params: RecipeParams, *, sibling_compression: int | None = None) → GSplatPartition[source]

Spatially partition, then build an additive ladder within each part.

to_spatial_partition yields a flat GSplatPartition whose children are single-level leaves; this replaces each part with its own additive ladder (clamped to the part’s splat count so no empty LOD bins are produced). sibling_compression forwards to _ladder_for_part() when this partition is the fine branch under a coarser lod-group sibling (overview).

luxar.gsplats.lod.recipes.build_adaptive(data: GSplatData, params: RecipeParams) → GSplatPartition[source]

Spatially partition, then give each part its own substitutive lod group.

The result is a kind=partition whose every child is a kind=lod group (coarse↔fine replacement per part), so each spatial cell frustum-culls AND picks its own LOD level by its own on-screen size — locally adaptive detail.

Contrast the siblings: tiles gives each part an additive (prefix- sum, accumulating) ladder; overview puts a single global substitutive cap above one partition. adaptive is the per-part substitutive form — the most adaptive of the three, for the largest scenes.

luxar.gsplats.lod.recipes.build_overview(data: GSplatData, params: RecipeParams) → GSplatLodGroup[source]

Coarse substitutive cap (far view) + a tiles fine branch.

The result is a kind=lod group with children coarsest→finest in memory ([coarse_leaf, fine_partition], matching the on-disk order). Each child’s coverage_fraction selector threshold is stamped onto its meta (honored by both the standalone writer and the scene graft) via partitioned_coverage_fractions (occupancy halving re-anchored at fills-screen): the coarse cap gets a fraction below the fine branch’s PARTITION_FINEST_AREA (screen-area 1.0), so the coarse overview shows at the opening framing and the fine partition takes over once you zoom the node up to filling the viewport. The fills-screen anchor is deliberate here — see partitioned_coverage_fractions for why a partition-bound ladder does not take the whole-object half-screen anchor (no per-dataset tuning; see RecipeParams).

luxar.gsplats.lod.recipes.PER_PART_RECIPES: tuple[str, ...] = ('stream', 'levels')

Per-part recipes — the recipes that have a single-part form (the building block of tiles/adaptive), usable for streaming per-part assembly such as the tiled-batch merge. stream → a prefix-sum ladder (tiles), levels → a coarse↔fine lod group (adaptive).

luxar.gsplats.lod.recipes.uniform_per_part_lod_warning(tiling_mode: str | None, recipe: str | None) → str | None[source]

Warn when ANY per-part LOD recipe is applied to uniform (apodized) tiles.

Uniform (--tiling uniform) tiles overlap with Hann-apodized halos that form a partition of unity: a boundary feature is split into two tapered splats in adjacent parts whose amplitudes sum to 1.0. That identity holds only at the finest level — per-part LOD coarsens each part independently, so it breaks at coarse levels for BOTH recipes (the viewer hard-switches levels with no cross-level blending, so the artifact is visible):

  • stream (a prefix ladder) orders by mass and keeps a prefix, so the low-amplitude halo splats are dropped first at coarse levels — the overlap loses signal and dims to a seam (often the worse of the two).

  • levels merges each part’s halo splats into representatives independently, so the complementary halves no longer align — the overlap smears at coarse levels.

Content tiling (--tiling content, disjoint core-keep parts) carries no shared halos, so per-part LOD is exact there for either recipe.

Returns the warning text (caller emits it) when tiling_mode is uniform and recipe is a per-part recipe, else None.

luxar.gsplats.lod.recipes.build_part_lod(part: GSplatLeaf | GSplatLodGroup | GSplatPartition, recipe: str, params: RecipeParams, *, cell: List[Tuple[float, float]] | None = None) → GSplatLeaf | GSplatLodGroup | GSplatPartition[source]

Give ONE partition child its own per-part LOD, with depth clamped to the part’s splat count (so a small part never synthesises degenerate levels).

This is the exact building block build_tiles() (stream) and build_adaptive() (levels) apply to every part — exposed so a streaming assembler (e.g. the tiled-batch merge) can LOD one part at a time without materialising the whole partition. Returns the per-part node: a leaf-with-ladder (stream) or a substitutive GSplatLodGroup (levels). Legacy spellings translate via canonical_recipe_name().

cell is this part’s own tile, per center dim, and is required by refine="volume": the re-fit crops the volume to the tile so it is not tempted to pull splats out of it to explain a neighbour’s signal. Callers that know the decomposition (the fit-time assembler, the batch merge) should pass it; without it a volume re-fit refuses rather than targeting the whole volume.

luxar.gsplats.lod.recipes.build_recipe(data: GSplatData, recipe: Literal['flat', 'stream', 'levels', 'tiles', 'overview', 'adaptive'], params: RecipeParams) → GSplatData | GSplatLeaf | GSplatLodGroup | GSplatPartition[source]

Build recipe from data.

Returns a GSplatData for the matrix recipes (flat/stream/levels) and a GSplatNode for the composed recipes (tiles/overview/adaptive) — see RecipeResult.

In-place Q·e quality annotation of an existing .gsplats.zarr store.

Legacy datasets predate the build-time quality stamps (luxar.gsplats.lod.quality), so the viewer falls back to committed-count crossovers for LOD upgrade decisions — the currency the sibling-aware ladder work showed is structurally late on shared-base stream ladders. This module retrofits the stamps without refitting or re-laddering:

  • lod_stats.energy_fraction_cum per additive sub-LOD — the cumulative self-energy fraction e(k) of the committed prefix. Cheap: an O(N) pass over the ALPHA-EFFECTIVE amplitudes (A·α — RGBA color-alpha folded in, matching the build path’s effective_amplitudes) + Cholesky diagonal (the on-disk order is the ladder order). LOD children additionally decode their covariance to stamp the per-level footprint without materializing full splat objects.

  • level_stats.reference_energy per leaf — the absolute self-energy weight w used for partition-level quality aggregation.

  • level_stats.quality per lod-group child (opt-in, with_quality=True) — the measured mixture-L² quality Q of each level vs its group’s finest content, via mixture_quality(). This loads full splat arrays (the level + the finest reference both resident), so it is the expensive half; the free e(k)/w stamps alone already enable the viewer’s energy-threshold upgrade rule.

Attrs are merged into the existing lod_stats / level_stats dicts (the keys the reader already recovers — format-additive, no version bump). After a non-dry run the root content_hash is re-stamped before zarr.consolidate_metadata (the writer’s order), so the viewer’s persistent cache invalidates on the changed attrs.

class luxar.gsplats.lod.annotate.AnnotateReport(path: str, dry_run: bool, leaves: List[LeafStamp] = <factory>, levels: List[LevelStamp] = <factory>)[source]

Everything annotate_quality_store() computed (and, unless dry_run, wrote).

path: str
dry_run: bool
leaves: List[LeafStamp]
levels: List[LevelStamp]
__init__(path: str, dry_run: bool, leaves: List[LeafStamp] = <factory>, levels: List[LevelStamp] = <factory>) → None
class luxar.gsplats.lod.annotate.LeafStamp(path: str, n_splats: int, energy_fraction_cum: List[float], reference_energy: float | None)[source]

The e(k)/w stamps computed for one leaf (splat set or additive ladder).

path: str
n_splats: int
energy_fraction_cum: List[float]

Cumulative energy fraction per additive sub-LOD; last entry is 1.0. Empty when a nonempty leaf has zero effective energy (no stamp written, matching the build path).

reference_energy: float | None

Absolute self-energy weight w (Σ aᵢ²·π^{D/2}·|Σᵢ|^{1/2}), or None when no weight was written — a REVEAL ladder carries neither half of the e/w pair, and the report must not name a number the store does not hold.

__init__(path: str, n_splats: int, energy_fraction_cum: List[float], reference_energy: float | None) → None
class luxar.gsplats.lod.annotate.LevelStamp(path: str, n_splats: int, quality: float, reference_energy: float | None)[source]

The measured Q stamp for one lod-group child (with_quality only).

path: str
n_splats: int
quality: float
reference_energy: float | None

the FINEST child’s total self-energy. None for a reveal child, which carries no weight (see LeafStamp).

Type:

The group-consistent w

__init__(path: str, n_splats: int, quality: float, reference_energy: float | None) → None
luxar.gsplats.lod.annotate.annotate_quality_store(path: str | Path, *, with_quality: bool = False, max_pair_splats: int = 2000000, device: str = 'auto', dry_run: bool = False) → AnnotateReport[source]

Annotate an existing .gsplats.zarr directory store in place.

Parameters:
  • path – A .gsplats.zarr directory (compressed .zip/.tar.gz stores are rejected — extraction is temp-dir based, so in-place is impossible).

  • with_quality – Also measure per-level Q vs each lod group’s finest content (loads full splat arrays; the e(k)/w/footprint stamps alone are O(N)).

  • max_pair_splats – Forwarded to mixture_quality().

  • device – Forwarded to mixture_quality().

  • dry_run – Compute and report everything, write nothing.

Returns:

The computed stamps per leaf (e(k), w) and per lod-group child (Q).

Return type:

AnnotateReport

Measured approximation quality for gsplat mixtures (the Q·e scheme).

The LOD viewer needs to answer “is the committed part of level A at least as good an approximation as what level B shows?” — and neither splat counts nor mass can answer it (counts compare merged coarse blobs against fine splats; mass conservation pins every complete level to the same total). The honest currency is a measured fidelity: the closed-form mixture L² against a common reference, normalized to a unitless quality

Q(approx, ref) = 1 − ‖approx − ref‖² / ‖ref‖² ∈ [0, 1]

with Q(ref, ref) = 1 by construction. Q is computed ONCE at build / annotate time per substitutive level (against the group’s finest content) and stamped into level_stats; the viewer combines it with the additive ladder’s cumulative energy fraction e(k) (lod_stats) into the committed quality Q·e(k) — see docs/specs/GSPLATS_ZARR_FORMAT.md.

Estimator design — ‖A−B‖² = ‖A‖² − 2⟨A,B⟩ + ‖B‖² where every term expands into pairwise Gaussian inner products:

  • The per-splat SELF diagonals of ‖A‖²/‖B‖² are O(N) closed form and always computed EXACTLY on the full mixtures.

  • Every off-diagonal / cross sum is a directed row-sum estimate: a fixed-seed uniform sample of query splats, each queried against the other side’s spatial-hash grid (kNN + radius prune), scaled by n/|queries|. Query sampling is the load-bearing scalability lever — the hash grid gathers candidates in a per-query Python loop (~100 µs/query on both its backends), so querying every splat of a multi-million mixture is intractable; a bounded query sample estimates the same sums unbiasedly at constant cost. The cross term averages the A-side and B-side row-sum estimates for symmetric coverage.

  • TRUNCATION CONSISTENCY IS LOAD-BEARING: all directed sums of one comparison share per-side radii (shrunk to a density budget derived from FULL-population counts, so exact and sampled runs truncate identically) and adaptive k sized so the radius is the binding prune. For A == B the four directed queries then see identical grids/k/radii and the three terms cancel: mixture_quality(x, x) ≈ 1 by construction.

  • Row subsampling for the pair views uses a fixed-seed UNIFORM draw (deterministic ⇒ reproducible stamps; uniform ⇒ the inverse-inclusion rescale is unbiased). Evenly-spaced strides are unsafe: both mixtures are Hilbert/ladder-ordered and two regular strides alias, over-including near-duplicate cross partners.

Kernel sums run in float64 (MPS lacks float64 → CPU, mirroring make_substitutive_lod); the spatial grids run on the fast device when one is available (positions are float32 there).

No optimizer, no gradients — this module only measures.

@module luxar.gsplats.lod.quality

class luxar.gsplats.lod.quality.QualityResult(quality: float, l2_sq: float, approx_norm_sq: float, ref_norm_sq: float, approx_pair_fraction: float, ref_pair_fraction: float, n_cross_pairs: int, n_approx_pairs: int, n_ref_pairs: int)[source]

Result of mixture_quality() (all plain Python scalars).

quality: float

clamp(1 − l2_sq/ref_norm_sq, 0, 1) — the stampable quality.

l2_sq: float

‖approx − ref‖² (closed-form mixture L², estimated — see module doc).

approx_norm_sq: float
ref_norm_sq: float
approx_pair_fraction: float

Row-inclusion fractions of the pair views (1.0 = full mixture).

ref_pair_fraction: float
n_cross_pairs: int

Directed pairs evaluated for the cross / self sums (post-sampling).

n_approx_pairs: int
n_ref_pairs: int
__init__(quality: float, l2_sq: float, approx_norm_sq: float, ref_norm_sq: float, approx_pair_fraction: float, ref_pair_fraction: float, n_cross_pairs: int, n_approx_pairs: int, n_ref_pairs: int) → None
luxar.gsplats.lod.quality.mixture_quality(approx: GSplatData, reference: GSplatData, *, max_pair_splats: int = 2000000, config: L2RefineConfig | None = None, device: str = 'auto') → QualityResult[source]

Measure how well approx approximates reference.

Returns QualityResult with quality = 1 − ‖A−B‖²/‖B‖² clamped to [0, 1]. Degenerate inputs: an empty approx scores 0 (it explains none of the reference); an empty reference raises (quality against nothing is undefined).

Estimation caveats (see the module docstring for the design): the kNN + radius truncation is the same approximation the L² refiner trusts; query and row sampling make the off-diagonal terms estimates (exact diagonals dominate); when the two mixtures share literally identical splats the cross estimate concentrates on few matched pairs and gets noisy — the intended use (a MERGED coarse level vs the finest content) never shares rows; a mixture’s own row-prefix is what the cheap e(k) cumulative energy fraction is for.

luxar.gsplats.lod.quality.total_self_energy(data: GSplatData) → float[source]

Exact Σ aᵢ²·π^(D/2)·|Σᵢ|^(1/2) in O(N), without Torch.

Volume re-fit of a coarse LOD level (refine="volume").

The third quality rung for substitutive levels, above the moment-matched merge and the mixture-space refine="l2" pass: warm-start a full Gaussian-splat fit against the source volume from the merge output. Unlike l2 — whose target is the fine mixture and therefore inherits the fine fit’s own error — this optimizes the true render-fidelity objective at the coarse budget. Benchmarked on real microscopy (skimage cells3d nuclei): +5–6 dB full-res and +10–12 dB at viewing scale over the merge, with unchanged splat count and lower cross-level drift than a cold fit. (Measured before the #1172 amplitude-convention fix; the never-worse guard below bounds the outcome at the merge, so the sign of the gain is safe, but the magnitudes have not been re-measured.)

This module is a thin orchestration layer at GSplatData altitude: the heavy lifting (rasterizer, Adam, schedulers) is entirely fit_gaussian_splats() with a GSplatData warm-start seed. It deliberately does NOT live in _substitutive/ — that subpackage’s contract is silent tensor kernels that know nothing about GSplatData.

Safety: the returned splats are never worse than the seed — both the seed and the re-fit candidate are rendered to the volume’s grid and the lower-MSE one wins. Two additional guards keep the ladder coherent:

  • Mass pinning (conserve_mass, default on): the re-fit’s amplitudes are rescaled so its rendered DC equals the seed’s — the seed’s mass was already pinned to the fine chain’s by the substitutive conserve_mass step, so without this the (volume-accurate) re-fit reintroduces the cross-level brightness pop that step exists to prevent.

  • Frame checks (both directions): a seed whose center bounding box falls clearly outside the volume’s voxel index range (an ENLARGED physical frame, e.g. fit --voxel-size 4 or gsplat transform --scale) skips the re-fit up front; a SHRUNK frame (physical units below 1 per voxel — the common sub-micron microscopy case) fits inside that box, so it is caught after the fit by the relocation check: a fit that wholesale moved/rescaled the splats is a frame mismatch, and the seed is returned with a warning. In either case a voxel-frame re-fit would have won the MSE guard while being misplaced relative to the rest of the ladder.

class luxar.gsplats.lod.volume_refit.VolumeRefitConfig(iters: int = 300, lr: float = 0.01, early_stop_patience: int = 50, never_worse: bool = True, conserve_mass: bool = True, frame_tolerance: float = 0.5, image_min: float | None = None)[source]

Knobs for the volume re-fit of one coarse level.

Only iters and conserve_mass are user-exposed (via --refine-iters and the ladder-wide --conserve-mass flag); the rest are fixed operating constants, not a tuning surface. The benchmark showed the warm-started fit near-converged by 150–300 iterations.

iters: int = 300
lr: float = 0.01

Adam learning rate — fit_gaussian_splats’ default, which the benchmark used unchanged.

early_stop_patience: int = 50

Loss plateau patience before the fit stops early (warm starts sit close to a minimum, so a short fuse saves most of the budget on easy levels).

never_worse: bool = True

Render both seed and candidate to the volume grid and keep the lower-MSE one. Disable only in tests probing the raw fit path.

conserve_mass: bool = True

Rescale the re-fit’s amplitudes so its rendered DC equals the seed’s (whose mass the substitutive conserve_mass step already pinned to the fine chain’s). Keeps brightness constant across LOD switches; the re-fit otherwise tracks the volume’s true DC, which the finest level may under-explain — a visible pop. Follows the ladder’s conserve_mass.

frame_tolerance: float = 0.5

skip the re-fit (returning the seed) when more than this fraction of seed centers lie outside the volume’s voxel index range, padded by this fraction of each extent. Catches physical-unit or transform-scaled coordinate frames that the MSE guard cannot.

Type:

Frame-mismatch heuristic

image_min: float | None = None

The level the INPUT fit subtracted (stats["image_min"]), or None when the store does not record one.

The seed’s amplitudes are background-relative — that is the recipe’s contract and why seed_amps_background_relative is set below. The volume handed in is NOT: it is the raw source. Left unreconciled, the inner fit inherits floor="auto" and re-estimates a background from this volume, so a re-fitted level can land on a different basis from the ladder’s other levels — the conserve_mass DC pinning partly hides it, which is why it went unnoticed (#1177). Supplying the level lets the re-fit run on the ladder’s own basis instead of guessing a new one.

__init__(iters: int = 300, lr: float = 0.01, early_stop_patience: int = 50, never_worse: bool = True, conserve_mass: bool = True, frame_tolerance: float = 0.5, image_min: float | None = None) → None
luxar.gsplats.lod.volume_refit.volume_refine_splats(seed: GSplatData, volume: ndarray, *, config: VolumeRefitConfig, device: str | None = None) → Tuple[GSplatData, Dict[str, Any]][source]

Warm-start re-fit seed against volume; keep whichever is closer.

Parameters:
  • seed (GSplatData) – One coarse level’s merge output (a flat splat set). Its centers must be in the volume’s voxel coordinate frame — the frame fit_gaussian_splats emits, so any level derived from a fit of this volume qualifies. A seed whose bounding box clearly disagrees with that frame is returned untouched (see VolumeRefitConfig.frame_tolerance).

  • volume (np.ndarray) – The source volume, full resolution (fitting a blurred/downscaled proxy was benchmarked and rejected — it discards positional detail the merge seed inherits from the sharp fine fit).

  • config (VolumeRefitConfig) – Operating constants; config.iters is the one quality/time knob and config.conserve_mass follows the ladder-wide setting.

  • device (str, optional) – Torch device for the fit and the guard renders (None = auto).

Returns:

The refined level (or the untouched seed when it renders closer to the volume, or on a frame mismatch) and a flat, JSON-safe stats dict: mse_seed, mse_refit, improved, seed_won, mass_pinned, mass_scale, frame_mismatch, n_seed, n_refit, iters, wall_s.

Return type:

(GSplatData, dict)

Node Tree

The in-memory gsplat node tree (leaf / lod / partition nodes) shared by the fitting, LOD, and I/O layers — the v3.4 .gsplats.zarr on-disk structure.

Node-tree model for Gaussian splats.

This is the unified in-memory representation behind the v3.0 .gsplats.zarr format and the scene gsplat-node subtree: a standalone .gsplats.zarr is a detached node subtree, and embedding it into a scene is a graft of that subtree.

Three node types compose freely (and nest arbitrarily):

  • GSplatLeaf — a gsplats leaf carrying an additive ladder (one or more AdditiveSubLOD, prefix-sum / additive LOD). The trivial single-splat-set case is a leaf with a one-entry ladder.

  • GSplatLodGroup — substitutive LOD: children are rendered one at a time (the scene kind=lod Group). Children are ordered coarsest → finest in memory — the SAME order as the on-disk child_<i> layout (child_0 = coarsest), so the serializer writes them straight through with no reversal. default_level is a derived property (= the finest, last child).

  • GSplatPartition — spatial split: all children are rendered (the scene kind=partition Group), each carrying its own position_bounds.

The classes are intentionally small, pure, and immutable (frozen dataclasses) so they are trivially unit-testable in isolation. Per-node metadata (LOD provenance such as compression_factor / parent_method / level_index, the view-driven coverage_fraction selector threshold, per-node stats) lives in a free-form meta dict on each node — mirroring the zarr .zattrs a node carries on disk.

The tree_from_substitutive_levels() / substitutive_levels_from_tree() bridge converts to and from the derived 2-D substitutive × additive matrix view (GSplatData.substitutive_levels, finest-first by convention). The tree is the single in-memory ground truth (GSplatData stores a node and derives the matrix view on demand); the matrix is exactly one shape of the tree: a single GSplatLodGroup of leaves (or, for a single substitutive level, a bare GSplatLeaf). This bridge is the ONE place the coarsest-first tree order is reversed to the finest-first matrix-view convention and back.

class luxar.gsplats.tree.GSplatLeaf(additive_sublods: List[AdditiveSubLOD], meta: Dict[str, Any] = <factory>)[source]

Bases: object

A gsplats leaf carrying an additive ladder (≥ 1 AdditiveSubLOD).

additive_sublods

The additive (prefix-sum) ladder. Always ≥ 1 entry; a single entry is the trivial “no additive sub-ordering” case.

Type:

list[AdditiveSubLOD]

meta

Free-form per-node metadata (the node’s zarr .zattrs). Recognised optional keys include compression_factor / parent_method / level_index (LOD provenance when this leaf is a substitutive level), coverage_fraction (selector threshold when a child of a lod group), and stats.

Type:

dict

__post_init__() → None[source]

Reject an empty ladder and additive sub-LODs of mixed dimensionality.

The leaf’s ndim is read from the first sub-LOD, so a mix would silently mis-describe the rest; both are hard errors.

property n_additive_sublods: int

Number of additive sub-LODs in this leaf (≥ 1).

property n_splats: int

Total splats across this leaf’s additive ladder.

property ndim: int

Spatial dimensionality (from the first additive sub-LOD).

__init__(additive_sublods: List[AdditiveSubLOD], meta: Dict[str, Any] = <factory>) → None
class luxar.gsplats.tree.GSplatLodGroup(children: ~typing.List[~luxar.gsplats.tree.GSplatLeaf | ~luxar.gsplats.tree.GSplatLodGroup | ~luxar.gsplats.tree.GSplatPartition], meta: ~typing.Dict[str, ~typing.Any] = <factory>)[source]

Bases: object

Substitutive LOD group — children rendered one at a time (kind=lod).

Children are ordered coarsest → finest in memory, matching the on-disk child_<i> layout (child_0 = coarsest) so the serializer needs no reversal. default_level is a derived property (= the finest, last child): the level a simple consumer renders by default. It is deliberately distinct from the on-disk default_level (a viewer progressive-load hint = coarsest), which the serializer stamps independently.

__post_init__() → None[source]

Reject an empty group and children of mixed dimensionality.

The group’s ndim is read from the first child, so a dimensionality mix would silently mis-describe the rest; both are hard errors.

property default_level: int

The finest child’s index (last entry, coarsest→finest order).

property n_children: int

Number of substitutive levels (children) in the group.

property n_splats: int

Splats of the default child (a substitutive group renders one child).

__init__(children: ~typing.List[~luxar.gsplats.tree.GSplatLeaf | ~luxar.gsplats.tree.GSplatLodGroup | ~luxar.gsplats.tree.GSplatPartition], meta: ~typing.Dict[str, ~typing.Any] = <factory>) → None
class luxar.gsplats.tree.GSplatPartition(children: ~typing.List[~luxar.gsplats.tree.GSplatLeaf | ~luxar.gsplats.tree.GSplatLodGroup | ~luxar.gsplats.tree.GSplatPartition], max_elements: int = 0, meta: ~typing.Dict[str, ~typing.Any] = <factory>, bsp_tree: ~typing.Dict[str, ~typing.Any] | None = None)[source]

Bases: object

Spatial partition group — all children rendered (kind=partition).

Each child is a spatial part; max_elements records the BSP target used to build the partition.

bsp_tree (optional) is the serialized split-plane record of the BSP that produced the parts — a nested {"axis", "split", "left", "right"} / leaf {"part": i} dict (see luxar.core.group.partition.BSPNode. to_serializable()). When present it is written to the kind=partition group’s attrs so the viewer can order the parts back-to-front exactly (painter’s algorithm), correct even with the camera inside the volume. None when the parts did not come from a single BSP split (e.g. a streamed grid/content merge) — the viewer then falls back to a centroid heuristic.

__post_init__() → None[source]

Reject an empty partition and parts of mixed dimensionality.

The partition’s ndim is read from the first part, so a mix would silently mis-describe the rest; both are hard errors.

property n_splats: int

Total splats across all parts (a partition renders every child).

__init__(children: ~typing.List[~luxar.gsplats.tree.GSplatLeaf | ~luxar.gsplats.tree.GSplatLodGroup | ~luxar.gsplats.tree.GSplatPartition], max_elements: int = 0, meta: ~typing.Dict[str, ~typing.Any] = <factory>, bsp_tree: ~typing.Dict[str, ~typing.Any] | None = None) → None
luxar.gsplats.tree.GSplatNode

A node in the gsplat tree — a leaf or one of the two group kinds.

alias of GSplatLeaf | GSplatLodGroup | GSplatPartition

luxar.gsplats.tree.iter_leaves(node: GSplatLeaf | GSplatLodGroup | GSplatPartition) → Iterator[GSplatLeaf][source]

Yield every GSplatLeaf in node (depth-first, pre-order).

luxar.gsplats.tree.total_splats(node: GSplatLeaf | GSplatLodGroup | GSplatPartition) → int[source]

Total splats across every leaf in the subtree (ignores LOD selection).

Distinct from node.n_splats, which honours substitutive selection (a lod group reports only its default child). This sums all stored splats.

luxar.gsplats.tree.node_ndim(node: GSplatLeaf | GSplatLodGroup | GSplatPartition) → int[source]

Spatial dimensionality of the subtree (from its first leaf).

luxar.gsplats.tree.center_bounds(node: GSplatLeaf | GSplatLodGroup | GSplatPartition) → Tuple[ndarray, ndarray] | None[source]

Axis-aligned bounds of all splat centers in the subtree.

Returns (min, max) float arrays of shape (d,), or None if the subtree holds zero splats. This is a center-only bound. The serializer’s position_bounds is the verbatim center bounds (matching the scene path); only chunk_bounds widen each chunk by the ellipsoidal extent at the dataset’s own truncation_radius (the support it was fitted and is rendered at).

luxar.gsplats.tree.map_leaves(node: GSplatLeaf | GSplatLodGroup | GSplatPartition, fn: Callable[[GSplatLeaf], GSplatLeaf | GSplatLodGroup | GSplatPartition]) → GSplatLeaf | GSplatLodGroup | GSplatPartition[source]

Rebuild the tree with fn applied to every leaf, preserving its shape.

Walks the (immutable, frozen) tree depth-first and returns a NEW tree of the same shape — same group kinds, GSplatPartition.max_elements and bsp_tree, and per-node meta — in which each GSplatLeaf is replaced by fn(leaf) (fn typically returns a transformed leaf). This is the write-side workhorse for tree-aware ops (e.g. gsplat transform on a kind=partition) that the flat GSplatData path — which only handles matrix-shaped trees — cannot express.

luxar.gsplats.tree.without_meta_key(node: GSplatLeaf | GSplatLodGroup | GSplatPartition, key: str) → GSplatLeaf | GSplatLodGroup | GSplatPartition[source]

Rebuild the tree with key removed from every node’s meta.

Unlike map_leaves() (which copies group meta verbatim), this scrubs a key from leaves AND group nodes. Its use is dropping the coverage_fraction LOD-switch threshold after a geometry transform so the writer re-derives it: a stale threshold on a group node (an overview partition child, or an adaptive per-part lod group) is otherwise re-applied verbatim by the serializer. (Coverage fractions are derived from the ladder’s LENGTH and its topology, not from geometry, hence invariant to scale/rotate/translate — so this re-derives the same value; it is retained as a safety net for transforms that also re-ladder and change the number of levels.)

luxar.gsplats.tree.iter_default_leaves(node: GSplatLeaf | GSplatLodGroup | GSplatPartition) → Iterator[GSplatLeaf][source]

Yield the leaves of the default-rendered selection.

Mirrors the n_splats selection semantics: a partition renders all parts, but a substitutive lod group renders only its default (finest) child — so coarse substitutive levels (downsampled representations of the same splats) are skipped. Use this for global statistics (centroid, max amplitude) so the same splat is not double-counted across levels. (Contrast iter_leaves(), which yields every stored leaf regardless of LOD selection.)

luxar.gsplats.tree.amplitude_weighted_centroid(node: GSplatLeaf | GSplatLodGroup | GSplatPartition) → ndarray | None[source]

Global amplitude-weighted centroid over the default-rendered splat set.

Returns the (d,) centroid (float64), or None for an empty tree. Falls back to the unweighted center mean when the total amplitude is zero — matching center_at_centroid() on a single leaf, so a matrix-shaped tree gives an identical result.

luxar.gsplats.tree.global_amplitude_max(node: GSplatLeaf | GSplatLodGroup | GSplatPartition) → float[source]

Maximum amplitude over the default-rendered splat set (0.0 if empty).

luxar.gsplats.tree.nondegenerate_axes(node: GSplatLeaf | GSplatLodGroup | GSplatPartition, eps: float = 1e-06, fallback: bool = True) → ndarray[source]

Axes with real covariance extent over the default-rendered splat set.

The node-tree twin of GSplatData._nondegenerate_axes: an axis is spatial if its maximum marginal sigma across the splats exceeds eps; a zero-variance categorical axis (a stacked-time / channel axis) is excluded. Used by transform --center to re-origin only the spatial axes. Reduces to a per-axis max-sigma vector (via each leaf’s marginal_sigmas) and applies the shared spatial-axis rule. With fallback=True (the default) falls back to all axes when none qualify (or the tree is empty); fallback=False returns an empty selection instead, so a caller can tell an all-degenerate store from a genuinely all-spatial one.

luxar.gsplats.tree.node_from_substitutive_levels(levels: List[SubstitutiveLevel]) → GSplatNode[source]

Build the tree shape from a finest-first matrix view — no stamping.

The lightweight inverse of substitutive_levels_from_tree(): a single level → a bare GSplatLeaf; multiple levels → a GSplatLodGroup reversed to coarsest-first (matching disk). This is what GSplatData stores as its ground-truth node on construction — cheap, with no coverage_fraction derivation (the view-driven thresholds are a serialize-time concern, stamped by tree_from_substitutive_levels() / re-derived by the writer).

luxar.gsplats.tree.gate_authored_selector(children: List[GSplatLeaf | GSplatLodGroup | GSplatPartition], meta_selector: str | None, *, source: str) → Tuple[List[GSplatLeaf | GSplatLodGroup | GSplatPartition], str][source]

The SELECTOR/THRESHOLD CONSISTENCY gate both serializers share.

A kind=lod group’s meta selector describes its AUTHORED per-child coverage_fraction thresholds, so the two writers (io/_compiler/gsplat_tree.write_gsplat_node and gsplats_pipeline/from_io.graft_gsplat_node) must agree on when it can be preserved — otherwise a store grafted into a scene would render differently from the same store opened directly. Returns the (possibly scrubbed) children and the selector to stamp:

  • unknown meta_selector → ValueError before anything is written (the READER whitelists stale spellings away; one arriving here is a hand-built tree that would otherwise write an out-of-vocabulary selector into a store claiming v3.4 compliance);

  • PARTIALLY-authored ladder → the authored remnant is scrubbed (warned) so the caller’s fallback derivation covers every child uniformly, and the stamp is "screen-area" (the units of every live derivation);

  • fully authored + explicit selector → preserved verbatim, after validating the thresholds against that selector’s contract;

  • fully authored + NO selector → legacy "coverage" (the viewer’s own missing-selector fallback; authored = legacy is the library-wide convention), likewise validated.

luxar.gsplats.tree.tree_from_substitutive_levels(levels: List[SubstitutiveLevel], coverage: Callable[[List[int]], List[float]] | None = None, *, selector: str | None = None) → GSplatNode[source]

Build a node tree from the historical 2-D matrix representation.

  • A single substitutive level → a bare GSplatLeaf (its additive ladder), carrying that level’s provenance in meta.

  • Multiple substitutive levels → a GSplatLodGroup of one leaf per level, reversed to coarsest-first (levels is the finest-first matrix view; the tree stores coarsest-first to match disk). The in-memory default_level is the derived finest (last) child; the persisted on-disk default_level is the viewer’s coarsest-first render hint (stamped by the serializer), a separate concept.

Each child of a multi-level lod group is back-filled with a derived coverage_fraction selector threshold (a SCREEN-AREA fraction by occupancy halving; the group meta carries selector="screen-area" to name the units), so a standalone substitutive .gsplats.zarr selects levels correctly in the viewer rather than being stuck at the finest level. This is the same single-sourced coverage_fractions() derivation the scene path uses.

coverage overrides that derivation. It defaults to the whole-object coverage_fractions(); a caller building a ladder that is bound to a spatial partition (the adaptive recipe’s per-tile groups) passes partitioned_coverage_fractions() instead, which keeps the fills-screen anchor. See that function for the rule and why a per-tile ladder must not take the whole-object anchor.

selector names the UNITS the produced thresholds are in (stamped onto the group meta, honored by both serializers). When omitted it follows the library convention — DERIVED thresholds are screen-area, custom/authored ones are legacy: "screen-area" for the built-in derivation (coverage is None), and the legacy "coverage" when a custom coverage callable is supplied, so an unchanged external caller’s callback-produced thresholds keep the diagonal semantics they were written against rather than being silently reinterpreted as area fractions. A caller whose callable produces area fractions (the recipes pass the built-in area derivations through this parameter) says so explicitly with selector="screen-area".

This is the inverse of substitutive_levels_from_tree() for any tree that is matrix-shaped (a leaf, or a lod group whose children are all leaves).

luxar.gsplats.tree.substitutive_levels_from_tree(node: GSplatNode) → Tuple[List[SubstitutiveLevel], int][source]

Project a matrix-shaped tree back to (substitutive_levels, default).

Accepts the two matrix shapes produced by tree_from_substitutive_levels():

  • a bare GSplatLeaf → one substitutive level, default 0;

  • a GSplatLodGroup whose children are all leaves → one level per child, reversed from the tree’s coarsest-first order to the matrix view’s finest-first convention (index 0 = finest). The returned default is always 0 (the matrix view’s finest), distinct from the tree’s coarsest-first on-disk hint.

Raises ValueError for genuinely non-matrix trees (partitions, or lod groups with non-leaf children) — those have no rectangular-matrix equivalent and must be consumed through the tree directly.

luxar.gsplats.tree.is_matrix_shaped(node: GSplatLeaf | GSplatLodGroup | GSplatPartition) → bool[source]

True if node maps to a flat substitutive × additive matrix.

I.e. a bare leaf, or a lod group whose children are all leaves.

Content Planning

Density-driven box planning for content-adaptive tiled fits (luxar gsplat fit --tiling content).

Content-aware fit planner.

Decides how to decompose a volume into fit regions and how many splats each gets, driven by a cheap content scan and the calibration’s transferable splats-per-feature density. Noise-agnostic (the calibration owns the K-selection / noise axis); this module owns the tiling / scale / budget axis.

Pipeline:

scan_content(volume) -> ContentField (coarse feature density) plan_partition(field, density) -> FitPlan (boxes + per-box budgets) fit_planned(volume, plan) -> GSplatData (fit each box, merge)

class luxar.gsplats.planner.ContentField(density: ndarray, cell: int, shape: tuple, method: str)[source]

Bases: object

Coarse feature-density grid over a volume + fast box queries.

density: ndarray

Coarse grid (ceil(shape/cell) per axis); each cell = feature count.

cell: int

Full-resolution voxels per coarse cell.

shape: tuple

Original (full-resolution) volume shape.

method: str
property total: float
box_weight(z0: int, z1: int, y0: int, y1: int, x0: int, x1: int) → float[source]

Feature weight inside the full-res half-open box, via coarse cells.

marginal(box: tuple, axis: int) → ndarray[source]

Weighted projection of box onto axis (over the box’s cells).

__init__(density: ndarray, cell: int, shape: tuple, method: str) → None
class luxar.gsplats.planner.FitPlan(volume_shape: ~typing.List[int], boxes: ~typing.List[~luxar.gsplats.planner.spec.PlanBox], overlap: int, feature_method: str, min_leaf: int, max_leaf: int, density: ~typing.Dict[str, ~typing.Any] = <factory>, bsp_tree: ~typing.Dict[str, ~typing.Any] | None = None, meta: ~typing.Dict[str, ~typing.Any] = <factory>)[source]

Bases: object

A content-balanced decomposition of a volume into budgeted fit regions.

volume_shape: List[int]
boxes: List[PlanBox]
overlap: int

Voxel halo added per box face at fit time (for seamless blending).

feature_method: str

Content metric used (peaks | edges | intensity).

min_leaf: int
max_leaf: int
density: Dict[str, Any]

The SplatDensity (as dict) used to size budgets, if any.

bsp_tree: Dict[str, Any] | None = None

Split planes of the recursion that produced boxes, serialized.

The planner IS a recursive BSP, so the boxes have an exact back-to-front order for any camera pose — but only if the split planes survive to the fitted output. Carried here (rather than recomputed downstream) and handed to GSplatData.partition_from_regions, which prunes it to the boxes that actually produced splats and stamps it on the kind=partition node as its bsp_tree attr. See prune_serialized_bsp_tree().

Leaf part labels index boxes directly. None for a plan that did not come from one recursion (e.g. read from a pre-#1555 plan.json), which downstream treats as “no tree” — the viewer’s centroid fallback.

__init__(volume_shape: ~typing.List[int], boxes: ~typing.List[~luxar.gsplats.planner.spec.PlanBox], overlap: int, feature_method: str, min_leaf: int, max_leaf: int, density: ~typing.Dict[str, ~typing.Any] = <factory>, bsp_tree: ~typing.Dict[str, ~typing.Any] | None = None, meta: ~typing.Dict[str, ~typing.Any] = <factory>) → None
meta: Dict[str, Any]
property total_budget: int
property n_boxes: int
overlap_fraction() → Tuple[float, float][source]

(median, max) per-box halo overhead fraction.

fit_planned pads each box by overlap on both sides of every axis, so the fitted volume is (L + 2*overlap) per axis; the wasted (halo) fraction is 1 - prod(L / (L + 2*overlap)).

to_json(path: Path) → None[source]
classmethod from_json(path: Path) → FitPlan[source]
class luxar.gsplats.planner.PlanBox(box: List[int], n_features: int, budget: int)[source]

Bases: object

One fit region: a half-open box [z0:z1, y0:y1, x0:x1] + a budget.

box: List[int]

Flat [z0, z1, y0, y1, x0, x1] in the input volume’s voxel coordinates.

n_features: int

Feature content of the box (from the planner’s content scan).

budget: int

Splat seed budget for this box (from SplatDensity.predict_k).

property dims: List[int]
property voxels: int
__init__(box: List[int], n_features: int, budget: int) → None
luxar.gsplats.planner.fit_planned(volume: ndarray, plan: FitPlan, *, device: str | None = None, verbose: bool = False, progress_callback: Callable[[int, int, str], None] | None = None, partition: bool = False, recipe: str | None = None, recipe_params: Any | None = None, **fit_kwargs: Any) → Any[source]

Fit every box in plan and return the merged result.

fit_kwargs are forwarded to fit_gaussian_splats per box (preset / n_iters / loss / cull_retention / …). Each box’s seed budget comes from the plan; near-empty boxes (budget 0) are skipped.

fit_kwargs["floor"] must already be a CONCRETE level (or "none"): every box crop is handed the same value, so a spec like auto/pNN would be re-estimated against each crop and abutting core-kept boxes would subtract wildly different pedestals — visible brightness steps at box boundaries. The CLI resolves it once against the whole volume before calling here (luxar.cli.gsplat_ops.fitting.fit_utils.resolve_shared_floor). What is shared is the floor ARGUMENT, not the input: every box is still handed its own crop. The whole-volume norm_range below gives those crops the same effective lower bound even when its low endpoint exceeds the resolved level.

fit_kwargs["norm_range"] should likewise describe the whole source volume in raw input units. When omitted, this function resolves it once from volume and forwards the same pair to every box.

With partition=True (the CLI default) the per-box splats are kept as a kind=partition tree — one part per box (boxes are core-disjoint, so this is exact) — for viewer frustum culling; a GSplatNode is returned. With partition=False the boxes are concatenated into a single flat GSplatData leaf (--flat). Both shapes are scored against the whole reference volume; tree-shaped results carry the merged block in their root meta["fit_stats"] for the CLI writer to persist.

Boxes are fit sequentially. For concurrent fitting on one GPU use luxar.gsplats.planner.fit_planned_parallel.fit_planned_parallel() (gsplat fit --tiling content -j N), which fits each box in its own subprocess and merges the same way.

luxar.gsplats.planner.fit_planned_parallel(plan: FitPlan, *, jobs: int, tmp_dir: Path, worker_cmd_builder: Callable[[int, Path], list[str]], volume: Any = None, device: str | None = None, keep_boxes: bool = False, partition: bool = False, recipe: str | None = None, recipe_params: Any | None = None, verbose: bool = True) → Any[source]

Fit every budgeted box via concurrent worker subprocesses, then merge.

Parameters:
  • plan (FitPlan) – The plan whose boxes to fit. Only boxes with budget > 0 are spawned.

  • jobs (int) – Maximum number of concurrent worker processes.

  • tmp_dir (Path) – Directory for per-box outputs. Cleared first; removed on success unless keep_boxes; retained on failure for inspection.

  • worker_cmd_builder (callable) – (box_idx, out_path) -> argv returning the command to fit one box. The injection seam for testing (see _default_worker_cmd_builder()).

  • volume (array-like, optional) – The exact array the workers fit: same channel/timepoint selection and same resolution level, on plan.volume_shape’s grid. Required to stamp merged quality metrics on the merged result; another array with the same shape would produce a plausible but invalid score. Direct callers may omit it, in which case the omission is announced.

  • device (str, optional) – Device used to render the merged reconstruction for scoring. None auto-detects, matching render_to_volume_tensor().

  • keep_boxes (bool, default False) – Keep the per-box temp outputs after a successful merge and let each retained worker score and stamp its own box output.

  • verbose (bool, default True) – Emit the section header and per-box progress lines. False (fit --quiet) suppresses progress output; failures still raise.

Returns:

Flat concatenation, or a tree retaining every box as an additive part. Both carry whole-volume merged quality metrics when volume is available; a tree stores them in its root meta["fit_stats"].

Return type:

GSplatData or GSplatNode

Raises:
  • RuntimeError – If any worker exits non-zero, or exits cleanly but writes neither an output nor an .empty marker, or writes an unreadable store. The message names the offending boxes; tmp_dir is retained.

  • ValueError – If every box produced 0 splats.

luxar.gsplats.planner.plan_partition(field: ContentField, density: SplatDensity | dict, *, target_features: int | None = None, min_leaf: int = 256, max_leaf: int = 512, overlap: int = 32) → FitPlan[source]

Build a content-balanced, size-bounded BSP + per-leaf budgets.

target_features defaults to the density’s reference feature count, so each leaf holds ~the calibrated reference content and therefore gets ~the calibrated k_star budget — the cleanest “calibrate at the scale you fit at” coupling.

The recursion’s split planes are retained on FitPlan.bsp_tree (leaf labels index FitPlan.boxes). They used to be discarded, leaving the fitted partition with no way to say how its parts stack up and the viewer guessing from part centroids — which is not a valid painter’s order and pops at the seams as the camera orbits (#1555).

luxar.gsplats.planner.plan_volume(volume: ndarray, density: SplatDensity | dict, *, feature_method: str = 'peaks', cell: int = 16, target_features: int | None = None, min_leaf: int = 256, max_leaf: int = 512, overlap: int = 32, threshold_abs: float | None = None) → FitPlan[source]

Convenience: scan volume then plan. Returns a FitPlan.

threshold_abs defaults to the density’s recorded feature_threshold so the scan counts features on the same absolute scale as the calibration’s reference — the only way the per-box budgets are correctly scaled.

luxar.gsplats.planner.scan_content(volume: ndarray, cell: int = 16, method: str = 'peaks', downsample: int = 1, threshold_rel: float = 0.1, threshold_abs: float | None = None) → ContentField[source]

Compute a coarse feature-density field over volume (CPU, one pass).

The total feature count is consistent with ``calibration.count_features`` (same detector, same params) so the calibration’s splats-per-feature density transfers correctly to the planner’s per-box counts. This is why the default downsample=1: a coarser scan would shrink the feature counts relative to the calibrated reference and miscalibrate the budgets. downsample>1 trades that consistency for speed and must only be used if the density was calibrated at the same downsample.

Parameters:
  • volume (np.ndarray) – 3-D input volume.

  • cell (int, default 16) – Coarse-cell edge (full-res voxels). Sets the planner’s spatial resolution.

  • method ({"peaks", "edges", "intensity"}, default "peaks") – Feature detector — matches calibration.count_features.

  • downsample (int, default 1) – Detect features on a downsample-strided volume (1 = consistent with count_features); coordinates are mapped back to full-res before binning.

  • threshold_rel (float, default 0.1) – Relative intensity threshold for peak / edge / foreground detection.

luxar.gsplats.planner.fit_planned.CONTENT_CULL_RETENTION: float = 0.999

Near-lossless post-fit retention every content box is fitted at.

The fitter’s own default is 0.95, which discards the bottom 5% of cumulative amplitude after EVERY fit. Content is the --cal-driven path, so it is where that 0.95 — one half of the false signal_limited curve luxar gsplat cal used to report, the other half being too few iterations at high K — is most in play; every preset overrides it, and a preset-less content fit should not be the one invocation that keeps it. A per-box cull also compounds (the merged store loses the weakest splats of every box rather than of the volume), though that argument does not single content out: uniform tiling’s default partition merge culls each tile independently too (only its --flat merge culls once, globally) and stays at the fitter default on purpose. The CLI content path imports this constant so its default and the library’s cannot drift apart.

Scene Interop

Bridge fitted gsplats into Luxar scenes (add_gsplats_from_file and related conversion helpers).

Interoperability adapters between Luxar Gaussian splats and external tools.

Provides readers for the classical (photogrammetric) Gaussian-splat file formats — INRIA point_cloud.ply, antimatter15 .splat, Niantic .spz, and SuperSplat compressed .ply — plus two tracking bridges: a reader for GEFF cell-lineage graphs (geff) and an exporter to tracksdata (the Royer-lab multi-object-tracking data structure). Adapters import optional external dependencies lazily, so this package imports cleanly without the extras installed.

class luxar.gsplats.interop.ClassicalSplats(positions: ndarray, scales: ndarray, quaternions: ndarray, opacities: ndarray, colors: ndarray, sh_degree: int = 0, source_format: str = '', y_up: bool = False)[source]

Bases: object

Decoded classical splats in the source file’s world (x, y, z) frame.

All decode-time nonlinearities are already applied: scales are linear standard deviations (exp applied), opacities are in [0, 1] (sigmoid applied), quaternions are unit-norm w-first, and colors are DC-baked RGB in [0, 1].

positions: ndarray
scales: ndarray
quaternions: ndarray
opacities: ndarray
colors: ndarray
sh_degree: int = 0
source_format: str = ''
y_up: bool = False
property n_splats: int
__init__(positions: ndarray, scales: ndarray, quaternions: ndarray, opacities: ndarray, colors: ndarray, sh_degree: int = 0, source_format: str = '', y_up: bool = False) → None
class luxar.gsplats.interop.TrackingGraph(node_ids: ndarray, t: ndarray, positions: ndarray, edges: ndarray, scale: Tuple[float, float, float] = (1.0, 1.0, 1.0), units: Tuple[str | None, ...] = (None, None, None))[source]

Bases: object

A cell-tracking lineage graph in plain NumPy arrays.

node_ids: ndarray

(N,) node identifiers as stored.

Often not contiguous — the Biohub challenge encodes global_t * 1e9 + cell_id. Positional indices into the coordinate arrays are what the rest of this class works in; index_of() maps ids to them.

t: ndarray

(N,) integer timepoint per node.

positions: ndarray

(N, 3) node coordinates in voxel units, ordered (z, y, x).

edges: ndarray

(E, 2) directed (source_id, target_id) pairs, in node id space.

Cell-tracking edges point forward in time.

scale: Tuple[float, float, float] = (1.0, 1.0, 1.0)

Voxel size (z, y, x) from the GEFF axis metadata (see positions_um()).

units: Tuple[str | None, ...] = (None, None, None)

Physical unit per spatial axis, when the store declares one.

property n_nodes: int
property n_edges: int
property n_timepoints: int

One past the largest timepoint index (0 for an empty graph).

positions_um() → ndarray[source]

(N, 3) positions in physical units — voxel coordinates × voxel size.

__init__(node_ids: ndarray, t: ndarray, positions: ndarray, edges: ndarray, scale: Tuple[float, float, float] = (1.0, 1.0, 1.0), units: Tuple[str | None, ...] = (None, None, None)) → None
index_of() → Dict[int, int][source]

Map node id -> positional index.

edge_indices() → ndarray[source]

(E, 2) edges remapped from node ids to positional indices.

Edges naming a node absent from the store are dropped — a crop of a larger movie can legitimately reference cells outside its own bounds.

divisions() → ndarray[source]

Positional indices of dividing cells (out-degree >= 2).

lineage_ids() → ndarray[source]

(N,) connected-component id per node — one id per lineage tree.

Components are computed on the undirected graph, so a whole lineage (a founder cell and every descendant) shares one id and can be given one colour. Ids are assigned in order of each component’s earliest node.

luxar.gsplats.interop.classical_to_gsplat_data(cs: ClassicalSplats, *, rotate_x180: bool | None = None, flip: str = '') → GSplatData[source]

Convert decoded classical splats to a GSplatData.

The covariance is rebuilt as Σ = (M·R) · diag(scales²) · (M·R)ᵀ where R comes from the quaternion and M is the orientation matrix (_orientation_matrix()), then factorized to Luxar’s packed lower-triangular Cholesky form. Opacities ride in the RGBA color alpha channel (amplitudes are constant 1 — see the inline note); the DC color (display-referred sRGB) is converted to Luxar’s linear-light store via srgb_to_linear(). Columns stay in world (x, y, z) order — that is what downstream dimension inference labels x/y/z.

rotate_x180=None (default) applies the 180°-about-X COLMAP → Y-up fix exactly when the source dialect needs it (cs.y_up False); SPZ declares RUB/Y-up data and is left untouched. Pass an explicit bool to override.

The applied orientation and source dialect are recorded under stats["interop"] so an eventual export can invert them.

luxar.gsplats.interop.detect_classical_format(path: str | Path) → str[source]

Detect which classical dialect path holds.

.splat and .spz are keyed on the extension (.spz additionally verified by the gzip magic); .ply is sniffed from the header — a chunk element marks the SuperSplat compressed dialect, INRIA properties (f_dc_0 / scale_0 / rot_0) mark the reference dialect.

luxar.gsplats.interop.export_inria_ply(input_path: str | Path, output_path: str | Path, **kwargs: object) → int[source]

Export a .gsplats.zarr to an INRIA PLY file; returns the splat count.

Keyword arguments are forwarded to gsplat_data_to_inria_ply(). Partition / nested trees have no flat equivalent — flatten first (luxar gsplat flatten).

luxar.gsplats.interop.gsplat_data_to_inria_ply(data: GSplatData, *, opacity_policy: OpacityPolicy = 'normalized', constant_opacity: float = 1.0, color_source: ColorSource = 'auto', colormap: str | None = None, sh_degree: int = 0, undo_orientation: bool = True, timepoint: int | None = None, slice_dim: int | None = None, slice_index: int | None = None) → bytes[source]

Serialize a GSplatData as an INRIA 3DGS point_cloud.ply.

Parameters:
  • data – Source splats (any matrix shape; the finest content is exported).

  • opacity_policy – normalized (robust rescale of amplitudes into (0, 1), the honest default for unbounded emission weights), amplitude (clip raw values), or constant (fixed constant_opacity). A color alpha channel multiplies into both data-driven policies verbatim, so imported data (amplitudes = 1, opacity in alpha) round-trips verbatim (no rescale; float-precise in opacity, not bit-exact) under the default.

  • color_source – auto = per-splat colors if present, else colormap if given, else white; or force colors / colormap / white.

  • colormap – Colormap name for baking scalar amplitudes to RGB.

  • sh_degree – 0 (default) writes only the DC band; higher degrees emit zero-filled f_rest bands for viewers that insist on them.

  • undo_orientation – Invert the import-time orientation recorded in stats["interop"] so import → export round-trips exactly.

  • slice_index (timepoint / slice_dim /) – 3D selection for nD data; timepoint slices the last (stacked) dimension (see _select_3d()).

Returns:

The complete PLY file contents.

luxar.gsplats.interop.gsplats_to_tracksdata_graph(gsplats: GSplatData, frame_shape: tuple[int, ...], *, t: int = 0, n_sigma: float = 2.0, graph: BaseGraph | None = None, sort_by_amplitude: bool = True) → BaseGraph[source]

Insert every splat of gsplats as a node at time t in a tracksdata graph.

Each node carries the default tracksdata attributes t, mask (tracksdata.nodes.Mask) and bbox, plus amplitude and the per-axis position (z/y/x for the trailing dims). Call repeatedly with increasing t (and the same graph) to build a time-lapse, then use tracksdata’s edge operators to link nodes into lineages.

Parameters:
  • gsplats (GSplatData) – Fitted splats. gsplats.ndim must equal len(frame_shape).

  • frame_shape (tuple[int, ]) – Spatial shape the masks are rasterized into.

  • t (int) – Timepoint to assign to all nodes added by this call.

  • n_sigma (float) – Mask support radius passed to splat_mask_and_bbox().

  • graph (tracksdata.graph.BaseGraph | None) – Graph to add to; a new in-memory (RustWorkX) graph is created if None.

  • sort_by_amplitude (bool) – Add nodes in ascending alpha-effective amplitude (A·α) order so brighter splats paint last in a tracksdata.array.GraphArrayView (matches the original POC). The amplitude node attribute is likewise A·α, so the ranking is meaningful for imported classical splats (whose raw amplitude is a constant 1, with opacity in the color alpha channel).

Returns:

The graph the nodes were added to.

Return type:

tracksdata.graph.BaseGraph

Raises:

ImportError – If the optional tracksdata extra is not installed.

luxar.gsplats.interop.import_gsplats(path: str | Path, *, format: str = 'auto', rotate_x180: bool | None = None, flip: str = '') → GSplatData[source]

Read a classical Gaussian-splat file into a GSplatData.

Parameters:
  • path – Source file (.ply — INRIA or SuperSplat compressed, .splat, .spz) or a PlayCanvas SOG bundle (a directory with meta.json + WebPs, that meta.json, or a .sog ZIP).

  • format – One of auto (default, sniffed via detect_classical_format()) or an explicit dialect name from CLASSICAL_FORMATS.

  • rotate_x180 – Apply the canonical COLMAP → Y-up orientation fix (180° rotation about X). Default None = per-dialect (on for the Y-down dialects INRIA/.splat/SuperSplat, off for Y-up SPZ).

  • flip – Additional axes to mirror, e.g. "x" or "xz".

Returns:

A single-leaf GSplatData with per-splat colors, ready for .save(), LOD recipes, or Scene.add_gsplats_from_data.

luxar.gsplats.interop.quat_to_rotmat(q: ndarray) → ndarray[source]

Convert unit quaternions (N, 4) (w, x, y, z) to rotation matrices (N, 3, 3).

Quaternions are re-normalized defensively; zero-norm quaternions decode to the identity rotation.

luxar.gsplats.interop.read_antimatter_splat(path: str | Path) → ClassicalSplats[source]

Read an antimatter15 .splat file (flat 32-byte records).

Scales are stored linear (exp already applied by the converter), color is DC-baked RGB with opacity in the alpha byte, and the rotation is the unit quaternion quantized as round(q * 128 + 128) in (w, x, y, z) order.

luxar.gsplats.interop.read_geff(path: str | Path) → TrackingGraph[source]

Read a .geff tracking graph into a TrackingGraph.

Parameters:

path – Path to the .geff store root (the directory holding zarr.json).

Returns:

Node ids, timepoints, voxel-space ZYX positions, edges, and the voxel size read from the GEFF axis metadata.

Return type:

TrackingGraph

Raises:

ValueError – If the store is not a GEFF group, or lacks the t/z/y/x node properties this reader needs.

luxar.gsplats.interop.read_inria_ply(path: str | Path) → ClassicalSplats[source]

Read an INRIA-style 3DGS point_cloud.ply.

The header drives the layout: the SH degree is derived from the number of f_rest_* properties, so degree-0..3 files all parse. f_rest bands (view-dependent color) are dropped; the DC band is baked to RGB.

luxar.gsplats.interop.read_sog(path: str | Path) → ClassicalSplats[source]

Read a PlayCanvas SOG (Spatially Ordered Gaussians) bundle → ClassicalSplats.

SOG v2 is a meta.json referencing lossless WebP images; path may be the bundle directory, its meta.json, or a .sog ZIP. Per-Gaussian attributes are co-located across images (same pixel = same Gaussian):

  • means_l/means_u — 16-bit-per-axis position, dequantized into the per-axis [mins, maxs] log domain, then the symmetric log is undone (sign(n)·(exp|n|−1)).

  • scales — RGB indices into a 256-entry log-domain codebook (exp).

  • quats — smallest-three: three stored components in (w,x,y,z) order mapped to [−√½, +√½], the omitted (largest) component recovered as √(1−Σ) and its slot given by alpha − 252.

  • sh0 — RGB indices into a DC codebook (0.5 + c·SH_C0) + opacity in alpha.

Higher-order SH (shN) is intentionally dropped — the DC-only policy shared with the other classical dialects.

luxar.gsplats.interop.read_spz(path: str | Path) → ClassicalSplats[source]

Read a Niantic/Scaniverse .spz file (gzipped quantized splats).

Supports the legacy gzip container (versions 1–3, the format written by Scaniverse and shipped as the official samples). The v4 “NGSP” container (per-attribute ZSTD streams) is detected and rejected with a clear error. SPZ data is RUB (right-up-back, the three.js convention) — already Y-up.

luxar.gsplats.interop.read_supersplat_ply(path: str | Path) → ClassicalSplats[source]

Read a PlayCanvas/SuperSplat compressed .ply (chunked, bit-packed).

Layout (splat-transform reference): a chunk element with 12 or 18 float32 min/max bounds per 256-splat chunk, and a vertex element of four uint32 bitfields per splat (position 11-10-11, rotation 2+10-10-10 smallest-three, scale 11-10-11 in log space, color 8-8-8-8). The optional sh element (f_rest bands) is dropped (DC-only policy).

luxar.gsplats.interop.rotmat_to_quat(R: ndarray) → ndarray[source]

Convert rotation matrices (N, 3, 3) to unit quaternions (N, 4) (w, x, y, z).

Uses Shepperd’s method (branch on the largest diagonal combination) for numerical stability near 180° rotations. Inputs must be proper rotations (det = +1); the caller is responsible for reflection correction.

luxar.gsplats.interop.splat_mask_and_bbox(center: ndarray, cholesky_factor: ndarray, frame_shape: tuple[int, ...], *, n_sigma: float = 2.0) → tuple[ndarray, ndarray][source]

Rasterize one splat’s n_sigma support to a local boolean mask + bbox.

The covariance is Sigma = L @ L.T for the lower-triangular Cholesky factor L = cholesky_factor. A voxel x is inside the support iff its Mahalanobis distance ||L^-1 (x - center)|| <= n_sigma.

Rather than evaluating that over the whole frame (O(n_voxels) per splat), the axis-aligned bounding box of the n_sigma ellipsoid is computed analytically — the half-extent along axis k is n_sigma * sqrt(Sigma_kk) = n_sigma * ||L[k]|| — and only that local box is rasterized, then clamped to frame_shape.

Parameters:
  • center (np.ndarray) – Splat center, shape (d,), in voxel coordinates matching frame_shape.

  • cholesky_factor (np.ndarray) – Lower-triangular Cholesky factor of the covariance, shape (d, d).

  • frame_shape (tuple[int, ]) – Shape of the frame the masks live in, length d.

  • n_sigma (float) – Mahalanobis radius of the support (default 2.0).

Returns:

  • bbox (np.ndarray) – [start_0, ..., start_{d-1}, stop_0, ..., stop_{d-1}] (int, half-open, clamped to frame_shape).

  • mask (np.ndarray) – Boolean array of shape stop - start (empty if the box is degenerate).

Preprocessing

Volume preprocessing shared by fitting and calibration (background-floor suppression, normalization, denoising).

Preprocessing for Gaussian splat fitting.

This subpackage provides GPU-accelerated preprocessing operations for scientific volumes, with a focus on denoising before Gaussian splat fitting. Noisy data wastes splats on background artifacts; denoising first leads to more efficient, higher-quality fits.

Key Features: - Non-Local Means (NLM) denoising for 2D images and 3D volumes - Three-tier backend: skimage (CPU reference), PyTorch (GPU), CUDA (maximum perf) - Automatic backend selection based on device and availability - Noise2Self (J-invariant) calibration for automatic h parameter selection

Example

>>> import torch
>>> from luxar.gsplats.preprocessing import denoise_nlm, calibrate_nlm_h
>>>
>>> volume = torch.randn(64, 128, 128)  # noisy 3D volume
>>>
>>> # Auto-calibrate denoising strength
>>> h = calibrate_nlm_h(volume, device='cuda')
>>>
>>> # Denoise with best available backend
>>> denoised = denoise_nlm(volume, h=h, device='cuda')
luxar.gsplats.preprocessing.calibrate_all_channels(input_path: Path, n_timepoints: int, n_channels: int, channel_indices: list[int] | None = None, timepoint_indices: list[int] | None = None, array_key: str | None = None, calibration_samples: int = 5, patch_size: int = 3, search_distance: int = 5, backend: str = 'auto', device: str | None = None, h_override: float | None = None, axes: str | None = None) → dict[int, float][source]

Calibrate NLM h for all channels.

axes explicitly labels the source array when positional slicing is unsafe.

Returns:

Mapping from channel index to calibrated h value.

Return type:

dict[int, float]

luxar.gsplats.preprocessing.calibrate_h_for_channel(input_path: Path, channel: int, sample_timepoints: list[int], array_key: str | None = None, patch_size: int = 3, search_distance: int = 5, backend: str = 'auto', device: str | None = None, axes: str | None = None) → float[source]

Calibrate NLM h for one channel by sampling timepoints.

Loads the central 2D slice at each sample timepoint, normalizes to [0,1], runs Noise2Self calibration, and returns the median h across timepoints. axes explicitly labels the source array when positional slicing is unsafe.

luxar.gsplats.preprocessing.calibrate_nlm_h(volume: torch.Tensor, h_range: Sequence[float] | None = None, patch_size: int = 3, search_distance: int = 5, *, stride: int = 2, backend: str = 'auto', device: str | torch.device | None = None, use_2d_slice: bool = True, slice_index: int | None = None) → float[source]

Find optimal NLM filtering strength h via Noise2Self cross-validation.

The J-invariant method masks subsets of pixels, denoises without their contribution, then measures how well the denoised values predict the held-out originals. The h with the lowest mean squared error wins.

Parameters:
  • volume (torch.Tensor) – 2D (H, W) or 3D (D, H, W) input tensor.

  • h_range (sequence of float, optional) – Candidate h values. Default: arange(0.005, 0.08, 0.005).

  • patch_size (int) – Comparison patch side length (odd).

  • search_distance (int) – Search window half-size.

  • stride (int) – Stride for J-invariant mask grid. Smaller stride = more masks = more accurate but slower. Default 2 (4 masks in 2D, 8 in 3D).

  • backend (str) – Backend for the inner denoise_nlm calls.

  • device (str or torch.device, optional) – Target device for calibration computation.

  • use_2d_slice (bool) – If True and volume is 3D, calibrate on a single 2D slice for speed (matching the typical microscopy workflow). For volumes of other dimensionality (1D, 2D, or 4D+), this flag is silently ignored and calibration runs on the full volume — graceful fallback rather than an error, since the slice optimization is only meaningful for the 3D microscopy case.

  • slice_index (int, optional) – Which z-slice to use when use_2d_slice=True and the volume is 3D. Default: middle slice. Ignored for non-3D volumes.

Returns:

Optimal h parameter.

Return type:

float

luxar.gsplats.preprocessing.denoise_nlm(volume: torch.Tensor, h: float, patch_size: int = 3, search_distance: int = 5, *, backend: str = 'auto', device: str | torch.device | None = None, chunk_size: int | None = None) → torch.Tensor[source]

Non-Local Means denoising for 2D images and 3D volumes.

For each pixel/voxel x, computes a weighted average over a local search neighbourhood. Weights are derived from the similarity of small patches centred on x and each neighbour y:

NLM(x) = sum_y w(x,y) * I(y) / sum_y w(x,y) w(x,y) = exp( -||P(x) - P(y)||^2 / (patch_vol * h^2) )

Three backends are available, selected automatically or via backend:

  • 'cuda' — bare-metal CUDA kernel (fastest, requires compiled ext)

  • 'pytorch' — pure-PyTorch GPU implementation

  • 'skimage' — scikit-image CPU reference (slowest, always available)

Parameters:
  • volume (torch.Tensor) – 2D (H, W) or 3D (D, H, W) input tensor, float32, ideally normalised to [0, 1].

  • h (float) – Filtering strength. Larger values smooth more aggressively.

  • patch_size (int) – Side length of comparison patches (must be odd). Default 3.

  • search_distance (int) – Half-size of the search window around each pixel/voxel. Default 5.

  • backend (str) – 'auto' (default), 'cuda', 'pytorch', or 'skimage'.

  • device (str or torch.device, optional) – Target device. If None, uses volume.device.

  • chunk_size (int, optional) – For large 3D volumes, process in overlapping chunks of this many slices along dim 0. Only used by the 'pytorch' backend.

Returns:

Denoised tensor, same shape, dtype, and device as volume.

Return type:

torch.Tensor

luxar.gsplats.preprocessing.denoise_volume_array(volume: ndarray, h: float, patch_size: int = 3, search_distance: int = 5, backend: str = 'auto', device: str | None = None, use_2d: bool = False, chunk_size: int | None = None, norm_range: tuple[float, float] | None = None) → ndarray[source]

Denoise a single 3D volume (or 2D image) with NLM.

Normalizes to [0,1], denoises, denormalizes back.

Parameters:
  • volume (np.ndarray) – Float32 input volume (2D or 3D).

  • h (float) – NLM filtering strength (calibrated in [0,1] normalized space).

  • use_2d (bool) – If True, denoise slice-by-slice (2D) instead of full 3D.

  • chunk_size (int, optional) – Process 3D volumes in overlapping chunks of this many Z-slices. Auto-computed to keep memory under ~16 GB if not specified. Only used with the pytorch backend for 3D volumes.

  • norm_range (tuple[float, float], optional) – Fixed whole-volume (vmin, vmax) to normalize against instead of this array’s own min/max. Tiled callers pass the global range so a fixed h yields scale-consistent smoothing across all tiles.

luxar.gsplats.preprocessing.denormalize_volume(volume: ndarray, vmin: float, vmax: float) → ndarray[source]

Reverse [0, 1] normalization.

luxar.gsplats.preprocessing.normalize_volume(volume: ndarray, value_range: tuple[float, float] | None = None) → tuple[ndarray, float, float][source]

Normalize float32 volume to [0, 1] range.

Parameters:
  • volume (np.ndarray) – Float32 input volume.

  • value_range (tuple[float, float], optional) – Fixed (vmin, vmax) to normalize against instead of the volume’s own min/max. Used by tiled callers so a fixed NLM h means the same smoothing strength in every tile (each tile is normalized against the WHOLE-volume range, not its own extent).

Returns:

  • normalized (np.ndarray) – Volume scaled to [0, 1].

  • vmin (float) – Minimum actually used (for denormalization).

  • vmax (float) – Maximum actually used (for denormalization).

GPU Profiling

GPU memory and performance profiling for automatic tile-size selection.

GPU benchmark profile management for multi-GPU systems.

Stores and aggregates benchmark results in a YAML file at ~/.luxar/gpu_profiles.yaml. Each GPU (keyed by its canonical name) can have multiple benchmark runs; a summary is recomputed on every append using average throughput and conservative (min) OOM boundaries.

This module is consumed by luxar gsplat batch-fit to auto-select tile sizes and estimate wall times.

luxar.gsplats.gpu_profile.load_profiles(path: Path = PosixPath('/home/runner/.luxar/gpu_profiles.yaml')) → Dict[str, Any][source]

Load the multi-GPU profile YAML.

Returns an empty structure if the file does not exist. On first access, auto-migrates a v1 profile if found.

luxar.gsplats.gpu_profile.save_profiles(profiles: Dict[str, Any], path: Path = PosixPath('/home/runner/.luxar/gpu_profiles.yaml')) → None[source]

Write the multi-GPU profile YAML atomically.

luxar.gsplats.gpu_profile.append_run(gpu_name: str, run_data: Dict[str, Any], gpu_info: Dict[str, Any], path: Path = PosixPath('/home/runner/.luxar/gpu_profiles.yaml')) → None[source]

Append a benchmark run for a GPU and recompute the summary.

Parameters:
  • gpu_name – Canonical GPU name (e.g. "NVIDIA GeForce RTX 3090 Ti").

  • run_data – Dict with keys throughput, oom_boundaries, splat_sweep (optional), recommendations, timestamp, cuda_version, pytorch_version, free_memory_gb.

  • gpu_info – Dict with keys total_memory_gb, compute_capability, sm_count.

  • path – Profile YAML path.

luxar.gsplats.gpu_profile.recompute_summary(runs: List[Dict[str, Any]]) → Dict[str, Any][source]

Recompute aggregated summary from all benchmark runs.

  • Throughput: average across runs (more stable).

  • OOM boundaries: min across runs (conservative — never recommend a size that OOMed even once).

  • Recommendations: derived from averaged throughput.

luxar.gsplats.gpu_profile.get_gpu_summary(gpu_name: str | None = None, gpu_mem: float | None = None, path: Path = PosixPath('/home/runner/.luxar/gpu_profiles.yaml')) → Dict[str, Any] | None[source]

Get the summary for a GPU.

Parameters:
  • gpu_name – Exact GPU name. If None, tries auto-detection via torch.cuda, then falls back to the only profiled GPU.

  • gpu_mem – If set and gpu_name is None, pick the profile whose total_memory_gb is closest to this value.

  • path – Profile YAML path.

Returns:

Summary dict or None if no matching profile exists.

luxar.gsplats.gpu_profile.get_gpu_throughput_table(gpu_name: str | None = None, path: Path = PosixPath('/home/runner/.luxar/gpu_profiles.yaml')) → List[Dict[str, Any]] | None[source]

Get the averaged 3D throughput table for a GPU.

Returns list of throughput entries sorted by voxel count, or None.

luxar.gsplats.gpu_profile.list_profiled_gpus(path: Path = PosixPath('/home/runner/.luxar/gpu_profiles.yaml')) → List[str][source]

List all GPU names that have profiles.

luxar.gsplats.gpu_profile.migrate_v1_profile(old_path: Path, new_path: Path = PosixPath('/home/runner/.luxar/gpu_profiles.yaml')) → bool[source]

Migrate old single-GPU profile (v1) to the new multi-GPU format.

Reads the old YAML, wraps its data as a single run, writes to the new location. Renames the old file to .bak.

Returns:

True if migration happened, False if old file not found or invalid.

Volume Rendering

Render Gaussian splats back to volume arrays for quality comparison.

GSplats rendering module.

This module provides high-performance rendering functions for Gaussian splats, with automatic backend selection (CUDA, MPS, CPU).

luxar.gsplats.rendering.render_to_volume(gsplat_data: GSplatData, shape: Tuple[int, ...], device: str | None = None, truncate: float = 2.75, intensity_floor: float = 1e-05, chunk_size: int | None = None) → np.ndarray[source]

Render Gaussian splats to a volume using GPU-accelerated rendering.

This function automatically selects the fastest available backend (CUDA, MPS, or CPU) and uses the optimized PyTorch renderer from the models package.

Parameters:
  • gsplat_data (GSplatData) – The Gaussian splat data to render, containing centers, Cholesky factors, and amplitudes.

  • shape (Tuple[int, ]) – Output volume shape (e.g., (128, 128, 128) for 3D).

  • device (str, optional) – Device to use for rendering. None and "auto" auto-detect the best device. Options: “auto”, “cuda”, “mps”, “cpu”.

  • truncate (float, default DEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations. Gaussians are evaluated within this radius from their centers.

  • intensity_floor (float, default 1e-5) – Minimum intensity threshold for amplitude-aware culling. Splats with contributions below this threshold are culled early for performance.

  • chunk_size (int, optional) – Chunk size for memory management when processing large volumes. If None, automatically calculated based on available memory.

Returns:

Rendered volume with the same shape as specified, as a NumPy array.

Return type:

np.ndarray

Examples

>>> from luxar.gsplats.rendering import render_to_volume
>>> volume = render_to_volume(gsplat_data, shape=(128, 128, 128))

Notes

  • The rendering uses the fast PyTorch renderer with specialized 2D/3D fast paths

  • For 8K splats on 128³ volume: substantially faster than NumPy implementation (often orders of magnitude on GPU; varies by hardware)

  • Supports nD rendering with automatic chunking to prevent OOM

  • Uses standard Gaussian falloff: exp(-0.5 * ||y||^2)

luxar.gsplats.rendering.render_to_volume_tensor(gsplat_data: GSplatData, shape: Tuple[int, ...], device: str | None = None, truncate: float = 2.75, intensity_floor: float = 1e-05, chunk_size: int | None = None) → torch.Tensor[source]

Render Gaussian splats to a volume, returning a GPU tensor.

Same as render_to_volume() but returns a torch.Tensor on the rendering device instead of a NumPy array. This avoids an unnecessary GPU → CPU copy when the result will be consumed by further GPU operations (e.g. quality-metric computation).

Parameters:
  • gsplat_data (GSplatData) – The Gaussian splat data to render.

  • shape (Tuple[int, ]) – Output volume shape (e.g., (128, 128, 128) for 3D).

  • device (str, optional) – Device to use for rendering. None and "auto" auto-detect the best device.

  • truncate (float, default DEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations.

  • intensity_floor (float, default 1e-5) – Minimum intensity threshold for amplitude-aware culling.

  • chunk_size (int, optional) – Chunk size for memory management when processing large volumes.

Returns:

Rendered volume on the rendering device.

Return type:

torch.Tensor