# AMPL Development Tutorial 4/6 – Benders Decomposition via PYTHON scripting#

Description: In this fourth installment of our six-part series, we advance our exploration by demonstrating how to adapt our AMPL script for use with AMPL’s Python API. This adaptation facilitates easier integration into production environments by transitioning the control flow of the Benders algorithm to the API level. We will explore how to import data into AMPL from Pandas data frames and detail the process of extracting data from AMPL for use within Python. This approach not only streamlines the interaction between AMPL and Python but also sets the stage for implementing parallel processing of subproblems in subsequent sections. In our final notebook, we will introduce the AMPLS library. This powerful tool enhances the efficiency of Benders decomposition by allowing for the export of AMPL model instances as persistent solver representations, further optimizing the solution process.

Tags: amplpy, ampl, mip, stochastic, facility location, benders, decomposition

Notebook author: Gyorgy Matyasfalvi <gyorgy@ampl.com>

References:

AMPL a Modeling Language for Mathematical Programming – Robert Fourer et al.

Intro to Linear Optimization – Dimitris Bertsimas et al.

SCIP Optimization Suite example projects – Stephen J. Maher (ZIB)

```
# Install dependencies
%pip install -q amplpy pandas
```

```
# Google Colab & Kaggle integration
from amplpy import AMPL, ampl_notebook
ampl = ampl_notebook(
modules=["open"], # modules to install
license_uuid="default", # license to use
) # instantiate AMPL object and register magics
```

```
# Import all necessary libraries
import pandas as pd
```

## Introduction#

To ensure a cohesive understanding and for ease of reference, we will revisit the foundational concepts of stochastic programming and the theory behind Benders decomposition in this notebook. This repetition will help reinforce these critical concepts and provide a seamless transition into the PYTHON script implementing Benders decomposition of our stochastic facility location problem.

## Two-stage stochastic programming problem formulation#

Consider a decision-making process occurring over two sequential stages. Initially, a decision vector \(x\) is selected. Following this, new information becomes available, prompting the selection of a second-stage decision vector \(y\). We introduce \(S\) possible scenarios for the newly acquired information, with the actual scenario being revealed only after the selection of \(x\). Each scenario is indexed by \(s\), and its occurrence probability is denoted by \(\alpha^s\), which we assume to be positive. Given the sequential nature of decision-making, the choice of \(y\) can be scenario-dependent, denoted as \(y^s\) to reflect this.

The decision vectors \(x\) and \(y^s\) incur costs represented by vectors \(f\) and \(q\), respectively. The first-stage decisions are constrained by:

Furthermore, the combination of first and second-stage decisions must satisfy the scenario-specific constraints:

for every scenario \(s\).
The uncertainty in the model is encapsulated by \(\xi := (q, B, D, d)\), where a constraint is deemed part of the second stage if it involves any nonzero coefficients of variables \({y}\).
The matrices \({B}^s\) are referred to as *technology matrices*, which determine the influence of the decision at the first stage on the second stage.
The objective is to determine the choices of \(x\) and \(y^s\) for all \(s=1, \ldots, S\) that minimize the expected total cost:

This formulation leads to the following two-stage stochastic optimization problem:

we will refer to the above problem as the **master problem**.
Notice that even if the number of possible scenarios \(S\) is moderate, this formulation can be a large linear programming problem.
However, a decomposition method can help.

## Benders decomposition for two-stage problems#

Given a vector \(\hat{x}\), using the dual simplex method solve the subproblem (for all \(s \in S\)):

\[\begin{split} \begin{array}{crl} \min & (q^s)^T y^s & \\ \textrm{s.t.} & D^s y^s & = d^s - B^s \hat{x} \\ & y^s & \geq 0 \end{array} \end{split}\]The dual of (5) takes the following form:

\[\begin{split} \begin{array}{crl} \max & (p^s)^T (d^s - B^s \hat{x}) & \\ \textrm{s.t.} & (p^s)^T D^s & <= (q^s)^T \end{array} \end{split}\]Let

\[\begin{split} \begin{array}{crrl} & P \;\; = & \{ p & | \;\; p^T D \leq q^T \} & \\ \end{array} \end{split}\]We assume that \(P\) is nonempty and has at least one extreme point (so we can apply duality theory). Let \(p_i\), \(i = 1, \ldots, I\), be the extreme points, and let \(w^j\), \(j = 1, \ldots, J\), be a complete set of extreme rays of \(P\).

