Adding a binary constraint in Pulp python without violating the linearity constraint

Viewed 2490

Dear fellow python users,

I am building a multi period multi product planning model using Pulp. What the model should do is rather simple: plan production against minimal holding and production costs while meeting demand.

I have the following data:

periods = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
products = ['A', 'B', 'C']

And I create the following variables:

prod_vars = pulp.LpVariable.dicts('production', [(i,j) for i in products for j in periods],0) 
inv_vars = pulp.LpVariable.dicts('inventory', [(i,j) for i in products for j in periods],0)

There are 2 constraints, 1 to always meet demand and 1 to stay below production capacity. Please note that there is a dataframe (input_data) that retrieves the value of the given demand for that period.

for i in products:
    for j in periods[1:]: 
        model.addConstraint(pulp.LpConstraint(
                    e=inv_vars[(i,j-1)] + prod_vars[(i,j)] - inv_vars[(i,j)],
                sense=pulp.LpConstraintEQ,
                name='inv_balance_'  + str(i)+ str(j),
                rhs=input_data[i][j-1]))

for j in periods:
model.addConstraint(pulp.LpConstraint(
    e=(pulp.lpSum(prod_vars[(i,j)] for i in products)),
    sense=pulp.LpConstraintLE,
    name='total_production_capacity'+str(j),
    rhs=(input_data['production_capacity'][j-1])

Then I add cost function and set the objective:

total_production_cost = production_cost*pulp.lpSum(prod_vars)
total_holding_cost =holding_cost * pulp.lpSum(inv_vars)

objective = total_holding_cost + total_production_cost + activation_cost model.setObjective(objective)

This model works all fine and gives me an output like this. prod_vars: (A,1) =5, (B,1)=10, (C,1)=15 and so on for all periods. However: I want to penalize the system for producing multiple products. I.e., adding fixed costs when choosing to produce a second or third product. It would then be more benefical to produce more of product A and hold inventory for some months then to produce A every month. I tried so by adding another variable:

use_vars = pulp.LpVariable.dicts('uselocation', [(i,j) for i in products for j in periods] , 0,1,pulp.LpBinary)

And add fixed costs for using the variable:

activation_cost = pulp.lpSum(activation_cost*use_vars[(i,j)] for i in products for j in periods)

I think that I would need to multiply al my prod_vars by my use_vars in the two constraints. However, if I would do this in my first inventory constraint, Pulp gives the error that my constraint is not linear anymore.

Does someone know how I can make this happen?? Thanks in advance :)

2 Answers

I want to penalize the system for producing multiple products. I.e., adding fixed costs when choosing to produce a second or third product.

It is best to step away from code and look at it mathematically.

Let x(i,t)>=0 be the production of item i in period t 

As we need to count, we need binary variables. So, introduce:

y(i,t) = 1 if item i is produced in period t
         0 otherwise

Then we can add

x(i,t) <= M*y(i,t)   (M large enough constant: i.e. capacity)

This implements y(i,t)=0 => x(i,t)=0. We don't have to worry about the other way around x(i,t)=0 => y(i,t)=0 as that is taken care of by the objective (minimize cost).

To add a special cost for producing 2 or 3 products, we need one more binary variable:

count(k,t) = 1 if the number of products produced is k (k=0,1,2,3)
           = 0 otherwise

This can be calculated as:

 y(A,t)+y(B,t)+y(C,t) = 1*count(1,t)+2*count(2,t)+3*count(3,t)   
 count(1,t)+count(2,t)+count(3,t) <= 1

Now you can add say: 100*count(2,t)+200*count(3,t) to your cost calculation. (Note: just for completeness: I assumed the cost for 3 products is at least as large as the cost for 2 products).

To achieve what you intend you can add additional constraints connecting prod_vars and use_vars like (pseudo-code):

prod_vars[(i, j)] >= use_vars[(i, j)] forall i, j
prod_vars[(i, j)] <= M * use_vars[(i, j)] forall i, j

where M can be set to max(input_data['production_capacity']).

Doing this you don't need modify the original constraints.

Related