简体   繁体   中英

How to set up a linear programming model (transportation problem) using python/PuLp

I am working on a transportation/replenishment model wherein I need to solve for lowest cost. The variables are:

  • Warehouses - several possible origin points of a shipment.
  • Items - in this example I only use two items. Each Item-Store combination has a unique demand value.
  • Inventory - available inventory for each 'Item' in each 'Warehouse'
  • Stores - the destination point for each shipment. In this example I only use two Stores.
  • Costs - unique costs for each Warehouse-Item-Store combination, which will be used to solve for lowest cost.
  • Demand - the quantity of each 'Item' that each 'Store' wants to receive; the model should fulfill 100% unless inventory is not available.

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM