Skip to content

Commit ea28cc4

Browse files
SebastianM-CClaude
andcommitted
refactor(OptimizationIpopt)!: IpoptOptimizer now stores solver specific options
Move Ipopt-specific options from solve() to IpoptOptimizer struct fields. Options are now set via the struct constructor or additional_options dict, while common interface args (reltol, maxiters, maxtime, verbose) remain in solve(). BREAKING CHANGE: Ipopt options must now be passed to IpoptOptimizer() constructor instead of solve(). Co-authored-by: Claude <claude@anthropic.com>
1 parent b32c9be commit ea28cc4

File tree

4 files changed

+218
-41
lines changed

4 files changed

+218
-41
lines changed

lib/OptimizationIpopt/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "OptimizationIpopt"
22
uuid = "43fad042-7963-4b32-ab19-e2a4f9a67124"
33
authors = ["Sebastian Micluța-Câmpeanu <sebastian.mc95@proton.me> and contributors"]
4-
version = "0.1.2"
4+
version = "0.2.0"
55

66
[deps]
77
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"

lib/OptimizationIpopt/src/OptimizationIpopt.jl

Lines changed: 202 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,162 @@ export IpoptOptimizer
1111

1212
const DenseOrSparse{T} = Union{Matrix{T}, SparseMatrixCSC{T}}
1313

