Riemannian Manifold API
Manifolds and their geodesic operations, plus the epsilon-net construction that approximates a manifold by a quantum graph.
kmeanssa_ng.riemannian_manifold
Riemannian Manifold module for kmeanssa-ng.
BolzaSurface
Bases: RiemannianManifold
The Bolza surface as a quotient-aware Riemannian manifold.
Points are (..., 2) real (Re, Im) coordinates in the fundamental
octagon of the Poincaré disk. The geodesic operations are intrinsic to the
surface: :meth:log takes each target to its nearest copy under the Fuchsian
group, and :meth:exp folds every step back into the fundamental domain. With
:meth:random_uniform, :meth:embed, :meth:log, :meth:exp and :meth:norm
all quotient-aware, the intrinsic :class:RepulsionNet builds a correct net,
whereas an embedding-based (extrinsic) search would fail across the identified
boundary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ball
|
Group ball used by the quotient metric (defaults to the cached geometric ball, exact over the fundamental domain). |
None
|
Source code in kmeanssa_ng/riemannian_manifold/bolza.py
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 | |
embed(points)
Not available: the quotient has no faithful Euclidean embedding.
embed exists so that an extrinsic neighbour search (a KD-tree on
ambient coordinates) can accelerate strategies on isometrically embedded
manifolds. The Bolza surface is a quotient: points glued across the octagon
boundary are close on the surface yet far apart in any disk-coordinate
image, so no such embedding is faithful. Returning the disk coordinates
anyway would make extrinsic neighbour search silently wrong, so this raises
instead. Use the intrinsic strategies (:class:RepulsionNet, which searches
by geodesic distance), not the extrinsic ones.
Source code in kmeanssa_ng/riemannian_manifold/bolza.py
random_tangent(base_point, rng)
Metric-isotropic tangent draw on the conformal disk chart.
The hyperbolic metric is conformal with factor 2/(1 - |z|^2), so scaling an ambient Gaussian by (1 - |z|^2)/2 yields a vector whose components are standard normal in an orthonormal frame of the metric -- the same Brownian law at every point of the surface.
Source code in kmeanssa_ng/riemannian_manifold/bolza.py
random_uniform(n, random_state=None)
(n, 2) points uniform by hyperbolic area in the fundamental octagon.
Source code in kmeanssa_ng/riemannian_manifold/bolza.py
EpsilonNetStrategy
Bases: ABC
Abstract base class for epsilon-net placement strategies.
Source code in kmeanssa_ng/riemannian_manifold/epsilon_net.py
build(manifold, n)
abstractmethod
Return n points on manifold forming a quasi-uniform net.
FibonacciNet
Bases: EpsilonNetStrategy
Deterministic Fibonacci lattice on the 2-sphere.
A near-optimal, low-discrepancy net available only for S^2; random_state
is ignored since the construction is deterministic.
Source code in kmeanssa_ng/riemannian_manifold/epsilon_net.py
KarcherFrechetMean
Bases: LloydUpdateStrategy
Update strategy that computes the new center as the Fréchet mean (Karcher mean) of the points in the cluster.
The mean is computed by the intrinsic Karcher iteration
mean <- exp_mean(average of log_mean(x_i)), driven entirely by the
space's own exp/log maps. This works on every space of the
package -- including quotient surfaces like Bolza, whose log picks
the nearest copy under the group action, which a chart-level estimator
(e.g. geomstats' FrechetMean on the underlying manifold) would
silently ignore.
The iteration is deterministic, fast and locally exact, but it needs the
space to provide exp/log and it descends to the nearest critical
point. For a stochastic, globally-minded alternative that works on any
metric space of the package (graphs included), see
SimulatedAnnealingFrechetMean.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_iter
|
int
|
Maximum number of Karcher iterations. |
64
|
tol
|
float
|
Stop when the Riemannian norm of the mean update step falls below this threshold. |
1e-09
|
Source code in kmeanssa_ng/riemannian_manifold/update.py
update(points, space)
Compute the new center for a given cluster of points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
list[RiemannianPoint]
|
A list of points belonging to a single cluster. |
required |
space
|
'RiemannianManifold'
|
The Riemannian manifold in which the points and center exist. |
required |
Returns:
| Type | Description |
|---|---|
'RiemannianCenter'
|
The new center for the cluster. |
Source code in kmeanssa_ng/riemannian_manifold/update.py
RepulsionNet
Bases: EpsilonNetStrategy
Uniform init relaxed by truncated Riesz repulsion (Riemannian gradient flow).
Fully intrinsic: each point is pushed away from its k nearest neighbours
-- found by geodesic distance -- along geodesics, with a step that is a
shrinking fraction of the local spacing, until the configuration freezes into
a near-regular net. Being driven only by the manifold's intrinsic log,
exp and norm, it is correct on any compact Riemannian manifold,
including quotient spaces (e.g. the Bolza surface) where an ambient embedding
would misrepresent neighbours across identified boundaries.
Neighbour search is O(n^2) in the point count. When the manifold is
isometrically embedded and ambient (chordal) proximity matches geodesic
proximity -- as for S^2 in R^3 -- :class:RepulsionNetExtrinsicSpeedup
replaces it by an O(n log n) KD-tree search on the embedding.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
int
|
Number of nearest neighbours each point repels. |
12
|
n_iter
|
int
|
Number of relaxation steps. |
600
|
riesz_s
|
float
|
Exponent of the repulsion kernel (force ~ 1 / d^(s+1)). |
1.0
|
cooling
|
float
|
Larger values cool more slowly (step ~ 1 / (1 + t / cooling)). |
300.0
|
random_state
|
int | Generator | None
|
Seed or Generator for the uniform initialisation. |
None
|
Source code in kmeanssa_ng/riemannian_manifold/epsilon_net.py
RepulsionNetExtrinsicSpeedup
Bases: RepulsionNet
:class:RepulsionNet with an extrinsic neighbour search (optimisation).
Overrides only the neighbour step, replacing the O(n^2) geodesic search by an
O(n log n) Euclidean k-NN (KD-tree) on manifold.embed(X). The repulsion
dynamics are otherwise identical.
Valid ONLY when embed is an isometric embedding whose ambient (chordal)
neighbour ordering matches the geodesic one -- true for a compact manifold
embedded in ambient space (e.g. S^2 in R^3), FALSE for a quotient space: there,
points glued across an identified boundary are geodesic neighbours yet lie far
apart in the embedding, so the KD-tree returns the wrong neighbours. Use the
intrinsic :class:RepulsionNet in that case.
Source code in kmeanssa_ng/riemannian_manifold/epsilon_net.py
RiemannianCenter
Bases: RiemannianPoint, Center
A movable cluster center on a Riemannian manifold.
Centers can perform: - Brownian motion: Random walk using Geomstats BrownianMotion - Drift: Directed movement toward target points along geodesics
Attributes:
| Name | Type | Description |
|---|---|---|
space |
RiemannianManifold
|
The Riemannian manifold this center belongs to. |
coordinates |
RiemannianManifold
|
The coordinates of the center on the manifold. |
Example
Source code in kmeanssa_ng/riemannian_manifold/center.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | |
__init__(point, rng=None)
Initialize a center from a point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point
|
RiemannianPoint
|
The initial point location. |
required |
rng
|
Generator | None
|
Random number generator. If None, creates a new default_rng(). |
None
|
Source code in kmeanssa_ng/riemannian_manifold/center.py
__repr__()
__str__()
User-friendly string representation.
Source code in kmeanssa_ng/riemannian_manifold/center.py
brownian_motion(time_to_travel)
Perform Brownian motion on the Riemannian manifold.
Implements a simple Brownian motion by: 1. Generating a random tangent vector 2. Scaling it by sqrt(time_to_travel) 3. Moving along the geodesic in that direction
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
time_to_travel
|
float
|
Time parameter (distance ~ sqrt(time)). |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If time_to_travel is negative or not numeric. |
Source code in kmeanssa_ng/riemannian_manifold/center.py
clone()
Create an independent copy of this center.
The cloned center shares the same manifold space but has independent coordinates. This is much faster than deepcopy.
Returns:
| Type | Description |
|---|---|
RiemannianCenter
|
A new RiemannianCenter with the same location but independent state. |
Example
Source code in kmeanssa_ng/riemannian_manifold/center.py
drift(target_point, prop_to_travel)
Move toward a target point along the geodesic.
Moves a proportion of the geodesic distance to the target point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target_point
|
RiemannianPoint
|
The point to move toward. |
required |
prop_to_travel
|
float
|
Proportion of distance to travel (0 to 1). |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If target_point is None, prop_to_travel is not numeric, or prop_to_travel is not in [0, 1]. |
Note
On manifolds where geodesics are not unique (e.g., antipodal points on a sphere), the drift may not move the center. This is a known limitation of Geomstats' geodesic computation. In practice, this rarely occurs due to the measure-zero probability of exact antipodal configurations, and the Brownian motion component of the simulated annealing algorithm provides thermal agitation to escape such degenerate configurations.
Source code in kmeanssa_ng/riemannian_manifold/center.py
RiemannianManifold
Bases: Space
A Riemannian manifold space using geomstats.
This class wraps a geomstats manifold object and implements the Space interface for k-means clustering on Riemannian manifolds.
Attributes:
| Name | Type | Description |
|---|---|---|
manifold |
The geomstats manifold object. |
Note
On manifolds with non-unique geodesics (e.g., antipodal points on spheres), the drift operation may exhibit degenerate behavior where centers do not move toward their targets. This is a known limitation of geodesic computation. The Brownian motion in the simulated annealing algorithm provides thermal agitation to escape such configurations.
Example
from geomstats.geometry.hypersphere import Hypersphere
sphere = Hypersphere(dim=2)
space = RiemannianManifold(sphere)
points = space.sample_points(100, strategy=UniformManifoldSampling())
centers = [space.center_from_point(p) for p in points[:3]]
energy = space.calculate_energy(centers, observations=points)
Source code in kmeanssa_ng/riemannian_manifold/space.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | |
dim
property
Intrinsic dimension of the manifold.
is_sphere
property
Whether the underlying manifold is a hypersphere.
shape
property
Ambient shape of a single point on the manifold.
__init__(manifold)
Initialize a Riemannian manifold space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manifold
|
A geomstats manifold object (e.g., Hypersphere, Hyperboloid). |
required |
calculate_energy(centers, how='empirical', observations=None)
Calculate the k-means energy for the given centers.
The energy is the mean squared distance from each observation to its nearest center. The observations must be passed explicitly: they belong to the algorithm evaluating the energy, not to the manifold, so several algorithms can share one space without interfering.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
centers
|
list[RiemannianCenter]
|
List of cluster centers. |
required |
how
|
str
|
Only |
'empirical'
|
observations
|
list[RiemannianPoint] | None
|
The points defining the empirical measure
( |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The k-means energy (mean squared distance to nearest center). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mode is not "empirical", observations are missing, or the centers list is empty. |
Source code in kmeanssa_ng/riemannian_manifold/space.py
center_from_point(point)
Create a RiemannianCenter object from a RiemannianPoint object.
distance(point1, point2)
Compute the geodesic distance between two points.
Uses the manifold's Riemannian metric to compute the distance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
point1
|
RiemannianPoint
|
First point. |
required |
point2
|
RiemannianPoint
|
Second point. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The geodesic distance between point1 and point2. |
Source code in kmeanssa_ng/riemannian_manifold/space.py
distances_from_centers(centers, target)
Compute distances from multiple centers to a single target point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
centers
|
list[RiemannianCenter]
|
List of k centers to compute distances from. |
required |
target
|
RiemannianPoint
|
The target point. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of shape (k,) with distances from each center to target. |
Example
Source code in kmeanssa_ng/riemannian_manifold/space.py
embed(points)
Ambient coordinates of points (for neighbour search).
Points are already extrinsic here, so this is the identity; it exists so geodesic strategies can stay agnostic to the coordinate representation.
Source code in kmeanssa_ng/riemannian_manifold/space.py
exp(base_point, tangent_vec)
Riemannian exponential: retract tangent_vec from base_point.
Batched over leading axes; arrays are ambient (extrinsic) coordinates.
Source code in kmeanssa_ng/riemannian_manifold/space.py
get_point_type()
log(base_point, point)
Riemannian logarithm: tangent at base_point pointing to point.
Inverse of :meth:exp; its Riemannian norm is the geodesic distance.
Source code in kmeanssa_ng/riemannian_manifold/space.py
norm(base_point, tangent_vec)
Riemannian norm of tangent_vec in the tangent space at base_point.
random_tangent(base_point, rng)
Tangent vector with iid standard-normal components in an
orthonormal basis of the metric at base_point.
This is the Brownian direction of the annealing: drawing it in a metric-orthonormal frame is what makes the exploration isotropic and its law identical at every point. On a hypersphere the ambient Euclidean frame is orthonormal for the induced metric, so projecting an ambient Gaussian is exact. For a general metric it is not (on a conformal chart the ambient draw is up to several times longer near the boundary than at the center), so spaces without a known orthonormal frame refuse rather than silently bias the dynamics.
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If no metric-isotropic draw is implemented for this manifold. Override this method with a draw in an orthonormal frame of the metric. |
Source code in kmeanssa_ng/riemannian_manifold/space.py
random_uniform(n, random_state=None)
Sample n points uniformly, reproducibly from random_state.
Returns an (n, dim + 1) array of ambient coordinates.
Source code in kmeanssa_ng/riemannian_manifold/space.py
to_tangent(base_point, ambient_vec)
Project an ambient vector onto the tangent space at base_point.
RiemannianPoint
Bases: Point
A point on a Riemannian manifold.
A Riemannian point is represented by its coordinates on the manifold. The coordinates are validated to ensure they belong to the manifold.
Attributes:
| Name | Type | Description |
|---|---|---|
space |
RiemannianManifold
|
The Riemannian manifold space this point belongs to. |
coordinates |
RiemannianManifold
|
The coordinates of the point on the manifold. |
Example
Source code in kmeanssa_ng/riemannian_manifold/point.py
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | |
space
property
The Riemannian manifold space this point belongs to.
__init__(space, coordinates)
Initialize a point on a Riemannian manifold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
space
|
RiemannianManifold
|
The Riemannian manifold space containing this point. |
required |
coordinates
|
ndarray
|
The coordinates of the point on the manifold. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If space is None, coordinates are not a numpy array, or coordinates don't belong to the manifold. |
Source code in kmeanssa_ng/riemannian_manifold/point.py
__repr__()
__str__()
String representation of the point.
Source code in kmeanssa_ng/riemannian_manifold/point.py
Sphere
Bases: RiemannianManifold
Hypersphere with closed-form geodesic operations.
The unit sphere's exponential, logarithm, distance and tangent projection have simple closed forms. Overriding them here avoids geomstats' generic per-call overhead (~7x on a single point), which dominates the manifold annealing loop; the results match geomstats to machine precision. Every override is vectorised over leading axes, matching the base class.
Source code in kmeanssa_ng/riemannian_manifold/space.py
UniformNet
Bases: EpsilonNetStrategy
Plain uniform sampling. A baseline: fast, but irregular for finite n.
Source code in kmeanssa_ng/riemannian_manifold/epsilon_net.py
approximate_geodesic_space(manifold, n, *, net=None, ell=None, random_state=None, intrinsic=False)
Approximate manifold by a quantum graph on an n-point epsilon-net.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manifold
|
RiemannianManifold
|
The geodesic space to approximate. |
required |
n
|
int
|
Number of net points. |
required |
net
|
EpsilonNetStrategy | None
|
Placement strategy (defaults to :class: |
None
|
ell
|
float | None
|
Connection radius (defaults to sqrt of the covering radius). |
None
|
random_state
|
int | Generator | None
|
Seed for the net and the covering-radius estimate. |
None
|
intrinsic
|
bool
|
Build the net and graph without an ambient embedding (required for quotient spaces such as the Bolza surface). |
False
|
Source code in kmeanssa_ng/riemannian_manifold/graph.py
build_epsilon_net_graph(manifold, points, ell=None, *, precompute=True, covering_test=10000, random_state=None, intrinsic=False)
Connect an epsilon-net into a QuantumGraph (edges within ell).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manifold
|
RiemannianManifold
|
The manifold the points live on. |
required |
points
|
ndarray
|
An |
required |
ell
|
float | None
|
Connection radius l(epsilon). Defaults to sqrt(covering radius). |
None
|
precompute
|
bool
|
Precompute pairwise shortest-path distances on the graph. |
True
|
covering_test
|
int
|
Sample size used to estimate the covering radius. |
10000
|
random_state
|
int | Generator | None
|
Seed for the covering-radius estimate. |
None
|
intrinsic
|
bool
|
Build edges from exhaustive geodesic distances instead of an ambient nearest-neighbour prefilter. Required for spaces without a faithful embedding (e.g. quotients like the Bolza surface); O(n^2). |
False
|
Returns:
| Type | Description |
|---|---|
QuantumGraph
|
A QuantumGraph with unit node/edge weights and geodesic edge lengths. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the resulting graph is disconnected (net too sparse for
|
Source code in kmeanssa_ng/riemannian_manifold/graph.py
create_bolza_surface()
Create the Bolza surface, a compact genus-2 hyperbolic space.
The Bolza surface is the quotient of the Poincaré disk by the Fuchsian group gluing opposite sides of a regular hyperbolic octagon (interior angle pi/4). It has constant curvature -1 and is the most symmetric closed genus-2 surface -- a negatively curved counterpart to the sphere. Unlike the other factories it carries no geomstats backend: its geodesic operations are closed-form and quotient-aware, so it plugs into the intrinsic epsilon-net strategies.
Returns:
| Name | Type | Description |
|---|---|---|
A |
BolzaSurface
|
class: |
BolzaSurface
|
in the fundamental octagon. |
Source code in kmeanssa_ng/riemannian_manifold/generators.py
create_hyperbolic_space(dim=2, **kwargs)
Create a hyperbolic space wrapped in a RiemannianManifold.
Creates the hyperboloid model of hyperbolic space H^dim.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dim
|
int
|
Dimension of the hyperbolic space (default: 2). |
2
|
**kwargs
|
Additional arguments passed to Hyperboloid constructor. |
{}
|
Returns:
| Type | Description |
|---|---|
RiemannianManifold
|
A RiemannianManifold wrapping the hyperbolic space. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If dim is not a positive integer (raised by Geomstats). |
TypeError
|
If dim is not a valid type (raised by Geomstats). |
Example
Note
This uses the hyperboloid model of hyperbolic geometry.
Source code in kmeanssa_ng/riemannian_manifold/generators.py
create_sphere(dim=2, **kwargs)
Create a hypersphere space wrapped in a RiemannianManifold.
Creates a hypersphere S^dim embedded in R^(dim+1). The sphere is equipped with the standard round metric inherited from the Euclidean ambient space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dim
|
int
|
Dimension of the sphere (default: 2 for the standard 2-sphere S^2). - dim=1: Circle S^1 in R^2 - dim=2: Standard sphere S^2 in R^3 - dim=3: 3-sphere S^3 in R^4 etc. |
2
|
**kwargs
|
Additional arguments passed to Hypersphere constructor. |
{}
|
Returns:
| Type | Description |
|---|---|
RiemannianManifold
|
A RiemannianManifold wrapping the hypersphere. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If dim is not a positive integer (raised by Geomstats). |
TypeError
|
If dim is not a valid type (raised by Geomstats). |
Example
Note
The sphere S^dim is the set of unit vectors in R^(dim+1): S^dim = {x ∈ R^(dim+1) : ||x|| = 1}
Source code in kmeanssa_ng/riemannian_manifold/generators.py
estimate_covering_radius(manifold, points, n_test=10000, random_state=None, *, intrinsic=False)
Estimate the covering radius max_z min_i d(z, x_i) by dense sampling.
Nearest net points are found in ambient coordinates (exact when the ambient
distance is monotone in the geodesic one, e.g. on the sphere), then the
geodesic distance to that nearest point is measured exactly. With
intrinsic=True the nearest net point is instead found by exhaustive
geodesic distance -- for spaces without a faithful embedding (e.g. quotients),
at O(n_test * n) cost.
Source code in kmeanssa_ng/riemannian_manifold/graph.py
:::