# Pulp Python: How to formulate a time-based variable for shipping demurrage

I am working on a shipping optimisation problem that aims to minimise demurrage charges as a result of low/insufficient inventory.

I have daily vessel requirement (sales) data in the format sales[vessel_start_date][vessel][product_required_tonnes]. I also have the daily stock inventory level in the form port_inventory[date][product_availability_tonnes]

• Demurrage is defined as: (total_days_in_port - laycan_days) * per_diem_charge. It is incurred when there is insufficient material at the port to load the vessel and the vessel's laycan period (vessel_start_date plus 10 days) has expired.
• Each vessel takes two days to load.

I would be very grateful if anyone could help me formulate this expression in pulp, or give me a steer. Many thanks.

I have created a model that I believe addresses your objective of minimizing demurrage charges. It utilizes indicator variables for each day and vessel showing when each vessel is finally loaded. The factors for the variables in the objective function are to be calculated based on the demurrage formula you provided.

The first thing to do with this model is to ensure that you have the port inventory working correctly, then add the demurrage calculation for the objective. The formulation I have provided does not deal with the two day loading period. You'll want to fill in details on how inventory increases at the port.

vessels = ['a', 'b']
sales_days = list(range(20))
sales = {1: {"a": 10}, 3: {"b": 10}}
vessel_qty = {"a": 10, "b": 10}
per_diem_charge = 100
laycan_days = 10
initial_port_inventory = 2
daily_inventory_increase = 1
model = pulp.LpProblem("PortDemurrage")
day_port_inventory = pulp.LpVariable.dict("port_inventory", sales_days,
lowBound=0, cat="Integer")
# set initial inventory
model += day_port_inventory[0] == initial_port_inventory
vessel_start_dates = {vessel: day for day in sales for vessel in sales[day]}
# denote the vessel sales by day the vessel is finally loaded
vessel_sales = pulp.LpVariable.dicts("vessel_sales", [f"{vessel}_{day}" for
vessel in vessels for day in sales_days[vessel_start_dates[vessel]:]],
lowBound=0, cat="Integer")
for day in sales_days[1:]:
day_sales = 0
for vessel in vessels:
if vessel_start_dates[vessel] <= day:
day_sales += vessel_sales[f"{vessel}_{day}"]
model += day_port_inventory[day] == day_port_inventory[day - 1] +
daily_inventory_increase - day_sales

for vessel in vessels:
vessel_start_date = vessel_start_dates[vessel]
possible_days = sales_days[vessel_start_date:]
model += sum(vessel_sales[f"{vessel}_{day}"] for day in possible_days) ==
vessel_qty[vessel]

obj = 0
final_day_vars = []
for vessel in vessels:
demurrage_day_start = vessel_start_dates[vessel]+laycan_days+1
vessel_start_date = vessel_start_dates[vessel]
for day in sales_days[vessel_start_date:]:
vessel_day_var = pulp.LpVariable(f"{vessel}_sales_day_{day}",
lowBound=0, upBound=1, cat="Integer")
final_day_vars.append(vessel_day_var)
if day >= demurrage_day_start:
obj += vessel_day_var * (per_diem_charge * (day -
vessel_start_dates[vessel] - laycan_days))
model += vessel_sales[f"{vessel}_{day}"] <= vessel_day_var *
vessel_qty[vessel]
model += sum(final_day_vars) == len(vessels)
model += obj, "minimize demurrage charge"


Solving this model produces a solution where vessel a is finally loaded on day 8 and vessel b is finally loaded on day 18. The objective value of this solution is 500, since vessel b needs to wait 15 days after arrival.

• Thank you very much for the comprehensive response - very much appreciated! – cmp Jun 13 '20 at 21:39
• could you please me clarify what 'vessel_qty[vessel]' is? – cmp Jun 16 '20 at 13:59
• It is the load requirement quantity by vessel. – Wesley Dyk Jun 17 '20 at 17:43
• Thank you for clarifying, much appreciated. I dont suppose you would perhaps be able to help me with my other question? or.stackexchange.com/questions/4394/… – cmp Jun 17 '20 at 18:44
• I added a portion of the mathematical formulation in that question. I believe that the pulp implementation here addresses both questions. – Wesley Dyk Jun 17 '20 at 22:56