diff --git a/Project.toml b/Project.toml index dbdb67d4..6fa8dd88 100644 --- a/Project.toml +++ b/Project.toml @@ -16,7 +16,7 @@ SolverTools = "b5612192-2639-5dc1-abfe-fbedd65fab29" [compat] Krylov = "0.9.6" -LinearOperators = "2.0" +LinearOperators = "2.9" NLPModels = "0.21" NLPModelsModifiers = "0.7" SolverCore = "0.3" diff --git a/README.md b/README.md index cd21ff94..d8ee3f1d 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ This package provides an implementation of four classic algorithms for unconstra > high-order regularized models. *Mathematical Programming*, 163(1), 359-368. > DOI: [10.1007/s10107-016-1065-8](https://doi.org/10.1007/s10107-016-1065-8) +- `R2N`: An inexact second-order quadratic regularization method for unconstrained optimization (with shifted L-BFGS or shifted Hessian operator); - `fomo`: a first-order method with momentum for unconstrained optimization; - `tron`: a pure Julia implementation of TRON, a trust-region solver for bound-constrained optimization described in @@ -63,7 +64,7 @@ using JSOSolvers, ADNLPModels # Rosenbrock nlp = ADNLPModel(x -> 100 * (x[2] - x[1]^2)^2 + (x[1] - 1)^2, [-1.2; 1.0]) -stats = lbfgs(nlp) # or trunk, tron, R2 +stats = lbfgs(nlp) # or trunk, tron, R2, R2N ``` ## How to cite diff --git a/docs/src/solvers.md b/docs/src/solvers.md index 322f7c2e..12b840b5 100644 --- a/docs/src/solvers.md +++ b/docs/src/solvers.md @@ -7,10 +7,11 @@ - [`trunk`](@ref) - [`R2`](@ref) - [`fomo`](@ref) +- [`R2N`](@ref) | Problem type | Solvers | | --------------------- | -------- | -| Unconstrained NLP | [`lbfgs`](@ref), [`tron`](@ref), [`trunk`](@ref), [`R2`](@ref), [`fomo`](@ref)| +| Unconstrained NLP | [`lbfgs`](@ref), [`tron`](@ref), [`trunk`](@ref), [`R2`](@ref), [`fomo`](@ref), ['R2N'] | | Unconstrained NLS | [`trunk`](@ref), [`tron`](@ref) | | Bound-constrained NLP | [`tron`](@ref) | | Bound-constrained NLS | [`tron`](@ref) | @@ -23,4 +24,5 @@ tron trunk R2 fomo +R2N ``` diff --git a/src/JSOSolvers.jl b/src/JSOSolvers.jl index 0e86dfd6..62b0af5a 100644 --- a/src/JSOSolvers.jl +++ b/src/JSOSolvers.jl @@ -45,6 +45,7 @@ end include("lbfgs.jl") include("trunk.jl") include("fomo.jl") +include("R2N.jl") # Unconstrained solvers for NLS include("trunkls.jl") diff --git a/src/R2N.jl b/src/R2N.jl new file mode 100644 index 00000000..837e9ce9 --- /dev/null +++ b/src/R2N.jl @@ -0,0 +1,423 @@ +export R2N, R2NSolver +export ShiftedLBFGSSolver + +abstract type AbstractShiftedLBFGSSolver end + +struct ShiftedLBFGSSolver <: AbstractShiftedLBFGSSolver + # Shifted LBFGS-specific fields +end + +const R2N_allowed_subsolvers = [CgLanczosShiftSolver, MinresSolver, ShiftedLBFGSSolver] + +""" + R2N(nlp; kwargs...) + +An inexact second-order quadratic regularization method for unconstrained optimization (with shifted L-BFGS or shifted Hessian operator). + +For advanced usage, first define a `R2NSolver` to preallocate the memory used in the algorithm, and then call `solve!`: + + solver = R2NSolver(nlp) + solve!(solver, nlp; kwargs...) + +# Arguments +- `nlp::AbstractNLPModel{T, V}` is the model to solve, see `NLPModels.jl`. + +# Keyword arguments +- `x::V = nlp.meta.x0`: the initial guess. +- `atol::T = √eps(T)`: absolute tolerance. +- `rtol::T = √eps(T)`: relative tolerance: algorithm stops when ‖∇f(xᵏ)‖ ≤ atol + rtol * ‖∇f(x⁰)‖. +- `η1 = eps(T)^(1/4)`, `η2 = T(0.95)`: step acceptance parameters. +- `γ1 = T(1/2)`, `γ2 = 1/γ1`: regularization update parameters. +- `σmin = eps(T)`: step parameter for R2N algorithm. +- `max_eval::Int = -1`: maximum number of evaluation of the objective function. +- `max_time::Float64 = 30.0`: maximum time limit in seconds. +- `max_iter::Int = typemax(Int)`: maximum number of iterations. +- `verbose::Int = 0`: if > 0, display iteration details every `verbose` iteration. +- `subsolver_type::Union{Type{<:KrylovSolver}, Type{ShiftedLBFGSSolver}} = ShiftedLBFGSSolver`: the subsolver to solve the shifted system. The `MinresSolver` which solves the shifted linear system exactly at each iteration. Using the exact solver is only possible if `nlp` is an `LBFGSModel`. +- `subsolver_verbose::Int = 0`: if > 0, display iteration information every `subsolver_verbose` iteration of the subsolver if KrylovSolver type is selected. + +See `JSOSolvers.R2N_allowed_subsolvers` for a list of available `SubSolver`. + +# Output +The value returned is a `GenericExecutionStats`, see `SolverCore.jl`. + +# Callback +$(Callback_docstring) + +# Examples +```jldoctest +using JSOSolvers, ADNLPModels +nlp = ADNLPModel(x -> sum(x.^2), ones(3)) +stats = R2N(nlp) + +# output + +"Execution stats: first-order stationary" +``` + +```jldoctest +using JSOSolvers, ADNLPModels +nlp = ADNLPModel(x -> sum(x.^2), ones(3)) +solver = R2NSolver(nlp); +stats = solve!(solver, nlp) + +# output + +"Execution stats: first-order stationary" +``` +""" + +mutable struct R2NSolver{ + T, + V, + Op <: AbstractLinearOperator{T}, + Op2 <: AbstractLinearOperator{T}, + Sub <: Union{KrylovSolver{T, T, V}, ShiftedLBFGSSolver}, +} <: AbstractOptimizationSolver + x::V + cx::V + gx::V + gn::V + σ::T + μ::T + H::Op + opI::Op2 + Hs::V + s::V + obj_vec::V # used for non-monotone behaviour + subsolver_type::Sub + cgtol::T +end + +function R2NSolver( + nlp::AbstractNLPModel{T, V}; + non_mono_size = 1, + subsolver_type::Union{Type{<:KrylovSolver}, Type{ShiftedLBFGSSolver}} = MinresSolver, +) where {T, V} + subsolver_type in R2N_allowed_subsolvers || + error("subproblem solver must be one of $(R2N_allowed_subsolvers)") + + !(subsolver_type <: ShiftedLBFGSSolver) || + (nlp isa LBFGSModel) || + error("Unsupported subsolver type, ShiftedLBFGSSolver can only be used by LBFGSModel") + + non_mono_size >= 1 || error("non_mono_size must be greater than or equal to 1") + + nvar = nlp.meta.nvar + x = V(undef, nvar) + cx = V(undef, nvar) + gx = V(undef, nvar) + gn = isa(nlp, QuasiNewtonModel) ? V(undef, nvar) : V(undef, 0) + Hs = V(undef, nvar) + H = isa(nlp, QuasiNewtonModel) ? nlp.op : hess_op!(nlp, x, Hs) + opI = opEye(T, nvar) + Op = typeof(H) + Op2 = typeof(opI) + σ = zero(T) + μ = zero(T) + s = V(undef, nvar) + cgtol = one(T) + obj_vec = fill(typemin(T), non_mono_size) + subsolver = + isa(subsolver_type, Type{ShiftedLBFGSSolver}) ? subsolver_type() : subsolver_type(nvar, nvar, V) + + Sub = typeof(subsolver) + return R2NSolver{T, V, Op, Op2, Sub}( + x, + cx, + gx, + gn, + σ, + μ, + H, + opI, + Hs, + s, + obj_vec, + subsolver, + cgtol, + ) +end + +function SolverCore.reset!(solver::R2NSolver{T}) where {T} + fill!(solver.obj_vec, typemin(T)) + reset!(solver.H) + solver +end +function SolverCore.reset!(solver::R2NSolver{T}, nlp::AbstractNLPModel) where {T} + fill!(solver.obj_vec, typemin(T)) + # @assert (length(solver.gn) == 0) || isa(nlp, QuasiNewtonModel) + solver.H = isa(nlp, QuasiNewtonModel) ? nlp.op : hess_op!(nlp, solver.x, solver.Hs) + + solver +end + +@doc (@doc R2NSolver) function R2N( + nlp::AbstractNLPModel{T, V}; + subsolver_type::Union{Type{<:KrylovSolver}, Type{ShiftedLBFGSSolver}} = MinresSolver, + non_mono_size = 1, + kwargs..., +) where {T, V} + solver = R2NSolver(nlp; non_mono_size = non_mono_size, subsolver_type = subsolver_type) + return solve!(solver, nlp; non_mono_size = non_mono_size, kwargs...) +end + +function SolverCore.solve!( + solver::R2NSolver{T, V}, + nlp::AbstractNLPModel{T, V}, + stats::GenericExecutionStats{T, V}; + callback = (args...) -> nothing, + x::V = nlp.meta.x0, + atol::T = √eps(T), + rtol::T = √eps(T), + η1 = T(0.0001), + η2 = T(0.001), + λ = T(2), + σmin = zero(T), + max_time::Float64 = 30.0, + max_eval::Int = -1, + max_iter::Int = typemax(Int), + verbose::Int = 0, + subsolver_verbose::Int = 0, + non_mono_size = 1, +) where {T, V} + unconstrained(nlp) || error("R2N should only be called on unconstrained problems.") + + @assert(λ > 1) + reset!(stats) + start_time = time() + set_time!(stats, 0.0) + μmin = σmin + n = nlp.meta.nvar + x = solver.x .= x + ck = solver.cx + ∇fk = solver.gx # k-1 + ∇fn = solver.gn #current + s = solver.s + H = solver.H + Hs = solver.Hs + σk = solver.σ + μk = solver.μ + cgtol = solver.cgtol + subsolver_solved = false + + set_iter!(stats, 0) + f0 = obj(nlp, x) + set_objective!(stats, f0) + + grad!(nlp, x, ∇fk) + isa(nlp, QuasiNewtonModel) && (∇fn .= ∇fk) + norm_∇fk = norm(∇fk) + set_dual_residual!(stats, norm_∇fk) + + μk = 2^round(log2(norm_∇fk + 1)) / norm_∇fk + σk = μk * norm_∇fk + ρk = zero(T) + + # Stopping criterion: + fmin = min(-one(T), f0) / eps(T) + unbounded = f0 < fmin + + ϵ = atol + rtol * norm_∇fk + optimal = norm_∇fk ≤ ϵ + + if optimal + @info("Optimal point found at initial point") + @info log_header( + [:iter, :f, :grad_norm, :mu, :sigma, :rho, :dir], + [Int, Float64, Float64, Float64, Float64, Float64, String], + hdr_override = Dict( + :f => "f(x)", + :grad_norm => "‖∇f‖", + :mu => "μ", + :sigma => "σ", + :rho => "ρ", + :dir => "DIR", + ), + ) + + # Define and log the row information with corresponding data values + @info log_row([stats.iter, stats.objective, norm_∇fk, μk, σk, ρk, ""]) + end + if verbose > 0 && mod(stats.iter, verbose) == 0 + @info log_header( + [:iter, :f, :grad_norm, :mu, :sigma, :rho, :dir], + [Int, Float64, Float64, Float64, Float64, Float64, String], + hdr_override = Dict( + :f => "f(x)", + :grad_norm => "‖∇f‖", + :mu => "μ", + :sigma => "σ", + :rho => "ρ", + :dir => "DIR", + ), + ) + @info log_row([stats.iter, stats.objective, norm_∇fk, μk, σk, ρk, ""]) + end + + set_status!( + stats, + get_status( + nlp, + elapsed_time = stats.elapsed_time, + optimal = optimal, + unbounded = unbounded, + max_eval = max_eval, + iter = stats.iter, + max_iter = max_iter, + max_time = max_time, + ), + ) + + callback(nlp, solver, stats) + + done = stats.status != :unknown + cgtol = max(rtol, min(T(0.1), √norm_∇fk, T(0.9) * cgtol)) + + while !done + ∇fk .*= -1 + subsolver_solved = subsolve!(solver.subsolver_type, solver, s, zero(T), n, subsolver_verbose) + if !subsolver_solved + @warn("Subsolver failed to solve the shifted system") + break + end + slope = dot(s, ∇fk) # = -∇fkᵀ s because we flipped the sign of ∇fk + mul!(Hs, H, s) + curv = dot(s, Hs) + + ΔTk = slope - curv / 2 + ck .= x .+ s + fck = obj(nlp, ck) + + if non_mono_size > 1 #non-monotone behaviour + k = mod(stats.iter, non_mono_size) + 1 + solver.obj_vec[k] = stats.objective + fck_max = maximum(solver.obj_vec) + ρk = (fck_max - fck) / (fck_max - stats.objective + ΔTk) + else + ρk = (stats.objective - fck) / ΔTk + end + + # Update regularization parameters and Acceptance of the new candidate + step_accepted = ρk >= η1 && σk >= η2 + if step_accepted + μk = max(μmin, μk / λ) + x .= ck + grad!(nlp, x, ∇fk) + if isa(nlp, QuasiNewtonModel) + ∇fn .-= ∇fk + ∇fn .*= -1 # = ∇f(xₖ₊₁) - ∇f(xₖ) + push!(nlp, s, ∇fn) + ∇fn .= ∇fk + end + set_objective!(stats, fck) + unbounded = fck < fmin + norm_∇fk = norm(∇fk) + else + μk = μk * λ + ∇fk .*= -1 + end + + set_iter!(stats, stats.iter + 1) + set_time!(stats, time() - start_time) + + cgtol = max(rtol, min(T(0.1), √norm_∇fk, T(0.9) * cgtol)) + set_dual_residual!(stats, norm_∇fk) + + solver.σ = σk + solver.μ = μk + solver.cgtol = cgtol + + callback(nlp, solver, stats) + + norm_∇fk = stats.dual_feas # if the user change it, they just change the stats.norm , they also have to change cgtol + μk = solver.μ + cgtol = solver.cgtol + σk = μk * norm_∇fk + optimal = norm_∇fk ≤ ϵ + if verbose > 0 && mod(stats.iter, verbose) == 0 + @info log_row([ + stats.iter, # Current iteration number + stats.objective, # Objective function value + norm_∇fk, # Gradient norm + μk, # Mu value + σk, # Sigma value + ρk, # Rho value + step_accepted ? "↘" : "↗", # Step acceptance status + ]) + end + if stats.status == :user + done = true + else + set_status!( + stats, + get_status( + nlp, + elapsed_time = stats.elapsed_time, + optimal = optimal, + unbounded = unbounded, + max_eval = max_eval, + iter = stats.iter, + max_iter = max_iter, + max_time = max_time, + ), + ) + done = stats.status != :unknown + end + end + + set_solution!(stats, x) + return stats +end + +# Dispatch for MinresSolver +function subsolve!(subsolver::MinresSolver, R2N::R2NSolver, s, atol, n, subsolver_verbose) + ∇f_neg = R2N.gx + H = R2N.H + σ = R2N.σ + cgtol = R2N.cgtol + + minres!( + subsolver, + H, + ∇f_neg, # b + λ = σ, + itmax = max(2 * n, 50), + atol = atol, + rtol = cgtol, + verbose = subsolver_verbose, + ) + s .= subsolver.x + return issolved(subsolver) +end + +# Dispatch for KrylovSolver +function subsolve!(subsolver::CgLanczosShiftSolver, R2N::R2NSolver, s, atol, n, subsolver_verbose) + ∇f_neg = R2N.gx + H = R2N.H + σ = R2N.σ + opI = R2N.opI + cgtol = R2N.cgtol + + cg_lanczos_shift!( + subsolver, + H, + ∇f_neg, + opI * σ, #shift vector σ * I + atol = atol, + rtol = cgtol, + itmax = 2 * n, + verbose = subsolver_verbose, + ) + s .= subsolver.x + return issolved(subsolver) +end + +# Dispatch for ShiftedLBFGSSolver +function subsolve!(subsolver::ShiftedLBFGSSolver, R2N::R2NSolver, s, atol, n, subsolver_verbose) + ∇f_neg = R2N.gx + H = R2N.H + σ = R2N.σ + solve_shifted_system!(s, H, ∇f_neg, σ) + return true +end diff --git a/test/allocs.jl b/test/allocs.jl index b042ef32..2657581d 100644 --- a/test/allocs.jl +++ b/test/allocs.jl @@ -30,17 +30,26 @@ end if Sys.isunix() @testset "Allocation tests" begin - @testset "$symsolver" for symsolver in - (:LBFGSSolver, :FoSolver, :FomoSolver, :TrunkSolver, :TronSolver) + @testset "$name" for (name, symsolver) in ( + # (:R2N, :R2NSolver), + (:R2N_exact, :R2NSolver), + (:R2, :FoSolver), + (:fomo, :FomoSolver), + (:lbfgs, :LBFGSSolver), + (:tron, :TronSolver), + (:trunk, :TrunkSolver), + ) for model in NLPModelsTest.nlp_problems nlp = eval(Meta.parse(model))() - if unconstrained(nlp) || (bound_constrained(nlp) && (symsolver == :TronSolver)) - if (symsolver == :FoSolver || symsolver == :FomoSolver) + if unconstrained(nlp) || (bound_constrained(nlp) && (name == :TronSolver)) + if (name == :FoSolver || name == :FomoSolver) solver = eval(symsolver)(nlp; M = 2) # nonmonotone configuration allocates extra memory + elseif name == :R2N_exact + solver = eval(symsolver)(LBFGSModel(nlp), subsolver_type = JSOSolvers.ShiftedLBFGSSolver) else solver = eval(symsolver)(nlp) end - if symsolver == :FomoSolver + if name == :FomoSolver T = eltype(nlp.meta.x0) stats = GenericExecutionStats(nlp, solver_specific = Dict(:avgβmax => T(0))) else diff --git a/test/callback.jl b/test/callback.jl index ddadc799..3656d4b6 100644 --- a/test/callback.jl +++ b/test/callback.jl @@ -16,6 +16,11 @@ using ADNLPModels, JSOSolvers, LinearAlgebra, Logging #, Plots R2(nlp, callback = cb) end @test stats.iter == 8 + + stats = with_logger(NullLogger()) do + R2N(nlp, callback = cb) + end + @test stats.iter == 8 stats = with_logger(NullLogger()) do lbfgs(nlp, callback = cb) diff --git a/test/consistency.jl b/test/consistency.jl index fb725b5b..735ae37c 100644 --- a/test/consistency.jl +++ b/test/consistency.jl @@ -10,28 +10,44 @@ function consistency() @testset "Consistency" begin args = Pair{Symbol, Number}[:atol => 1e-6, :rtol => 1e-6, :max_eval => 20000, :max_time => 60.0] - @testset "NLP with $mtd" for mtd in [trunk, lbfgs, tron, R2, fomo] + @testset "NLP with $mtd" for (mtd, solver) in [ + ("trunk", trunk), + ("lbfgs", lbfgs), + ("tron", tron), + ("R2", R2), + # ("R2N", R2N), + ("R2N_exact", (nlp; kwargs...) -> R2N(LBFGSModel(nlp), subsolver_type = JSOSolvers.ShiftedLBFGSSolver; kwargs...)), + ("fomo", fomo), + ] with_logger(NullLogger()) do reset!(unlp) - stats = mtd(unlp; args...) + stats = solver(unlp; args...) @test stats isa GenericExecutionStats @test stats.status == :first_order reset!(unlp) - stats = mtd(unlp; max_eval = 1) + stats = solver(unlp; max_eval = 1) @test stats.status == :max_eval slow_nlp = ADNLPModel(x -> begin sleep(0.1) f(x) end, unlp.meta.x0) - stats = mtd(slow_nlp; max_time = 0.0) + stats = solver(slow_nlp; max_time = 0.0) @test stats.status == :max_time end end - @testset "Quasi-Newton NLP with $mtd" for mtd in [trunk, lbfgs, tron, R2, fomo] + @testset "Quasi-Newton NLP with $mtd" for (mtd, solver) in [ + ("trunk", trunk), + ("lbfgs", lbfgs), + ("tron", tron), + ("R2", R2), + # ("R2N", R2N), + ("R2N_exact", (nlp; kwargs...) -> R2N(LBFGSModel(nlp), subsolver_type = JSOSolvers.ShiftedLBFGSSolver; kwargs...)), + ("fomo", fomo), + ] with_logger(NullLogger()) do reset!(qnlp) - stats = mtd(qnlp; args...) + stats = solver(qnlp; args...) @test stats isa GenericExecutionStats @test stats.status == :first_order end diff --git a/test/restart.jl b/test/restart.jl index 38765465..a2976aea 100644 --- a/test/restart.jl +++ b/test/restart.jl @@ -1,4 +1,6 @@ @testset "Test restart with a different initial guess: $fun" for (fun, s) in ( + # (:R2N, :R2NSolver), + (:R2N_exact, :R2NSolver), (:R2, :FoSolver), (:fomo, :FomoSolver), (:lbfgs, :LBFGSSolver), @@ -7,9 +9,15 @@ ) f(x) = (x[1] - 1)^2 + 4 * (x[2] - x[1]^2)^2 nlp = ADNLPModel(f, [-1.2; 1.0]) + if fun == :R2N_exact + nlp = LBFGSModel(nlp) + solver = eval(s)(nlp,subsolver_type = JSOSolvers.ShiftedLBFGSSolver) + else + solver = eval(s)(nlp) + end stats = GenericExecutionStats(nlp) - solver = eval(s)(nlp) + stats = SolverCore.solve!(solver, nlp, stats) @test stats.status == :first_order @test isapprox(stats.solution, [1.0; 1.0], atol = 1e-6) @@ -44,6 +52,8 @@ end end @testset "Test restart with a different problem: $fun" for (fun, s) in ( + # (:R2N, :R2NSolver), + (:R2N_exact, :R2NSolver), (:R2, :FoSolver), (:fomo, :FomoSolver), (:lbfgs, :LBFGSSolver), @@ -52,15 +62,25 @@ end ) f(x) = (x[1] - 1)^2 + 4 * (x[2] - x[1]^2)^2 nlp = ADNLPModel(f, [-1.2; 1.0]) + if fun == :R2N_exact + nlp = LBFGSModel(nlp) + solver = eval(s)(nlp,subsolver_type = JSOSolvers.ShiftedLBFGSSolver) + else + solver = eval(s)(nlp) + end stats = GenericExecutionStats(nlp) - solver = eval(s)(nlp) stats = SolverCore.solve!(solver, nlp, stats) @test stats.status == :first_order @test isapprox(stats.solution, [1.0; 1.0], atol = 1e-6) f2(x) = (x[1])^2 + 4 * (x[2] - x[1]^2)^2 nlp = ADNLPModel(f2, [-1.2; 1.0]) + if fun == :R2N_exact + nlp = LBFGSModel(nlp) + else + solver = eval(s)(nlp) + end SolverCore.reset!(solver, nlp) stats = SolverCore.solve!(solver, nlp, stats, atol = 1e-10, rtol = 1e-10) diff --git a/test/runtests.jl b/test/runtests.jl index 724a4169..7509b708 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,37 +18,53 @@ using JSOSolvers end @testset "Test iteration limit" begin - @testset "$fun" for fun in (R2, fomo, lbfgs, tron, trunk) + @testset "$name" for (name, solver) in [ + ("trunk", trunk), + ("lbfgs", lbfgs), + ("tron", tron), + ("R2", R2), + # ("R2N", R2N), + ("R2N_exact", (nlp; kwargs...) -> R2N(LBFGSModel(nlp), subsolver_type = JSOSolvers.ShiftedLBFGSSolver; kwargs...)), + ("fomo", fomo), + ] f(x) = (x[1] - 1)^2 + 4 * (x[2] - x[1]^2)^2 nlp = ADNLPModel(f, [-1.2; 1.0]) - - stats = eval(fun)(nlp, max_iter = 1) + stats = eval(solver)(nlp, max_iter = 1) + stats = solver(nlp, max_iter = 1) @test stats.status == :max_iter end + @testset "$(fun)-NLS" for fun in (tron, trunk) f(x) = [x[1] - 1; 2 * (x[2] - x[1]^2)] nlp = ADNLSModel(f, [-1.2; 1.0], 2) - stats = eval(fun)(nlp, max_iter = 1) @test stats.status == :max_iter end end @testset "Test unbounded below" begin - @testset "$fun" for fun in (R2, fomo, lbfgs, tron, trunk) + @testset "$name" for (name, solver) in [ + ("trunk", trunk), + ("lbfgs", lbfgs), + ("tron", tron), + ("R2", R2), + # ("R2N", R2N), + ("R2N_exact", (nlp; kwargs...) -> R2N(LBFGSModel(nlp), subsolver_type = JSOSolvers.ShiftedLBFGSSolver; kwargs...)), + ("fomo", fomo), + ] T = Float64 x0 = [T(0)] f(x) = -exp(x[1]) nlp = ADNLPModel(f, x0) - - stats = eval(fun)(nlp) + stats = solver(nlp) @test stats.status == :unbounded @test stats.objective < -one(T) / eps(T) end end include("restart.jl") +include("test_edge_cases.jl") include("callback.jl") include("consistency.jl") include("test_solvers.jl") @@ -93,4 +109,4 @@ end end stats = trunk(nlp, callback = callback, M = M) @test stats.status == :first_order -end +end \ No newline at end of file diff --git a/test/test_edge_cases.jl b/test/test_edge_cases.jl new file mode 100644 index 00000000..cfab11e8 --- /dev/null +++ b/test/test_edge_cases.jl @@ -0,0 +1,64 @@ +# 1. Test error for constrained problems +@testset "Constrained Problems Error" begin + f(x) = (x[1] - 1.0)^2 + 100 * (x[2] - x[1]^2)^2 + x0 = [-1.2; 1.0] + lvar = [-Inf; 0.1] + uvar = [0.5; 0.5] + c(x) = [x[1] + x[2] - 2; x[1]^2 + x[2]^2] + lcon = [0.0; -Inf] + ucon = [Inf; 1.0] + nlp_constrained = ADNLPModel(f, x0, lvar, uvar, c, lcon, ucon) + + solver = R2NSolver(nlp_constrained) + stats = GenericExecutionStats(nlp_constrained) + + @test_throws ErrorException begin + SolverCore.solve!(solver, nlp_constrained, stats) + end +end + +# 2. Test error when non_mono_size < 1 +@testset "non_mono_size < 1 Error" begin + f(x) = (x[1] - 1)^2 + 4 * (x[2] - x[1]^2)^2 + nlp = ADNLPModel(f, [-1.2; 1.0]) + + @test_throws ErrorException begin + R2NSolver(nlp; non_mono_size = 0) + end + @test_throws ErrorException begin + R2N(nlp; non_mono_size = 0) + end + + @test_throws ErrorException begin + R2NSolver(nlp; non_mono_size = -1) + end + @test_throws ErrorException begin + R2N(nlp; non_mono_size = -1) + end +end + +# 3. Test error when subsolver_type is ShiftedLBFGSSolver but nlp is not of type LBFGSModel +@testset "ShiftedLBFGSSolver with wrong nlp type Error" begin + f(x) = (x[1] - 1)^2 + 4 * (x[2] - x[1]^2)^2 + nlp = ADNLPModel(f, [-1.2; 1.0]) + + @test_throws ErrorException begin + R2NSolver(nlp; subsolver_type = ShiftedLBFGSSolver) + end + @test_throws ErrorException begin + R2N(nlp; subsolver_type = ShiftedLBFGSSolver) + end +end + +# 4. Test error when subsolver_type is not a subtype of R2N_allowed_subsolvers +@testset "Invalid subsolver type Error" begin + f(x) = (x[1] - 1)^2 + 4 * (x[2] - x[1]^2)^2 + nlp = ADNLPModel(f, [-1.2; 1.0]) + + @test_throws ErrorException begin + R2NSolver(nlp; subsolver_type = CgSolver) + end + @test_throws ErrorException begin + R2N(nlp; subsolver_type = CgSolver) + end +end diff --git a/test/test_solvers.jl b/test/test_solvers.jl index dc82ed84..b5a1769c 100644 --- a/test/test_solvers.jl +++ b/test/test_solvers.jl @@ -8,6 +8,8 @@ function tests() ("lbfgs", lbfgs), ("tron", tron), ("R2", R2), + # ("R2N", R2N), + ("R2N_exact", (nlp; kwargs...) -> R2N(LBFGSModel(nlp), subsolver_type = JSOSolvers.ShiftedLBFGSSolver; kwargs...)), ("fomo_r2", fomo), ("fomo_tr", (nlp; kwargs...) -> fomo(nlp, step_backend = JSOSolvers.tr_step(); kwargs...)), ]