14-
struct IpoptOptimizer end
14+
"""
15+
IpoptOptimizer(; kwargs...)
16+
17+
Optimizer using the Interior Point Optimizer (Ipopt) for nonlinear optimization.
18+
19+
Ipopt is designed to find (local) solutions of mathematical optimization problems of the form:
20+
21+
min f(x)
22+
s.t. g_L ≤ g(x) ≤ g_U
23+
x_L ≤ x ≤ x_U
24+
25+
where f(x) and g(x) are twice continuously differentiable functions.
26+
27+
# Common Interface Arguments
28+
29+
The following common optimization arguments can be passed to `solve`:
30+
- `reltol`: Overrides the Ipopt `tol` option (desired convergence tolerance)
31+
- `maxiters`: Overrides the Ipopt `max_iter` option (maximum iterations)
32+
- `maxtime`: Overrides the Ipopt `max_wall_time` option (maximum wall clock time)
33+
- `verbose`: Overrides the Ipopt `print_level` option (0 for silent, 5 for default, up to 12 for maximum verbosity)
34+
35+
# Keyword Arguments
36+
37+
## Termination Options
38+
- `acceptable_tol::Float64 = 1e-6`: Acceptable convergence tolerance (relative)
39+
- `acceptable_iter::Int = 15`: Number of acceptable iterations before termination
40+
- `dual_inf_tol::Float64 = 1.0`: Desired threshold for dual infeasibility
41+
- `constr_viol_tol::Float64 = 1e-4`: Desired threshold for constraint violation
42+
- `compl_inf_tol::Float64 = 1e-4`: Desired threshold for complementarity conditions
43+
44+
## Output Options
45+
- `print_timing_statistics::String = "no"`: Print timing statistics at end of optimization
46+
- `print_info_string::String = "no"`: Print info string with algorithm details
47+
48+
## Linear Solver Options
49+
- `linear_solver::String = "mumps"`: Linear solver to use (mumps, ma27, ma57, ma86, ma97, pardiso, wsmp, etc.)
50+
- `linear_system_scaling::String = "none"`: Method for scaling linear system (none, mc19, slack-based)
51+
- `hsllib::String = ""`: Path to HSL library (if using HSL solvers)
52+
- `pardisolib::String = ""`: Path to Pardiso library (if using Pardiso)
53+
- `linear_scaling_on_demand::String = "yes"`: Enable scaling on demand for linear systems
54+
55+
## NLP Scaling Options
56+
- `nlp_scaling_method::String = "gradient-based"`: Scaling method for NLP (none, user-scaling, gradient-based, equilibration-based)
57+
- `nlp_scaling_max_gradient::Float64 = 100.0`: Maximum gradient after scaling
58+
- `honor_original_bounds::String = "no"`: Honor original variable bounds after scaling
59+
- `check_derivatives_for_naninf::String = "no"`: Check derivatives for NaN/Inf values
60+
61+
## Barrier Parameter Options
62+
- `mu_strategy::String = "monotone"`: Update strategy for barrier parameter (monotone, adaptive)
63+
- `mu_oracle::String = "quality-function"`: Oracle for adaptive mu strategy
64+
- `mu_init::Float64 = 0.1`: Initial value for barrier parameter
65+
- `adaptive_mu_globalization::String = "obj-constr-filter"`: Globalization strategy for adaptive mu
66+
67+
## Warm Start Options
68+
- `warm_start_init_point::String = "no"`: Use warm start from previous solution
69+
70+
## Hessian Options
71+
- `hessian_approximation::String = "exact"`: How to approximate the Hessian (exact, limited-memory)
72+
- `limited_memory_max_history::Int = 6`: History size for limited-memory Hessian approximation
73+
- `limited_memory_update_type::String = "bfgs"`: Quasi-Newton update formula for limited-memory approximation (bfgs, sr1)
74+
75+
## Line Search Options
76+
- `accept_every_trial_step::String = "no"`: Accept every trial step (disables line search)
77+
- `line_search_method::String = "filter"`: Line search method (filter, penalty, cg-penalty)
78+
79+
## Restoration Phase Options
80+
- `expect_infeasible_problem::String = "no"`: Enable if problem is expected to be infeasible
81+
82+
## Additional Options
83+
- `additional_options::Dict{String, Any} = Dict()`: Dictionary to set any other Ipopt option not explicitly listed above.
84+
See https://coin-or.github.io/Ipopt/OPTIONS.html for the full list of available options.
85+
86+
# Examples
87+
88+
```julia
89+
using Optimization, OptimizationIpopt
90+
91+
# Basic usage with default settings
92+
opt = IpoptOptimizer()
93+
94+
# Customized settings
95+
opt = IpoptOptimizer(
96+
linear_solver = "ma57", # needs HSL solvers configured
97+
nlp_scaling_method = "equilibration-based",
98+
hessian_approximation = "limited-memory",
99+
additional_options = Dict(
100+
"alpha_for_y" => "primal",
101+
"recalc_y" => "yes"
102+
)
103+
)
104+
105+
# Solve with common interface arguments
106+
result = solve(prob, opt;
107+
reltol = 1e-8, # Sets Ipopt's tol
108+
maxiters = 5000, # Sets Ipopt's max_iter
109+
maxtime = 300.0, # Sets Ipopt's max_wall_time (in seconds)
110+
verbose = 3 # Sets Ipopt's print_level
111+
)
112+
```
113+
114+
# References
115+
116+
For complete documentation of all Ipopt options, see:
117+
https://coin-or.github.io/Ipopt/OPTIONS.html
118+
"""
119+
@kwdef struct IpoptOptimizer
120+
# Most common Ipopt-specific options (excluding common interface options)
121+
122+
# Termination
123+
acceptable_tol::Float64 = 1e-6
124+
acceptable_iter::Int = 15
125+
dual_inf_tol::Float64 = 1.0
126+
constr_viol_tol::Float64 = 1e-4
127+
compl_inf_tol::Float64 = 1e-4
128+
129+
# Output options
130+
print_timing_statistics::String = "no"
131+
print_info_string::String = "no"
132+
133+
# Linear solver
134+
linear_solver::String = "mumps"
135+
linear_system_scaling::String = "none"
136+
hsllib::String = ""
137+
pardisolib::String = ""
138+
linear_scaling_on_demand = "yes"
139+
140+
# NLP options
141+
nlp_scaling_method::String = "gradient-based"
142+
nlp_scaling_max_gradient::Float64 = 100.0
143+
honor_original_bounds::String = "no"
144+
check_derivatives_for_naninf::String = "no"
145+
146+
# Barrier parameter
147+
mu_strategy::String = "monotone"
148+
mu_oracle::String = "quality-function"
149+
mu_init::Float64 = 0.1
150+
adaptive_mu_globalization::String = "obj-constr-filter"
151+
152+
# Warm start
153+
warm_start_init_point::String = "no"
154+
155+
# Hessian approximation
156+
hessian_approximation::String = "exact"
157+
limited_memory_max_history::Int = 6
158+
limited_memory_update_type::String = "bfgs"
159+
160+
# Line search
161+
accept_every_trial_step::String = "no"
162+
line_search_method::String = "filter"
163+
164+
# Restoration phase
165+
expect_infeasible_problem::String = "no"
166+
167+
# Additional options for any other Ipopt parameters
168+
additional_options::Dict{String, Any} = Dict{String, Any}()
169+
end
15170

