# Multi period inventory / reordering MILP in PULP

I'm trying to optimize reorder quantities for $$n$$ products over $$m$$ periods in PuLP. The total reorder quantity should fill a container. I've never worked with multiple periods before, and I can't get it to work. I tried to formulate it the following way:

Minimize the sum over all ROQ (reorder quantity)

s.t.

ROQ > 0

Inventory m+1 = Inventory m - demand m + ROQ m > 0

sum(ROQ n) m = container * trigger

I've created an integer trigger variable so I can order any quantity that's a multiple of a container, or nothing. It seemed relatively straightforward, but the solver keeps telling me the problem is infeasible to solve. I assume the problem is where I try to set next periods inventory as this periods inventory - demand + ROQ, because turning the equation around throws an error. I can't figure out how to overcome this problem. Can you point me in the right direction?

The formulation in pulp is:

import numpy as np
import pandas as pd
import random
import pulp

import warnings

SKUs = 10
periods = 12
initial_inv = 50
container = 100

warnings.filterwarnings("ignore")

df_DEM = pd.DataFrame(np.random.randint(0,200, size=(SKUs,periods)))
df_INV = pd.DataFrame(np.random.randint(0,200, size=(SKUs,periods+1)))
df_REP = pd.DataFrame(np.random.randint(0,1, size=(SKUs,periods)))

problem = pulp.LpProblem('inventory', pulp.LpMinimize)

#variables
ROQ = pulp.LpVariable.dicts('ROQ', ((i, j) for i in range(SKUs) for j in range(periods)), lowBound=0, cat='Continuous')
trigger = pulp.LpVariable.dicts('trigger', (i for i in range(periods)), cat='Integer')

# set objective function
problem += pulp.lpSum((ROQ[i,j]) for i in range(SKUs) for j in range(periods))

#constraints
#1 ROQs > 0
for i in range(SKUs):
for j in range(periods):
problem += ROQ[i,j] >= 0
#2 trigger >= 0
for j in range(periods):
problem += trigger[i] >= 0

#3 inventory in each period > 0
for i in range(SKUs):
for j in range(periods):
problem += df_INV.loc[i,j]-df_DEM.loc[i,j]+ROQ[i,j] >= 0
#4 inventory for the next period = inventory from this period + demand - reorder quantity
for i in range(SKUs):
for j in range(periods):
problem +=  df_INV.loc[i,j] - df_DEM.loc[i,j] + ROQ[i,j] == df_INV.loc[i,j+1]
#5 ROQ is in multiples of a container
for j in range(periods):
problem += pulp.lpSum(ROQ[i,j] for i in range(SKUs)) == trigger[j] * container

# set solver options
solver = pulp.PULP_CBC_CMD(fracGap=0.001, msg=True, warmStart=False)

# solve problem
status = problem.solve(solver)
print(pulp.LpStatus[status], round(pulp.value(problem.objective),0))

• A useful debugging tool is to write out the LP file and study that carefully. Oct 28 at 14:03
• Yes, you can access the LP file either with a print(problem) (in this case it is printed in your terminal) or with problem.writeLP("my_problem.lp"), in which case it exported in your working directory. Oct 28 at 14:45

• you can remove constraints $$\#1$$ and $$\#2$$, as you have already defined the variables as non negative
• the problem is infeasible, because the inventory is not a variable, it is already defined and is an input of the model. Consequently, with constraints $$\#4$$, ROQ variables are also defined and have no other possible values than the ones imposed by these constraints, which are not compatible with constraints $$\#5$$. Only the initial value of the inventory should be defined.

So what you have to do is to define a new set of variables to track the inventory, and replace df_INV with those variables, for example, like this:

import numpy as np
import pandas as pd
import random
import pulp

import warnings

SKUs = 10
periods = 10
initial_inv = 50
container = 100

warnings.filterwarnings("ignore")

df_DEM = pd.DataFrame(np.random.randint(0,200, size=(SKUs,periods)))
df_REP = pd.DataFrame(np.random.randint(0,1, size=(SKUs,periods)))

problem = pulp.LpProblem('inventory', pulp.LpMinimize)

#variables
ROQ = pulp.LpVariable.dicts('ROQ', ((i, j) for i in range(SKUs) for j in range(periods)), lowBound=0, cat='Continuous')
trigger = pulp.LpVariable.dicts('trigger', (i for i in range(periods)), cat='Integer')
inventory = pulp.LpVariable.dicts('inv', ((i, j) for i in range(SKUs) for j in range(periods+1)), lowBound=0, cat='Continuous')

# set objective function
problem += pulp.lpSum((ROQ[i,j]) for i in range(SKUs) for j in range(periods))

#constraints
#3 inventory in each period > 0
for i in range(SKUs):
for j in range(periods):
problem += inventory[i,j]-df_DEM.loc[i,j]+ROQ[i,j] >= 0

#4 inventory for the next period = inventory from this period + demand - reorder quantity
for i in range(SKUs):
for j in range(periods):
problem +=  inventory[i,j] - df_DEM.loc[i,j] + ROQ[i,j] == inventory[i,j+1]
problem += inventory[i,0] == initial_inv
#5 ROQ is in multiples of a container
for j in range(periods):
problem += pulp.lpSum(ROQ[i,j] for i in range(SKUs)) == trigger[j] * container

# set solver options
solver = pulp.PULP_CBC_CMD(fracGap=0.001, msg=True, warmStart=False)
# solve problem
status = problem.solve(solver)
print(pulp.LpStatus[status], round(pulp.value(problem.objective),0))

• Thank you very much, this was extremely helpful. Also for the hint with how to read the lp file. Highly appreciated!
– Jan
Oct 29 at 6:22