Utils Package
The utils package provides cross-cutting utility functions used by Luxar’s core
runtime. Demo-owned download, dataset, and runtime helpers are exposed through
the luxar.demos barrel instead.
Utility functions for Luxar.
Note: As of v1.4.0, scalar broadcasting is handled by ArrayEncoder in luxar.encoding. The broadcast_*_to_points() functions have been removed (obsolete).
- luxar.utils.arbol_warnings() Iterator[None][source]
Display warnings via arbol, or standard display when arbol hides them.
Usable as a decorator (
@arbol_warnings()). No-op when warning display is already owned by someone else (see_default_display_active()).
- luxar.utils.install_arbol_warnings() None[source]
Process-wide install for application entry points (the luxar CLI).
Skipped when a recorder or custom hook already owns warning display, so in-process CLI test harnesses (
CliRunnerunderpytest.warns) keep capturing warnings normally.
- luxar.utils.ensure_float32(array: NDArray) NDArray[float32][source]
Ensure array is float32 dtype, converting if necessary.
- Parameters:
array – Input array
- Returns:
Array with float32 dtype
- luxar.utils.validate_array_shape(array: NDArray, expected_shape: Tuple[int, ...] | List[Tuple[int, ...]], name: str = 'array', check_finite: bool = False) None[source]
Validate that an array has the expected shape.
- Parameters:
array – Array to validate
expected_shape – Expected shape or list of acceptable shapes
name – Name for error messages
check_finite – If True, additionally reject arrays containing NaN or ±Inf. Defaults to False for backward compatibility; opt in from callers whose contract forbids non-finite content (most validation/* uses already check this separately; new call sites should set check_finite=True at the boundary rather than re-validating downstream).
- Raises:
ValueError – If array doesn’t match expected shape(s), or if
check_finite=Trueand the array contains NaN / ±Inf.
- luxar.utils.atomic_copy_file(src: Path, dst: Path) Path[source]
Copy the FILE
srcontodstatomically (temp sibling + rename).shutil.copy2writes straight intodst, so an interruption (Ctrl-C, full disk, SIGKILL) leaves a truncated file under the final name — which a later run may mistake for a complete cache entry, or must quarantine and re-fetch. Copying to a sibling temp file and renaming makesdstappear only once it is complete. The temp file’s contents are flushed withos.fsyncbefore the rename, sodstcannot surface with unwritten blocks under a complete-looking name after a crash:os.replaceorders the rename against other renames, not against the preceding writes.Unlike
atomic_copytree(), an existingdstIS replaced: every caller is refreshing a cache entry and wants overwrite semantics. Metadata is preserved (copystat, i.e.copy2semantics), so mtime-based staleness checks keep working — including a read-only source mode, which is applied only after the flush so the copy itself never needs a writabledst.- Parameters:
src – Existing regular file to copy.
dst – Destination path; replaced if present. Parents are created.
- Returns:
dst.- Raises:
FileNotFoundError –
srcis missing or is not a regular file.OSError – Any I/O failure. The temp file is removed before re-raising, so
dstkeeps whatever it had before the call.
- luxar.utils.atomic_copytree(src: Path, dst: Path) None[source]
Copy the directory tree at
srctodstatomically.Writes to a sibling temp directory
dst.parent/.tmp_<dst.name>_<uuid>, then performs an atomicos.replace(tmp, dst)on success. On failure the temp directory is removed and the exception is re-raised, so the destination either exists in full or does not exist at all.- Parameters:
src – Source directory to copy. Must exist.
dst – Destination path. Must NOT already exist — the caller is responsible for clearing it if overwrite is desired (matches
shutil.copytree’s default behaviour).
- Raises:
FileExistsError –
dstalready exists.FileNotFoundError –
srcdoes not exist or isn’t a directory.OSError – Any I/O failure during the copy. The temp directory is cleaned up before re-raising.
Note
os.replaceis atomic on POSIX when the source and destination live on the same filesystem (always true here — we use a sibling temp dir). On Windows,os.replaceis atomic for files but the directory rename is implemented viaMoveFileExW, which gives the same atomicity guarantees in practice.
- luxar.utils.deprecated_kwarg_alias(kwargs: MutableMapping[str, object], old: str, new: str, *, since: str, remove_after: str, stacklevel: int = 3) MutableMapping[str, object][source]
Forward a renamed keyword argument from its old name to its new one.
Mutates
kwargsin place: whenoldis present it is popped, its value stored undernew, andwarn_deprecated()fires. Whenoldis absent nothing happens and nothing is emitted, so the call is free on the modern path. Passing both spellings is an error — silently preferring one would hide a real mistake in the caller.- Parameters:
kwargs – The
**kwargsmapping of the function accepting the alias.old – The deprecated keyword name.
new – The current keyword name.
since – Release that introduced the deprecation (
YYYY.MM.DD).remove_after – Earliest release or date the alias may be removed after.
stacklevel – As for
warn_deprecated(); the default attributes the warning to the caller of the function that ownskwargs.
- Returns:
The same mapping, for callers that prefer an expression.
- Raises:
TypeError – If both
oldandneware present inkwargs.
Example
>>> def add_points(name, positions, **kwargs): ... deprecated_kwarg_alias( ... kwargs, "reveal_centre", "reveal_center", ... since="2026.10.01", remove_after="2027.04.01", ... ) ... reveal_center = kwargs.pop("reveal_center", None)
- luxar.utils.deprecation_message(old: str, new: str | None, *, since: str, remove_after: str) str[source]
Build the one sentence every Luxar deprecation notice uses.
- Parameters:
old – The deprecated spelling as the user would type it (
"optimise","--reveal-centre","reveal_centre=").new – The replacement, or
Nonewhen the feature is going away with no successor.since – The Luxar release (CalVer,
YYYY.MM.DD) that introduced the deprecation.remove_after – The earliest release or date after which the alias may be removed, per the compatibility policy.
- Returns:
The formatted notice, ending in a full stop.
- luxar.utils.warn_deprecated(old: str, new: str | None = None, *, since: str, remove_after: str, stacklevel: int = 3) None[source]
Emit the standard deprecation notice as a
DeprecationWarning.- Parameters:
old – The deprecated spelling.
new – The replacement spelling, or
Nonewhen there is none.since – Release that introduced the deprecation (
YYYY.MM.DD).remove_after – Earliest release or date the alias may be removed after.
stacklevel – Frames to skip so the warning is attributed to the user’s code. The default of
3is right when this is called directly from inside the deprecated function: one frame for this helper, one for the deprecated function, landing on its caller.
Example
>>> def optimise_store(*args, **kwargs): # the alias ... warn_deprecated( ... "luxar.io.optimise_store", ... "luxar.io.optimize_store", ... since="2026.10.01", ... remove_after="2027.04.01", ... ) ... return optimize_store(*args, **kwargs)
- luxar.utils.create_lorenz_attractor(store_path: str | Path, n_points: int = 10000, seed: int | None = None) None[source]
Create a demo scene with a Lorenz attractor visualization.
This creates a beautiful butterfly-shaped 3D structure with colors that transition smoothly over time, demonstrating the Luxar scene format with an aesthetically pleasing mathematical visualization.
- Parameters:
store_path – Path to the Zarr store to create
n_points – Number of points to generate along the attractor
seed – Random seed for reproducible results
Example
>>> from luxar.utils import create_lorenz_attractor >>> create_lorenz_attractor('demo.zarr', n_points=50000)
- luxar.utils.create_random_spheres(store_path: str | Path, n_spheres: int = 100, points_per_sphere: int = 1000, seed: int | None = None) None[source]
Create a demo scene with random colored spheres.
- Parameters:
store_path – Path to the Zarr store to create
n_spheres – Number of spheres to generate
points_per_sphere – Points per sphere
seed – Random seed for reproducible results
- luxar.utils.create_time_series_demo(store_path: str | Path, n_timepoints: int = 10, n_points_per_time: int = 1000, seed: int | None = None) None[source]
Create a 4D time series demo scene.
- Parameters:
store_path – Path to the Zarr store to create
n_timepoints – Number of time points
n_points_per_time – Points per time step
seed – Random seed
- luxar.utils.get_datasets_dir(create: bool = True) Path[source]
Get the datasets directory at project root.
- Parameters:
create – Create the directory if it doesn’t exist (default True). Pass False to resolve the path read-only (e.g. when only checking whether an output exists, so listing does not create dirs).
- Returns:
Path to datasets/ directory
- luxar.utils.get_demos_output_dir(create: bool = True) Path[source]
Get output directory for demo scripts.
- Parameters:
create – Create the directory (and its parent) if missing (default True). Pass False for read-only path resolution (status/inventory), so a read-only command like
luxar demo listnever createsdatasets/.- Returns:
Path to datasets/demos/ directory
- luxar.utils.get_examples_output_dir(create: bool = True) Path[source]
Get output directory for example scripts.
- Parameters:
create – Create the directory (and its parent) if missing (default True).
- Returns:
Path to datasets/examples/ directory
- luxar.utils.get_project_root() Path[source]
Find the Luxar project root directory.
Traverses up from this file’s location looking for pyproject.toml. Result is cached for performance.
- Returns:
Path to project root containing pyproject.toml
- Raises:
RuntimeError – If project root cannot be found
Array Utilities
Array utility functions for Luxar processing.
This module provides efficient utilities for array processing and validation.
Note: As of v1.4.0, scalar broadcasting is handled by ArrayEncoder in luxar.encoding. The broadcast_*_to_points() functions have been removed (obsolete).
- Key Functions:
ensure_float32: Convert arrays to float32 dtype for GPU compatibility
validate_array_shape: Validate array dimensions with helpful errors
Example
>>> # Ensure array is float32
>>> data = ensure_float32(my_array)
>>> # Validate shape
>>> validate_array_shape(colors, (1000, 3), name="colors")
- luxar.utils.array.ensure_float32(array: NDArray) NDArray[float32][source]
Ensure array is float32 dtype, converting if necessary.
- Parameters:
array – Input array
- Returns:
Array with float32 dtype
- luxar.utils.array.validate_array_shape(array: NDArray, expected_shape: Tuple[int, ...] | List[Tuple[int, ...]], name: str = 'array', check_finite: bool = False) None[source]
Validate that an array has the expected shape.
- Parameters:
array – Array to validate
expected_shape – Expected shape or list of acceptable shapes
name – Name for error messages
check_finite – If True, additionally reject arrays containing NaN or ±Inf. Defaults to False for backward compatibility; opt in from callers whose contract forbids non-finite content (most validation/* uses already check this separately; new call sites should set check_finite=True at the boundary rather than re-validating downstream).
- Raises:
ValueError – If array doesn’t match expected shape(s), or if
check_finite=Trueand the array contains NaN / ±Inf.
Reusable Demo Generators
Color conversion and stacking helpers for Luxar demos.
- luxar.utils.colors.hsv_to_rgb(h: ndarray, s: Any = 1.0, v: Any = 1.0) ndarray[source]
Vectorized HSV→RGB for arrays of hues (all in [0, 1]).
A single shared implementation for the rainbow/hue-ramp colouring several demos each re-derived by hand.
his an array (or scalar);s/vmay be scalars or broadcastable arrays.- Returns:
(..., 3)float32 RGB in [0, 1] with the same leading shape ash.
- class luxar.utils.colors.StackedColorings(positions: np.ndarray, colors: np.ndarray, labels: list[str] | None, categories: list[str], keys: list[str] | None = None)[source]
Result of
stack_colorings()— a point cloud replicated once per coloring scheme along a leading categorical axis.
- luxar.utils.colors.stack_colorings(coords: ndarray, colorings: list[dict], keys: Sequence[str] | None = None) StackedColorings[source]
Replicate a point cloud once per coloring scheme along a categorical axis.
This is the render-proven pattern used by the census / peak-UMAP demos to give a single embedding several switchable color views: the N points are stacked K times (one block per coloring) and a leading
coloringindex column is prepended so a categoricalcoloringdimension can select which block (which colour scheme) is shown. Concentrating the stacking here keeps the per-point alignment of positions / colours / hover-labels correct and tested in one place instead of hand-rolled in each demo.- Parameters:
coords –
(N, D)spatial coordinates (Dis usually 3).colorings –
ordered list of dicts, one per color scheme. Each dict has:
"label": category name shown on thecoloringdimension."colors":(N, 3)float RGB for this scheme."labels"(optional):(N,)per-point hover strings for this scheme. If ANY coloring omits labels, the combinedlabelsisNone(hover disabled) rather than misaligned.
keys – optional
(N,)machine-readable per-point strings for link / copy templates to substitute as{hover_key}(#1917). Passed once for the whole cloud rather than per coloring, because a point’s IDENTITY does not change with the colour scheme — only its label does. Tiled K times here so it stays aligned with the stacked positions, which is the alignment this helper exists to own.
- Returns:
A
StackedColorings.positionshas shape(N*K, D+1)with column 0 = the coloring index;categoriesis the ordered label list for buildingDimension("coloring", categories=...).
Reusable demo scene generators.
- luxar.utils.scenes.create_lorenz_attractor(store_path: str | Path, n_points: int = 10000, seed: int | None = None) None[source]
Create a demo scene with a Lorenz attractor visualization.
This creates a beautiful butterfly-shaped 3D structure with colors that transition smoothly over time, demonstrating the Luxar scene format with an aesthetically pleasing mathematical visualization.
- Parameters:
store_path – Path to the Zarr store to create
n_points – Number of points to generate along the attractor
seed – Random seed for reproducible results
Example
>>> from luxar.utils import create_lorenz_attractor >>> create_lorenz_attractor('demo.zarr', n_points=50000)
- luxar.utils.scenes.create_random_spheres(store_path: str | Path, n_spheres: int = 100, points_per_sphere: int = 1000, seed: int | None = None) None[source]
Create a demo scene with random colored spheres.
- Parameters:
store_path – Path to the Zarr store to create
n_spheres – Number of spheres to generate
points_per_sphere – Points per sphere
seed – Random seed for reproducible results
- luxar.utils.scenes.create_time_series_demo(store_path: str | Path, n_timepoints: int = 10, n_points_per_time: int = 1000, seed: int | None = None) None[source]
Create a 4D time series demo scene.
- Parameters:
store_path – Path to the Zarr store to create
n_timepoints – Number of time points
n_points_per_time – Points per time step
seed – Random seed
Deprecation Notices
The post-release deprecation mechanism, built ahead of its first use. Public
names that change after the first release keep working for the window promised
in Compatibility & Deprecation Policy and announce the rename through
these helpers; the CLI half, luxar.cli.utils.deprecated_option, prints the
same sentence to stderr.
Deprecation helpers for the post-release compatibility window.
Before the first PyPI release a rename is a hard cut — the old spelling is
rejected with a pointer to the new one (see _reject_renamed_method_flags in
luxar.cli.lod and LEGACY_RECIPE_NAMES in luxar.gsplats.lod.recipes).
After it, a public name that changes keeps working for the window the
compatibility policy promises (docs/guides/user/COMPATIBILITY_POLICY.md:
two releases or six months, whichever is longer) while warning, and these
helpers are how every such alias says so in one voice.
Two entry points:
warn_deprecated()emits the standard sentence as aDeprecationWarning. Call it from inside the deprecated function, property, or alias so the defaultstacklevelpoints at the user’s call site rather than at Luxar.deprecated_kwarg_alias()handles the commonest case, a renamed keyword argument: it moves the old key onto the new one in the caller’s**kwargsand warns, so the function body only ever reads the new name.
The CLI counterpart, luxar.cli.utils.deprecated_option, prints to stderr
instead of warning: Python hides DeprecationWarning by default outside
__main__, which is the right default for a library and the wrong one for a
command someone just typed.
Every message carries the release the deprecation started in and the point after which the alias may go, so a reader of the warning never has to look the window up.
- luxar.utils.deprecation.deprecated_kwarg_alias(kwargs: MutableMapping[str, object], old: str, new: str, *, since: str, remove_after: str, stacklevel: int = 3) MutableMapping[str, object][source]
Forward a renamed keyword argument from its old name to its new one.
Mutates
kwargsin place: whenoldis present it is popped, its value stored undernew, andwarn_deprecated()fires. Whenoldis absent nothing happens and nothing is emitted, so the call is free on the modern path. Passing both spellings is an error — silently preferring one would hide a real mistake in the caller.- Parameters:
kwargs – The
**kwargsmapping of the function accepting the alias.old – The deprecated keyword name.
new – The current keyword name.
since – Release that introduced the deprecation (
YYYY.MM.DD).remove_after – Earliest release or date the alias may be removed after.
stacklevel – As for
warn_deprecated(); the default attributes the warning to the caller of the function that ownskwargs.
- Returns:
The same mapping, for callers that prefer an expression.
- Raises:
TypeError – If both
oldandneware present inkwargs.
Example
>>> def add_points(name, positions, **kwargs): ... deprecated_kwarg_alias( ... kwargs, "reveal_centre", "reveal_center", ... since="2026.10.01", remove_after="2027.04.01", ... ) ... reveal_center = kwargs.pop("reveal_center", None)
- luxar.utils.deprecation.deprecation_message(old: str, new: str | None, *, since: str, remove_after: str) str[source]
Build the one sentence every Luxar deprecation notice uses.
- Parameters:
old – The deprecated spelling as the user would type it (
"optimise","--reveal-centre","reveal_centre=").new – The replacement, or
Nonewhen the feature is going away with no successor.since – The Luxar release (CalVer,
YYYY.MM.DD) that introduced the deprecation.remove_after – The earliest release or date after which the alias may be removed, per the compatibility policy.
- Returns:
The formatted notice, ending in a full stop.
- luxar.utils.deprecation.warn_deprecated(old: str, new: str | None = None, *, since: str, remove_after: str, stacklevel: int = 3) None[source]
Emit the standard deprecation notice as a
DeprecationWarning.- Parameters:
old – The deprecated spelling.
new – The replacement spelling, or
Nonewhen there is none.since – Release that introduced the deprecation (
YYYY.MM.DD).remove_after – Earliest release or date the alias may be removed after.
stacklevel – Frames to skip so the warning is attributed to the user’s code. The default of
3is right when this is called directly from inside the deprecated function: one frame for this helper, one for the deprecated function, landing on its caller.
Example
>>> def optimise_store(*args, **kwargs): # the alias ... warn_deprecated( ... "luxar.io.optimise_store", ... "luxar.io.optimize_store", ... since="2026.10.01", ... remove_after="2027.04.01", ... ) ... return optimize_store(*args, **kwargs)
Console Output
luxar.utils.verbosity turns Luxar’s own narration down, or off. The library
layers below the CLI narrate through arbol — the right default for a long CLI
run, and the wrong one in a notebook cell or a napari plugin. Reachable from the
package root as luxar.set_verbosity() / luxar.verbosity().
Note that this writes process-global arbol state, so it is neither per-call nor thread-safe; the module docstring states the constraints in full.
Turn Luxar’s console output down, or off.
Luxar’s library layers narrate what they are doing through arbol — 547 aprint
calls below the CLI, in gsplats (314), io (146) and core (75). That
is the right default for a long CLI run, and the wrong one for a notebook cell
or a napari plugin, where the same tree scrolls past the result you asked for.
Until this module there was no way to say so: no verbose or quiet
parameter on Scene.save() or fit_gaussian_splats(), and no global switch
that was part of Luxar’s own API (audit finding A15-08).
Two entry points, one mechanism:
import luxar
luxar.set_verbosity("silent") # process-wide, until changed again
with luxar.verbosity("summary"): # scoped, restores on exit
scene.save()
Levels, and exactly what each one does:
|
Normal arbol narration is suppressed; Python warnings still surface via their standard display. |
|
Top-level lines and one level of nesting; deeper sections become truncation notices (depth 1). |
|
Three levels of nesting (depth 3). |
|
Everything. The default — unchanged behaviour. |
an |
That many levels of nesting. |
Implementation, stated plainly because it constrains how you can use this: these
functions set arbol.Arbol.enable_output and arbol.Arbol.max_depth, which
are class attributes. So:
The setting is process-global, not per-call and not per-object. A
verbosity()block around onesave()also quiets anything else running concurrently.It is not thread-safe. Two threads entering different
verbosity()blocks will interleave and the loser’s restore will win. The switches and arbol’s depth counter are shared class state.It changes the shared settings for all arbol users in the process, including other libraries. There is no Luxar-only namespace to scope it to.
A per-call verbosity= argument on the public entry points, and routing
library messages through logging so a handler could filter by module, are
both possible later; neither is needed to make the output silenceable, which is
what was actually missing.
max_depth=0 is deliberately not the silent level: arbol still prints
depth-0 lines plus a “(log tree truncated here)” notice for each section it
truncates at the cap, so a caller asking for silence and getting truncation
notices would be worse than the status quo. Measured, not assumed — see
tests/test_verbosity.py::TestTheDocumentedEffectsAreReal::test_depth_zero_is_not_silence.
- luxar.utils.verbosity.get_verbosity() Literal['silent', 'summary', 'normal', 'full'] | int[source]
Return the current verbosity as a level name, or an int depth.
Output disabled reports
"silent"regardless of the current depth. Otherwise a name is returned when the live arbol settings match a named level exactly, and the raw depth is returned when they do not. Consequently,set_verbosity(get_verbosity())is lossy for a manually disabled finite depth;verbosity()still restores the exact switch pair.- Returns:
One of the level names, or the current maximum depth as an int.
- luxar.utils.verbosity.set_verbosity(level: Literal['silent', 'summary', 'normal', 'full'] | int) None[source]
Set how much Luxar narrates, process-wide.
- Parameters:
level –
"silent","summary","normal","full", or an int giving the maximum section nesting depth to show.0shows top-level lines only and is not silence — pass"silent".- Raises:
ValueError – If
levelis an unknown name or a negative depth.TypeError – If
levelis a bool.
Example
>>> import luxar >>> luxar.set_verbosity("silent") >>> luxar.set_verbosity("full") # back to the default
See the module docstring for the scope and thread-safety caveats: this writes process-global arbol state.
- luxar.utils.verbosity.verbosity(level: Literal['silent', 'summary', 'normal', 'full'] | int) Iterator[None][source]
Set the verbosity for the duration of a block, then restore it.
Restores the exact previous
enable_output/max_depthpair rather than the level name it resolves to, so a caller who had setArbol.max_depthby hand gets their own value back.- Parameters:
level – As
set_verbosity().- Yields:
Nothing.
Example
>>> import luxar >>> with luxar.verbosity("silent"): ... pass # nothing this block does will print
Not thread-safe — see the module docstring.