3
$\begingroup$

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))    
$\endgroup$
3
  • 2
    $\begingroup$ A useful debugging tool is to write out the LP file and study that carefully. $\endgroup$ Oct 28, 2021 at 14:03
  • 1
    $\begingroup$ 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. $\endgroup$
    – Kuifje
    Oct 28, 2021 at 14:45
  • $\begingroup$ Sorry, I do not know where else to ask, than in the answer section, did you end up getting a code for this up and running? It could be very useful for me to use. B.r. $\endgroup$
    – jox23
    Jan 25 at 23:29

1 Answer 1

5
$\begingroup$

Some comments on your model:

  • 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)) 
$\endgroup$
1
  • $\begingroup$ Thank you very much, this was extremely helpful. Also for the hint with how to read the lp file. Highly appreciated! $\endgroup$
    – Jan
    Oct 29, 2021 at 6:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.