Benders cut generation
Coluna provides an interface and generic functions to implement a Benders cut generation algorithm.
In this section, we are first going to present the generic functions, the implementation with some theory backgrounds and then give the references of the interface. The default implementation is based on the paper of
You can find the generic functions and the interface in the Benders
submodule and the default implementation in the Algorithm
submodule at src/Algorithm/benders
.
Context
The Benders
submodule provides an interface and generic functions to implement a benders cut generation algorithm. The implementation depends on an object called context
.
Coluna.Benders.AbstractBendersContext
— TypeSupertype for the objects to which belongs the implementation of the Benders cut generation and that stores any kind of information during the execution of the Bender cut generation algorithm.
Benders provides two types of context:
Coluna.Algorithm.BendersContext
— TypeBendersContext(reformulation, algo_params) -> BendersContext
Default implementation of the Benders algorithm.
Coluna.Algorithm.BendersPrinterContext
— TypeBendersPrinterContext(reformulation, algo_params) -> BendersPrinterContext
Creates a context to run the default implementation of the Benders algorithm together with a printer that prints information about the algorithm execution.
Generic functions
Generic functions are the core of the Benders cut generation algorithm. There are three generic functions:
Coluna.Benders.run_benders_loop!
— FunctionMain loop of the Benders cut generation algorithm.
See ...
Coluna.Benders.run_benders_iteration!
— FunctionRuns one iteration of a Benders cut generation algorithm.
See ...
These functions are independent of any other submodule of Coluna. You can use them to implement your own Benders cut generation algorithm.
Reformulation
The default implementation works with a reformulated problem contained in MathProg.Reformulation
where master and subproblems are MathProg.Formulation
objects.
The master has the following form:
\[\begin{aligned} \min \quad& cx + \sum_{k \in K} \eta_k & &\\ \text{s.t.} \quad& Ax \geq a & & (1) \\ & \text{< benders cuts>} & & (2) \\ & l_1 \leq x \leq u_1 & & (3) \\ & \eta_k \in \mathbb{R} & \forall k \in K \quad& (4) \end{aligned}\]
where $x$ are first-stage variables, $\eta_k$ is the second-stage cost variable for the subproblem $k$, constraints $(1)$ are the first-stage constraints, constraints $(2)$ are the Benders cuts, constraints $(3)$ are the bounds on the first-stage variables, and expression $(4)$ shows that second-stage variables are free.
The subproblems have the following form:
\[\begin{aligned} \min \quad& fy + {\color{gray} \mathbf{1}z' + \mathbf{1}z''} &&& \\ \text{s.t.} \quad& Dy {\color{gray} + z'} \geq d - B\bar{x} && (5) \quad& {\color{blue}(\pi)} \\ & Ey {\color{gray} + z''} \geq e && (6) \quad& {\color{blue}(\rho)} \\ & l_2 \leq y \leq u_2 && (7) \quad& {\color{blue}(\sigma)} \end{aligned}\]
where $y$ are second-stage variables, $z'$ and $z''$ are artificial variables (in grey because they are deactivated by default), constraints (5) are the reformulation of linking constraints using the first-stage solution $\bar{x}$, constraints (6) are the second-stage constraints, and constraints (7) are the bounds on the second-stage variables. In blue, we define the dual variables associated to these constraints.
References:
Coluna.Benders.is_minimization
— FunctionReturns true
if the objective sense is minimization, false
otherwise.
Coluna.Benders.get_reform
— FunctionReturns Benders reformulation.
Coluna.Benders.get_master
— FunctionReturns the master problem.
Coluna.Benders.get_benders_subprobs
— FunctionReturns the separation subproblems.
Main loop
This is a description of how the Coluna.Benders.run_benders_loop!
generic function behaves with the default implementation.
The loop stops if one of the following conditions is met:
- the master is infeasible
- a separation subproblem is infeasible
- the time limit is reached
- the maximum number of iterations is reached
- no new cut is generated
The default implementation returns:
Coluna.Algorithm.BendersOutput
— TypeOutput of the default implementation of the Benders algorithm.
It contains:
infeasible
: the original problem is infeasibletime_limit_reached
: the time limit was reachedmlp
: the final bound obtained with the Benders cut algorithmip_primal_sol
: the best primal solution to the original problem found by the Benders cut algorithm
References:
Coluna.Benders.setup_reformulation!
— FunctionPrepares the reformulation before starting the Benders cut generation algorithm.
Coluna.Benders.stop_benders
— FunctionReturns true
if the Benders cut generation algorithm must stop, false
otherwise.
Coluna.Benders.after_benders_iteration
— FunctionPlaceholder method called after each iteration of the Benders cut generation algorithm.
Coluna.Benders.AbstractBendersOutput
— TypeSupertype for the custom objects that will store the output of the Benders cut generation algorithm.
Coluna.Benders.benders_output_type
— Functionbenders_output_type(context) -> Type{<:AbstractBendersOutput}
Returns the type of the custom object that will store the output of the Benders cut generation algorithm.
Coluna.Benders.new_output
— FunctionReturns a new instance of the custom object that stores the output of the Benders cut generation algorithm.
Benders cut generation iteration
This is a description of how the Coluna.Benders.run_benders_iteration!
generic function behaves with the default implementation.
These are the main steps of a Benders cut generation iteration without stabilization. Click on the step to go to the corresponding section.
In the default implementation, some sections may have different behaviors depending on the result of previous steps.
Master optimization
This operation consists in optimizing the master problem in order to find a first-level solution $\bar{x}$.
In the default implementation, master optimization can be performed using SolveLpForm
(LP solver) or SolveIpForm
(MILP solver). When getting the solution, we store the current value of second stage variables $\bar{\eta}_k$ as incumbent value (see Coluna.MathProg.getcurincval
).
It returns an object of the following type:
Coluna.Algorithm.BendersMasterResult
— TypeOutput of the default implementation of the Benders.optimize_master_problem!
method.
It contains:
ip_solver
:true
if the master problem is solved with a MIP solver and involves integral variables,false
otherwise.result
: the result of the master problem optimization stored in anOptimizationState
object.infeasible
:true
if the master at the current iteration is infeasible;false
otherwise.unbounded
:true
if the master at the current iteration is unbounded;false
otherwise.certificate
:true
if the master at the current iteration is unbounded and if the current result is a dual infeasibility certificate,false
otherwise.
References:
Coluna.Benders.optimize_master_problem!
— Functionoptimize_master_problem!(master, context, env) -> MasterResult
Returns an instance of a custom object MasterResult
that implements the following methods:
is_unbounded(res::MasterResult) -> Bool
is_infeasible(res::MasterResult) -> Bool
is_certificate(res::MasterResult) -> Bool
get_primal_sol(res::MasterResult) -> Union{Nothing, PrimalSolution}
Go back to the cut generation iteration diagram.
Unbounded master case
Second stage cost $\eta_k$ variables are free. As a consequence, the master problem is unbounded when there is no optimality Benders cuts.
In this case, Coluna.Benders.treat_unbounded_master_problem_case!
is called. The main goal of the default implementation of this method is to get the dual infeasibility certificate of the master problem.
If the master has been solved with a MIP solver at the previous step, we need to relax the integrality constraints to get a dual infeasibility certificate.
If the solver does not provide a dual infeasibility certificate, the implementation has an "emergency" routine to provide a first-stage feasible solution by solving the master LP with cost of second stage variables set to zero. We recommend using a solver that provides a dual infeasibility certificate and avoiding the "emergency" routine.
References:
Coluna.Benders.treat_unbounded_master_problem_case!
— Functiontreat_unbounded_master_problem_case!(master, context, env) -> MasterResult
When after a call to optimize_master_problem!
, the master is unbounded, this method is called. Returns an instance of a custom object MasterResult
.
Go back to the cut generation iteration diagram.
Setup separation subproblems
The separation subproblems differs depending on whether the restricted master is unbounded or not:
- if the restricted master is optimal, the generic function calls
Coluna.Benders.update_sp_rhs!
- if the restricted master is unbounded, the generic function calls
Coluna.Benders.setup_separation_for_unbounded_master_case!
Default implementation of Coluna.Benders.update_sp_rhs!
updates the right-hand side of the linking constraints (5).
Reference:
Coluna.Benders.update_sp_rhs!
— Functionupdate_sp_rhs!(context, sp, mast_primal_sol)
Updates the right-hand side of the separation problem sp
by fixing the first-level solution obtained by solving the master problem mast_primal_sol
.
Default implementation of Coluna.Benders.setup_separation_for_unbounded_master_case!
gives rise to the formulation proposed in Lemma 2 of Bonami et al:
\[\begin{aligned} (SepB) \equiv \min \quad& fy + {\color{gray} \mathbf{1}z' + \mathbf{1}z''} &&& \\ \text{s.t.} \quad& Dy {\color{gray} + z'} \geq -B\bar{x} && (5a) \quad& {\color{blue}(\pi)} \\ & Ey {\color{gray} + z''} \geq 0 && (6a) \quad& {\color{blue}(\rho)} \\ & y \geq 0 && (7a) \quad& {\color{blue}(\sigma)} \end{aligned}\]
where $y$ are second-stage variables, $z'$ and $z''$ are artificial variables (in grey because they are deactivated by default), and $\bar{x}$ is an unbounded ray of the restricted master.
Reference:
Coluna.Benders.setup_separation_for_unbounded_master_case!
— Functionsetup_separation_for_unbounded_master_case!(context, sp, mast_primal_sol)
Updates the separation problem to derive a cut when the master problem is unbounded.
Subproblem iterator
Not implemented yet.
Separation subproblem optimization
The default implementation first optimize the subproblem without the artificial variables $z'$ and $z''$. In the case where it finds $(\bar{\pi}, \bar{\rho}, \bar{\sigma})$ an optimal dual solution to the subproblem, the following cut is generated:
\[\eta_k + \bar{\pi}Bx \geq d\bar{\pi} + \bar{\rho}e + \bar{\sigma_{\leq}} l_2 + \bar{\sigma_{\geq}} u_2\]
with $\bar{\sigma_{\leq}} l_2$ (respectively $\bar{\sigma_{\geq}} u_2$) the dual of the left part (respectively the right part) of constraint $l_2 \leq y \leq u_2$ of the subproblem.
In the case where it finds the subproblem infeasible, it calls Coluna.Benders.treat_infeasible_separation_problem_case!
. The default implementation of this method activates the artificial variables $z'$ and $z''$, sets the cost of second stage variables to 0, and optimizes the subproblem again.
If a solution with no artificial variables is found, the following cut is generated:
\[\bar{\pi}Bx \geq d\bar{\pi} + \bar{\rho}e + \bar{\sigma_{\leq}} l_2 + \bar{\sigma_{\geq}} u_2\]
Both methods return an object of the following type:
Coluna.Algorithm.BendersSeparationResult
— TypeOutput of the default implementation of the Benders.optimize_separation_problem!
and Benders.treat_infeasible_separation_problem_case!
methods.
It contains:
second_stage_estimation_in_master
: the value of the second stage cost variable in the solution to the master problem.second_stage_cost
: the value of the second stage cost variable in the solution to the separation problem.lp_primal_sol
: the primal solution to the separation problem.infeasible
:true
if the current separation problem is infeasible;false
otherwise.unbounded
:true
if the current separation problem is unbounded;false
otherwise.cut
: the cut generated by the separation problem.infeasible_treatment
:true
if this object is an output of theBenders.treat_infeasible_separation_problem_case!
method;false
otherwise.unbounded_master
:true
if the separation subproblem has the form of Lemma 2 to separate a cut to truncate an unbounded ray of the restricted master problem;false
otherwise.
References:
Coluna.Benders.optimize_separation_problem!
— Functionoptimize_separation_problem!(context, sp_to_solve, env, unbounded_master) -> SeparationResult
Returns an instance of a custom object SeparationResult
that implements the following methods:
is_unbounded(res::SeparationResult) -> Bool
is_infeasible(res::SeparationResult) -> Bool
get_obj_val(res::SeparationResult) -> Float64
get_primal_sol(res::SeparationResult) -> Union{Nothing, PrimalSolution}
get_dual_sp_sol(res::SeparationResult) -> Union{Nothing, DualSolution}
Coluna.Benders.treat_infeasible_separation_problem_case!
— Functiontreat_infeasible_separation_problem_case!(context, sp_to_solve, env, unbounded_master) -> SeparationResult
When after a call to optimize_separation_problem!
, the separation problem is infeasible, this method is called. Returns an instance of a custom object SeparationResult
.
Go back to the cut generation iteration diagram.
Set of generated cuts
You can define your data structure to manage the cuts generated at a given iteration. Columns are inserted after the optimization of all the separation subproblems to allow the parallelization of the latter.
In the default implementation, cuts are represented by the following data structure:
Coluna.Algorithm.GeneratedCut
— TypeSolution to the separation problem together with its corresponding benders cut.
It contains:
min_sense
:true
if it's a minimization problem;false
otherwise.lhs
: the left-hand side of the cut.rhs
: the right-hand side of the cut.dual_sol
: an optimal dual solution to the separation problem.
We use the following data structures to store the cuts and the primal solutions to the subproblems:
Coluna.Algorithm.CutsSet
— TypeStores a collection of cuts.
It contains cuts
a vector of GeneratedCut
objects.
Coluna.Algorithm.SepSolSet
— TypePrimal solutions to the separation problems optimized at the current iteration. This is used to build a primal solution.
It contains sols
a vector of primal solutions.
The default implementation of push_in_set!
has the responsibility to check if the cut is violated. Given $\bar{\eta}_k$ solution to the restricted master and $\bar{y}$ solution to the separation problem, the cut is considered as violated when:
- the separation subproblem was infeasible
- or $\bar{\eta}_k \geq f\bar{y}$
References:
Coluna.Benders.set_of_cuts
— FunctionReturns an empty container that will store all the cuts generated by the separation problems during an iteration of the Benders cut generation algorithm. One must be able to iterate on this container to insert the cuts in the master problem.
Coluna.Benders.set_of_sep_sols
— FunctionReturns an empty container that will store the primal solutions to the separation problems at a given iteration of the Benders cut generation algorithm.
Coluna.Benders.push_in_set!
— Functionpush_in_set!(context, cut_pool, sep_result) -> Bool
Inserts a cut in the set of cuts generated at a given iteration of the Benders cut generation algorithm. The cut_pool
structure is generated by set_of_cuts(context)
.
push_in_set!(context, sep_sp_sols, sep_result) -> Bool
Inserts a primal solution to a separation problem in the set of primal solutions generated at a given iteration of the Benders cut generation algorithm. The sep_sp_sols
structure is generated by set_of_sep_sols(context)
.
Returns true
if the cut or the primal solution was inserted in the set, false
otherwise.
Go back to the cut generation iteration diagram.
Unboundedness check
This check is performed only when the restricted master is unbounded.
To perform this check, we need a solution to each separation problem.
Let $(\bar{\eta}_k)_{k \in K}$ be the value of second stage variables in the dual infeasibility certificate of the restricted master. Let $\bar{y}$ be an optimal solution to the separation problem (SepB).
As indicated by Bonami et al., if $f\bar{y} \leq \sum\limits_{k \in K} \bar{\eta}_k$, then the original problem is unbounded (by definition of an unbounded ray of the original problem).
References:
Coluna.Benders.master_is_unbounded
— FunctionReturns true
if the master has been proven unbounded, false
otherwise.
Cuts insertion
The default implementation inserts into the master all the cuts stored in the CutsSet
object.
Reference:
Coluna.Benders.insert_cuts!
— FunctionInserts the cuts into the master.
Go back to the cut generation iteration diagram.
Current primal solution
Lorem ipsum.
References:
Coluna.Benders.build_primal_solution
— FunctionBuilds a primal solution to the original problem from the primal solution to the master problem and the primal solutions to the separation problems.
Iteration output
Coluna.Algorithm.BendersIterationOutput
— TypeOutput of the default implementation of an iteration of the Benders algorithm.
It contains:
min_sense
: the original problem is a minimization problemnb_new_cuts
: the number of new cuts added to the master problemip_primal_sol
: the primal solution to the original problem found during this iterationinfeasible
: the original problem is infeasibletime_limit_reached
: the time limit was reachedmaster
: the solution value to the master problem
References:
Coluna.Benders.AbstractBendersIterationOutput
— TypeSupertype for the custom objects that will store the output of a Benders iteration.
Coluna.Benders.benders_iteration_output_type
— Functionbenders_iteration_output_type(context) -> Type{<:AbstractBendersIterationOutput}
Returns the type of the custom object that will store the output of a Benders iteration.
Coluna.Benders.new_iteration_output
— FunctionReturns a new instance of the custom object that stores the output of a Benders iteration.
Go back to the cut generation iteration diagram.
Getters for Result data structures
Method name | Master | Separation |
---|---|---|
is_unbounded | X | X |
is_infeasible | X | X |
is_certificate | X | |
get_primal_sol | X | X |
get_dual_sol | X | |
get_obj_val | X | X |
Coluna.Benders.is_unbounded
— FunctionReturns true
if the problem is unbounded, false
otherwise.
Coluna.Benders.is_infeasible
— FunctionReturns true
if the master is infeasible, false
otherwise.
Coluna.Benders.is_certificate
— FunctionReturns the certificate of dual infeasibility if the master is unbounded, nothing
otherwise.
Coluna.Benders.get_primal_sol
— FunctionReturns the primal solution of the master problem if it exists, nothing
otherwise.
Coluna.Benders.get_dual_sol
— FunctionReturns the dual solution of the separation problem if it exists; nothing
otherwise.
Coluna.Benders.get_obj_val
— FunctionReturns the objective value of the master or separation problem.
Go back to the cut generation iteration diagram.
Stabilization
Not implemented yet.