Skip to content

Commit 0a88534

Browse files
committed
atlas on the Grassmann manifold
1 parent e30b26a commit 0a88534

4 files changed

Lines changed: 304 additions & 1 deletion

File tree

src/Manifolds.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,7 @@ export AbstractMetric,
736736
RiemannianMetric,
737737
StiefelSubmersionMetric,
738738
WarpedMetric
739-
export AbstractAtlas, RetractionAtlas
739+
export AbstractAtlas, GrassmannAtlas, RetractionAtlas
740740
# Vector transport types
741741
export AbstractVectorTransportMethod, ParallelTransport, ProjectionTransport
742742
# Retraction types

src/manifolds/Grassmann.jl

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,26 @@ function Grassmann(n::Int, k::Int, field::AbstractNumbers = ℝ; parameter::Symb
8282
return Grassmann{field, typeof(size)}(size)
8383
end
8484

85+
@doc raw"""
86+
GrassmannAtlas()
87+
88+
The standard atlas of the real Grassmann manifold ``\mathrm{Gr}(n,k)``. Its
89+
``\binom{n}{k}`` charts are indexed by ordered tuples `i` of `k` row indices.
90+
The chart indexed by `i` contains the subspaces whose corresponding `k`-by-`k`
91+
minor in the rows `i` is invertible.
92+
93+
For a Stiefel representative `p`, let `pᵢ` denote this minor and let `j` be
94+
the complementary row indices. The coordinate map is
95+
96+
````math
97+
\varphi_i([p]) = \operatorname{vec}\left((p p_i^{-1})_j\right).
98+
````
99+
100+
Its inverse inserts the reshaped coordinates into the rows `j`, inserts the
101+
identity into the rows `i`, and orthonormalizes the resulting matrix.
102+
"""
103+
struct GrassmannAtlas <: AbstractAtlas{ℝ} end
104+
85105
function allocation_promotion_function(::Grassmann{ℂ}, f, args::Tuple)
86106
return complex
87107
end
@@ -239,3 +259,100 @@ Return the default vector transport method for the [`Grassmann`](@ref) manifold,
239259
which is `ParallelTransport``()`.
240260
"""
241261
default_vector_transport_method(::Grassmann) = ParallelTransport()
262+
263+
function _grassmann_chart_rows(M::Grassmann, i::AbstractVector)
264+
n, k = get_parameter(M.size)
265+
rows = collect(i)
266+
if length(rows) != k || !all(row -> 1 <= row <= n, rows) || length(unique(rows)) != k
267+
throw(ArgumentError("A Grassmann chart index must contain $k distinct row indices in 1:$n."))
268+
end
269+
return rows
270+
end
271+
272+
function _grassmann_chart_complement(M::Grassmann, i::AbstractVector)
273+
n, _ = get_parameter(M.size)
274+
return setdiff(collect(1:n), _grassmann_chart_rows(M, i))
275+
end
276+
277+
@doc raw"""
278+
inner(M::Grassmann, A::GrassmannAtlas, i::AbstractVector, a, Xc, Yc)
279+
280+
Compute the Riemannian inner product of coordinate vectors `Xc` and `Yc` at
281+
coordinates `a` in the chart `i` of the standard [`GrassmannAtlas`](@ref).
282+
Writing `Z = reshape(a, n-k, k)`, `U = reshape(Xc, n-k, k)`,
283+
`V = reshape(Yc, n-k, k)`, and `G = I + ZᵀZ`, the formula is
284+
285+
````math
286+
g_Z(U,V) = \operatorname{tr}\left(G^{-1} U^\mathsf{T}
287+
\left(I - ZG^{-1}Z^\mathsf{T}\right)V\right).
288+
````
289+
"""
290+
function inner(M::Grassmann{ℝ}, ::GrassmannAtlas, i::AbstractVector, a, Xc, Yc)
291+
_grassmann_chart_rows(M, i)
292+
n, k = get_parameter(M.size)
293+
coordinate_dimension = k * (n - k)
294+
length(a) == coordinate_dimension || throw(DimensionMismatch("Expected $coordinate_dimension chart coordinates."))
295+
length(Xc) == coordinate_dimension || throw(DimensionMismatch("Expected $coordinate_dimension vector coordinates."))
296+
length(Yc) == coordinate_dimension || throw(DimensionMismatch("Expected $coordinate_dimension vector coordinates."))
297+
Z = reshape(a, n - k, k)
298+
U = reshape(Xc, n - k, k)
299+
V = reshape(Yc, n - k, k)
300+
G = I + transpose(Z) * Z
301+
return dot(U / G, V - Z * (G \ (transpose(Z) * V)))
302+
end
303+
304+
@doc raw"""
305+
affine_connection!(M::Grassmann, Zc, A::GrassmannAtlas, i, a, Xc, Yc)
306+
307+
Store the Levi-Civita covariant derivative of `Yc` in direction `Xc` in
308+
`Zc`, using the standard [`GrassmannAtlas`](@ref). Writing
309+
`Z = reshape(a, n-k, k)`, `U = reshape(Xc, n-k, k)`,
310+
`V = reshape(Yc, n-k, k)`, and `G = I + ZᵀZ`, the coordinate expression is
311+
312+
````math
313+
\nabla_U V = -UG^{-1}Z^\mathsf{T}V - VG^{-1}Z^\mathsf{T}U.
314+
````
315+
"""
316+
function affine_connection!(
317+
M::Grassmann{ℝ},
318+
Zc,
319+
::GrassmannAtlas,
320+
i::AbstractVector,
321+
a,
322+
Xc,
323+
Yc,
324+
)
325+
_grassmann_chart_rows(M, i)
326+
n, k = get_parameter(M.size)
327+
coordinate_dimension = k * (n - k)
328+
length(a) == coordinate_dimension || throw(DimensionMismatch("Expected $coordinate_dimension chart coordinates."))
329+
length(Xc) == coordinate_dimension || throw(DimensionMismatch("Expected $coordinate_dimension vector coordinates."))
330+
length(Yc) == coordinate_dimension || throw(DimensionMismatch("Expected $coordinate_dimension vector coordinates."))
331+
length(Zc) == coordinate_dimension || throw(DimensionMismatch("Expected $coordinate_dimension output coordinates."))
332+
Z = reshape(a, n - k, k)
333+
U = reshape(Xc, n - k, k)
334+
V = reshape(Yc, n - k, k)
335+
G = I + transpose(Z) * Z
336+
Zc .= vec(-(U * (G \ (transpose(Z) * V)) + V * (G \ (transpose(Z) * U))))
337+
return Zc
338+
end
339+
340+
@doc raw"""
341+
det_local_metric(M::Grassmann, A::GrassmannAtlas, i::AbstractVector, a)
342+
343+
Return the determinant of the local metric in the standard
344+
[`GrassmannAtlas`](@ref) at coordinates `a` in chart `i`.
345+
"""
346+
function det_local_metric(M::Grassmann{ℝ}, A::GrassmannAtlas, i::AbstractVector, a)
347+
return det(local_metric(M, A, i, a))
348+
end
349+
350+
@doc raw"""
351+
inverse_chart_injectivity_radius(M::Grassmann, A::GrassmannAtlas, i)
352+
353+
Return the injectivity radius of an affine chart in the standard
354+
[`GrassmannAtlas`](@ref), which is infinite.
355+
"""
356+
function inverse_chart_injectivity_radius(M::Grassmann{ℝ}, ::GrassmannAtlas, i::AbstractVector)
357+
return Inf
358+
end

src/manifolds/GrassmannStiefel.jl

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,126 @@ which is given by a zero matrix the same size as `p`.
518518
zero_vector(::Grassmann, ::Any...)
519519

520520
zero_vector!(::Grassmann, X, p) = fill!(X, 0)
521+
522+
# GrassmannAtlas
523+
524+
function _grassmann_largest_minor_rows(p, k)
525+
n = size(p, 1)
526+
rows = collect(1:k)
527+
best_rows = copy(rows)
528+
best_minor = zero(real(eltype(p)))
529+
while true
530+
minor = abs(det(p[rows, :]))
531+
if minor > best_minor
532+
best_minor = minor
533+
best_rows .= rows
534+
end
535+
536+
position = k
537+
while position >= 1 && rows[position] == n - k + position
538+
position -= 1
539+
end
540+
position == 0 && break
541+
rows[position] += 1
542+
for next_position in (position + 1):k
543+
rows[next_position] = rows[next_position - 1] + 1
544+
end
545+
end
546+
iszero(best_minor) && throw(DomainError(p, "The point has no invertible $k-by-$k row minor."))
547+
return best_rows
548+
end
549+
550+
@doc raw"""
551+
get_chart_index(M::Grassmann, A::GrassmannAtlas, p)
552+
get_chart_index(M::Grassmann, A::GrassmannAtlas, i, a)
553+
554+
Return a chart index suitable for a point `p` or coordinates `a` in chart `i`.
555+
For a point, the index of the row minor with the largest absolute determinant is
556+
returned, breaking ties lexicographically. For coordinates, the represented
557+
point is reconstructed and the same selection rule is applied.
558+
"""
559+
function get_chart_index(M::Grassmann{ℝ}, ::GrassmannAtlas, p)
560+
_, k = get_parameter(M.size)
561+
return _grassmann_largest_minor_rows(p, k)
562+
end
563+
function get_chart_index(M::Grassmann{ℝ}, A::GrassmannAtlas, i::AbstractVector, a)
564+
return get_chart_index(M, A, get_point(M, A, i, a))
565+
end
566+
567+
@doc raw"""
568+
get_parameters!(M::Grassmann, a, A::GrassmannAtlas, i, p)
569+
570+
Store the standard affine coordinates of `p` in `a` for the chart indexed by
571+
the row tuple `i`. The selected row minor of `p` must be invertible.
572+
"""
573+
function get_parameters!(M::Grassmann{ℝ}, a, ::GrassmannAtlas, i::AbstractVector, p)
574+
rows = _grassmann_chart_rows(M, i)
575+
complement = _grassmann_chart_complement(M, i)
576+
p_rows = p[rows, :]
577+
iszero(det(p_rows)) && throw(DomainError(p, "The point does not belong to chart $i."))
578+
a .= vec((p / p_rows)[complement, :])
579+
return a
580+
end
581+
582+
@doc raw"""
583+
get_point!(M::Grassmann, p, A::GrassmannAtlas, i::AbstractVector, a)
584+
585+
Store in `p` the point represented by affine coordinates `a` in the chart of
586+
the standard [`GrassmannAtlas`](@ref) indexed by `i`.
587+
"""
588+
function get_point!(M::Grassmann{ℝ}, p, ::GrassmannAtlas, i::AbstractVector, a)
589+
rows = _grassmann_chart_rows(M, i)
590+
complement = _grassmann_chart_complement(M, i)
591+
n, k = get_parameter(M.size)
592+
length(a) == k * (n - k) || throw(DimensionMismatch("Expected $(k * (n - k)) chart coordinates."))
593+
fill!(p, zero(eltype(p)))
594+
p[rows, :] .= Matrix{eltype(p)}(I, k, k)
595+
p[complement, :] .= reshape(a, n - k, k)
596+
return project!(M, p, p)
597+
end
598+
599+
@doc raw"""
600+
get_coordinates_induced_basis!(M::Grassmann, c, p, X, B::InducedBasis{<:Any, <:Any, <:GrassmannAtlas})
601+
602+
Store in `c` the coordinates of a tangent vector `X` at `p` with respect to
603+
the basis induced by the standard [`GrassmannAtlas`](@ref).
604+
"""
605+
function get_coordinates_induced_basis!(
606+
M::Grassmann{ℝ},
607+
c,
608+
p,
609+
X,
610+
B::InducedBasis{ℝ, TangentSpaceType, <:GrassmannAtlas},
611+
)
612+
rows = _grassmann_chart_rows(M, B.i)
613+
complement = _grassmann_chart_complement(M, B.i)
614+
p_rows = p[rows, :]
615+
normalized_p = p / p_rows
616+
normalized_X = X / p_rows
617+
c .= vec(normalized_X[complement, :] .- normalized_p[complement, :] * normalized_X[rows, :])
618+
return c
619+
end
620+
621+
@doc raw"""
622+
get_vector_induced_basis!(M::Grassmann, X, p, c, B::InducedBasis{ℝ, TangentSpaceType, <:GrassmannAtlas})
623+
624+
Store in `X` the tangent vector at `p` represented by coordinates `c` with
625+
respect to the basis induced by the standard [`GrassmannAtlas`](@ref).
626+
"""
627+
function get_vector_induced_basis!(
628+
M::Grassmann{ℝ},
629+
X,
630+
p,
631+
c,
632+
B::InducedBasis{ℝ, TangentSpaceType, <:GrassmannAtlas},
633+
)
634+
rows = _grassmann_chart_rows(M, B.i)
635+
complement = _grassmann_chart_complement(M, B.i)
636+
n, k = get_parameter(M.size)
637+
length(c) == k * (n - k) || throw(DimensionMismatch("Expected $(k * (n - k)) basis coordinates."))
638+
fill!(X, zero(eltype(X)))
639+
X[complement, :] .= reshape(c, n - k, k)
640+
X .= X * p[rows, :]
641+
project!(M, X, p, X)
642+
return X
643+
end

test/manifolds-old/grassmann.jl

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
include("../header.jl")
2+
using DiffEqCallbacks, OrdinaryDiffEq
3+
using ForwardDiff
24

35
@testset "Grassmann" begin
46
@testset "Real" begin
@@ -409,4 +411,65 @@ include("../header.jl")
409411
@test get_embedding(M, typeof(p2)) == Euclidean(3, 3; parameter = :field)
410412
@test get_total_space(M) == Stiefel(3, 2; parameter = :field)
411413
end
414+
415+
@testset "GrassmannAtlas" begin
416+
M = Grassmann(4, 2)
417+
A = GrassmannAtlas()
418+
charts = ([1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4])
419+
a = [0.1, -0.2, 0.3, -0.4]
420+
c = [0.2, -0.1, 0.4, 0.3]
421+
d = [-0.3, 0.5, 0.1, -0.2]
422+
423+
@test Manifolds.get_chart_index(M, A, [1, 2], 3 * a) == [1, 4]
424+
for i in charts
425+
@testset "chart $i" begin
426+
p = get_point(M, A, i, a)
427+
@test is_point(M, p; error = :error)
428+
@test get_parameters(M, A, i, p) a
429+
@test Manifolds.get_chart_index(M, A, i, a) ==
430+
Manifolds.get_chart_index(M, A, p)
431+
@test Manifolds.inverse_chart_injectivity_radius(M, A, i) == Inf
432+
433+
selected_i = Manifolds.get_chart_index(M, A, p)
434+
@test selected_i in charts
435+
selected_a = get_parameters(M, A, selected_i, p)
436+
@test isapprox(M, p, get_point(M, A, selected_i, selected_a))
437+
438+
B = induced_basis(M, A, i)
439+
X = get_vector(M, p, c, B)
440+
Y = get_vector(M, p, d, B)
441+
@test is_vector(M, p, X; error = :error, atol = 1.0e-14)
442+
@test get_coordinates(M, p, X, B) c
443+
@test Manifolds.inner(M, A, i, a, c, c) Manifolds.inner(M, p, X, X)
444+
@test Manifolds.inner(M, A, i, a, c, d) Manifolds.inner(M, p, X, Y)
445+
@test Manifolds.det_local_metric(M, A, i, a) > 0
446+
447+
# TODO: check against the embedding-based implementation of the Levi-Civita connection
448+
Zc = affine_connection(M, A, i, a, c, d)
449+
@test Zc Manifolds.levi_civita_affine_connection(M, A, i, a, c, d)
450+
affine_connection!(M, Zc, A, i, a, c, d)
451+
@test Zc Manifolds.levi_civita_affine_connection(M, A, i, a, c, d)
452+
end
453+
end
454+
455+
@test_throws DomainError get_parameters(M, A, [1, 2], [0.0 0.0; 0.0 0.0; 1.0 0.0; 0.0 1.0])
456+
@test_throws ArgumentError get_point(M, A, [1, 1], a)
457+
458+
@testset "chart integration" begin
459+
i = [1, 2]
460+
Xc = [0.04, -0.03, 0.02, 0.01]
461+
Yc = [-0.02, 0.03, 0.01, -0.04]
462+
p = get_point(M, A, i, a)
463+
B = induced_basis(M, A, i)
464+
X = get_vector(M, p, Xc, B)
465+
Y = get_vector(M, p, Yc, B)
466+
q = exp(M, p, X)
467+
468+
exp_solution = Manifolds.solve_chart_exp_ode(
469+
M, a, Xc, A, i; final_time = 1.0, abstol = 1.0e-10, reltol = 1.0e-10,
470+
)
471+
q_chart, X_chart = exp_solution(1.0)
472+
@test isapprox(M, q_chart, q; atol = 1.0e-8)
473+
end
474+
end
412475
end

0 commit comments

Comments
 (0)