Skip to content

Commit 3bc2bb2

Browse files
lkdvosclaude
andcommitted
refactor: share input handling and reconstruction between matrix functions
`squareroot`, `logarithm` and `power` all map a square matrix to a matrix of the same size and scalar type, so their `copy_input`, `check_input` and `initialize_output` contracts were identical modulo the function name -- six methods repeated three times over. Fold the bodies into `_matrixfunction_copy_input` and `_matrixfunction_check_input`, whose own dispatch absorbs the `Diagonal` and `DiagonalAlgorithm` specializations, so each function keeps just the one line that names it. The three `MatrixFunctionViaEig` kernels shared an identical skeleton, differing only in which scalar function is applied to the eigenvalues: solve `V f(D) V⁻¹` and, for real input, discard the imaginary part left by the complex decomposition. That becomes `_apply_eig!`, taking the already transformed eigenvalues. Similarly `_apply_eigh!` captures the `V f(D) V'`-then-project shape shared by `logarithm` and integer `power`, and pairs with the existing `_mul_herm!` for the cases where a symmetric product makes the result hermitian by construction instead. Deliberately left alone: the `MatrixFunctionViaLA` kernels, whose bodies look alike but must not be merged -- `_copy_result!` did exactly that and was removed in 2698e26, because `squareroot`/`logarithm` need a strict complex-result check while `power` has to tolerate rounding-level imaginary components. `exponential` also stays out, as its real/complex branch keys on the scalar `τ` as well as the matrix eltype. Behaviour is unchanged: the matrix-function tests report the same 4701 passing as before the refactor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 351fb16 commit 3bc2bb2

4 files changed

Lines changed: 69 additions & 90 deletions

File tree

src/implementations/logarithm.jl

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
11
# Inputs
22
# ------
3-
function copy_input(::typeof(logarithm), A::AbstractMatrix)
4-
return copy!(similar(A, float(eltype(A))), A)
5-
end
6-
copy_input(::typeof(logarithm), A::Diagonal) = map_diagonal(float, A)
3+
copy_input(::typeof(logarithm), A::AbstractMatrix) = _matrixfunction_copy_input(A)
74

85
function check_input(::typeof(logarithm!), A::AbstractMatrix, logA, alg::AbstractAlgorithm)
9-
m = LinearAlgebra.checksquare(A)
10-
@check_size(logA, (m, m))
11-
@check_scalar(logA, A)
12-
return nothing
13-
end
14-
15-
function check_input(::typeof(logarithm!), A::AbstractMatrix, logA, ::DiagonalAlgorithm)
16-
m = LinearAlgebra.checksquare(A)
17-
@assert isdiag(A)
18-
@assert logA isa Diagonal
19-
@check_size(logA, (m, m))
20-
@check_scalar(logA, A)
21-
return nothing
6+
return _matrixfunction_check_input(A, logA, alg)
227
end
238

