diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e2e9a..0913b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,22 @@ because a changelog assembled after the fact loses exactly the reasoning that ma keeping. The `[Unreleased]` target below is provisional — confirm it when the first entry is written. +## [Unreleased] — targeting 0.2.2 + +### New Features + +- **`spectrumplot` draws a reduced basis's spectrum, and `Makie` joins `[weakdeps]`, at `"0.24"`.** + `spectrumplot(σ::AbstractVector{<:Real}; title = "", ylabel = "", size = (620, 380))` draws `σ` + against its index on a log10 axis and returns a `Makie.FigureAxisPlot`. It lives in the extension + `ReducedBasisMethodsMakieExt`, which loads with `Makie`, so without `Makie` the call is a + `MethodError`; `save` needs a backend such as `CairoMakie`. + +### Changed + +- **The docs environment drops `Weave` and `Plots`, and `get_DEIM_interpolation_matrix` carries a + `jldoctest`.** No page used either package. This is the only `jldoctest` in the package; + `test/quality/doctests.jl` runs it in the `doctests` group. + ## [0.2.1] — 2026-10-14 ### New Features diff --git a/Project.toml b/Project.toml index a09871b..1fdd0c3 100644 --- a/Project.toml +++ b/Project.toml @@ -11,10 +11,17 @@ MultiIndexArrays = "2f7e0951-2673-41f4-a6a8-e6772a4b2a08" ReducedComplexityModeling = "abb4722c-1d8e-48eb-a3bd-45b3116bfaba" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +[weakdeps] +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" + +[extensions] +ReducedBasisMethodsMakieExt = "Makie" + [compat] GeometricEquations = "0.21" HDF5 = "0.16, 0.17" LinearAlgebra = "1" +Makie = "0.24" MultiIndexArrays = "0.1.2" ReducedComplexityModeling = "0.1.1" Statistics = "1" diff --git a/docs/Project.toml b/docs/Project.toml index 14d3885..a73d4fd 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,10 +1,6 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" ReducedBasisMethods = "91da5cd7-58ae-4d67-87db-f03d1c8da121" -Weave = "44d3d7a6-8a23-5bf8-98c5-b353f8df5ec9" [compat] Documenter = "1.19.0" -Plots = "1.41.7" -Weave = "0.10.12" diff --git a/docs/make.jl b/docs/make.jl index 5a7a20f..613b552 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,10 +1,5 @@ using ReducedBasisMethods using Documenter -using Weave - -# weave("src/poisson.jmd", -# out_path = "src", -# doctype = "github") makedocs(; modules = [ReducedBasisMethods], diff --git a/ext/ReducedBasisMethodsMakieExt.jl b/ext/ReducedBasisMethodsMakieExt.jl new file mode 100644 index 0000000..de351ad --- /dev/null +++ b/ext/ReducedBasisMethodsMakieExt.jl @@ -0,0 +1,19 @@ +module ReducedBasisMethodsMakieExt + +using Makie +using ReducedBasisMethods + +function ReducedBasisMethods.spectrumplot(σ::AbstractVector{<:Real}; + title = "", ylabel = "", size = (620, 380)) + all(isfinite, σ) || throw(ArgumentError("the spectrum holds a NaN or an Inf")) + any(>(0), σ) || throw(ArgumentError("the spectrum holds no positive value")) + + # A log-scaled axis cannot draw the exact zeros of a rank-deficient snapshot set or the + # round-off negatives of a Gram spectrum, so every value below the floor is lifted onto it. + y = max.(Float64.(σ), eps(Float64) * Float64(maximum(σ))) + + return lines(y; figure = (; size), + axis = (; title, titlealign = :left, xlabel = "i", ylabel, yscale = log10)) +end + +end diff --git a/src/ReducedBasisMethods.jl b/src/ReducedBasisMethods.jl index d69e17a..c007efa 100644 --- a/src/ReducedBasisMethods.jl +++ b/src/ReducedBasisMethods.jl @@ -39,6 +39,10 @@ include("reduced_equations.jl") export reduced_ode, reduced_pode +include("diagnostics.jl") + +export spectrumplot + export h5save, h5load include("reduced_tensor.jl") diff --git a/src/algorithms/deim.jl b/src/algorithms/deim.jl index 89c6ef1..d72753d 100644 --- a/src/algorithms/deim.jl +++ b/src/algorithms/deim.jl @@ -66,6 +66,15 @@ Returns the `N × k` matrix `Π` whose column `i` is the unit vector of the row Its element type is `float(eltype(Ψ))`, so a `Float32` basis gives a `Float32` interpolation matrix. The selection, and with it the `ArgumentError` for `k > N` and for a column whose largest residual magnitude is not finite and positive, is [`get_DEIM_indices`](@ref)'s. + +```jldoctest +julia> using ReducedBasisMethods + +julia> get_DEIM_interpolation_matrix([1 1; 2 -1]) +2×2 Matrix{Float64}: + 0.0 1.0 + 1.0 0.0 +``` """ function get_DEIM_interpolation_matrix(Ψ::AbstractMatrix) N, k = size(Ψ) diff --git a/src/diagnostics.jl b/src/diagnostics.jl new file mode 100644 index 0000000..10052be --- /dev/null +++ b/src/diagnostics.jl @@ -0,0 +1,20 @@ +""" + spectrumplot(σ::AbstractVector{<:Real}; title = "", ylabel = "", size = (620, 380)) + +Draw the spectrum `σ` of a reduced basis against its index on a log-scaled axis, in `Float64`. +The values are drawn as given: singular values, or the squared singular values (Gram eigenvalues) +that a `ReducedBasis` stores in `Λ`. `ylabel` names which, and `size` is the figure size. + +Every value below `eps(Float64) * maximum(σ)` is drawn at that floor, so the exact zeros and the +round-off negatives of a Gram spectrum stay on the axis. A spectrum that holds a `NaN` or an `Inf`, +or no positive value, throws an `ArgumentError`. + +Returns a `Makie.FigureAxisPlot`. Defined in a package extension that loads with `Makie`; `save` +needs a backend such as `CairoMakie`. + +```julia +using ReducedBasisMethods, CairoMakie +save("spectrum.png", spectrumplot(b.Λ; ylabel = "λᵢ")) +``` +""" +function spectrumplot end diff --git a/test/Project.toml b/test/Project.toml index 7226238..45fc48b 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,5 +1,7 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" GeometricBrackets = "2a9ffa85-4e0d-473f-b05e-278d80d8d306" GeometricIntegrators = "dcce2d33-59f6-5b8d-9047-0defad88ae06" @@ -17,6 +19,8 @@ ReducedBasisMethods = {path = ".."} [compat] Aqua = "0.8.18" +CairoMakie = "0.15" +Documenter = "1.19.0" ForwardDiff = "1.4.6" GeometricBrackets = "0.1.1, 0.2" GeometricIntegrators = "0.18.5" diff --git a/test/diagnostics.jl b/test/diagnostics.jl new file mode 100644 index 0000000..aebbb43 --- /dev/null +++ b/test/diagnostics.jl @@ -0,0 +1,48 @@ +using CairoMakie +using ReducedBasisMethods +using Test + +"The plotted values of a `FigureAxisPlot`." +plotted_y(fap) = [q[2] for q in fap.plot.positions[]] + +@testset "Diagnostics" begin + @test Base.get_extension(ReducedBasisMethods, :ReducedBasisMethodsMakieExt) isa Module + + @testset "a spectrum is drawn as given and saved headless ($T)" for T in (Float64, Float32) + σ = T.(10.0 .^ (-(1:10))) + fap = spectrumplot(σ) + @test fap isa CairoMakie.Makie.FigureAxisPlot + @test eltype(eltype(fap.plot.positions[])) == Float64 + @test plotted_y(fap) == σ + + path = joinpath(mktempdir(), "spectrum.png") + save(path, fap) + @test read(path)[1:4] == UInt8[0x89, 0x50, 0x4e, 0x47] + end + + @testset "the axis label and the figure size" begin + @test spectrumplot([1.0]).axis.ylabel[] == "" + fap = spectrumplot([1.0, 0.5]; ylabel = "λᵢ", size = (300, 200)) + @test fap.axis.ylabel[] == "λᵢ" + @test Base.size(fap.figure.scene) == (300, 200) + end + + @testset "a one-element and a BigFloat spectrum" begin + @test plotted_y(spectrumplot([3.0])) == [3.0] + @test plotted_y(spectrumplot(big.([1.0, 0.5, 0.25]))) == [1.0, 0.5, 0.25] + end + + @testset "values below the floor are drawn at the floor ($T)" for T in (Float64, Float32) + # the floor is eps(Float64) * maximum(σ) = eps(Float64) + σ = T[1, 1e-3, 1e-20, 0, -1e-18] + @test plotted_y(spectrumplot(σ)) == [1, Float64(T(1e-3)), eps(), eps(), eps()] + end + + @testset "a spectrum with a NaN, an Inf or no positive value is refused" begin + @test_throws ArgumentError spectrumplot(Float64[]) + @test_throws ArgumentError spectrumplot([1.0, NaN]) + @test_throws ArgumentError spectrumplot([1.0, Inf]) + @test_throws ArgumentError spectrumplot(zeros(3)) + @test_throws ArgumentError spectrumplot([0.0, -1e-17]) + end +end diff --git a/test/integration/load_without_makie.jl b/test/integration/load_without_makie.jl new file mode 100644 index 0000000..75f1e3a --- /dev/null +++ b/test/integration/load_without_makie.jl @@ -0,0 +1,13 @@ +using ReducedBasisMethods +using Test + +# `Makie` is a weak dependency: loading the package, in an environment that has a Makie backend +# installed, loads no Makie module, and `spectrumplot` has no method. +@testset "Load without Makie" begin + code = """ + using ReducedBasisMethods, Test + @test !any(m -> occursin("Makie", m.name), keys(Base.loaded_modules)) + @test_throws MethodError spectrumplot([1.0, 0.5]) + """ + @test success(`$(Base.julia_cmd()) --startup-file=no --project=$(Base.active_project()) -e $code`) +end diff --git a/test/quality/docs.jl b/test/quality/docs.jl new file mode 100644 index 0000000..cbe58ff --- /dev/null +++ b/test/quality/docs.jl @@ -0,0 +1,12 @@ +using ReducedBasisMethods +using Test + +@testset "no physical word in the code or the documentation" begin + # The package knows no model. `CHANGELOG.md` and `reference/` may name one. + root = pkgdir(ReducedBasisMethods) + files = [joinpath(path, file) for dir in ("src", "ext", "docs/src") + for (path, _, names) in walkdir(joinpath(root, dir)) for file in names] + for file in [files; joinpath(root, "docs", "make.jl")] + @test !occursin(r"vlasov|particle"i, read(file, String)) + end +end diff --git a/test/quality/doctests.jl b/test/quality/doctests.jl new file mode 100644 index 0000000..ecb34f5 --- /dev/null +++ b/test/quality/doctests.jl @@ -0,0 +1,5 @@ +using ReducedBasisMethods +using Documenter: doctest + +# The docstring doctests. `manual = false` leaves the manual's doctests to `docs/make.jl`. +doctest(ReducedBasisMethods; manual = false) diff --git a/test/runtests.jl b/test/runtests.jl index fdbc3f8..3f03610 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,6 +4,7 @@ const GROUPS = isempty(ARGS) ? ["core", "slow"] : ARGS if "core" in GROUPS @safetestset "Aqua" include("quality/aqua.jl") + @safetestset "Docs" include("quality/docs.jl") @safetestset "Generic-only skeleton" include("integration/skeleton.jl") @safetestset "Reference spectra script" include("integration/reference_spectra.jl") @safetestset "Reduced basis" include("reducedbasis.jl") @@ -16,3 +17,10 @@ if "core" in GROUPS @safetestset "Symplectic DEIM" include("algorithms/symplectic_deim.jl") @safetestset "Reduced equations" include("reduced_equations.jl") end +if "slow" in GROUPS + @safetestset "Load without Makie" include("integration/load_without_makie.jl") + @safetestset "Diagnostics" include("diagnostics.jl") +end +if "doctests" in GROUPS + @safetestset "Doctests" include("quality/doctests.jl") +end