16171
@static if isdefined(SciMLBase, :supports_opt_cache_interface)
17172
function SciMLBase.supports_opt_cache_interface(alg::IpoptOptimizer)
@@ -53,11 +208,9 @@ function __map_optimizer_args(cache,
53208
maxtime::Union{Number, Nothing} = nothing,
54209
abstol::Union{Number, Nothing} = nothing,
55210
reltol::Union{Number, Nothing} = nothing,
56-
hessian_approximation = "exact",
57211
verbose = false,
58-
progress = false,
59-
callback = nothing,
60-
kwargs...)
212+
progress::Bool = false,
213+
callback = nothing)
61214
jacobian_sparsity = jacobian_structure(cache)
62215
hessian_sparsity = hessian_lagrangian_structure(cache)
63216

@@ -103,37 +256,53 @@ function __map_optimizer_args(cache,
103256
eval_jac_g,
104257
eval_h
105258
)
106-
progress_callback = IpoptProgressLogger(cache.progress, cache, prob)
259+
260+
# Set up progress callback
261+
progress_callback = IpoptProgressLogger(progress, callback, prob, cache.n, cache.num_cons, maxiters, cache.iterations)
107262
intermediate = (args...) -> progress_callback(args...)
108263
Ipopt.SetIntermediateCallback(prob, intermediate)
109264

110-
if !isnothing(maxiters)
111-
Ipopt.AddIpoptIntOption(prob, "max_iter", maxiters)
112-
end
113-
if !isnothing(maxtime)
114-
Ipopt.AddIpoptNumOption(prob, "max_wall_time", float(maxtime))
265+
# Apply all options from struct using reflection and type dispatch
266+
for field in propertynames(opt)
267+
field == :additional_options && continue # Skip the dict field
268+
269+
field_str = string(field)
270+
value = getproperty(opt, field)
271+
272+
# Apply option based on type
273+
if value isa Int
274+
Ipopt.AddIpoptIntOption(prob, field_str, value)
275+
elseif value isa Float64
276+
Ipopt.AddIpoptNumOption(prob, field_str, value)
277+
elseif value isa String
278+
Ipopt.AddIpoptStrOption(prob, field_str, value)
279+
end
115280
end
116-
if !isnothing(reltol)
117-
Ipopt.AddIpoptNumOption(prob, "tol", reltol)
281+
282+
# Apply additional options with type dispatch
283+
for (key, value) in opt.additional_options
284+
if value isa Int
285+
Ipopt.AddIpoptIntOption(prob, key, value)
286+
elseif value isa Float64
287+
Ipopt.AddIpoptNumOption(prob, key, float(value))
288+
elseif value isa String
289+
Ipopt.AddIpoptStrOption(prob, key, value)
290+
else
291+
error("Unsupported option type $(typeof(value)) for option $key. Must be Int, Float64, or String")
292+
end
118293
end
294+
295+
# Override with common interface arguments if provided
296+
!isnothing(reltol) && Ipopt.AddIpoptNumOption(prob, "tol", reltol)
297+
!isnothing(maxiters) && Ipopt.AddIpoptIntOption(prob, "max_iter", maxiters)
298+
!isnothing(maxtime) && Ipopt.AddIpoptNumOption(prob, "max_wall_time", Float64(maxtime))
299+
300+
# Handle verbose override
119301
if verbose isa Bool
120302
Ipopt.AddIpoptIntOption(prob, "print_level", verbose * 5)
121-
else
303+
elseif verbose isa Int
122304
Ipopt.AddIpoptIntOption(prob, "print_level", verbose)
123305
end
124-
Ipopt.AddIpoptStrOption(prob, "hessian_approximation", hessian_approximation)
125-
126-
for kw in pairs(kwargs)
127-
if kw[2] isa Int
128-
Ipopt.AddIpoptIntOption(prob, string(kw[1]), kw[2])
129-
elseif kw[2] isa Float64
130-
Ipopt.AddIpoptNumOption(prob, string(kw[1]), kw[2])
131-
elseif kw[2] isa String
132-
Ipopt.AddIpoptStrOption(prob, string(kw[1]), kw[2])
133-
else
134-
error("Keyword argument type $(typeof(kw[2])) not recognized")
135-
end
136-
end
137306

