Home Supply Chain Case Study - Aggregate Planning
Post
Cancel

Supply Chain Case Study - Aggregate Planning

(Important note: To get the most out of the below case study, the solution report, which includes LP formulations, optimal values of the decision variables, and interpretations of the results, can be used as a companion.)

Case Definition

Can Caravan is a renowned caravan manufacturer, who offers a variety of 42 models to its customers. These 42 models are grouped under two main categories with respect to their manufacturing requirements, i.e. basic and pro series. For the June 2022-May 2023 period, the company wishes to develop an aggregate production plan.

The monthly demand forecast for different caravan series for the planning period is given below.

BasicPro
Jun.222814
Jul.222010
Aug.22264
Sep.22244
Oct.22186
Nov.22108
Dec.222210
Jan.232012
Feb.23186
Mar.23328
Apr.233414
May.23366

Cost of producing a basic series caravan is estimated to be $6250, excluding cost of direct labour. This figure is $9750 for pro series caravans. Considering the direct labour requirements, a basic series product demands 380 man.hours for production, whereas a pro series caravan requires 530 man.hours. Holding cost of a basic caravan is estimated to be $250 per caravan per month, whereas it costs $500 to hold one unit of pro caravan in stock for a month. At the end of May 2022, the company projects to have 8 units of basic model, and 3 units of pro model caravans in it stocks.

Currently the company employs 86 assembly workers, who work 180 hours per month on average. Average monthly salary of an assembly worker is $420. Workers can be asked to work overtime, which is limited by 40 hours per month. The hourly fee for overtime is 50% more than the regular hourly fee.

Considering the administrative costs and inefficiencies during the orientation period, cost of employing a new worker is estimated to be $800 per worker. During lay-offs, the company pays $1400 per worker.

Base Problem

Formulate and solve the aggregate production planning problem. To develop the aggregate plan, you will need to construct and solve a linear programming problem minimizing the overall cost comprised of production, holding inventory and workforce related (regular time and overtime) costs. Shortages are not allowed. Draw a bar chart of monthly production and inventory for the board meeting. Comment on your plan including the total production, inventory, workforce used, and related costs.

In your solution to the base problem, investigate the inventory projections for both models. When is it optimal to carry base version in the inventory, and when is it optimal to carry the pro version in the inventory? When do you keep both models in the inventory? Explain.

Solution (Base Problem)

1
2
3
4
5
6
# Import libraries

from    pulp    import *                # Will be used as the solver
import  numpy               as np                   
import  matplotlib.pyplot   as plt      # Will be used to generate plots                   

1
2
3
4
5
# Definition of the problem

model = LpProblem(  name    = "aggregate-planning", 
                    sense   = LpMinimize            )
                    
1
2
3
4
5
6
# Definition of the sets

T       = range(1,13)                   # Set of months:                    T = {1, 2, 3, 4 ... 12}
T_ext   = range(13)                     # Extended time horizon             T = 0 included
M       = ("b", "p")                    # Set of aggregated product groups: basic and pro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Definition of the parameters

D = (   (28, 14), 
        (20, 10),
        (26, 4),
        (24, 4),
        (18, 6),
        (10, 8),
        (22, 10),
        (20, 12),
        (18, 6),
        (32, 8),
        (34, 14),
        (36, 6)     )           # Monthly demands

wInit   = 86                    # Initial # of workers
h       = 800                   # Hiring cost [$ / worker]
f       = 1400                  # Firing cost [$ / worker]

c       = (250, 500)            # Inventory costs [$ / month]
iInit   = (8, 3)                # Initial inventory

pCost   = (6250, 9750)          # Production costs [$ / unit]

pWf     = (380, 530)            # Production workforce requirements [man*hours / unit]
p       = 180                   # Default productivity of a worker [hours / month]

r       = 420                   # Regular time employee cost [$ / month]
o       = (420/180)*1.5*40      # Overtime employee cost (regular hourly wage * 1.5 * hours) [$ / month]

oLim    = 40                    # Overtime working limit [hours / month]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Definition of the decision variables

X = {   (i, t): LpVariable( name="X-{model}-{month}".format(    model   = M[i],                                     # Amount of produced models per month
                                                                month   = t     if t>=10 
                                                                                else "0{}".format(t)),              # Standardizing the time indices with two digits
                            lowBound=0                                                                 )
                                                                                for i in range(len(M)) for t in T       }


