diff --git a/src/exp_baseexp.jl b/src/exp_baseexp.jl index 6ecc913..6069d93 100644 --- a/src/exp_baseexp.jl +++ b/src/exp_baseexp.jl @@ -83,6 +83,7 @@ function exponential!( LAPACK.gesv!(temp, X) else s = log2(nA / 5.4) # power of 2 later reversed by squaring + si = 0 # always defined so the s > 0 squaring loop is type-stable if s > 0 si = ceil(Int, s) A ./= convert(T, 2^si) diff --git a/src/kiops.jl b/src/kiops.jl index 9510cfa..acf3ac9 100644 --- a/src/kiops.jl +++ b/src/kiops.jl @@ -117,10 +117,15 @@ function kiops( omega = NaN orderold = true kestold = true + # `order`/`kest` carry their previous value across iterations (the `orderold`/ + # `kestold` flags select "reuse"); seed them with the first-iteration defaults so + # they are always defined before use rather than only conditionally assigned. + order = 0.0 + kest = 2 l = 1 - local beta, kest + local beta while tau_now < tau_end oldj = Ks.m arnoldi!( diff --git a/src/krylov_phiv.jl b/src/krylov_phiv.jl index a0e521b..496e330 100644 --- a/src/krylov_phiv.jl +++ b/src/krylov_phiv.jl @@ -87,6 +87,14 @@ function expv!( ) where {Tw, T, U} m, beta, V, H = Ks.m, Ks.beta, getV(Ks), getH(Ks) @assert length(w) == size(V, 1) "Dimension mismatch" + if iszero(beta) + # Zero input: the Krylov basis V was never initialized (firststep! skips + # the fill when beta == 0), so `beta * V * expHe` would be `0 * garbage`, + # which is NaN whenever V holds uninitialized memory. The result is exactly + # zero, matching the complex `expv!` method's guard below. + w .= false + return w + end if isnothing(cache) cache = Matrix{U}(undef, m, m) elseif isa(cache, ExpvCache) diff --git a/test/qa/qa.jl b/test/qa/qa.jl index dcc58f5..e096d72 100644 --- a/test/qa/qa.jl +++ b/test/qa/qa.jl @@ -14,39 +14,60 @@ using ExponentialUtilities, Aqua, JET, Test Aqua.test_undefined_exports(ExponentialUtilities) end +# Analyze only ExponentialUtilities' own code. Without this, JET on Julia 1.12 traces +# into LinearAlgebra/Base internals (e.g. `norm(::Vector)` -> `norm_recursive_check`, +# and the broadcast `unalias`/`copyto_unaliased!` path over `Adjoint{T, Union{}}`) and +# reports abstract-interpretation artifacts there that are not under this package's +# control. Scoping to `ExponentialUtilities` keeps full coverage of this package's code +# (it still flags real `may be undefined` findings here) without asserting that all of +# the stdlib is JET-clean. +const JET_TARGET = (ExponentialUtilities,) + @testset "JET static analysis" begin @testset "expv" begin - rep = JET.report_call(expv, (Float64, Matrix{Float64}, Vector{Float64})) + rep = JET.report_call( + expv, (Float64, Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end @testset "arnoldi" begin - rep = JET.report_call(arnoldi, (Matrix{Float64}, Vector{Float64})) + rep = JET.report_call( + arnoldi, (Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end @testset "phi" begin - rep = JET.report_call(phi, (Matrix{Float64}, Int)) + rep = JET.report_call(phi, (Matrix{Float64}, Int); target_modules = JET_TARGET) @test length(JET.get_reports(rep)) == 0 end @testset "exponential!" begin - rep = JET.report_call(ExponentialUtilities.exponential!, (Matrix{Float64},)) + rep = JET.report_call( + ExponentialUtilities.exponential!, (Matrix{Float64},); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end @testset "phiv" begin - rep = JET.report_call(phiv, (Float64, Matrix{Float64}, Vector{Float64}, Int)) + rep = JET.report_call( + phiv, (Float64, Matrix{Float64}, Vector{Float64}, Int); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end @testset "kiops" begin - rep = JET.report_call(kiops, (Float64, Matrix{Float64}, Vector{Float64})) + rep = JET.report_call( + kiops, (Float64, Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end @testset "expv_timestep" begin - rep = JET.report_call(expv_timestep, (Float64, Matrix{Float64}, Vector{Float64})) + rep = JET.report_call( + expv_timestep, (Float64, Matrix{Float64}, Vector{Float64}); target_modules = JET_TARGET + ) @test length(JET.get_reports(rep)) == 0 end end diff --git a/test/test_groups.toml b/test/test_groups.toml index 9fbb711..3856fb2 100644 --- a/test/test_groups.toml +++ b/test/test_groups.toml @@ -6,7 +6,10 @@ # QA runs the metadata/static-analysis checks (Aqua + JET) in the isolated # test/qa environment. # GPU runs the CUDA tests in the isolated test/gpu environment on a self-hosted -# GPU runner (matching the pre-conversion GPU.yml workflow). +# GPU runner (matching the pre-conversion GPU.yml workflow). It is `in_all = false` +# so it only ever runs under an explicit GROUP=GPU on the CUDA-equipped runner and +# is never pulled into the "All" aggregate (which a non-GPU job can fall back to), +# where `using CUDA` errors with "CUDA driver not functional". [Core] versions = ["lts", "1", "pre"] @@ -19,3 +22,4 @@ versions = ["lts", "1"] versions = ["1"] runner = ["self-hosted", "Linux", "X64", "gpu"] timeout = 60 +in_all = false