From 4a446ac73f90066e6fb4ee47bdb3787da802ec4f Mon Sep 17 00:00:00 2001 From: Alex Knudson Date: Sun, 13 Sep 2026 09:52:35 -0700 Subject: [PATCH 1/7] Moved rand_negdef and default_negdef to Internals --- src/internals/Internals.jl | 1 + src/internals/gen_cor.jl | 51 ++++++++++++++++++++++++++++++++++++ test/Datasets.jl | 53 ++++++++++++-------------------------- 3 files changed, 69 insertions(+), 36 deletions(-) create mode 100644 src/internals/gen_cor.jl 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..58bd24b --- /dev/null +++ b/src/internals/gen_cor.jl @@ -0,0 +1,51 @@ +export default_negdef, rand_negdef + +""" + default_negdef([T=Float64]) + +Returns a 4×4 invalid correlation matrix and an optional mask. +""" +function default_negdef(::Type{T}) 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 X, m +end + +default_negdef() = default_negdef(Float64) + +""" + 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 From 43fb855ee019520a60a3372088c42e120a3657f6 Mon Sep 17 00:00:00 2001 From: Alex Knudson Date: Sun, 13 Sep 2026 10:04:44 -0700 Subject: [PATCH 2/7] update tests to use the Internals method --- src/internals/gen_cor.jl | 8 ++++---- test/runtests.jl | 1 + test/test_api.jl | 10 ++++------ test/test_convergence.jl | 16 +++++++-------- test/test_jump.jl | 8 +++----- test/test_masking.jl | 7 ++----- test/test_robustness.jl | 25 ++++++++++------------- test/test_simple_api.jl | 43 +++++++++++++++++++++------------------- 8 files changed, 55 insertions(+), 63 deletions(-) diff --git a/src/internals/gen_cor.jl b/src/internals/gen_cor.jl index 58bd24b..1070e4d 100644 --- a/src/internals/gen_cor.jl +++ b/src/internals/gen_cor.jl @@ -1,11 +1,11 @@ export default_negdef, rand_negdef """ - default_negdef([T=Float64]) + default_negdef([T=Float64]; include_mask=false) Returns a 4×4 invalid correlation matrix and an optional mask. """ -function default_negdef(::Type{T}) where {T <: AbstractFloat} +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. @@ -26,10 +26,10 @@ function default_negdef(::Type{T}) where {T <: AbstractFloat} X = convert(AbstractMatrix{T}, A) m = convert(BitMatrix, mask) - return X, m + return include_mask ? (X, m) : X end -default_negdef() = default_negdef(Float64) +default_negdef(; include_mask::Bool = false) = default_negdef(Float64; include_mask) """ rand_negdef([T=Float64], n; max_attempts=100) diff --git a/test/runtests.jl b/test/runtests.jl index b38aa9b..f06978b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ using SafeTestsets +# Register data deps once before all other tests include("datadeps_registration.jl") # Package Quality 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..7f58a4c 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_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_nothrow nearest_cor(A; mask = m) +@test_nothrow nearest_cor!(A; mask = m) From b35b048816a417b6bd7dbd2f9245f87da8dfaf0e Mon Sep 17 00:00:00 2001 From: Alex Knudson Date: Sun, 13 Sep 2026 10:28:42 -0700 Subject: [PATCH 3/7] add simple benchmark and workflow --- .github/workflows/benchmark.yml | 27 +++++++++++++++++++++++++++ benchmark/benchmarks.jl | 25 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 .github/workflows/benchmark.yml create mode 100644 benchmark/benchmarks.jl diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..bcdb740 --- /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", "lts"] + 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..ab9a15c --- /dev/null +++ b/benchmark/benchmarks.jl @@ -0,0 +1,25 @@ +using BenchmarkTools +using Random, LinearAlgebra +using NearestCorrelationMatrix +using NearestCorrelationMatrix.Internals: rand_negdef + +Random.seed!(0x00c0ffee) + +const SUITE = BenchmarkGroup() + +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) From 9f2bb62e40a5353e962d15472e3c8361133e02bd Mon Sep 17 00:00:00 2001 From: Alex Knudson Date: Sun, 13 Sep 2026 10:43:49 -0700 Subject: [PATCH 4/7] marked two tests as broken --- test/test_simple_api.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_simple_api.jl b/test/test_simple_api.jl index 7f58a4c..af517bb 100644 --- a/test/test_simple_api.jl +++ b/test/test_simple_api.jl @@ -15,7 +15,7 @@ A = default_negdef() @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!(A) @test_isimplemented nearest_cor!(A, Newton()) @@ -40,5 +40,5 @@ A = rand(Float16, 4, 4) # (#41) uses an algorithm that supports masking when a mask is given A, m = default_negdef(; include_mask = true) -@test_nothrow nearest_cor(A; mask = m) -@test_nothrow nearest_cor!(A; mask = m) +@test_broken nearest_cor(A; mask = m) +@test_broken nearest_cor!(A; mask = m) From 30d160267631d5e948c2a4b5ca1f3d1e2a537ecf Mon Sep 17 00:00:00 2001 From: Alex Knudson Date: Sun, 13 Sep 2026 11:01:09 -0700 Subject: [PATCH 5/7] properly interpolate parameters --- benchmark/benchmarks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl index ab9a15c..008cac5 100644 --- a/benchmark/benchmarks.jl +++ b/benchmark/benchmarks.jl @@ -8,7 +8,7 @@ Random.seed!(0x00c0ffee) const SUITE = BenchmarkGroup() 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)) + 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) From 2369fd3cb9960c5676f753f5a5e622d0087ecc6d Mon Sep 17 00:00:00 2001 From: Alex Knudson Date: Sun, 13 Sep 2026 11:09:36 -0700 Subject: [PATCH 6/7] rand_negdef copied to benchmark.jl to avoid reaching in to Internals --- benchmark/benchmarks.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl index 008cac5..a440740 100644 --- a/benchmark/benchmarks.jl +++ b/benchmark/benchmarks.jl @@ -1,12 +1,22 @@ using BenchmarkTools using Random, LinearAlgebra using NearestCorrelationMatrix -using NearestCorrelationMatrix.Internals: rand_negdef +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 From 94827b91ae32d524d1eacd72a6877565bac55c63 Mon Sep 17 00:00:00 2001 From: Alex Knudson Date: Sun, 13 Sep 2026 11:28:44 -0700 Subject: [PATCH 7/7] only benchmark on release branch of Julia --- .github/workflows/benchmark.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index bcdb740..d1a6bbc 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - version: ["1", "lts"] + version: ["1"] env: # Force consistent Julia depot path for self-hosted runners JULIA_DEPOT_PATH: ~/.julia