Skip to content

Commit 5ab2c78

Browse files
committed
refactor: reuse existing code patterns in matrix-function kernels
Move `_mul_herm!` from `implementations/polar.jl` to `implementations/projections.jl` and use it for the symmetric-product kernels of `squareroot` and fractional `power` via `MatrixFunctionViaEigh`, so hermitian output is guaranteed through the same mechanism as the polar decomposition (BLAS `herk`/`syrk`, explicit projection otherwise, GPU overrides). Rewrite the `MatrixFunctionViaLA` kernels in the style of the `exponential` implementation: an explicit eltype branch with a `real.(...)` assignment instead of the bespoke `_copy_result!` helper. `squareroot`/`logarithm` use a strict complex-result check (LinearAlgebra returns real results whenever the principal value is real), while `power` tolerates rounding-level imaginary components since fractional powers are computed in complex arithmetic upstream.
1 parent 97fc243 commit 5ab2c78

6 files changed

Lines changed: 51 additions & 50 deletions

File tree

src/implementations/logarithm.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@ initialize_output(::typeof(logarithm!), A::AbstractMatrix, ::AbstractAlgorithm)
3535
function logarithm!(A::AbstractMatrix, logA, alg::MatrixFunctionViaLA)
3636
check_input(logarithm!, A, logA, alg)
3737
isempty(alg.kwargs) || throw(ArgumentError("`MatrixFunctionViaLA` does not accept keyword arguments for `logarithm`"))
38-
result = LinearAlgebra.log(A)
39-
_copy_result!(logarithm!, logA, result)
38+
# `LinearAlgebra.log` of a real matrix is real whenever the principal logarithm is,
39+
# so a complex result with a real output signals a genuine domain violation
40+
logAc = LinearAlgebra.log(A)
41+
if eltype(logAc) <: Complex && !(eltype(logA) <: Complex)
42+
throw(_realness_domainerror(logarithm!))
43+
end
44+
copy!(logA, logAc)
4045
return logA
4146
end
4247

src/implementations/matrixfunctions.jl

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -61,31 +61,12 @@ function _check_nonzero_eigenvalues(λ, atol::Real)
6161
end
6262

6363
# For `MatrixFunctionViaLA`, domain violations surface as a complex result from
64-
# `LinearAlgebra`; detect this after the fact to preserve type-stable output.
65-
# `LinearAlgebra` sometimes computes a real result in complex arithmetic (e.g. fractional
66-
# powers via a complex Schur decomposition), so rounding-level imaginary components do
67-
# not signal a domain violation and are simply discarded.
68-
function _copy_result!(f, out::AbstractMatrix, result::AbstractMatrix)
69-
if eltype(result) <: Complex && !(eltype(out) <: Complex)
70-
# base the tolerance on the working precision, which is the lower of the two:
71-
# `LinearAlgebra` may internally promote, e.g. `Float32` fractional powers are
72-
# computed with `Float64`-eltype results but `Float32`-level accuracy
73-
atol = max(defaulttol(out), defaulttol(result)) * norm(result, Inf)
74-
for x in result
75-
if abs(imag(x)) > atol
76-
throw(
77-
DomainError(
78-
f,
79-
"The result of this matrix function applied to the given real matrix is complex (eigenvalues on the negative real axis). " *
80-
"Pass a complex matrix to obtain the principal value, or use `MatrixFunctionViaEigh`/`MatrixFunctionViaEig` with a suitable " *
81-
"`domain_atol` if the offending eigenvalues are rounding artifacts."
82-
)
83-
)
84-
end
85-
end
86-
out .= real.(result)
87-
else
88-
copy!(out, result)
89-
end
90-
return out
64+
# `LinearAlgebra` while the output should remain real.
65+
function _realness_domainerror(f)
66+
return DomainError(
67+
f,
68+
"The result of this matrix function applied to the given real matrix is complex (eigenvalues on the negative real axis). " *
69+
"Pass a complex matrix to obtain the principal value, or use `MatrixFunctionViaEigh`/`MatrixFunctionViaEig` with a suitable " *
70+
"`domain_atol` if the offending eigenvalues are rounding artifacts."
71+
)
9172
end

src/implementations/polar.jl

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,19 +81,6 @@ function right_polar!(A::AbstractMatrix, PWᴴ, alg::PolarViaSVD)
8181
return (P, Wᴴ)
8282
end
8383

