Decomposing a scenario-feasibility MILP with binary recourse in JuMP

I have a two-stage multi-scenario MILP in JuMP with an inventory/order-flow structure.

The first-stage variables are shared across all scenarios. Each scenario then has its own continuous flow variables and binary timing variables. The objective and epigraph constraints depend on the shared first-stage variables. For fixed first-stage variables, the scenario-specific part is essentially a feasibility recourse problem.

A runnable extensive-form model builder with example data is here:

By default, the script builds a small two-scenario instance using scenarios 0 and 1. The data directory also contains additional scenarios, but the larger extensive form is the scale at which solving directly becomes difficult.

It can be run with:

julia OptBMExtensive.jl

The script builds the extensive-form model and reports its size; it does not attempt to solve the large instance directly. I have not included my attempted Benders implementation because I am mainly trying to understand the correct formulation, decomposition, and cut structure, rather than debug a particular implementation.

Model structure

The sets are i \in I for initial items, t,k \in T for periods, and s \in S for scenarios.

The shared first-stage variables are x_i and \theta, with 0 \le x_i \le 1 for all i \in I, and \theta \ge 0.

The objective is \min \; \theta.

The scenario-dependent epigraph constraints are \theta \ge \sum_{i \in I} v^0_{s,i,1} x_i for all s \in S.

For each scenario, the remaining variables are scenario-specific. They include order quantities y_{s,t} \ge 0, usage variables 0 \le u^0_{s,i,t} \le 1 and u^1_{s,k,t} \ge 0, stock variables q^0_{s,i,t} \ge 0 and q^1_{s,k,t} \ge 0, and binary timing variables \delta_{s,t} \in \{0,1\} and \gamma_{s,t} \in \{0,1\}.

Here y is an order quantity, u is usage, q is remaining stock, \delta indicates whether a period is an order period, and \gamma indicates whether initial items may be used in that period.

The initial-item flow constraints are q^0_{s,i,1} + u^0_{s,i,1} = x_i for all s \in S and i \in I, and q^0_{s,i,t} + u^0_{s,i,t} = q^0_{s,i,t-1} for all s \in S, i \in I, and t > 1.

The ordered-item flow constraints are q^1_{s,k,k} = y_{s,k} for all s \in S and k \in T, and q^1_{s,k,t} + u^1_{s,k,t} = q^1_{s,k,t-1} for all s \in S, k \in T, and t > k.

For compactness, define the following period-level expressions:

A^0_{s,t} = \sum_{i \in I} r^0_{s,i,t} q^0_{s,i,t},

A^1_{s,t} = \sum_{k<t} r^1_{s,k,t} q^1_{s,k,t},

B^0_{s,t} = \sum_{i \in I} v^0_{s,i,t} u^0_{s,i,t},

B^1_{s,t} = \sum_{k<t} v^1_{s,k,t} u^1_{s,k,t}.

The per-period balance constraint is A^0_{s,t} + A^1_{s,t} + B^0_{s,t} + B^1_{s,t} - p_{s,t} y_{s,t} = d_{s,t} for all s \in S and t \in T.

The timing logic is represented with big-M constraints:

y_{s,t} \le M^y_{s,t}\delta_{s,t} for all s \in S and t \in T.

\sum_{i \in I} u^0_{s,i,t} + \sum_{k<t} u^1_{s,k,t} \le M^u_{s,t}(1-\delta_{s,t}) for all s \in S and t \in T.

\sum_{k \le t} q^1_{s,k,t} \le M^q_{s,t}(1-\gamma_{s,t}) for all s \in S and t \in T.

\sum_{i \in I} u^0_{s,i,t} \le M^0_{s,t}\gamma_{s,t} for all s \in S and t \in T.

The interpretation of these binary constraints is:

  • The binary variable \delta_{s,t} enforces order/use exclusivity in each scenario and period. If \delta_{s,t}=1, then y_{s,t} may be positive but all usage in that period is forced to zero. If \delta_{s,t}=0, then y_{s,t}=0 and usage may occur.

  • The binary variable \gamma_{s,t} enforces a priority rule between ordered stock and initial-item usage. If \gamma_{s,t}=1, then initial items may be used, but remaining ordered stock at the end of the period is forced to zero. If \gamma_{s,t}=0, then initial-item usage is forced to zero.

So the intended logic is: a period cannot both order and use inventory, and initial items can only be used once ordered stock has been exhausted by the end of that period.

Therefore, for fixed first-stage variables x, each scenario subproblem is a MILP feasibility problem: Q_s(x)=0 if scenario s is feasible for x, and Q_s(x)=+\infty otherwise.

Approximate JuMP structure

The JuMP structure is roughly:

model = Model()

@variable(model, 0 <= initial_qty[1:n_items] <= 1)
@variable(model, theta >= 0)