138307
return prob
139308
end
@@ -173,7 +342,9 @@ function SciMLBase.__solve(cache::IpoptCache)
173342
reltol = cache.solver_args.reltol,
174343
maxiters = maxiters,
175344
maxtime = maxtime,
176-
cache.solver_args...)
345+
verbose = get(cache.solver_args, :verbose, false),
346+
progress = cache.progress,
347+
callback = get(cache.solver_args, :callback, nothing))
177348

178349
opt_setup.x .= cache.reinit_cache.u0
179350

@@ -191,7 +362,7 @@ function SciMLBase.__solve(cache::IpoptCache)
191362
minimizer = opt_setup.x
192363

193364
stats = Optimization.OptimizationStats(; time = time() - start_time,
194-
iterations = cache.iterations, fevals = cache.f_calls, gevals = cache.f_grad_calls)
365+
iterations = cache.iterations[], fevals = cache.f_calls, gevals = cache.f_grad_calls)
195366

196367
finalize(opt_setup)
197368

@@ -210,12 +381,14 @@ function SciMLBase.__init(prob::OptimizationProblem,
210381
maxtime::Union{Number, Nothing} = nothing,
211382
abstol::Union{Number, Nothing} = nothing,
212383
reltol::Union{Number, Nothing} = nothing,
384+
progress::Bool = false,
213385
kwargs...)
214386
cache = IpoptCache(prob, opt;
215387
maxiters,
216388
maxtime,
217389
abstol,
218390
reltol,
391+
progress,
219392
kwargs...
220393
)
221394
cache.reinit_cache.u0 .= prob.u0

lib/OptimizationIpopt/src/cache.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ mutable struct IpoptCache{T, F <: OptimizationFunction, RC, LB, UB, I, S,
1818
const progress::Bool
1919
f_calls::Int
2020
f_grad_calls::Int
21-
iterations::Cint
21+
const iterations::Ref{Int}
2222
obj_expr::Union{Expr, Nothing}
2323
cons_expr::Union{Vector{Expr}, Nothing}
2424
const opt::O
@@ -139,7 +139,7 @@ function IpoptCache(prob, opt;
139139
progress,
140140
0,
141141
0,
142-
Cint(0),
142+
Ref(0),
143143
obj_expr,
144144
cons_expr,
145145
opt,

lib/OptimizationIpopt/src/callback.jl

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ struct IpoptState
1515
lambda::Vector{Float64}
1616
end
1717

18-
struct IpoptProgressLogger{C <: IpoptCache, P}
18+
struct IpoptProgressLogger{C, P}
1919
progress::Bool
20-
cache::C
20+
callback::C
2121
prob::P
22+
n::Int
23+
num_cons::Int
24+
maxiters::Union{Nothing, Int}
25+
iterations::Ref{Int}
2226
end
2327

2428
function (cb::IpoptProgressLogger)(
@@ -34,8 +38,8 @@ function (cb::IpoptProgressLogger)(
3438
alpha_pr::Float64,
3539
ls_trials::Cint
3640
)
37-
n = cb.cache.n
38-
m = cb.cache.num_cons
41+
n = cb.n
42+
m = cb.num_cons
3943
u, z_L, z_U = zeros(n), zeros(n), zeros(n)
4044
g, lambda = zeros(m), zeros(m)
4145
scaled = false
@@ -60,10 +64,10 @@ function (cb::IpoptProgressLogger)(
6064

6165
opt_state = Optimization.OptimizationState(;
6266
iter = Int(iter_count), u, objective = obj_value, original)
63-
cb.cache.iterations = iter_count
67+
cb.iterations[] = Int(iter_count)
6468

65-
if cb.cache.progress
66-
maxiters = cb.cache.solver_args.maxiters
69+
if cb.progress
70+
maxiters = cb.maxiters
6771
msg = "objective: " *
6872
sprint(show, obj_value, context = :compact => true)
6973
if !isnothing(maxiters)
@@ -72,10 +76,10 @@ function (cb::IpoptProgressLogger)(
7276
_id=:OptimizationIpopt)
7377
end
7478
end
75-
if !isnothing(cb.cache.callback)
79+
if !isnothing(cb.callback)
7680
# return `true` to keep going, or `false` to terminate the optimization
7781
# this is the other way around compared to Optimization.jl callbacks
78-
!cb.cache.callback(opt_state, obj_value)
82+
!cb.callback(opt_state, obj_value)
7983
else
8084
true
8185
end

0 commit comments

Comments
 (0)