User cuts for BilevelJuMP model

How do I add cuts during the solve process when using BilevelJuMP.jl. My upper-level objective is a sum of binary variables and thus I know that my solution (optimal objective value) is eventually going to be an integer value. However, when I look at the log of Gurobi, I see that it often struggles to improve the objective bound, and I want to add a cut based on the current objective bound. To explain further, consider the following Gurobi log:

Root relaxation: objective 8.998468e+01, 518 iterations, 0.01 seconds

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0   89.98468    0   10          -   89.98468      -     -    0s
     0     0   89.98234    0   14          -   89.98234      -     -    0s
     0     0   89.97892    0   13          -   89.97892      -     -    0s
     0     0   89.96860    0   16          -   89.96860      -     -    0s
     0     0   89.94980    0   19          -   89.94980      -     -    0s
     0     0   89.94980    0   19          -   89.94980      -     -    0s
     0     0   89.94980    0   22          -   89.94980      -     -    0s
     0     0   89.94980    0   22          -   89.94980      -     -    0s
     0     0   89.94980    0   23          -   89.94980      -     -    0s
H    0     0                      78.0000000   89.94980  15.3%     -    0s
     0     2   89.94980    0   23   78.00000   89.94980  15.3%     -    0s
H   31    40                      79.0000000   89.94980  13.9%  66.9    0s
H   79    88                      82.0000000   89.94980  9.69%  48.9    0s
H  230   238                      83.0000000   89.91294  8.33%  34.7    1s
  4487  2368   87.55668   29   20   83.00000   88.54009  6.67%  67.9    5s
 10587  6671   85.95524   32   21   83.00000   87.92902  5.94%  66.2   10s
 17385 11235   84.89499   41   65   83.00000   87.87144  5.87%  73.7   15s
 23324 14212   84.29968   43   59   83.00000   87.78450  5.76%  74.9   23s
 24901 15632   85.96120   35   13   83.00000   87.74486  5.72%  74.4   25s
 32564 20416   84.67600   54   72   83.00000   87.56339  5.50%  74.8   30s
 38959 24058   84.95694   35   77   83.00000   87.33329  5.22%  74.7   35s
 47877 28915   86.30447   31  101   83.00000   87.07891  4.91%  73.5   40s
 56460 34265     cutoff   51        83.00000   86.97277  4.79%  72.2   45s
 62710 38323   85.75822   32   19   83.00000   86.96494  4.78%  71.4   50s
 69402 42717   85.67587   32   25   83.00000   86.95674  4.77%  71.8   55s
 77405 47259   85.84095   43   22   83.00000   86.94781  4.76%  70.7   60s
 84003 51593   85.85930   40   18   83.00000   86.94201  4.75%  70.8   65s
 91384 56395   84.98545   33   10   83.00000   86.93370  4.74%  70.3   70s
 100029 60781   85.88272   42   67   83.00000   86.92359  4.73%  70.4   76s
 107143 64839   84.65086   28   74   83.00000   86.91290  4.71%  70.1   80s
 113864 68059   84.43224   59   48   83.00000   86.89911  4.70%  70.2   85s
 120449 71624     cutoff   53        83.00000   86.88479  4.68%  70.5   90s
 127217 75347   85.82191   38   23   83.00000   86.86795  4.66%  70.4   95s
 133858 78801   84.02847   44   51   83.00000   86.84816  4.64%  70.5  100s
 138806 81250   85.95811   39   15   83.00000   86.83431  4.62%  70.5  105s
 145960 85375   85.87520   42   18   83.00000   86.81807  4.60%  70.7  110s
 152275 89068   85.55562   29   28   83.00000   86.80187  4.58%  70.7  115s
 159666 92576   85.54116   31  101   83.00000   86.78135  4.56%  70.8  120s
 166000 96009   85.52644   45   62   83.00000   86.76866  4.54%  70.9  125s
 172782 98758   85.28485   42   55   83.00000   86.75497  4.52%  71.1  130s
 179430 102455   84.95709   33   21   83.00000   86.73603  4.50%  71.1  135s
 186111 105424   84.95057   44   14   83.00000   86.71476  4.48%  71.2  141s
 190993 108810   85.58797   45   12   83.00000   86.70265  4.46%  71.1  145s
 198359 111618   85.59287   37   67   83.00000   86.68456  4.44%  70.9  153s
 198377 112559   85.51505   38   63   83.00000   86.68395  4.44%  70.9  155s
 205236 116157   85.89955   23   28   83.00000   86.66800  4.42%  70.8  160s
 212192 120081   84.33530   53   46   83.00000   86.65369  4.40%  70.8  165s
 218863 123904   85.97682   29   22   83.00000   86.63594  4.38%  70.8  170s
 226166 127846   86.50704   38   22   83.00000   86.61843  4.36%  70.7  175s
 232597 131329   85.02548   44   83   83.00000   86.60379  4.34%  70.8  180s
 240631 135281   84.69241   33   77   83.00000   86.58375  4.32%  70.9  186s
 245577 138076   85.28130   42   58   83.00000   86.57491  4.31%  71.0  190s
 252705 141379   85.90826   37   21   83.00000   86.55902  4.29%  71.1  195s
 259339 144601   84.90111   48   72   83.00000   86.54584  4.27%  71.2  200s
 266309 148418   85.98033   22   20   83.00000   86.52943  4.25%  71.3  205s
 272807 151235   85.91119   38   26   83.00000   86.51782  4.24%  71.3  211s
 278635 154397   84.80089   44   63   83.00000   86.50499  4.22%  71.3  215s
 286104 158498     cutoff   59        83.00000   86.48901  4.20%  71.2  220s
 292976 162017   85.75805   32   89   83.00000   86.47756  4.19%  71.2  225s
 300377 165168     cutoff   47        83.00000   86.46300  4.17%  71.1  230s
 306389 168831   84.93951   47   19   83.00000   86.45334  4.16%  71.2  235s
 313521 172557     cutoff   47        83.00000   86.43800  4.14%  71.2  241s
 321063 175959   85.80951   40   16   83.00000   86.42237  4.12%  71.1  246s
 325538 178310   84.94510   41   72   83.00000   86.41506  4.11%  71.2  250s
 332609 181354   85.16023   39   67   83.00000   86.40126  4.10%  71.2  259s
 332631 182064   85.13804   40   67   83.00000   86.40099  4.10%  71.2  261s
 337444 184433   84.53113   47   65   83.00000   86.39368  4.09%  71.3  265s
 342821 187027   84.79693   41   47   83.00000   86.38475  4.08%  71.3  270s
 347751 189828   84.96194   49   63   83.00000   86.37539  4.07%  71.4  275s
 353484 192295   85.87731   34   86   83.00000   86.36465  4.05%  71.4  280s
 358339 195075   85.36642   38  100   83.00000   86.35668  4.04%  71.4  285s
 364166 197676   85.84782   36   20   83.00000   86.34536  4.03%  71.5  290s
 371185 200969   84.68117   52   56   83.00000   86.33693  4.02%  71.6  296s
 376339 203420     cutoff   41        83.00000   86.32835  4.01%  71.7  300s
 381610 205712   85.77387   28   28   83.00000   86.31993  4.00%  71.7  305s
 389399 209002   84.53351   44   59   83.00000   86.30526  3.98%  71.7  311s
 394705 211597   84.75263   38   63   83.00000   86.29478  3.97%  71.7  315s
 399615 214150   84.97995   43    7   83.00000   86.28584  3.96%  71.7  320s
 407286 217533   84.68814   45   68   83.00000   86.26921  3.94%  71.6  326s