Define \(z^s(\hat{x})^*\) as the optimal cost obtained from problem (5) described above. Returning to the optimization of \(x\), we encounter the following problem:

\[\begin{split} \begin{array}{crcrcrccrcl} \min & f^T x & + & \sum_{s=1}^S \alpha^s z^s(x) & \\ \textrm{s.t.} & A x & & & = {b} \\ & x & \geq 0 & & \end{array} \end{split}\]Under our assumption that the set \(P\) is nonempty, either the dual problem (6) has an optimal solution and \(z^s(\hat{x})\) is finite, or the optimal dual cost is infinite (indicating that the primal (5) is infeasible).

In particular, \(z^s(x) \leq \infty \leftrightarrow (w^j)^T (d^s - B^s x) \leq 0, \; \forall j\) and whenever \(z^s(x)\) is finite the optimum of (6) must be attained at an extreme point of the set \(P\) meaning \(z^s(x)\) is the smallest number \(z^s\) such that \((p^i)^T (d^s - B^s x) \leq z^s, \; \forall i\), leading us to the following master-problem formulation:

\[\begin{split} \begin{array}{crcrcrccrcl} \min & f^T x & + & \sum_{s=1}^S \alpha^s z^s & & \\ \textrm{s.t.} & A x & & & = b & \\ & (p^i)^T (d^s - B^s x) & & & \leq z^s & \forall \; i, s \\ & (w^j)^T (d^s - B^s x) & & & \leq 0 & \forall \; j, s \\ & x & \geq 0 & & & \end{array} \end{split}\]**In our algorithm we will only generate constraints that we find to be violated by the current solution.**

## The algorithm#

**Initialization step**: initialize \((\hat{x}, \hat{z}^1, \ldots, \hat{z}^S)\) to zero.**Sub-problem step**: using the dual-simplex method solve the sub-problems individually for every \(s \in S\):

**Check optimality of master-problem**: If for every \(s\), the corresponding sub-problem is feasible and**\((q^s)^T \hat{y}^s \leq \hat{z}^s\)**then all constraints are satisfied and we have an optimal solution to the master problem.**Add optimality cut**: If the sub-problem corresponding to some \(s\) has an optimal solution such that**\((q^s)^T \hat{y}^s > \hat{z}^s\)**the following cut is added to the master problem:

$\( (p^s)^T (d^s - B^s x) \leq z^s, \)\( where \)p^s$ is an optimal basic feasible solution to the dual of the sub-problem.**Add feasibility cut**: If the sub-problem corresponding to some \(s\) is infeasible, its dual has infinite cost and the following cut is added to the master problem:

$\( (w^s)^T (d^s - B^s x) \leq 0, \)\( where \)w^s$ is a positive cost extreme ray.

## Stochastic facility location a concrete example#

**Given:**

A set of facilities: \(I\).

A set of customers: \(J\).

Set of scenarios: \(S\) (representing different customer demands).

**Task:**

Find the minimum cost facilities to open such that the customer demand can be satisfied in all scenarios.

### Variables#

\(x_i \in \{0, 1\} \quad \forall i \in I\)

\(x_i = 1\) if facility \(i\) is opened.

\(y_{ij}^s \geq 0 \quad \forall i \in I, \forall j \in J, \forall s \in S\)

\(y_{ij}^s\) is the level of demand for customer \(j\) satisfied by facility \(i\) in scenario \(s\).

### Parameters:#

\(\alpha^s\): the probability of scenario \(s\).

\(f_i\): the fixed cost for opening facility \(i\),

\(q_{ij}\): the cost of servicing customer \(j\) from facility \(i\),

\(\lambda_j^s\): the demand of customer \(j\) in scenario \(s\),

\(k_i:\) the capacity of facility \(i\).

### The extensive form#

The extensive form of our stochastic program can be formulated as follows:

### The sub-problem#

Given the above formulation for the extensive form (12) we can express our scenario specific sub-problems as follows

### The master-problem#

The master-problem takes the following form

## AMPL implementation#

Converting the mathematical formulation of our decomposition scheme into an AMPL model is straightforward. Similar to the extensive form, AMPL enables the modeling of problems in a way that closely aligns with their conceptual structure. In AMPL, sets and parameters are globally accessible, allowing for seamless integration across different problem components.

