Skip to content

Commit be6fbd2

Browse files
SebastianM-Cclaude
andauthored
Use the default OptimizationCache in OptimizationIpopt (#1257)
Replace the bespoke IpoptCache (copied from OptimizationMOI's evaluator/cache split) with OptimizationBase's default OptimizationCache, mirroring the structure of OptimizationMadNLP: the package now only defines the optimizer struct, the SciMLBase traits, and __solve(::OptimizationCache{<:IpoptOptimizer}), with the solver-specific state (derivative buffers, sparsity, call counters) held by a transient IpoptEvaluator built inside __solve. This removes the hand-rolled __init, the ReInitCache delegation, the solution accessors, and the dead MOI leftovers (int, obj_expr/cons_expr, discarded sys). Behavior fixes that fell out of the refactor: - MaxSense is now handled with Ipopt's obj_scaling_factor = -1 instead of manually negating in the evaluators, fixing the sign of sol.objective and of the value passed to user callbacks. - The live cache.p is forwarded to the instantiated derivative closures (guarded for AutoSymbolics/NoAD/parameterless paths), so reinit!(cache; p = ...) no longer silently converges to the optimum of the old parameters. - requireshessian/requiresconshess/requireslagh are conditional on hessian_approximation, so limited-memory runs skip second-order AD entirely and pass eval_h = nothing; requireslagh was previously missing and the unused cons_vjp is no longer generated (Ipopt's C API has no vjp/jvp callbacks). Test suite cleanup, cross-checked against the Ipopt repo: - Fix the mistranslated MyNLP problem and the invented LuksanVlcek1 constraint bounds/starting point; pin their optima. - The restoration phase test's constraints are mutually infeasible, so assert ReturnCode.Infeasible instead of a vacuous guarded check. - Make the warm-start test actually warm-start via reinit!, verify print levels through Ipopt's output_file, make the option-priority test discriminating, check the derivative checker's verdict, and seed the stress-test RNGs. - Add tests for the MaxSense objective sign, reinit! with new parameters, and the limited-memory trait behavior. - Prune redundant testsets (NLS, mixed-integer, non-smooth robust optimization, dead complementarity block, duplicate verbose loop) and drop the inaccurate "automatically translated" headers, keeping EPL attribution on the genuinely Ipopt-derived problems. MTK reinit! stays @test_broken: both the plain-vector and symbolic-map forms are blocked upstream (ReInitCache field conversion and SciMLBase's process_p_u0_symbolic fallback). Claude-Session: https://claude.ai/code/session_01BTyNNDwaNrJFyNzN6XTqcn Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 3b0cff7 commit be6fbd2

9 files changed

Lines changed: 490 additions & 633 deletions

File tree

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 = "1.2.3"
4+
version = "1.3.0"
55
[deps]
66
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"
77
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

lib/OptimizationIpopt/src/OptimizationIpopt.jl

Lines changed: 79 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export IpoptOptimizer
1313
"""
1414
IpoptOptimizer(; kwargs...)
1515
16-
Optimizer using the Interior Point Optimizer (Ipopt) for nonlinear OptimizationBase.
16+
Optimizer using the Interior Point Optimizer (Ipopt) for nonlinear optimization.
1717
1818
Ipopt is designed to find (local) solutions of mathematical optimization problems of the form:
1919
@@ -134,7 +134,7 @@ https://coin-or.github.io/Ipopt/OPTIONS.html
134134
linear_system_scaling::String = "none"
135135
hsllib::String = ""
136136
pardisolib::String = ""
137-
linear_scaling_on_demand = "yes"
137+
linear_scaling_on_demand::String = "yes"
138138

139139
# NLP options
140140
nlp_scaling_method::String = "gradient-based"
@@ -174,22 +174,29 @@ end
174174
SciMLBase.allowscallback(alg::IpoptOptimizer) = true
175175
OptimizationBase.supports_sense(::IpoptOptimizer) = true
176176

177-
# Compatibility with OptimizationBase@v3
178-
function SciMLBase.supports_opt_cache_interface(alg::IpoptOptimizer)
179-
return true
177+
# Second order derivatives are only needed when Ipopt uses the exact Hessian
178+
# (the user may request the limited-memory quasi-Newton approximation either
179+
# via the struct field or via `additional_options`).
180+
function _exact_hessian(opt::IpoptOptimizer)
181+
return get(
182+
opt.additional_options, "hessian_approximation", opt.hessian_approximation
183+
) == "exact"
180184
end
181185

182186
function SciMLBase.requiresgradient(opt::IpoptOptimizer)
183187
return true
184188
end
185189
function SciMLBase.requireshessian(opt::IpoptOptimizer)
186-
return true
190+
return _exact_hessian(opt)
187191
end
188192
function SciMLBase.requiresconsjac(opt::IpoptOptimizer)
189193
return true
190194
end
191195
function SciMLBase.requiresconshess(opt::IpoptOptimizer)
192-
return true
196+
return _exact_hessian(opt)
197+
end
198+
function SciMLBase.requireslagh(opt::IpoptOptimizer)
199+
return _exact_hessian(opt)
193200
end
194201

195202
function SciMLBase.allowsbounds(opt::IpoptOptimizer)
@@ -199,59 +206,74 @@ function SciMLBase.allowsconstraints(opt::IpoptOptimizer)
199206
return true
200207
end
201208

202-
include("cache.jl")
209+
include("evaluator.jl")
203210
include("callback.jl")
204211

205212
function __map_optimizer_args(
206-
cache,
213+
evaluator::IpoptEvaluator,
207214
opt::IpoptOptimizer;
208215
maxiters::Union{Number, Nothing} = nothing,
209216
maxtime::Union{Number, Nothing} = nothing,
210217
abstol::Union{Number, Nothing} = nothing,
211218
reltol::Union{Number, Nothing} = nothing,
212219
verbose = false,
213220
progress::Bool = false,
214-
callback = nothing
221+
callback = nothing,
222+
iterations::Ref{Int} = Ref(0)
215223
)
216-
jacobian_sparsity = jacobian_structure(cache)
217-
hessian_sparsity = hessian_lagrangian_structure(cache)
218-
219-
eval_f(x) = eval_objective(cache, x)
220-
eval_grad_f(x, grad_f) = eval_objective_gradient(cache, grad_f, x)
221-
eval_g(x, g) = eval_constraint(cache, g, x)
224+
cache = evaluator.cache
225+
n = length(cache.u0)
226+
num_cons = cache.ucons === nothing ? 0 : length(cache.ucons)
227+
lb = isnothing(cache.lb) ? fill(-Inf, n) : cache.lb
228+
ub = isnothing(cache.ub) ? fill(Inf, n) : cache.ub
229+
lcons = isnothing(cache.lcons) ? fill(-Inf, num_cons) : Vector{Float64}(cache.lcons)
230+
ucons = isnothing(cache.ucons) ? fill(Inf, num_cons) : Vector{Float64}(cache.ucons)
231+
232+
jacobian_sparsity = jacobian_structure(evaluator)
233+
234+
eval_f(x) = eval_objective(evaluator, x)
235+
eval_grad_f(x, grad_f) = eval_objective_gradient(evaluator, grad_f, x)
236+
eval_g(x, g) = eval_constraint(evaluator, g, x)
222237
function eval_jac_g(x, rows, cols, values)
223238
if values === nothing
224-
for i in 1:length(jacobian_sparsity)
239+
for i in eachindex(jacobian_sparsity)
225240
rows[i], cols[i] = jacobian_sparsity[i]
226241
end
227242
else
228-
eval_constraint_jacobian(cache, values, x)
243+
eval_constraint_jacobian(evaluator, values, x)
229244
end
230245
return
231246
end
232-
function eval_h(x, rows, cols, obj_factor, lambda, values)
233-
if values === nothing
234-
for i in 1:length(hessian_sparsity)
235-
rows[i], cols[i] = hessian_sparsity[i]
247+
248+
if _exact_hessian(opt)
249+
hessian_sparsity = hessian_lagrangian_structure(evaluator)
250+
nnz_hess = length(hessian_sparsity)
251+
eval_h = function (x, rows, cols, obj_factor, lambda, values)
252+
if values === nothing
253+
for i in eachindex(hessian_sparsity)
254+
rows[i], cols[i] = hessian_sparsity[i]
255+
end
256+
else
257+
eval_hessian_lagrangian(evaluator, values, x, obj_factor, lambda)
236258
end
237-
else
238-
eval_hessian_lagrangian(cache, values, x, obj_factor, lambda)
259+
return
239260
end
240-
return
261+
else
262+
# Ipopt never calls the Hessian callback with
263+
# hessian_approximation = "limited-memory"
264+
nnz_hess = 0
265+
eval_h = nothing
241266
end
242267

243-
lb = isnothing(cache.lb) ? fill(-Inf, cache.n) : cache.lb
244-
ub = isnothing(cache.ub) ? fill(Inf, cache.n) : cache.ub
245-
246268
prob = Ipopt.CreateIpoptProblem(
247-
cache.n,
269+
n,
248270
lb,
249271
ub,
250-
cache.num_cons,
251-
cache.lcons,
252-
cache.ucons,
253-
length(jacobian_structure(cache)),
254-
length(hessian_lagrangian_structure(cache)),
272+
num_cons,
273+
lcons,
274+
ucons,
275+
length(jacobian_sparsity),
276+
nnz_hess,
255277
eval_f,
256278
eval_g,
257279
eval_grad_f,
@@ -261,11 +283,21 @@ function __map_optimizer_args(
261283

262284
# Set up progress callback
263285
progress_callback = IpoptProgressLogger(
264-
progress, callback, prob, cache.n, cache.num_cons, maxiters, cache.iterations
286+
progress, callback, prob, n, num_cons, maxiters, iterations
265287
)
266288
intermediate = (args...) -> progress_callback(args...)
267289
Ipopt.SetIntermediateCallback(prob, intermediate)
268290

291+
# Ipopt only minimizes; a negative objective scaling factor makes it
292+
# maximize instead, while the objective it reports stays unscaled.
293+
obj_scaling = Float64(get(opt.additional_options, "obj_scaling_factor", 1.0))
294+
if cache.sense === OptimizationBase.MaxSense
295+
obj_scaling = -obj_scaling
296+
end
297+
if obj_scaling != 1.0
298+
Ipopt.AddIpoptNumOption(prob, "obj_scaling_factor", obj_scaling)
299+
end
300+
269301
# Apply all options from struct using reflection and type dispatch
270302
for field in propertynames(opt)
271303
field == :additional_options && continue # Skip the dict field
@@ -285,6 +317,7 @@ function __map_optimizer_args(
285317

286318
# Apply additional options with type dispatch
287319
for (key, value) in opt.additional_options
320+
key == "obj_scaling_factor" && continue # Applied above, sense-aware
288321
if value isa Int
289322
Ipopt.AddIpoptIntOption(prob, key, value)
290323
elseif value isa Float64
@@ -301,7 +334,7 @@ function __map_optimizer_args(
301334
if !isnothing(abstol)
302335
@SciMLMessage(
303336
lazy"common abstol is currently not used by $(opt)",
304-
verbose, :unsupported_kwargs
337+
cache.verbose, :unsupported_kwargs
305338
)
306339
end
307340
!isnothing(reltol) && !in("tol", optkeys) && Ipopt.AddIpoptNumOption(prob, "tol", reltol)
@@ -352,20 +385,24 @@ function map_retcode(solvestat)
352385
end
353386
end
354387

355-
function SciMLBase.__solve(cache::IpoptCache)
388+
function SciMLBase.__solve(cache::OptimizationCache{O}) where {O <: IpoptOptimizer}
356389
maxiters = OptimizationBase._check_and_convert_maxiters(cache.solver_args.maxiters)
357390
maxtime = OptimizationBase._check_and_convert_maxtime(cache.solver_args.maxtime)
358391

392+
evaluator = IpoptEvaluator(cache)
393+
iterations = Ref(0)
394+
359395
opt_setup = __map_optimizer_args(
360-
cache,
396+
evaluator,
361397
cache.opt;
362398
abstol = cache.solver_args.abstol,
363399
reltol = cache.solver_args.reltol,
364400
maxiters = maxiters,
365401
maxtime = maxtime,
366-
verbose = cache.solver_args.verbose,
402+
verbose = get(cache.solver_args, :verbose, cache.verbose),
367403
progress = cache.progress,
368-
callback = cache.callback
404+
callback = cache.callback,
405+
iterations = iterations
369406
)
370407

371408
opt_setup.x .= cache.reinit_cache.u0
@@ -385,7 +422,8 @@ function SciMLBase.__solve(cache::IpoptCache)
385422

386423
stats = OptimizationBase.OptimizationStats(;
387424
time = time() - start_time,
388-
iterations = cache.iterations[], fevals = cache.f_calls, gevals = cache.f_grad_calls
425+
iterations = iterations[], fevals = evaluator.f_calls,
426+
gevals = evaluator.f_grad_calls
389427
)
390428

391429
finalize(opt_setup)
@@ -401,28 +439,4 @@ function SciMLBase.__solve(cache::IpoptCache)
401439
)
402440
end
403441

404-
function SciMLBase.__init(
405-
prob::OptimizationProblem,
406-
opt::IpoptOptimizer;
407-
maxiters::Union{Number, Nothing} = nothing,
408-
maxtime::Union{Number, Nothing} = nothing,
409-
abstol::Union{Number, Nothing} = nothing,
410-
reltol::Union{Number, Nothing} = nothing,
411-
progress::Bool = false,
412-
kwargs...
413-
)
414-
cache = IpoptCache(
415-
prob, opt;
416-
maxiters,
417-
maxtime,
418-
abstol,
419-
reltol,
420-
progress,
421-
kwargs...
422-
)
423-
cache.reinit_cache.u0 .= prob.u0
424-
425-
return cache
426-
end
427-
428442
end # OptimizationIpopt

0 commit comments

Comments
 (0)