optimizationjuliajulia-jumpipopt

Making changes during a optimization loop, IPOPT


I am currently working with some complicated problems where I want to make a small change to my objective function (just increasing a constant) if some statement is not satisfied in an iteration. I want to check this while IPOPT is running (before optimize! (model)is finished) Is this possible? Can I use the callback function to change the objective function from one iteration to the next?


Solution

  • IPOPT has a callback implemented, but it gives you access only to a handful information, see for instance ( https://github.com/jump-dev/Ipopt.jl ):

    using JuMP, Ipopt, Test
    model = Model(Ipopt.Optimizer)
    set_silent(model)
    @variable(model, x >= 1)
    @objective(model, Min, x + 0.5)
    x_vals = Float64[]
    function my_callback(
       alg_mod::Cint,
       iter_count::Cint,
       obj_value::Float64,
       inf_pr::Float64,
       inf_du::Float64,
       mu::Float64,
       d_norm::Float64,
       regularization_size::Float64,
       alpha_du::Float64,
       alpha_pr::Float64,
       ls_trials::Cint,
    )
       push!(x_vals, callback_value(model, x))
       @test isapprox(obj_value, 1.0 * x_vals[end] + 0.5, atol = 1e-1)
       # return `true` to keep going, or `false` to terminate the optimization.
       return iter_count < 1
    end
    MOI.set(model, Ipopt.CallbackFunction(), my_callback)
    optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INTERRUPTED
    @test length(x_vals) == 2
    

    However, you cannot modify the problem. One of the main reasons is that IPOPT is not coded in Julia, so a conversion in C is done along the way.

    The only solution I see is to use a flexible modeling structure (one that would let you modify the objective) and a solver coded in pure Julia. Still, it is a bit of work.

    For instance, if you deal with unconstrained optimization problems you could use lbfgs as follows:

    using ADNLPModels, JSOSolvers, ForwardDiff, NLPModels
    
    # Using ADNLPModel for its flexibility.
    # But we need to deactivate the pre-computation done on the objective function
    # https://juliasmoothoptimizers.github.io/tutorials/generic-adnlpmodels/
    struct GenericGradientBackend <: ADNLPModels.ADBackend end
    GenericGradientBackend(args...; kwargs...) = GenericGradientBackend()
    
    function ADNLPModels.gradient!(::GenericGradientBackend, g, f, x)
      return ForwardDiff.gradient!(g, f, x)
    end
    
    nlp = ADNLPModel(
      x -> 100 * (x[2] - 2)^2 + 2 * (x[1] - 1)^2 + (x[1] - x[2]^2)^2,
      [-1.2; 1.0],
      gradient_backend = GenericGradientBackend,
    )
    
    # Then, we define the callback
    function cb(nlp, solver, stats)
      @show stats.iter
      @show solver.x
      if stats.iter > 1
        nlp.f = x -> 2 * (x[2] - 2)^2 + 100 * (x[1] - 1)^2 + (x[1] - x[2]^2)^2
      end
      @show obj(nlp, solver.x)
    end
    # call JSOSolvers.lbfgs https://juliasmoothoptimizers.github.io/tutorials/introduction-to-jsosolvers/
    stats = lbfgs(nlp, callback = cb, verbose = 1)
    

    the output will be

    julia> stats = lbfgs(nlp, callback = cb, verbose = 1)
    [ Info:   iter      f(x)      ‖∇f‖      ∇fᵀd      bk  
    stats.iter = 0
    solver.x = [-1.2, 1.0]
    obj(nlp, solver.x) = 114.52000000000001
    [ Info:      0   1.1e+02   1.9e+02  -3.7e+04       6
    stats.iter = 1
    solver.x = [-1.1459328, 1.7831552000000002]
    obj(nlp, solver.x) = 32.62282328590102
    [ Info:      1   3.3e+01   2.1e+01  -2.2e+00       0
    stats.iter = 2
    solver.x = [-1.0645542968025667, 1.8450655818506445]
    obj(nlp, solver.x) = 446.25681758709055
    [ Info:      2   3.1e+01   1.7e+01  -5.0e+00      25
    stats.iter = 3
    solver.x = [-1.0645542967686918, 1.8450655818604536]
    obj(nlp, solver.x) = 446.25681757311787
    [ Info:      3   4.5e+02   4.2e+02  -3.1e+03       1
    stats.iter = 4
    solver.x = [1.9246380432427963, 2.7281329116323207]
    obj(nlp, solver.x) = 117.00501528447751
    [ Info:      4   1.2e+02   1.8e+02  -1.9e+02       0
    stats.iter = 5
    solver.x = [0.9801608161355717, 2.3095374587434887]
    obj(nlp, solver.x) = 19.186582034724903
    [ Info:      5   1.9e+01   4.3e+01  -4.8e+00       0
    stats.iter = 6
    solver.x = [0.9652544626973018, 2.189462921736651]
    obj(nlp, solver.x) = 14.849879523336808
    [ Info:      6   1.5e+01   3.7e+01  -1.9e+01       0
    stats.iter = 7
    solver.x = [1.0002184791527038, 1.6363671414275927]
    obj(nlp, solver.x) = 3.078398087147021
    [ Info:      7   3.1e+00   1.0e+01  -2.1e+00       0
    stats.iter = 8
    solver.x = [1.0089309580533612, 1.4229017930719876]
    obj(nlp, solver.x) = 1.7057450643634193
    [ Info:      8   1.7e+00   3.5e+00  -4.3e-01       0
    stats.iter = 9
    solver.x = [1.0053404604019518, 1.2982558231123305]
    obj(nlp, solver.x) = 1.4503155493415034
    [ Info:      9   1.5e+00   7.8e-01  -2.4e-02       0
    stats.iter = 10
    solver.x = [1.0064110790068321, 1.2657336397712304]
    obj(nlp, solver.x) = 1.4372277943232419
    [ Info:     10   1.4e+00   1.2e-01  -8.8e-04       1
    stats.iter = 11
    solver.x = [1.0047305130737216, 1.2632117110472276]
    obj(nlp, solver.x) = 1.4372011985565385
    [ Info:     11   1.4e+00   2.4e-01  -3.3e-04       0
    stats.iter = 12
    solver.x = [1.00584984478302, 1.2615152914236158]
    obj(nlp, solver.x) = 1.4370347773092513
    [ Info:     12   1.4e+00   1.5e-03  -4.5e-08       0
    stats.iter = 13
    solver.x = [1.0058545642521763, 1.2614703004604053]
    obj(nlp, solver.x) = 1.4370347544382267
    [ Info:     13   1.4e+00   9.6e-06  -2.5e-12       0
    stats.iter = 14
    solver.x = [1.005854516585087, 1.26146995704064]
    obj(nlp, solver.x) = 1.4370347544370015
    [ Info:     14   1.4e+00   5.6e-07
    "Execution stats: first-order stationary"