import JuMP, Gurobi
CR = JuMP.Model(() -> Gurobi.Optimizer())
JuMP.@variable(CR, x[1:3])
JuMP.set_lower_bound(x[1], exp(1)); JuMP.set_upper_bound(x[1], exp(2))
JuMP.set_lower_bound(x[3], 1e-6)
JuMP.@objective(CR, Min, -x[1] * x[2] - log(x[3]) + x[2]^2/4)
JuMP.optimize!(CR) # see the following Gurobi's log
# Solution count 1: -11.993
# Model is unbounded
# Best objective -1.199295535608e+01, best bound -, gap -
JuMP.termination_status(CR) # DUAL_INFEASIBLE
JuMP.objective_value(CR) # -11.992955356078266
JuMP.objective_bound(CR) # 1.0e100
# This relation is illogical in a Min-Program
# It should always holds that obj_value >= obj_bound ???
# I think in this case the outcome of `JuMP.objective_bound(CR)` should be something like -1.0e100
primal_status(model) is NO_SOLUTION, thus, calling objective_value and objective_bound is undefined behavior.
Please remember to _always_use is_solved_and_feasible, or check the termination status and the primal status before querying a primal solution.
julia> solution_summary(model)
* Solver : Gurobi
* Status
Result count : 1
Termination status : DUAL_INFEASIBLE
Message from the solver:
"Model was proven to be unbounded. Important note: an unbounded status indicates the presence of an unbounded ray that allows the objective to improve without limit. It says nothing about whether the model has a feasible solution. If you require information on feasibility, you should set the objective to zero and reoptimize."
* Candidate solution (result #1)
Primal status : NO_SOLUTION
Dual status : NO_SOLUTION
Objective value : -1.19930e+01
Objective bound : 1.00000e+100
Dual objective value : 1.00000e+100
import JuMP
import Gurobi
GRB_ENV = Gurobi.Env()
CR = JuMP.Model(() -> Gurobi.Optimizer(GRB_ENV))
JuMP.@variable(CR, x)
JuMP.@objective(CR, Min, (x - 1.) * x * (x + 1.))
JuMP.optimize!(CR) # see the continual logging
# here you hit `ctrl + C`
# Then you query
JuMP.termination_status(CR) # it shows OPTIMIZE_NOT_CALLED
# if I recall correctly, this should be INTERRUPTED ???
I’ve only encountered these in my life
Enum MathOptInterface.TerminationStatusCode:
OPTIMIZE_NOT_CALLED = 0 # exclusively only occurs when you forget to `optimize!(model)`
OPTIMAL = 1
INFEASIBLE = 2
DUAL_INFEASIBLE = 3
LOCALLY_SOLVED = 4
INFEASIBLE_OR_UNBOUNDED = 6
TIME_LIMIT = 12
SLOW_PROGRESS = 19
NUMERICAL_ERROR = 20
INTERRUPTED = 23 # happens when you press `ctrl + C` to manually stop the solver, but you could fetch solutions
I mean, if I recall correctly, if I interrupt Gurobi through my keyboard.
I could query the current solution like JuMP.value(x).
But now it only gives me the error.
This is not what it used to be!
import JuMP
import Gurobi
GRB_ENV = Gurobi.Env()
CR = JuMP.Model(() -> Gurobi.Optimizer(GRB_ENV))
JuMP.set_attribute(CR, "TIME_LIMIT", 4.)
JuMP.@variable(CR, x)
JuMP.@objective(CR, Min, (x - 1.) * x * (x + 1.))
JuMP.optimize!(CR) # see the continual logging
# then it terminates
@assert JuMP.termination_status(CR) == JuMP.TIME_LIMIT
x = JuMP.value(x) # -302.7000000000001
ub = JuMP.objective_value(CR) # -2.773527798300003e7
lb = JuMP.objective_bound(CR) # -5.36870912e14
lb < ub && println("This is sane 😄")
# Comment: This is the correct behavior since I can query the status (even before Gurobi returns to Julia)
I’ll say again in bold: Please remember to always use is_solved_and_feasible , or check the termination status and the primal status before querying a primal solution. It is NOT sufficient to check only termination_status, although it worked in this case.
Is this problem exclusive to Gurobi?
It seems that this happens with Ipopt too.
“this” means if you manually interrupt the solver, then the termination_status is not JuMP.INTERRUPTED but see below:
julia> JuMP.optimize!(CR)
******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
Ipopt is released as open source code under the Eclipse Public License (EPL).
For more information visit https://github.com/coin-or/Ipopt
******************************************************************************
This is Ipopt version 3.14.14, running with linear solver MUMPS 5.6.2.
Number of nonzeros in equality constraint Jacobian...: 0
Number of nonzeros in inequality constraint Jacobian.: 0
Number of nonzeros in Lagrangian Hessian.............: 1
ERROR: InterruptException:
Stacktrace:
[1] eval_objective_gradient(model::Ipopt.Optimizer, grad::Vector{Float64}, x::Vector{Float64})
@ Ipopt K:\judepot1113\packages\Ipopt\KZuET\src\MOI_wrapper.jl:872
[2] (::Ipopt.var"#eval_grad_f_cb#4"{Ipopt.Optimizer})(x::Vector{Float64}, grad_f::Vector{Float64})
@ Ipopt K:\judepot1113\packages\Ipopt\KZuET\src\MOI_wrapper.jl:981
[3] _Eval_Grad_F_CB(n::Int32, x_ptr::Ptr{Float64}, ::Int32, grad_f::Ptr{Float64}, user_data::Ptr{Nothing})
@ Ipopt K:\judepot1113\packages\Ipopt\KZuET\src\C_wrapper.jl:56
[4] IpoptSolve
@ K:\judepot1113\packages\Ipopt\KZuET\src\C_wrapper.jl:399 [inlined]
[5] optimize!(model::Ipopt.Optimizer)
@ Ipopt K:\judepot1113\packages\Ipopt\KZuET\src\MOI_wrapper.jl:1131
[6] optimize!
@ K:\judepot1113\packages\MathOptInterface\TYq6d\src\Bridges\bridge_optimizer.jl:367 [inlined]
[7] optimize!
@ K:\judepot1113\packages\MathOptInterface\TYq6d\src\MathOptInterface.jl:122 [inlined]
[8] optimize!(m::MathOptInterface.Utilities.CachingOptimizer{…})
@ MathOptInterface.Utilities K:\judepot1113\packages\MathOptInterface\TYq6d\src\Utilities\cachingoptimizer.jl:327
[9] optimize!(model::JuMP.Model; ignore_optimize_hook::Bool, _differentiation_backend::MathOptInterface.Nonlinear.SparseReverseMode, kwargs::@Kwargs{})
@ JuMP K:\judepot1113\packages\JuMP\xlp0s\src\optimizer_interface.jl:595
[10] optimize!(model::JuMP.Model)
@ JuMP K:\judepot1113\packages\JuMP\xlp0s\src\optimizer_interface.jl:546
[11] top-level scope
@ REPL[7]:1
Some type information was truncated. Use `show(err)` to see complete types.
julia> JuMP.termination_status(CR)
OPTIMIZE_NOT_CALLED::TerminationStatusCode = 0
I think at least in literal meaning this is incorrect.
JuMP should reports JuMP.INTERRUPTED.
And OPTIMIZE_NOT_CALLED should be exclusively refer to the situation when I forget to write JuMP.optimize!(model).
I think this is my correct memory about JuMP in the past 2 years.
I think JuMP.primal_status is somewhat unimportant, given the presence of JuMP.has_values. I’ve just written a rigorous procedure about this, please see About JuMP.is_solved_and_feasible.
In most cases, if you interrupt the solver mid-solve, it won’t have a solution to return to you. So in the Ipopt case, theres essentially no difference between reporting INTERRUPTED and OPTIMIZE_NOT_CALLED. But yes, we could do better at handling this.
According to my experience, this is not the case, at least for Gurobi.
The following 2 cases should be similar:
I set a TimeLimit at 4 seconds for Gurobi. Then After 4 seconds, after normality check (i.e. something like assert_is_solved_and...), I can get the current solution (which should be good, as Gurobi spend 4 seconds to reach).
I haven’t set attributes. I let Gurobi run 4 seconds. Then I use Ctrl+C to interrupt it. After normality check, I can also get the current solution.
This was my knowledge about Gurobi used in conjunction with JuMP. And I think this is the ideal behavior. (Gurobi 12’s behavior is disquieting for me😑)
If you are solving an LP and stop with a time limit, it probably won’t have a feasible point to return. If you are solving a MIP, it depends whether Gurobi found a primal feasible point. It might find one in 1 second, or it might take 1000 years. You should not rely on the fact that Gurobi will have a solution when stopped early.
Actually I think this is 99% of the case. Since Gurobi tends to use heuristic first and it can indeed generate a feasible solution quickly at first, such that the primal bound can be updated soon.
Well, from the Gurobi logging we know that it fails to offer a primal feasible solution.
But importantly it can offer a valid dual bound, as shown in best bound 0.000000000000e+00.
In this case I’m expecting:
After I hit ctrl + C, I call a normality check