Skip to main content

vitali

Multiscale geometric analysis of point clouds, in Rust, with Python bindings and a MATLAB bridge.

Two constructions live here, both from Mauro Maggioni and coauthors, and neither with a maintained implementation in any package ecosystem before this crate.

Geometric multi-resolution analysis builds a multiscale, piecewise-affine approximation of a point cloud: a tree partition of the data, a local PCA scaling basis and centre at every node, and a geometric wavelet basis relating each node to its parent. Allard, Chen and Maggioni, Multi-scale geometric methods for data sets II: geometric multi-resolution analysis, Applied and Computational Harmonic Analysis, 2012; Chen and Maggioni, Multiscale geometric wavelets for the analysis of point clouds, CISS, 2010.

Diffusion wavelets build an orthonormal multiresolution directly from a diffusion operator, rather than from translates and dilates of a fixed mother wavelet: rank-revealing Gram-Schmidt on the operator's columns gives a scaling basis, the operator is compressed onto it, the compressed operator is squared to represent the next dyadic power, and the process repeats. Coifman and Maggioni, Diffusion wavelets, Applied and Computational Harmonic Analysis, 2006.

The crate is named for the Vitali covering lemma. The greedy net that builds every scale of the GMRA partition is a Vitali covering argument.

Prior art in August 2026

Geometric multi-resolution analysis has three GitHub repositories, none above one star, one a 2015 Python toolkit and one an R package last pushed in February 2021. Nothing on PyPI, in the Julia General registry, or on crates.io.

Diffusion wavelets have nothing in any ecosystem. Wavelets.jl, ContinuousWavelets.jl and PyWavelets are classical wavelets on regular grids. The construction that builds an orthonormal multiresolution from a diffusion operator exists in the 2007 MATLAB sources and nowhere else.

Install

cargo add vitali     # Rust
pip install vitali   # Python

Rust

use vitali::gmra;

let points = gmra::datasets::d_sphere(4000, 4, 100, 0);
let tree = gmra::build(&points, &gmra::Options::default());

println!("{} nodes, depth {}", tree.nodes.len(), tree.depth());

let scales = gmra::reconstruct_all_scales(&tree, &points);
for (j, approx) in scales.iter().enumerate() {
    let err = (approx - &points).norm() / points.norm();
    println!("scale {}: relative error {:.3e}", j + 1, err);
}

Diffusion wavelets take a sparse operator:

use vitali::diffusion_wavelets::{Options, build_tree, circle_diffusion};

let t = circle_diffusion(256);
let tree = build_tree(&t, &Options::default());

for level in &tree.scaling {
    println!("level {}: dimension {}", level.level, level.ext_basis.ncols());
}

Point clouds are D x N in Rust, one column per point, following the reference MATLAB distribution.

Python

Point clouds are (n_points, n_features) in Python, one row per point, following scikit-learn. The boundary transposes.

import numpy as np
import vitali

X = vitali.datasets.d_sphere(4000, d=4, n_features=100, seed=0)
tree = vitali.gmra(X, min_leaf_size=50)

tree.median_scaling_dimension()   # 4.0, the manifold's true dimension
tree.depth
tree.num_nodes

scales = tree.reconstruct_all_scales(X)
for j, approx in enumerate(scales, start=1):
    err = np.linalg.norm(approx - X) / np.linalg.norm(X)
    print(f"scale {j}: relative error {err:.3e}")

A node exposes its own geometry:

node = tree.node(tree.leaves()[0])
node.centre          # (n_features,)
node.scaling_basis   # (local_dim, n_features), one direction per row
node.sigmas          # every singular value, before truncation
node.wavelet_basis   # the basis relating this node to its parent
node.radius

Diffusion wavelets take a dense array or any scipy sparse matrix, which is converted to CSR without ever being densified:

import scipy.sparse as sp
import vitali

T = sp.csr_matrix(vitali.circle_diffusion(256))
tree = vitali.diffusion_wavelets(T, precision=1e-10)

tree.scaling_dimensions()          # dimension at every level
tree.scaling()[0].ext_basis        # Phi_1 in the original delta basis
tree.wavelet()[1].ext_basis        # Psi_2 in the original delta basis

MATLAB

MATLAB reaches vitali through its Python interface, so the Rust runs underneath without a MEX build. Point pyenv at an interpreter your MATLAB release supports, once per machine:

pyenv(Version = "/path/to/python", ExecutionMode = "OutOfProcess");

Out-of-process execution keeps the extension's libraries away from the ones MATLAB loads itself, and is the sensible default.