AMPL facilitates the assignment of variables, objective functions, and constraints to specific named problems using the syntax `problem <problemName>: <list of model entities>`

.
In the model file presented below, we’ve organized the content to clearly indicate which elements pertain to the master problem and which to the subproblems.
This organization is primarily for clarity and ease of understanding; the AMPL interpreter treats the entire file as a single problem until it encounters explicit problem statements.

Beyond the entities familiar from the extensive form, we introduce additional parameters like `sub_facility_open`

, which convey the master problem’s solutions to the subproblems.
We also incorporate parameters for the dual values from the subproblems, namely `customer_price`

and `facility_price`

.
New constraints, often referred to as cuts, are added to the master problem based on the subproblems’ feasibility and optimality relative to the master problem’s objectives.

Furthermore, a new set of variables, `sub_variable_cost`

, is added to the master problem. These variables account for the variable costs identified in each scenario’s subproblem, contributing to the overall total cost calculation.

```
%%writefile floc_bend.mod
# Global sets
set FACILITIES; # set of facilities
set CUSTOMERS; # set of customers
set SCENARIOS; # set of scenarios
# Global parameters
param customer_demand{CUSTOMERS, SCENARIOS} >= 0; # customer_demand of customer j in scenario s
param facility_capacity{FACILITIES} >= 0; # facility_capacity of facility_open i
### SUBPROBLEM ###
# Subproblem parameters
param variable_cost{FACILITIES, CUSTOMERS} >= 0; # variable cost of satisfying customer_demand of customer j from facility_open i
# Bender's parameters
param sub_facility_open{FACILITIES} binary, default 1; # 1 if facility i is open, 0 otherwise
param sub_scenario symbolic in SCENARIOS;
# Subproblem variables
var production{FACILITIES, CUSTOMERS, SCENARIOS} >= 0; # production from facility i to satisfy customer demand j in scenario s
# Subproblem objective
minimize operating_cost:
sum{i in FACILITIES, j in CUSTOMERS} variable_cost[i,j]*production[i,j,sub_scenario];
# Subproblem constraints
s.t. satisfying_customer_demand{j in CUSTOMERS}:
sum{i in FACILITIES} production[i,j,sub_scenario] >= customer_demand[j,sub_scenario];
s.t. facility_capacity_limits{i in FACILITIES}:
sum{j in CUSTOMERS} production[i,j,sub_scenario] <= facility_capacity[i] * sub_facility_open[i];
### MASTER PROBLEM ###
# Master parameters
param fixed_cost{FACILITIES} >= 0; # fixed cost of opening facility_open i
param prob{SCENARIOS} default 1/card(SCENARIOS); # probability of scenario s
# Bender's parameters
param nITER >= 0 integer;
param cut_type {1..nITER, SCENARIOS} symbolic within {"opt", "feas", "none"};
param customer_price{CUSTOMERS, SCENARIOS, 1..nITER} >= 0; # dual variable for the demand constraint
param facility_price{FACILITIES, SCENARIOS, 1..nITER} <= 0; # dual variable for the capacity constraint
# Master variables
var sub_variable_cost{SCENARIOS} >= 0; # subproblem objective bound
var facility_open{FACILITIES} binary; # 1 if facility i is open, 0 otherwise
# Master objective
minimize total_cost:
sum{i in FACILITIES} fixed_cost[i]*facility_open[i] + sum{s in SCENARIOS} prob[s]*sub_variable_cost[s];
# Master constraints
s.t. sufficient_production_capacity:
sum{i in FACILITIES} facility_capacity[i]*facility_open[i] >= max{s in SCENARIOS} sum{j in CUSTOMERS} customer_demand[j,s];
s.t. optimality_cut {k in 1..nITER, s in SCENARIOS: cut_type[k,s] == "opt"}:
sum{j in CUSTOMERS} customer_price[j,s,k]*customer_demand[j,s] +
sum{i in FACILITIES} facility_price[i,s,k]*facility_capacity[i]*facility_open[i] <= sub_variable_cost[s];
s.t. feasibility_cut {k in 1..nITER, s in SCENARIOS: cut_type[k,s] == "feas"}:
sum{j in CUSTOMERS} customer_price[j,s,k]*customer_demand[j,s] +
sum{i in FACILITIES} facility_price[i,s,k]*facility_capacity[i]*facility_open[i] <= 0;
```

