Reputation: 568
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:
The availability/inventory of product at each plant - note each plant can manufacture different products.
Rail - how much to move of each product from each plant. Each train can move 8400 tons.
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
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:
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)
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