Two conversions do all the work, and both are in matlab/ in this repository. tonumpy respects MATLAB's column-major order, which is where a hand-written conversion usually goes wrong:

addpath("matlab");

X = rand(4000, 100);              % n_points by n_features
tree = py.vitali.gmra(tonumpy(X), pyargs("min_leaf_size", int64(50)));

k = double(tree.median_scaling_dimension())
d = double(tree.depth)

scales = cell(tree.reconstruct_all_scales(tonumpy(X)));
approx = fromnumpy(scales{end});   % back to a MATLAB double matrix
err = norm(approx - X, "fro") / norm(X, "fro")

Three points decide whether a call works:

  • Keyword arguments go through pyargs. Every option is keyword-only.
  • Integers need an explicit cast. MATLAB numeric literals are doubles, and min_leaf_size, max_dim, levels, stop_fcns, manifold_dimension and every seed are integers. Pass int64(50), never 50.
  • Orientation is transposed against the MATLAB toolboxes. GMRA.m and DWPTree.m take D x N, one column per point. vitali's Python surface takes (n_points, n_features). Data prepared by GenerateData_and_SetParameters.m passes as tonumpy(X.').

A diffusion operator goes across as a dense matrix:

T = fromnumpy(py.vitali.circle_diffusion(int64(256)));   % or your own operator
tree = py.vitali.diffusion_wavelets(tonumpy(T), pyargs("precision", 1e-10));

levels = cell(tree.scaling());
Phi1 = fromnumpy(levels{1}.ext_basis);

matlab/vitali_demo.m runs the whole sequence end to end and prints the recovered dimension and the per-scale reconstruction error.

Differences from the reference distribution

Partitioning. The reference distribution partitions with METIS-based nested dissection or a cover tree, both reached only through precompiled MEX binaries with no source in the distribution: nesdis.mexa64 and covertree.mexa64 do not load under GNU Octave ('covertree' undefined near line 4, checked directly). This crate recursively splits each node's points at the median of their projection onto the node's top local PCA direction, which is a principal-direction divisive partition tree. Node for node, reconstruction error and dimension counts here are not directly comparable against a run using either reference partitioner. The comparisons below are against independently generated data of known dimension, and against the reference distribution's own reported figures as a target range.

Reconstruction. The reference distribution's inverse transform accumulates wavelet detail along the root-to-leaf path, corrected at every step by a tangential-correction mechanism (addTangentialCorrections, its own default) that keeps the accumulated sum aligned with each node's own direct local PCA reconstruction. Without that correction the accumulated sum drifts, because two nodes' independently computed local PCA planes are not nested in general, unlike diffusion wavelets' scaling spaces: an early build of the wavelet-sum reconstruction here measured 26x worse error at the finest scale than the same node's own direct reconstruction (0.66 against 0.025 relative error) on curved test data, worsening with depth. Reproducing that bookkeeping (ScalBasisChange, ScalWavBasisChange, ScalCenterConsts) is out of scope. reconstruct_all_scales instead reconstructs each scale directly from that scale's own node, centre + Phi' Phi (x - centre), which is the value the tangential correction exists to make the accumulated sum equal. Wavelet bases and translation constants are still constructed and available on every node, for compression-oriented use.

Precision. This crate is f64 throughout. See the D-Cube result below.

op_threshold. DWPTree.m's docstring states that OpThreshold defaults to 0; its shipped code defaults it to Precision when left unset. Options::op_threshold follows the shipped code as an explicit field with no implicit default, so a caller wanting the docstring's behaviour passes 0.0 and a caller wanting the shipped behaviour passes precision itself.

Validation

35 tests. Diffusion wavelets are checked against the four structural identities the construction is stated by, and against MATLAB-exported fixtures; GMRA is checked against manifolds of known intrinsic dimension.

Structural identities of the diffusion wavelets

On the circle at N = 256:

Identity Residual
Orthonormality, Phi_j' Phi_j = I 2e-15
Nesting, span Phi_j inside span Phi_{j-1} 3e-15
Wavelet-scaling complement 6e-16
Level-j operator equals the compression of T^(2^(j-1)) 5e-15

Dimension recovery under GMRA

Median scaling dimension across the whole tree, against each manifold's true dimension, fully adaptive (no fixed manifold_dimension, internal_threshold = 0.5, leaf_threshold = 1e-2, both reference defaults), N = 4000:

Manifold True dimension Median recovered
D-Sphere 4 4
D-Cube 6 5
SwissRoll 2 2
S-Manifold 2 2

Reconstruction error per scale under GMRA

