Transport Planning

A firm must transport machines from production plants A, B and C to warehouses X, Y and Z. Five machines are required in X, 4 in Y and 3 in Z, whereas 8 machines are available in A, 5 in B and 3 in C. The transport costs (in euros) between sites are provided in the table below.

Plant/Warehouse

X

Y

Z

A

50

60

30

B

60

40

20

C

40

70

30

a) Formulate an integer linear programming model that minimizes transport costs. This is a simple transportation problem, where the objective is to minimize the transportation costs. Let us note the transportation costs from plant \(i\) to warehouse \(j\) as \(c_{ij}\), our model is:

Indices

  • \(i\) Production plants, \(i \in [A,B,C]\)

  • \(j\) warehouses, \(j \in [X,Y,C]\)

Decision variables

  • \(x_{ij}\) (Integer) Number of machines to transport from production plant \(i\) to warehouse \(j\)

Objective function

\(z = \sum_i{\sum_j{c_{ij}*x_{ij}}}\)

Constraints Demand constraint

\(\sum_i{x_{ij}} \geq d_j \quad \forall j\)

b) Assume that the cost of transporting a machine from plant B increases by €10 for all the machines as of the third one; that is, the 4th, the 5th, etc. Reformulate the model in Section a) by considering this assumption. In this case, we need to define a new decision variable to factor in the extra costs incurred if we source more than three machines from plant B. Let us note these new decision variables as \(x*_{Bj}\) and represents the machines that are sourced from factory B from the third machine. Le us also note as \(c*_{Bj}\) the additional costs incurred from the third machine:

Plant/Warehouse

X

Y

Z

B

70

50

30

Now, our problem model becomes:

Decision variables

  • \(x_{ij}\) (Integer) Number of machines to transport from production plant \(i\) to warehouse \(j\)

  • \(x*_{Bj}\) (Integer) Number of machines to transport from production plant B to warehouse \(j\) from the third one

  • \(Y\) (Binary) Used to determine whether the number of machines sourced from B is greater than 3.

Objective function

\(z = \sum_i{\sum_j{c_{ij}*x_{ij}}} + \sum_j{c*_{Bj}*x*_{Bj}}\)

Constraints Demand constraint

\(\sum_i{x_{ij}} + x*_{Bj} \geq d_j \quad \forall j\)

Logical constraints \(\sum_j{x_{Bj}}\leq 3\)

\(\sum_j{x_{Bj}} - 3 + M*Y \geq 0\)

\(x*_{Bj} + M*Y \geq 0 \quad \forall j\)

\(x*_{Bj} \leq M*(1-Y) \quad \forall j\)

Where M is a very large number. To explain the introduction of the binary decision variables and the set of constraints, note that what we want to accomplish is the following logic:

“if \(\sum_j{x_{Bj}}=3\) then \(x*_{Bj} \geq 0\) else \(x*_{Bj} = 0\)

This is equivalent to the following:

\(((\sum_j{x_{Bj}}=3) \wedge (x*_{Bj} \geq 0)) \vee ((\sum_j{x_{Bj}} \leq 3) \wedge (x*_{Bj} = 0))\)

where \(\wedge\) is the logical AND operator, and \(\vee\) is the logical or. That is, we want to ensure that \(x*_{Bj}\) is greater than zero when \(\sum_j{x_{Bj}}=3\) or that \(x*_{Bj} = 0\) when \(\sum_j{x_{Bj}} \leq 3\). We introduce the new binary decision variable \(Y\) to decide which of this two (mutually exclusive) conditions is true. Then, we multiply the binary decision variable by a very large number to render the constraints of the other condition irrelevant.

We introduce the constraints:

\(\sum_j{x_{Bj}} - 3 + M*Y \geq 0\)

\(x*_{Bj} + M*Y \geq 0 \quad \forall j\)

These constraints correspond to the left side of the \(\vee\) operator.

Similarly, to account for the right hand side, we introduce the constraints:

\(\sum_j{x_{Bj}} - 3 \leq M*(1-Y)\)

\(x*_{Bj} \leq M*(1-Y) \quad \forall j\)

Note that, if Y = 0, the logical constraints become:

\(\sum_j{x_{Bj}}\leq 3\)

\(\sum_j{x_{Bj}} - 3 \geq 0\) (and as per the previous constraint, the sum can only be equal to 3).

\(x*_{Bj} \geq 0 \quad \forall j\)

\(\sum_j{x_{Bj}} - 3 \leq M\) (irrelevant because M is a really large number)

\(x*_{Bj} \leq M \quad \forall j\) (irrelevant because M is a really large number)

