Skip to content

Commit 8e5792b

Browse files
feat: exponential (#465)
* Exponential via MatrixAlgebraKit * include case where t is real and tau is complex * Add test * Fix formatting * Update correspondence with MAK and extend tests * Update src/factorizations/matrixalgebrakit.jl Co-authored-by: Lukas Devos <ldevos98@gmail.com> * deprecate `exp!` * Remove extra definitions that are now in MatrixAlgebraKit * bump MAK version * update implementations * remove double test * refactor exponential tests in the style of other tests * fix `copy_input` shouldn't alias * some more small fixes --------- Co-authored-by: Lukas Devos <ldevos98@gmail.com>
1 parent cfaa073 commit 8e5792b

7 files changed

Lines changed: 131 additions & 15 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ FiniteDifferences = "0.12"
5454
GPUArrays = "11.4.1"
5555
LRUCache = "1.6"
5656
LinearAlgebra = "1"
57-
MatrixAlgebraKit = "0.6.8"
57+
MatrixAlgebraKit = "0.6.9"
5858
Mooncake = "0.5.27"
5959
OhMyThreads = "0.8.0"
6060
Printf = "1"

docs/src/man/linearalgebra.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Note that, because the adjoint interchanges domain and codomain, we have `space(
4040

4141
When tensor map instances are endomorphisms, i.e. they have the same domain and codomain, there is a multiplicative identity which can be obtained as `one(t)` or `one!(t)`, where the latter overwrites the contents of `t`.
4242
The multiplicative identity on a space `V` can also be obtained using `id(A, V)` as discussed [above](@ref ss_tensor_construction), such that for a general homomorphism `t′`, we have `t′ == id(codomain(t′)) * t′ == t′ * id(domain(t′))`.
43-
Returning to the case of endomorphisms `t`, we can compute the trace via `tr(t)` and exponentiate them using `exp(t)`, or if the contents of `t` can be destroyed in the process, `exp!(t)`.
43+
Returning to the case of endomorphisms `t`, we can compute the trace via `tr(t)` and exponentiate them using `exp(t)` (or the more general `exponential(t)`), or if the contents of `t` can be destroyed in the process, `exponential!(t)`.
4444
Furthermore, there are a number of tensor factorizations for both endomorphisms and general homomorphisms that we discuss on the [Tensor factorizations](@ref ss_tensor_factorization) page.
4545

4646
Finally, there are a number of operations that also belong in this paragraph because of their analogy to common matrix operations.

src/TensorKit.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export left_orth, right_orth, left_null, right_null,
8282
qr_full, qr_compact, qr_null, lq_full, lq_compact, lq_null,
8383
qr_full!, qr_compact!, qr_null!, lq_full!, lq_compact!, lq_null!,
8484
svd_compact!, svd_full!, svd_trunc!, svd_compact, svd_full, svd_trunc, svd_vals, svd_vals!,
85-
exp, exp!,
85+
exp, exp!, exponential, exponential!,
8686
eigh_full!, eigh_full, eigh_trunc!, eigh_trunc, eigh_vals!, eigh_vals,
8787
eig_full!, eig_full, eig_trunc!, eig_trunc, eig_vals!, eig_vals,
8888
eigen, eigen!,

src/factorizations/matrixalgebrakit.jl

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ for f in
88
:eig_full, :eig_vals, :eigh_full, :eigh_vals,
99
:left_polar, :right_polar,
1010
:project_hermitian, :project_antihermitian, :project_isometric,
11+
:exponential,
1112
]
1213
f! = Symbol(f, :!)
1314
@eval function MAK.default_algorithm(::typeof($f!), ::Type{T}; kwargs...) where {T <: AbstractTensorMap}
@@ -18,6 +19,11 @@ for f in
1819
end
1920
end
2021

22+
MAK.default_algorithm(::typeof(exponential!), ::Type{Tuple{E, T}}; kwargs...) where {E <: Number, T <: AbstractTensorMap} =
23+
MAK.default_algorithm(exponential!, blocktype(T); kwargs...)
24+
MAK.copy_input(::typeof(exponential), (τ, t)::Tuple{E, T}) where {E <: Number, T <: AbstractTensorMap} =
25+
(τ, copy_oftype(t, (E <: Complex ? complex : identity)(factorisation_scalartype(exponential, t))))
26+
2127
_select_truncation(f, ::AbstractTensorMap, trunc::TruncationStrategy) = trunc
2228
function _select_truncation(::typeof(left_null!), ::AbstractTensorMap, trunc::NamedTuple)
2329
return MAK.null_truncation_strategy(; trunc...)
@@ -49,9 +55,10 @@ for f! in (
4955
:qr_null!, :lq_null!,
5056
:svd_vals!, :eig_vals!, :eigh_vals!,
5157
:project_hermitian!, :project_antihermitian!, :project_isometric!,
58+
:exponential!,
5259
)
5360
@eval function MAK.$f!(t::AbstractTensorMap, N, alg::AbstractAlgorithm)
54-
$(f! in (:eig_vals!, :eigh_vals!, :project_hermitian!, :project_antihermitian!) && :(LinearAlgebra.checksquare(t)))
61+
$(f! in (:eig_vals!, :eigh_vals!, :project_hermitian!, :project_antihermitian!, :exponential!) && :(LinearAlgebra.checksquare(t)))
5562
foreachblock(t, N) do _, (tblock, Nblock)
5663
Nblock′ = $f!(tblock, Nblock, alg)
5764
# deal with the case where the output is not the same as the input
@@ -62,6 +69,19 @@ for f! in (
6269
end
6370
end
6471

72+
# Exponential with Tuple
73+
function MAK.exponential!((τ, t)::Tuple{E, T}, N, alg::AbstractAlgorithm) where {E <: Number, T <: AbstractTensorMap}
74+
LinearAlgebra.checksquare(t)
75+
foreachblock(t, N) do _, (tblock, Nblock)
76+
Nblock′ = exponential!((τ, tblock), Nblock, alg)
77+
# deal with the case where the output is not the same as the input
78+
Nblock === Nblock′ || copy!(Nblock, Nblock′)
79+
return nothing
80+
end
81+
return N
82+
end
83+
84+
6585
MAK.zero!(t::AbstractTensorMap) = zerovector!(t)
6686

6787
# Default algorithm
@@ -76,6 +96,7 @@ for f in [
7696
:left_polar, :right_polar,
7797
:left_orth, :right_orth, :left_null, :right_null,
7898
:project_hermitian, :project_antihermitian, :project_isometric,
99+
:exponential,
79100
]
80101
f! = Symbol(f, :!)
81102
@eval MAK.$f!(t::AbstractTensorMap, alg::DefaultAlgorithm) =
@@ -95,6 +116,12 @@ for f in [
95116
MAK.$f!(t, out, MAK.select_algorithm(MAK.$f!, t, nothing; alg.kwargs...))
96117
end
97118

119+
# resolve `DefaultAlgorithm` for the tuple form at the tensor level, mirroring the loop below
120+
MAK.exponential!((τ, t)::Tuple{E, T}, alg::DefaultAlgorithm) where {E <: Number, T <: AbstractTensorMap} =
121+
MAK.exponential!((τ, t), MAK.select_algorithm(exponential!, t, nothing; alg.kwargs...))
122+
MAK.exponential!((τ, t)::Tuple{E, T}, out, alg::DefaultAlgorithm) where {E <: Number, T <: AbstractTensorMap} =
123+
MAK.exponential!((τ, t), out, MAK.select_algorithm(exponential!, t, nothing; alg.kwargs...))
124+
98125

99126
# Singular value decomposition
100127
# ----------------------------
@@ -221,3 +248,9 @@ MAK.initialize_output(::typeof(project_antihermitian!), tsrc::AbstractTensorMap,
221248
tsrc
222249
MAK.initialize_output(::typeof(project_isometric!), tsrc::AbstractTensorMap, ::AbstractAlgorithm) =
223250
similar(tsrc)
251+
252+
# Exponential
253+
# ----------------
254+
MAK.initialize_output(::typeof(exponential!), t::AbstractTensorMap, ::AbstractAlgorithm) = t
255+
MAK.initialize_output(::typeof(exponential!), (τ, t)::Tuple{Number, AbstractTensorMap}, ::AbstractAlgorithm) = t
256+
MAK.initialize_output(::typeof(exponential!), (τ, t)::Tuple{T1, AbstractTensorMap{T2}}, ::AbstractAlgorithm) where {T1 <: Complex, T2 <: Real} = similar(t, complex(eltype(t)))

src/factorizations/utility.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ end
55
factorisation_scalartype(f, t) = factorisation_scalartype(t)
66

77
function copy_oftype(t::AbstractTensorMap, T::Type{<:Number})
8-
return copy!(similar(t, T, space(t)), t)
8+
return copy!(similar(t, T), t)
99
end
1010

1111
function _reverse!(t::AbstractTensorMap; dims = :)

src/tensors/linalg.jl

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function compose(A::AbstractTensorMap, B::AbstractTensorMap)
4141
end
4242
Base.:*(t1::AbstractTensorMap, t2::AbstractTensorMap) = compose(t1, t2)
4343

44-
Base.exp(t::AbstractTensorMap) = exp!(copy(t))
44+
Base.exp(t::AbstractTensorMap) = exponential(t)
4545
function Base.:^(t::AbstractTensorMap, p::Integer)
4646
return p < 0 ? Base.power_by_squaring(inv(t), -p) : Base.power_by_squaring(t, p)
4747
end
@@ -416,15 +416,7 @@ function Base.:(/)(t1::AbstractTensorMap, t2::AbstractTensorMap)
416416
return t
417417
end
418418

419-
# TensorMap exponentation:
420-
function exp!(t::TensorMap)
421-
domain(t) == codomain(t) ||
422-
error("Exponential of a tensor only exist when domain == codomain.")
423-
for (c, b) in blocks(t)
424-
copy!(b, LinearAlgebra.exp!(b))
425-
end
426-
return t
427-
end
419+
@deprecate exp!(t) exponential!(t)
428420

429421
# Sylvester equation with TensorMap objects:
430422
function LinearAlgebra.sylvester(A::AbstractTensorMap, B::AbstractTensorMap, C::AbstractTensorMap)

test/tensors/exponential.jl

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using Test, TestExtras
2+
using TensorKit
3+
using MatrixAlgebraKit: DefaultAlgorithm, MatrixFunctionViaLA, MatrixFunctionViaEig,
4+
MatrixFunctionViaEigh, MatrixFunctionViaTaylor
5+
using Random
6+
7+
spacelist = default_spacelist(fast_tests)
8+
scalartypes = (Float32, Float64, ComplexF64)
9+
10+
# algorithms that agree on Hermitian input
11+
hermitian_algs = (
12+
MatrixFunctionViaLA(), MatrixFunctionViaEig(DefaultAlgorithm()),
13+
MatrixFunctionViaEigh(DefaultAlgorithm()), MatrixFunctionViaTaylor(),
14+
)
15+
# algorithms valid for general (non-Hermitian) input
16+
general_algs = (
17+
MatrixFunctionViaLA(), MatrixFunctionViaEig(DefaultAlgorithm()),
18+
MatrixFunctionViaTaylor(),
19+
)
20+
21+
for V in spacelist
22+
I = sectortype(first(V))
23+
Istr = TensorKit.type_repr(I)
24+
println("---------------------------------------")
25+
println("Matrix exponentials with symmetry: $Istr")
26+
println("---------------------------------------")
27+
@timedtestset "Matrix exponentials with symmetry: $Istr" verbose = true begin
28+
V1, V2, V3, V4, V5 = V
29+
W = V1 V2 V3
30+
Vd = fuse(V1 V2)
31+
32+
# explicit (non-diagonal) algorithms only apply to dense endomorphisms;
33+
# `DiagonalTensorMap` inputs are exercised through the default algorithm below
34+
@testset "exponential for Hermitian matrices" begin
35+
for T1 in scalartypes, T2 in scalartypes,
36+
A in (randn(T1, V1, V1), randn(T1, W, W))
37+
38+
A = project_hermitian!(A)
39+
τ = rand(T2)
40+
41+
expA = @constinferred exponential(A)
42+
expτA = @constinferred exponential((τ, A))
43+
44+
for alg in hermitian_algs
45+
@test expA @constinferred exponential(A, alg)
46+
@test expτA @constinferred exponential((τ, A), alg)
47+
end
48+
end
49+
end
50+
51+
@testset "exponential! for general matrices" begin
52+
for T1 in scalartypes, T2 in scalartypes,
53+
A in (
54+
randn(T1, V1, V1), randn(T1, W, W),
55+
DiagonalTensorMap(randn(T1, reduceddim(Vd)), Vd),
56+
)
57+
58+
τ = rand(T2)
59+
60+
expA = @constinferred exponential(A)
61+
if !(A isa DiagonalTensorMap) # diagonal only supports the default algorithm == DiagonalAlgorithm
62+
for alg in general_algs
63+
@test expA exponential(A, alg)
64+
end
65+
end
66+
67+
@test exponential!(copy(A)) exponential!((1.0, copy(A)))
68+
69+
# exp(A)² == exp(2A)
70+
@test expA * expA exponential((2, A))
71+
72+
# in-place semantics: aliases the input, unless a complex scalar forces
73+
# a real tensor to widen to complex
74+
Ain = copy(A)
75+
A2 = @constinferred exponential!((τ, Ain))
76+
A isa DiagonalTensorMap && @test A2 isa DiagonalTensorMap
77+
if T1 <: Real && T2 <: Complex
78+
@test A2 !== Ain
79+
else
80+
@test A2 === Ain
81+
end
82+
83+
# exp(τA) and exp(-τA) are inverse
84+
# the inverse roundtrip is ill-conditioned; only assert at full (Float64) precision
85+
expτA = exponential!((τ, copy(A)))
86+
expmτA = exponential!((-τ, copy(A)))
87+
real(scalartype(expτA)) == Float64 && @test expτA * expmτA id(scalartype(expτA), domain(A))
88+
end
89+
end
90+
end
91+
end

0 commit comments

Comments
 (0)