from pulp import LpMaximize, LpMinimize, LpProblem, LpStatus, lpSum, LpVariable
import pandas as pd
import numpy as np
The Molokai Nut Company (MNC) makes four different products from macadamia nuts grown in the Hawaiian Islands: chocolate-coated whole nuts (Whole), chocolate-coated nut clusters (Cluster), chocolate-coated nut crunch bars (Crunch) and plain roasted nuts (Roasted).
To meet marketing demands for the coming week, MNC needs to produce at least 1,000 pounds of the Whole product, between 400 and 500 pounds of the Cluster product, no more than 150 pounds of the Crunch product and no more than 200 pounds of the Roasted product.
Each pound of the Whole, Cluster, Crunch and Roasted product contains, respectively, 60%, 40%, 20% and 100% macadamia nuts with the remaining weight made up of chocolate coating. The company has 1100 pounds of nuts and 800 pounds of chocolate available for use in the next week.
The various products are made using four different machines that hull the nuts, roast the nuts, coat the nuts in chocolate, and package the products. Table 1 summarizes the time required in minutes for each product on each machine. Each machine has 60 hours of production time available in the coming week.
Table 1: Machine Minutes Required per Pound
Machine | Whole | Cluster | Crunch | Roasted |
---|---|---|---|---|
Hulling | 1.00 | 1.00 | 1.00 | 1.00 |
Roasting | 2.00 | 1.50 | 1.00 | 4.00 |
Coating | 1.00 | 0.70 | 0.20 | 0 |
Packaging | 2.50 | 1.60 | 1.25 | 1.00 |
The company's controller recently presented management with a financial summary of MNC's average weekly operations over the past quarter. The Variable Margin per pound for each of the products is shown below.
Product | VM/lb |
---|---|
Whole | \$1.93 |
Cluster | \$1.04 |
Crunch | \$1.15 |
Roasted | \$1.33 |
A. Build a linear programming model to identify the production levels for MNC's four products that maximize total VM.
B. If MNC wanted to decrease the production of any product, which product would you recommend and why?
C. Which machine capacities would you recommend the company look into expanding? If they can only expand one machine capacity, which machine should they target?
data = {'Whole':{'Hulling': 1.0, 'Roasting': 2.00, 'Coating': 1.00, 'Packaging': 2.50, 'nuts': 0.6, 'chocolate': 0.4, 'price': 1.93},
'Cluster':{'Hulling': 1.0, 'Roasting': 1.50, 'Coating': 0.70, 'Packaging': 1.60, 'nuts': 0.4, 'chocolate': 0.6,'price': 1.04},
'Crunch':{'Hulling': 1.0, 'Roasting': 1.00, 'Coating': 0.20, 'Packaging': 1.25, 'nuts': 0.2, 'chocolate': 0.8, 'price': 1.15},
'Roasted':{'Hulling': 1.0, 'Roasting': 4.00, 'Coating': 0.00, 'Packaging': 1.00, 'nuts': 1, 'chocolate': 0, 'price': 1.33}}
df = pd.DataFrame(data).T
df
Hulling | Roasting | Coating | Packaging | nuts | chocolate | price | |
---|---|---|---|---|---|---|---|
Whole | 1.0 | 2.0 | 1.0 | 2.50 | 0.6 | 0.4 | 1.93 |
Cluster | 1.0 | 1.5 | 0.7 | 1.60 | 0.4 | 0.6 | 1.04 |
Crunch | 1.0 | 1.0 | 0.2 | 1.25 | 0.2 | 0.8 | 1.15 |
Roasted | 1.0 | 4.0 | 0.0 | 1.00 | 1.0 | 0.0 | 1.33 |
model = LpProblem("ProductDecision", LpMaximize)
Number = LpVariable.dicts("productNum_", df.index , lowBound = 0)
Number
{'Whole': productNum__Whole, 'Cluster': productNum__Cluster, 'Crunch': productNum__Crunch, 'Roasted': productNum__Roasted}
model += lpSum(Number[p] * df.loc[p,'price'] for p in df.index)
model
ProductDecision: MAXIMIZE 1.04*productNum__Cluster + 1.15*productNum__Crunch + 1.33*productNum__Roasted + 1.93*productNum__Whole + 0.0 VARIABLES productNum__Cluster Continuous productNum__Crunch Continuous productNum__Roasted Continuous productNum__Whole Continuous
model += Number['Whole'] >= 1000
model += Number['Cluster'] >= 400
model += Number['Cluster'] <= 500
model += Number['Crunch'] <= 150
model += Number['Roasted'] <= 200
model
ProductDecision: MAXIMIZE 1.04*productNum__Cluster + 1.15*productNum__Crunch + 1.33*productNum__Roasted + 1.93*productNum__Whole + 0.0 SUBJECT TO _C1: productNum__Whole >= 1000 _C2: productNum__Cluster >= 400 _C3: productNum__Cluster <= 500 _C4: productNum__Crunch <= 150 _C5: productNum__Roasted <= 200 VARIABLES productNum__Cluster Continuous productNum__Crunch Continuous productNum__Roasted Continuous productNum__Whole Continuous
machine = ['Hulling', 'Roasting', 'Coating', 'Packaging']
for m in machine:
model += lpSum(Number[p] * df.loc[p,m] for p in df.index) <= 3600
model
ProductDecision: MAXIMIZE 1.04*productNum__Cluster + 1.15*productNum__Crunch + 1.33*productNum__Roasted + 1.93*productNum__Whole + 0.0 SUBJECT TO _C1: productNum__Whole >= 1000 _C2: productNum__Cluster >= 400 _C3: productNum__Cluster <= 500 _C4: productNum__Crunch <= 150 _C5: productNum__Roasted <= 200 _C6: productNum__Cluster + productNum__Crunch + productNum__Roasted + productNum__Whole <= 3600 _C7: 1.5 productNum__Cluster + productNum__Crunch + 4 productNum__Roasted + 2 productNum__Whole <= 3600 _C8: 0.7 productNum__Cluster + 0.2 productNum__Crunch + productNum__Whole <= 3600 _C9: 1.6 productNum__Cluster + 1.25 productNum__Crunch + productNum__Roasted + 2.5 productNum__Whole <= 3600 VARIABLES productNum__Cluster Continuous productNum__Crunch Continuous productNum__Roasted Continuous productNum__Whole Continuous
model += lpSum(Number[p] * df.loc[p,'nuts'] for p in df.index) <= 1100
model += lpSum(Number[p] * df.loc[p,'chocolate'] for p in df.index) <= 800
model
ProductDecision: MAXIMIZE 1.04*productNum__Cluster + 1.15*productNum__Crunch + 1.33*productNum__Roasted + 1.93*productNum__Whole + 0.0 SUBJECT TO _C1: productNum__Whole >= 1000 _C2: productNum__Cluster >= 400 _C3: productNum__Cluster <= 500 _C4: productNum__Crunch <= 150 _C5: productNum__Roasted <= 200 _C6: productNum__Cluster + productNum__Crunch + productNum__Roasted + productNum__Whole <= 3600 _C7: 1.5 productNum__Cluster + productNum__Crunch + 4 productNum__Roasted + 2 productNum__Whole <= 3600 _C8: 0.7 productNum__Cluster + 0.2 productNum__Crunch + productNum__Whole <= 3600 _C9: 1.6 productNum__Cluster + 1.25 productNum__Crunch + productNum__Roasted + 2.5 productNum__Whole <= 3600 _C10: 0.4 productNum__Cluster + 0.2 productNum__Crunch + productNum__Roasted + 0.6 productNum__Whole <= 1100 _C11: 0.6 productNum__Cluster + 0.8 productNum__Crunch + 0.4 productNum__Whole <= 800 VARIABLES productNum__Cluster Continuous productNum__Crunch Continuous productNum__Roasted Continuous productNum__Whole Continuous
model.solve()
1
LpStatus[model.status]
'Optimal'
model.objective.value()
2839.075
for v in model.variables(): print(f"{v.name}: {v.varValue}")
productNum__Cluster: 400.0 productNum__Crunch: 150.0 productNum__Roasted: 197.5 productNum__Whole: 1030.0
The maximum total variable margin is 2839.075.
model = LpProblem("ProductDecision", LpMaximize)
Number = LpVariable.dicts("productNum_", df.index , lowBound = 0)
model += lpSum(Number[p] * df.loc[p,'price'] for p in df.index)
model += Number['Whole'] >= 900
model += Number['Cluster'] >= 400
model += Number['Cluster'] <= 500
model += Number['Crunch'] <= 150
model += Number['Roasted'] <= 200
machine = ['Hulling', 'Roasting', 'Coating', 'Packaging']
for m in machine:
model += lpSum(Number[p] * df.loc[p,m] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'nuts'] for p in df.index) <= 1100
model += lpSum(Number[p] * df.loc[p,'chocolate'] for p in df.index) <= 800
print(model.solve())
print(LpStatus[model.status])
print(model.objective.value())
for v in model.variables(): print(f"{v.name}: {v.varValue}")
1 Optimal 2839.075 productNum__Cluster: 400.0 productNum__Crunch: 150.0 productNum__Roasted: 197.5 productNum__Whole: 1030.0
model = LpProblem("ProductDecision", LpMaximize)
Number = LpVariable.dicts("productNum_", df.index , lowBound = 0)
model += lpSum(Number[p] * df.loc[p,'price'] for p in df.index)
model += Number['Whole'] >= 1000
model += Number['Cluster'] >= 300
model += Number['Cluster'] <= 400
model += Number['Crunch'] <= 150
model += Number['Roasted'] <= 200
machine = ['Hulling', 'Roasting', 'Coating', 'Packaging']
for m in machine:
model += lpSum(Number[p] * df.loc[p,m] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'nuts'] for p in df.index) <= 1100
model += lpSum(Number[p] * df.loc[p,'chocolate'] for p in df.index) <= 800
print(model.solve())
print(LpStatus[model.status])
print(model.objective.value())
for v in model.variables(): print(f"{v.name}: {v.varValue}")
1 Optimal 2859.99 productNum__Cluster: 300.0 productNum__Crunch: 150.0 productNum__Roasted: 200.0 productNum__Whole: 1093.0
model = LpProblem("ProductDecision", LpMaximize)
Number = LpVariable.dicts("productNum_", df.index , lowBound = 0)
model += lpSum(Number[p] * df.loc[p,'price'] for p in df.index)
model += Number['Whole'] >= 1000
model += Number['Cluster'] >= 400
model += Number['Cluster'] <= 500
model += Number['Crunch'] <= 50
model += Number['Roasted'] <= 200
machine = ['Hulling', 'Roasting', 'Coating', 'Packaging']
for m in machine:
model += lpSum(Number[p] * df.loc[p,m] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'nuts'] for p in df.index) <= 1100
model += lpSum(Number[p] * df.loc[p,'chocolate'] for p in df.index) <= 800
print(model.solve())
print(LpStatus[model.status])
print(model.objective.value())
for v in model.variables(): print(f"{v.name}: {v.varValue}")
1 Optimal 2820.5750000000003 productNum__Cluster: 400.0 productNum__Crunch: 50.0 productNum__Roasted: 197.5 productNum__Whole: 1080.0
model = LpProblem("ProductDecision", LpMaximize)
Number = LpVariable.dicts("productNum_", df.index , lowBound = 0)
model += lpSum(Number[p] * df.loc[p,'price'] for p in df.index)
model += Number['Whole'] >= 1000
model += Number['Cluster'] >= 400
model += Number['Cluster'] <= 500
model += Number['Crunch'] <= 150
model += Number['Roasted'] <= 100
machine = ['Hulling', 'Roasting', 'Coating', 'Packaging']
for m in machine:
model += lpSum(Number[p] * df.loc[p,m] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'nuts'] for p in df.index) <= 1100
model += lpSum(Number[p] * df.loc[p,'chocolate'] for p in df.index) <= 800
print(model.solve())
print(LpStatus[model.status])
print(model.objective.value())
for v in model.variables(): print(f"{v.name}: {v.varValue}")
1 Optimal 2784.67 productNum__Cluster: 400.0 productNum__Crunch: 150.0 productNum__Roasted: 100.0 productNum__Whole: 1069.0
model = LpProblem("ProductDecision", LpMaximize)
Number = LpVariable.dicts("productNum_", df.index , lowBound = 0)
model += lpSum(Number[p] * df.loc[p,'price'] for p in df.index)
model += Number['Whole'] >= 1000
model += Number['Cluster'] >= 400
model += Number['Cluster'] <= 500
model += Number['Crunch'] <= 150
model += Number['Roasted'] <= 200
machine = ['Hulling', 'Roasting', 'Coating', 'Packaging']
for m in machine:
model += lpSum(Number[p] * df.loc[p,m] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'nuts'] for p in df.index) <= 1100
model += lpSum(Number[p] * df.loc[p,'chocolate'] for p in df.index) <= 800
print(model.solve())
print(LpStatus[model.status])
print(model.objective.value())
for name, constraint in model.constraints.items(): print(f"{name}: {(constraint.value() - constraint.constant)}")
1 Optimal 2839.075 _C1: 1030.0 _C2: 400.0 _C3: 400.0 _C4: 150.0 _C5: 197.5 _C6: 1777.5 _C7: 3600.0 _C8: 1340.0 _C9: 3600.0 _C10: 1005.5 _C11: 772.0
From the model in question a, we find that when roasting and packaging machines reach full capacity, hulling and coating machine still remain a lots working hours. So, we want recommend the compant to expand roasting and packaging machines.
model = LpProblem("ProductDecision", LpMaximize)
Number = LpVariable.dicts("productNum_", df.index , lowBound = 0)
model += lpSum(Number[p] * df.loc[p,'price'] for p in df.index)
model += Number['Whole'] >= 1000
model += Number['Cluster'] >= 400
model += Number['Cluster'] <= 500
model += Number['Crunch'] <= 150
model += Number['Roasted'] <= 200
machine = ['Hulling', 'Roasting', 'Coating', 'Packaging']
model += lpSum(Number[p] * df.loc[p,'Hulling'] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'Roasting'] for p in df.index) <= 4000
model += lpSum(Number[p] * df.loc[p,'Coating'] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'Packaging'] for p in df.index) <= 4000
model += lpSum(Number[p] * df.loc[p,'nuts'] for p in df.index) <= 1100
model += lpSum(Number[p] * df.loc[p,'chocolate'] for p in df.index) <= 800
print(model.solve())
print(LpStatus[model.status])
print(model.objective.value())
for name, constraint in model.constraints.items(): print(f"{name}: {(constraint.value() - constraint.constant)}")
1 Optimal 3130.5635519499997 _C1: 1225.4545 _C2: 400.0 _C3: 400.0 _C4: 87.272727 _C5: 187.27273 _C6: 1899.9999569999998 _C7: 3887.2726470000002 _C8: 1522.9090453999997 _C9: 3999.99988875 _C10: 1099.9999754 _C11: 799.9999816000001
Next, we need more tests in deciding which machine to expand.
model = LpProblem("ProductDecision", LpMaximize)
Number = LpVariable.dicts("productNum_", df.index , lowBound = 0)
model += lpSum(Number[p] * df.loc[p,'price'] for p in df.index)
model += Number['Whole'] >= 1000
model += Number['Cluster'] >= 400
model += Number['Cluster'] <= 500
model += Number['Crunch'] <= 150
model += Number['Roasted'] <= 200
machine = ['Hulling', 'Roasting', 'Coating', 'Packaging']
model += lpSum(Number[p] * df.loc[p,'Hulling'] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'Roasting'] for p in df.index) <= 4000
model += lpSum(Number[p] * df.loc[p,'Coating'] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'Packaging'] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'nuts'] for p in df.index) <= 1100
model += lpSum(Number[p] * df.loc[p,'chocolate'] for p in df.index) <= 800
print(model.solve())
print(LpStatus[model.status])
print(model.objective.value())
for name, constraint in model.constraints.items(): print(f"{name}: {(constraint.value() - constraint.constant)}")
1 Optimal 2840.4700000000003 _C1: 1029.0 _C2: 400.0 _C3: 400.0 _C4: 150.0 _C5: 200.0 _C6: 1779.0 _C7: 3608.0 _C8: 1339.0 _C9: 3600.0 _C10: 1007.4 _C11: 771.6
model = LpProblem("ProductDecision", LpMaximize)
Number = LpVariable.dicts("productNum_", df.index , lowBound = 0)
model += lpSum(Number[p] * df.loc[p,'price'] for p in df.index)
model += Number['Whole'] >= 1000
model += Number['Cluster'] >= 400
model += Number['Cluster'] <= 500
model += Number['Crunch'] <= 150
model += Number['Roasted'] <= 200
machine = ['Hulling', 'Roasting', 'Coating', 'Packaging']
model += lpSum(Number[p] * df.loc[p,'Hulling'] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'Roasting'] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'Coating'] for p in df.index) <= 3600
model += lpSum(Number[p] * df.loc[p,'Packaging'] for p in df.index) <= 4000
model += lpSum(Number[p] * df.loc[p,'nuts'] for p in df.index) <= 1100
model += lpSum(Number[p] * df.loc[p,'chocolate'] for p in df.index) <= 800
print(model.solve())
print(LpStatus[model.status])
print(model.objective.value())
for name, constraint in model.constraints.items(): print(f"{name}: {(constraint.value() - constraint.constant)}")
1 Optimal 3076.0416019500003 _C1: 1273.3333 _C2: 400.0 _C3: 400.0 _C4: 63.33333300000001 _C5: 97.5 _C6: 1834.1666330000003 _C7: 3599.999933 _C8: 1565.9999666000003 _C9: 3999.9999162500003 _C10: 1034.1666466 _C11: 799.9999864
Based on two tests above, we can find that expanding packaging machine can bring more variable margin. As a result, I recommend the company to target on the packaging machine.