Architecture and Core Concepts
This guide explains the fundamental ideas, design philosophy, and architectural decisions behind Luxar.
Big Picture: What is Luxar?
Luxar is a high-performance system for compiling and visualizing arbitrary-sized n-dimensional scenes containing points, lines, Gaussian splats, and triangle meshes. It enables:
Interactive exploration of billion-primitive datasets (points, lines, splats; meshes to millions of triangles) at 60 FPS
Arbitrary dimensionality (3D, 4D, 5D, nD) with intuitive navigation
Memory efficiency through progressive loading and intelligent caching
High quality rendering with HDR support and post-processing effects
Multi-primitive scenes: Points, lines, Gaussian splats, and triangle meshes (the meshes are shaded; the other three are emissive)
Key Innovation: Combine spatial indexing with nD hypersphere slicing to enable interactive exploration of datasets that don’t fit in memory.
Luxar delivers visualization performance limited only by your graphics card, display resolution, and network bandwidth—not by software constraints.
Design Philosophy
Luxar is built on several core principles:
Progressive Everything
Motivation: Billion-point datasets cannot fit in memory.
Solution: Write data progressively (no caching), load data lazily (chunk-based), render incrementally (spatial culling).
Benefit: Handle TB-scale datasets on GB-scale machines.
Spatial Locality is Sacred
Motivation: Adjacent points in space should be adjacent in memory for compression and cache efficiency.
Technique: Morton/Hilbert space-filling curves map nD coordinates to 1D ordering while preserving locality.
Impact: 2-10× better compression, 5-20× faster queries.
nD First, 3D Second
Philosophy: Don’t reduce nD data to 3D - visualize it natively.
Approach: Display 3 dimensions, slice through others with hypersphere visibility.
Result: Time-series, multi-channel, hyperspectral data visualized naturally.
Zero-Copy When Possible
Motivation: Memory copies are expensive for large datasets.
Implementation: Use views, references, broadcasting instead of duplication.
Example: Uniform color stored once, broadcasted to 1M points = 99.9% memory savings.
Semantic Awareness
Idea: Know what data represents (positions, colors, radii) to make smart encoding decisions.
Benefit: Automatic quantization with appropriate precision for each data type.
Architectural Layers
Luxar consists of five distinct layers:
Server Layer (FastAPI)
Purpose: Serve Zarr datasets over HTTP for browser-based visualization
Key Components:
create_server_app(): Factory function for FastAPI applicationsDirectoryListingStaticFiles: Custom static file handler with Zarr supportNetworkSimulationMiddleware: Test performance under realistic network conditionsCORS middleware: Enable cross-origin requests from viewer
Architecture:
The server layer sits between storage and viewer, providing:
Static File Serving: Zarr chunks and metadata served as static files
Directory Listing: JSON directory listing for Zarr structure discovery
Health Checks:
/healthendpoint for monitoringNetwork Simulation: Optional middleware to simulate bandwidth, latency, packet loss
Design Decisions:
Why FastAPI?
Modern async framework (handles many concurrent requests efficiently)
Automatic OpenAPI documentation
Type hints for request/response validation
Easy middleware integration
Why static file serving?
Zarr is designed for HTTP range requests
No need for complex query API - chunks are addressed directly
Browser can cache chunks efficiently
CDN-friendly for production deployments
Why directory listing?
Viewer needs to discover Zarr structure
Standard HTTP directory indices do not work for all browsers
JSON format enables programmatic access
Server Creation Example:
from luxar.cli.main import create_server_app
import uvicorn
# Create configured FastAPI app
app = create_server_app("/path/to/data.luxar.zarr")
# Run server
uvicorn.run(app, host="127.0.0.1", port=8000)
The server automatically:
Serves consolidated metadata for fast initialization
Provides directory listings for Zarr groups
Handles CORS for cross-origin viewer access
Includes health check at
/health
Integration Testing:
The server is designed for testing without mocking:
Create real server instances in tests
Make actual HTTP requests
Verify end-to-end behavior
Test with real Zarr data
See Programmatic Server Creation for detailed examples.
Python Layer (Data Creation)
Purpose: Create and encode visualization data
Packages:
luxar.core- Scene graph data structuresluxar.validation- Input validation with helpful errorsluxar.encoding- Semantic type-aware array encodingluxar.io- Progressive writing with spatial ordering
Key Concepts:
Scene Graph: Hierarchical organization (Scene → Groups → DataNodes)
Dimensions: Define nD space with units, ranges, navigation
Transforms: 4×4 matrices compose hierarchically (parent × child)
Semantic Types: COORDINATE, COLOR, POSITIVE_SCALAR, BOUNDED_SCALAR
Encoding Modes: AUTO (smart), PRECISION (lossless), MEMORY (aggressive compression)
Data Flow:
User Data (numpy arrays)
↓
Validation (check shapes, ranges, types)
↓
Semantic Typing (understand what data represents)
↓
Encoding (quantize based on semantic type)
↓
Spatial Ordering (Morton/Hilbert curves)
↓
Progressive Writing (no memory cache, stream to disk)
↓
Zarr Store (compressed, indexed, ready to serve)
Storage Layer (Zarr Format)
Purpose: Efficient storage and streaming
Key Innovations:
Chunk-based spatial index: Precomputed bounding boxes enable O(chunks × dims) queries
Compound ordering: Discrete dimensions (time, channel) → spatial dimensions (x, y, z)
Compression pipeline: Quantization (2-4×) + blosc/zstd (2-10×) = 4-40× total
Metadata consolidation: Root metadata document enables fast initialization
Design Decisions:
Why Zarr?
Cloud-native format (HTTP range requests)
Chunked storage (load only what’s visible)
Compression support (built-in)
Python and JavaScript libraries available
Why Morton/Hilbert ordering?
Space-filling curves preserve spatial locality
Adjacent points in nD space → adjacent in 1D storage
Better compression ratios (similar values group together)
Faster queries (scan fewer chunks)
TypeScript Layer (Viewer)
Purpose: Interactive visualization in the browser
Packages:
cache- Three-level caching (Memory, OPFS, HTTP)data- Scene loading, spatial queries, nD slicingrendering- WebGL materials, post-processingscene- Scene graph management, animationcontrols- Camera controls (orbit, fly)input- Keyboard/mouse handlingui- GUI components (sliders, monitors)
Key Algorithms:
Chunk Query (O(chunks × dims)):
Test each chunk’s bounding box for AABB intersection with query region. Load only chunks whose bounds overlap the view.
nD Hypersphere Slicing:
Points with radius R are visible if their distance in non-displayed dimensions ≤ R. Effective radius shrinks as you navigate away:
effective_radius = √(radius² - distance_in_nondisplayed_dims²)Intelligent Prefetching:
When loading chunk N, predict user will navigate to adjacent chunks (±1 in each dimension). Prefetch in background for instant loading.
Array Decoding:
Reverse Python encoding: dequantize integers → floats, expand broadcasted values, resolve LUT indices.
Design Decisions:
Why multiple cache levels?
L0 (decompressed in-memory): Ready-to-use decoded chunks, no re-decompress
L1 (memory, segmented LRU): Ultra-fast (~1μs) compressed chunks
L2 (OPFS): Persistent (~1ms) across page reloads
HTTP: Unlimited (~100ms) source of truth
(An additional “S-cache” — the SliceCache — sits above L0 and LRU-caches fully decoded per-slice geometry ladders, so revisiting a slice skips the whole query→fetch→decode pipeline.)
Why nD slicing vs dimension reduction?
Preserves all data dimensions
Intuitive keyboard navigation (1-9 select a non-displayed dimension, [ ] navigate)
No information loss from projection
WebGL Layer (Rendering)
Purpose: GPU-accelerated rendering
Key Techniques:
World-Space Point Sizing:
Points are specified in world units (micrometers, etc.) and automatically sized based on camera distance using angular diameter.
Gaussian Splatting:
Smooth point rendering using 2D Gaussian kernel in fragment shader. Creates soft, anti-aliased points.
HDR Rendering:
Float16 framebuffers support colors > 1.0 for scientific accuracy. Tone mapping converts to display range.
Multi-Pass Effects:
Post-processing effects (bloom, detector noise) composed dynamically based on enabled features.
Core Concepts Deep Dive
nD Visualization Paradigm
The Problem: How do you visualize 5D data (x, y, z, time, channel) on a 2D screen?
Traditional Approach (Dimension Reduction):
Project 5D → 3D using PCA, t-SNE, UMAP
Loss: Lose actual spatial relationships, lose 2 dimensions of information
Luxar Approach (nD Slicing):
Display 3 dimensions (x, y, z)
Slice through 2 dimensions (time, channel)
Points visible based on hypersphere intersection
Benefits:
No information loss - all 5 dimensions preserved
Intuitive - navigate time like any other dimension
Fast - spatial index makes queries O(chunks) not O(points)
Spatial Indexing Strategy
The Problem: Loading all points to find which are visible is too slow for billion-point datasets.
Solution: Chunk-based AABB spatial index
How it Works:
Preprocessing (Python, one-time):
Divide points into chunks (e.g., 2,048 points per chunk)
Calculate bounding box for each chunk
Store
chunk_boundsarray:[xmin, xmax, ymin, ymax, ...]per chunk
Query (TypeScript, real-time):
User navigates to position P with tolerance T
Test each chunk: does its AABB intersect the query box?
Load only matching chunks (typically 1-10 out of 100-1000 chunks)
Point Filtering (TypeScript, real-time):
Within loaded chunks, test each point for visibility
Display points within hypersphere
Complexity:
Preprocessing: O(N) to calculate bounds
Query: O(chunks × dims) - typically 1000 chunks × 5 dims = 5000 ops
vs. Naive: O(N) - would be 1,000,000,000 ops for billion points!
Speedup: ~200,000× faster
Compound Ordering for nD Data
The Problem: How to organize nD data for efficient time-series animation AND spatial queries?
Naive Approach: Morton order on all dimensions
Pros: Simple, preserves locality
Cons: Time frames scattered throughout file, poor compression
Luxar Approach: Two-level compound ordering
Primary: Group by discrete dimensions (time, channel)
All time=0 points together
All time=1 points together
etc.
Secondary: Morton order within each group (x, y, z)
Spatial locality within each time frame
Better compression (similar time frames compress together)
Result:
Time-series animation: Load contiguous chunks (excellent I/O)
Spatial queries: Morton order within groups (excellent locality)
Compression: Grouping similar data (2-3× better ratios)
Gaussian Splatting Integration
The Idea: Fit smooth Gaussian “splats” to images instead of discrete pixels
Motivation:
Scientific images are inherently continuous (microscopy, astronomy)
Gaussian splats are a natural representation (PSF = Gaussian)
Enables denoising, compression, and continuous rendering at any zoom (no voxel interpolation)
Luxar Integration:
Fit splats using
luxar.gsplats.fit_gaussian_splats()Save as a
GSplatsnode in the scene graphRender using
GaussianSplatModel(differentiable PyTorch model)Visualize in viewer with same spatial indexing as points
Use Cases:
Compress microscopy images 10-100×
Denoise while preserving features
Render continuously at any zoom, free of voxel-interpolation artefacts
Multi-scale image analysis
Transform Composition
The Problem: Matrix multiplication order is confusing (left vs right multiply)
Luxar Convention: Right-multiply with explicit compose() function
from luxar.core.transforms import translate, rotate, compose
# Create transforms
T1 = translate(10, 0, 0) # Move right
T2 = rotate(90, 'z') # Rotate 90° around Z
T3 = translate(0, 5, 0) # Move up
# Compose: applies T1 first, T3 last
result = compose(T1, T2, T3)
# Mathematically: result = I @ T1 @ T2 @ T3
# Applied to point: (T1 @ T2 @ T3) @ point
Key Insight: compose(A, B, C) reads left-to-right but uses right-multiplication internally.
Why This Matters:
Transform composition order affects results (not commutative)
Parent transforms apply after child transforms (hierarchical)
NumPy (row-major) vs THREE.js (column-major) requires transposition
Encoding Strategy Selection
The Problem: One encoding doesn’t fit all data types
Semantic Type System:
Each array is classified by what it represents:
COORDINATE: Positions in space (bounds can vary, quantize with tolerance)
COLOR: RGB values (bounded [0,1] for SDR, unbounded for HDR)
POSITIVE_SCALAR: Radii, amplitudes (always ≥0, relative precision matters)
BOUNDED_SCALAR: Sharpness, opacity (fixed range, absolute precision)
Encoding Decision Tree:
Is data uniform (all values identical)?
├─ YES → Broadcast (store 1 value, 99.9% savings)
└─ NO ↓
Does exact duplicate exist in registry?
├─ YES → Array Reference (store pointer, 100% dedup)
└─ NO ↓
Does data have ≤256 unique values?
├─ YES → LUT Encoding (palette + indices, 4-8× savings)
└─ NO ↓
Dtype Encoding based on semantic type:
- COORDINATE → uint16 (±32K range, ~0.1% precision)
- COLOR (SDR) → uint8 (256 levels per channel)
- COLOR (HDR) → geolog_perchannel_u16 (per-channel true-log; decoded to float32)
- POSITIVE_SCALAR → uint16 (log quantization for range)
- BOUNDED_SCALAR → uint8 (linear quantization)
Result: Automatic optimal encoding with 4-40× compression while preserving visual quality.
Performance Model
Understanding Luxar’s performance characteristics.
Note
The numbers below are approximate order-of-magnitude estimates based on typical hardware (modern laptop/desktop with dedicated GPU, SSD storage, broadband network). Actual performance varies with hardware, dataset characteristics, and network conditions.
Write Performance (Python)
1M points, 3D:
- Without spatial ordering: ~5M points/sec
- With Morton ordering: ~1M points/sec (sorting overhead)
- With chunk bounds: ~800K points/sec (AABB calculation)
Tradeoff: 5× slower write, 200,000× faster queries (well worth it!)
Read Performance (TypeScript)
Query for visible points:
- Chunk query: ~1ms (O(chunks × dims), typically 1000 × 5)
- HTTP fetch: ~100ms per chunk (network latency)
- Decode: ~5ms per chunk (dequantization)
- Slice: ~10ms per chunk (visibility filtering)
Total: ~115ms for first load, ~16ms for cached
With prefetching: ~16ms average (cache hit rate >80%)
Rendering Performance (WebGL)
GPU rendering (60 FPS target):
- 100K points: Vertex shader ~2ms, Fragment ~4ms = ~6ms ✓
- 1M points: Vertex ~8ms, Fragment ~8ms = ~16ms ✓
- 10M points: Vertex ~60ms, Fragment ~60ms = ~120ms (~8 FPS) ⚠
Optimization: Spatial culling reduces points to render by 10-100×
When to Use Luxar
Luxar is Ideal For:
✅ Large datasets (>100K primitives, up to billions of points/lines/splats, or millions of triangles)
✅ nD scientific data (microscopy time-series, multi-channel imaging)
✅ Interactive exploration (need to navigate/inspect data)
✅ Remote visualization (data on server, view in browser)
✅ Publication figures (HDR, high quality rendering)
Consider Alternatives For:
❌ Small datasets (<10K points) - overhead not worth it, use matplotlib/plotly
❌ Real-time streaming - Luxar is for static datasets, not live data streams
❌ Mesh authoring / CAD - Luxar renders triangle meshes (
add_mesh,luxar mesh import), but it does not edit them, and a mesh has no spatial index (seedocs/specs/MESH_NODE_SPEC.md§9), so one level always loads whole. Spatial partitioning is available (add_mesh(partition=…)), which splits a large surface into frustum-cullable parts — but each part still loads whole❌ 2D plots - Use specialized 2D libraries (bokeh, plotly)
Common Workflows
Workflow 1: Microscopy Time-Series
# You have: 4D microscopy data (X, Y, Z, Time)
# Goal: Visualize cell tracking over time
from luxar.core import Scene, Dimensions, Dimension
from luxar.io import LuxarZarrCompiler
import numpy as np
# Define 4D dimensions
dims = Dimensions([
Dimension("X", unit="um", spatial=True, display=True),
Dimension("Y", unit="um", spatial=True, display=True),
Dimension("Z", unit="um", spatial=True, display=True),
Dimension("Time", discrete=True, display=False, step=0.5),
])
# Write with compound ordering for efficient time navigation
with LuxarZarrCompiler('cells.luxar.zarr') as c: # Hilbert ordering by default
scene = c.create_scene(dimensions=dims)
# Track cell positions over time
for t, positions_t in enumerate(cell_positions_by_time):
# Add time coordinate
positions_4d = np.column_stack([positions_t, np.full(len(positions_t), t * 0.5)])
scene.add_points(f"cells_t{t}", positions_4d, colors=track_colors[t])
# Result: Navigate time with [ ] keys, see cells move
Workflow 2: Multi-Channel Imaging
# You have: 5D data (X, Y, Z, Channel, Wavelength)
# Goal: Explore different fluorescence markers
dims = Dimensions([
Dimension("X", unit="um", spatial=True),
Dimension("Y", unit="um", spatial=True),
Dimension("Z", unit="um", spatial=True),
Dimension("Channel", discrete=True, display=False, categories=["DAPI", "GFP", "mCherry"]),
Dimension("Wavelength", unit="nm", display=False, discrete=True),
])
# At most 3 dimensions can be displayed, so Channel and Wavelength are
# non-displayed (navigated with the [ ] keys). A non-spatial, non-displayed
# dimension must be discrete, so Wavelength is marked discrete=True
# explicitly (otherwise Luxar sets it automatically and emits a warning).
# Categorical dimension allows channel selection by name
# Discrete + spatial ordering groups all DAPI points together
Workflow 3: Gaussian Splat Fitting
# You have: 2D/3D microscopy image
# Goal: Fit Gaussian splats for compression/denoising
from luxar.gsplats import fit_gaussian_splats
# Fit splats to image
result = fit_gaussian_splats(
image,
n_iters=300,
lr=0.01,
loss_type="l1",
seed_method="edges", # Edge-based seeding
)
# Save for visualization
result.save("fitted.gsplats.zarr")
# Reconstruction (render back to image/volume)
reconstructed = result.render_to_volume(shape=image.shape)
compression_ratio = image.nbytes / result.centers.nbytes
print(f"Compression: {compression_ratio:.1f}×")
Key Takeaways
Luxar is designed for scale: Billion points, TB datasets, interactive exploration
Spatial indexing is the foundation: Makes everything fast
nD is first-class: Not an afterthought, built-in from the start
Semantic awareness: Understanding data types enables smart encoding
Progressive architecture: No memory bottlenecks, handle any size data
Browser-based: No installation, works on any device, shareable URLs
Next Steps
Try it: Start with a Quick Start from any package README
Explore code: Well-documented with extensive examples
Ask questions: GitHub issues/discussions