diff --git a/docs/src/optimization_packages/ipopt.md b/docs/src/optimization_packages/ipopt.md index a1ce273ab..0e15b5a33 100644 --- a/docs/src/optimization_packages/ipopt.md +++ b/docs/src/optimization_packages/ipopt.md @@ -42,61 +42,130 @@ The algorithm supports: - Box constraints via `lb` and `ub` in the `OptimizationProblem` - General nonlinear equality and inequality constraints via `lcons` and `ucons` +### Basic Usage + +```julia +using Optimization, OptimizationIpopt + +# Create optimizer with default settings +opt = IpoptOptimizer() + +# Or configure Ipopt-specific options +opt = IpoptOptimizer( + acceptable_tol = 1e-8, + mu_strategy = "adaptive" +) + +# Solve the problem +sol = solve(prob, opt) +``` + ## Options and Parameters -### Common Options +### Common Interface Options -The following options can be passed as keyword arguments to `solve`: +The following options can be passed as keyword arguments to `solve` and follow the common Optimization.jl interface: -- `maxiters`: Maximum number of iterations (maps to Ipopt's `max_iter`) -- `maxtime`: Maximum wall time in seconds (maps to Ipopt's `max_wall_time`) +- `maxiters`: Maximum number of iterations (overrides Ipopt's `max_iter`) +- `maxtime`: Maximum wall time in seconds (overrides Ipopt's `max_wall_time`) - `abstol`: Absolute tolerance (not directly used by Ipopt) -- `reltol`: Convergence tolerance (maps to Ipopt's `tol`) -- `verbose`: Control output verbosity +- `reltol`: Convergence tolerance (overrides Ipopt's `tol`) +- `verbose`: Control output verbosity (overrides Ipopt's `print_level`) - `false` or `0`: No output - `true` or `5`: Standard output - - Integer values 0-12: Different verbosity levels (maps to `print_level`) -- `hessian_approximation`: Method for Hessian computation - - `"exact"` (default): Use exact Hessian - - `"limited-memory"`: Use L-BFGS approximation + - Integer values 0-12: Different verbosity levels -### Advanced Ipopt Options +### IpoptOptimizer Constructor Options -Any Ipopt option can be passed directly as keyword arguments. The full list of available options is documented in the [Ipopt Options Reference](https://coin-or.github.io/Ipopt/OPTIONS.html). Common options include: +Ipopt-specific options are passed to the `IpoptOptimizer` constructor. The most commonly used options are available as struct fields: -#### Convergence Options -- `tol`: Desired convergence tolerance (relative) -- `dual_inf_tol`: Dual infeasibility tolerance -- `constr_viol_tol`: Constraint violation tolerance -- `compl_inf_tol`: Complementarity tolerance +#### Termination Options +- `acceptable_tol::Float64 = 1e-6`: Acceptable convergence tolerance (relative) +- `acceptable_iter::Int = 15`: Number of acceptable iterations before termination +- `dual_inf_tol::Float64 = 1.0`: Desired threshold for dual infeasibility +- `constr_viol_tol::Float64 = 1e-4`: Desired threshold for constraint violation +- `compl_inf_tol::Float64 = 1e-4`: Desired threshold for complementarity conditions -#### Algorithm Options -- `linear_solver`: Linear solver to use +#### Linear Solver Options +- `linear_solver::String = "mumps"`: Linear solver to use - Default: "mumps" (included with Ipopt) - HSL solvers: "ma27", "ma57", "ma86", "ma97" (require [separate installation](https://github.com/jump-dev/Ipopt.jl?tab=readme-ov-file#linear-solvers)) - Others: "pardiso", "spral" (require [separate installation](https://github.com/jump-dev/Ipopt.jl?tab=readme-ov-file#linear-solvers)) -- `nlp_scaling_method`: Scaling method ("gradient-based", "none", "equilibration-based") -- `limited_memory_max_history`: History size for L-BFGS (when using `hessian_approximation="limited-memory"`) -- `mu_strategy`: Update strategy for barrier parameter ("monotone", "adaptive") +- `linear_system_scaling::String = "none"`: Method for scaling linear system. Use "mc19" for HSL solvers. + +#### NLP Scaling Options +- `nlp_scaling_method::String = "gradient-based"`: Scaling method for NLP + - Options: "none", "user-scaling", "gradient-based", "equilibration-based" +- `nlp_scaling_max_gradient::Float64 = 100.0`: Maximum gradient after scaling + +#### Barrier Parameter Options +- `mu_strategy::String = "monotone"`: Update strategy for barrier parameter ("monotone", "adaptive") +- `mu_init::Float64 = 0.1`: Initial value for barrier parameter +- `mu_oracle::String = "quality-function"`: Oracle for adaptive mu strategy + +#### Hessian Options +- `hessian_approximation::String = "exact"`: How to approximate the Hessian + - `"exact"`: Use exact Hessian + - `"limited-memory"`: Use L-BFGS approximation +- `limited_memory_max_history::Int = 6`: History size for L-BFGS +- `limited_memory_update_type::String = "bfgs"`: Quasi-Newton update formula ("bfgs", "sr1") #### Line Search Options -- `line_search_method`: Line search method ("filter", "penalty") -- `alpha_for_y`: Step size for constraint multipliers -- `recalc_y`: Control when multipliers are recalculated +- `line_search_method::String = "filter"`: Line search method ("filter", "penalty") +- `accept_every_trial_step::String = "no"`: Accept every trial step (disables line search) #### Output Options -- `print_timing_statistics`: Print detailed timing information ("yes"/"no") -- `print_info_string`: Print user-defined info string ("yes"/"no") +- `print_timing_statistics::String = "no"`: Print detailed timing information +- `print_info_string::String = "no"`: Print algorithm info string + +#### Warm Start Options +- `warm_start_init_point::String = "no"`: Use warm start from previous solution -Example with advanced options: +#### Restoration Phase Options +- `expect_infeasible_problem::String = "no"`: Enable if problem is expected to be infeasible + +### Additional Options Dictionary + +For Ipopt options not available as struct fields, use the `additional_options` dictionary: ```julia -sol = solve(prob, IpoptOptimizer(); - maxiters = 1000, - tol = 1e-8, +opt = IpoptOptimizer( linear_solver = "ma57", - mu_strategy = "adaptive", - print_timing_statistics = "yes" + additional_options = Dict( + "derivative_test" => "first-order", + "derivative_test_tol" => 1e-4, + "fixed_variable_treatment" => "make_parameter", + "alpha_for_y" => "primal" + ) +) +``` + +The full list of available options is documented in the [Ipopt Options Reference](https://coin-or.github.io/Ipopt/OPTIONS.html). + +### Option Priority + +Options follow this priority order (highest to lowest): +1. Common interface arguments passed to `solve` (e.g., `reltol`, `maxiters`) +2. Options in `additional_options` dictionary +3. Struct field values in `IpoptOptimizer` + +Example with multiple option sources: + +```julia +opt = IpoptOptimizer( + acceptable_tol = 1e-6, # Struct field + mu_strategy = "adaptive", # Struct field + linear_solver = "ma57", # Struct field (needs HSL) + print_timing_statistics = "yes", # Struct field + additional_options = Dict( + "alpha_for_y" => "primal", # Not a struct field + "max_iter" => 500 # Will be overridden by maxiters below + ) +) + +sol = solve(prob, opt; + maxiters = 1000, # Overrides max_iter in additional_options + reltol = 1e-8 # Sets Ipopt's tol ) ``` @@ -189,9 +258,9 @@ optfunc = OptimizationFunction(rosenbrock_nd, AutoZygote()) prob = OptimizationProblem(optfunc, x0, p) # Use L-BFGS approximation for Hessian -sol = solve(prob, IpoptOptimizer(); +sol = solve(prob, IpoptOptimizer( hessian_approximation = "limited-memory", - limited_memory_max_history = 10, + limited_memory_max_history = 10); maxiters = 1000) ``` @@ -235,8 +304,8 @@ prob = OptimizationProblem(optfunc, w0; ucons = [0.0, Inf]) sol = solve(prob, IpoptOptimizer(); - tol = 1e-8, - print_level = 5) + reltol = 1e-8, + verbose = 5) println("Optimal weights: ", sol.u) println("Expected return: ", dot(μ, sol.u)) @@ -249,13 +318,13 @@ println("Portfolio variance: ", sol.objective) 2. **Initial Points**: Provide good initial guesses when possible. Ipopt is a local optimizer and the solution quality depends on the starting point. -3. **Hessian Approximation**: For large problems or when Hessian computation is expensive, use `hessian_approximation = "limited-memory"`. +3. **Hessian Approximation**: For large problems or when Hessian computation is expensive, use `hessian_approximation = "limited-memory"` in the `IpoptOptimizer` constructor. 4. **Linear Solver Selection**: The choice of linear solver can significantly impact performance. For large problems, consider using HSL solvers (ma27, ma57, ma86, ma97). Note that HSL solvers require [separate installation](https://github.com/jump-dev/Ipopt.jl?tab=readme-ov-file#linear-solvers) - see the Ipopt.jl documentation for setup instructions. The default MUMPS solver works well for small to medium problems. 5. **Constraint Formulation**: Ipopt handles equality constraints well. When possible, formulate constraints as equalities rather than pairs of inequalities. -6. **Warm Starting**: When solving a sequence of similar problems, use the solution from the previous problem as the initial point for the next. +6. **Warm Starting**: When solving a sequence of similar problems, use the solution from the previous problem as the initial point for the next. You can enable warm starting with `IpoptOptimizer(warm_start_init_point = "yes")`. ## References diff --git a/lib/OptimizationIpopt/Project.toml b/lib/OptimizationIpopt/Project.toml index b5191b5b0..4b2d8a1fe 100644 --- a/lib/OptimizationIpopt/Project.toml +++ b/lib/OptimizationIpopt/Project.toml @@ -1,7 +1,7 @@ name = "OptimizationIpopt" uuid = "43fad042-7963-4b32-ab19-e2a4f9a67124" authors = ["Sebastian Micluța-Câmpeanu and contributors"] -version = "0.1.2" +version = "0.2.0" [deps] Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" diff --git a/lib/OptimizationIpopt/src/OptimizationIpopt.jl b/lib/OptimizationIpopt/src/OptimizationIpopt.jl index af75d0f11..5d05f1973 100644 --- a/lib/OptimizationIpopt/src/OptimizationIpopt.jl +++ b/lib/OptimizationIpopt/src/OptimizationIpopt.jl @@ -11,7 +11,162 @@ export IpoptOptimizer const DenseOrSparse{T} = Union{Matrix{T}, SparseMatrixCSC{T}} -struct IpoptOptimizer end +""" + IpoptOptimizer(; kwargs...) + +Optimizer using the Interior Point Optimizer (Ipopt) for nonlinear optimization. + +Ipopt is designed to find (local) solutions of mathematical optimization problems of the form: + + min f(x) + s.t. g_L ≤ g(x) ≤ g_U + x_L ≤ x ≤ x_U + +where f(x) and g(x) are twice continuously differentiable functions. + +# Common Interface Arguments + +The following common optimization arguments can be passed to `solve`: +- `reltol`: Overrides the Ipopt `tol` option (desired convergence tolerance) +- `maxiters`: Overrides the Ipopt `max_iter` option (maximum iterations) +- `maxtime`: Overrides the Ipopt `max_wall_time` option (maximum wall clock time) +- `verbose`: Overrides the Ipopt `print_level` option (0 for silent, 5 for default, up to 12 for maximum verbosity) + +# Keyword Arguments + +## Termination Options +- `acceptable_tol::Float64 = 1e-6`: Acceptable convergence tolerance (relative) +- `acceptable_iter::Int = 15`: Number of acceptable iterations before termination +- `dual_inf_tol::Float64 = 1.0`: Desired threshold for dual infeasibility +- `constr_viol_tol::Float64 = 1e-4`: Desired threshold for constraint violation +- `compl_inf_tol::Float64 = 1e-4`: Desired threshold for complementarity conditions + +## Output Options +- `print_timing_statistics::String = "no"`: Print timing statistics at end of optimization +- `print_info_string::String = "no"`: Print info string with algorithm details + +## Linear Solver Options +- `linear_solver::String = "mumps"`: Linear solver to use (mumps, ma27, ma57, ma86, ma97, pardiso, wsmp, etc.) +- `linear_system_scaling::String = "none"`: Method for scaling linear system (none, mc19, slack-based) +- `hsllib::String = ""`: Path to HSL library (if using HSL solvers) +- `pardisolib::String = ""`: Path to Pardiso library (if using Pardiso) +- `linear_scaling_on_demand::String = "yes"`: Enable scaling on demand for linear systems + +## NLP Scaling Options +- `nlp_scaling_method::String = "gradient-based"`: Scaling method for NLP (none, user-scaling, gradient-based, equilibration-based) +- `nlp_scaling_max_gradient::Float64 = 100.0`: Maximum gradient after scaling +- `honor_original_bounds::String = "no"`: Honor original variable bounds after scaling +- `check_derivatives_for_naninf::String = "no"`: Check derivatives for NaN/Inf values + +## Barrier Parameter Options +- `mu_strategy::String = "monotone"`: Update strategy for barrier parameter (monotone, adaptive) +- `mu_oracle::String = "quality-function"`: Oracle for adaptive mu strategy +- `mu_init::Float64 = 0.1`: Initial value for barrier parameter +- `adaptive_mu_globalization::String = "obj-constr-filter"`: Globalization strategy for adaptive mu + +## Warm Start Options +- `warm_start_init_point::String = "no"`: Use warm start from previous solution + +## Hessian Options +- `hessian_approximation::String = "exact"`: How to approximate the Hessian (exact, limited-memory) +- `limited_memory_max_history::Int = 6`: History size for limited-memory Hessian approximation +- `limited_memory_update_type::String = "bfgs"`: Quasi-Newton update formula for limited-memory approximation (bfgs, sr1) + +## Line Search Options +- `accept_every_trial_step::String = "no"`: Accept every trial step (disables line search) +- `line_search_method::String = "filter"`: Line search method (filter, penalty, cg-penalty) + +## Restoration Phase Options +- `expect_infeasible_problem::String = "no"`: Enable if problem is expected to be infeasible + +## Additional Options +- `additional_options::Dict{String, Any} = Dict()`: Dictionary to set any other Ipopt option not explicitly listed above. + See https://coin-or.github.io/Ipopt/OPTIONS.html for the full list of available options. + +# Examples + +```julia +using Optimization, OptimizationIpopt + +# Basic usage with default settings +opt = IpoptOptimizer() + +# Customized settings +opt = IpoptOptimizer( + linear_solver = "ma57", # needs HSL solvers configured + nlp_scaling_method = "equilibration-based", + hessian_approximation = "limited-memory", + additional_options = Dict( + "alpha_for_y" => "primal", + "recalc_y" => "yes" + ) +) + +# Solve with common interface arguments +result = solve(prob, opt; + reltol = 1e-8, # Sets Ipopt's tol + maxiters = 5000, # Sets Ipopt's max_iter + maxtime = 300.0, # Sets Ipopt's max_wall_time (in seconds) + verbose = 3 # Sets Ipopt's print_level +) +``` + +# References + +For complete documentation of all Ipopt options, see: +https://coin-or.github.io/Ipopt/OPTIONS.html +""" +@kwdef struct IpoptOptimizer + # Most common Ipopt-specific options (excluding common interface options) + + # Termination + acceptable_tol::Float64 = 1e-6 + acceptable_iter::Int = 15 + dual_inf_tol::Float64 = 1.0 + constr_viol_tol::Float64 = 1e-4 + compl_inf_tol::Float64 = 1e-4 + + # Output options + print_timing_statistics::String = "no" + print_info_string::String = "no" + + # Linear solver + linear_solver::String = "mumps" + linear_system_scaling::String = "none" + hsllib::String = "" + pardisolib::String = "" + linear_scaling_on_demand = "yes" + + # NLP options + nlp_scaling_method::String = "gradient-based" + nlp_scaling_max_gradient::Float64 = 100.0 + honor_original_bounds::String = "no" + check_derivatives_for_naninf::String = "no" + + # Barrier parameter + mu_strategy::String = "monotone" + mu_oracle::String = "quality-function" + mu_init::Float64 = 0.1 + adaptive_mu_globalization::String = "obj-constr-filter" + + # Warm start + warm_start_init_point::String = "no" + + # Hessian approximation + hessian_approximation::String = "exact" + limited_memory_max_history::Int = 6 + limited_memory_update_type::String = "bfgs" + + # Line search + accept_every_trial_step::String = "no" + line_search_method::String = "filter" + + # Restoration phase + expect_infeasible_problem::String = "no" + + # Additional options for any other Ipopt parameters + additional_options::Dict{String, Any} = Dict{String, Any}() +end @static if isdefined(SciMLBase, :supports_opt_cache_interface) function SciMLBase.supports_opt_cache_interface(alg::IpoptOptimizer) @@ -53,11 +208,9 @@ function __map_optimizer_args(cache, maxtime::Union{Number, Nothing} = nothing, abstol::Union{Number, Nothing} = nothing, reltol::Union{Number, Nothing} = nothing, - hessian_approximation = "exact", verbose = false, - progress = false, - callback = nothing, - kwargs...) + progress::Bool = false, + callback = nothing) jacobian_sparsity = jacobian_structure(cache) hessian_sparsity = hessian_lagrangian_structure(cache) @@ -103,37 +256,53 @@ function __map_optimizer_args(cache, eval_jac_g, eval_h ) - progress_callback = IpoptProgressLogger(cache.progress, cache, prob) + + # Set up progress callback + progress_callback = IpoptProgressLogger(progress, callback, prob, cache.n, cache.num_cons, maxiters, cache.iterations) intermediate = (args...) -> progress_callback(args...) Ipopt.SetIntermediateCallback(prob, intermediate) - if !isnothing(maxiters) - Ipopt.AddIpoptIntOption(prob, "max_iter", maxiters) - end - if !isnothing(maxtime) - Ipopt.AddIpoptNumOption(prob, "max_wall_time", float(maxtime)) + # Apply all options from struct using reflection and type dispatch + for field in propertynames(opt) + field == :additional_options && continue # Skip the dict field + + field_str = string(field) + value = getproperty(opt, field) + + # Apply option based on type + if value isa Int + Ipopt.AddIpoptIntOption(prob, field_str, value) + elseif value isa Float64 + Ipopt.AddIpoptNumOption(prob, field_str, value) + elseif value isa String + Ipopt.AddIpoptStrOption(prob, field_str, value) + end end - if !isnothing(reltol) - Ipopt.AddIpoptNumOption(prob, "tol", reltol) + + # Apply additional options with type dispatch + for (key, value) in opt.additional_options + if value isa Int + Ipopt.AddIpoptIntOption(prob, key, value) + elseif value isa Float64 + Ipopt.AddIpoptNumOption(prob, key, float(value)) + elseif value isa String + Ipopt.AddIpoptStrOption(prob, key, value) + else + error("Unsupported option type $(typeof(value)) for option $key. Must be Int, Float64, or String") + end end + + # Override with common interface arguments if provided + !isnothing(reltol) && Ipopt.AddIpoptNumOption(prob, "tol", reltol) + !isnothing(maxiters) && Ipopt.AddIpoptIntOption(prob, "max_iter", maxiters) + !isnothing(maxtime) && Ipopt.AddIpoptNumOption(prob, "max_wall_time", Float64(maxtime)) + + # Handle verbose override if verbose isa Bool Ipopt.AddIpoptIntOption(prob, "print_level", verbose * 5) - else + elseif verbose isa Int Ipopt.AddIpoptIntOption(prob, "print_level", verbose) end - Ipopt.AddIpoptStrOption(prob, "hessian_approximation", hessian_approximation) - - for kw in pairs(kwargs) - if kw[2] isa Int - Ipopt.AddIpoptIntOption(prob, string(kw[1]), kw[2]) - elseif kw[2] isa Float64 - Ipopt.AddIpoptNumOption(prob, string(kw[1]), kw[2]) - elseif kw[2] isa String - Ipopt.AddIpoptStrOption(prob, string(kw[1]), kw[2]) - else - error("Keyword argument type $(typeof(kw[2])) not recognized") - end - end return prob end @@ -173,7 +342,9 @@ function SciMLBase.__solve(cache::IpoptCache) reltol = cache.solver_args.reltol, maxiters = maxiters, maxtime = maxtime, - cache.solver_args...) + verbose = get(cache.solver_args, :verbose, false), + progress = cache.progress, + callback = get(cache.solver_args, :callback, nothing)) opt_setup.x .= cache.reinit_cache.u0 @@ -191,7 +362,7 @@ function SciMLBase.__solve(cache::IpoptCache) minimizer = opt_setup.x stats = Optimization.OptimizationStats(; time = time() - start_time, - iterations = cache.iterations, fevals = cache.f_calls, gevals = cache.f_grad_calls) + iterations = cache.iterations[], fevals = cache.f_calls, gevals = cache.f_grad_calls) finalize(opt_setup) @@ -210,12 +381,14 @@ function SciMLBase.__init(prob::OptimizationProblem, maxtime::Union{Number, Nothing} = nothing, abstol::Union{Number, Nothing} = nothing, reltol::Union{Number, Nothing} = nothing, + progress::Bool = false, kwargs...) cache = IpoptCache(prob, opt; maxiters, maxtime, abstol, reltol, + progress, kwargs... ) cache.reinit_cache.u0 .= prob.u0 diff --git a/lib/OptimizationIpopt/src/cache.jl b/lib/OptimizationIpopt/src/cache.jl index 1eb5ddac1..f95b9981b 100644 --- a/lib/OptimizationIpopt/src/cache.jl +++ b/lib/OptimizationIpopt/src/cache.jl @@ -18,7 +18,7 @@ mutable struct IpoptCache{T, F <: OptimizationFunction, RC, LB, UB, I, S, const progress::Bool f_calls::Int f_grad_calls::Int - iterations::Cint + const iterations::Ref{Int} obj_expr::Union{Expr, Nothing} cons_expr::Union{Vector{Expr}, Nothing} const opt::O @@ -139,7 +139,7 @@ function IpoptCache(prob, opt; progress, 0, 0, - Cint(0), + Ref(0), obj_expr, cons_expr, opt, diff --git a/lib/OptimizationIpopt/src/callback.jl b/lib/OptimizationIpopt/src/callback.jl index ab1f28b0e..ebe644f86 100644 --- a/lib/OptimizationIpopt/src/callback.jl +++ b/lib/OptimizationIpopt/src/callback.jl @@ -10,15 +10,36 @@ struct IpoptState alpha_du::Float64 alpha_pr::Float64 ls_trials::Cint + u::Vector{Float64} z_L::Vector{Float64} z_U::Vector{Float64} + g::Vector{Float64} lambda::Vector{Float64} end -struct IpoptProgressLogger{C <: IpoptCache, P} +struct IpoptProgressLogger{C, P} progress::Bool - cache::C + callback::C prob::P + n::Int + num_cons::Int + maxiters::Union{Nothing, Int} + iterations::Ref{Int} + # caches for GetIpoptCurrentIterate + u::Vector{Float64} + z_L::Vector{Float64} + z_U::Vector{Float64} + g::Vector{Float64} + lambda::Vector{Float64} +end + +function IpoptProgressLogger(progress::Bool, callback::C, prob::P, n::Int, num_cons::Int, + maxiters::Union{Nothing, Int}, iterations::Ref{Int}) where {C, P} + # Initialize caches + u, z_L, z_U = zeros(n), zeros(n), zeros(n) + g, lambda = zeros(num_cons), zeros(num_cons) + IpoptProgressLogger( + progress, callback, prob, n, num_cons, maxiters, iterations, u, z_L, z_U, g, lambda) end function (cb::IpoptProgressLogger)( @@ -34,12 +55,9 @@ function (cb::IpoptProgressLogger)( alpha_pr::Float64, ls_trials::Cint ) - n = cb.cache.n - m = cb.cache.num_cons - u, z_L, z_U = zeros(n), zeros(n), zeros(n) - g, lambda = zeros(m), zeros(m) scaled = false - Ipopt.GetIpoptCurrentIterate(cb.prob, scaled, n, u, z_L, z_U, m, g, lambda) + Ipopt.GetIpoptCurrentIterate( + cb.prob, scaled, cb.n, cb.u, cb.z_L, cb.z_U, cb.num_cons, cb.g, cb.lambda) original = IpoptState( alg_mod, @@ -53,17 +71,19 @@ function (cb::IpoptProgressLogger)( alpha_du, alpha_pr, ls_trials, - z_L, - z_U, - lambda + cb.u, + cb.z_L, + cb.z_U, + cb.g, + cb.lambda ) opt_state = Optimization.OptimizationState(; - iter = Int(iter_count), u, objective = obj_value, original) - cb.cache.iterations = iter_count + iter = Int(iter_count), cb.u, objective = obj_value, original) + cb.iterations[] = Int(iter_count) - if cb.cache.progress - maxiters = cb.cache.solver_args.maxiters + if cb.progress + maxiters = cb.maxiters msg = "objective: " * sprint(show, obj_value, context = :compact => true) if !isnothing(maxiters) @@ -72,10 +92,10 @@ function (cb::IpoptProgressLogger)( _id=:OptimizationIpopt) end end - if !isnothing(cb.cache.callback) + if !isnothing(cb.callback) # return `true` to keep going, or `false` to terminate the optimization # this is the other way around compared to Optimization.jl callbacks - !cb.cache.callback(opt_state, obj_value) + !cb.callback(opt_state, obj_value) else true end diff --git a/lib/OptimizationIpopt/test/additional_tests.jl b/lib/OptimizationIpopt/test/additional_tests.jl index abc15bcde..ec2836703 100644 --- a/lib/OptimizationIpopt/test/additional_tests.jl +++ b/lib/OptimizationIpopt/test/additional_tests.jl @@ -220,8 +220,8 @@ end @testset "BFGS approximation" begin optfunc = OptimizationFunction(rosenbrock, Optimization.AutoZygote()) prob = OptimizationProblem(optfunc, x0, p) - sol = solve(prob, IpoptOptimizer(); - hessian_approximation = "limited-memory") + sol = solve(prob, IpoptOptimizer( + hessian_approximation = "limited-memory")) @test SciMLBase.successful_retcode(sol) @test sol.u ≈ [1.0, 1.0] atol=1e-4 @@ -230,9 +230,9 @@ end @testset "SR1 approximation" begin optfunc = OptimizationFunction(rosenbrock, Optimization.AutoZygote()) prob = OptimizationProblem(optfunc, x0, p) - sol = solve(prob, IpoptOptimizer(); + sol = solve(prob, IpoptOptimizer( hessian_approximation = "limited-memory", - limited_memory_update_type = "sr1") + limited_memory_update_type = "sr1")) @test SciMLBase.successful_retcode(sol) @test sol.u ≈ [1.0, 1.0] atol=1e-4 diff --git a/lib/OptimizationIpopt/test/advanced_features.jl b/lib/OptimizationIpopt/test/advanced_features.jl index 1e34b253d..aa42b6900 100644 --- a/lib/OptimizationIpopt/test/advanced_features.jl +++ b/lib/OptimizationIpopt/test/advanced_features.jl @@ -21,10 +21,10 @@ using SparseArrays prob = OptimizationProblem(optfunc, x0, p) # Test with tight tolerances - sol = solve(prob, IpoptOptimizer(); - reltol = 1e-10, + sol = solve(prob, IpoptOptimizer( acceptable_tol = 1e-8, - acceptable_iter = 5) + acceptable_iter = 5); + reltol = 1e-10) @test SciMLBase.successful_retcode(sol) @test sol.u ≈ [1.0, 1.0] atol=1e-8 @@ -46,8 +46,8 @@ using SparseArrays lcons = [0.0, 0.0], ucons = [0.0, 0.0]) - sol = solve(prob, IpoptOptimizer(); - constr_viol_tol = 1e-8) + sol = solve(prob, IpoptOptimizer( + constr_viol_tol = 1e-8)) @test SciMLBase.successful_retcode(sol) @test sol.u[1] + sol.u[2] ≈ 2.0 atol=1e-7 @@ -64,9 +64,11 @@ using SparseArrays prob = OptimizationProblem(optfunc, [0.1, 0.1], nothing) # Run with derivative test level 1 (first derivatives only) - sol = solve(prob, IpoptOptimizer(); - derivative_test = "first-order", - derivative_test_tol = 1e-4) + sol = solve(prob, IpoptOptimizer( + additional_options = Dict( + "derivative_test" => "first-order", + "derivative_test_tol" => 1e-4 + ))) @test SciMLBase.successful_retcode(sol) end @@ -92,8 +94,8 @@ using SparseArrays prob = OptimizationProblem(optfunc, x0, p) # Test with different linear solver strategies - sol = solve(prob, IpoptOptimizer(); - linear_solver = "mumps") # or "ma27", "ma57", etc. if available + sol = solve(prob, IpoptOptimizer( + linear_solver = "mumps")) # or "ma27", "ma57", etc. if available @test SciMLBase.successful_retcode(sol) # Check that odd indices are close to 1 @@ -117,8 +119,8 @@ using SparseArrays ucons = [0.0]) # Solve with automatic scaling - sol = solve(prob, IpoptOptimizer(); - nlp_scaling_method = "gradient-based") + sol = solve(prob, IpoptOptimizer( + nlp_scaling_method = "gradient-based")) @test SciMLBase.successful_retcode(sol) # Check constraint satisfaction @@ -145,8 +147,10 @@ using SparseArrays lcons = [0.0, 0.0], ucons = [0.0, 0.0]) - sol = solve(prob, IpoptOptimizer(); - required_infeasibility_reduction = 0.9) + sol = solve(prob, IpoptOptimizer( + additional_options = Dict( + "required_infeasibility_reduction" => 0.9 + ))) if SciMLBase.successful_retcode(sol) # Check constraint satisfaction if successful @@ -167,16 +171,16 @@ using SparseArrays prob = OptimizationProblem(optfunc, x0, p) # Test adaptive mu strategy - sol = solve(prob, IpoptOptimizer(); + sol = solve(prob, IpoptOptimizer( mu_strategy = "adaptive", - mu_init = 0.1) + mu_init = 0.1)) @test SciMLBase.successful_retcode(sol) @test sol.u ≈ [1.0, 1.0] atol=1e-4 # Test monotone mu strategy - sol2 = solve(prob, IpoptOptimizer(); - mu_strategy = "monotone") + sol2 = solve(prob, IpoptOptimizer( + mu_strategy = "monotone")) @test SciMLBase.successful_retcode(sol2) @test sol2.u ≈ [1.0, 1.0] atol=1e-4 @@ -194,8 +198,10 @@ using SparseArrays lb = [-Inf, 2.0, -Inf], ub = [Inf, 2.0, Inf]) - sol = solve(prob, IpoptOptimizer(); - fixed_variable_treatment = "make_parameter") + sol = solve(prob, IpoptOptimizer( + additional_options = Dict( + "fixed_variable_treatment" => "make_parameter" + ))) @test SciMLBase.successful_retcode(sol) @test sol.u ≈ [1.0, 2.0, 3.0] atol=1e-6 @@ -213,9 +219,9 @@ using SparseArrays prob = OptimizationProblem(optfunc, zeros(n), nothing; sense = Optimization.MaxSense) - sol = solve(prob, IpoptOptimizer(); + sol = solve(prob, IpoptOptimizer( acceptable_tol = 1e-4, - acceptable_iter = 10, + acceptable_iter = 10); maxiters = 50) @test SciMLBase.successful_retcode(sol) @@ -240,7 +246,7 @@ end end @testset "Timing statistics" begin - sol = solve(prob, IpoptOptimizer(); print_timing_statistics = "yes") + sol = solve(prob, IpoptOptimizer(print_timing_statistics = "yes")) @test SciMLBase.successful_retcode(sol) end end diff --git a/lib/OptimizationIpopt/test/problem_types.jl b/lib/OptimizationIpopt/test/problem_types.jl index 9db84f19e..d47558629 100644 --- a/lib/OptimizationIpopt/test/problem_types.jl +++ b/lib/OptimizationIpopt/test/problem_types.jl @@ -155,7 +155,9 @@ using SparseArrays lb = [0.0, 0.0, -1.0], # a, b > 0 ub = [10.0, 10.0, 1.0]) - sol = solve(prob, IpoptOptimizer(), tol=1e-10, acceptable_tol=1e-10) + sol = solve(prob, IpoptOptimizer( + acceptable_tol = 1e-10); + reltol = 1e-10) @test SciMLBase.successful_retcode(sol) # Parameters should be close to true values (within noise) @@ -320,8 +322,8 @@ end lb = fill(-2π, n), ub = fill(2π, n)) - sol = solve(prob, IpoptOptimizer(); - hessian_approximation = "limited-memory") + sol = solve(prob, IpoptOptimizer( + hessian_approximation = "limited-memory")) @test SciMLBase.successful_retcode(sol) end diff --git a/lib/OptimizationIpopt/test/runtests.jl b/lib/OptimizationIpopt/test/runtests.jl index 40f3faf20..8d335ec0c 100644 --- a/lib/OptimizationIpopt/test/runtests.jl +++ b/lib/OptimizationIpopt/test/runtests.jl @@ -19,11 +19,11 @@ callback = function (state, l) return false end -sol = solve(prob, IpoptOptimizer(); callback, hessian_approximation = "exact") +sol = solve(prob, IpoptOptimizer(hessian_approximation = "exact"); callback) @test SciMLBase.successful_retcode(sol) @test sol ≈ [1, 1] -sol = solve(prob, IpoptOptimizer(); callback, hessian_approximation = "limited-memory") +sol = solve(prob, IpoptOptimizer(hessian_approximation = "limited-memory"); callback) @test SciMLBase.successful_retcode(sol) @test sol ≈ [1, 1] @@ -75,6 +75,7 @@ include("additional_tests.jl") include("advanced_features.jl") include("problem_types.jl") + @testset "tutorial" begin rosenbrock(x, p) = (p[1] - x[1])^2 + p[2] * (x[2] - x[1]^2)^2 x0 = zeros(2) @@ -115,3 +116,81 @@ end @test sol.u ≈ [2.0] # ≈ [2] end end + +@testset "Additional Options and Common Interface" begin + rosenbrock(x, p) = (p[1] - x[1])^2 + p[2] * (x[2] - x[1]^2)^2 + x0 = zeros(2) + p = [1.0, 100.0] + + @testset "additional_options dictionary" begin + optfunc = OptimizationFunction(rosenbrock, Optimization.AutoZygote()) + prob = OptimizationProblem(optfunc, x0, p) + + # Test with various option types + opt = IpoptOptimizer( + additional_options = Dict( + "derivative_test" => "first-order", # String + "derivative_test_tol" => 1e-4, # Float64 + "derivative_test_print_all" => "yes" # String + ) + ) + sol = solve(prob, opt) + @test SciMLBase.successful_retcode(sol) + + # Test options not in struct fields + opt2 = IpoptOptimizer( + additional_options = Dict( + "fixed_variable_treatment" => "make_parameter", + "required_infeasibility_reduction" => 0.9, + "alpha_for_y" => "primal" + ) + ) + sol2 = solve(prob, opt2) + @test SciMLBase.successful_retcode(sol2) + end + + @testset "Common interface arguments override" begin + optfunc = OptimizationFunction(rosenbrock, Optimization.AutoZygote()) + prob = OptimizationProblem(optfunc, x0, p) + + # Test that reltol overrides default tolerance + sol1 = solve(prob, IpoptOptimizer(); reltol = 1e-12) + @test SciMLBase.successful_retcode(sol1) + @test sol1.u ≈ [1.0, 1.0] atol=1e-10 + + # Test that maxiters limits iterations + sol2 = solve(prob, IpoptOptimizer(); maxiters = 5) + # May not converge with only 5 iterations + @test sol2.stats.iterations <= 5 + + # Test verbose levels + for verbose in [false, true, 0, 3, 5] + sol = solve(prob, IpoptOptimizer(); verbose = verbose, maxiters = 10) + @test sol isa SciMLBase.OptimizationSolution + end + + # Test maxtime + sol3 = solve(prob, IpoptOptimizer(); maxtime = 10.0) + @test SciMLBase.successful_retcode(sol3) + end + + @testset "Priority: struct < additional_options < solve args" begin + optfunc = OptimizationFunction(rosenbrock, Optimization.AutoZygote()) + prob = OptimizationProblem(optfunc, x0, p) + + # Struct field is overridden by solve argument + opt = IpoptOptimizer( + acceptable_tol = 1e-4, # Struct field + additional_options = Dict( + "max_iter" => 100 # Will be overridden by maxiters + ) + ) + + sol = solve(prob, opt; + maxiters = 50, # Should override additional_options + reltol = 1e-10) # Should set tol + + @test sol.stats.iterations <= 50 + @test SciMLBase.successful_retcode(sol) + end +end