Search code examples
pythonoptimizationlinear-programmingpulpmixed-integer-programming

PuLP Python - How to linearize an inequality involving a variable


I am working on a Copper payables problem where the objective function is to maximise the sum of copper payable over a time period, T.

Is it possible to formulate a constraint that compares a variable with/between two constants, dynamically:

i.e.

lower_bound, operator_1 (>, >=), variable, operator_2(<, <=) up_bound

Description of the problem

The total amount of payable tonnes i.e. what the customer will pay for is dependent on the copper content of the sales material.

  1. Each customer has a number of payable terms expressed as copper specification operable bounds, as per the below example data:

Customer data

import io
import pandas as pd

customer_payables = """customer, tier, specvalue_1, specoperator_1, specvalue_2, \
specoperator_2, coeff
    'abc', 1, 0, '>=', 20, '<=', 96.0
    'abc', 2, 20, '>', 24, '<=', 96.5 
    'abc', 3, 24, '>', 100, '<=', 96.65
    'def', 1, 0, '>=', 20, '<=', 96.0
    'def', 2, 20, '>=', 22, '<=', 96.66
    'def', 3, 22, '>=', 100', '<=', 97.0
    """

_cust_data = io.StringIO(customer_payables)
cust_df = pd.read_csv(_cust_data, sep=",")
cust_df = cust_df.set_index('customer')
cust_df
  1. I have a dataframe of available material, in tonnes, with specific copper content in two warehouses with two stockpiles. Note that the quality of this material changes over time:

##Stockpile data

stockpile_data_dict = {
    'Warehouse 1':{
        'Stockpile 1': {'cu': 27}, 
        'Stockpile 2': {'cu': 18}
        },
        'Warehouse 2': {
            'Stockpile 1':{'cu': 22}, 
            'Stockpile 2': {'cu': 16}}}
  
stockpile_df = pd.concat({k: pd.DataFrame(v).T for k, v in stockpile_data_dict.items()}, axis=0) 
stockpile_df

Question I have created a variable to represent the copper concentration for each warehouse, stockpile. This is kept as a variable as the intention is to model inventory over time, allowing the model a choice of when to sell material in order to maximise the payables:

cu_spec_of_sale_material = pulp.LpVariable.dicts(
    'Copper spec of sale material',
    ((warehouse, stockpile)
      for warehouse, stockpile in stockpile_df.index),
      cat='Continuous')

How can I create a linear constraint that returns the correct payable coefficient with respect to the copper concentration VALUE of this variable?

In pseudo code terms, it evaluates something like the below:

for customer, effective_tier in effective_payable_coefficient:
  if customer_lower_bound_val < cu_spec_sales_material[warehouse, stockpile] < customer_up_bound_val:
    PULP += effective_payable_coefficient[customer, effective_tier] == 1

I do not use Pulp very often, so please bear with me.

All help gratefully received, thank you.


Solution

  • I think you are looking for a linear formulation for the implication:

    a < x < b => y = 1
    

    where a,b are constants, x is a continuous variable and y is a binary variable.

    We can write this as:

     x ≤ a + M1 ⋅ δ + M1 ⋅ y
     x ≥ b - M2 ⋅ (1-δ) - M2 ⋅ y
     δ,y ∈ {0,1}
     x ∈ [L,U]
     M1 = U-a
     M2 = b-L
    

    δ is another binary variable, L,U are lower/upper bounds on x, M1,M2 are constants.

    Intuition: these constraints implement the implication:

     y = 0 =>  x ≤ a or x ≥ b
    

    which means that if a < x < b we must have y=1.

    To derive these constraints it is best to move away from your computer and use old-fashioned pen-and-paper.