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 throughluxar.gsplats.lod(whoselod/additive.pyimportsscipy.sparse) when authored ladder stats are recomputed after the intensity ops (scale_intensity/normalize_intensity/clamp_intensity/affine_intensity), afilter/filter_bythat actually removes splats, aslice_bythat crops, the heuristiccullmethodscumulative/amplitude_percentile/combined(hence a barecull(), whoseautoresolves tocumulative),embed_dimension, and a strictadditive_prefixview.'torch', imported earlier still — the rendering-basedcullmethodserror_budget/redundancy, and therefore anautohanded atargetor ashape.
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, orNone) –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 withseed_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, default0.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, orNone, 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_percentileunless 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 (
tupleoffloat, orNone, defaultNone) – Explicit(image_min, image_max)for normalization, replacing the pairnorm_percentilewould derive fromVitself. 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 withimage_max > image_min. Because such a range is a bounded-sample estimate, a voxel aboveimage_maxis left unclipped whennorm_percentile == 0(and the autoamp_maxrises with it), rather than flattening the brightest structure. Leave None for a whole-volume fit — the array already IS the volume.downscale (
int,sequenceofint, orNone, defaultNone) – 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=4reduces 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 (
floatorNone, defaultNone) – 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, default1000) – Maximum number of optimization iterations. Default is generous to allow max_abs_error convergence criterion to work effectively.lr (
float, default0.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, default1.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, defaultNone (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, defaultNone (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] * fractionindependently. E.g.,sigma_max_diag=1/16on a (50, 200, 300) volume gives[3.125, 12.5, 18.75].
amp_max (
floatorNone, defaultNone (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 (
floatorNone, default10.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, default2.75) – Truncation radius in standard deviations for rendering efficiency.device (
str, optional) – PyTorch device (“auto”, “cpu”, “cuda”, “mps”). Auto-detects forNoneor"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, defaultTrue) – Whether to print optimization progress.max_abs_error (
floatorNone, defaultNone (auto:0.01)) – Maximum absolute error threshold for convergence. If specified, optimization stops whenmax(|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 (
floatorNone, defaultNone) – 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 (
floatorNone, defaultNone) – 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, default15) – Scheduler patience: iterations without loss improvement before LR reduction.lr_reduction_factor (
float, default0.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], default300) – 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, defaultTrue) – Enable dynamic operations (seeding and pruning).dynamic_config (
DynamicOpsConfig, optional) – Configuration for dynamic operations. Uses defaults if None.dynamic_ops_verbose (
bool, defaultFalse) – Enable detailed console logging for dynamic operations. Shows residual analysis, seeding attempts and pruning operations.napari_movie (
bool, defaultFalse) – Record optimization movie for napari visualization. Enable this to create a time-series visualization of optimization progress.movie_every (
int, default1) – Record movie frame every N iterations.movie_max_frames (
int, defaultNone (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, defaultTrue) – 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, defaultTrue) – 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 (
floatorNone, default0.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 toNoneto disable.voxel_footprint_correction (
bool | float, defaultFalse) – 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 (
floatorNone, defaultNone) – 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, defaultFalse) – 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 - Autoinit_sigma: based on physical dimensions - Output coordinates: converted to physical space (seeoutput_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, defaultFalse) –Which intensity convention the amplitudes of a
seeds=GSplatDatacarry. Ignored for every other kind ofseeds.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 activeflooris 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, withimage_minnever added back), hence also what a.gsplats.zarrWRITTEN BY a fit (gsplat fit/gsplat lod) carries. They are rescaled asa / intensity_range.
A store that was IMPORTED (
gsplat importmaps 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
floorbe subtracted twice, initializing every seed dimmer than the floor to exactly 0; declaring True on raw amplitudes starts every seed too bright byfloor / intensity_range(#1172).source_dtype (
strorNone, defaultNone) – Element type of the volume as it was ACQUIRED (e.g."uint16"), when that differs fromV.dtype. Recorded instats["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 (asluxar.io.volume.load_volumedoes), otherwise the recorded source size describes the float working copy and overstates compression by the cast’s inflation factor. If None,V.dtypeis used.source_shape (
sequenceofintorNone, defaultNone) – 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 measuringVwould quote compression against the working copy. Pair it withsource_dtype— a declared grid with the cast’s dtype is still the wrong denominator. Recorded withstats["source_declared"] = Trueso 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:
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_passsplats to the current residual (clamp(V - render(accumulated), min=0)). Passes continue untilmax_splatsis reached, PSNR improvement drops belowpsnr_patiencedB, 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 untilmax_splatsis reached or PSNR patience triggers.asymmetric_penalty (
float, optional) – Asymmetric loss penalty factor (default 10.0).enable_dynamic_ops (
bool, defaultFalse) – 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 toTrueto opt in.cull_retention (
floatorNone, default0.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 toNoneto 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, default500) – Minimum optimizer iterations for residual passes (pass 1+). Pass 0 always honoursiters_per_passdirectly. The default of 500 is the historical floor that protects fit quality when callers supply a smalliters_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_completeand thestatsdict (stats['n_passes'],stats['pass_psnrs']); to build a streamable LOD ladder, hand the result toluxar.gsplats.lod.make_additive_lod().- Return type:
Notes
GPU utilization: Each pass fits only
max_splats_per_passsplats, 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--progressivewith--parallelto 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,IntensityMixinContainer for Gaussian splat data with always-LOD structure.
Every
GSplatDataholds one or more LOD levels (AdditiveSubLODinstances). A single-LOD dataset is simplyadditive_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, andlabel_idsare 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_sublodsmatrix API is preserved as derived finest-first views overself._node(which is stored coarsest-first, matching disk)._nodeis the internal fast path (used byfrom_tree()) that stores a pre-built node verbatim.
- 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’sstats(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 + 1LODs.- Raises:
IndexError – If
levelis 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 == 1and its lone substitutive level carries the additive ladder ofself’s levellevel. 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 whichlod --recipe overviewbuilds its merged coarse cap (asat_substitutive(n - 1).flattened()) and published the input fit’spsnr_dbon it. Level 0 is the finest content itself and keeps them. (_view_of_levelitself does not scrub:_map_substitutivewalks 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_statsQ / 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.pybuilds every coarse child of akind=lodgroup withat_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
methodparameter 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 thanredundancy_thresholdof 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_predand 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.shapewhen 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_KEYSand_CONTENT_SCOPED_OP_RECORD_KEYS): this is the single chokepoint every mask-based rewrite goes through —filter_by,slice_byand everycullstrategy — 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_dimsaxes; seescale()). The recommended “remove large diffuse background” knob — cleaner thanvolumeon 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_neighborsother splats withinneighbor_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 == 1containing 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
SubstitutiveLevelcarries its own additive ladder (one or moreAdditiveSubLOD). The resultingGSplatDatahasn_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
GSplatDatawith the given substitutive × additive matrix.
- classmethod from_tree(node: GSplatNode, stats: Dict[str, Any] | None = None) GSplatData
Construct a
GSplatDatafrom a matrix-shaped tree node.Accepts a bare
GSplatLeafor aGSplatLodGroupof leaves (the inverse oftree). Genuinely nested trees (partitions, or lod groups with non-leaf children) have no flatGSplatDataequivalent 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.
- 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)
- 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,);+infwhere a group has<= ksplats (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=partitiontree 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 ofPER_PART_RECIPES:stream→tilestopology,levels→adaptive) each part is given its OWN LOD viabuild_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-partgsplat lodpass (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_treeor embed in a scene) — a partition has no flat-matrixGSplatDataequivalent.bsp_treeis the decomposition’s serialized split planes — the producer’s, since this method is handed a decomposition rather than computing one (contrastto_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 inregion_labelsspace (default: the positions ofregions), 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 attruncation_radiussigmas, so the radius istruncation_radius * semi_axis.anisotropy=True→ the largest principal semi-axissqrt(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-axisdet(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 toself.truncation_radius.intensity_floor (
float, default1e-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).multipliermust 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 (seesoft_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.
16preserves the historical default;8opts into uint8 geometric-log codes;"auto"uses 8 bits only for 8-bit integer sources recorded instats["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 thecoarsen_dimscomplement in stats, else per-leaf auto-detect — seewrite_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_fractionLOD 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. Seecore.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_modeknob.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),scaledefaults 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.
Nonein 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).
widthis 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=partitiontree node.Recursively BSP-splits the splat centers so each part holds at most
max_elementssplats, using the shared splitters inluxar.core.group.partition(the same machinery the scene uses). Returns aGSplatPartition(a tree node, not aGSplatData— a partition has no flat-matrix equivalent); write it withwrite_gsplats_tree(one self-containedkind=partitionfile) or embed it in a scene. Each part gets its ownposition_boundsat 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.treenode subtree.The tree is the single in-memory ground truth (this just returns the stored node), behind the v3.0
.gsplats.zarrformat and the scene gsplat-node subtree. For the matrix shape it is one of: a singleGSplatLeaf(one substitutive level) or aGSplatLodGroupof leaves (multiple levels, coarsest first). Per-level provenance rides in each leaf’smeta. The view-drivencoverage_fractionthresholds are derived at serialize time (seesave()/ 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 GSplatData.additive_sublods: List['AdditiveSubLOD']
The finest level’s additive ladder (the “primary” sub-LODs).
Returns a fresh list (the
AdditiveSubLODelements are shared) so a caller mutating it cannot corrupt the ground-truth node or desync the cachedcenters/n_splats— matching the pre-refactor defensive copy and the siblingsubstitutive_levelsview’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 (
floorinfit_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.ndarrayorzarr.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 (
intortupleofint, default256) – Tile size per axis in voxels. Scalar is broadcast to all axes.overlap (
intortupleofint, default32) – Overlap width per axis in voxels. Scalar is broadcast.voxel_size (
floatorsequenceoffloat, optional) – Physical voxel spacing, forwarded to per-tile fitting.output_space (
str, default"real") – Coordinate space for output centers ("real"or"voxel").verbose (
bool, defaultTrue) – Print per-tile progress with arbol.progressive (
bool, defaultFalse) – 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, default5000) – Maximum splats per progressive pass (ignored if progressive=False).psnr_patience (
float, default0.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, defaultTrue) –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, thefit --tiling uniformsequential and-j Npaths, thefit --tile k/Mworker, and everybatch-fitproducer all build it. On(108, 1352, 532)attile_size=512, overlap=32that 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 acontent_hash. PassFalseto reproduce a historical grid.A folded tile also EXCEEDS
tile_size: it spans up totile_size + overlap - 1voxels on the folded axis, so peak per-tile memory is that much above whattile_sizealone suggests (256/32 -> 287, 1.41x the voxels in 3D; 256/64 -> 319, 1.93x; 24/8 -> 31, 2.16x). Sizetile_sizefor that worst case.tile_seed_counts (
sequenceofint, optional) – Exact per-tile integer seed counts in grid order. When provided, this overridesseedsfor each tile; its length must match the resolved grid. This is the CLI handoff for occupancy-weighted whole-volume budgets. A0entry means “this tile holds no signal”: the tile is NOT fitted and contributes a 0-splat placeholder, mirroring the--allow-empty-tilebatch worker (the inner fitter rejects a non-positive integerseeds).cull_retention (
floatorNone, default0.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 toNoneto disable.source_shape (
sequenceofint, optional) – Grid of the ACQUISITION, whenvolumeis 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.Nonemeasuresvolumeitself, which is right whenever nothing was preprocessed.source_dtype (
str, optional) – Element type the volume was STORED in, for the same reason as infit_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_iterswhenprogressive=True).seedsis 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 — unlesstile_seed_countsoverrides it per tile, which is what the CLI normally passes. The CLI’s--seedsIS 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 shareluxar.cli.gsplat_ops.fitting.fit_utils.split_seeds_across_tileswhere 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
statsfor a flat result and in the root node’s in-memorymeta["fit_stats"]for a tree, ready for the CLI writer to persist at the store root.- Return type:
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
volumeand 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_GBoverrides both budgets (0declines outright). Over budget, or on a failure, it says so even whenverbose=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--progressivewith--parallelto 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.ndarrayorzarr.Array) – Full volume (or lazy zarr array). Only the tile’s slice is materialized into memory viavolume[spec.slices].spec (
TileSpec) – Tile specification fromcompute_tile_specs(). Contains the per-face overlap sizes used for cosine window construction.voxel_size (
floatorsequenceoffloat, optional) – Physical voxel spacing. Passed through to the per-tile fitter (bothfit_gaussian_splats()and the progressive fitter) and used for correct center translation whenoutput_space="real".output_space (
str, default"real") – Coordinate space for output centers ("real"or"voxel").progressive (
bool, defaultFalse) – 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, default5000) – Maximum splats per progressive pass (ignored if progressive=False).psnr_patience (
float, default0.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.
seedshere 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--seedsis a whole-volume budget, divided before reaching this function: normally into one exact occupancy-weighted count per tile (tile_seed_countsonfit_tiled(),--tile-seed-counton a worker, both fromluxar.cli.gsplat_ops.fitting.fit_utils._weighted_uniform_seed_counts), and by the equal-share fallbackluxar.cli.gsplat_ops.fitting.fit_utils.split_seeds_across_tileswhere 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 wholevolumeviaresolve_volume_floor_denoised()(so independent tile workers agree on one level), with the “would erase all signal” guard applied. With_denoise_h/_denoise_paramsinfit_kwargsthat 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 apNNspec only (seeresolve_volume_floor_denoised()). A numeric value is taken at face value — the caller is expected to have guarded it (asfit_tiled()does withguard_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 withfloor="none"and the applied level is recorded inresult.stats["floor"](withimage_min/image_maxshifted back into the input volume’s units).
- Returns:
Fit result with centers in global volume coordinates (a single flat LOD;
progressive=Truechanges how each tile is optimized, not the structure of the result).- Return type:
- Raises:
ValueError – If
seedsin 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 (
floorinfit_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.ndarrayorzarr.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 (
intortupleofint, default256) – Tile size per axis in voxels. Scalar is broadcast to all axes.overlap (
intortupleofint, default32) – Overlap width per axis in voxels. Scalar is broadcast.voxel_size (
floatorsequenceoffloat, optional) – Physical voxel spacing, forwarded to per-tile fitting.output_space (
str, default"real") – Coordinate space for output centers ("real"or"voxel").verbose (
bool, defaultTrue) – Print per-tile progress with arbol.progressive (
bool, defaultFalse) – 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, default5000) – Maximum splats per progressive pass (ignored if progressive=False).psnr_patience (
float, default0.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, defaultTrue) –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, thefit --tiling uniformsequential and-j Npaths, thefit --tile k/Mworker, and everybatch-fitproducer all build it. On(108, 1352, 532)attile_size=512, overlap=32that 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 acontent_hash. PassFalseto reproduce a historical grid.A folded tile also EXCEEDS
tile_size: it spans up totile_size + overlap - 1voxels on the folded axis, so peak per-tile memory is that much above whattile_sizealone suggests (256/32 -> 287, 1.41x the voxels in 3D; 256/64 -> 319, 1.93x; 24/8 -> 31, 2.16x). Sizetile_sizefor that worst case.tile_seed_counts (
sequenceofint, optional) – Exact per-tile integer seed counts in grid order. When provided, this overridesseedsfor each tile; its length must match the resolved grid. This is the CLI handoff for occupancy-weighted whole-volume budgets. A0entry means “this tile holds no signal”: the tile is NOT fitted and contributes a 0-splat placeholder, mirroring the--allow-empty-tilebatch worker (the inner fitter rejects a non-positive integerseeds).cull_retention (
floatorNone, default0.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 toNoneto disable.source_shape (
sequenceofint, optional) – Grid of the ACQUISITION, whenvolumeis 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.Nonemeasuresvolumeitself, which is right whenever nothing was preprocessed.source_dtype (
str, optional) – Element type the volume was STORED in, for the same reason as infit_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_iterswhenprogressive=True).seedsis 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 — unlesstile_seed_countsoverrides it per tile, which is what the CLI normally passes. The CLI’s--seedsIS 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 shareluxar.cli.gsplat_ops.fitting.fit_utils.split_seeds_across_tileswhere 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
statsfor a flat result and in the root node’s in-memorymeta["fit_stats"]for a tree, ready for the CLI writer to persist at the store root.- Return type:
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
volumeand 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_GBoverrides both budgets (0declines outright). Over budget, or on a failure, it says so even whenverbose=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--progressivewith--parallelto 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;
Nonerecords 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 reconstructV - 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--denoisethat 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 nonedenoises 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--denoiseis 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_sourceis 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:
objectSpecification for a single tile within a larger volume.
- index
Flat index in [0, N) where N is total tile count. Used for
--tile N/MCLI addressing.- Type:
- origin
Global coordinate offset of this tile’s
[0, 0, ...]corner. Equal to the start of each slice, as float for voxel_size compatibility.
- border_low
Per-axis: True if the tile is the first on this axis (no taper on low side).
- border_high
Per-axis: True if the tile is the last on this axis (no taper on high side).
- overlap_low
Per-axis: actual overlap in voxels with the preceding tile on low side. Zero for the first tile on each axis.
- overlap_high
Per-axis: actual overlap in voxels with the following tile on high side. Zero for the last tile on each axis.
- 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 - overlapper axis. Edge tiles are clamped to the volume boundary and may be smaller thantile_size. Withfold_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 totile_size + overlap - 1voxels on the folded axis — a folded tile is the one case where a tile is BIGGER thantile_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 theoverlapparameter at volume edges) to ensure correct windowing.- Parameters:
volume_shape (
tupleofint) – Shape of the full volume, e.g.(500, 2048, 2048).tile_size (
intorsequenceofint) – Tile size per axis. Scalar is broadcast to all axes.overlap (
intorsequenceofint) – Overlap width per axis. Scalar is broadcast to all axes. Must satisfy0 <= overlap <= tile_size // 2on each axis. Overlaps larger than half the tile size cause triple tile overlap, which breaks the Hann partition-of-unity guarantee.fold_slivers (
bool, defaultTrue) – 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 Nfit --tiling uniformpaths, a hand-runfit --tile k/M, andbatch-fit— so pairing this function withfit_tile()by hand reproduces exactly the grid those commands fit and merge. PassFalseonly to rebuild the historical unfolded grid of a store written before #2838 (a legacybatch-fitmanifest 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:
- 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 originolands ato * f.voxel_sizewithoutput_space="real": the fit emits physical coordinates, so a tile originolands ato * voxel_size.
Both at once (a downscaled parallel fit with a
voxel_sizefrom--config) giveso * 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 (
sequenceofint, optional) – Per-axis--downscalefactors the grid was decimated by, orNonewhen the grid is at full resolution. A scalar is broadcast.voxel_size (
floatorsequenceoffloat, optional) – Physical voxel spacing the fit was given. A scalar is broadcast;Nonemeans unit spacing.output_space (
str, default"real") – The fit’s output space,"real"or"voxel". Thevoxel_sizeterm 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()’sscale, orNonewhen every factor is 1 (the two frames already agree).- Return type:
- Raises:
ValueError – On an
output_spaceoutside("real", "voxel"), or avoxel_sizethat is neither a scalar nor a length-ndimsequence. Both are refused rather than absorbed: an unrecognisedoutput_spacewould silently DROP thevoxel_sizeterm (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_treeform.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 at2 * 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 (
sequenceofTileSpec) – A full grid as returned bycompute_tile_specs().scale (
sequenceoffloat, 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:0would collapse the planes onto the origin and a negative factor would mirror the ordering the tree encodes. Useresolve_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 itsfit --tile i/Mworkers agree on the tile count M — while each worker rescales its own splats back to full resolution before writing.voxel_sizewithoutput_space="real". The specs are voxel coordinates, but a fit asked for real-space output emits centers in physical units (seefit_tile(), which offsets a tile byorigin * voxel_size).
Without a factor here, every plane of the resulting
kind=partitionwould 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 conventionrescale_centers()(and theorigin * voxel_sizeoffset) applies to the centers.
- Returns:
The serialized tree, or
Nonewhenspecsis empty or the grid subdivides an axis beyond the third. Those axes are stacked time/channel barriers and are never displayed.- Return type:
- Raises:
ValueError – If
scaleis 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
GSplatDataof isotropic Gaussians.Each point
ibecomes a Gaussian with centrepositions[i], isotropic covariancesigma_i^2 Iwheresigma_i = 2 * radii[i] * radius_scale / truncation_radius, and peak amplitudeopacity / (uRIF * sigma_i)(see the module docstring for the calibration). The result is a flat (single substitutive level, single additive sub-LOD)GSplatDataready to feed toluxar.gsplats.lod.substitutive.make_substitutive_lod().- Parameters:
positions (
array,shape (N,d)) – Point centres (any spatial dimensionalityd).radii (
array (N,)orfloat) – Per-point world radius (the 1% iso-contour radius), beforeradius_scale.colors (
array (N,3),uniform RGB(A), orNone) – 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.colorsis(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.Noneleaves colours unset.opacity (
float) – Node opacity baked into the lifted amplitude (peak match).radius_scale (
float) – Mirrors the shaderradiusScaledtype normalisation (e.g. 1/255 for uint8 radii). Default 1.0.truncation_radius (
float) – Gaussian truncationTin sigmas. Defaults toLIFT_TRUNCATION_RADIUS(3.0) — NOT the codebase-wideDEFAULT_TRUNCATION_RADIUS; see that constant for why._uniform_colors (
boolorNone) – PRIVATE.None(the default) means “nobody has classifiedcolorsyet” 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. Onlylift_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 thatcolorsis 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 == Nvalid splats (zero-radius points, e.g. from nD slicing, are dropped).- Return type:
- 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
GSplatDataof isotropic bead Gaussians.Each segment is sampled into a string of overlapping isotropic “bead” Gaussians spaced
bead_spacing_factor * σ_perpalong it (σ_perp = 2 w / Tper 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-bakedcolorsso non-linear colormaps get correct mid-segment colours.Parameters otherwise mirror
lift_points_to_gsplats()plusline_type/indices(how vertices form edges) andbead_spacing_factor—colorsincluded, 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 atTsigmas has its visible edge atT * sigma. The point’s screen radius isR * pointSizeFactor / (2 z)and the gsplat’s isT * f * sigma_world / zwithpointSizeFactor / f = 4(2 resY/tanvsresY/(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_scaleis the point’s world radius andTis the gsplat truncation radius (default 3.0). AtT = 3the gsplat Gaussian matches the point super-Gaussian profile to 0.45% relative L2 (the kernels coincide exactly atT* = sqrt(2 ln 100) ≈ 3.035).Brightness match. A single isotropic gsplat’s peak screen intensity is
a * sigma_world * uRayIntegralFactor(the ray-integral boostvAmplitude2D = a * sigmaRay * uRIFwithsigmaRay = sigma_worldfor an isotropic covariance, anduInvOneMinusC * (1 - uShiftC) = 1at the centre). A point’s peak alpha isopacity. 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:
objectConfiguration for Gaussian splat fitting.
Contains all parameters and settings needed for the fitting process.
- V: np.ndarray
- dynamic_config: DynamicOpsConfig
- device: torch.device
- 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:
objectRaw parameters threaded through the internal fitting pipeline.
fit_gaussian_splatsremains the explicit public API. This bundle removes the duplicate parameter signatures fromGaussianSplatFitter.fitandprepare_fit_configwhile preserving their existing values until the validation boundary normalizes them intoFitConfig.- V: np.ndarray
- __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:
objectData that has been preprocessed and is ready for optimization.
Contains normalized data, seed centers, and preprocessing metadata.
- V_tensor: torch.Tensor
- __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:
objectResults from the optimization process.
Contains final parameters, optimization statistics, and metadata.
- centers: torch.Tensor
- Ls: torch.Tensor
- amps: torch.Tensor
- __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:
- 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:
- luxar.gsplats.fitting.initialize_optimization(config: FitConfig, preprocessed_data: PreprocessedData) ModelComponents[source]
Initialize model, optimizer, and scheduler.
- Parameters:
config (
FitConfig) – Configuration for the fitting processpreprocessed_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 parameterspreprocessed_data (
PreprocessedData) – Preprocessed data containing target tensor and computed L1 valuesmodel (
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 schedulerloss_fn (
Callable) – Loss function that takes prediction and returns lossconfig (
FitConfig) – Configuration for optimizationpreprocessed_data (
PreprocessedData) – Preprocessed data including target tensor
- Returns:
Results from optimization including best state and statistics
- Return type:
- 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 loopconfig (
FitConfig) – Configuration used for fittingpreprocessed_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/volumeresult (
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.
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_splatstakes these as FLAT keyword arguments — there is nooptim=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_splatsdefaults 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_pointenforces that for every field of all three configs.- n_iters: int = 1000
- lr: float = 0.01
- scheduler_type: str = 'plateau'
- patience: int = 15
- lr_reduction_factor: float = 0.9
- sort_splats_enabled: bool = True
- sort_splats_interval: int = 1000
- 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"orloss_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'
- 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))
- truncate: float = 2.75
- output_space: str = 'real'
- 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_splatsremains the explicit public API. This bundle removes the duplicate parameter signatures fromGaussianSplatFitter.fitandprepare_fit_configwhile preserving their existing values until the validation boundary normalizes them intoFitConfig.- V: np.ndarray
- norm_percentile: float = 0.0
- n_iters: int = 1000
- lr: float = 0.01
- loss_type: str = 'l1'
- truncate: float = 2.75
- seed_method: str = 'auto'
- verbose: bool = True
- napari_movie: bool = False
- movie_every: int = 1
- scheduler_type: str = 'plateau'
- patience: int = 15
- lr_reduction_factor: float = 0.9
- dynamic_ops_verbose: bool = False
- clip_to_bounds: bool = False
- 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
- 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
- norm_percentile: float
- truncate: float
- n_iters: int
- lr: float
- loss_type: str
- scheduler_type: str
- patience: int
- lr_reduction_factor: float
- enable_dynamic_ops: bool
- dynamic_config: DynamicOpsConfig
- dynamic_ops_verbose: bool
- napari_movie: bool
- movie_every: int
- device: torch.device
- verbose: bool
- 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_amps_background_relative: bool = False
- init_L: np.ndarray | None = None
- init_amps: np.ndarray | None = None
- clip_to_bounds: bool = False
- voxel_size: np.ndarray | None = None
- output_space: str = 'real'
- sort_splats_enabled: bool = True
- sort_splats_interval: int = 1000
- iter_callback: IterCallback | None = None
- iter_callback_every: int = 25
- 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
- __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
- start_time: float
- end_time: float
- __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
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
FitConfigfromgsplats.fitting.config, so the config module could not name its own default from here without a circular import — which is howConstraintConfigcame to default toNonewhile 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 anyamp_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.ndarrayorzarr.Array) – Full volume (may be a lazy zarr array; only a bounded sample is read, via the same budget and block layout asresolve_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, defaultFalse) – 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.shapeand 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
floorspec 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.ndarrayorzarr.Array) – Full volume (may be a lazy zarr array; only a bounded sample is read).floor (
str,float, orNone) – Floor spec (see_resolve_floor()). A numeric spec (float or numeric string) short-circuits and is echoed back without touching the volume — unlessguard_numericis set;"none"/None/0returnNone.guard_numeric (
bool, defaultFalse) – Also apply the “floor >= max would erase all signal” guard to a numeric spec (one bounded sample read). PassTruewhere a USER-supplied spec is first turned into a level; leaveFalsefor 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.NoneusesFLOOR_SAMPLE_BUDGET_VOXELS.verbose (
bool, defaultFalse) – 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:
Notes
Memory bound: at most
FLOOR_SAMPLE_BUDGET_VOXELSvoxels 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.shapeand 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
-2is shifted up byV - (-2)— exactly what the non-tiled path’simage_min = max(resolved_floor, image_min)does whenresolved_flooris negative.The “floor >= max would erase all signal” guard is applied against the sampled max: such a level is refused with an
aprintwarning andNoneis returned. For numeric specs the guard runs only withguard_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 nonedoes 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 keepsresolve_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 apNNspec. See the Notes for the two regimes and the measurements behind them.- Parameters:
volume (
np.ndarrayorzarr.Array) – Full volume (may be lazy; only bounded samples are read).floor (
str,float, orNone) – Floor spec, exactly asresolve_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 toresolve_volume_floor()verbatim.denoise_params (
dict, optional) – The remainingdenoise_volume_arraykeyword arguments (patch_size,search_distance,backend,device,use_2d,norm_range), passed verbatim so the probe is smoothed exactly as the tiles are.Nonedelegates likedenoise_h=None.guard_numeric (
bool, defaultFalse) – Forwarded toresolve_volume_floor()(see there).sample_budget (
int, optional) – Override the bounded raw floor-sample voxel budget.NoneusesFLOOR_SAMPLE_BUDGET_VOXELS.verbose (
bool, defaultFalse) – Print the raw level, the correction and the final level. Forwarded toresolve_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:
Notes
Two regimes, one measured rule. The correction is applied where it is demonstrably right, and not applied where it is not:
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.Above the budget the probe is a handful of cubic centre crops, and whether its shift transfers depends on the ESTIMATOR. A
pNNpercentile shift does; theautohistogram-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 nonecomputes,_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_VOXELSvoxels per CALL, and none at all forautoabove the budget (regime 2 is decided fromvolume.shape, before anything is read). That is once per resolution, not once per tile — but every worker resolves its own level, so a-j Nrun or anM-way--tile k/Mfleet 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.shapeand the budget, so independent workers (--tile k/M,-j N) that share a volume,hand 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 resolvedautolevel 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-backendfor 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_Lis 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 processpreprocessed_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)
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).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 parameterspreprocessed_data (
PreprocessedData) – Preprocessed data containing target tensor and computed L1 valuesmodel (
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)
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.
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 schedulerloss_fn (
Callable) – Loss function that takes prediction and returns lossconfig (
FitConfig) – Configuration for optimizationpreprocessed_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.passesare 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_flooris that up-front level (Nonewhen 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_rangedescribes 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 whereimage_minIS the applied level.flooris 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 sameimage_min,image_maxandintensity_rangeas 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 loopconfig (
FitConfig) – Configuration used for fittingpreprocessed_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
- __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)
- 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:
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:
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:
ModulePyTorch 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:
Center position: Constrained to image domain via sigmoid parameterization
Covariance matrix: Represented via Cholesky decomposition L where Σ = L @ L^T
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, defaultDEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations for computational efficiency.device (
strortorch.device, optional) – PyTorch device for computations. Explicit values override auto-detection.use_cuda (
bool, defaultTrue) – Allow CUDA during auto-detection whendeviceis not provided.use_metal (
bool, defaultTrue) – Allow MPS/Metal during auto-detection whendeviceis 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]
- 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.Parameterattributes invalidates any optimizer state (Adam moments, momentum buffers, etc.) registered against the previous parameter tensors. Callers that intend to keep training after areplace_withmust rebuild the optimizer —initialize_optimizationis the canonical entry point.The current best-state restore path (
optimization.py::_restore_best_state) is safe because it runs purely undertorch.no_grad()and does NOT calloptimizer.step()afterwards: it only re-evaluates the loss so the reported metrics match the restored parameters.
- 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, defaultDEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations.intensity_floor (
floatorNone, default1e-5) – Minimum intensity threshold for amplitude-aware culling. PassNone(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, defaultDEFAULT_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, defaultDEFAULT_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, default1.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, default1.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:
ModulePyTorch 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:
Center position: Constrained to image domain via sigmoid parameterization
Covariance matrix: Represented via Cholesky decomposition L where Σ = L @ L^T
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, defaultDEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations for computational efficiency.device (
strortorch.device, optional) – PyTorch device for computations. Explicit values override auto-detection.use_cuda (
bool, defaultTrue) – Allow CUDA during auto-detection whendeviceis not provided.use_metal (
bool, defaultTrue) – Allow MPS/Metal during auto-detection whendeviceis 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.Parameterattributes invalidates any optimizer state (Adam moments, momentum buffers, etc.) registered against the previous parameter tensors. Callers that intend to keep training after areplace_withmust rebuild the optimizer —initialize_optimizationis the canonical entry point.The current best-state restore path (
optimization.py::_restore_best_state) is safe because it runs purely undertorch.no_grad()and does NOT calloptimizer.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.
- 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 decomposescales (
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, default500) – Number of optimization iterationslr (
float, default0.01) – Learning rate for Adam optimizerenergy_weight (
float, default0.01) – Weight for hierarchical energy penalty (higher = more energy to coarse)alpha (
float, default1.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], default10.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, defaultFalse) – Enable recording of optimization progress for napari movie visualizationmovie_every (
int, default1) – Record a movie frame every N iterations (only if napari_movie=True)movie_max_frames (
Optional[int], defaultNone) – 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.
- 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:
ModulePyTorch 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 scaleupsampled_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 modeltarget (
torch.Tensor) – Target image to reconstructenergy_weight (
float, default0.001) – Weight for energy penalty (higher pushes more energy to coarse scales)alpha (
float, default1.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], default10.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 numbersshape (
tuple) – Shape of the original datainterpolation (
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 dimensionalitytarget_shape (
Tuple[int,]) – Target shape to upsample tointerpolation (
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
GSplatNodeand hands it towrite_gsplats_tree(). Colors are written via the shared COLOR helper, which auto-detects SDR vs HDR (values > 1) — there is no explicitcolor_modeknob; 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 akind=lodgroup whose children are all leaves (the substitutive × additive matrix). A genuinely nested tree (akind=partitionroot, or a lod group with non-leaf children) has no flatGSplatDataequivalent and raisesValueError; useload_default_gsplatsto materialize the default-rendered selection, or consume the node tree directly withload_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 toNone. 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:
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:
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:
Parameter dilution (params_current / params_2d): More parameters need updates
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, defaultTrue) – Whether to allow uniform Cholesky factors (shape (k,)) for all splats.
- Returns:
- 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:
- 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 (
listofint,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 (
dictof{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()withinrange(tril_size(d)), preserving the row-major lower-triangular order. Empty ford == 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 whend == 1).
See also
merge_trilinverse 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 bysplit_tril()).offdiag (
np.ndarray,shape (...,d*(d-1)//2)) – Off-diagonal elements (as returned bysplit_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:
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:
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:
Parameter dilution (params_current / params_2d): More parameters need updates
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()withinrange(tril_size(d)), preserving the row-major lower-triangular order. Empty ford == 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 whend == 1).
See also
merge_trilinverse 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 bysplit_tril()).offdiag (
np.ndarray,shape (...,d*(d-1)//2)) – Off-diagonal elements (as returned bysplit_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, orNonewhen 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 viamerge_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 > 1its absence means a corrupt or partially-written store and raisesValueErrorrather than silently dropping every splat’s off-diagonal covariance. ReturnsNonewhen 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, orNoneif absent.- Returns:
Packed lower-triangular Cholesky factors, or
Noneif no Cholesky array exists in the store.- Return type:
np.ndarrayorNone
- 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, defaultTrue) – Whether to allow uniform Cholesky factors (shape (k,)) for all splats.
- Returns:
- 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:
- 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 (
listofint,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 (
dictof{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:
seed_from_decomposition: Scale-hierarchical detection via image decomposition. Each seed’s sigma equals the decomposition scale factor. Best for blob-like features.
seed_from_grid: Uniform grid seeding for spatial coverage. Isotropic Gaussians with user-defined or auto-computed sigma.
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, default1) – Number of finest scales to ignore for peak detection. Setting k=1 ignores the full-resolution scale to suppress noise.peaks_per_scale (
intorNone, optional) – Maximum number of peaks to extract per scale. If None, extract all.min_distance (
float, default2.0) – Minimum Euclidean distance between seeds (in voxels).threshold_rel (
float, default0.1) – Relative threshold for peak detection (0.0 to 1.0).decompose_kwargs (
dictorNone, optional) – Additional keyword arguments passed to decompose_image().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 (
intorNone, optional) – Target number of seeds. If None, auto-estimates based on image size.min_distance (
float, default2.0) – Minimum distance between seeds in voxels.edge_threshold_rel (
float, default0.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 (
floatorSequence[float]orNone, 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, default0.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 (
floatorNone, optional) – Gaussian sigma (standard deviation) for all seeds. If None, defaults tomean(spacing) / 2(ensures ~60% overlap at midpoints between adjacent grid points).exclude_below (
floatorNone, optional) – Absolute intensity threshold. Grid points where V < threshold are excluded. Mutually exclusive with exclude_below_percentile.exclude_below_percentile (
floatorNone, 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_seedslocations 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_sigmais 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:
Divide volume into non-overlapping tiles of size tile_size^d
For each tile:
Compute local histogram (nbins bins)
Apply contrast limiting (clip histogram peaks)
Compute CDF mapping (local histogram equalization)
Transform tile intensities
Result: Volume with locally-equalized contrast
- Parameters:
V (
torch.Tensor) – Input tensor of any dimensionality (1D, 2D, 3D, nD)tile_size (
int, default16) – 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 diameterclip_limit (
float, default2.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, default256) – 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 (
targetprovided): Uses the original volume that was fitted. Computes the residual
R = target - V_predand 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. Aredundancy_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,)
- 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 theredundancy_threshold.- Type:
- phase1_candidates
Number of individually safe candidates before the joint compounding check.
- Type:
- max_joint_error
The max
|R_joint|(error-budget) or max fractional contribution (redundancy) after removing the final set of culled splats.- Type:
- 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()withfractional=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 (
targetis not None) Computes the residual
R = target - V_predand establishes an error budgettau = percentile(|R|, error_percentile) * error_tolerance. A splat is safe to remove when the worst-case error increasemax_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 (
targetis None) Works from the splats alone — no target volume needed. Renders the full reconstruction
V_pred = sum g_iand 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 belowredundancy_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.TensororNone) – Target volume to compare against. If provided, error-budget mode is used. IfNone, 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:
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, ... )
- Error-budget mode (
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 astarget.max() - target.min().
- Returns:
Mean SSIM in [−1, 1] (typically [0, 1] for non-negative data).
- Return type:
- 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).
- 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.filtersbecause scikit-image lives in thedemosextra. 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>thatcompute_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_rangeis taken from the whole target, matchingluxar.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 totarget.max() - target.min()over the whole volume.threshold (
float, optional) – Foreground istarget > threshold. Defaults to Otsu.
- Returns:
fractionis 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_dbisnanwhen 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:
- 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 – seecompute_foreground_psnr().- Return type:
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):
Mask 5% of voxels with a deterministic Bernoulli draw (seed=42).
Replace masked voxels with the median of the unmasked voxels in their 3^D donut neighbourhood (centre excluded) — Noise2Self self-supervision.
Fit a Gaussian-splat model on the donut-filled volume at each
Kin a sweep; the optimiser never sees the original noisy values at masked positions.Evaluate held-out PSNR against the original (pre-fill) values at the masked positions.
The
Kthat maximises held-out PSNR is the principled splat budget — capacity beyondK*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:
objectOutput of
calibrate(). Serialisable to JSON.- full_psnr_db: List[float]
PSNR over the whole volume against the original — for cross-run comparison.
- held_out_peak: HeldOutPeak
The recommended K* and curve type.
- noise_floor: NoiseFloor
Ensemble noise-floor estimate for the input volume.
- 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.
- __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.
- predict_zero_baseline_mse: float = nan
MSE of the trivial all-zeros reconstruction at masked voxels.
- 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).
- exponent_fit: Dict[str, Any] | None = None
Multi-scale
ExponentFit(as dict) whencal --fit-exponentran; itsalphais also written intosplat_density.saturation_exponent.
- classmethod from_json(path: Path) CalibrationResult[source]
Load from JSON.
nullfloats becomenan.
- 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:
objectMulti-scale fit of the saturation exponent
alphainK ~ 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 regressinglog K*onlog n_features. The slope isalpha; the per-scale(n_features, k_star)points and the fitr_squaredare kept for reporting / provenance.- __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”.
- class luxar.gsplats.calibration.FloorEstimate(level: float, strategy: str)[source]
Bases:
objectResolved floor level and the estimator branch that produced it.
- 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:
objectDetected K* and qualitative shape of the held-out PSNR curve.
- 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. Forsignal_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_starand of the regime label: the interior argmax for a clear peak, otherwise the smallest K withinknee_margin_dbof the maximum. Forpeakandplateauthis equalsk_star; forsignal_limitedit is the (earlier) knee whilek_starremains the last/max K used for the splat budget. Defaults to 0; callers that predate this field should fall back tok_star(from_jsondoes this).
- 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
peakcurve).
- 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_dbof 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_staris then the last K (budget anchor);k_kneemay still be an earlier K when one is already withinknee_margin_dbof the maximum, and equals the last K only when no earlier K is that close.- Type:
True when the curve is signal-limited
- __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:
objectEnsemble noise-floor estimate for a [0, 1]-normalised volume.
All
sigma_*fields are noise standard deviations in the volume’s intensity units.psnr_max_dbis the corresponding PSNR ceiling assuming adata_range = 1.0.
- class luxar.gsplats.calibration.RDModel(floor: float, a: float, beta: float, rmse: float, n_points: int, converged_fraction: float)[source]
Bases:
objectParametric 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 originalsignal_limitedfalse alarm without an expensive dense high-K sweep.
- class luxar.gsplats.calibration.RegionSelection(origin: List[int], size: List[int], strategy: str, n_features: int, score: float)[source]
Bases:
objectProvenance of an auto-selected calibration sub-region.
- 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:
objectTransferable 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 viapredict_k()without re-calibrating — the cal→planner interface. Assumes tiles of roughly the reference scale (“calibrate at the scale you fit at”).- 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).
- 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
explicitis provided it takes precedence; otherwisen_pointsvalues are placed betweenk_minandk_maxaccording toprogression:"exp"— log-spaced (geometric). Default. Matches the manuscript’s{1K, 2K, ..., 512K}atn_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 whenpower > 1.
Duplicates from rounding are removed but the sequence is kept monotonic. Endpoints are guaranteed to be exactly
k_minandk_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_gridon volumeV.Pipeline (one execution per call):
Generate a deterministic CV mask, donut-fill
Vto makeV_filled.Fit a Gaussian-splat model with
fit_gaussian_splatsat eachKink_grid, usingV_filledas the target.fit_kwargsare forwarded verbatim exceptseeds(overridden per-K).Render each fit back to volume; compute held-out / train / full PSNR (and full SSIM) against the original
V.Estimate the noise floor on
V.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 (
sequenceofint) – Splat counts to evaluate.fit_kwargs (
dict, optional) – Forwarded tofit_gaussian_splats(preset / config / device / cull_retention / verbose / …). Theseedskey 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.zarroutputs. IfNone, 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:
- 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
alphainK ~ features^alphaby calibrating at several scales.For each edge length in
scalesa 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 samek_star_metricthe 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 byfit_saturation_exponent().Returns the
ExponentFit, orNonewhen fewer than two scales yield distinct feature counts (e.g. every scale collapsed to the whole volume). Runtime is roughlylen(scales)× a singlecalibrate()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);intensityis 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 chooseedges.threshold_abs (
float, optional) – Absolute detection level (the valuefeature_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/edgesthreshold the blurred / gradient field at it;intensitycountsV > thr(strict>, matchingforeground_mask_otsu()).**kwargs – Forwarded to the underlying estimator (e.g.
radius,threshold_relforpeaks).
- Returns:
A non-negative feature count.
- Return type:
- 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:
- Returns:
Trueat held-out positions,Falseelsewhere.- Return type:
np.ndarrayofbool,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)^Dcube around each masked voxel with the centre excluded — 26 neighbours in 3D whenr=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 toVandmaskalike, so a reflected masked voxel stays excluded). If every donor atradiusis 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
Vandmaskfor the current radius.- Parameters:
- Returns:
Copy of
Vwith masked voxels replaced by donut medians of their unmasked neighbours. Unmasked voxels are unchanged.- Return type:
np.ndarray,same shapeanddtype as ``V``- Raises:
ValueError – If the shapes differ,
radiusis 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 pedestalmode ≈ 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 tomode.
Notes
Exact-zero voxels (masked / out-of-FOV padding) are excluded so padding does not dominate the histogram. This function materializes
V; useluxar.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_nucleiin the manuscript).The PSNR ceiling assumes
data_range = 1.0(the [0, 1] normalisation enforced byfit_gaussian_splats). Whensigma_hatis 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_contentcounts on the identical scale (the detectors threshold relative to a blurred / gradient / Otsu level, NOT the raw max, so a naive0.1*maxdrifts — 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):Peak: the argmax is strictly interior AND both
mean(pre-argmax)andmean(post-argmax)are at least 0.1 dB below the peak. Return the argmax.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.
Plateau: otherwise. Return the smallest K within 0.3 dB of the maximum — the onset of diminishing returns.
The returned
HeldOutPeakreports bothk_star(the budget anchor above — max K forsignal_limited) andk_knee(the point of diminishing returns: the argmax for a clear peak, else the knee), plus supporting per-regime metadata.k_kneeequalsk_starforpeakandplateauand is the earlier knee forsignal_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).Noneif <3 finite points or the fit fails.error_valuesshould be held-out MSE.
- luxar.gsplats.calibration.fit_saturation_exponent(points: Sequence[Tuple[int, float, float]]) ExponentFit | None[source]
Least-squares fit of
alphainK ~ features^alpha(log-log regression).pointsis 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. ReturnsNonewhen fewer than two distinct feature counts remain (no spread to fit a slope).r_squaredis 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 structuralR²==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.ndarrayofbool) – Held-out mask. Must broadcast to the volume shape.data_range (
float, optional) – Dynamic range for PSNR. IfNone, usesV_original.max() - V_original.min()over the whole volume.
- Returns:
PSNR in dB.
+infwhen MSE is zero,nanwhen mask is empty.- Return type:
- 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, sofg_bg_ratio=1gives the two strata exactly equal total weight. Returnsnanwhen 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. Returnsnanwhen 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_sizewindows, 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 thanregion_sizeon 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 atK_min/K*/K_maxplus 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.zarrpaths in the same order asresult.k_values_requested. When provided, the third page of the PDF includes reconstruction slice montages at K_min, K*, K_max. WhenNone, 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-shapedGSplatDatawhere the selected substitutive level’sadditive_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 inluxar.gsplats.lod.substitutive. Entry point:make_substitutive_lod()returns a matrix-shapedGSplatDatawithn_substitutive = levels + 1and 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:
objectParameters 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 callingbuild_recipe(); passingmax_elements=Nonefalls back toDEFAULT_MAX_ELEMENTSso the builders are usable standalone too.- additive_method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto'
- __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
recipefromdata.Returns a
GSplatDatafor the matrix recipes (flat/stream/levels) and aGSplatNodefor the composed recipes (tiles/overview/adaptive) — seeRecipeResult.
- 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 ofauto,greedy,self_energy,mass,amplitude,spectral,random,radial.auto(the default) resolves togreedyat small N andself_energyabove_AUTO_ADDITIVE_MAX_N— seeresolve_additive_method(). See module docstring for details.truncation_sigmas (
float, optional) – Mahalanobis cutoff used for sparse-Gram pruning. Defaults to the dataset’s owntruncation_radius— the support the splats were fitted at and are rendered at. Only relevant forgreedyandspectral.max_n_dense (
int) – Forgreedy, 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 formethod='random'.reveal_center (
sequenceoffloat, optional) – Centre of the shells formethod='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 (
sequenceofint, 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 (
sequenceofint, optional) – RAW, pre-dim_ordercentre columns whose distinct combinations the viewer SLICES (a hidden time / channel axis) — NOT the scene’s post-dim_orderdimension positions, which are a different frame of reference. When given, the ordering chosen bymethodis re-emitted round-robin across those slices byinterleave_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 standaloneGSplatDatahas no display information to derive one from.
- Returns:
order[k]is the original index of the splat at rank $k$.- Return type:
np.ndarrayofshape (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
targetsplats 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 (floatin(0, 1]). Seeresolve_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 tocompute_additive_order()(auto/self_energy/mass/greedy/radial/ …).device – Device for the clustering pass (merge only).
seed – Seed for the
randomordering (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
mergestamps the RESOLVED set on the result (the writer turns it into the chunk-ordering barrier); aprefixignores the argument (aUserWarning, so the notice survivesverbose=False) and keeps the input’s stamp, having coarsened nothing. Under theluxarCLI that warning renders as an arbol line like any other output. The request is range-validated for BOTH families, before the family is chosen — undermethod="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
GSplatDatawith<= targetsplats, and close to it. Returns the input unchanged whentargetresolves to the full count.mergecan 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_dimsindex outside[0, data.ndim).
- luxar.gsplats.lod.interleave_order_across_slices(data: GSplatData, order: ndarray, slice_dims: Sequence[int]) ndarray[source]
Re-emit
orderround-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 inscripts/check_demo_ladders.py(#2485).Grouping the elements of
orderby their distinct combination of theslice_dimscentre 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 shapecheck_demo_ladders.py’s absolute first-paint arm asks for. Ties inside a pass are broken by position inorder, 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
orderthe coordinates left with NOTHING are the faintest ones — measured on 500 slices of 40 splats withbreakpoints=[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=3cleared the floor 5 times in 8 (p05 243-277 against a floor of 250) andn_lods=4never 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_dimsindexes RAW, PRE-dim_ordercentre columns, and that is the likeliest way to get this wrong. The ladder is built incore/group/gsplats_pipeline/from_data.pyABOVE theapply_dim_order_*pass thatlod_dispatchruns, so these are the columns of the array the caller handed in — NOT the scene’s post-dim_orderdimension positions. They coincide for the NEXRAD supercell only because itsdim_orderleaves time last in both frames. An author who reads off the scene’sDimensionslist 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_ordermixup 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
lexsortreproducesorder: 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: alod_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, andresolve_additive_axis_gsplats()applies oneslice_dimsto every substitutive level, giving a slice-even finest level and silently uneven coarse ones. The defaultAutocoarsening (hidden axis as a hard barrier) is safe.- Parameters:
data (
GSplatData) – The datasetorderindexes; only itscentersare read.order (
np.ndarray) – A length-N integer permutation, as returned bycompute_additive_order(). Validated for shape, dtype and range; duplicate entries are the caller’s responsibility (see_validate_interleave_order()).slice_dims (
sequenceofint) – RAW, pre-dim_ordercentre 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.ndarrayofshape (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
GSplatDatawithn_lods(or as resolved bybreakpoints)AdditiveSubLODlevels 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: thesubstitutive_levelargument (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 whenbreakpoints='equal-count'. Ignored whenbreakpointsis a list or'stream:<c>'(those determine the level count themselves).method (
str) – Ordering method (seecompute_additive_order()).breakpoints (:py:class:
``’equal-count’:py:class:``, :py:class:``’stream:<c>’:py:class:``, orlistofint / float) –'equal-count':n_lodslevels of (nearly-)equal size.'stream:<c>': geometric streaming ladder — cumulative cuts[c, 2c, 4c, …, N]sized so the first chunk iscsplats (bandwidth-derived viastreaming_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 atDEFAULT_STREAM_MAX_LEVELSlevels.'equi-energy:<n>':nrungs 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 aboveDEFAULT_MAX_ADDITIVE_COMMITsplit into capped steps (luxar.utils.lod_breakpoints.equi_energy_cuts()). Pair with a contribution-firstmethod(self_energy, the large-N default) — underrandomthe 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 owntruncation_radius.max_n_dense (
int) – Threshold below whichgreedyuses a dense Gram + scan-greedy.seed (
int, optional) – Random seed formethod='random'.substitutive_level (
int, optional) – Index of the substitutive level to build the ladder for. Defaults todata.default_substitutive.reveal_center (
sequenceoffloat, 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 (
sequenceofint, 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 (
sequenceofint, optional) – RAW, pre-dim_ordercentre columns the viewer SLICES (a hidden time / channel axis) — NOT the scene’s post-dim_orderdimension positions. When given, the ordering is re-emitted round-robin across those slices (seeinterleave_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 everymethod, and theenergy_fraction_cumstamps 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’s1/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 akind=lodgroup:applyLodFadehas a second caller in the viewer’sscene/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-fractionbreakpointsresolve 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
GSplatDatawith the samen_substitutiveasdata; 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 callsmake_additive_lod()on each substitutive level (inner axis). The result is a single matrix-shapedGSplatDatawithn_substitutive = levels + 1andM_i = n_additive_lods(or as resolved bybreakpoints) 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_inflationis 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 sourcevolume— seemake_substitutive_lod()).lloyd_iterations – Substitutive axis algorithm parameters (
coverage_inflationis 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 sourcevolume— seemake_substitutive_lod()).candidate_bins_k – Substitutive axis algorithm parameters (
coverage_inflationis 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 sourcevolume— seemake_substitutive_lod()).color_weight – Substitutive axis algorithm parameters (
coverage_inflationis 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 sourcevolume— seemake_substitutive_lod()).coverage_inflation – Substitutive axis algorithm parameters (
coverage_inflationis 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 sourcevolume— seemake_substitutive_lod()).refine – Substitutive axis algorithm parameters (
coverage_inflationis 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 sourcevolume— seemake_substitutive_lod()).refine_iters – Substitutive axis algorithm parameters (
coverage_inflationis 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 sourcevolume— seemake_substitutive_lod()).volume – Substitutive axis algorithm parameters (
coverage_inflationis 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 sourcevolume— seemake_substitutive_lod()).volume_axes – Substitutive axis algorithm parameters (
coverage_inflationis 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 sourcevolume— seemake_substitutive_lod()).device – Substitutive axis algorithm parameters (
coverage_inflationis 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 sourcevolume— seemake_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 owntruncation_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 owntruncation_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(); seeluxar.gsplats.lod.quality).quality_max_pair_splats – Opt-in measured Q·e quality stamps per substitutive level (passed to
make_substitutive_lod(); seeluxar.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
ℓhasceil(N / K^ℓ)splats.levels – Number of coarser levels to produce. The returned object has
levels + 1substitutive levels (the original at index 0).method – Partition algorithm, or
"auto"(default)."auto"resolves per level:"greedy"when the level’s input has<= 5000splats (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 anexp(-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: roughly0.1to1for greedy and1to10for Lloyd spans a soft-to-strong hue preference. Becausemethod="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.0disables (historical pure-moment-matching behaviour). Trade-off: coarse levels look slightly smoother; each splat’s integral (X-ray projection) is preserved exactly. Withrefine="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;Falserestores 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 globalconserve_massrescale 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 sourcevolumeitself (a fullfit_gaussian_splats()pass seeded by the merge) — the highest-fidelity option (+5–12 dB over the merge on real microscopy, seevolume_refit); requiresvolume. With barrier dims (coarsen_dimsset) each barrier group is re-fitted against its OWN slice of the volume, in the coarsened dims only — seevolume_regionsfor 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 forl2(L2RefineConfig), 300 forvolume(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 thevolumeaxis holding center dimi.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 toGSplatDatahas 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 atorch.device.seed – Seeds the L2-refine minibatch pair sampler when
refine="l2"(a localtorch.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
coloringaxis, time, or channel).label_idsare also exact barriers whenever present.None(default) coarsens over all center dims (the historical behavior). Passing all dims is equivalent toNone. 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 stampquality+reference_energyinto every level’s stats — the Q of the viewer’s committed qualityQ·e(k).reference_energyis 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 + 1and a single additive sub-LOD per substitutive level (the finest atsubstitutive_levels[0]).- Return type:
GSplatData- Raises:
ValueError – If
compression_factor < 2,levels < 1, ormethodis 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
methodfornsplats, handling the"auto"sentinel.auto→greedywhenn <= _AUTO_ADDITIVE_MAX_N(high quality and affordable at small N), elseself_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 owntruncation_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 canonicalDEFAULT_TRUNCATION_RADIUS(2.75) was pruned at a support it never had.getattrwith that same constant as fallback mirrors the defensive read inGSplatData.principal_radii(gsplats/_data/metrics.py): it covers the (structural) case of a data-like object that exposes no radius at all, and — sincegetattrswallows anyAttributeError, 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_breakpointsso 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.
- 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 ofnsplats.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 belownand lets_resolve_breakpointsappend the finaln; 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 FULLn.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:1000000on a 50 k dataset) and must abort loudly, exactly like a direct whole-datasetmake_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
orderround-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 inscripts/check_demo_ladders.py(#2485).Grouping the elements of
orderby their distinct combination of theslice_dimscentre 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 shapecheck_demo_ladders.py’s absolute first-paint arm asks for. Ties inside a pass are broken by position inorder, 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
orderthe coordinates left with NOTHING are the faintest ones — measured on 500 slices of 40 splats withbreakpoints=[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=3cleared the floor 5 times in 8 (p05 243-277 against a floor of 250) andn_lods=4never 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_dimsindexes RAW, PRE-dim_ordercentre columns, and that is the likeliest way to get this wrong. The ladder is built incore/group/gsplats_pipeline/from_data.pyABOVE theapply_dim_order_*pass thatlod_dispatchruns, so these are the columns of the array the caller handed in — NOT the scene’s post-dim_orderdimension positions. They coincide for the NEXRAD supercell only because itsdim_orderleaves time last in both frames. An author who reads off the scene’sDimensionslist 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_ordermixup 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
lexsortreproducesorder: 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: alod_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, andresolve_additive_axis_gsplats()applies oneslice_dimsto every substitutive level, giving a slice-even finest level and silently uneven coarse ones. The defaultAutocoarsening (hidden axis as a hard barrier) is safe.- Parameters:
data (
GSplatData) – The datasetorderindexes; only itscentersare read.order (
np.ndarray) – A length-N integer permutation, as returned bycompute_additive_order(). Validated for shape, dtype and range; duplicate entries are the caller’s responsibility (see_validate_interleave_order()).slice_dims (
sequenceofint) – RAW, pre-dim_ordercentre 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.ndarrayofshape (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 ofauto,greedy,self_energy,mass,amplitude,spectral,random,radial.auto(the default) resolves togreedyat small N andself_energyabove_AUTO_ADDITIVE_MAX_N— seeresolve_additive_method(). See module docstring for details.truncation_sigmas (
float, optional) – Mahalanobis cutoff used for sparse-Gram pruning. Defaults to the dataset’s owntruncation_radius— the support the splats were fitted at and are rendered at. Only relevant forgreedyandspectral.max_n_dense (
int) – Forgreedy, 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 formethod='random'.reveal_center (
sequenceoffloat, optional) – Centre of the shells formethod='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 (
sequenceofint, 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 (
sequenceofint, optional) – RAW, pre-dim_ordercentre columns whose distinct combinations the viewer SLICES (a hidden time / channel axis) — NOT the scene’s post-dim_orderdimension positions, which are a different frame of reference. When given, the ordering chosen bymethodis re-emitted round-robin across those slices byinterleave_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 standaloneGSplatDatahas no display information to derive one from.
- Returns:
order[k]is the original index of the splat at rank $k$.- Return type:
np.ndarrayofshape (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
nsplats? (#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 ofAdditiveSubLODobjects that build would EMIT, not a re-derivation free to drift. The loop below also MIRRORS that build loop’sif end <= prev: continuede-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 againstmake_additive_lod(...).n_additive_sublodsper breakpoint kind intests/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 wrapperresolve_additive_rungs()):partition=and a multi-rung ladder are mutually exclusive, and that gate runs beforegraft_gsplat_nodebuilds akind=partitionwrapper it would otherwise strand. Presence of theadditive_lod=kwarg is not the question —{"n_lods": 1}resolves to one rung and partitions perfectly well, while{"method": "radial"}carries non_lodsto read and falls to then_lods=4default — four rungs on any leaf of >= 4 splats, andnon a smaller one, since equal-count cuts clamp to the leaf’s own size.Returns
Nonefor 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-positiven_lods, an unknown breakpoints string, a mixed list, a counts list exceedingn, …). 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.Nonesays 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 straysubstitutive_levelkey, 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 <= 0returns1, mirroringmake_additive_lod()’s empty-leaf branch, which emits exactly one sub-LOD labelledlod_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
GSplatDatawithn_lods(or as resolved bybreakpoints)AdditiveSubLODlevels 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: thesubstitutive_levelargument (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 whenbreakpoints='equal-count'. Ignored whenbreakpointsis a list or'stream:<c>'(those determine the level count themselves).method (
str) – Ordering method (seecompute_additive_order()).breakpoints (:py:class:
``’equal-count’:py:class:``, :py:class:``’stream:<c>’:py:class:``, orlistofint / float) –'equal-count':n_lodslevels of (nearly-)equal size.'stream:<c>': geometric streaming ladder — cumulative cuts[c, 2c, 4c, …, N]sized so the first chunk iscsplats (bandwidth-derived viastreaming_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 atDEFAULT_STREAM_MAX_LEVELSlevels.'equi-energy:<n>':nrungs 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 aboveDEFAULT_MAX_ADDITIVE_COMMITsplit into capped steps (luxar.utils.lod_breakpoints.equi_energy_cuts()). Pair with a contribution-firstmethod(self_energy, the large-N default) — underrandomthe 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 owntruncation_radius.max_n_dense (
int) – Threshold below whichgreedyuses a dense Gram + scan-greedy.seed (
int, optional) – Random seed formethod='random'.substitutive_level (
int, optional) – Index of the substitutive level to build the ladder for. Defaults todata.default_substitutive.reveal_center (
sequenceoffloat, 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 (
sequenceofint, 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 (
sequenceofint, optional) – RAW, pre-dim_ordercentre columns the viewer SLICES (a hidden time / channel axis) — NOT the scene’s post-dim_orderdimension positions. When given, the ordering is re-emitted round-robin across those slices (seeinterleave_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 everymethod, and theenergy_fraction_cumstamps 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’s1/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 akind=lodgroup:applyLodFadehas a second caller in the viewer’sscene/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-fractionbreakpointsresolve 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
GSplatDatawith the samen_substitutiveasdata; 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 below5000splats (highest quality and fast there),"kmeans_lloyd"above (greedy is ~50-100x slower at large N). For a large dataset the coarse early levels usekmeans_lloydand the small later levels switch togreedy."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 intoM = N/Kcontiguous, balanced, spatially coherent bins inO(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_lloydvariant 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 (seevolume_refit; requires thevolumeargument).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_dimsstamp (make_substitutive_lod()here,decimate()’smergefamily, and thebatch-fit mergeper-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 overviewandfit --recipe levelsbuild theirpipeline/group out of their INPUT’s stats rather than out of the recipe they ran, so the reduction’s own choice — an explicit--coarsen-dimsincluded — never lands on disk, and an absent key reads exactly like thenullbelow. 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 (thecoarsen_dims=Nonedefault, 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 writtennullfrom an absent key, so both read as “no provenance” and fall through todetect_barrier_dimsauto-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.ndimis only read to EXPAND aNonerequest, so a caller that always names its dims may passNonefor it rather than a stand-in width — a made-up width is the one thing this must not appear to assert. The twoNones 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
ℓhasceil(N / K^ℓ)splats.levels – Number of coarser levels to produce. The returned object has
levels + 1substitutive levels (the original at index 0).method – Partition algorithm, or
"auto"(default)."auto"resolves per level:"greedy"when the level’s input has<= 5000splats (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 anexp(-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: roughly0.1to1for greedy and1to10for Lloyd spans a soft-to-strong hue preference. Becausemethod="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.0disables (historical pure-moment-matching behaviour). Trade-off: coarse levels look slightly smoother; each splat’s integral (X-ray projection) is preserved exactly. Withrefine="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;Falserestores 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 globalconserve_massrescale 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 sourcevolumeitself (a fullfit_gaussian_splats()pass seeded by the merge) — the highest-fidelity option (+5–12 dB over the merge on real microscopy, seevolume_refit); requiresvolume. With barrier dims (coarsen_dimsset) each barrier group is re-fitted against its OWN slice of the volume, in the coarsened dims only — seevolume_regionsfor 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 forl2(L2RefineConfig), 300 forvolume(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 thevolumeaxis holding center dimi.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 toGSplatDatahas 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 atorch.device.seed – Seeds the L2-refine minibatch pair sampler when
refine="l2"(a localtorch.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
coloringaxis, time, or channel).label_idsare also exact barriers whenever present.None(default) coarsens over all center dims (the historical behavior). Passing all dims is equivalent toNone. 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 stampquality+reference_energyinto every level’s stats — the Q of the viewer’s committed qualityQ·e(k).reference_energyis 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 + 1and a single additive sub-LOD per substitutive level (the finest atsubstitutive_levels[0]).- Return type:
GSplatData- Raises:
ValueError – If
compression_factor < 2,levels < 1, ormethodis 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_targetrepresentatives — 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_lodreduces 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"— seemake_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_idsare also exact barriers whenever present. Default: all center dims.
- Returns:
A flat
GSplatDatawith at mostn_targetsplats. 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 < 1ormethodis 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 BSPkind=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 + atilesfine branch for close-up (unbalanced by design: detail only where you look). Huge scenes with a “see everything first” need.adaptive—tileswhere EVERY tile carries its ownlevelsgroup: 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 aGSplatData, written viaGSplatData.save().the composed recipes (
tiles/overview/adaptive) return aGSplatNodetree, written viawrite_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
RecipeNamefor 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
GSplatNodetree.
- 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 callingbuild_recipe(); passingmax_elements=Nonefalls back toDEFAULT_MAX_ELEMENTSso the builders are usable standalone too.- additive_method: Literal['auto', 'greedy', 'self_energy', 'mass', 'amplitude', 'spectral', 'random', 'radial'] = 'auto'
- __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_partitionyields a flatGSplatPartitionwhose 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_compressionforwards 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=partitionwhose every child is akind=lodgroup (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:
tilesgives each part an additive (prefix- sum, accumulating) ladder;overviewputs a single global substitutive cap above one partition.adaptiveis 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
tilesfine branch.The result is a
kind=lodgroup with children coarsest→finest in memory ([coarse_leaf, fine_partition], matching the on-disk order). Each child’scoverage_fractionselector threshold is stamped onto itsmeta(honored by both the standalone writer and the scene graft) viapartitioned_coverage_fractions(occupancy halving re-anchored at fills-screen): the coarse cap gets a fraction below the fine branch’sPARTITION_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 — seepartitioned_coverage_fractionsfor why a partition-bound ladder does not take the whole-object half-screen anchor (no per-dataset tuning; seeRecipeParams).
- 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).levelsmerges 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_modeis uniform andrecipeis a per-part recipe, elseNone.
- 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) andbuild_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 substitutiveGSplatLodGroup(levels). Legacy spellings translate viacanonical_recipe_name().cellis this part’s own tile, per center dim, and is required byrefine="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
recipefromdata.Returns a
GSplatDatafor the matrix recipes (flat/stream/levels) and aGSplatNodefor the composed recipes (tiles/overview/adaptive) — seeRecipeResult.
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_cumper additive sub-LOD — the cumulative self-energy fractione(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’seffective_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_energyper leaf — the absolute self-energy weightwused for partition-level quality aggregation.level_stats.qualityper lod-group child (opt-in,with_quality=True) — the measured mixture-L² qualityQof each level vs its group’s finest content, viamixture_quality(). This loads full splat arrays (the level + the finest reference both resident), so it is the expensive half; the freee(k)/wstamps 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, unlessdry_run, wrote).- levels: List[LevelStamp]
- 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).
- 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).
- 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_qualityonly).
- 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.zarrdirectory store in place.- Parameters:
path – A
.gsplats.zarrdirectory (compressed.zip/.tar.gzstores 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:
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 == Bthe four directed queries then see identical grids/k/radii and the three terms cancel:mixture_quality(x, x) ≈ 1by 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).
- 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
approxapproximatesreference.Returns
QualityResultwithquality = 1 − ‖A−B‖²/‖B‖²clamped to [0, 1]. Degenerate inputs: an emptyapproxscores 0 (it explains none of the reference); an emptyreferenceraises (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 substitutiveconserve_massstep, 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 4orgsplat 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
itersandconserve_massare user-exposed (via--refine-itersand the ladder-wide--conserve-massflag); the rest are fixed operating constants, not a tuning surface. The benchmark showed the warm-started fit near-converged by 150–300 iterations.- 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_massstep 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’sconserve_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"]), orNonewhen the store does not record one.The seed’s amplitudes are background-relative — that is the recipe’s contract and why
seed_amps_background_relativeis set below. The volume handed in is NOT: it is the raw source. Left unreconciled, the inner fit inheritsfloor="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 — theconserve_massDC 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.
- 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
seedagainstvolume; 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 framefit_gaussian_splatsemits, so any level derived from a fit of this volume qualifies. A seed whose bounding box clearly disagrees with that frame is returned untouched (seeVolumeRefitConfig.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.itersis the one quality/time knob andconfig.conserve_massfollows 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
seedwhen 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 moreAdditiveSubLOD, 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 scenekind=lodGroup). Children are ordered coarsest → finest in memory — the SAME order as the on-diskchild_<i>layout (child_0 = coarsest), so the serializer writes them straight through with no reversal.default_levelis a derived property (= the finest, last child).GSplatPartition— spatial split: all children are rendered (the scenekind=partitionGroup), each carrying its ownposition_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:
objectA 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 includecompression_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), andstats.- Type:
- 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:
objectSubstitutive 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_levelis a derived property (= the finest, last child): the level a simple consumer renders by default. It is deliberately distinct from the on-diskdefault_level(a viewer progressive-load hint = coarsest), which the serializer stamps independently.
- 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:
objectSpatial partition group — all children rendered (
kind=partition).Each child is a spatial part;
max_elementsrecords 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 (seeluxar.core.group.partition.BSPNode. to_serializable()). When present it is written to thekind=partitiongroup’s attrs so the viewer can order the parts back-to-front exactly (painter’s algorithm), correct even with the camera inside the volume.Nonewhen 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.
- 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
GSplatLeafinnode(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,), orNoneif the subtree holds zero splats. This is a center-only bound. The serializer’sposition_boundsis the verbatim center bounds (matching the scene path); onlychunk_boundswiden each chunk by the ellipsoidal extent at the dataset’s owntruncation_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
fnapplied 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_elementsandbsp_tree, and per-nodemeta— in which eachGSplatLeafis replaced byfn(leaf)(fntypically returns a transformed leaf). This is the write-side workhorse for tree-aware ops (e.g.gsplat transformon akind=partition) that the flatGSplatDatapath — 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
keyremoved from every node’smeta.Unlike
map_leaves()(which copies groupmetaverbatim), this scrubs a key from leaves AND group nodes. Its use is dropping thecoverage_fractionLOD-switch threshold after a geometry transform so the writer re-derives it: a stale threshold on a group node (anoverviewpartition child, or anadaptiveper-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_splatsselection 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. (Contrastiter_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), orNonefor an empty tree. Falls back to the unweighted center mean when the total amplitude is zero — matchingcenter_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.0if 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 exceedseps; a zero-variance categorical axis (a stacked-time / channel axis) is excluded. Used bytransform --centerto re-origin only the spatial axes. Reduces to a per-axis max-sigma vector (via each leaf’smarginal_sigmas) and applies the shared spatial-axis rule. Withfallback=True(the default) falls back to all axes when none qualify (or the tree is empty);fallback=Falsereturns 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 bareGSplatLeaf; multiple levels → aGSplatLodGroupreversed to coarsest-first (matching disk). This is whatGSplatDatastores as its ground-truth node on construction — cheap, with nocoverage_fractionderivation (the view-driven thresholds are a serialize-time concern, stamped bytree_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=lodgroup’s metaselectordescribes its AUTHORED per-childcoverage_fractionthresholds, so the two writers (io/_compiler/gsplat_tree.write_gsplat_nodeandgsplats_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→ValueErrorbefore 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 inmeta.Multiple substitutive levels → a
GSplatLodGroupof one leaf per level, reversed to coarsest-first (levelsis the finest-first matrix view; the tree stores coarsest-first to match disk). The in-memorydefault_levelis the derived finest (last) child; the persisted on-diskdefault_levelis 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_fractionselector threshold (a SCREEN-AREA fraction by occupancy halving; the group meta carriesselector="screen-area"to name the units), so a standalone substitutive.gsplats.zarrselects levels correctly in the viewer rather than being stuck at the finest level. This is the same single-sourcedcoverage_fractions()derivation the scene path uses.coverageoverrides that derivation. It defaults to the whole-objectcoverage_fractions(); a caller building a ladder that is bound to a spatial partition (theadaptiverecipe’s per-tile groups) passespartitioned_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.selectornames 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 customcoveragecallable 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 withselector="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
GSplatLodGroupwhose 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
ValueErrorfor 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
nodemaps 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:
objectCoarse feature-density grid over a volume + fast box queries.
- 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.
- 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:
objectA content-balanced decomposition of a volume into budgeted fit regions.
- 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 thekind=partitionnode as itsbsp_treeattr. Seeprune_serialized_bsp_tree().Leaf
partlabels indexboxesdirectly.Nonefor a plan that did not come from one recursion (e.g. read from a pre-#1555plan.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
- class luxar.gsplats.planner.PlanBox(box: List[int], n_features: int, budget: int)[source]
Bases:
objectOne fit region: a half-open box
[z0:z1, y0:y1, x0:x1]+ a budget.
- 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
planand return the merged result.fit_kwargsare forwarded tofit_gaussian_splatsper 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 likeauto/pNNwould 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-volumenorm_rangebelow 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 fromvolumeand forwards the same pair to every box.With
partition=True(the CLI default) the per-box splats are kept as akind=partitiontree — one part per box (boxes are core-disjoint, so this is exact) — for viewer frustum culling; aGSplatNodeis returned. Withpartition=Falsethe boxes are concatenated into a single flatGSplatDataleaf (--flat). Both shapes are scored against the whole reference volume; tree-shaped results carry the merged block in their rootmeta["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 withbudget > 0are spawned.jobs (
int) – Maximum number of concurrent worker processes.tmp_dir (
Path) – Directory for per-box outputs. Cleared first; removed on success unlesskeep_boxes; retained on failure for inspection.worker_cmd_builder (
callable) –(box_idx, out_path) -> argvreturning 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, onplan.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.Noneauto-detects, matchingrender_to_volume_tensor().keep_boxes (
bool, defaultFalse) – Keep the per-box temp outputs after a successful merge and let each retained worker score and stamp its own box output.verbose (
bool, defaultTrue) – 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
volumeis available; a tree stores them in its rootmeta["fit_stats"].- Return type:
GSplatDataorGSplatNode- Raises:
RuntimeError – If any worker exits non-zero, or exits cleanly but writes neither an output nor an
.emptymarker, or writes an unreadable store. The message names the offending boxes;tmp_diris 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_featuresdefaults to the density’s reference feature count, so each leaf holds ~the calibrated reference content and therefore gets ~the calibratedk_starbudget — the cleanest “calibrate at the scale you fit at” coupling.The recursion’s split planes are retained on
FitPlan.bsp_tree(leaf labels indexFitPlan.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
volumethen plan. Returns aFitPlan.threshold_absdefaults to the density’s recordedfeature_thresholdso 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>1trades 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, default16) – Coarse-cell edge (full-res voxels). Sets the planner’s spatial resolution.method (
{"peaks", "edges", "intensity"}, default"peaks") – Feature detector — matchescalibration.count_features.downsample (
int, default1) – Detect features on adownsample-strided volume (1 = consistent withcount_features); coordinates are mapped back to full-res before binning.threshold_rel (
float, default0.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 that0.95— one half of the falsesignal_limitedcurveluxar gsplat calused 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--flatmerge 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:
objectDecoded classical splats in the source file’s world (x, y, z) frame.
All decode-time nonlinearities are already applied:
scalesare linear standard deviations (expapplied),opacitiesare in[0, 1](sigmoid applied),quaternionsare unit-norm w-first, andcolorsare DC-baked RGB in[0, 1].
- 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:
objectA 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.
- 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 (seepositions_um()).
- units: Tuple[str | None, ...] = (None, None, None)
Physical unit per spatial axis, when the store declares one.
- 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
- 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.
- 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)ᵀwhereRcomes from the quaternion andMis 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 viasrgb_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_upFalse); 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
pathholds..splatand.spzare keyed on the extension (.spzadditionally verified by the gzip magic);.plyis sniffed from the header — achunkelement 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.zarrto 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
GSplatDataas an INRIA 3DGSpoint_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), orconstant(fixedconstant_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 forcecolors/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_restbands 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;
timepointslices 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
gsplatsas a node at timetin a tracksdata graph.Each node carries the default tracksdata attributes
t,mask(tracksdata.nodes.Mask) andbbox, plusamplitudeand the per-axis position (z/y/xfor the trailing dims). Call repeatedly with increasingt(and the samegraph) to build a time-lapse, then use tracksdata’s edge operators to link nodes into lineages.- Parameters:
gsplats (
GSplatData) – Fitted splats.gsplats.ndimmust equallen(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 tosplat_mask_and_bbox().graph (
tracksdata.graph.BaseGraph | None) – Graph to add to; a new in-memory (RustWorkX) graph is created ifNone.sort_by_amplitude (
bool) – Add nodes in ascending alpha-effective amplitude (A·α) order so brighter splats paint last in atracksdata.array.GraphArrayView(matches the original POC). Theamplitudenode 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
tracksdataextra 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 withmeta.json+ WebPs, thatmeta.json, or a.sogZIP).format – One of
auto(default, sniffed viadetect_classical_format()) or an explicit dialect name fromCLASSICAL_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
GSplatDatawith per-splat colors, ready for.save(), LOD recipes, orScene.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
.splatfile (flat 32-byte records).Scales are stored linear (
expalready applied by the converter), color is DC-baked RGB with opacity in the alpha byte, and the rotation is the unit quaternion quantized asround(q * 128 + 128)in (w, x, y, z) order.
- luxar.gsplats.interop.read_geff(path: str | Path) TrackingGraph[source]
Read a
.gefftracking graph into aTrackingGraph.- Parameters:
path – Path to the
.geffstore root (the directory holdingzarr.json).- Returns:
Node ids, timepoints, voxel-space ZYX positions, edges, and the voxel size read from the GEFF axis metadata.
- Return type:
- Raises:
ValueError – If the store is not a GEFF group, or lacks the
t/z/y/xnode 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_restbands (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.jsonreferencing lossless WebP images;pathmay be the bundle directory, itsmeta.json, or a.sogZIP. 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 byalpha − 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
.spzfile (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
chunkelement with 12 or 18 float32 min/max bounds per 256-splat chunk, and avertexelement 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 optionalshelement (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_sigmasupport to a local boolean mask + bbox.The covariance is
Sigma = L @ L.Tfor the lower-triangular Cholesky factorL = cholesky_factor. A voxelxis 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 then_sigmaellipsoid is computed analytically — the half-extent along axiskisn_sigma * sqrt(Sigma_kk) = n_sigma * ||L[k]||— and only that local box is rasterized, then clamped toframe_shape.- Parameters:
center (
np.ndarray) – Splat center, shape(d,), in voxel coordinates matchingframe_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, lengthd.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 toframe_shape).mask (
np.ndarray) – Boolean array of shapestop - 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.
axesexplicitly 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.
axesexplicitly 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 (
sequenceoffloat, 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 innerdenoise_nlmcalls.device (
strortorch.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 whenuse_2d_slice=Trueand the volume is 3D. Default: middle slice. Ignored for non-3D volumes.
- Returns:
Optimal h parameter.
- Return type:
- 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 (
strortorch.device, optional) – Target device. If None, usesvolume.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 thepytorchbackend 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 fixedhyields 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 NLMhmeans the same smoothing strength in every tile (each tile is normalized against the WHOLE-volume range, not its own extent).
- Returns:
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 viatorch.cuda, then falls back to the only profiled GPU.gpu_mem – If set and gpu_name is None, pick the profile whose
total_memory_gbis closest to this value.path – Profile YAML path.
- Returns:
Summary dict or
Noneif 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.Noneand"auto"auto-detect the best device. Options: “auto”, “cuda”, “mps”, “cpu”.truncate (
float, defaultDEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations. Gaussians are evaluated within this radius from their centers.intensity_floor (
float, default1e-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 atorch.Tensoron 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.Noneand"auto"auto-detect the best device.truncate (
float, defaultDEFAULT_TRUNCATION_RADIUS) – Truncation radius in standard deviations.intensity_floor (
float, default1e-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