for s in scenarios
    @variable(model, order_qty[1:n_periods] >= 0)

    @variable(model, 0 <= use_initial[1:n_items, 1:n_periods] <= 1)
    @variable(model, use_order[1:n_periods, 1:n_periods] >= 0)

    @variable(model, initial_stock[1:n_items, 1:n_periods] >= 0)
    @variable(model, order_stock[k = 1:n_periods, t = k:n_periods] >= 0)

    @variable(model, order_period[1:n_periods], Bin)
    @variable(model, initial_use_period[1:n_periods], Bin)

    # Coupling to the first-stage variables
    @constraint(model, [i = 1:n_items],
        initial_stock[i, 1] + use_initial[i, 1] == initial_qty[i]
    )

    @constraint(model, [i = 1:n_items, t = 2:n_periods],
        initial_stock[i, t] + use_initial[i, t] == initial_stock[i, t - 1]
    )

    # Ordered-item flow
    @constraint(model, [k = 1:n_periods],
        order_stock[k, k] == order_qty[k]
    )

    @constraint(model, [k = 1:n_periods, t = k+1:n_periods],
        order_stock[k, t] + use_order[k, t] == order_stock[k, t - 1]
    )

    # Scenario balance constraints and big-M timing constraints follow here.

    @constraint(model,
        theta >= sum(initial_value[s, i, 1] * initial_qty[i] for i in 1:n_items)
    )
end

@objective(model, Min, theta)

Formulation/decomposition question

I am unsure what the right decomposition should be for this structure.

One possible decomposition is:

  • master problem: shared variables x and \theta;
  • scenario subproblem: all scenario-specific variables for fixed x.

However, after fixing x, each scenario subproblem still contains the binary variables \delta and \gamma. Therefore, the scenario subproblem is a MILP feasibility problem, not a continuous LP feasibility problem. Classical LP-dual Benders feasibility cuts do not seem directly applicable.

Another possibility is to move the timing binaries \delta and \gamma into the master. Then the scenario subproblem becomes continuous, so Farkas/dual feasibility cuts should in principle be available. However, this makes the master much larger and more combinatorial, and on larger instances this has not worked well.

So my main question is:

What master/subproblem split would you recommend for this model, and what valid feasibility cuts would that split imply?

More specifically:

  1. Should the master contain only the shared first-stage variables x and \theta, leaving the binary timing variables in the scenario subproblems? If so, what kind of logic-based Benders or feasibility cuts would be valid for this structure?

  2. Or should the master include the timing binaries \delta and \gamma so that each scenario subproblem is continuous? If so, would the right cuts be LP Farkas feasibility cuts, and what would those cuts look like structurally?

  3. Are there stronger cuts or reformulations that exploit the flow/timing structure, rather than just excluding one infeasible binary pattern with a no-good cut?

  4. Once the formulation is chosen, would you recommend implementing it in JuMP as an outer-loop decomposition, or as branch-and-Benders using lazy constraints?

I would especially appreciate advice on what should be in the master, what should be in the subproblem, and what the valid feasibility cuts would look like.

Hi @decomp-lab, welcome to the forum :smile:

This is a rather open-ended question where the answer is “it depends”.

  1. For binary second-stage, you should really look to the literature. It depends if you want a local or global solution. Some things for you to search: “Lagrangian dual” + “Benders”
  2. Bringing second-stage variables back to the master is one possibility. They become “normal” state variables, so you just need regular optimality cuts
  3. I don’t know that we can help with this. We try to focus on Julia/JuMP related code questions. The MIP literature is vast.
  4. You could do either. See Benders decomposition · JuMP. This also explains how to compute feasibility cuts.

Selfish plug: you might also consider trying GitHub - odow/SDDP.jl: A JuMP extension for Stochastic Dual Dynamic Programming · GitHub.

Thanks, that’s helpful.

I think my original question was probably too broad.

For context, I had tried one Benders variant before posting. I put `x`, `theta`, `delta`, and `gamma` in the master, so that once those were fixed each scenario subproblem was continuous. I then used LP/Farkas feasibility cuts from the scenario subproblems.

That worked on a small test instance, but on the larger instance it did not find a feasible incumbent in a reasonable time. My suspicion is that the cuts are weak, the big-M formulation is making the master too hard, or both.

For now, I am not really looking for a global optimality proof. A good feasible solution/policy would already be useful.

I’ll take your suggestion and try formulating it in SDDP.jl. My rough plan is to treat the shared first-stage decision `x` as the state passed into the recourse stage, with the scenario-specific order/use/stock variables and the timing binaries in the second-stage model. Since the second stage still has binaries, I’ll look at the duality handlers you linked to, probably starting with `LagrangianDuality` or `BanditDuality`.

One thing I’ll need to handle is that the second stage is mostly a feasibility problem rather than a natural recourse-cost minimisation problem, so I may need to add penalised slack variables to get a useful value function.

I’ll try this route first. If I run into problems with the SDDP.jl formulation, I’ll post a smaller follow-up example.

Thanks again for the pointers.

I think I can have some comments on the topic but since I’m now having some ailments on my eye​:smiling_face_with_tear:, I’m now resting. I’ll see if I can share some insights after recovery.

Master side can indeed be the bottleneck of 2SSP-BD, especially if the num of scenarios is large.

I’ve taken a look at your actual code. SDDP is probably the wrong tool for the job.

How many scenarios do you want to scale to?

For now, I am not really looking for a global optimality proof. A good feasible solution/policy would already be useful.

The easiest way to start is just to write Benders. But when you want to compute the dual to bring a cut back, solve the LP relaxation instead of the binary second-stage.