In the above snippet, we can see that the objective bound is between 86 and 87 for a long time. But, an objective bound of 86.xx implies that my objective can only be <=86. So, when the objective bound takes these non-integer values, then I want to add a cut that can do something like floor(current_objective_bound). I believe I can add user cuts as is described here; however, it doesn’t work as intended because my model is a BilevelJuMP model and not a JuMP model?

If an MWE is needed (y is the integer variable here):

using JuMP, BilevelJuMP, Gurobi

model = BilevelModel(Gurobi.Optimizer, mode = BilevelJuMP.SOS1Mode())

@variable(Lower(model), x)
@variable(Upper(model), y, Int)

@objective(Upper(model), Min,  y)
@constraints(Upper(model), begin
    x <= 5
    y <= 8
    y >= 0
end)

@objective(Lower(model), Min, -x)
@constraints(Lower(model), begin
     x +  y <= 8
    4x +  y >= 8
    2x +  y <= 13
    2x - 7y <= 0
end)

function my_callback_function(cb_data)
    y_val = callback_value(cb_data, y)
    con = @build_constraint(y<= floor(y_val))
    println("Adding $(con)")
    MOI.submit(model, MOI.UserCut(cb_data), con)
end

MOI.set(model, MOI.UserCutCallback(), my_callback_function)

The last line throws the following error as expected:

ERROR: MethodError: no method matching set(::BilevelModel, ::MathOptInterface.UserCutCallback, ::typeof(my_callback_function))

Is there a workaround to this problem?
Any help is appreciated. Thanks.

@joaquimg can correct me, but I don’t think there is a workaround.

Setting a solver independent callback to a JuMP extension seems quite difficult to get right. Part of that problem is that the model you see isn’t the model that gets solved.

1 Like

Ah, that’s unfortunate. How about solver-specific callbacks? I believe even they wouldn’t be of much help here for the same reason as you stated. Is that right? (I have never used solver-specific callbacks though, so I have the least intuition of how they would work in such a scenario.)

Thanks for your input!

BilevelJuMP is not ready for callbacks.
In theory, it is feasible. But you have to go through the internal mappings.
It would be a nice thing to add! If you want to start a PR I can review and guide you.

2 Likes