Meshing a Manifold
Why mesh a manifold?
kmeanssa-ng clusters points on a quantum graph—a
network whose edges are continuous. But a lot of data does not live on a
graph at all: directions on a sphere, points in hyperbolic
space, orientations on a rotation group. These are smooth geodesic
spaces, not networks.
The bridge is an \(\varepsilon\)-net: a finite set of points spread so densely over the manifold that no location is farther than \(\varepsilon\) from some net point. Connect the net points that are close together and you obtain a quantum graph that approximates the manifold—shortest paths through the graph track true geodesics, to within \(\varepsilon\). You can then cluster on that graph exactly as on any other. This approximation is the computational backbone of online \(k\)-means on geodesic spaces: as the net is refined (\(\varepsilon \to 0\)), clustering on the graph converges to clustering on the manifold.
The manifolds themselves (create_sphere, create_hyperbolic_space,
RiemannianPoint) are described in Riemannian
Manifolds; this page is about turning one into
a graph.
One call: approximate_geodesic_space
Given a manifold and a number of net points,
approximate_geodesic_space builds the whole approximating graph—place
the net, estimate its resolution, connect nearby points:
from kmeanssa_ng import create_sphere, approximate_geodesic_space, FibonacciNet
sphere = create_sphere(2)
graph = approximate_geodesic_space(sphere, 500, net=FibonacciNet())
print(f"{graph.number_of_nodes()} nodes, {graph.number_of_edges()} edges")
500 nodes, 3021 edges
The result is an ordinary QuantumGraph whose nodes sit on the sphere
and whose edge lengths are geodesic distances. Everything
downstream—sampling, simulated annealing, evaluation metrics—works
unchanged.
Clustering on the mesh
Because the mesh is a quantum graph, clustering is the familiar workflow:
from kmeanssa_ng import SimulatedAnnealing, KMeansPlusPlus, MinimizeEnergy
from kmeanssa_ng.quantum_graph.sampling import UniformNodeSampling
points = graph.sample_points(200, strategy=UniformNodeSampling())
sa = SimulatedAnnealing(points, k=3, lambda0=1.0, beta0=0.5, step_size=0.05)
centers = sa.run(KMeansPlusPlus(), MinimizeEnergy(), robust_prop=0.1)
print(f"Found {len(centers)} cluster centers on the sphere")
Found 3 cluster centers on the sphere
Choosing the net: placement strategies
How well the graph approximates the manifold depends on how evenly the net covers it. Placement follows the same strategy pattern as sampling and initialization:
| Strategy | Manifold | Idea |
|---|---|---|
RepulsionNet |
any (compact) | start from a uniform random cloud and let the points repel each other along geodesics until they freeze into a near-regular configuration—like charged particles on a surface |
FibonacciNet |
sphere \(\mathbb{S}^2\) only | a deterministic golden-angle spiral; near-optimal and instant |
UniformNet |
any | plain uniform sampling—a fast baseline, but irregular for finite \(n\) |
RepulsionNet is the general-purpose choice: it is driven only by the
manifold’s exponential and logarithm maps, so the same code meshes the
sphere, hyperbolic space, and any other compact manifold.
from kmeanssa_ng import RepulsionNet
net = RepulsionNet(random_state=0)
net_points = net.build(sphere, 300)
print(f"Repulsion net: {net_points.shape[0]} points on the sphere")
Repulsion net: 300 points on the sphere
How fine is the net? Covering radius
The covering radius \(\varepsilon\) is the largest distance from any
point of the manifold to its nearest net point—the net’s resolution.
estimate_covering_radius measures it by dense sampling, and the graph
connects points within \(\ell(\varepsilon) = \sqrt{\varepsilon}\), the
regime in which the graph faithfully approximates the geodesic space:
import numpy as np
from kmeanssa_ng import estimate_covering_radius, build_epsilon_net_graph
eps = estimate_covering_radius(sphere, net_points, random_state=0)
print(f"covering radius eps = {eps:.3f} connection radius sqrt(eps) = {np.sqrt(eps):.3f}")
# build_epsilon_net_graph gives explicit control over the connection radius
graph = build_epsilon_net_graph(sphere, net_points, ell=float(np.sqrt(eps)))
print(f"{graph.number_of_edges()} edges within l(eps)")
covering radius eps = 0.143 connection radius sqrt(eps) = 0.379
1219 edges within l(eps)
Refining the net (more points) shrinks \(\varepsilon\), and the graph resolves finer structure on the manifold—at the cost of a larger graph to precompute.
[!NOTE]
On non-compact domains such as hyperbolic space, unbounded repulsion pushes points toward the boundary and leaves the interior sparse.
RepulsionNettargets compact manifolds; bounded regions with a soft confining potential are a planned extension.