cmp
cmp

Reputation: 568

Pulp: How to add/subtract decision variable outputs together

I am working on a rail scheduling problem that moves product from a production plant to a storage facility to satisfy demand.

I am new to pulp so finding this difficult to understand why this isn't working, and unfortunately there is very little documentation on the subject.

The problem

There are three decision variables to monitor:

  1. The availability/inventory of product at each plant - note each plant can manufacture different products.

  2. Rail - how much to move of each product from each plant. Each train can move 8400 tons.

  3. The inventory of each product at the storage facility.

Upon running the program, the rail decision variable works correctly i.e. the output is as expected, however the inventory at the plant and storage facility is not showing the amount removed and subsequently added by the rail.

Data & code below:

import pulp as pulp
import pandas as pd
import datetime

#rail capacity df from plant: no_trains_per_day max
rail_capacity_df_daily = {'ABC': {'capacity_per_day': 1, 'max': 19},
                          'DEF': {'capacity_per_day': 1, 'max': 50}}
rail_capacity_df = pd.DataFrame.from_dict(rail_capacity_df_daily ,orient='Index')

# facilities_df
facilities_inventory = {'BZL': {'current': 100000, 'max': 210000}, 
                        'AFM': {'current': 100000, 'max': 190000},
                        'PRE': {'current': 100000, 'max': 245000}}
facilities_df = pd.DataFrame.from_dict(facilities_inventory, orient='Index')


# plants_df
plant_df_inventory = {('ABC', 'PRE'): {'inventory': 196710, 'daily_production': 6000},
                      ('ABC', 'AFM'): {'inventory': 199910, 'daily_production': 5000},
                      ('DEF', 'BZL'): {'inventory': 127110, 'daily_production': 5000},
                      ('DEF', 'PRE'): {'inventory': 227100, 'daily_production': 6000}}  

plants_df = pd.DataFrame.from_dict(plant_df_inventory,orient='Index').rename_axis(['plant', 'product'])

# Sales demand

sales_demand = {'2020-04-24': {'AFM': 10000, 'PRE': 15000, 'BZL': 10000},
                '2020-04-25': {'AFM': 10000, 'PRE': 15000, 'BZL': 10000},
                '2020-04-26': {'AFM': 10000, 'PRE': 15000, 'BZL': 10000},
                '2020-04-27': {'AFM': 10000, 'PRE': 15000, 'BZL': 10000},
                '2020-04-28': {'AFM': 10000, 'PRE': 15000, 'BZL': 10000},
                '2020-04-29': {'AFM': 10000, 'PRE': 15000, 'BZL': 10000},}

sales_df = pd.DataFrame.from_dict(sales_demand, orient='Index').rename_axis(['date'])

# Demand: Current Sales Demand
sales_demand = sales_df.to_dict(orient='index')

# PLANNING HORIZON PARAMS  
_current_date = pd.to_datetime(datetime.datetime.today().strftime('%Y%m%d'))
planning_horizon_max = datetime.datetime.today() + datetime.timedelta(4)
planning_horizon_max = pd.to_datetime(planning_horizon_max.strftime('%Y%m%d'))

# COMBINATION VARS
dates = [d.strftime('%F') for d in pd.date_range(_current_date,planning_horizon_max)]
plant_combinations = [(plant, product) for plant, product in plants_df.index]
products = [p for p in facilities_df.index] 
plants = ['ABC', 'DEF']

# Sales Demand: Grade Combinations by Date
demand_requirements = [(d, p) for d in dates for p in products]

# INVENTORY 
# Initial Storage Inventory
storage_inv = dict(zip(facilities_df.index, facilities_df['current']))

storage_max = dict(zip(facilities_df.index, facilities_df['max']))

# Initial Plant Inventory
plant_current_inventory = dict(zip(plants_df.index, plants_df.inventory))
plant_daily_production = dict(zip(plants_df.index, plants_df.daily_production))


# DECISION VARIABLES
# Plant facility vars
plant_inventory_vars = pulp.LpVariable.dicts(
    'Plant Inventory',
    ((date, plant, product) for date in dates for (plant, product) in plant_combinations),
    cat='Continuous',
    lowBound=0) 

# Storage Facility Vars
storage_facility_vars = pulp.LpVariable.dicts(
    'Storage Inventory',
    ((d, p) for d in dates for p in products),
    cat='Integer',
    lowBound=0)

# Total train capacity per plant dict
train_load_limit_daily = dict(zip(rail_capacity_df.index, 
                                  rail_capacity_df.capacity_per_day))

# Decision Vars: date, plant, product
train_consignment_variables = pulp.LpVariable.dicts(
    'Rail Loadings From plant',
    ((date, plant, product) for date in dates for (plant, product) in plant_combinations),
    cat='Continuous',
    lowBound=0) 


# OPTIMISATION

# Instantiate 
model = pulp.LpProblem('Rail Optimisation', pulp.LpMinimize)

solver = pulp.PULP_CBC_CMD()
solver.tmpDir = 'Users\CPrice2'

# Objective Function
model += pulp.lpSum(storage_max[product] 
    - storage_facility_vars[(date, product)] for (date, product) in storage_facility_vars), 'Minimise stockpile shortfalls'

    # PLANT INVENTORY
for date in dates:
  current_date = datetime.date.today().strftime('%F')
  date_t_minus_one = datetime.datetime.strptime(date, '%Y-%m-%d') - datetime.timedelta(days=1)
  date_t_minus_one = date_t_minus_one.strftime('%F')
  for plant, product in plant_combinations:
    if date == current_date:
      # Set current inventory
      model += plant_current_inventory[(plant, product)] - \
          train_consignment_variables[(date, plant, product)] == \
          plant_inventory_vars[(date, plant, product)] + \
          plant_daily_production[(plant, product)]
    else:
      # Get inventory from t-1
      model += plant_inventory_vars[(f'{date_t_minus_one}', plant, product)] - \
          train_consignment_variables[(date, plant, product)] == \
          plant_inventory_vars[(date, plant, product)] + \
          plant_daily_production[(plant, product)]

# Trains: Daily Rail Out Constraint 
for date in dates:
  for plant in plants:
    plant_product_combination = [tup for tup in plant_combinations if tup[0] == plant]
    variable_list = []
    for (plant_, product_) in plant_product_combination:
      variable = train_consignment_variables[(date, plant_, product_)]
      variable_list.append(variable)
    model += pulp.lpSum(var for var in variable_list) == train_load_limit_daily[plant] * 8400

# STORAGE FACILITY 
for date in dates:
  current_date = datetime.date.today().strftime('%F')
  date_t_minus_one = datetime.datetime.strptime(date, '%Y-%m-%d') - datetime.timedelta(days=1)
  date_t_minus_one = date_t_minus_one.strftime('%F')
  for plant, product in plant_combinations:
    if date == current_date:
      # Current Inv == current inventory + train in
      model += storage_inv[product] + \
          train_consignment_variables[(date, plant, product)] == \
          storage_facility_vars[(date, product)] - sales_demand[date][product] 
    else:
      model += storage_facility_vars[(f'{date_t_minus_one}', product)] + \
          train_consignment_variables[(date, plant, product)] == \
          storage_facility_vars[(date, product)] - sales_demand[date][product]

# Run solver
model.solve(solver)
pulp.LpStatus[model.status]

# Storage Out
storage_facility_out = []

for (date, product) in storage_facility_vars:
  var_out = {
      'Date': date,
      'Product': product,
      'Out Inventory': storage_facility_vars[(date, product)].varValue
  }
  storage_facility_out.append(var_out)

storage_facility_out_df = pd.DataFrame.from_records(storage_facility_out).sort_values(['Date', 'Product'])
storage_facility_out_df.set_index(['Date', 'Product'], inplace=True)

# Rail Out
rail_optimisation_outputs = []

for date, plant, product in train_consignment_variables:
  var_output = {
      'Date': date,
      'Plant': plant,
      'Product': product,
      'Rail_Out': train_consignment_variables[(date, plant, product)].varValue
  }
  rail_optimisation_outputs.append(var_output)

output_df = pd.DataFrame.from_records(rail_optimisation_outputs).sort_values(['Date', 'Plant', 'Product'])
output_df.set_index(['Date', 'Plant', 'Product'], inplace=True)

# Production Plant Out
plant_stock_out = []

for date, plant, product in plant_inventory_vars:
  var_out = {
      'Date': date,
      'Plant': plant,
      'Product': product,
      'Out Inventory': plant_inventory_vars[(date, plant, product)].varValue
      }
  plant_stock_out.append(var_out)
plant_stock_out_df = pd.DataFrame.from_records(plant_stock_out).sort_values(['Date', 'Product'])
plant_stock_out_df.set_index(['Date', 'Plant', 'Product'], inplace=True)
plant_stock_out_df

When I access the outputs of each decision variable:

train_consignment_vars.varValue = output ok.

For both plant and storage facilities I get the following:

storage_facility_vars.varValue = AttributeError: 'float' object has no attribute 'value'. If I dont call .varValue, I simply get the dictionary values without accounting for the amount added/removed by rail.

Upvotes: 1

Views: 1065

Answers (1)

kabdulla
kabdulla

Reputation: 5419

Without your code in the form of a reproducible example I can't be sure of all of the issues but here are a few:

  1. After you've already added in your objective functions you seem to add further expressions which are not in the form of contraints for example:

model += pulp.lpSum(plant_inventory_vars[(date, plant, product)]) - pulp.lpSum(train_consignment_variables[(date, plant, product)])

This is not a constraint. The thing after model += needs to take the form "A == B", or "A <= B", or "A >= B". Your expression does not.

There is another one here:

model += pulp.lpSum(port_inventory_vars[(date, product)]) + pulp.lpSum(train_consignment_variables[(date, plant, product)] for plant, product in plant_combinations)

  1. As pointed out by @pchtsp you are overwritting some of your pulp variables:

storage_facility_vars[(date, product)] = plant_current_inv[product]

The general approach in linear programming is that you declare the variables, the objective to be optimized and then the constraints that exist. In PULP the objective and constraints are added to the problem using the model += syntax. What you are doing here is taking a linear variable you've created and overwritting it with whatever is in plant_current_inv[product'. I think what you want to be doing instead is setting an equality constraint and adding that to the problem.

Upvotes: 1

Related Questions