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?