Creating JuMP decision variables more cautiously

I’m studying the Unit Commitment problem which typically has a 24-hour planning horizon. So each generator (if we use the 2-Bin formulation with turn-on decision u and ON/OFF state decision x) will have at least 48 binary decisions.

In practice I find some Unit Commitment instances fairly challenging to the solver, e.g. Gurobi may spend 1 minute to reach a rgap which is still above 0.1% (I solve them as subproblems in Benders decomposition, so 1min is pretty slow).

So this make me think about more efficient ways to code my UC problem with julia.

One idea I can come up with is to:

  • avoid creating unnecessary decision variables

For example, one part of the UC problem is to impose the minimum up/down time constraints, with the input data

# ON(1)/OFF(0) state history trajectory: xH[1] is the nearest history, while xH[end] is the most ancient
xH = Bool[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];

t = 1 # here-and-now label of time slot
T = 23 # Look-ahead length; such that the planning horizon is `t:t+T`

UT, DT = 20, 4 # Minimum Up time, Minimum Down time

I think most people would at first just directly call the following macros, which introduces too many decisions recklessly

import JuMP
m = JuMP.Model();
x = JuMP.@variable(m, [t:t+T], Bin)
u = JuMP.@variable(m, [t:t+T], Bin)
# here we created 48 decision variables

One alternative way (my method) to create the decision variables is

import JuMP
m = JuMP.Model();
x = Dict{Int, Union{Bool, JuMP.VariableRef}}(); # use Dict (rather than Vector) as it's more flexible in practical systems
u = Dict{Int, Union{Bool, JuMP.VariableRef}}();
let Ofst = 0
    i = t + Ofst
    if xH[1] # the unit was ON
        u[i] = false # must not turn on
        if any(==(0), view(xH, 1:UT-Ofst))
            x[i] = true # must maintain its ON state
        else
            x[i] = JuMP.@variable(m, binary=true) # this is a proper decision to make
        end
    else # the unit was OFF
        if any(view(xH, 1:DT-Ofst))
            u[i] = x[i] = false # must maintain its OFF state, thus must not turn on
        else
            tmp = JuMP.@variable(m, binary=true) # allocate one variable instead of two
            u[i] = x[i] = tmp # turn on => ON state; otherwise, OFF state
        end
    end
end
for Ofst = 1:T
    i = t + Ofst
    if x[i-1] === true # the unit was fixed at ON
        u[i] = false
        if any(==(0), view(xH, 1:UT-Ofst))
            x[i] = true
        else
            x[i] = JuMP.@variable(m, binary=true)
        end
    elseif x[i-1] === false # the unit was fixed at OFF
        if any(view(xH, 1:DT-Ofst))
            u[i] = x[i] = false
        else
            u[i] = x[i] = JuMP.@variable(m, binary=true)
        end
    else # isa JuMP.VariableRef
        u[i] = JuMP.@variable(m, binary=true)
        x[i] = JuMP.@variable(m, binary=true)
    end
end

, in which case I’m only creating 11 decisions (the following code is only for printing purpose):

julia> @show x = [x[i] for i=t:t+T] u = [u[i] for i=t:t+T]
x = [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, _[1], _[3], _[5], _[7], _[9], _[11]]
u = [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, _[2], _[4], _[6], _[8], _[10]]

(of course this trick doesn’t obviate the need to add all other valid physical constraints, where x and u should be referred to.)

I believe my Dict{Int, Union{Bool, JuMP.VariableRef}}() container here is a good design. Any suggestions?

Is there a question here? You’ll probably find that Gurobi presolves out the trivial decisions. You can probably build the problem faster by not including them, but that comes at the trade off of more complicated code to read and some potential type stability issues.

some potential type stability issues.

I’m interested in this point, how is julia’s performance about the Union?


You’ll probably find that Gurobi presolves out the trivial decisions

I think explicitly making them numeric scalars is better. A decision variable is probably much heavier in the computer storage. And when it is a explicit decision, it has LP-relaxation tightness issues, for instance…