```
Overwriting floc_bend.mod
```

### Data as Python objects#

In this notebook, we continue to work with the same dataset as in the previous installments.
However, the format in which we store our data differs; we now utilize Python lists and Pandas data frames. Initially, we create these Python objects and subsequently employ `amplpy`

methods to seamlessly transfer the data into AMPL.

```
# Define the data
# Define sets
FACILITIES = ["Baytown_TX", "Beaumont_TX", "Baton_Rouge_LA"]
CUSTOMERS = ["San_Antonio_TX", "Dallas_TX", "Jackson_MS", "Birmingham_AL"]
SCENARIOS = ["Low", "Medium", "High"]
# Define parameters indexed over FACILITIES
fixed_cost_df = pd.DataFrame(
[400000, 200000, 600000], index=FACILITIES, columns=["fixed_cost"]
)
facility_capacity_df = pd.DataFrame(
[1550, 650, 1750], index=FACILITIES, columns=["facility_capacity"]
)
# Define parameters indexed over SCENARIOS
prob_df = pd.DataFrame([0.25, 0.5, 0.25], index=SCENARIOS, columns=["prob"])
# Define parameters indexed over CUSTOMERS and SCENARIOS
customer_demand_df = pd.DataFrame(
[450, 650, 887, 910, 1134, 1456, 379, 416, 673, 91, 113, 207],
index=pd.MultiIndex.from_product(
[CUSTOMERS, SCENARIOS], names=["CUSTOMERS", "SCENARIOS"]
),
columns=["customer_demand"],
)
# Define parameters indexed over FACILITIES and CUSTOMERS
variable_cost_df = pd.DataFrame(
[
5739.725,
6539.725,
8650.40,
22372.1125,
6055.05,
6739.055,
8050.40,
21014.225,
8650.40,
7539.055,
4539.72,
15024.325,
],
index=pd.MultiIndex.from_product(
[FACILITIES, CUSTOMERS], names=["FACILITIES", "CUSTOMERS"]
),
columns=["variable_cost"],
)
```

### The Benders algorithm script in Python#

The Python snippet below begins the process by importing the required model file and configuring the necessary settings. Additionally, it loads data from the previously defined Python objects.

For lists, we can directly assign data to AMPL sets using the syntax `ampl.set['<set name>'] = <Python list holding data>`

.
When dealing with parameters represented in Pandas dataframes, we employ the `set_data()`

method.
This method matches the dataframe’s column names with corresponding entities in the AMPL model, ensuring proper assignment of values.
It’s important to ensure that the dataframe’s indexing aligns with the AMPL model’s indexing scheme.

To define suffixes and problem structures, we utilize the `eval()`

method.

Unlike in the AMPL-only approach, we no longer need to define most loop logic parameters in AMPL; they will be represented as Python objects instead.
The control flow of the algorithm is managed through Python’s control structures.
The notable exception is the `nITER`

parameter, which remains defined in AMPL to allow us to index constraints and price parameters adequately.