249
# Algorithm selection
@@ -48,24 +33,17 @@ end
4833
function logarithm!(A::AbstractMatrix, logA, alg::MatrixFunctionViaEigh)
4934
check_input(logarithm!, A, logA, alg)
5035
D, V = eigh_full!(A, alg.eigh_alg)
51-
VD = V * logarithm!(D, D, DiagonalAlgorithm(; domain_atol = alg.domain_atol))
52-
mul!(logA, VD, V')
53-
return project_hermitian!(logA)
36+
diag_alg = DiagonalAlgorithm(; domain_atol = alg.domain_atol)
37+
return _apply_eigh!(logA, V, logarithm!(D, D, diag_alg))
5438
end
5539

5640
function logarithm!(A::AbstractMatrix, logA, alg::MatrixFunctionViaEig)
5741
check_input(logarithm!, A, logA, alg)
5842
D, V = eig_full!(A, alg.eig_alg)
43+
# a real result requires the spectrum to stay off the negative real axis
44+
eltype(A) <: Real && _clamp_domain_eigenvalues!(D, alg.domain_atol)
5945
diag_alg = DiagonalAlgorithm(; domain_atol = alg.domain_atol)
60-
if eltype(A) <: Real
61-
_clamp_domain_eigenvalues!(D, alg.domain_atol)
62-
VlogD = V * logarithm!(D, D, diag_alg)
63-
logAc = rdiv!(VlogD, LinearAlgebra.lu!(V))
64-
return logA .= real.(logAc)
65-
else
66-
logA .= V .* transpose(diagview(logarithm!(D, D, diag_alg)))
67-
return rdiv!(logA, LinearAlgebra.lu!(V))
68-
end
46+
return _apply_eig!(logA, V, logarithm!(D, D, diag_alg))
6947
end
7048

7149
# Diagonal logic

src/implementations/matrixfunctions.jl

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,52 @@
1+
# Shared input and output handling
2+
# --------------------------------
3+
# `squareroot`, `logarithm` and `power` all map a square matrix to a matrix of the same size and
4+
# scalar type, so they agree on `copy_input`, `check_input` and `initialize_output`; only their
5+
# kernels differ. Each implementation file forwards to the helpers below, which keeps the
6+
# per-function definitions to the one line that names the function.
7+
8+
_matrixfunction_copy_input(A::AbstractMatrix) = copy!(similar(A, float(eltype(A))), A)
9+
_matrixfunction_copy_input(A::Diagonal) = map_diagonal(float, A)
10+
11+
function _matrixfunction_check_input(A::AbstractMatrix, out, ::AbstractAlgorithm)
12+
m = LinearAlgebra.checksquare(A)
13+
@check_size(out, (m, m))
14+
@check_scalar(out, A)
15+
return nothing
16+
end
17+
18+
function _matrixfunction_check_input(A::AbstractMatrix, out, ::DiagonalAlgorithm)
19+
m = LinearAlgebra.checksquare(A)
20+
@assert isdiag(A)
21+
@assert out isa Diagonal
22+
@check_size(out, (m, m))
23+
@check_scalar(out, A)
24+
return nothing
25+
end
26+
27+
# Shared reconstruction from an eigenvalue decomposition
28+
# ------------------------------------------------------
29+
# Both take the already-transformed eigenvalues `fD = f(D)` and rebuild `f(A)`.
30+
31+
# `f(A) = V f(D) V⁻¹` for a general `A`. A real matrix has a complex decomposition but a real
32+
# `f(A)` whenever `f(D)` closes under conjugation, so the imaginary part is dropped after the
33+
# solve; the callers are responsible for rejecting the inputs where it would not be.
34+
function _apply_eig!(fA, V, fD)
35+
if eltype(fA) <: Real
36+
VfD = V * fD
37+
fAc = rdiv!(VfD, LinearAlgebra.lu!(V))
38+
return fA .= real.(fAc)
39+
else
40+
fA .= V .* transpose(diagview(fD))
41+
return rdiv!(fA, LinearAlgebra.lu!(V))
42+
end
43+
end
44+
45+
# `f(A) = V f(D) V'` for a hermitian `A`. The product is hermitian only up to roundoff, so it is
46+
# projected afterwards; where `f` admits a square root, prefer building `f(A)` as a symmetric
47+
# product via `_mul_herm!`, which is hermitian by construction.
48+
_apply_eigh!(fA, V, fD) = project_hermitian!(mul!(fA, V * fD, V'))
49+
150
# Shared helpers for matrix functions with a restricted domain
251
# -------------------------------------------------------------
352

src/implementations/power.jl

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
11
# Inputs
22
# ------
3-
function copy_input(::typeof(power), A::AbstractMatrix, p::Real)
4-
return copy!(similar(A, float(eltype(A))), A), p
5-
end
6-
copy_input(::typeof(power), A::Diagonal, p::Real) = map_diagonal(float, A), p
3+
copy_input(::typeof(power), A::AbstractMatrix, p::Real) = _matrixfunction_copy_input(A), p
74

85
function check_input(::typeof(power!), A::AbstractMatrix, p::Real, powA, alg::AbstractAlgorithm)
9-
m = LinearAlgebra.checksquare(A)
10-
@check_size(powA, (m, m))
11-
@check_scalar(powA, A)
12-
return nothing
13-
end
14-
15-
function check_input(::typeof(power!), A::AbstractMatrix, p::Real, powA, ::DiagonalAlgorithm)
16-
m = LinearAlgebra.checksquare(A)
17-
@assert isdiag(A)
18-
@assert powA isa Diagonal
19-
@check_size(powA, (m, m))
20-
@check_scalar(powA, A)
21-
return nothing
6+
return _matrixfunction_check_input(A, powA, alg)
227
end
238

249
# Algorithm selection
@@ -60,32 +45,20 @@ function power!(A::AbstractMatrix, p::Real, powA, alg::MatrixFunctionViaEigh)
6045
isone(p) && ((powA === A || copy!(powA, A)); return powA)
6146
D, V = eigh_full!(A, alg.eigh_alg)
6247
diag_alg = DiagonalAlgorithm(; domain_atol = alg.domain_atol)
63-
if isinteger(p)
64-
VD = V * power!(D, p, D, diag_alg)
65-
mul!(powA, VD, V')
66-
return project_hermitian!(powA)
67-
else
68-
# `A^p = (V * D^(p/2)) * (V * D^(p/2))'` is hermitian by construction
69-
Vs = rmul!(V, power!(D, p / 2, D, diag_alg))
70-
return _mul_herm!(powA, Vs)
71-
end
48+
isinteger(p) && return _apply_eigh!(powA, V, power!(D, p, D, diag_alg))
49+
# `A^p = (V * D^(p/2)) * (V * D^(p/2))'` is hermitian by construction
50+
return _mul_herm!(powA, rmul!(V, power!(D, p / 2, D, diag_alg)))
7251
end
7352

7453
function power!(A::AbstractMatrix, p::Real, powA, alg::MatrixFunctionViaEig)
7554
check_input(power!, A, p, powA, alg)
7655
iszero(p) && return one!(powA)
7756
isone(p) && ((powA === A || copy!(powA, A)); return powA)
7857
D, V = eig_full!(A, alg.eig_alg)
58+
# only a fractional power of a real matrix needs the spectrum off the negative real axis
59+
eltype(A) <: Real && !isinteger(p) && _clamp_domain_eigenvalues!(D, alg.domain_atol)
7960
diag_alg = DiagonalAlgorithm(; domain_atol = alg.domain_atol)
80-
if eltype(A) <: Real
81-
isinteger(p) || _clamp_domain_eigenvalues!(D, alg.domain_atol)
82-
VpD = V * power!(D, p, D, diag_alg)
83-
powAc = rdiv!(VpD, LinearAlgebra.lu!(V))
84-
return powA .= real.(powAc)
85-
else
86-
powA .= V .* transpose(diagview(power!(D, p, D, diag_alg)))
87-
return rdiv!(powA, LinearAlgebra.lu!(V))
88-
end
61+
return _apply_eig!(powA, V, power!(D, p, D, diag_alg))
8962
end
9063

9164
# Diagonal logic

src/implementations/squareroot.jl

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
11
# Inputs
22
# ------
3-
function copy_input(::typeof(squareroot), A::AbstractMatrix)
4-
return copy!(similar(A, float(eltype(A))), A)
5-
end
6-
copy_input(::typeof(squareroot), A::Diagonal) = map_diagonal(float, A)
3+
copy_input(::typeof(squareroot), A::AbstractMatrix) = _matrixfunction_copy_input(A)
74

85
function check_input(::typeof(squareroot!), A::AbstractMatrix, sqrtA, alg::AbstractAlgorithm)
9-
m = LinearAlgebra.checksquare(A)
10-
@check_size(sqrtA, (m, m))
11-
@check_scalar(sqrtA, A)
12-
return nothing
13-
end
14-
15-
function check_input(::typeof(squareroot!), A::AbstractMatrix, sqrtA, ::DiagonalAlgorithm)
16-
m = LinearAlgebra.checksquare(A)
17-
@assert isdiag(A)
18-
@assert sqrtA isa Diagonal
19-
@check_size(sqrtA, (m, m))
20-
@check_scalar(sqrtA, A)
21-
return nothing
6+
return _matrixfunction_check_input(A, sqrtA, alg)
227
end
238

249
# Algorithm selection
@@ -56,16 +41,10 @@ end
5641
function squareroot!(A::AbstractMatrix, sqrtA, alg::MatrixFunctionViaEig)
5742
check_input(squareroot!, A, sqrtA, alg)
5843
D, V = eig_full!(A, alg.eig_alg)
44+
# a real result requires the spectrum to stay off the negative real axis
45+
eltype(A) <: Real && _clamp_domain_eigenvalues!(D, alg.domain_atol)
5946
diag_alg = DiagonalAlgorithm(; domain_atol = alg.domain_atol)
60-
if eltype(A) <: Real
61-
_clamp_domain_eigenvalues!(D, alg.domain_atol)
62-
VsqrtD = V * squareroot!(D, D, diag_alg)
63-
sqrtAc = rdiv!(VsqrtD, LinearAlgebra.lu!(V))
64-
return sqrtA .= real.(sqrtAc)
65-
else
66-
sqrtA .= V .* transpose(diagview(squareroot!(D, D, diag_alg)))
67-
return rdiv!(sqrtA, LinearAlgebra.lu!(V))
68-
end
47+
return _apply_eig!(sqrtA, V, squareroot!(D, D, diag_alg))
6948
end
7049

7150
# Diagonal logic

0 commit comments

Comments
 (0)