đ Get Started with Manifolds.jl
This is a short overview of Manifolds.jl
and how to get started working with your first Manifold. we first need to install the package, using for example
using Pkg; Pkg.add("Manifolds")
Then you can load the package with
using Manifolds
Using the Library of Manifolds
Manifolds.jl
is first of all a library of manifolds, see the list in the menu here under âbasic manifoldsâ.
Letâs look at three examples together with the first few functions on manifolds.
1. The Euclidean space
The Euclidean Space Euclidean brings us (back) into linear case of vectors, so in terms of manifolds, this is a very simple one. It is often useful to compare to classical algorithms, or implementations.
Mâ = Euclidean(3)
Euclidean(3; field=â)
Since a manifold is a type in Julia, we write it in CamelCase. Its parameters are first a dimension or size parameter of the manifold, sometimes optional is a field the manifold is defined over.
For example the above definition is the same as the real-valued case
Mâ === Euclidean(3, field=â)
true
But we even introduced a short hand notation, since â is also just a symbol/variable to useâ
Mâ === â^3
true
And similarly here are two ways to create the manifold of vectors of length two with complex entries â or mathematically the space $\mathbb C^2$
Euclidean(2, field=â) === â^2
true
The easiest to check is the dimension of a manifold. Here we have three âdirections to walk intoâ at every point $p\in \mathbb R ^3$ so manifold_dimension
is
manifold_dimension(Mâ)
3
2. The hyperbolic space
The $d$-dimensional hyperbolic space is usually represented in $\mathbb R^{d+1}$ as the set of points $p\in\mathbb R^3$ fulfilling
\[p_1^2+p_2^2+â s+p_d^2-p_{d+1}^2 = -1.\]
We define the manifold using
Mâ = Hyperbolic(2)
Hyperbolic(2)
And we can again just start with looking at the manifold dimension of Mâ
manifold_dimension(Mâ)
2
A next useful function is to check, whether some $pâ\mathbb R^3$ is a point on the manifold Mâ
. We can check
is_point(Mâ, [0, 0, 1])
true
or
is_point(Mâ, [1, 0, 1])
false
Keyword arguments are passed on to any numerical checks, for example an absolute tolerance when checking the above equality.
But in an interactive session an error message might be helpful. A positional (third) argument is present to activate this. Setting this parameter to true, we obtain an error message that gives insight into why the point is not a point on Mâ
. Note that the LoadError:
is due to quarto, on REPL
you would just get the DomainError
.
is_point(Mâ, [0, 0, 1.001]; error=:error)
LoadError: DomainError with -1.0020009999999997:
The point [0.0, 0.0, 1.001] does not lie on Hyperbolic(2) since its Minkowski inner product is not -1.
DomainError with -1.0020009999999997:
The point [0.0, 0.0, 1.001] does not lie on Hyperbolic(2) since its Minkowski inner product is not -1.
Stacktrace:
[1] is_point(::ManifoldsBase.TraitList{IsEmbeddedManifold, ManifoldsBase.TraitList{IsDefaultMetric{MinkowskiMetric}, ManifoldsBase.TraitList{IsMetricManifold, ManifoldsBase.EmptyTrait}}}, M::Hyperbolic{ManifoldsBase.TypeParameter{Tuple{2}}}, p::Vector{Float64}; error::Symbol, kwargs::@Kwargs{})
@ ManifoldsBase ~/.julia/packages/ManifoldsBase/zJJKa/src/decorator_trait.jl:416
[2] is_point
@ ~/.julia/packages/ManifoldsBase/zJJKa/src/decorator_trait.jl:386 [inlined]
[3] #is_point#149
@ ~/.julia/packages/ManifoldsBase/zJJKa/src/nested_trait.jl:313 [inlined]
[4] #is_point#148
@ ~/.julia/packages/ManifoldsBase/zJJKa/src/nested_trait.jl:306 [inlined]
[5] top-level scope
@ In[13]:1
3. The sphere
The sphere $\mathbb S^d$ is the $d$-dimensional sphere represented in its embedded form, that is unit vectors $p \in \mathbb R^{d+1}$ with unit norm $\lVert p \rVert_2 = 1$.
Mâ = Sphere(2)
Sphere(2, â)
If we only have a point that is approximately on the manifold, we can allow for a tolerance. Usually these are the same values of atol
and rtol
allowed in isapprox
, i.e. we get
is_point(Mâ, [0, 0, 1.001]; atol=1e-3)
true
Here we can show a last nice check: is_vector
to check whether a tangent vector X
is a representation of a tangent vector $XâT_p\mathcal M$ to a point p
on the manifold.
This function has two positional arguments, the first to again indicate whether to throw an error, the second to disable the check that p
is a valid point on the manifold. Usually this validity is essential for the tangent check, but if it was for example performed before, it can be turned off to spare time.
For example in our first example the point is not of unit norm
is_vector(Mâ, [2, 0, 0], [0, 1, 1])
false
But the orthogonality of p
and X
is still valid, we can disable the point check, but even setting the error to true we get here
is_vector(Mâ, [2, 0, 0], [0, 1, 1], true, false)
false
But of course it is better to use a valid point in the first place
is_vector(Mâ, [1, 0, 0], [0, 1, 1])
true
and for these we again get informative error messages
@expect_error is_vector(Mâ, [1, 0, 0], [0.1, 1, 1]; error=:error) DomainError
LoadError: LoadError: UndefVarError: `@expect_error` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
in expression starting at In[19]:1
LoadError: UndefVarError: `@expect_error` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
in expression starting at In[19]:1
To learn about how to define a manifold yourself check out the How to define your own manifold tutorial of ManifoldsBase.jl
.â
Building more advanced manifolds
Based on these basic manifolds we can directly build more advanced manifolds.
The first one concerns vectors or matrices of data on a manifold, the PowerManifold.
Mâ = Mâ^2
PowerManifold(Hyperbolic(2), 2)
Then points are represented by arrays, where the power manifold dimension is added in the end. In other words â for the hyperbolic manifold here, we have a matrix with 2 columns, where each column is a valid point on hyperbolic space.
p = [0 0; 0 1; 1 sqrt(2)]
3Ă2 Matrix{Float64}:
0.0 0.0
0.0 1.0
1.0 1.41421
[is_point(Mâ, p[:, 1]), is_point(Mâ, p[:, 2])]
2-element Vector{Bool}:
1
1
But of course the method we used previously also works for power manifolds:
is_point(Mâ, p)
true
Note that nested power manifolds are combined into one as in
Mââ = Mâ^4
PowerManifold(Hyperbolic(2), 2, 4)
which represents $2Ă4$ â matrices of hyperbolic points represented in $3Ă2Ă4$ arrays.
We can â alternatively â use a power manifold with nested arrays
Mâ
= PowerManifold(Mâ, NestedPowerRepresentation(), 2)
PowerManifold(Sphere(2, â), NestedPowerRepresentation(), 2)
which emphasizes that we have vectors of length 2 that contain points, so we store them that way.
pâ = [[0.0, 0.0, 1.0], [0.0, 1.0, 0.0]]
2-element Vector{Vector{Float64}}:
[0.0, 0.0, 1.0]
[0.0, 1.0, 0.0]
To unify both representations, elements of the power manifold can also be accessed in the classical indexing fashion, if we start with the corresponding manifold first. This way one can implement algorithms also independent of which representation is used.â
p[Mâ, 1]
3-element Vector{Float64}:
0.0
0.0
1.0
pâ[Mâ
, 2]
3-element Vector{Float64}:
0.0
1.0
0.0
Another constructor is the ProductManifold to combine different manifolds. Here of course the order matters. First we construct these using $Ă$
Mâ = Mâ Ă Mâ
ProductManifold with 2 submanifolds:
Hyperbolic(2)
Sphere(2, â)
Since now the representations might differ from element to element, we have to encapsulate these in their own type.
using RecursiveArrayTools: ArrayPartition
pâ = ArrayPartition([0, 0, 1], [0, 1, 0])
([0, 0, 1], [0, 1, 0])
Here ArrayPartition
taken from RecursiveArrayTools.jl
to store the point on the product manifold efficiently in one array, still allowing efficient access to the product elements.
is_point(Mâ, pâ; error=:error)
true
But accessing single components still works the same.â
pâ[Mâ, 1]
3-element Vector{Int64}:
0
0
1
Finally, also the TangentBundle
, the manifold collecting all tangent spaces on a manifold is available asâ
Mâ = TangentBundle(Mâ)
TangentBundle(Sphere(2, â))
Implementing generic Functions
In this section we take a look how to implement generic functions on manifolds.
For our example here, we want to implement the so-called BĂ©zier curve using the so-called de-Casteljau algorithm. The linked algorithm can easily be generalised to manifolds by replacing lines with geodesics. This was for example used in [BG18] and the following example is an extended version of an example from [ABBR23].
The algorithm works recursively. For the case that we have a BĂ©zier curve with just two points, the algorithm just evaluates the geodesic connecting both at some time point $tâ[0,1]$. The function to evaluate a shortest geodesic (it might not be unique, but then a deterministic choice is taken) between two points p
and q
on a manifold M
shortest_geodesic(M, p, q, t)
.
function de_Casteljau(M::AbstractManifold, t, pts::NTuple{2})
return shortest_geodesic(M, pts[1], pts[2], t)
end
de_Casteljau (generic function with 1 method)
function de_Casteljau(M::AbstractManifold, t, pts::NTuple)
p = de_Casteljau(M, t, pts[1:(end - 1)])
q = de_Casteljau(M, t, pts[2:end])
return shortest_geodesic(M, p, q, t)
end
de_Casteljau (generic function with 2 methods)
Which can now be used on any manifold where the shortest geodesic is implemented
Now on several manifolds the exponential map and its (locally defined) inverse, the logarithmic map might not be available in an implementation. So one way to generalise this, is the use of a retraction (see [AMS08], Def. 4.1.1 for details) and its (local) inverse.
The function itself is quite similar to the exponential map, just that retract(M, p, X, m)
has one further parameter, the type of retraction to take, so m
is a subtype of m
, the same for the inverse_retract(M, p, q, n)
with an AbstractInverseRetractionMethod
n
.
Thinking of a generic implementation, we would like to have a way to specify one, that is available. This can be done by using default_retraction_method
and default_inverse_retraction_method
, respectively. We implement
function generic_de_Casteljau(
M::AbstractManifold,
t,
pts::NTuple{2};
m::AbstractRetractionMethod=default_retraction_method(M),
n::AbstractInverseRetractionMethod=default_inverse_retraction_method(M),
)
X = inverse_retract(M, pts[1], pts[2], n)
return retract(M, pts[1], X, t, m)
end
generic_de_Casteljau (generic function with 1 method)
and for the recursion
function generic_de_Casteljau(
M::AbstractManifold,
t,
pts::NTuple;
m::AbstractRetractionMethod=default_retraction_method(M),
n::AbstractInverseRetractionMethod=default_inverse_retraction_method(M),
)
p = generic_de_Casteljau(M, t, pts[1:(end - 1)]; m=m, n=n)
q = generic_de_Casteljau(M, t, pts[2:end]; m=m, n=n)
X = inverse_retract(M, p, q, n)
return retract(M, p, X, t, m)
end
generic_de_Casteljau (generic function with 2 methods)
Note that on a manifold M
where the exponential map is implemented, the default_retraction_method(M)
returns ExponentialRetraction
, which yields that the retract
function falls back to calling exp
.
The same mechanism exists for parallel_transport_to(M, p, X, q)
and the more general vector_transport_to(M, p, X, q, m)
whose AbstractVectorTransportMethod
m
has a default defined by default_vector_transport_method(M)
.
Allocating and in-place computations
Memory allocation is a critical performance issue when programming in Julia. To take this into account, Manifolds.jl
provides special functions to reduce the amount of allocations.
We again look at the exponential map. On a manifold M
the exponential map needs a point p
(to start from) and a tangent vector X
, which can be seen as direction to âwalk intoâ as well as the length to walk into this direction. In Manifolds.jl
the function can then be called with q = exp(M, p, X)
(see exp(M, p, X)
). This function returns the resulting point q
, which requires to allocate new memory.
To avoid this allocation, the function exp!(M, q, p, X)
can be called. Here q
is allocated beforehand and is passed as the memory, where the result is returned in. It might be used even for interims computations, as long as it does not introduce side effects. Thas means that even with exp!(M, p, p, X)
the result is correct.
Letâs look at an example.
We take another look at the Sphere
, but now a high-dimensional one. We can also illustrate how to generate radnom points and tangent vectors.
M = Sphere(10000)
pâ = rand(M)
X = rand(M; vector_at=pâ)
Looking at the allocations required we get
@allocated exp(M, pâ, X)
11026976
While if we have already allocated memory for the resulting point on the manifold, for example
qâ = zero(pâ);
There are no new memory allocations necessary if we use the in-place function.â
@allocated exp!(M, qâ, pâ, X)
0
This methodology is used for all functions that compute a new point or tangent vector. By default all allocating functions allocate memory and call the in-place function. This also means that if you implement a new manifold, you just have to implement the in-place version.
Decorating a manifold
As you saw until now, an [AbstractManifold
]@extref ManifoldsBase.AbstractManifold
) describes a Riemannian manifold. For completeness, this also includes the chosen Riemannian metric tensor or inner product on the tangent spaces.
In Manifolds.jl
these are assumed to be a âreasonable defaultâ. For example on the Sphere
(n)
we used above, the default metric is the one inherited from restricting the inner product from the embedding space onto each tangent space.
Consider a manifold like
Mâ = SymmetricPositiveDefinite(3)
SymmetricPositiveDefinite(3)
which is the manifold of $3Ă3$ matrices that are symmetric and positive definite. which has a default as well, the affine invariant AffineInvariantMetric
, but also has several different metrics.
To switch the metric, we use the idea of a decorator pattern approach. Defining
Mââ = MetricManifold(Mâ, BuresWassersteinMetric())
MetricManifold(SymmetricPositiveDefinite(3), BuresWassersteinMetric())
changes the manifold to use the BuresWassersteinMetric
.
This changes all functions that depend on the metric, most prominently the Riemannian matric, but also the exponential and logarithmic map and hence also geodesics.
All functions that are not dependent on a metric â for example the manifold dimension, the tests of points and vectors we already looked at, but also all retractions â stay unchanged. This means that for example
[manifold_dimension(Mââ), manifold_dimension(Mâ)]
2-element Vector{Int64}:
6
6
both calls the same underlying function. On the other hand with
pâ
, Xâ
= one(zeros(3, 3)), [1.0 0.0 1.0; 0.0 1.0 0.0; 1.0 0.0 1.0]
([1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0], [1.0 0.0 1.0; 0.0 1.0 0.0; 1.0 0.0 1.0])
but for example the exponential map and the norm yield different results
[exp(Mâ, pâ
, Xâ
), exp(Mââ, pâ
, Xâ
)]
2-element Vector{Matrix{Float64}}:
[4.194528049465325 0.0 3.194528049465325; 0.0 2.718281828459045 0.0; 3.194528049465325 0.0 4.194528049465328]
[2.5 0.0 1.5; 0.0 2.25 0.0; 1.5 0.0 2.5]
[norm(Mâ, pâ
, Xâ
), norm(Mââ, pâ
, Xâ
)]
2-element Vector{Float64}:
2.23606797749979
1.118033988749895
Technically this done using Traits â the trait here is the IsMetricManifold
trait. Our trait system allows to combine traits but also to inherit properties in a hierarchical way, see here for the technical details.
The same approach is used for
- specifying a different connection
- specifying a manifold as a certain quotient manifold
- specifying a certain embeddings
- specify a certain group action
Again, for all of these, the concrete types only have to be used if you want to do a second, different from the details, property, for example a second way to embed a manifold. If a manifold is (in its usual representation) an embedded manifold, this works with the default manifold type already, since then it is again set as the reasonable default.
Literature
- [AMS08]
- P.-A. Absil, R. Mahony and R. Sepulchre. Optimization Algorithms on Matrix Manifolds (Princeton University Press, 2008), available online at press.princeton.edu/chapters/absil/.
- [ABBR23]
- S. D. Axen, M. Baran, R. Bergmann and K. Rzecki. Manifolds.Jl: An Extensible Julia Framework for Data Analysis on Manifolds. ACM Transactions on Mathematical Software 49 (2023).
- [BG18]
- R. Bergmann and P.-Y. Gousenbourger. A variational model for data fitting on manifolds by minimizing the acceleration of a Bézier curve. Frontiers in Applied Mathematics and Statistics 4 (2018), arXiv:1807.10090.