84-
# Implement `mul!(C, A', A)` and guarantee the result is hermitian.
85-
# For BLAS calls that dispatch to `syrk` or `herk` this works automatically
86-
# for GPU this currently does not seem to be guaranteed so we manually project
87-
function _mul_herm!(C, A)
88-
mul!(C, A, A')
89-
project_hermitian!(C)
90-
return C
91-
end
92-
function _mul_herm!(C::YALAPACK.BlasMat{T}, A::YALAPACK.BlasMat{T}) where {T <: YALAPACK.BlasFloat}
93-
mul!(C, A, A')
94-
return C
95-
end
96-
9784
# Implementation via Newton
9885
# --------------------------
9986
function left_polar!(A::AbstractMatrix, WP, alg::PolarNewton)

src/implementations/power.jl

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,19 @@ initialize_output(::typeof(power!), A::AbstractMatrix, p::Real, ::AbstractAlgori
3535
function power!(A::AbstractMatrix, p::Real, powA, alg::MatrixFunctionViaLA)
3636
check_input(power!, A, p, powA, alg)
3737
isempty(alg.kwargs) || throw(ArgumentError("`MatrixFunctionViaLA` does not accept keyword arguments for `power`"))
38-
result = A^p
39-
_copy_result!(power!, powA, result)
38+
powAc = A^p
39+
if eltype(powAc) <: Complex && !(eltype(powA) <: Complex)
40+
# `LinearAlgebra` computes fractional powers of real matrices in complex
41+
# arithmetic and only casts back to real when the result is exactly real,
42+
# so rounding-level imaginary components do not signal a domain violation.
43+
# The tolerance is based on the working precision, which may be lower than
44+
# the result eltype suggests (e.g. `Float32` input promotes to `ComplexF64`).
45+
atol = defaulttol(powA) * norm(powAc, Inf)
46+
all(x -> abs(imag(x)) <= atol, powAc) || throw(_realness_domainerror(power!))
47+
powA .= real.(powAc)
48+
return powA
49+
end
50+
copy!(powA, powAc)
4051
return powA
4152
end
4253

@@ -49,16 +60,16 @@ function power!(A::AbstractMatrix, p::Real, powA, alg::MatrixFunctionViaEigh)
4960
λ .= λ .^ p
5061
VD = V * D
5162
mul!(powA, VD, V')
63+
return project_hermitian!(powA)
5264
else
5365
atol = something(alg.domain_atol, default_domain_atol(λ))
5466
p < 0 && _check_nonzero_eigenvalues(λ, atol)
5567
_clamp_domain_eigenvalues!(λ, atol)
5668
# `A^p = (V * D^(p/2)) * (V * D^(p/2))'` is hermitian by construction
5769
λ .= λ .^ (p / 2)
5870
Vs = rmul!(V, D)
59-
mul!(powA, Vs, Vs')
71+
return _mul_herm!(powA, Vs)
6072
end
61-
return project_hermitian!(powA)
6273
end
6374

6475
function power!(A::AbstractMatrix, p::Real, powA, alg::MatrixFunctionViaEig)

src/implementations/projections.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,16 @@ end
140140

141141
_imimag(x::Real) = zero(x)
142142
_imimag(x::Complex) = im * imag(x)
143+
144+
# Implement `mul!(C, A, A')` and guarantee the result is hermitian.
145+
# For BLAS calls that dispatch to `syrk` or `herk` this works automatically
146+
# for GPU this currently does not seem to be guaranteed so we manually project
147+
function _mul_herm!(C, A)
148+
mul!(C, A, A')
149+
project_hermitian!(C)
150+
return C
151+
end
152+
function _mul_herm!(C::YALAPACK.BlasMat{T}, A::YALAPACK.BlasMat{T}) where {T <: YALAPACK.BlasFloat}
153+
mul!(C, A, A')
154+
return C
155+
end

src/implementations/squareroot.jl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@ initialize_output(::typeof(squareroot!), A::AbstractMatrix, ::AbstractAlgorithm)
3535
function squareroot!(A::AbstractMatrix, sqrtA, alg::MatrixFunctionViaLA)
3636
check_input(squareroot!, A, sqrtA, alg)
3737
isempty(alg.kwargs) || throw(ArgumentError("`MatrixFunctionViaLA` does not accept keyword arguments for `squareroot`"))
38-
result = LinearAlgebra.sqrt(A)
39-
_copy_result!(squareroot!, sqrtA, result)
38+
# `LinearAlgebra.sqrt` of a real matrix is real whenever the principal square root is,
39+
# so a complex result with a real output signals a genuine domain violation
40+
sqrtAc = LinearAlgebra.sqrt(A)
41+
if eltype(sqrtAc) <: Complex && !(eltype(sqrtA) <: Complex)
42+
throw(_realness_domainerror(squareroot!))
43+
end
44+
copy!(sqrtA, sqrtAc)
4045
return sqrtA
4146
end
4247

@@ -49,8 +54,7 @@ function squareroot!(A::AbstractMatrix, sqrtA, alg::MatrixFunctionViaEigh)
4954
# `sqrt(A) = (V * D^(1/4)) * (V * D^(1/4))'` is hermitian by construction
5055
λ .= sqrt.(sqrt.(λ))
5156
Vs = rmul!(V, D)
52-
mul!(sqrtA, Vs, Vs')
53-
return project_hermitian!(sqrtA)
57+
return _mul_herm!(sqrtA, Vs)
5458
end
5559

5660
function squareroot!(A::AbstractMatrix, sqrtA, alg::MatrixFunctionViaEig)

0 commit comments

Comments
 (0)