Skip to content

Commit 3a7cee3

Browse files
lkdvosclaude
andcommitted
fix: make exponential hermitian to the last bit for complex input
`MatrixFunctionViaEigh` built the real case as the symmetric product `VexpD * transpose(VexpD)`, exact by construction, but the complex case as a plain `VexpD * V'` with no closing projection, so the result was only approximately hermitian. `squareroot`, `logarithm` and `power` are all exact for both scalar types, leaving `exponential` the odd one out. Since `eigh_full!` returns real eigenvalues, `exp(τD/2)` is self-adjoint whenever `τ` is real, and `exp(τA) = (V exp(τD/2)) * (V exp(τD/2))'` is then hermitian by construction. Route the complex case through `_mul_herm!`, the same mechanism the other three use. This needed a second change to take effect for `exponential(A)` itself: the implicit scalar was `one(eltype(A))`, i.e. `1.0 + 0.0im` for a complex matrix, which sent a plain `exp(A)` down the complex-`τ` branch even though the scalar was real. Use `one(real(eltype(A)))` instead. The same edit for `MatrixFunctionViaEig` is behaviour-neutral -- its branch condition already excluded a complex `A` -- but avoids needless complex-scalar arithmetic. A genuinely complex `τ` is still left unprojected, which is not an oversight: `exp(τA)` of a hermitian `A` is hermitian only for real `τ`, and projecting would silently return a different matrix. Verified against `LinearAlgebra.exp(Hermitian(A))` for `Float32`, `Float64`, `ComplexF32` and `ComplexF64` with both `QRIteration` and `DivideAndConquer`, and for `Diagonal` with complex storage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c074ea7 commit 3a7cee3

2 files changed

Lines changed: 23 additions & 12 deletions

File tree

src/implementations/exponential.jl

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ function exponential!(A::AbstractMatrix, expA, alg::MatrixFunctionViaLA)
6262
return expA
6363
end
6464

65-
exponential!(A::AbstractMatrix, expA, alg::MatrixFunctionViaEigh) = exponential!((one(eltype(A)), A), expA, alg)
66-
exponential!(A::AbstractMatrix, expA, alg::MatrixFunctionViaEig) = exponential!((one(eltype(A)), A), expA, alg)
65+
# the implicit scalar is real even for a complex matrix, which keeps `exp(A)` of a hermitian `A`
66+
# on the branch that produces a hermitian result
67+
exponential!(A::AbstractMatrix, expA, alg::MatrixFunctionViaEigh) = exponential!((one(real(eltype(A))), A), expA, alg)
68+
exponential!(A::AbstractMatrix, expA, alg::MatrixFunctionViaEig) = exponential!((one(real(eltype(A))), A), expA, alg)
6769

6870
function exponential!((τ, A)::Tuple{Number, AbstractMatrix}, expA, alg::AbstractAlgorithm)
6971
expA .= A .* τ
@@ -74,18 +76,21 @@ function exponential!((τ, A)::Tuple{Number, AbstractMatrix}, expA, alg::MatrixF
7476
check_input(exponential!, (τ, A), expA, alg)
7577
D, V = eigh_full!(A, alg.eigh_alg)
7678
if eltype(A) <: Real
79+
# `exp(τA) = (V exp(τD/2)) * transpose(V exp(τD/2))` is symmetric by construction;
80+
# `transpose` rather than `'` keeps this valid for a complex `τ` as well
7781
if eltype(τ) <: Real
7882
VexpD = rmul!(V, exponential!((τ / 2, D), D))
7983
else
8084
VexpD = V * exponential((τ / 2, D))
8185
end
8286
return mul!(expA, VexpD, transpose(VexpD))
87+
elseif eltype(τ) <: Real
88+
# `D` is real, so `exp(τD/2)` is self-adjoint for a real `τ` and
89+
# `exp(τA) = (V exp(τD/2)) * (V exp(τD/2))'` is hermitian by construction
90+
return _mul_herm!(expA, rmul!(V, exponential!((τ / 2, D), D)))
8391
else
84-
if eltype(τ) <: Real
85-
VexpD = V * exponential!((τ, D), D)
86-
else
87-
VexpD = V * exponential((τ, D))
88-
end
92+
# a complex `τ` makes `exp(τA)` normal but not hermitian, so there is nothing to project
93+
VexpD = V * exponential((τ, D))
8994
return mul!(expA, VexpD, V')
9095
end
9196
end

test/testsuite/matrixfunctions/exponential.jl

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,22 @@ function test_exponential_hermitian(
8080
@test logarithm(expA, alg) A
8181
@test A == Ac
8282

83-
# `exponential` forms the real case as a symmetric product `VexpD * transpose(VexpD)`,
84-
# which is hermitian to the last bit, but the complex case as a plain `VexpD * V'` with
85-
# no closing projection, so there it is only approximately hermitian. This differs from
86-
# `squareroot`, `logarithm` and `power`, which are exact for both scalar types.
87-
if exact_hermiticity && eltype(T) <: Real
83+
# `exp(A)` is built as a product of a factor with its own adjoint (transpose, in the real
84+
# case), so it comes out hermitian to the last bit for both scalar types.
85+
if exact_hermiticity
8886
@test ishermitian(expA)
8987
test_spectrum && @test eigh_vals(expA) exp.(eigh_vals(A))
9088
else
9189
@test ishermitian(expA; rtol = precision(T))
9290
end
91+
92+
# the scaled entry point keeps that guarantee for a real `τ`, since `exp(τD/2)` is then
93+
# still self-adjoint. A complex `τ` makes `exp(τA)` normal but not hermitian, which
94+
# `test_exponential_scaled` covers, so only correctness is asserted there.
95+
τ = randn(rng, real(eltype(T)))
96+
expτA = @testinferred exponential((τ, A), alg)
97+
@test expτA exponential* A, alg)
98+
exact_hermiticity && @test ishermitian(expτA)
9399
end
94100
end
95101

0 commit comments

Comments
 (0)