diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index c29db74..1ef78d0 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -6,7 +6,6 @@ on: jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' - # ubuntu-slim doesn't support containers, and thus won't work runs-on: ubuntu-latest steps: - uses: JuliaRegistries/TagBot@v1.25.11 diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..d1a6bbc --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,27 @@ +name: Benchmark this PR + +on: + pull_request: + branches: [main] + +permissions: + pull-requests: write + contents: read + +jobs: + benchmark: + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + version: ["1"] + env: + # Force consistent Julia depot path for self-hosted runners + JULIA_DEPOT_PATH: ~/.julia + steps: + - uses: MilesCranmer/AirspeedVelocity.jl@action-v1 + with: + julia-version: ${{ matrix.version }} + script: "benchmark/benchmarks.jl" + job-summary: true \ No newline at end of file diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl new file mode 100644 index 0000000..a440740 --- /dev/null +++ b/benchmark/benchmarks.jl @@ -0,0 +1,35 @@ +using BenchmarkTools +using Random, LinearAlgebra +using NearestCorrelationMatrix +using NearestCorrelationMatrix.Internals: symmetrize! + +Random.seed!(0x00c0ffee) + +const SUITE = BenchmarkGroup() + +function rand_negdef(n::Int) + while true + A = 2.0 * rand(Float64, n, n) .- 1.0 + symmetrize!(A) + A[diagind(A)] .= 1.0 + !isposdef(A) && return A + end + return zeros(Float64, 0, 0) +end + +function create_benchmarkable(n, alg; evals, samples, seconds) + return @benchmarkable nearest_cor(A, $alg) evals = evals samples = samples seconds = seconds setup = (A = rand_negdef($n)) +end + +function create_benchmark_group(n::Int; evals = 5, samples = 100, seconds = 60) + grp = BenchmarkGroup() + grp["Nw"] = create_benchmarkable(n, Newton; evals, samples, seconds) + grp["AP"] = create_benchmarkable(n, AlternatingProjections; evals, samples, seconds) + grp["AA"] = create_benchmarkable(n, AcceleratedAP; evals, samples, seconds) + grp["Di"] = create_benchmarkable(n, DirectProjection; evals, samples, seconds) + return grp +end + +SUITE["n=10"] = create_benchmark_group(10; samples = 30) +SUITE["n=100"] = create_benchmark_group(100; samples = 30) +SUITE["n=1000"] = create_benchmark_group(1000; samples = 30, seconds = 180) diff --git a/src/internals/Internals.jl b/src/internals/Internals.jl index 3647df6..93ea117 100644 --- a/src/internals/Internals.jl +++ b/src/internals/Internals.jl @@ -5,6 +5,7 @@ using LinearAlgebra include("common.jl") include("projections.jl") include("checks.jl") +include("gen_cor.jl") include("newton_internals.jl") diff --git a/src/internals/gen_cor.jl b/src/internals/gen_cor.jl new file mode 100644 index 0000000..1070e4d --- /dev/null +++ b/src/internals/gen_cor.jl @@ -0,0 +1,51 @@ +export default_negdef, rand_negdef + +""" + default_negdef([T=Float64]; include_mask=false) + +Returns a 4×4 invalid correlation matrix and an optional mask. +""" +function default_negdef(::Type{T}; include_mask::Bool = false) where {T <: AbstractFloat} + # !WARNING! The data in this generator must never be edited to ensure consistency with + # future releases. + + A = Float64[ + 1.0 -0.2188 -0.79 0.7773 + -0.2188 1.0 0.2559 -0.5977 + -0.79 0.2559 1.0 0.2266 + 0.7773 -0.5977 0.2266 1.0 + ] + + mask = [ + 1 1 0 0 + 1 1 0 0 + 0 0 1 1 + 0 0 1 1 + ] + + X = convert(AbstractMatrix{T}, A) + m = convert(BitMatrix, mask) + + return include_mask ? (X, m) : X +end + +default_negdef(; include_mask::Bool = false) = default_negdef(Float64; include_mask) + +""" + rand_negdef([T=Float64], n; max_attempts=100) + +Generates a random pseudo-correlation matrix that is negative definite. +""" +function rand_negdef(::Type{T}, n::Int; max_attempts::Int = 100) where {T <: AbstractFloat} + attempts = 0 + while attempts < max_attempts + r = 2 * rand(T, n, n) .- one(T) + symmetrize!(r) + r[diagind(r)] .= one(T) + !isposdef(r) && return r + attempts += 1 + end + return Matrix{T}(undef, 0, 0) +end + +rand_negdef(n::Int; max_attempts::Int = 100) = rand_negdef(Float64, n; max_attempts) diff --git a/test/Datasets.jl b/test/Datasets.jl index 24e2060..8afa95c 100644 --- a/test/Datasets.jl +++ b/test/Datasets.jl @@ -5,7 +5,6 @@ using LinearAlgebra import MAT export - default_negdef, bccd16, beyu11, bhwi01, @@ -48,24 +47,6 @@ function vec_to_mat(x::AbstractVector{T}; diag_val::Real = 1) where {T} return A end -""" - default_negdef(T) - -Gets a 4×4 invalid correlation matrix for testing. -""" -function default_negdef(::Type{T}) where {T} - r = [ - 1.0 -0.2188 -0.79 0.7773 - -0.2188 1.0 0.2559 -0.5977 - -0.79 0.2559 1.0 0.2266 - 0.7773 -0.5977 0.2266 1.0 - ] - - return convert(Matrix{T}, r) -end - -default_negdef() = default_negdef(Float64) - """ BCCD16 is a 3250×3250 invalid correlation matrix constructed from data for banks in 27 EU member states. @@ -146,13 +127,13 @@ Source: """ function fing97() A = Float64[ - 1.0 0.18 -0.13 -0.26 0.19 -0.25 -0.12 - 0.18 1.0 0.22 -0.14 0.31 0.16 0.09 - -0.13 0.22 1.0 0.06 -0.08 0.04 0.04 - -0.26 -0.14 0.06 1.0 0.85 0.85 0.85 - 0.19 0.31 -0.08 0.85 1.0 0.85 0.85 - -0.25 0.16 0.04 0.85 0.85 1.0 0.85 - -0.12 0.09 0.04 0.85 0.85 0.85 1.0 + 1.0 0.18 -0.13 -0.26 0.19 -0.25 -0.12 + 0.18 1.0 0.22 -0.14 0.31 0.16 0.09 + -0.13 0.22 1.0 0.06 -0.08 0.04 0.04 + -0.26 -0.14 0.06 1.0 0.85 0.85 0.85 + 0.19 0.31 -0.08 0.85 1.0 0.85 0.85 + -0.25 0.16 0.04 0.85 0.85 1.0 0.85 + -0.12 0.09 0.04 0.85 0.85 0.85 1.0 ] mask = [trues(3, 3) falses(3, 4); falses(4, 3) I(4)] @@ -182,12 +163,12 @@ St. Petersburg, Russia, 2013. Revised June 2014. """ function mmb13() A = Float64[ - 0.010712 0.000654 0.002391 0.010059 -0.008321 0.001738 - 0.000654 0.000004 0.002917 0.00065 0.002263 0.002913 - 0.002391 0.002917 0.013225 -0.000525 0.010834 0.010309 - 0.010059 0.00065 -0.000525 0.009409 -0.010584 -0.001175 - -0.008321 0.002263 0.010834 -0.010584 0.019155 0.008571 - 0.001738 0.002913 0.010309 -0.001175 0.008571 0.007396 + 0.010712 0.000654 0.002391 0.010059 -0.008321 0.001738 + 0.000654 0.000004 0.002917 0.00065 0.002263 0.002913 + 0.002391 0.002917 0.013225 -0.000525 0.010834 0.010309 + 0.010059 0.00065 -0.000525 0.009409 -0.010584 -0.001175 + -0.008321 0.002263 0.010834 -0.010584 0.019155 0.008571 + 0.001738 0.002913 0.010309 -0.001175 0.008571 0.007396 ] d = sqrt.(diag(A)) @@ -206,10 +187,10 @@ stress testing for value-at-risk. Journal of Risk, 5(4):75-89, 2003 """ function tec03() return Float64[ - 1 -0.55 -0.15 -0.1 - -0.55 1 0.9 0.9 - -0.15 0.9 1 0.9 - -0.1 0.9 0.9 1 + 1 -0.55 -0.15 -0.1 + -0.55 1 0.9 0.9 + -0.15 0.9 1 0.9 + -0.1 0.9 0.9 1 ] end diff --git a/test/test_api.jl b/test/test_api.jl index b70d3d6..9682f21 100644 --- a/test/test_api.jl +++ b/test/test_api.jl @@ -1,21 +1,19 @@ using Test using InteractiveUtils using NearestCorrelationMatrix - -include("Datasets.jl") -using .Datasets +using NearestCorrelationMatrix.Internals: default_negdef include("CustomTestMacros.jl") using .CustomTestMacros internal_algtypes = setdiff(subtypes(NCMAlgorithm), (JuMPAlgorithm,)) -r0 = default_negdef(Float64) +A = default_negdef() # variants of NCMProblem @test_isdefined NCMProblem -@test_isimplemented NCMProblem(r0) -prob = NCMProblem(r0) +@test_isimplemented NCMProblem(A) +prob = NCMProblem(A) # variations of init @test_isdefined init diff --git a/test/test_convergence.jl b/test/test_convergence.jl index 1ceea32..6c3c439 100644 --- a/test/test_convergence.jl +++ b/test/test_convergence.jl @@ -2,11 +2,9 @@ using Test using InteractiveUtils using LinearAlgebra using NearestCorrelationMatrix +using NearestCorrelationMatrix.Internals: default_negdef import NearestCorrelationMatrix as NCM -include("Datasets.jl") -using .Datasets - include("CustomTestMacros.jl") using .CustomTestMacros @@ -14,8 +12,8 @@ internal_algtypes = setdiff(subtypes(NCMAlgorithm), (JuMPAlgorithm,)) function test_simple(algtype) return @testset "$(NCM.alg_name(algtype))" begin - r0 = default_negdef(Float64) - prob = NCMProblem(r0) + A = default_negdef() + prob = NCMProblem(A) alg = autotune(algtype, prob) cache = init(prob, alg) sol = solve!(cache) @@ -26,14 +24,14 @@ function test_simple(algtype) @test sol.X isa Symmetric # Handle Symmetric type matrices - r0 = default_negdef(Float64) - prob = NCMProblem(Symmetric(r0)) + A = default_negdef() + prob = NCMProblem(Symmetric(A)) alg = autotune(algtype, prob) @test_nothrow solve(prob, alg) # Handle Float16 input matrices - r0 = default_negdef(Float16) - prob = NCMProblem(r0) + A = default_negdef(Float16) + prob = NCMProblem(A) alg = autotune(algtype, prob) if NCM.supports_float16(alg) @test_nothrow solve(prob, alg) diff --git a/test/test_jump.jl b/test/test_jump.jl index 3fac963..2fbc71c 100644 --- a/test/test_jump.jl +++ b/test/test_jump.jl @@ -1,15 +1,13 @@ using Test using NearestCorrelationMatrix +using NearestCorrelationMatrix.Internals: default_negdef using JuMP, COSMO -include("Datasets.jl") -using .Datasets - include("CustomTestMacros.jl") using .CustomTestMacros -r0 = default_negdef(Float64) -prob = NCMProblem(r0) +A = default_negdef() +prob = NCMProblem(A) @test_isdefined JuMPAlgorithm @test_isimplemented JuMPAlgorithm(COSMO.Optimizer) diff --git a/test/test_masking.jl b/test/test_masking.jl index 845f1cd..6757d1b 100644 --- a/test/test_masking.jl +++ b/test/test_masking.jl @@ -2,11 +2,9 @@ using Test using LinearAlgebra using InteractiveUtils using NearestCorrelationMatrix +using NearestCorrelationMatrix.Internals: default_negdef import NearestCorrelationMatrix as NCM -include("Datasets.jl") -using .Datasets - include("CustomTestMacros.jl") using .CustomTestMacros @@ -15,8 +13,7 @@ supported_types = (Float64, Float32, Float16) for algtype in masking_algs, T in supported_types @testset "$(NCM.alg_name(algtype)) - $T" begin - A, m = usgs13() - A = convert(Matrix{T}, A) + A, m = default_negdef(T; include_mask = true) X = copy(A) prob = NCMProblem(X; mask = m) alg = autotune(algtype, prob) diff --git a/test/test_robustness.jl b/test/test_robustness.jl index 2474ee0..f4fe96c 100644 --- a/test/test_robustness.jl +++ b/test/test_robustness.jl @@ -1,20 +1,17 @@ -function rand_negdef(::Type{T}, n) where {T} - while true - r = 2 * rand(T, n, n) .- one(T) - symmetrize!(r) - r[diagind(r)] .= one(T) - - !isposdef(r) && return r - end - return -end +using Test +using LinearAlgebra +using NearestCorrelationMatrix +using NearestCorrelationMatrix.Internals: rand_negdef +using JuMP, COSMO +include("CustomTestMacros.jl") +using .CustomTestMacros function test_robust_reps(algtype::Type, nreps, size, T, test_pd; kwargs...) return @testset "$(size)×$(size)" begin for _ in 1:nreps - r0 = rand_negdef(T, size) - prob = NCMProblem(r0) + A = rand_negdef(T, size) + prob = NCMProblem(A) alg = autotune(algtype, prob) cache = init(prob, alg; kwargs...) sol = solve!(cache) @@ -31,8 +28,8 @@ end function test_robust_reps(alg::NCMAlgorithm, nreps, size, T, test_pd; kwargs...) return @testset "$(size)×$(size)" begin for _ in 1:nreps - r0 = rand_negdef(T, size) - prob = NCMProblem(r0) + A = rand_negdef(T, size) + prob = NCMProblem(A) cache = init(prob, alg; kwargs...) sol = solve!(cache) diff --git a/test/test_simple_api.jl b/test/test_simple_api.jl index c3e83fb..af517bb 100644 --- a/test/test_simple_api.jl +++ b/test/test_simple_api.jl @@ -1,9 +1,7 @@ using Test using LinearAlgebra using NearestCorrelationMatrix - -include("Datasets.jl") -using .Datasets +using NearestCorrelationMatrix.Internals: default_negdef include("CustomTestMacros.jl") using .CustomTestMacros @@ -11,31 +9,36 @@ using .CustomTestMacros @test_isdefined nearest_cor @test_isdefined nearest_cor! -r = default_negdef(Float64) +A = default_negdef() -@test_isimplemented nearest_cor(r) -@test_isimplemented nearest_cor(r, Newton()) -@test_isimplemented nearest_cor(r, Newton) +@test_isimplemented nearest_cor(A) +@test_isimplemented nearest_cor(A, Newton()) +@test_isimplemented nearest_cor(A, Newton) -@test nearest_cor(r) isa AbstractMatrix +@test nearest_cor(A) isa AbstractMatrix -@test_isimplemented nearest_cor!(r) -@test_isimplemented nearest_cor!(r, Newton()) -@test_isimplemented nearest_cor!(r, Newton) +@test_isimplemented nearest_cor!(A) +@test_isimplemented nearest_cor!(A, Newton()) +@test_isimplemented nearest_cor!(A, Newton) -@test nearest_cor!(r) isa AbstractMatrix +@test nearest_cor!(A) isa AbstractMatrix # not symmetric input -r = rand(4, 4) -@test_nothrow nearest_cor(r) -@test_nothrow nearest_cor!(r) +A = rand(4, 4) +@test_nothrow nearest_cor(A) +@test_nothrow nearest_cor!(A) # Symmetric type input -r = Symmetric(rand(4, 4)) -@test_nothrow nearest_cor(r) -@test_nothrow nearest_cor!(r) +A = Symmetric(rand(4, 4)) +@test_nothrow nearest_cor(A) +@test_nothrow nearest_cor!(A) # Float16 input -r = rand(Float16, 4, 4) -@test_nothrow nearest_cor(r) -@test_nothrow nearest_cor!(r) +A = rand(Float16, 4, 4) +@test_nothrow nearest_cor(A) +@test_nothrow nearest_cor!(A) + +# (#41) uses an algorithm that supports masking when a mask is given +A, m = default_negdef(; include_mask = true) +@test_broken nearest_cor(A; mask = m) +@test_broken nearest_cor!(A; mask = m)