The reference distribution's own double-precision run on D-Cube reconstructs to machine precision at every scale with no rise anywhere. Its single-precision run shows a non-monotone error rising to about 0.04 near the deepest scale. With manifold_dimension fixed at the true dimension (matching the reference distribution's own driver for these synthetic cases) and a leaf size comfortably above the dimension being estimated locally (min_leaf_size = 50 for 6-dimensional data), D-Cube at N = 4000, D = 50 gives:

[3.55e-16, 3.22e-16, 2.93e-16, 2.99e-16, 2.73e-16, 2.45e-16, 2.26e-16, 2.01e-16]

Machine precision at every one of the eight scales, and no rise between consecutive scales above 1e-12, which is the threshold the test asserts. Variation between neighbouring scales sits at the 1e-18 level, which is rounding rather than a trend. That reproduces the reference distribution's double-precision finding independently and through a different partitioner, and supports reading the reported single-precision spike as an artefact of that floating-point path rather than a property of the construction.

A smaller leaf size (min_leaf_size = 20, twenty points for a 6-dimensional local PCA) reproduces a related, smaller artefact: one leaf under-samples and keeps rank 5 rather than 6, leaving every dimension-fixed internal scale at machine precision and the finest scale at 0.37% relative error. Both runs are kept as tests. The effect is small, explained, and specific to under-sampled local PCA.

Curved data reconstructs approximately, with error decreasing by scale and settling above machine precision at the finest one. N = 2000, D = 50, min_leaf_size = 20, manifold_dimension fixed at the true dimension, seed 1:

D-Sphere (d = 4): [0.431, 0.246, 0.213, 0.172, 0.126, 0.074, 0.060, 0.025]
SwissRoll (d = 2): [0.390, 0.240, 0.179, 0.151, 0.102, 0.067, 0.035, 0.004]

cargo test -p vitali --release --test reconstruction -- --nocapture prints all of these, and crates/vitali/tests/ states the assertion behind each one.

Layout

crates/vitali/      the library, published to crates.io
crates/vitali-py/   PyO3 bindings, published to PyPI as vitali
matlab/             tonumpy, fromnumpy and a demo script

Licence

Apache-2.0.

Metadata

Release files for vitali 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vitali 0.1.0
File Size Uploaded
vitali-0.1.0.tar.gz 53.0 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for vitali 0.1.0
File
vitali-0.1.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
vitali-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
vitali-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details
vitali-0.1.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details

Total release size: 3.1 MB

Release files / vitali-0.1.0.tar.gz

Download URL vitali-0.1.0.tar.gz
Size 53.0 kB
Tags Source
SHA-256 checksum
How to use checksums
e9daf4a7d56b2414a74bf31ebc3776c6c6fcd63b97de34683b8cd675f0f36ecd
BLAKE2b-256 checksum
How to use checksums
dfdf320645d229132a5ae2c7f0fbb3c2528c02381d82fd0fd2a79a9c5a454cca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.

Transparency log

Release files / vitali-0.1.0-cp310-abi3-win_amd64.whl

Download URL vitali-0.1.0-cp310-abi3-win_amd64.whl
Size 297.3 kB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
202e3d2212f3d6be81486b2bfd8960c321dd077dd72559970df4db99704a3dc6
BLAKE2b-256 checksum
How to use checksums
936c39dfe2f3664bd32410a75a89319372190ed35d47a6a358a94b7398427612
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.

Transparency log

Release files / vitali-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL vitali-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 1.3 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
eb404fa8837ed7f2eb2925e5f71d91deb52f68d53a3eced4638fe2cc1c9cee82
BLAKE2b-256 checksum
How to use checksums
c7c6f85bf02012990ba4b08616a372edf7e725e1255ecc9834533b3a390f5fe3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.

Transparency log

Release files / vitali-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL vitali-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 1.1 MB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
b2c56bda075c931f3ddf2cc6cb293d8412426d9942b95f708d2cee2770523cb9
BLAKE2b-256 checksum
How to use checksums
ebe19689365a5ee9da4ce6a811cdd586cb1c25c6322a06bd4a8114b9115e969e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.

Transparency log

Release files / vitali-0.1.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL vitali-0.1.0-cp310-abi3-macosx_11_0_arm64.whl
Size 377.9 kB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
59c48bfe4213d87b73395c7f9ee8d6c91eee417a7c4a621af007fd530ad659e8
BLAKE2b-256 checksum
How to use checksums
d52725c80247162a0b574d95c4f8e00a517a1a47c8d4b44128a24232691eed82
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page