I = {   (i, t): LpVariable( name="I-{model}-{month}".format(    model   = M[i],                                     # Inventory at the end of the month t
                                                                month   =   t   if t>=10 
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for i in range(len(M)) for t in T_ext   }


W = {   (t): LpVariable(    name="W-{}"             .format(                t   if t>=10                            # # of workers      for     t = 0...12
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T_ext                          }


H = {   (t): LpVariable(    name="H-{}"             .format(                t   if t>=10                            # Hired workers     for     t = 1...12
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T                              }


F = {   (t): LpVariable(    name="F-{}"             .format(                t   if t>=10                            # Fired workers     for     t = 1...12
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T                              }


O = {   (t): LpVariable(    name="O-{}"             .format(                t   if t>=10                            # Overtime amount   for     t = 1...12
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T                              }


1
2
3
4
5
6
7
8
9
10
11
12
# Statement of the objective function: The total annual costs must be minimized.

model += lpSum(
                [   pCost[i]*X[(i,t)]   +                                       # Production costs,       
                    c[i]*I[(i,t)]                                               # and inventory holding costs
                                            for t in T                          # are summed over each month,
                                            for i in range(len(M)   )  ] +      # and each product group.
                [   r*W[t] + o*O[t]     +                                       # Wages,
                    h*H[t] + f*F[t]                                             # and hiring/firing costs
                                            for t in T                 ]        # are summed over each month.                            
                                                                            )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Definition of the constraints


model += (W[0]      ==  wInit                                                                   # Setting the initial workforce                                   
                                                                                    )


for i in range(len(M)):                                                                         # Setting the initial inventory levels
    model += (I[(i, 0)] ==  iInit[i]
                                                                                    )


for i in range(len(M)):                                                                         # Inventory balance / demand satisfaction
    for t in T:
        model += (  I[(i,t-1)] + X[(i,t)] == I[(i,t)] + D[t-1][i]
                                                                                    )


for t in T:                                                                                     # Workforce balance
    model += (  W[t] == W[t-1] + H[t] - F[t]
                                                                                    )


for t in T:                                                                                     # # of overtime workers cannot exceed the total workforce
    model += (  O[t] <= W[t]
                                                                                    )            


for t in T:                                                                                     # Capacity constraint
    model += (  lpSum(X[(i,t)]*pWf[i] for i in range(len(M))) <= p*W[t] + oLim*O[t]                              
                                                                                    )


1
2
model.solve()
LpStatus[model.status]
1
'Optimal'
1
print("z* = ", value(model.objective))
1
z* =  3143976.3829999994
1
2
3
4
5
6
7
production  = []
inventory   = []

for i in range(len(M)):
    production. append([v.varValue for v in model.variables() if ("X" in v.name) & (M[i] in v.name)])
    inventory.  append([v.varValue for v in model.variables() if ("I" in v.name) & (M[i] in v.name)])
            
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Plotting the monthly production plan

months = ['Jun22', 'Jul22', 'Aug22', 'Sep22', 'Oct22', 'Nov22',
          'Dec22', 'Jan23', 'Feb23', 'Mar23', 'Apr23', 'May23']

X_axis = np.arange(len(T))          

for i in range(len(production)):
        plt.bar(X_axis - 0.2 + i*0.4, production[i], 0.4, label = "{} Series".format(M[i]))

plt.xticks(X_axis, months, rotation = 90)

plt.xlabel(     "Time Horizon")
plt.ylabel(     "Number of Units Planned to Produce")
plt.title(      "Monthly Production Rates")

plt.legend()
plt.show()


png

Figure 1. Optimal Monthly Production Plan for the Base Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Plotting the monthly inventory levels

X_axis = np.arange(len(T))          

for i in range(len(inventory)):
        plt.bar(X_axis - 0.2 + i*0.4, inventory[i][1:], 0.4, label = "{} Series".format(M[i]))

plt.xticks(X_axis, months, rotation = 90)

plt.xlabel(     "Time Horizon")
plt.ylabel(     "Inventory Level")
plt.title(      "Monthly Inventory Levels")

plt.legend()
plt.show()


png

Figure 2. Optimal Inventory Levels for the Base Model

Extension 1

During the months of December and January you have the option increasing your regular man-hour capacity by bringing temporary skilled workers from another plant. Therefore, there is no hiring cost. Including the relocation cost, the total cost of extra labour force will be $15/hour.

Solution (Extension 1)

1
2
from    pulp    import *         

1
2
3
4
5
# Problem

model = LpProblem(  name    = "aggregate-planning", 
                    sense   = LpMinimize            )
                    
1
2
3
4
5
6
# Sets

T       = range(1,13)                   
T_ext   = range(13)                    
M       = ("b", "p")                    

1
2
3
4
# New set: index of the months during which extra workforce can be brought

EM = [7, 8]     # December and January

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Parameters

D = (   (28, 14), 
        (20, 10),
        (26, 4),
        (24, 4),
        (18, 6),
        (10, 8),
        (22, 10),
        (20, 12),
        (18, 6),
        (32, 8),
        (34, 14),
        (36, 6)     )           

wInit   = 86                    
h       = 800                   
f       = 1400                  

c       = (250, 500)            
iInit   = (8, 3)                

pCost   = (6250, 9750)          

pWf     = (380, 530)           
p       = 180                  

r       = 420                  
o       = (420/180)*1.5*40     

oLim    = 40
                    
1
2
3
4
# New parameter: cost of bringing temporary workers

e = 15 # [$ / hours]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Decision variables

X = {   (i, t): LpVariable( name="X-{model}-{month}".format(    model   = M[i],                                     
                                                                month   = t     if t>=10 
                                                                                else "0{}".format(t)),              
                            lowBound=0                                                                 )
                                                                                for i in range(len(M)) for t in T       }


I = {   (i, t): LpVariable( name="I-{model}-{month}".format(    model   = M[i],                                     
                                                                month   =   t   if t>=10 
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for i in range(len(M)) for t in T_ext   }


W = {   (t): LpVariable(    name="W-{}"             .format(                t   if t>=10                            
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T_ext                          }


H = {   (t): LpVariable(    name="H-{}"             .format(                t   if t>=10                            
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T                              }


F = {   (t): LpVariable(    name="F-{}"             .format(                t   if t>=10                            
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T                              }


O = {   (t): LpVariable(    name="O-{}"             .format(                t   if t>=10                            
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T                              }

1
2
3
4
5
6
7
# New decision variable: # of extra working hours supplied by temporary workers

E = {   (t): LpVariable(    name="E-{}"             .format(                t   if t>=10                            
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in EM                          }
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Modified objective function

model += lpSum(
                [   pCost[i]*X[(i,t)]   +                                       
                    c[i]*I[(i,t)]                                               
                                            for t in T                          
                                            for i in range(len(M)   )   ] +      
                [   r*W[t] + o*O[t]     +                                       
                    h*H[t] + f*F[t]                                                  
                                            for t in T                  ] +
                [   e*E[t]                                                
                                            for t in EM                 ]           # Extra workforce costs                           
                                                                            )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Constraints


model += (W[0]      ==  wInit                                                                                                    
                                                                                )


for i in range(len(M)):                                                                         
    model += (I[(i, 0)] ==  iInit[i]
                                                                                )


for i in range(len(M)):                                                                         
    for t in T:
        model += (  I[(i,t-1)] + X[(i,t)] == I[(i,t)] + D[t-1][i]
                                                                                )


for t in T:                                                                                    
    model += (  W[t] == W[t-1] + H[t] - F[t]
                                                                                )


for t in T:                                                                                     
    model += (  O[t] <= W[t]
                                                                                )            

1
2
3
4
5
6
7
8
9
10
# Modified capacity constraint

for t in T:                                                                                     
    if t in EM:
        model += (  lpSum(X[(i,t)]*pWf[i] for i in range(len(M))) <= p*W[t] + oLim*O[t] + E[t]                             
                                                                                                )
    else:
        model += (  lpSum(X[(i,t)]*pWf[i] for i in range(len(M))) <= p*W[t] + oLim*O[t]     
                                                                                                )
                                                                                                
1
2
model.solve()
LpStatus[model.status]
1
'Optimal'
1
print("z* = ", value(model.objective))
1
z* =  3143976.3829999994

Extension 2

The gross space requirements of a basic model needs 40 sq meters for storage, whereas a pro model needs 60 sq meters in the finished goods park of the company. The company has total parking area of 500 sq meters. Considering this space constraint would you revise your aggregate plan? The company also has the option of renting extra parking space for a fee of $1 per sq meter per month. Would you consider making a rental agreement, and if so for which months?

Production Engineering and Work Study Department warns you that the standard hours are measured with an error of 10% (i.e., all labor requirement values can change by 10%). Assuming that the storage constraint is active, how sensitive is your optimal plan for scenario to changes in the estimation of labor requirements? Interpret your findings.

Solution (Extension 2)

1
2
3
4
5
# Import libraries

from    pulp    import *  
import  pandas  as pd        # Will be used to generate and export tables                        

1
2
3
4
5
# Problem

model = LpProblem(  name    = "aggregate-planning", 
                    sense   = LpMinimize            )
                    
1
2
3
4
5
6
# Sets

T       = range(1,13)                   
T_ext   = range(13)                     
M       = ("b", "p")                    

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Parameters

D = (   (28, 14), 
        (20, 10),
        (26, 4),
        (24, 4),
        (18, 6),
        (10, 8),
        (22, 10),
        (20, 12),
        (18, 6),
        (32, 8),
        (34, 14),
        (36, 6)     )           

wInit   = 86                    
h       = 800                   
f       = 1400                  

c       = (250, 500)            
iInit   = (8, 3)                

pCost   = (6250, 9750)          

pWf     = (380, 530)            
p       = 180                   

r       = 420                   
o       = (420/180)*1.5*40      

oLim    = 40                    

1
2
3
4
5
# New parameters

e       = 1             # Cost of renting extra parking space       [$ per sq meter]
gp      = 500           # Total capacity of company's goods park    [sq meters]   
sr      = (40, 60)      # Space requirements                        [sq meters]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Decision variables

X = {   (i, t): LpVariable( name="X-{model}-{month}".format(    model   = M[i],                                     
                                                                month   = t     if t>=10 
                                                                                else "0{}".format(t)),              
                            lowBound=0                                                                 )
                                                                                for i in range(len(M)) for t in T       }


I = {   (i, t): LpVariable( name="I-{model}-{month}".format(    model   = M[i],                                     
                                                                month   =   t   if t>=10 
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for i in range(len(M)) for t in T_ext   }


W = {   (t): LpVariable(    name="W-{}"             .format(                t   if t>=10                            
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T_ext                          }


H = {   (t): LpVariable(    name="H-{}"             .format(                t   if t>=10                            
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T                              }


F = {   (t): LpVariable(    name="F-{}"             .format(                t   if t>=10                           
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T                              }


O = {   (t): LpVariable(    name="O-{}"             .format(                t   if t>=10                            
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T                              }


1
2
3
4
5
6
# New decision variable: amount of extra space rented

E = {   (t): LpVariable(    name="E-{}"             .format(                t   if t>=10                            
                                                                                else "0{}".format(t)),
                            lowBound=0                                                               )              
                                                                                for t in T                              }
1
2
3
4
5
6
7
8
9
10
11
12
# Modified objective function

model += lpSum(
                [   pCost[i]*X[(i,t)]   +                                    
                    c[i]*I[(i,t)]                                            
                                            for t in T                       
                                            for i in range(len(M)   )   ] +  
                [   r*W[t] + o*O[t]     +                                    
                    h*H[t] + f*F[t]     +
                    e*E[t]                                          
                                            for t in T                  ]    
                                                                            )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Constraints


model += (W[0]      ==  wInit                                                                               
                                                                                    )


for i in range(len(M)):                                                                         
    model += (I[(i, 0)] ==  iInit[i]
                                                                                    )


for i in range(len(M)):                                                                         
    for t in T:
        model += (  I[(i,t-1)] + X[(i,t)] == I[(i,t)] + D[t-1][i]
                                                                                    )


for t in T:                                                                                     
    model += (  W[t] == W[t-1] + H[t] - F[t]
                                                                                    )


for t in T:                                                                                     
    model += (  O[t] <= W[t]
                                                                                    )           


for t in T:                                                                                     
    model += (  lpSum(X[(i,t)]*pWf[i] for i in range(len(M))) <= p*W[t] + oLim*O[t]             
                                                                                    )

1
2
3
4
5
# New constraint: Goods park capacity

for t in T:                                                                                     
    model += (  lpSum(X[(i,t)]*sr[i]  for i in range(len(M))) <= gp     + E[t]
                                                                                    )
1
2
model.solve()
LpStatus[model.status]
1
'Optimal'
1
print("z* = ", value(model.objective))
1
z* =  3155116.3799999994
1
2
3
4
o = [{'name':name, 'shadow price':c.pi, 'slack': c.slack} 
     for name, c in model.constraints.items()]
print(pd.DataFrame(o))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    name shadow price slack
0    _C1         None  None
1    _C2         None  None
2    _C3         None  None
3    _C4         None  None
4    _C5         None  None
..   ...          ...   ...
70  _C71         None  None
71  _C72         None  None
72  _C73         None  None
73  _C74         None  None
74  _C75         None  None

[75 rows x 3 columns]

IE 313 - Supply Chain Management
Boğaziçi University - Industrial Engineering Department
GitHub Repository

This post is licensed under CC BY 4.0 by the author.