I am working on a transportation/replenishment model wherein I need to solve for lowest cost. The variables are:
I am not very experienced with Python. It seems that I am somewhat close, however, I have a problem I haven't been able to fix yet: if Inventory is too low to fulfill all Demand, the model will break and return an "infeasible" result. Instead of this, I want the model to satisfy Demand until Inventory reaches zero and then return the optimized results up to that point. I understand that the result I am getting now is because I have set fulfilled qty equal to demand in one of my constraints, but I am not sure how to modify/fix it.
Here is the code so far - this is a result of much Google searching and sort of combining bits and pieces of code together like Dr. Frankenstein - if anything in here looks stupid please let me know. With the current inputs this will not work since Inventory does not satisfy Demand, but it seems to work if Inventory is higher (eg change Store1-SKU_B demand from 250 to 50)
from pulp import *
import pandas as pd
# Creates a list of all the supply nodes
warehouses = ["WHS_1","WHS_2","WHS_3"]
# Creates a dictionary for Inventory by Node-SKU
inventory = {"WHS_1": {"SKU_A":50,"SKU_B":100},
"WHS_2": {"SKU_A":50,"SKU_B":75} ,
"WHS_3": {"SKU_A":150,"SKU_B":25} ,
}
# Store list
stores = ["Store1","Store2"]
# SKU list
items = ["SKU_A","SKU_B"]
# Creates a dictionary for the number of units of demand for each Store-SKU
demand = {
"Store1": {"SKU_A":100,"SKU_B":250},
"Store2": {"SKU_A":100,"SKU_B":50},
}
# Creates a dictionary for the lane cost for each Node-Store-SKU
costs = {
"WHS_1": {"Store1": {"SKU_A":10.50,"SKU_B":3.75},
"Store2": {"SKU_A":15.01,"SKU_B":5.15}},
"WHS_2": {"Store1": {"SKU_A":9.69,"SKU_B":3.45},
"Store2": {"SKU_A":17.50,"SKU_B":6.06}},
"WHS_3": {"Store1": {"SKU_A":12.12,"SKU_B":5.15},
"Store2": {"SKU_A":16.16,"SKU_B":7.07}},
}
# Creates the 'prob' variable to contain the problem data
prob = LpProblem("StoreAllocation", LpMinimize)
# Creates a list of tuples containing all the possible routes for transport
routes = [(w, s, i) for w in warehouses for s in stores for i in items]
# A dictionary called 'Vars' is created to contain the referenced variables(the routes)
vars = LpVariable.dicts("Route", (warehouses, stores, items), 0, None, LpInteger)
# The objective function is added to 'prob' first
prob += (
lpSum([vars[w][s][i] * costs[w][s][i] for (w, s, i) in routes]),
"Sum_of_Transporting_Costs",
)
# Supply constraint, must not exceed Node Inventory
for w in warehouses:
for i in items:
prob += (
lpSum([vars[w][s][i] for s in stores]) <= inventory[w][i],
f"Sum_of_Products_out_of_Warehouse_{w}{i}",
)
# Supply constraint, supply to equal demand
for s in stores:
for i in items:
prob += (
lpSum([vars[w][s][i] for w in warehouses]) == demand[s][i],
f"Sum_of_Products_into_Store{s}{i}",
)
# The problem data is written to an .lp file
prob.writeLP("TestProblem.lp")
prob.solve()
# The status of the solution is printed to the screen
print("Status:", LpStatus[prob.status])
# Each of the variables is printed with it's resolved optimum value
for v in prob.variables():
print(v.name, "=", v.varValue)
# The optimised objective function value is printed to the screen
print("Total Cost of Fulfillment = ", value(prob.objective))
This is good. Your model is set up well. Let's talk about supply...
So this is a common transshipment model and you want to minimize cost, but the default answer is to ship nothing for a cost of zero, which is not good. As you know, you need upward pressure on the deliveries to meet demand, or at least do the best you can with the inventory on hand if demand > inventory.
The first "cheap and easy" thing to do is to reduce the aggregate deliveries of each product to what is available... across all stores. In your current code you are trying to force the deliveries == demand, which may not be possible. So you can take a step back and just say "deliver the aggregate demand, or at least all of the inventory". In pseudocode that would be something like:
total_delivery[sku] = min(all inventory, demand)
You could do the same for the other SKU's and then just sum all of the deliveries by SKU across all warehouses and destintations and force:
for SKU in SKUs:
sum(deliver[w, s, sku] for w in warehouses for s in stores) >= total_delivery[sku]
Realize that the parameter total_delivery
is NOT a variable, it is discernible from the data before doing anything.
The above will make the model run, but there are issues. The model will likely "overdeliver" to some sites because we are aggregating the demand. So if you have 100 of something and split demand of 50/50 in 2 sites, it will deliver 100 to the cheapest site.... not good. So you need to add a constraint to limit the delivery to each site to the demand, regardless of source. Something like:
for s in stores:
for sku in skus:
sum(deliver[w, s, sku] for w in warehouses) <= demand[s, sku]
The addition of those should make your model run. The result (if inventory is short) will be disproportionate delivery to the cheap sites. Perhaps that is OK. Balancing it is a little complicated.
...
Regarding your model, you have your variable constructed as a nested list... That is why you need to index it as vars[w][s][i]
. That is perfectly fine, but I find it much easier to tuple-index the variables and you already have the basis set routes
to work with. So I would:
deliver = LpVariable.dicts("deliver", routes, 0, None, LpInteger)
then you can index it like I have in the examples above...
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.