```
ampl = AMPL() # Instantiate an AMPL object
file_path = "floc_bend.mod" # Define the file path
ampl.read(file_path) # Read the model from file
# Set options
ampl.option["solver"] = "highs" # Select the solver
# Ensure that the solver uses the dual simplex method and returns dual rays that's why presolve is turned off:
ampl.option["highs_options"] = "pre:solve off alg:simplex 1"
# Also turn off presolve in AMPL so we get info from the solver:
ampl.option["presolve"] = 0
ampl.option["display_eps"] = 0.000001 # Set the display precision
ampl.option["omit_zero_rows"] = 1 # Omit zero rows in the output
# Load data
# Load sets
ampl.set["FACILITIES"] = FACILITIES
ampl.set["CUSTOMERS"] = CUSTOMERS
ampl.set["SCENARIOS"] = SCENARIOS
# Load parameters
ampl.set_data(fixed_cost_df)
ampl.set_data(variable_cost_df)
ampl.set_data(customer_demand_df)
ampl.set_data(facility_capacity_df)
ampl.set_data(prob_df)
# Set up AMPL and Python for Benders decomposition
# Define suffix for storing dual rays
ampl.eval(r"suffix dunbdd;")
# Define Master and Sub problems
ampl.eval(
r"""
problem Master: facility_open, sub_variable_cost, total_cost, optimality_cut, feasibility_cut, sufficient_production_capacity;
problem Sub: production, operating_cost, satisfying_customer_demand, facility_capacity_limits;
"""
)
# Loop logic variables
epsilon = 0.00001
n_iter = 0
ampl.param["nITER"] = n_iter
violation_ctr = 0
# Start Bender's loop
while True:
violation_ctr = 0
n_iter += 1
ampl.param["nITER"] = n_iter
print(f"\n\nITERATION {n_iter}\n")
# Loop through the scenarios and solve each subproblem and update dual prices for each scenario
for s in SCENARIOS:
ampl.param["sub_scenario"] = s
ampl.solve(problem="Sub")
# Check for feasibility
if ampl.solve_result == "infeasible":
ampl.param["cut_type"][n_iter, s] = "feas"
# Update cut_type, customer_price, and facility_price inside AMPL
ampl.eval(
r"""
let {j in CUSTOMERS} customer_price[j,sub_scenario,nITER] := satisfying_customer_demand[j].dunbdd;
let {i in FACILITIES} facility_price[i,sub_scenario,nITER] := facility_capacity_limits[i].dunbdd;
"""
)
print(f"{n_iter}: Feasibility cut added for scenario {s}")
# Check for optimality
elif (
ampl.obj["operating_cost"].value()
> ampl.var["sub_variable_cost"][s].value() + epsilon
):
ampl.param["cut_type"][n_iter, s] = "opt"
# Update cut_type, customer_price, and facility_price inside AMPL
ampl.eval(
r"""
let {j in CUSTOMERS} customer_price[j,sub_scenario,nITER] := satisfying_customer_demand[j].dual;
let {i in FACILITIES} facility_price[i,sub_scenario,nITER] := facility_capacity_limits[i].dual;
"""
)
print(f"{n_iter}: Optimality cut added for scenario {s}")
else:
violation_ctr += 1
ampl.param["cut_type"][n_iter, s] = "none"
# If no cuts are added we can break the loop as we have found the optimal solution
if violation_ctr == len(SCENARIOS):
break
# Solve the master problem
print(f"\nSOLVING THE MASTER PROBLEM\n")
ampl.solve(problem="Master")
ampl.eval(r"let {i in FACILITIES} sub_facility_open[i] := facility_open[i];")
print(f"\n\nOPTIMAL SOLUTION FOUND\nExpected cost = {ampl.obj['total_cost'].value()}\n")
ampl.eval(r"display facility_open;")
```

```
ITERATION 1
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 11621793.46
4 simplex iterations
0 barrier iterations
1: Optimality cut added for scenario Low
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 14779784.86
5 simplex iterations
0 barrier iterations
1: Optimality cut added for scenario Medium
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 21050711.2
6 simplex iterations
0 barrier iterations
1: Optimality cut added for scenario High
SOLVING THE MASTER PROBLEM
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 16688018.6
0 simplex iterations
1 branching nodes
ITERATION 2
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 11621793.46
0 simplex iterations
0 barrier iterations
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 14966984.86
2 simplex iterations
0 barrier iterations
2: Optimality cut added for scenario Medium
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 21570711.2
0 simplex iterations
0 barrier iterations
SOLVING THE MASTER PROBLEM
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 16758018.6
1 simplex iterations
1 branching nodes
ITERATION 3
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 11621793.46
0 simplex iterations
0 barrier iterations
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 14779784.86
1 simplex iterations
0 barrier iterations
HiGHS 1.6.0: pre:solve = off
alg:simplex = 1
HiGHS 1.6.0: optimal solution; objective 21050711.2
1 simplex iterations
0 barrier iterations
OPTIMAL SOLUTION FOUND
Expected cost = 16758018.596250001
facility_open [*] :=
Baton_Rouge_LA 1
Baytown_TX 1
Beaumont_TX 1
;
```

## Conclusion#

Transitioning control flow of the Benders algorithm to Python has not altered the core solution of our problem.
However, by shifting the control of the solve executions to Python code, we’ve significantly enhanced the flexibility of our algorithm’s control mechanism.
This newfound flexibility opens the door to sophisticated enhancements, such as parallelizing the scenario iterations within the main `while`

loop.
With the Benders algorithm’s control flow now in Python, implementing such parallelization becomes far more feasible and straightforward.