Whereas if Y = 1, the logical constraints become:

\(\sum_j{x_{Bj}}\leq 3\)

\(\sum_j{x_{Bj}} - 3 + M \geq 0\) (irrelevant because M is a really large number)

\(x*_{Bj} + M \geq 0 \quad \forall j\) (irrelevant because M is a really large number)

\(\sum_j{x_{Bj}} - 3 \leq 0\) (this is the same as the previous constraint, so also irrelevant)

\(x*_{Bj} \leq 0 \quad \forall j\) (and since they cannot be negative, they must be equal to zero)

Since the constraint \(\sum_j{x_{Bj}} - 3 \leq M*(1-Y)\) is irrelevant when Y=0 and when Y=1, it is removed, and we get the set of constraints in the solution above.

Solution in Python

The following script solves the problem using Python. ### Requirements First, install the requirements:

[ ]:
!pip install pulp
!pip install pandas
!pip install IPython
[ ]:
import pulp
import pandas as pd
#And we will use numpy to perform array operations
import numpy as np
#We will use display and Markdown to format the output of code cells as Markdown
from IPython.display import display, Markdown

# Warehouses and dictionaries
plants = ('A', 'B', 'C')
warehouses = ('X', 'Y', 'Z')
#Transportation costs
transportation_costs = [[50, 60, 30], [60, 40, 20], [40, 70, 30]]

# Instantiate model
model = pulp.LpProblem("Transport Planning", pulp.LpMinimize)

# Demand
demand = [5, 4, 3]

# Capacities
capacities = [8, 5, 3]

variables = pulp.LpVariable.dicts("x",
                                  [(i, j) for i in plants for j in warehouses],
                                  lowBound=0,
                                  cat='Integer')

model += (
             pulp.lpSum([
                 transportation_costs[i][j] * variables[(plants[i], warehouses[j])]
                 for i in range(len(plants)) for j in range(len(warehouses))])
         ), "Transportation Cost"


# Capacity constraints
for i in range(len(plants)):
    model += pulp.lpSum([
        variables[(plants[i], warehouses[j])]
        for j in range(len(warehouses))]) <= capacities[i], plants[i]

# Demand
for j in range(len(warehouses)):
    model += pulp.lpSum([
        variables[(plants[i], warehouses[j])]
        for i in range(len(plants))]) >= demand[j], warehouses[j]
[9]:
import pulp
import pandas as pd
#And we will use numpy to perform array operations
import numpy as np
#We will use display and Markdown to format the output of code cells as Markdown
from IPython.display import display, Markdown

# Warehouses and dictionaries
plants = ('A', 'B', 'C')
warehouses = ('X', 'Y', 'Z')
#Transportation costs
transportation_costs = [[50, 60, 30], [60, 40, 20], [40, 70, 30]]

# Instantiate model
model = pulp.LpProblem("Transport Planning", pulp.LpMinimize)

# Demand
demand = [5, 4, 3]

# Capacities
capacities = [8, 5, 3]

variables = pulp.LpVariable.dicts("x",
                                  [(i, j) for i in plants for j in warehouses],
                                  lowBound=0,
                                  cat='Integer')

model += (
             pulp.lpSum([
                 transportation_costs[i][j] * variables[(plants[i], warehouses[j])]
                 for i in range(len(plants)) for j in range(len(warehouses))])
         ), "Transportation Cost"


# Capacity constraints
for i in range(len(plants)):
    model += pulp.lpSum([
        variables[(plants[i], warehouses[j])]
        for j in range(len(warehouses))]) <= capacities[i], plants[i]

# Demand
for j in range(len(warehouses)):
    model += pulp.lpSum([
        variables[(plants[i], warehouses[j])]
        for i in range(len(plants))]) >= demand[j], warehouses[j]
[10]:
# Solve our problem
model.solve()
print(pulp.LpStatus[model.status])

# Solution
max_z = pulp.value(model.objective)
print(max_z)
Optimal
460.0
[19]:
var_df = pd.DataFrame.from_dict(variables, orient="index",
                                columns = ["Variables"], dtype=object)

var_df["Solution"] = var_df["Variables"].apply(lambda item: item.varValue)
index = pd.MultiIndex.from_product([plants, warehouses], names=['Plants', 'Warehouses'])
var_df2 = pd.DataFrame(var_df["Solution"], index=index, columns = ["Solution"])
display(var_df2.unstack())
Solution
Warehouses X Y Z
Plants
A 2.0 0.0 2.0
B 0.0 4.0 1.0
C 3.0 0.0 0.0
[ ]: