Add an "optimize_assert_optimal" functionality in JuMP would be handier?

I added is_solved_and_feasible for a few reasons:

  • Many questions on this forum were because people did not check the termination status before querying results
  • Checking the termination status is a bit subtle, because users need to understand OPTIMAL and LOCALLY_SOLVED, and also the primal (and dual) statuses.
  • For the basic use case, it saves quite a few lines of code, and it makes the code written easier to understand.

I don’t think we will add this macro. The first reason is that it hides too much from the user while saving too few lines of code. Your line:

@optimise_assert_optimal(model_123) # be silent because optimality

is essentially just:

JuMP.optimize!(model_123)
if (status = JuMP.termination_status(model_123)) != JuMP.OPTIMAL
    error("model_123: $status")
end

A second reason is that, while macros are cool and JuMP uses them a lot, they’re somewhat over-used and abused by Julia programmers. There is almost never a good reason to write a macro for user-code.

For example, at the cost of an extra argument, you could use a function:

function optimize!_and_assert_optimality(model, name)
    JuMP.optimize!(model)
    if (status = JuMP.termination_status(model)) != JuMP.OPTIMAL
        error("$name: $status")
    end
end

optimize!_and_assert_optimality(model_123, "model_123")

One place where the function approach would be better is:

models = Dict("A" => model_123, "B" => model456)
for (name, model) in models
    @optimise_assert_optimal model
    # or
    optimize!_and_assert_optimality(model, name)
end

Your macro approach will just print ERROR: model: status whereas the function will print ERROR: A: status and ERROR: B: status.

That being said, a great thing about JuMP is that because it is embedded in Julia you are more than welcome to write and use this macro yourself! We just won’t be adding it to JuMP.

On a more technical note, one reason that I strongly, strongly discourage you to write your own macro is that they can be tricky to get correct. Consider what happens if you use @optimise_assert_optimal in a function:

julia> import JuMP

julia> import Gurobi

julia> function optimise(model) return (JuMP.optimize!(model); JuMP.termination_status(model)) end
optimise (generic function with 1 method)

julia> macro optimise_assert_optimal(model)
           name = string(model)
           return quote
               status = optimise($model)
               status == JuMP.OPTIMAL || error($name * ": " * string(status))
           end
       end
@optimise_assert_optimal (macro with 1 method)

julia> function foo()
           model_123 = JuMP.Model(Gurobi.Optimizer)
           @optimise_assert_optimal(model_123) # be silent because optimality
       end
foo (generic function with 1 method)

julia> foo()
Set parameter LicenseID to value 890341
ERROR: UndefVarError: `model_123` not defined
Stacktrace:
 [1] macro expansion
   @ ./REPL[4]:4 [inlined]
 [2] foo()
   @ Main ./REPL[5]:3
 [3] top-level scope
   @ REPL[6]:1

to fix, you need to use esc:

julia> import JuMP

julia> import Gurobi

julia> macro optimise_assert_optimal(model)
           name_str = string(model, ": ")
           code = quote
               JuMP.optimize!($model)
               if (status = JuMP.termination_status($model)) != JuMP.OPTIMAL
                   error($name_str * string(status))
               end
           end
           return esc(code)
       end
@optimise_assert_optimal (macro with 1 method)

julia> function foo()
           model_123 = JuMP.Model(Gurobi.Optimizer)
           @optimise_assert_optimal(model_123) # be silent because optimality
       end
foo (generic function with 1 method)

julia> foo()
Set parameter LicenseID to value 890341
Gurobi Optimizer version 12.0.0 build v12.0.0rc1 (mac64[x86] - Darwin 24.1.0 24B83)

CPU model: Intel(R) Core(TM) i5-8259U CPU @ 2.30GHz
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads

Optimize a model with 0 rows, 0 columns and 0 nonzeros
Model fingerprint: 0xf9715da1
Coefficient statistics:
  Matrix range     [0e+00, 0e+00]
  Objective range  [0e+00, 0e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [0e+00, 0e+00]
Presolve time: 0.00s
Presolve: All rows and columns removed
Iteration    Objective       Primal Inf.    Dual Inf.      Time
       0    0.0000000e+00   0.000000e+00   0.000000e+00      0s

Solved in 0 iterations and 0.00 seconds (0.00 work units)
Optimal objective  0.000000000e+00

User-callback calls 23, time in user-callback 0.00 sec
2 Likes