diff --git a/NEWS.md b/NEWS.md index 3f4a4f37e4919..8dd55d403e7b5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -163,6 +163,7 @@ Standard library changes * The BLAS submodule now supports the level-2 BLAS subroutine `hpmv!` ([#34211]). * `normalize` now supports multidimensional arrays ([#34239]). * `lq` factorizations can now be used to compute the minimum-norm solution to under-determined systems ([#34350]). +* `sqrt(::Hermitian)` now treats slightly negative eigenvalues as zero for nearly semidefinite matrices, and accepts a new `rtol` keyword argument for this tolerance ([#35057]). * The BLAS submodule now supports the level-2 BLAS subroutine `spmv!` ([#34320]). * The BLAS submodule now supports the level-1 BLAS subroutine `rot!` ([#35124]). * New generic `rotate!(x, y, c, s)` and `reflect!(x, y, c, s)` functions ([#35124]). diff --git a/stdlib/LinearAlgebra/src/dense.jl b/stdlib/LinearAlgebra/src/dense.jl index 3739b6dca5947..26d6915aad0cf 100644 --- a/stdlib/LinearAlgebra/src/dense.jl +++ b/stdlib/LinearAlgebra/src/dense.jl @@ -710,8 +710,15 @@ If `A` has no negative real eigenvalues, compute the principal matrix square roo that is the unique matrix ``X`` with eigenvalues having positive real part such that ``X^2 = A``. Otherwise, a nonprincipal square root is returned. -If `A` is symmetric or Hermitian, its eigendecomposition ([`eigen`](@ref)) is -used to compute the square root. Otherwise, the square root is determined by means of the +If `A` is real-symmetric or Hermitian, its eigendecomposition ([`eigen`](@ref)) is +used to compute the square root. For such matrices, eigenvalues λ that +appear to be slightly negative due to roundoff errors are treated as if they were zero +More precisely, matrices with all eigenvalues `≥ -rtol*(max |λ|)` are treated as semidefinite +(yielding a Hermitian square root), with negative eigenvalues taken to be zero. +`rtol` is a keyword argument to `sqrt` (in the Hermitian/real-symmetric case only) that +defaults to machine precision scaled by `size(A,1)`. + +Otherwise, the square root is determined by means of the Björck-Hammarling method [^BH83], which computes the complex Schur form ([`schur`](@ref)) and then the complex square root of the triangular factor. diff --git a/stdlib/LinearAlgebra/src/symmetric.jl b/stdlib/LinearAlgebra/src/symmetric.jl index 20a25084372dd..b29aab58b8baa 100644 --- a/stdlib/LinearAlgebra/src/symmetric.jl +++ b/stdlib/LinearAlgebra/src/symmetric.jl @@ -1007,22 +1007,27 @@ end for func in (:log, :sqrt) + # sqrt has rtol arg to handle matrices that are semidefinite up to roundoff errors + rtolarg = func === :sqrt ? Any[Expr(:kw, :(rtol::Real), :(eps(real(float(one(T))))*size(A,1)))] : Any[] + rtolval = func === :sqrt ? :(-maximum(abs, F.values) * rtol) : 0 @eval begin - function ($func)(A::HermOrSym{<:Real}) + function ($func)(A::HermOrSym{T}; $(rtolarg...)) where {T<:Real} F = eigen(A) - if all(λ -> λ ≥ 0, F.values) - retmat = (F.vectors * Diagonal(($func).(F.values))) * F.vectors' + λ₀ = $rtolval # treat λ ≥ λ₀ as "zero" eigenvalues up to roundoff + if all(λ -> λ ≥ λ₀, F.values) + retmat = (F.vectors * Diagonal(($func).(max.(0, F.values)))) * F.vectors' else retmat = (F.vectors * Diagonal(($func).(complex.(F.values)))) * F.vectors' end return Symmetric(retmat) end - function ($func)(A::Hermitian{<:Complex}) + function ($func)(A::Hermitian{T}; $(rtolarg...)) where {T<:Complex} n = checksquare(A) F = eigen(A) - if all(λ -> λ ≥ 0, F.values) - retmat = (F.vectors * Diagonal(($func).(F.values))) * F.vectors' + λ₀ = $rtolval # treat λ ≥ λ₀ as "zero" eigenvalues up to roundoff + if all(λ -> λ ≥ λ₀, F.values) + retmat = (F.vectors * Diagonal(($func).(max.(0, F.values)))) * F.vectors' for i = 1:n retmat[i,i] = real(retmat[i,i]) end diff --git a/stdlib/LinearAlgebra/test/symmetric.jl b/stdlib/LinearAlgebra/test/symmetric.jl index f1ebe8a727aa5..9f75d21d46b96 100644 --- a/stdlib/LinearAlgebra/test/symmetric.jl +++ b/stdlib/LinearAlgebra/test/symmetric.jl @@ -663,4 +663,19 @@ end @test LinearAlgebra.hermitian_type(Int) == Int end +@testset "sqrt(nearly semidefinite)" begin + let A = [0.9999999999999998 4.649058915617843e-16 -1.3149405273715513e-16 9.9959579317056e-17; -8.326672684688674e-16 1.0000000000000004 2.9280733590254494e-16 -2.9993900031619594e-16; 9.43689570931383e-16 -1.339206523454095e-15 1.0000000000000007 -8.550505126287743e-16; -6.245004513516506e-16 -2.0122792321330962e-16 1.183061278035052e-16 1.0000000000000002], + B = [0.09648289218436859 0.023497875751503007 0.0 0.0; 0.023497875751503007 0.045787575150300804 0.0 0.0; 0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0], + C = Symmetric(A*B*A'), # semidefinite up to roundoff + Csqrt = sqrt(C) + @test Csqrt isa Symmetric{Float64} + @test Csqrt*Csqrt ≈ C rtol=1e-14 + end + let D = Symmetric(Matrix(Diagonal([1 0; 0 -1e-14]))) + @test sqrt(D) ≈ [1 0; 0 1e-7im] rtol=1e-14 + @test sqrt(D, rtol=1e-13) ≈ [1 0; 0 0] rtol=1e-14 + @test sqrt(D, rtol=1e-13)^2 ≈ D rtol=1e-13 + end +end + end # module TestSymmetric