# Creating JuMP decision variables more cautiously

**URL:** <https://discourse.julialang.org/t/creating-jump-decision-variables-more-cautiously/137208>\
**Category:** Optimization (Mathematical)\
**Tags:** jump\
**Created:** [May 20, 2026, 6:32am UTC](https://discourse.julialang.org/t/creating-jump-decision-variables-more-cautiously/137208 "2026-05-20T06:32:17Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![WalterMadelim](https://avatars.discourse-cdn.com/v4/letter/w/3e96dc/32.png) [@WalterMadelim](https://discourse.julialang.org/u/WalterMadelim)\
**Post date:** [May 20, 2026, 6:32am UTC](https://discourse.julialang.org/t/creating-jump-decision-variables-more-cautiously/137208/1 "2026-05-20T06:32:17Z")

</div>

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

```julia-auto
# 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

```julia
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

```julia-auto
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)) || pAH[z][g] + Pmin > Ofst*Rd + Sd
            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)) || pAH[z][g] + Pmin > Ofst*Rd + Sd
            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-auto
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?

---

<div class="post-metadata">

**Author:** ![odow](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/odow/32/28685_2.png) [@odow](https://discourse.julialang.org/u/odow)\
**Post date:** [May 20, 2026, 6:50am UTC](https://discourse.julialang.org/t/creating-jump-decision-variables-more-cautiously/137208/2 "2026-05-20T06:50:22Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![WalterMadelim](https://avatars.discourse-cdn.com/v4/letter/w/3e96dc/32.png) [@WalterMadelim](https://discourse.julialang.org/u/WalterMadelim)\
**Post date:** [May 20, 2026, 6:55am UTC](https://discourse.julialang.org/t/creating-jump-decision-variables-more-cautiously/137208/3 "2026-05-20T06:55:52Z")

</div>

> 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…

---

<div class="post-metadata">

**Author:** ![odow](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/odow/32/28685_2.png) [@odow](https://discourse.julialang.org/u/odow)\
**Post date:** [May 20, 2026, 8:17am UTC](https://discourse.julialang.org/t/creating-jump-decision-variables-more-cautiously/137208/4 "2026-05-20T08:17:54Z")

</div>

Did you benchmark both approaches?

---

<div class="post-metadata">

**Author:** ![WalterMadelim](https://avatars.discourse-cdn.com/v4/letter/w/3e96dc/32.png) [@WalterMadelim](https://discourse.julialang.org/u/WalterMadelim)\
**Post date:** [May 21, 2026, 2:52am UTC](https://discourse.julialang.org/t/creating-jump-decision-variables-more-cautiously/137208/5 "2026-05-21T02:52:31Z")

</div>

I adopted my variable reduction method, and now I’m pretty content with the current performance.

In my 2SSP, the num of scenario is

```julia-auto
julia> length(sub)
6048

```

each scenario subproblem is a 24-hour UC MIP problem which has the size

```julia-auto
julia> Settings.getmodelintattr(sub[1],"NumVars")
16820

julia> Settings.getmodelintattr(sub[1],"NumConstrs")
20316

```

. And I wrote a multithreading Bender algorithm to train the test case with 126 physical cores, the runtime is

```julia-auto
6544.712524 seconds (1.59 M allocations: 87.830 MiB, 1024 lock conflicts, 0.24% compilation time)

```

The termination optimality gap is 0.1%

```julia-auto
Sol=1: lb = 4338.060817948018, agap = 4.575924878499791, rgap = 0.0010537203891296308

```

And the memory usage is 500GiB.  
And since my master problem is being cut-generated, this info is beneficial:

```julia-auto
julia> Settings.getmodelintattr(mst, "NumConstrs")
53495

```

meaning that each scenario contributed about 9 cuts.

I only used strengthened Benders cut.

The interesting conclusion is that: one subproblem MIP cannot be solved to exact optimality doesn’t mean that the global problem cannot be trained to high accuracy global optimum.
