diff --git a/src/MatrixAlgebraKit.jl b/src/MatrixAlgebraKit.jl index 5f872bb5..d3b45845 100644 --- a/src/MatrixAlgebraKit.jl +++ b/src/MatrixAlgebraKit.jl @@ -19,6 +19,8 @@ export eigh_full, eigh_vals, eigh_trunc export eigh_full!, eigh_vals!, eigh_trunc! export eig_full, eig_vals, eig_trunc export eig_full!, eig_vals!, eig_trunc! +export gen_eig_full, gen_eig_vals +export gen_eig_full!, gen_eig_vals! export schur_full, schur_vals export schur_full!, schur_vals! export left_polar, right_polar @@ -45,6 +47,7 @@ include("common/safemethods.jl") include("common/view.jl") include("common/regularinv.jl") include("common/matrixproperties.jl") +include("common/gauge.jl") include("yalapack.jl") include("algorithms.jl") @@ -54,6 +57,7 @@ include("interface/lq.jl") include("interface/svd.jl") include("interface/eig.jl") include("interface/eigh.jl") +include("interface/gen_eig.jl") include("interface/schur.jl") include("interface/polar.jl") include("interface/orthnull.jl") @@ -64,6 +68,7 @@ include("implementations/lq.jl") include("implementations/svd.jl") include("implementations/eig.jl") include("implementations/eigh.jl") +include("implementations/gen_eig.jl") include("implementations/schur.jl") include("implementations/polar.jl") include("implementations/orthnull.jl") diff --git a/src/algorithms.jl b/src/algorithms.jl index 7dbfe46e..2dc94e01 100644 --- a/src/algorithms.jl +++ b/src/algorithms.jl @@ -106,10 +106,14 @@ explicitly. New types should prefer to register their default algorithms in the type domain. """ default_algorithm default_algorithm(f::F, A; kwargs...) where {F} = default_algorithm(f, typeof(A); kwargs...) +default_algorithm(f::F, A, B; kwargs...) where {F} = default_algorithm(f, typeof(A), typeof(B); kwargs...) # avoid infinite recursion: function default_algorithm(f::F, ::Type{T}; kwargs...) where {F,T} throw(MethodError(default_algorithm, (f, T))) end +function default_algorithm(f::F, ::Type{TA}, ::Type{TB}; kwargs...) where {F,TA,TB} + throw(MethodError(default_algorithm, (f, TA, TB))) +end @doc """ copy_input(f, A) @@ -153,28 +157,8 @@ macro algdef(name) end) end -""" - @functiondef f - -Convenience macro to define the boilerplate code that dispatches between several versions of `f` and `f!`. -By default, this enables the following signatures to be defined in terms of -the final `f!(A, out, alg::Algorithm)`. - -```julia - f(A; kwargs...) - f(A, alg::Algorithm) - f!(A, [out]; kwargs...) - f!(A, alg::Algorithm) -``` - -See also [`copy_input`](@ref), [`select_algorithm`](@ref) and [`initialize_output`](@ref). -""" -macro functiondef(f) - f isa Symbol || throw(ArgumentError("Unsupported usage of `@functiondef`")) - f! = Symbol(f, :!) - - ex = quote - # out of place to inplace +function _arg_expr(::Val{1}, f, f!) + return quote # out of place to inplace $f(A; kwargs...) = $f!(copy_input($f, A); kwargs...) $f(A, alg::AbstractAlgorithm) = $f!(copy_input($f, A), alg) @@ -215,7 +199,110 @@ macro functiondef(f) # copy documentation to both functions Core.@__doc__ $f, $f! end - return esc(ex) +end + +function _arg_expr(::Val{2}, f, f!) + return quote + # out of place to inplace + $f(A, B; kwargs...) = $f!(copy_input($f, A, B)...; kwargs...) + $f(A, B, alg::AbstractAlgorithm) = $f!(copy_input($f, A, B)..., alg) + + # fill in arguments + function $f!(A, B; alg=nothing, kwargs...) + return $f!(A, B, select_algorithm($f!, (A, B), alg; kwargs...)) + end + function $f!(A, B, out; alg=nothing, kwargs...) + return $f!(A, B, out, select_algorithm($f!, (A, B), alg; kwargs...)) + end + function $f!(A, B, alg::AbstractAlgorithm) + return $f!(A, B, initialize_output($f!, A, B, alg), alg) + end + + # define fallbacks for algorithm selection + @inline function select_algorithm(::typeof($f), A, alg::Alg; kwargs...) where {Alg} + return select_algorithm($f!, A, alg; kwargs...) + end + # define default algorithm fallbacks for out-of-place functions + # in terms of the corresponding in-place function + @inline function default_algorithm(::typeof($f), A, B; kwargs...) + return default_algorithm($f!, A, B; kwargs...) + end + # define default algorithm fallbacks for out-of-place functions + # in terms of the corresponding in-place function for types, + # in principle this is covered by the definition above but + # it is necessary to avoid ambiguity errors with the generic definitions: + # ```julia + # default_algorithm(f::F, A; kwargs...) where {F} = default_algorithm(f, typeof(A); kwargs...) + # function default_algorithm(f::F, ::Type{T}; kwargs...) where {F,T} + # throw(MethodError(default_algorithm, (f, T))) + # end + # ``` + @inline function default_algorithm(::typeof($f), ::Type{A}, ::Type{B}; kwargs...) where {A, B} + return default_algorithm($f!, A, B; kwargs...) + end + + # copy documentation to both functions + Core.@__doc__ $f, $f! + end +end + +""" + @functiondef [n_args=1] f + +Convenience macro to define the boilerplate code that dispatches between several versions of `f` and `f!`. +By default, `f` accepts a single argument `A`. This enables the following signatures to be defined in terms of +the final `f!(A, out, alg::Algorithm)`. + +```julia + f(A; kwargs...) + f(A, alg::Algorithm) + f!(A, [out]; kwargs...) + f!(A, alg::Algorithm) +``` + +The number of inputs can be set with the `n_args` keyword +argument, so that + +```julia +@functiondef n_args=2 f +``` + +would create + +```julia + f(A, B; kwargs...) + f(A, B, alg::Algorithm) + f!(A, B, [out]; kwargs...) + f!(A, B, alg::Algorithm) +``` + +See also [`copy_input`](@ref), [`select_algorithm`](@ref) and [`initialize_output`](@ref). +""" +macro functiondef(args...) + kwargs = map(args[1:end-1]) do kwarg + if kwarg isa Symbol + :($kwarg = $kwarg) + elseif Meta.isexpr(kwarg, :(=)) + kwarg + else + throw(ArgumentError("Invalid keyword argument '$kwarg'")) + end + end + isempty(kwargs) || length(kwargs) == 1 || throw(ArgumentError("Only one keyword argument to `@functiondef` is supported")) + f_n_args = 1 # default + if length(kwargs) == 1 + kwarg = only(kwargs) # only one kwarg is currently supported, TODO modify if we support more + key::Symbol, val = kwarg.args + key === :n_args || throw(ArgumentError("Unsupported keyword argument $key to `@functiondef`")) + (isa(val, Integer) && val > 0) || throw(ArgumentError("`n_args` keyword argument to `@functiondef` should be an integer > 0")) + f_n_args = val + end + + f = args[end] + f isa Symbol || throw(ArgumentError("Unsupported usage of `@functiondef`")) + f! = Symbol(f, :!) + + return esc(_arg_expr(Val(f_n_args), f, f!)) end """ diff --git a/src/common/gauge.jl b/src/common/gauge.jl new file mode 100644 index 00000000..0f64e0a2 --- /dev/null +++ b/src/common/gauge.jl @@ -0,0 +1,8 @@ +function gaugefix!(V::AbstractMatrix) + for j in axes(V, 2) + v = view(V, :, j) + s = conj(sign(argmax(abs, v))) + @inbounds v .*= s + end + return V +end diff --git a/src/implementations/eig.jl b/src/implementations/eig.jl index 6d0efcca..84e97c4f 100644 --- a/src/implementations/eig.jl +++ b/src/implementations/eig.jl @@ -61,11 +61,7 @@ function eig_full!(A::AbstractMatrix, DV, alg::LAPACK_EigAlgorithm) YALAPACK.geevx!(A, D.diag, V; alg.kwargs...) end # TODO: make this controllable using a `gaugefix` keyword argument - for j in 1:size(V, 2) - v = view(V, :, j) - s = conj(sign(argmax(abs, v))) - v .*= s - end + V = gaugefix!(V) return D, V end diff --git a/src/implementations/gen_eig.jl b/src/implementations/gen_eig.jl new file mode 100644 index 00000000..859260fa --- /dev/null +++ b/src/implementations/gen_eig.jl @@ -0,0 +1,85 @@ +# Inputs +# ------ +function copy_input(::typeof(gen_eig_full), A::AbstractMatrix, B::AbstractMatrix) + return copy!(similar(A, float(eltype(A))), A), copy!(similar(B, float(eltype(B))), B) +end +function copy_input(::typeof(gen_eig_vals), A::AbstractMatrix, B::AbstractMatrix) + return copy_input(gen_eig_full, A, B) +end + +function check_input(::typeof(gen_eig_full!), A::AbstractMatrix, B::AbstractMatrix, WV) + ma, na = size(A) + mb, nb = size(B) + ma == na || throw(DimensionMismatch("square input matrix A expected")) + mb == nb || throw(DimensionMismatch("square input matrix B expected")) + ma == mb || throw(DimensionMismatch("first dimension of input matrices expected to match")) + na == nb || throw(DimensionMismatch("second dimension of input matrices expected to match")) + W, V = WV + @assert W isa Diagonal && V isa AbstractMatrix + @check_size(W, (ma, ma)) + @check_scalar(W, A, complex) + @check_scalar(W, B, complex) + @check_size(V, (ma, ma)) + @check_scalar(V, A, complex) + @check_scalar(V, B, complex) + return nothing +end +function check_input(::typeof(gen_eig_vals!), A::AbstractMatrix, B::AbstractMatrix, W) + ma, na = size(A) + mb, nb = size(B) + ma == na || throw(DimensionMismatch("square input matrix A expected")) + mb == nb || throw(DimensionMismatch("square input matrix B expected")) + ma == mb || throw(DimensionMismatch("dimension of input matrices expected to match")) + @assert W isa AbstractVector + @check_size(W, (na,)) + @check_scalar(W, A, complex) + @check_scalar(W, B, complex) + return nothing +end + +# Outputs +# ------- +function initialize_output(::typeof(gen_eig_full!), A::AbstractMatrix, B::AbstractMatrix, ::LAPACK_EigAlgorithm) + n = size(A, 1) # square check will happen later + Tc = complex(eltype(A)) + W = Diagonal(similar(A, Tc, n)) + V = similar(A, Tc, (n, n)) + return (W, V) +end +function initialize_output(::typeof(gen_eig_vals!), A::AbstractMatrix, B::AbstractMatrix, ::LAPACK_EigAlgorithm) + n = size(A, 1) # square check will happen later + Tc = complex(eltype(A)) + D = similar(A, Tc, n) + return D +end + +# Implementation +# -------------- +# actual implementation +function gen_eig_full!(A::AbstractMatrix, B::AbstractMatrix, WV, alg::LAPACK_EigAlgorithm) + check_input(gen_eig_full!, A, B, WV) + W, V = WV + if alg isa LAPACK_Simple + isempty(alg.kwargs) || + throw(ArgumentError("LAPACK_Simple (ggev) does not accept any keyword arguments")) + YALAPACK.ggev!(A, B, W.diag, V, similar(W.diag, eltype(A))) + else # alg isa LAPACK_Expert + throw(ArgumentError("LAPACK_Expert is not supported for ggev")) + end + # TODO: make this controllable using a `gaugefix` keyword argument + V = gaugefix!(V) + return W, V +end + +function gen_eig_vals!(A::AbstractMatrix, B::AbstractMatrix, W, alg::LAPACK_EigAlgorithm) + check_input(gen_eig_vals!, A, B, W) + V = similar(A, complex(eltype(A)), (size(A, 1), 0)) + if alg isa LAPACK_Simple + isempty(alg.kwargs) || + throw(ArgumentError("LAPACK_Simple (ggev) does not accept any keyword arguments")) + YALAPACK.ggev!(A, B, W, V, similar(W, eltype(A))) + else # alg isa LAPACK_Expert + throw(ArgumentError("LAPACK_Expert is not supported for ggev")) + end + return W +end diff --git a/src/interface/eig.jl b/src/interface/eig.jl index fd75193f..90fde0e0 100644 --- a/src/interface/eig.jl +++ b/src/interface/eig.jl @@ -1,13 +1,3 @@ -# Eig API -# ------- -# TODO: export? or not export but mark as public ? -function eig!(A::AbstractMatrix, args...; kwargs...) - return eig_full!(A, args...; kwargs...) -end -function eig(A::AbstractMatrix, args...; kwargs...) - return eig_full(A, args...; kwargs...) -end - # Eig functions # ------------- diff --git a/src/interface/gen_eig.jl b/src/interface/gen_eig.jl new file mode 100644 index 00000000..46d0c22d --- /dev/null +++ b/src/interface/gen_eig.jl @@ -0,0 +1,69 @@ +# Gen Eig functions +# ------------- + +# TODO: kwargs for sorting eigenvalues? + +docs_gen_eig_note = """ +Note that [`gen_eig_full`](@ref) and its variants do not assume additional structure on the inputs, +and therefore will always return complex eigenvalues and eigenvectors. For the real +generalized eigenvalue decomposition is not yet supported. +""" + +# TODO: do we need "full"? +""" + gen_eig_full(A, B; kwargs...) -> W, V + gen_eig_full(A, B, alg::AbstractAlgorithm) -> W, V + gen_eig_full!(A, B, [WV]; kwargs...) -> W, V + gen_eig_full!(A, B, [WV], alg::AbstractAlgorithm) -> W, V + +Compute the full generalized eigenvalue decomposition of the square matrices `A` and `B`, +such that `A * V = B * V * W`, where the invertible matrix `V` contains the generalized eigenvectors +and the diagonal matrix `W` contains the associated generalized eigenvalues. + +!!! note + The bang method `gen_eig_full!` optionally accepts the output structure and + possibly destroys the input matrices `A` and `B`. + Always use the return value of the function as it may not always be + possible to use the provided `WV` as output. + +!!! note + $(docs_gen_eig_note) + +See also [`gen_eig_vals(!)`](@ref eig_vals). +""" +@functiondef n_args=2 gen_eig_full + +""" + gen_eig_vals(A, B; kwargs...) -> W + gen_eig_vals(A, B, alg::AbstractAlgorithm) -> W + gen_eig_vals!(A, B, [W]; kwargs...) -> W + gen_eig_vals!(A, B, [W], alg::AbstractAlgorithm) -> W + +Compute the list of generalized eigenvalues of `A` and `B`. + +!!! note + The bang method `gen_eig_vals!` optionally accepts the output structure and + possibly destroys the input matrices `A` and `B`. Always use the return + value of the function as it may not always be possible to use the + provided `W` as output. + +!!! note + $(docs_gen_eig_note) + +See also [`gen_eig_full(!)`](@ref gen_eig_full). +""" +@functiondef n_args=2 gen_eig_vals + +# Algorithm selection +# ------------------- +default_gen_eig_algorithm(A, B; kwargs...) = default_gen_eig_algorithm(typeof(A), typeof(B); kwargs...) +default_gen_eig_algorithm(::Type{TA}, ::Type{TB}; kwargs...) where {TA, TB} = throw(MethodError(default_gen_eig_algorithm, (TA,TB))) +function default_gen_eig_algorithm(::Type{TA}, ::Type{TB}; kwargs...) where {TA<:YALAPACK.BlasMat,TB<:YALAPACK.BlasMat} + return LAPACK_Simple(; kwargs...) +end + +for f in (:gen_eig_full!, :gen_eig_vals!) + @eval function default_algorithm(::typeof($f), ::Tuple{A, B}; kwargs...) where {A, B} + return default_gen_eig_algorithm(A, B; kwargs...) + end +end diff --git a/src/yalapack.jl b/src/yalapack.jl index e8f4b685..ec37b674 100644 --- a/src/yalapack.jl +++ b/src/yalapack.jl @@ -1616,12 +1616,14 @@ for (gees, geesx, geev, geevx, ggev, elty, celty, relty) in end function ggev!(A::AbstractMatrix{$elty}, B::AbstractMatrix{$elty}, W::AbstractVector{$celty}=similar(A, $celty, size(A, 1)), - V::AbstractMatrix{$celty}=similar(A, $celty)) + V::AbstractMatrix{$celty}=similar(A, $celty), + Wbeta::AbstractVector{$elty}=similar(A, $elty, size(A, 1))) require_one_based_indexing(A, B, V, W) chkstride1(A, B, V, W) n = checksquare(A) n == checksquare(B) || throw(DimensionMismatch("size mismatch between A and B")) n == length(W) || throw(DimensionMismatch("length mismatch between A and W")) + n == length(Wbeta) || throw(DimensionMismatch("length mismatch between A and W_beta")) if length(V) == 0 jobvr = 'N' else @@ -1638,7 +1640,6 @@ for (gees, geesx, geev, geevx, ggev, elty, celty, relty) in info = Ref{BlasInt}() VL = similar(A, n, 0) ldvl = stride(VL, 2) - if eltype(A) <: Real W2 = reinterpret($elty, W) # reuse memory, we will have to reorder afterwards to bring real and imaginary @@ -1649,15 +1650,15 @@ for (gees, geesx, geev, geevx, ggev, elty, celty, relty) in ldvr = stride(VR, 2) for i in 1:2 # first call returns lwork as work[1] #! format: off - ccall((@blasfunc($geev), libblastrampoline), Cvoid, + ccall((@blasfunc($ggev), libblastrampoline), Cvoid, (Ref{UInt8}, Ref{UInt8}, - Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, - Ptr{$elty}, Ptr{$elty}, Ptr{$elty}, Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, + Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, + Ptr{$elty}, Ptr{$elty}, Ptr{$elty}, Ptr{$elty}, Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, Ptr{BlasInt}, Clong, Clong), jobvl, jobvr, - n, A, lda, - WR, WI, VL, ldvl, VR, ldvr, + n, A, lda, B, ldb, + WR, WI, Wbeta, VL, ldvl, VR, ldvr, work, lwork, info, 1, 1) #! format: on @@ -1668,20 +1669,20 @@ for (gees, geesx, geev, geevx, ggev, elty, celty, relty) in end end else - VR = V - ldvr = stride(VR, 2) - rwork = Vector{$relty}(undef, 2n) + VR = V + ldvr = stride(VR, 2) + rwork = Vector{$relty}(undef, 8*n) for i in 1:2 #! format: off - ccall((@blasfunc($geev), libblastrampoline), Cvoid, + ccall((@blasfunc($ggev), libblastrampoline), Cvoid, (Ref{UInt8}, Ref{UInt8}, - Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, - Ptr{$elty}, Ptr{$elty}, Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, + Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, + Ptr{$elty}, Ptr{$elty}, Ptr{$elty}, Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, Ptr{$elty}, Ref{BlasInt}, Ptr{$relty}, Ptr{BlasInt}, Clong, Clong), jobvl, jobvr, - n, A, lda, - W, VL, ldvl, VR, ldvr, + n, A, lda, B, ldb, + W, Wbeta, VL, ldvl, VR, ldvr, work, lwork, rwork, info, 1, 1) #! format: on @@ -1695,7 +1696,11 @@ for (gees, geesx, geev, geevx, ggev, elty, celty, relty) in # Cleanup the output in the real case if eltype(A) <: Real - _reorder_realeigendecomposition!(W, WR, WI, work, VR, jobvr) + _reorder_realeigendecomposition!(W, WR, WI, Wbeta, work, VR, jobvr) + else + for i in 1:n + W[i] /= Wbeta[i] + end end return W, V end @@ -1733,6 +1738,37 @@ function _reorder_realeigendecomposition!(W, WR, WI, work, VR, jobvr) end end +function _reorder_realeigendecomposition!(W, WR, WI, Wbeta, work, VR, jobvr) + # first reorder eigenvalues and recycle work as temporary buffer to efficiently implement the permutation + n = size(W, 1) + resize!(work, n) + copy!(work, WI) + for i in n:-1:1 + W[i] = (WR[i] + im * work[i]) / Wbeta[i] + end + if jobvr == 'V' # also reorganise vectors + i = 1 + while i <= n + if iszero(imag(W[i])) # real eigenvalue => real eigenvector + for j in n:-1:1 + VR[2 * j, i] = 0 + VR[2 * j - 1, i] = VR[j, i] + end + i += 1 + else # complex eigenvalue => complex eigenvector and conjugate + @assert i != n + for j in n:-1:1 + VR[2 * j, i] = VR[j, i + 1] + VR[2 * j - 1, i] = VR[j, i] + VR[2 * j, i + 1] = -VR[j, i + 1] + VR[2 * j - 1, i + 1] = VR[j, i] + end + i += 2 + end + end + end +end + # SVD for (gesvd, gesdd, gesvdx, gejsv, gesvj, elty, relty) in ((:dgesvd_, :dgesdd_, :dgesvdx_, :dgejsv_, :dgesvj_, :Float64, :Float64), diff --git a/test/gen_eig.jl b/test/gen_eig.jl new file mode 100644 index 00000000..c46dfcf2 --- /dev/null +++ b/test/gen_eig.jl @@ -0,0 +1,62 @@ +using MatrixAlgebraKit +using Test +using TestExtras +using StableRNGs +using LinearAlgebra: Diagonal + +@testset "gen_eig_full! for T = $T" for T in (Float32, Float64, ComplexF32, ComplexF64) + rng = StableRNG(123) + m = 54 + for alg in (LAPACK_Simple(), :LAPACK_Simple, LAPACK_Simple) + A = randn(rng, T, m, m) + B = randn(rng, T, m, m) + A_init = copy(A) + B_init = copy(B) + Tc = complex(T) + + W, V = @constinferred gen_eig_full(A, B; alg=($alg)) + @test eltype(W) == eltype(V) == Tc + @test A_init == A + @test B_init == B + @test A * V ≈ B * V * Diagonal(W) + + alg′ = @constinferred MatrixAlgebraKit.select_algorithm(gen_eig_full!, (A, B), $alg) + + Ac = similar(A) + Bc = similar(B) + W2, V2 = @constinferred gen_eig_full!(copy!(Ac, A), copy!(Bc, B), (W, V), alg′) + @test W2 === W + @test V2 === V + @test A * V ≈ B * V * Diagonal(W) + + Ac = similar(A) + Bc = similar(B) + W2, V2 = @constinferred gen_eig_full!(copy!(Ac, A), copy!(Bc, B), (W, V)) + @test W2 === W + @test V2 === V + @test A * V ≈ B * V * Diagonal(W) + + Wc = @constinferred gen_eig_vals(A, B, alg′) + @test eltype(Wc) == Tc + @test W ≈ Diagonal(Wc) + + end + A = randn(rng, T, m, m) + B = randn(rng, T, m, m) + @test_throws ArgumentError("LAPACK_Expert is not supported for ggev") gen_eig_full(A, B; alg=LAPACK_Expert()) + @test_throws ArgumentError("LAPACK_Simple (ggev) does not accept any keyword arguments") gen_eig_full(A, B; alg=LAPACK_Simple(bad="sad")) + @test_throws ArgumentError("LAPACK_Expert is not supported for ggev") gen_eig_vals(A, B; alg=LAPACK_Expert()) + @test_throws ArgumentError("LAPACK_Simple (ggev) does not accept any keyword arguments") gen_eig_vals(A, B; alg=LAPACK_Simple(bad="sad")) + + # a tuple of the input types is passed to `default_algorithm` + @test_throws MethodError MatrixAlgebraKit.default_algorithm(gen_eig_full, A, B) + @test_throws MethodError MatrixAlgebraKit.default_algorithm(gen_eig_vals, A, B) + if T <: Real + # Float16 isn't supported + Afp16 = Float16.(A) + Bfp16 = Float16.(B) + @test_throws MethodError MatrixAlgebraKit.default_algorithm(gen_eig_full, (Afp16, Bfp16)) + @test_throws MethodError MatrixAlgebraKit.default_gen_eig_algorithm(Afp16, Bfp16) + end +end + diff --git a/test/runtests.jl b/test/runtests.jl index 13f33f09..54923e85 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -23,6 +23,9 @@ if !is_buildkite @safetestset "General Eigenvalue Decomposition" begin include("eig.jl") end + @safetestset "Generalized Eigenvalue Decomposition" begin + include("gen_eig.jl") + end @safetestset "Schur Decomposition" begin include("schur.jl") end