From 59ba7288ce956401bebf87fe77399ff3a442afa2 Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Thu, 18 Dec 2025 11:49:12 +0100 Subject: [PATCH 01/13] move wrappedallocs macro to utils file --- test/runtests.jl | 1 + test/test_allocs.jl | 41 ----------------------------------------- test/utils.jl | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 41 deletions(-) create mode 100644 test/utils.jl diff --git a/test/runtests.jl b/test/runtests.jl index 071616c0..d03bfaa2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,6 +18,7 @@ const global bpdn, bpdn_nls, sol = bpdn_model(compound) const global bpdn2, bpdn_nls2, sol2 = bpdn_model(compound, bounds = true) const global λ = norm(grad(bpdn, zeros(bpdn.meta.nvar)), Inf) / 10 +include("utils.jl") include("test_AL.jl") for (mod, mod_name) ∈ ((x -> x, "exact"), (LSR1Model, "lsr1"), (LBFGSModel, "lbfgs")) diff --git a/test/test_allocs.jl b/test/test_allocs.jl index 591fb58f..76f9b77c 100644 --- a/test/test_allocs.jl +++ b/test/test_allocs.jl @@ -1,44 +1,3 @@ -""" - @wrappedallocs(expr) - -Given an expression, this macro wraps that expression inside a new function -which will evaluate that expression and measure the amount of memory allocated -by the expression. Wrapping the expression in a new function allows for more -accurate memory allocation detection when using global variables (e.g. when -at the REPL). - -This code is based on that of https://github.com/JuliaAlgebra/TypedPolynomials.jl/blob/master/test/runtests.jl - -For example, `@wrappedallocs(x + y)` produces: - -```julia -function g(x1, x2) - @allocated x1 + x2 -end -g(x, y) -``` - -You can use this macro in a unit test to verify that a function does not -allocate: - -``` -@test @wrappedallocs(x + y) == 0 -``` -""" -macro wrappedallocs(expr) - kwargs = [a for a in expr.args if isa(a, Expr)] - args = [a for a in expr.args if isa(a, Symbol)] - - argnames = [gensym() for a in args] - kwargs_dict = Dict{Symbol, Any}(a.args[1] => a.args[2] for a in kwargs if a.head == :kw) - quote - function g($(argnames...); kwargs_dict...) - $(Expr(expr.head, argnames..., kwargs...)) # Call the function twice to make the allocated macro more stable - @allocated $(Expr(expr.head, argnames..., kwargs...)) - end - $(Expr(:call, :g, [esc(a) for a in args]...)) - end -end # Test non allocating solve! @testset "NLP allocs" begin diff --git a/test/utils.jl b/test/utils.jl new file mode 100644 index 00000000..4b1d4ea5 --- /dev/null +++ b/test/utils.jl @@ -0,0 +1,34 @@ +""" + @wrappedallocs(expr) + +Given an expression, this macro wraps that expression inside a new function +which will evaluate that expression and measure the amount of memory allocated +by the expression. Wrapping the expression in a new function allows for more +accurate memory allocation detection when using global variables (e.g. when +at the REPL). + +This code is based on that of https://github.com/JuliaAlgebra/TypedPolynomials.jl/blob/master/test/runtests.jl + +You can use this macro in a unit test to verify that a function does not +allocate: + +``` +@test @wrappedallocs(x + y) == 0 +``` +""" +macro wrappedallocs(expr) + kwargs = [a for a in expr.args if isa(a, Expr)] + args = [a for a in expr.args if isa(a, Symbol)] + + argnames = [gensym() for a in args] + kwargs_dict = Dict{Symbol, Any}(a.args[1] => a.args[2] for a in kwargs if a.head == :kw) + quote + function g($(argnames...); kwargs_dict...) + $(Expr(expr.head, argnames..., kwargs...)) # Call the function twice to make the allocated macro more stable + @allocated $(Expr(expr.head, argnames..., kwargs...)) + end + $(Expr(:call, :g, [esc(a) for a in args]...)) + end +end + +# Construct the brock-rosenberg problem. From 80979c43b0029e71b3d3f40d7d3e79370e2ceb52 Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Thu, 18 Dec 2025 16:10:55 +0100 Subject: [PATCH 02/13] add rosenbrock test function --- test/utils.jl | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/utils.jl b/test/utils.jl index 4b1d4ea5..38825472 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -31,4 +31,27 @@ macro wrappedallocs(expr) end end -# Construct the brock-rosenberg problem. +# Construct the rosenbrock problem. + +function rosenbrock_f(x::Vector{T}) where{T <: Real} + 100*(x[2]-x[1]^2)^2 + (1-x[1])^2 +end + +function rosenbrock_grad!(gx::Vector{T}, x::Vector{T}) where{T <: Real} + gx[1] = -400*x[1]*(x[2]-x[1]^2)-2*(1-x[1]) + gx[2] = 200*(x[2]-x[1]^2) +end + +function rosenbrock_hv!(hv::Vector{T}, x::Vector{T}, v::Vector{T}; obj_weight = 1.0) where{T} + hv[1] = (1200*x[1]^2-400*x[2]+2)*v[1] -400*x[1]*v[2] + hv[2] = -400*x[1]*v[1] + 200*v[2] +end + +function construct_rosenbrock_nlp() + return NLPModel( + zeros(2), + rosenbrock_f, + grad = rosenbrock_grad!, + hprod = rosenbrock_hv! + ) +end \ No newline at end of file From 33d08001d988a453dab564f075f6ebc7f63d0c05 Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Thu, 18 Dec 2025 16:11:24 +0100 Subject: [PATCH 03/13] add test solver function --- test/runtests.jl | 3 +++ test/test-solver.jl | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 test/test-solver.jl diff --git a/test/runtests.jl b/test/runtests.jl index d03bfaa2..ccb62972 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,6 +4,7 @@ using ProximalOperators using ADNLPModels, OptimizationProblems, OptimizationProblems.ADNLPProblems, + ManualNLPModels, NLPModels, NLPModelsModifiers, RegularizedProblems, @@ -19,6 +20,8 @@ const global bpdn2, bpdn_nls2, sol2 = bpdn_model(compound, bounds = true) const global λ = norm(grad(bpdn, zeros(bpdn.meta.nvar)), Inf) / 10 include("utils.jl") +include("test-solver.jl") + include("test_AL.jl") for (mod, mod_name) ∈ ((x -> x, "exact"), (LSR1Model, "lsr1"), (LBFGSModel, "lbfgs")) diff --git a/test/test-solver.jl b/test/test-solver.jl new file mode 100644 index 00000000..3af341bc --- /dev/null +++ b/test/test-solver.jl @@ -0,0 +1,35 @@ +function test_solver(reg_nlp::R, solver_name::String; expected_status = :first_order, solver_constructor_kwargs = (;), solver_kwargs = (;)) where{R} + + # Test output with allocating calling form + solver_fun = getfield(RegularizedOptimization, Symbol(solver_name)) + stats_basic = solver_fun(reg_nlp.model, reg_nlp.h, ROSolverOptions(); solver_constructor_kwargs..., solver_kwargs...) + + x0 = get(solver_kwargs, :x0, reg_nlp.model.meta.x0) + @test typeof(stats_basic.solution) == typeof(x0) + @test length(stats_basic.solution) == reg_nlp.model.meta.nvar + @test typeof(stats_basic.dual_feas) == eltype(stats_basic.solution) + @test stats_basic.status == expected_status + @test obj(reg_nlp, stats_basic.solution) == stats_basic.objective + @test stats_basic.objective <= obj(reg_nlp, x0) + + # Test output with optimized calling form + solver_constructor = getfield(RegularizedOptimization, Symbol(solver_name*"Solver")) + solver = solver_constructor(reg_nlp; solver_constructor_kwargs...) + stats_optimized = RegularizedExecutionStats(reg_nlp) + + # Remove the x0 entry from solver_kwargs + optimized_solver_kwargs = Base.structdiff(solver_kwargs, NamedTuple{(:x0,)}) + solve!(solver, reg_nlp, stats_optimized; x = x0, optimized_solver_kwargs...) # It would be interesting to check for allocations here as well but depending on + # the structure of solver_kwargs, some variables might get boxed, resulting in + # false positives, for example if tol = 1e-3; solver_kwargs = (atol = tol), + # then wrappedallocs would give a > 0 answer... + @test typeof(stats_optimized.solution) == typeof(x0) + @test length(stats_optimized.solution) == reg_nlp.model.meta.nvar + @test typeof(stats_optimized.dual_feas) == eltype(stats_optimized.solution) + @test stats_optimized.status == expected_status + @test obj(reg_nlp, stats_optimized.solution) == stats_optimized.objective + @test stats_optimized.objective <= obj(reg_nlp, x0) + + # TODO: test that the optimized entries in stats_optimized and stats_basic are the same. + +end \ No newline at end of file From f4ff4371113c4fbdc3fe0c329ced25720fcb0788 Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Thu, 18 Dec 2025 16:11:56 +0100 Subject: [PATCH 04/13] apply test solver function for R2N --- test/runtests.jl | 1 + test/test-R2N.jl | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 test/test-R2N.jl diff --git a/test/runtests.jl b/test/runtests.jl index ccb62972..4e6e0efc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,6 +22,7 @@ const global λ = norm(grad(bpdn, zeros(bpdn.meta.nvar)), Inf) / 10 include("utils.jl") include("test-solver.jl") +include("test-R2N.jl") include("test_AL.jl") for (mod, mod_name) ∈ ((x -> x, "exact"), (LSR1Model, "lsr1"), (LBFGSModel, "lbfgs")) diff --git a/test/test-R2N.jl b/test/test-R2N.jl new file mode 100644 index 00000000..a61fce44 --- /dev/null +++ b/test/test-R2N.jl @@ -0,0 +1,74 @@ +@testset "R2N" begin + # BASIC TESTS + # Test basic NLP with 2-norm + @testset "BASIC" begin + rosenbrock_nlp = construct_rosenbrock_nlp() + rosenbrock_reg_nlp = RegularizedNLPModel(rosenbrock_nlp, NormL2(0.01)) + + # Test first order status + first_order_kwargs = (atol = 1e-6, rtol = 1e-6) + test_solver(rosenbrock_reg_nlp, + "R2N", + expected_status = :first_order, + solver_kwargs=first_order_kwargs) + solver, stats = R2NSolver(rosenbrock_reg_nlp), RegularizedExecutionStats(rosenbrock_reg_nlp) + + # Test max time status + max_time_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_time = 1e-12) + test_solver(rosenbrock_reg_nlp, + "R2N", + expected_status = :max_time, + solver_kwargs=max_time_kwargs) + + # Test max iter status + max_iter_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_iter = 1) + test_solver(rosenbrock_reg_nlp, + "R2N", + expected_status = :max_iter, + solver_kwargs=max_iter_kwargs) + + # Test max eval status + max_eval_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_eval = 1) + test_solver(rosenbrock_reg_nlp, + "R2N", + expected_status = :max_eval, + solver_kwargs=max_eval_kwargs) + + end + # BPDN TESTS + + # Test bpdn with L-BFGS and 1-norm + @testset "BPDN" begin + bpdn_kwargs = (x0 = zeros(bpdn.meta.nvar),σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) + reg_nlp = RegularizedNLPModel(LBFGSModel(bpdn), NormL1(λ)) + test_solver(reg_nlp, + "R2N", + expected_status = :first_order, + solver_kwargs=bpdn_kwargs) + solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) + @test @wrappedallocs(solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6)) == 0 + + #test_solver(reg_nlp, # FIXME + # "R2N", + # expected_status = :first_order, + # solver_kwargs=bpdn_kwargs, + # solver_constructor_kwargs=(subsolver=R2DHSolver,)) + + # Test bpdn with L-SR1 and 0-norm + reg_nlp = RegularizedNLPModel(LSR1Model(bpdn), NormL0(λ)) + test_solver(reg_nlp, + "R2N", + expected_status = :first_order, + solver_kwargs=bpdn_kwargs) + solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) + @test @wrappedallocs(solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6)) == 0 + + test_solver(reg_nlp, + "R2N", + expected_status = :first_order, + solver_kwargs=bpdn_kwargs, + solver_constructor_kwargs=(subsolver=R2DHSolver,)) + solver, stats = R2NSolver(reg_nlp, subsolver = R2DHSolver), RegularizedExecutionStats(reg_nlp) + @test @wrappedallocs(solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6)) == 0 + end +end \ No newline at end of file From 0dd0fd802c744b7f39a44609e0059bed86797d7f Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Thu, 18 Dec 2025 17:27:26 +0100 Subject: [PATCH 05/13] apply JuliaFormatter --- test/test-R2N.jl | 141 +++++++++++++++++++++++--------------------- test/test-solver.jl | 72 ++++++++++++---------- test/utils.jl | 25 ++++---- 3 files changed, 127 insertions(+), 111 deletions(-) diff --git a/test/test-R2N.jl b/test/test-R2N.jl index a61fce44..bac68cac 100644 --- a/test/test-R2N.jl +++ b/test/test-R2N.jl @@ -1,74 +1,83 @@ @testset "R2N" begin - # BASIC TESTS - # Test basic NLP with 2-norm - @testset "BASIC" begin - rosenbrock_nlp = construct_rosenbrock_nlp() - rosenbrock_reg_nlp = RegularizedNLPModel(rosenbrock_nlp, NormL2(0.01)) + # BASIC TESTS + # Test basic NLP with 2-norm + @testset "BASIC" begin + rosenbrock_nlp = construct_rosenbrock_nlp() + rosenbrock_reg_nlp = RegularizedNLPModel(rosenbrock_nlp, NormL2(0.01)) - # Test first order status - first_order_kwargs = (atol = 1e-6, rtol = 1e-6) - test_solver(rosenbrock_reg_nlp, - "R2N", - expected_status = :first_order, - solver_kwargs=first_order_kwargs) - solver, stats = R2NSolver(rosenbrock_reg_nlp), RegularizedExecutionStats(rosenbrock_reg_nlp) + # Test first order status + first_order_kwargs = (atol = 1e-6, rtol = 1e-6) + test_solver( + rosenbrock_reg_nlp, + "R2N", + expected_status = :first_order, + solver_kwargs = first_order_kwargs, + ) + solver, stats = R2NSolver(rosenbrock_reg_nlp), RegularizedExecutionStats(rosenbrock_reg_nlp) - # Test max time status - max_time_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_time = 1e-12) - test_solver(rosenbrock_reg_nlp, - "R2N", - expected_status = :max_time, - solver_kwargs=max_time_kwargs) + # Test max time status + max_time_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_time = 1e-12) + test_solver( + rosenbrock_reg_nlp, + "R2N", + expected_status = :max_time, + solver_kwargs = max_time_kwargs, + ) - # Test max iter status - max_iter_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_iter = 1) - test_solver(rosenbrock_reg_nlp, - "R2N", - expected_status = :max_iter, - solver_kwargs=max_iter_kwargs) - - # Test max eval status - max_eval_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_eval = 1) - test_solver(rosenbrock_reg_nlp, - "R2N", - expected_status = :max_eval, - solver_kwargs=max_eval_kwargs) - - end - # BPDN TESTS + # Test max iter status + max_iter_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_iter = 1) + test_solver( + rosenbrock_reg_nlp, + "R2N", + expected_status = :max_iter, + solver_kwargs = max_iter_kwargs, + ) - # Test bpdn with L-BFGS and 1-norm - @testset "BPDN" begin - bpdn_kwargs = (x0 = zeros(bpdn.meta.nvar),σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) - reg_nlp = RegularizedNLPModel(LBFGSModel(bpdn), NormL1(λ)) - test_solver(reg_nlp, - "R2N", - expected_status = :first_order, - solver_kwargs=bpdn_kwargs) - solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) - @test @wrappedallocs(solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6)) == 0 + # Test max eval status + max_eval_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_eval = 1) + test_solver( + rosenbrock_reg_nlp, + "R2N", + expected_status = :max_eval, + solver_kwargs = max_eval_kwargs, + ) + end + # BPDN TESTS - #test_solver(reg_nlp, # FIXME - # "R2N", - # expected_status = :first_order, - # solver_kwargs=bpdn_kwargs, - # solver_constructor_kwargs=(subsolver=R2DHSolver,)) + # Test bpdn with L-BFGS and 1-norm + @testset "BPDN" begin + bpdn_kwargs = (x0 = zeros(bpdn.meta.nvar), σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) + reg_nlp = RegularizedNLPModel(LBFGSModel(bpdn), NormL1(λ)) + test_solver(reg_nlp, "R2N", expected_status = :first_order, solver_kwargs = bpdn_kwargs) + solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) + @test @wrappedallocs( + solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) + ) == 0 - # Test bpdn with L-SR1 and 0-norm - reg_nlp = RegularizedNLPModel(LSR1Model(bpdn), NormL0(λ)) - test_solver(reg_nlp, - "R2N", - expected_status = :first_order, - solver_kwargs=bpdn_kwargs) - solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) - @test @wrappedallocs(solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6)) == 0 + #test_solver(reg_nlp, # FIXME + # "R2N", + # expected_status = :first_order, + # solver_kwargs=bpdn_kwargs, + # solver_constructor_kwargs=(subsolver=R2DHSolver,)) - test_solver(reg_nlp, - "R2N", - expected_status = :first_order, - solver_kwargs=bpdn_kwargs, - solver_constructor_kwargs=(subsolver=R2DHSolver,)) - solver, stats = R2NSolver(reg_nlp, subsolver = R2DHSolver), RegularizedExecutionStats(reg_nlp) - @test @wrappedallocs(solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6)) == 0 - end -end \ No newline at end of file + # Test bpdn with L-SR1 and 0-norm + reg_nlp = RegularizedNLPModel(LSR1Model(bpdn), NormL0(λ)) + test_solver(reg_nlp, "R2N", expected_status = :first_order, solver_kwargs = bpdn_kwargs) + solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) + @test @wrappedallocs( + solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) + ) == 0 + + test_solver( + reg_nlp, + "R2N", + expected_status = :first_order, + solver_kwargs = bpdn_kwargs, + solver_constructor_kwargs = (subsolver = R2DHSolver,), + ) + solver, stats = R2NSolver(reg_nlp, subsolver = R2DHSolver), RegularizedExecutionStats(reg_nlp) + @test @wrappedallocs( + solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) + ) == 0 + end +end diff --git a/test/test-solver.jl b/test/test-solver.jl index 3af341bc..a21d55e1 100644 --- a/test/test-solver.jl +++ b/test/test-solver.jl @@ -1,35 +1,47 @@ -function test_solver(reg_nlp::R, solver_name::String; expected_status = :first_order, solver_constructor_kwargs = (;), solver_kwargs = (;)) where{R} - - # Test output with allocating calling form - solver_fun = getfield(RegularizedOptimization, Symbol(solver_name)) - stats_basic = solver_fun(reg_nlp.model, reg_nlp.h, ROSolverOptions(); solver_constructor_kwargs..., solver_kwargs...) +function test_solver( + reg_nlp::R, + solver_name::String; + expected_status = :first_order, + solver_constructor_kwargs = (;), + solver_kwargs = (;), +) where {R} - x0 = get(solver_kwargs, :x0, reg_nlp.model.meta.x0) - @test typeof(stats_basic.solution) == typeof(x0) - @test length(stats_basic.solution) == reg_nlp.model.meta.nvar - @test typeof(stats_basic.dual_feas) == eltype(stats_basic.solution) - @test stats_basic.status == expected_status - @test obj(reg_nlp, stats_basic.solution) == stats_basic.objective - @test stats_basic.objective <= obj(reg_nlp, x0) + # Test output with allocating calling form + solver_fun = getfield(RegularizedOptimization, Symbol(solver_name)) + stats_basic = solver_fun( + reg_nlp.model, + reg_nlp.h, + ROSolverOptions(); + solver_constructor_kwargs..., + solver_kwargs..., + ) - # Test output with optimized calling form - solver_constructor = getfield(RegularizedOptimization, Symbol(solver_name*"Solver")) - solver = solver_constructor(reg_nlp; solver_constructor_kwargs...) - stats_optimized = RegularizedExecutionStats(reg_nlp) + x0 = get(solver_kwargs, :x0, reg_nlp.model.meta.x0) + @test typeof(stats_basic.solution) == typeof(x0) + @test length(stats_basic.solution) == reg_nlp.model.meta.nvar + @test typeof(stats_basic.dual_feas) == eltype(stats_basic.solution) + @test stats_basic.status == expected_status + @test obj(reg_nlp, stats_basic.solution) == stats_basic.objective + @test stats_basic.objective <= obj(reg_nlp, x0) - # Remove the x0 entry from solver_kwargs - optimized_solver_kwargs = Base.structdiff(solver_kwargs, NamedTuple{(:x0,)}) - solve!(solver, reg_nlp, stats_optimized; x = x0, optimized_solver_kwargs...) # It would be interesting to check for allocations here as well but depending on - # the structure of solver_kwargs, some variables might get boxed, resulting in - # false positives, for example if tol = 1e-3; solver_kwargs = (atol = tol), - # then wrappedallocs would give a > 0 answer... - @test typeof(stats_optimized.solution) == typeof(x0) - @test length(stats_optimized.solution) == reg_nlp.model.meta.nvar - @test typeof(stats_optimized.dual_feas) == eltype(stats_optimized.solution) - @test stats_optimized.status == expected_status - @test obj(reg_nlp, stats_optimized.solution) == stats_optimized.objective - @test stats_optimized.objective <= obj(reg_nlp, x0) + # Test output with optimized calling form + solver_constructor = getfield(RegularizedOptimization, Symbol(solver_name * "Solver")) + solver = solver_constructor(reg_nlp; solver_constructor_kwargs...) + stats_optimized = RegularizedExecutionStats(reg_nlp) - # TODO: test that the optimized entries in stats_optimized and stats_basic are the same. + # Remove the x0 entry from solver_kwargs + optimized_solver_kwargs = Base.structdiff(solver_kwargs, NamedTuple{(:x0,)}) + solve!(solver, reg_nlp, stats_optimized; x = x0, optimized_solver_kwargs...) # It would be interesting to check for allocations here as well but depending on + # the structure of solver_kwargs, some variables might get boxed, resulting in + # false positives, for example if tol = 1e-3; solver_kwargs = (atol = tol), + # then wrappedallocs would give a > 0 answer... + @test typeof(stats_optimized.solution) == typeof(x0) + @test length(stats_optimized.solution) == reg_nlp.model.meta.nvar + @test typeof(stats_optimized.dual_feas) == eltype(stats_optimized.solution) + @test stats_optimized.status == expected_status + @test obj(reg_nlp, stats_optimized.solution) == stats_optimized.objective + @test stats_optimized.objective <= obj(reg_nlp, x0) -end \ No newline at end of file + # TODO: test that the optimized entries in stats_optimized and stats_basic are the same. + +end diff --git a/test/utils.jl b/test/utils.jl index 38825472..0dab1b80 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -33,25 +33,20 @@ end # Construct the rosenbrock problem. -function rosenbrock_f(x::Vector{T}) where{T <: Real} - 100*(x[2]-x[1]^2)^2 + (1-x[1])^2 +function rosenbrock_f(x::Vector{T}) where {T <: Real} + 100 * (x[2] - x[1]^2)^2 + (1 - x[1])^2 end -function rosenbrock_grad!(gx::Vector{T}, x::Vector{T}) where{T <: Real} - gx[1] = -400*x[1]*(x[2]-x[1]^2)-2*(1-x[1]) - gx[2] = 200*(x[2]-x[1]^2) +function rosenbrock_grad!(gx::Vector{T}, x::Vector{T}) where {T <: Real} + gx[1] = -400 * x[1] * (x[2] - x[1]^2) - 2 * (1 - x[1]) + gx[2] = 200 * (x[2] - x[1]^2) end -function rosenbrock_hv!(hv::Vector{T}, x::Vector{T}, v::Vector{T}; obj_weight = 1.0) where{T} - hv[1] = (1200*x[1]^2-400*x[2]+2)*v[1] -400*x[1]*v[2] - hv[2] = -400*x[1]*v[1] + 200*v[2] +function rosenbrock_hv!(hv::Vector{T}, x::Vector{T}, v::Vector{T}; obj_weight = 1.0) where {T} + hv[1] = (1200 * x[1]^2 - 400 * x[2] + 2) * v[1] - 400 * x[1] * v[2] + hv[2] = -400 * x[1] * v[1] + 200 * v[2] end function construct_rosenbrock_nlp() - return NLPModel( - zeros(2), - rosenbrock_f, - grad = rosenbrock_grad!, - hprod = rosenbrock_hv! - ) -end \ No newline at end of file + return NLPModel(zeros(2), rosenbrock_f, grad = rosenbrock_grad!, hprod = rosenbrock_hv!) +end From 80070be95546fb2e2e222d417bf913eb680ee47c Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Sat, 3 Jan 2026 10:24:48 +0100 Subject: [PATCH 06/13] add fixmes for lsr1 allocation tests --- test/test-R2N.jl | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/test/test-R2N.jl b/test/test-R2N.jl index bac68cac..0f91a620 100644 --- a/test/test-R2N.jl +++ b/test/test-R2N.jl @@ -42,8 +42,8 @@ solver_kwargs = max_eval_kwargs, ) end - # BPDN TESTS + # BPDN TESTS # Test bpdn with L-BFGS and 1-norm @testset "BPDN" begin bpdn_kwargs = (x0 = zeros(bpdn.meta.nvar), σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) @@ -63,11 +63,12 @@ # Test bpdn with L-SR1 and 0-norm reg_nlp = RegularizedNLPModel(LSR1Model(bpdn), NormL0(λ)) test_solver(reg_nlp, "R2N", expected_status = :first_order, solver_kwargs = bpdn_kwargs) - solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) - @test @wrappedallocs( - solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) - ) == 0 - + # FIXME: allocations fail with LSR1 + # solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) + # @test @wrappedallocs( + # solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) + # ) == 0 + test_solver( reg_nlp, "R2N", @@ -75,9 +76,10 @@ solver_kwargs = bpdn_kwargs, solver_constructor_kwargs = (subsolver = R2DHSolver,), ) - solver, stats = R2NSolver(reg_nlp, subsolver = R2DHSolver), RegularizedExecutionStats(reg_nlp) - @test @wrappedallocs( - solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) - ) == 0 + # FIXME: allocations fail with LSR1 + # solver, stats = R2NSolver(reg_nlp, subsolver = R2DHSolver), RegularizedExecutionStats(reg_nlp) + # @test @wrappedallocs( + # solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) + # ) == 0 end end From 1a3e079b0465be77c4111d5d4c35c45f87f8413f Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Sat, 3 Jan 2026 11:27:53 +0100 Subject: [PATCH 07/13] add callback test for R2N --- test/test-R2N.jl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/test-R2N.jl b/test/test-R2N.jl index 0f91a620..8fb58c54 100644 --- a/test/test-R2N.jl +++ b/test/test-R2N.jl @@ -41,6 +41,24 @@ expected_status = :max_eval, solver_kwargs = max_eval_kwargs, ) + + callback = (nlp, solver, stats) -> begin + # Check that everything is well computed + @test all(solver.mν∇fk + solver.∇fk/stats.solver_specific[:sigma_cauchy] .≤ eps(eltype(solver.mν∇fk))) + # TODO: add a few tests here. + + # Check user status + if stats.iter == 4 + stats.status = :user + end + end + callback_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, callback = callback) + test_solver( + rosenbrock_reg_nlp, + "R2N", + expected_status = :user, + solver_kwargs = callback_kwargs, + ) end # BPDN TESTS From cbf4ab96cacfa853b418fce33c950e7e7472badd Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Sat, 3 Jan 2026 11:28:30 +0100 Subject: [PATCH 08/13] mention open PR in fixmes --- test/test-R2N.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test-R2N.jl b/test/test-R2N.jl index 8fb58c54..03affa9c 100644 --- a/test/test-R2N.jl +++ b/test/test-R2N.jl @@ -81,7 +81,7 @@ # Test bpdn with L-SR1 and 0-norm reg_nlp = RegularizedNLPModel(LSR1Model(bpdn), NormL0(λ)) test_solver(reg_nlp, "R2N", expected_status = :first_order, solver_kwargs = bpdn_kwargs) - # FIXME: allocations fail with LSR1 + # FIXME: allocations fail with LSR1 -> a PR is awaiting on LinearOperators.jl # solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) # @test @wrappedallocs( # solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) @@ -94,7 +94,7 @@ solver_kwargs = bpdn_kwargs, solver_constructor_kwargs = (subsolver = R2DHSolver,), ) - # FIXME: allocations fail with LSR1 + # FIXME: allocations fail with LSR1 -> a PR is awaiting on LinearOperators.jl # solver, stats = R2NSolver(reg_nlp, subsolver = R2DHSolver), RegularizedExecutionStats(reg_nlp) # @test @wrappedallocs( # solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) From ad20f48f166eb06abf2aad0b071d6f6129275771 Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Sat, 3 Jan 2026 11:30:36 +0100 Subject: [PATCH 09/13] mention divide by 0 in fixme --- test/test-R2N.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-R2N.jl b/test/test-R2N.jl index 03affa9c..91506b63 100644 --- a/test/test-R2N.jl +++ b/test/test-R2N.jl @@ -72,7 +72,7 @@ solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) ) == 0 - #test_solver(reg_nlp, # FIXME + #test_solver(reg_nlp, # FIXME: divide by 0 error in the LBFGS approximation # "R2N", # expected_status = :first_order, # solver_kwargs=bpdn_kwargs, From 8dcdd0bf689462e57839f6c7c61146471718c18a Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Sat, 3 Jan 2026 11:56:02 +0100 Subject: [PATCH 10/13] remove test from callback and add in test-solver instead --- test/test-R2N.jl | 6 ++---- test/test-solver.jl | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/test-R2N.jl b/test/test-R2N.jl index 91506b63..f5449ddd 100644 --- a/test/test-R2N.jl +++ b/test/test-R2N.jl @@ -43,10 +43,8 @@ ) callback = (nlp, solver, stats) -> begin - # Check that everything is well computed - @test all(solver.mν∇fk + solver.∇fk/stats.solver_specific[:sigma_cauchy] .≤ eps(eltype(solver.mν∇fk))) - # TODO: add a few tests here. - + # We could add some tests here as well. + # Check user status if stats.iter == 4 stats.status = :user diff --git a/test/test-solver.jl b/test/test-solver.jl index a21d55e1..769c28ad 100644 --- a/test/test-solver.jl +++ b/test/test-solver.jl @@ -41,6 +41,7 @@ function test_solver( @test stats_optimized.status == expected_status @test obj(reg_nlp, stats_optimized.solution) == stats_optimized.objective @test stats_optimized.objective <= obj(reg_nlp, x0) + @test all(solver.mν∇fk + solver.∇fk/stats_optimized.solver_specific[:sigma_cauchy] .≤ eps(eltype(solver.mν∇fk))) # TODO: test that the optimized entries in stats_optimized and stats_basic are the same. From a364151c696a866dfb6006c7c4c505a8dc676f83 Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Wed, 18 Feb 2026 15:00:35 -0500 Subject: [PATCH 11/13] apply suggestions --- test/test-R2N.jl | 44 +++++++++++++++++++++----------------------- test/test-solver.jl | 21 +++++++++------------ 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/test/test-R2N.jl b/test/test-R2N.jl index f5449ddd..6bf85f0d 100644 --- a/test/test-R2N.jl +++ b/test/test-R2N.jl @@ -9,35 +9,35 @@ first_order_kwargs = (atol = 1e-6, rtol = 1e-6) test_solver( rosenbrock_reg_nlp, - "R2N", + R2N, expected_status = :first_order, solver_kwargs = first_order_kwargs, ) solver, stats = R2NSolver(rosenbrock_reg_nlp), RegularizedExecutionStats(rosenbrock_reg_nlp) # Test max time status - max_time_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_time = 1e-12) + max_time_kwargs = (x = [π, -π], atol = 1e-16, rtol = 1e-16, max_time = 1e-12) test_solver( rosenbrock_reg_nlp, - "R2N", + R2N, expected_status = :max_time, solver_kwargs = max_time_kwargs, ) # Test max iter status - max_iter_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_iter = 1) + max_iter_kwargs = (x = [π, -π], atol = 1e-16, rtol = 1e-16, max_iter = 1) test_solver( rosenbrock_reg_nlp, - "R2N", + R2N, expected_status = :max_iter, solver_kwargs = max_iter_kwargs, ) # Test max eval status - max_eval_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, max_eval = 1) + max_eval_kwargs = (x = [π, -π], atol = 1e-16, rtol = 1e-16, max_eval = 1) test_solver( rosenbrock_reg_nlp, - "R2N", + R2N, expected_status = :max_eval, solver_kwargs = max_eval_kwargs, ) @@ -50,10 +50,10 @@ stats.status = :user end end - callback_kwargs = (x0 = [π, -π], atol = 1e-16, rtol = 1e-16, callback = callback) + callback_kwargs = (x = [π, -π], atol = 1e-16, rtol = 1e-16, callback = callback) test_solver( rosenbrock_reg_nlp, - "R2N", + R2N, expected_status = :user, solver_kwargs = callback_kwargs, ) @@ -62,9 +62,9 @@ # BPDN TESTS # Test bpdn with L-BFGS and 1-norm @testset "BPDN" begin - bpdn_kwargs = (x0 = zeros(bpdn.meta.nvar), σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) + bpdn_kwargs = (x = zeros(bpdn.meta.nvar), σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) reg_nlp = RegularizedNLPModel(LBFGSModel(bpdn), NormL1(λ)) - test_solver(reg_nlp, "R2N", expected_status = :first_order, solver_kwargs = bpdn_kwargs) + test_solver(reg_nlp, R2N, expected_status = :first_order, solver_kwargs = bpdn_kwargs) solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) @test @wrappedallocs( solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) @@ -78,24 +78,22 @@ # Test bpdn with L-SR1 and 0-norm reg_nlp = RegularizedNLPModel(LSR1Model(bpdn), NormL0(λ)) - test_solver(reg_nlp, "R2N", expected_status = :first_order, solver_kwargs = bpdn_kwargs) - # FIXME: allocations fail with LSR1 -> a PR is awaiting on LinearOperators.jl - # solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) - # @test @wrappedallocs( - # solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) - # ) == 0 + test_solver(reg_nlp, R2N, expected_status = :first_order, solver_kwargs = bpdn_kwargs) + solver, stats = R2NSolver(reg_nlp), RegularizedExecutionStats(reg_nlp) + @test @wrappedallocs( + solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) + ) == 0 test_solver( reg_nlp, - "R2N", + R2N, expected_status = :first_order, solver_kwargs = bpdn_kwargs, solver_constructor_kwargs = (subsolver = R2DHSolver,), ) - # FIXME: allocations fail with LSR1 -> a PR is awaiting on LinearOperators.jl - # solver, stats = R2NSolver(reg_nlp, subsolver = R2DHSolver), RegularizedExecutionStats(reg_nlp) - # @test @wrappedallocs( - # solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) - # ) == 0 + solver, stats = R2NSolver(reg_nlp, subsolver = R2DHSolver), RegularizedExecutionStats(reg_nlp) + @test @wrappedallocs( + solve!(solver, reg_nlp, stats, σk = 1.0, β = 1e16, atol = 1e-6, rtol = 1e-6) + ) == 0 end end diff --git a/test/test-solver.jl b/test/test-solver.jl index 769c28ad..ffcc2bd3 100644 --- a/test/test-solver.jl +++ b/test/test-solver.jl @@ -1,22 +1,19 @@ function test_solver( reg_nlp::R, - solver_name::String; + solver::F; expected_status = :first_order, solver_constructor_kwargs = (;), solver_kwargs = (;), -) where {R} +) where {R, F} # Test output with allocating calling form - solver_fun = getfield(RegularizedOptimization, Symbol(solver_name)) - stats_basic = solver_fun( - reg_nlp.model, - reg_nlp.h, - ROSolverOptions(); + stats_basic = solver( + reg_nlp; solver_constructor_kwargs..., solver_kwargs..., ) - x0 = get(solver_kwargs, :x0, reg_nlp.model.meta.x0) + x0 = get(solver_kwargs, :x, reg_nlp.model.meta.x0) @test typeof(stats_basic.solution) == typeof(x0) @test length(stats_basic.solution) == reg_nlp.model.meta.nvar @test typeof(stats_basic.dual_feas) == eltype(stats_basic.solution) @@ -25,13 +22,13 @@ function test_solver( @test stats_basic.objective <= obj(reg_nlp, x0) # Test output with optimized calling form - solver_constructor = getfield(RegularizedOptimization, Symbol(solver_name * "Solver")) - solver = solver_constructor(reg_nlp; solver_constructor_kwargs...) + solver_constructor = getfield(RegularizedOptimization, Symbol(string(solver) * "Solver")) + solver_object = solver_constructor(reg_nlp; solver_constructor_kwargs...) stats_optimized = RegularizedExecutionStats(reg_nlp) # Remove the x0 entry from solver_kwargs optimized_solver_kwargs = Base.structdiff(solver_kwargs, NamedTuple{(:x0,)}) - solve!(solver, reg_nlp, stats_optimized; x = x0, optimized_solver_kwargs...) # It would be interesting to check for allocations here as well but depending on + solve!(solver_object, reg_nlp, stats_optimized; x = x0, optimized_solver_kwargs...) # It would be interesting to check for allocations here as well but depending on # the structure of solver_kwargs, some variables might get boxed, resulting in # false positives, for example if tol = 1e-3; solver_kwargs = (atol = tol), # then wrappedallocs would give a > 0 answer... @@ -41,7 +38,7 @@ function test_solver( @test stats_optimized.status == expected_status @test obj(reg_nlp, stats_optimized.solution) == stats_optimized.objective @test stats_optimized.objective <= obj(reg_nlp, x0) - @test all(solver.mν∇fk + solver.∇fk/stats_optimized.solver_specific[:sigma_cauchy] .≤ eps(eltype(solver.mν∇fk))) + @test all(solver_object.mν∇fk + solver_object.∇fk/stats_optimized.solver_specific[:sigma_cauchy] .≤ eps(eltype(solver_object.mν∇fk))) # TODO: test that the optimized entries in stats_optimized and stats_basic are the same. From ead3fc5b558e64002fdfe8b96350a6ff205b01b7 Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Wed, 18 Feb 2026 15:02:06 -0500 Subject: [PATCH 12/13] remove unnecessary computation --- test/test-R2N.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test-R2N.jl b/test/test-R2N.jl index 6bf85f0d..5a865c21 100644 --- a/test/test-R2N.jl +++ b/test/test-R2N.jl @@ -13,7 +13,6 @@ expected_status = :first_order, solver_kwargs = first_order_kwargs, ) - solver, stats = R2NSolver(rosenbrock_reg_nlp), RegularizedExecutionStats(rosenbrock_reg_nlp) # Test max time status max_time_kwargs = (x = [π, -π], atol = 1e-16, rtol = 1e-16, max_time = 1e-12) From e40e5607fb70bf4491fce63d76b7c92fef2322a6 Mon Sep 17 00:00:00 2001 From: Maxence Gollier Date: Wed, 18 Feb 2026 15:11:37 -0500 Subject: [PATCH 13/13] remove unnecessary code --- test/test-solver.jl | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/test-solver.jl b/test/test-solver.jl index ffcc2bd3..d3e0e3f5 100644 --- a/test/test-solver.jl +++ b/test/test-solver.jl @@ -26,12 +26,7 @@ function test_solver( solver_object = solver_constructor(reg_nlp; solver_constructor_kwargs...) stats_optimized = RegularizedExecutionStats(reg_nlp) - # Remove the x0 entry from solver_kwargs - optimized_solver_kwargs = Base.structdiff(solver_kwargs, NamedTuple{(:x0,)}) - solve!(solver_object, reg_nlp, stats_optimized; x = x0, optimized_solver_kwargs...) # It would be interesting to check for allocations here as well but depending on - # the structure of solver_kwargs, some variables might get boxed, resulting in - # false positives, for example if tol = 1e-3; solver_kwargs = (atol = tol), - # then wrappedallocs would give a > 0 answer... + solve!(solver_object, reg_nlp, stats_optimized; solver_kwargs...) @test typeof(stats_optimized.solution) == typeof(x0) @test length(stats_optimized.solution) == reg_nlp.model.meta.nvar @test typeof(stats_optimized.dual_feas) == eltype(stats_optimized.solution)