Reputation: 21
I'd like to find the optimal solution for buying goods from suppliers where the shipping cost is dependent on the cost of goods bought from given supplier. I'm using Pyomo. My code so far is:
model = ConcreteModel(name="(MN_2)")
# products
N = ['prod1', 'prod2', 'prod3']
# suppliers
M = ['A', 'B']
# price
p = {('prod1', 'A'): 10,
('prod2', 'A'): 9,
('prod3', 'A'): 50,
('prod1', 'B'): 16,
('prod2', 'B'): 20,
('prod3', 'B'): 35}
# user quantity contraint
q_u = {('prod1', 'A'): 2,
('prod2', 'A'): 1,
('prod3', 'A'): 1,
('prod1', 'B'): 1,
('prod2', 'B'): 1,
('prod3', 'B'): 1}
# seller quantity contraint
q_s = {('prod1', 'A'): 20,
('prod2', 'A'): 10,
('prod3', 'A'): 10,
('prod1', 'B'): 10,
('prod2', 'B'): 10,
('prod3', 'B'): 10}
# quantity of product n bough in shop m
model.x = Var(N, M, bounds=(0,10))
def obj_rule(model):
return sum(p[n,m]*model.x[n,m] for n in N for m in M)
model.obj = Objective(rule=obj_rule)
def user_quantity(model, n, m):
return model.x[n,m] >= q_u[n,m]
model.user_quantity = Constraint(N, M, rule=user_quantity)
def seller_quantity(model, n, m):
return model.x[n,m] <= q_s[n,m]
model.seller_quantity = Constraint(N, M, rule=seller_quantity)
solver = SolverFactory('glpk')
solver.solve(model)
model.x.pprint()
What I'm struggling with is how to include the shipping cost that is dependent on the cost of goods bought from given supplier. For example:
For supplier A: shipping cost is =
For supplier B: shipping cost is =
Upvotes: 0
Views: 647
Reputation: 1336
The constraints you're describing are an implementation of if-then condition which is described here. The quirk is that your conditions require the binary variable to be 1 if your procurement costs are less than or equal to some threshold rather than strictly less than the threshold. We can add a very small number (0.0001) to the threshold that doesn't affect adherence to the condition and allow us to use the new value in a strictly less than inequality.
To your initial model, you can add one new binary variable per seller (model.shipping_bin
) and one constraint per binary variable that forces the binary variable to be 1 if the cost is less than the threshold and 0 otherwise. We can then multiply by the shipping cost in the objective function by these variables.
# add new binary variables to track costs per supplier
model.shipping_bin = Var(M,within = Binary)
shipping_costs = {'A':10,'B':8}
shipping_thresholds = {'A':100,'B':150} # threshold to meet to not incur shipping
# We need big M values to multiply the binaries to enforce the constraint without constraining the procurement cost incurred.
# We can set to the maximum amount we expect to procure from the seller
# The largest cost you could incur from each seller is
# the price times the max quantity
shipping_big_m = {seller: sum([p[(prod,seller)] * q_s[(prod,seller)] for prod in N])
for seller in M}
# add constraints
def shipping_bin_rule(model,seller):
# Sets shipping binary var to 1 if threshold not met
# Allows it to be 0 otherwise
# 790 * (model.shipping_bin['A']) >= 100.0001 + cost of products from seller 'A'
# if cost of products from 'A' < 100.0001 then binary variable = 1
# 710 * (model.shipping_bin['B']) >= 150.0001 + cost of products from seller 'B'
# if cost of products from 'B' < 150.0001 then binary variable = 1
epsilon = .0001 # to make sure case where cost == threshold is still accounted for
return(shipping_big_m[seller] * model.shipping_bin[seller] >= shipping_thresholds[seller] + epsilon - sum([p[(product,seller)] * model.x[product,seller]
for product in N]))
model.shipping_bin_con = Constraint(M,rule = shipping_bin_rule)
# new objective function adding the shipping cost
def obj_with_shipping_rule(model):
orig_cost = obj_rule(model) # call the original function, but can combine into one function if desired
# apply the shipping cost if cost of products is less than threshold (binary is 0)
shipping_cost = sum([shipping_costs[seller] * model.shipping_bin[seller]
for seller in M])
return(orig_cost + shipping_cost)
# deactivate the original objective to apply the new one
model.obj.deactivate()
model.obj_with_shipping = Objective(rule = obj_with_shipping_rule)
# solve the model with new obj
solver.solve(model)
model.x.pprint() # x values remain unchanged
# x : Size=6, Index=x_index
# Key : Lower : Value : Upper : Fixed : Stale : Domain
# ('prod1', 'A') : 0 : 2.0 : 10 : False : False : Reals
# ('prod1', 'B') : 0 : 1.0 : 10 : False : False : Reals
# ('prod2', 'A') : 0 : 1.0 : 10 : False : False : Reals
# ('prod2', 'B') : 0 : 1.0 : 10 : False : False : Reals
# ('prod3', 'A') : 0 : 1.0 : 10 : False : False : Reals
# ('prod3', 'B') : 0 : 1.0 : 10 : False : False : Reals
# cost from A = 2 * 10 + 1 * 9 + 1 * 50 = 79 < 100 so model.shipping_bin['A'] = 1
# cost from B = 1 * 16 + 1 * 20 + 1 * 35 = 71 < 150 so model.shipping_bin['B'] = 1
model.shipping_bin.pprint()
# shipping_bin : Size=2, Index=shipping_bin_index
# Key : Lower : Value : Upper : Fixed : Stale : Domain
# A : 0 : 1.0 : 1 : False : False : Binary
# B : 0 : 1.0 : 1 : False : False : Binary
value(model.obj_with_shipping) # 168 (18 units larger than original because of shipping)
Upvotes: 1