Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/src/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ solver = IpoptSolver(nlp)
stats = solve!(solver, nlp, callback = my_callback, print_level = 0)
```

### JSO-style callback signature

In addition to the Ipopt-style callback parameters, this package also accepts a simpler JSO-style callback used across JuliaSmoothOptimizers packages:

- `cb(nlp, solver, stats) -> Bool`

Where `nlp` is the `AbstractNLPModel` being solved, `solver` is the internal Ipopt problem/solver handle, and `stats` is the `GenericExecutionStats` object that will be updated during the solve. The callback should return `true` to continue the optimization or `false` to stop (this maps to Ipopt's user-requested stop).

Example:

```@example ex4
function jso_cb(nlp, solver, stats)
println("iter=", stats.iter, " x=", solver.x)
return stats.iter < 5
end
stats = ipopt(nlp, callback = jso_cb, print_level = 0)
```

### Custom stopping criteria

Callbacks are particularly useful for implementing custom stopping criteria:
Expand Down
69 changes: 55 additions & 14 deletions src/NLPModelsIpopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ function SolverCore.solve!(
nlp::AbstractNLPModel,
stats::GenericExecutionStats;
callback = (args...) -> true,
callback_style::Symbol = :auto,
kwargs...,
)
problem = solver.problem
Expand Down Expand Up @@ -307,20 +308,60 @@ function SolverCore.solve!(
)
set_residuals!(stats, inf_pr, inf_du)
set_iter!(stats, Int(iter_count))
return callback(
alg_mod,
iter_count,
obj_value,
inf_pr,
inf_du,
mu,
d_norm,
regularization_size,
alpha_du,
alpha_pr,
ls_trials,
args...,
)

# Helper to normalize callback return value
_to_bool(rv) = (rv === nothing) ? true : Bool(rv)

if callback_style === :ipopt_full
return _to_bool(callback(
alg_mod,
iter_count,
obj_value,
inf_pr,
inf_du,
mu,
d_norm,
regularization_size,
alpha_du,
alpha_pr,
ls_trials,
args...,
))
elseif callback_style === :ipopt_short
return _to_bool(callback(alg_mod, iter_count, obj_value))
elseif callback_style === :jso
return _to_bool(callback(nlp, problem, stats))
end

try
return _to_bool(callback(nlp, problem, stats))
catch err
if !(isa(err, MethodError) || isa(err, ArgumentError))
rethrow(err)
end
end

try
return _to_bool(callback(
alg_mod,
iter_count,
obj_value,
inf_pr,
inf_du,
mu,
d_norm,
regularization_size,
alpha_du,
alpha_pr,
ls_trials,
args...,
))
catch err
if !(isa(err, MethodError) || isa(err, ArgumentError))
rethrow(err)
end
end
return _to_bool(callback(alg_mod, iter_count, obj_value))
end
SetIntermediateCallback(problem, solver_callback)

Expand Down
74 changes: 74 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,80 @@ end
@test stats.primal_feas ≈ 0.0
# @test stats.dual_feas ≈ 4.63

@testset "JSO callback stops after 5 iterations" begin
function jso_callback(nlp_in, solver_in, stats_in)
@test typeof(nlp_in) <: AbstractNLPModel
@test hasproperty(stats_in, :iter)
return stats_in.iter < 5
end
nlp = ADNLPModel(x -> (x[1] - 1)^2 + 100 * (x[2] - x[1]^2)^2, [-1.2; 1.0])
stats = ipopt(nlp, tol = 1e-12, callback = jso_callback, print_level = 0)
@test stats.status == :user
@test stats.solver_specific[:internal_msg] == :User_Requested_Stop
@test stats.iter == 5
end

@testset "JSO callback can read problem and nlp" begin
function jso_cb_problem_nlp(nlp_in, solver_in, stats_in)
@test typeof(nlp_in) <: AbstractNLPModel
@test length(solver_in.x) == nlp_in.meta.nvar
if nlp_in.meta.ncon > 0
@test length(solver_in.mult_g) == nlp_in.meta.ncon
end
return stats_in.iter < 3
end
nlp = ADNLPModel(x -> (x[1] - 1)^2 + 100 * (x[2] - x[1]^2)^2, [-1.2; 1.0])
stats = ipopt(nlp, callback = jso_cb_problem_nlp, print_level = 0)
@test stats.status == :user
@test stats.iter == 3
end

@testset "Short Ipopt-style 3-arg callback" begin
function short_cb(alg_mod, iter_count, obj_value)
@test isa(alg_mod, Integer)
@test iter_count >= 0
@test isa(obj_value, Real)
return iter_count < 4
end
nlp = ADNLPModel(x -> (x[1] - 1)^2 + 100 * (x[2] - x[1]^2)^2, [-1.2; 1.0])
stats = ipopt(nlp, callback = short_cb, callback_style = :ipopt_short, print_level = 0)
@test stats.status == :user
@test stats.iter == 4
end

@testset "JSO callback can use solver and nlp" begin
used_solver = Ref(false)
used_nlp = Ref(false)
function jso_cb(nlp_in, solver_in, stats_in)
# Use solver.x (problem current iterate)
@test length(solver_in.x) == nlp_in.meta.nvar
used_solver[] = true
# Use nlp to compute objective at current x
_ = obj(nlp_in, solver_in.x)
used_nlp[] = true
return stats_in.iter < 3
end
nlp = ADNLPModel(x -> (x[1] - 1)^2 + 100 * (x[2] - x[1]^2)^2, [-1.2; 1.0])
stats = ipopt(nlp, callback = jso_cb, print_level = 0)
@test stats.status == :user
@test used_solver[]
@test used_nlp[]
@test stats.iter == 3
end

@testset "Ipopt-style short callback (3 args)" begin
function short_cb(alg_mod, iter_count, obj_value)
@test isa(alg_mod, Integer)
@test isa(iter_count, Integer)
@test isa(obj_value, Real)
return iter_count < 2
end
nlp = ADNLPModel(x -> (x[1] - 1)^2 + 100 * (x[2] - x[1]^2)^2, [-1.2; 1.0])
stats = ipopt(nlp, callback = short_cb, callback_style = :ipopt_short, print_level = 0)
@test stats.status == :user
@test stats.iter == 2
end

nlp =
ADNLPModel(x -> (x[1] - 1)^2 + 4 * (x[2] - 3)^2, zeros(2), x -> [sum(x) - 1.0], [0.0], [0.0])
stats = ipopt(nlp, print_level = 0)
Expand Down