| name | inventory-routing-problem |
| description | When the user wants to jointly optimize inventory and routing decisions, implement vendor-managed inventory (VMI) systems, or coordinate replenishment and delivery logistics. Also use when the user mentions "IRP," "inventory routing," "vendor-managed inventory," "VMI optimization," "integrated inventory and transportation," "delivery scheduling with inventory," "maritime inventory routing," "petrol station replenishment," or "coordinated inventory-distribution." For pure routing, see vehicle-routing-problem or route-optimization. For pure inventory, see inventory-optimization or multi-echelon-inventory. |
Inventory Routing Problem (IRP)
You are an expert in Inventory Routing Problems (IRP) and integrated inventory-distribution optimization. Your goal is to help jointly optimize inventory management and vehicle routing decisions to minimize total system costs including inventory holding, routing, and potential stockouts.
Initial Assessment
Before solving inventory routing problems, understand:
-
System Structure
- Vendor-managed inventory (VMI) or retailer-managed?
- Number of customers/retailers?
- Single depot or multiple?
- Planning horizon (days, weeks)?
- Frequency of deliveries?
-
Inventory Characteristics
- Storage capacity at each location?
- Current inventory levels?
- Consumption/demand rates (deterministic or stochastic)?
- Minimum inventory levels (safety stock)?
- Maximum inventory levels (tank capacity, shelf space)?
- Product shelf life or perishability?
-
Routing Constraints
- Vehicle capacity (weight, volume)?
- Number of vehicles available?
- Maximum route duration or distance?
- Time windows for deliveries?
- Driver shift constraints?
- Accessibility restrictions?
-
Cost Structure
- Inventory holding costs at depot and customers?
- Transportation costs (per mile, per vehicle, per route)?
- Fixed cost per vehicle used?
- Penalty costs for stockouts?
- Setup/delivery fee per customer visit?
-
Service Requirements
- Must prevent stockouts?
- Minimum service frequency per customer?
- Priority customers?
- Contractual delivery requirements?
IRP Fundamentals
Problem Definition
The Inventory Routing Problem (IRP) integrates two classical problems:
- Inventory Management: When and how much to replenish each customer
- Vehicle Routing: How to efficiently route vehicles to serve customers
Key Trade-off:
- More frequent small deliveries → Higher routing costs, lower inventory
- Less frequent large deliveries → Lower routing costs, higher inventory
Problem Variants
1. Single-Period IRP
- One-time routing and delivery decision
- Given current inventory levels
- Minimize routing cost subject to inventory constraints
2. Multi-Period IRP
- Plan deliveries over time horizon (T periods)
- Account for inventory dynamics
- Most realistic and most complex
3. Deterministic vs. Stochastic IRP
- Deterministic: Known consumption rates
- Stochastic: Uncertain demand, requires safety stock
4. Maritime IRP (MIRP)
- Ships instead of trucks
- Larger capacities, longer travel times
- Often used for petrol/chemical distribution
Python Implementation: IRP Models
Single-Period IRP with MIP
import numpy as np
import pandas as pd
from pulp import *
from typing import List, Dict, Tuple
import matplotlib.pyplot as plt
from scipy.spatial.distance import cdist
class SinglePeriodIRP:
"""
Single-Period Inventory Routing Problem
Given:
- Current inventory at each customer
- Consumption rates
- Vehicle capacity
- Distance matrix
Decide:
- Which customers to visit
- How much to deliver to each
- Vehicle routes
"""
def __init__(self, num_customers: int, customer_locations: np.ndarray,
depot_location: np.ndarray, current_inventory: np.ndarray,
consumption_rates: np.ndarray, max_inventory: np.ndarray,
vehicle_capacity: float, num_vehicles: int,
holding_cost: float = 1.0, routing_cost_per_km: float = 1.0):
"""
Parameters:
-----------
num_customers : int
Number of customer locations
customer_locations : ndarray
(n x 2) array of customer coordinates
depot_location : ndarray
(2,) depot coordinates
current_inventory : ndarray
Current inventory level at each customer
consumption_rates : ndarray
Daily consumption at each customer
max_inventory : ndarray
Maximum storage capacity at each customer
vehicle_capacity : float
Vehicle capacity (units)
num_vehicles : int
Number of vehicles available
holding_cost : float
Inventory holding cost per unit per day
routing_cost_per_km : float
Cost per kilometer traveled
"""
self.n = num_customers
.customer_locations = customer_locations
.depot = depot_location
.I = current_inventory
.d = consumption_rates
.C = max_inventory
.Q = vehicle_capacity
.K = num_vehicles
.h = holding_cost
.c_routing = routing_cost_per_km
all_locations = np.vstack([depot_location, customer_locations])
.dist_matrix = cdist(all_locations, all_locations, metric=)
() -> :
prob = LpProblem(, LpMinimize)
nodes = (.n + )
customers = (, .n + )
vehicles = (.K)
x = {}
i nodes:
j nodes:
k vehicles:
i != j:
x[i, j, k] = LpVariable(, cat=)
y = {}
i customers:
k vehicles:
y[i, k] = LpVariable(, cat=)
q = {i: LpVariable(, lowBound=) i customers}
routing_cost = lpSum([
.dist_matrix[i, j] * .c_routing * x[i, j, k]
i nodes j nodes k vehicles i != j
])
inventory_after = {i: .I[i - ] + q[i] i customers}
holding_cost = .h * lpSum([inventory_after[i] i customers])
prob += routing_cost + holding_cost
i customers:
prob += lpSum([y[i, k] k vehicles]) <=
i customers:
k vehicles:
prob += lpSum([x[j, i, k] j nodes j != i]) == y[i, k]
prob += lpSum([x[i, j, k] j nodes j != i]) == y[i, k]
k vehicles:
prob += lpSum([x[, j, k] j customers]) <=
prob += lpSum([x[i, , k] i customers]) <=
prob += (lpSum([x[, j, k] j customers]) ==
lpSum([x[i, , k] i customers]))
k vehicles:
j customers:
prob += (lpSum([x[i, j, k] i nodes i != j]) ==
lpSum([x[j, i, k] i nodes i != j]))
k vehicles:
prob += lpSum([q[i] * y[i, k] i customers]) <= .Q
i customers:
prob += q[i] <= (.C[i - ] - .I[i - ]) * lpSum([y[i, k]
k vehicles])
min_delivery = (, time_until_next_delivery * .d[i - ] - .I[i - ])
prob += q[i] >= min_delivery * lpSum([y[i, k] k vehicles])
u = {i: LpVariable(, lowBound=, upBound=.n) i customers}
i customers:
j customers:
k vehicles:
i != j:
prob += u[i] - u[j] + .n * x[i, j, k] <= .n -
prob.solve(PULP_CBC_CMD(msg=))
routes = ._extract_routes(x, vehicles, nodes)
deliveries = {i: q[i].varValue q[i].varValue i customers}
total_distance = (
.dist_matrix[i, j] * x[i, j, k].varValue
i nodes j nodes k vehicles
i != j x[i, j, k].varValue >
)
total_delivery = (deliveries.values())
{
: LpStatus[prob.status],
: routes,
: deliveries,
: value(prob.objective),
: .c_routing * total_distance,
: .h * (.I[i - ] + deliveries[i]
i customers),
: total_distance,
: total_delivery,
: ([r r routes (r) > ])
}
():
routes = []
k vehicles:
route = []
current =
:
next_node =
j nodes:
j != current (current, j, k) x:
x[current, j, k].varValue > :
next_node = j
next_node next_node == :
(route) > :
route.append()
routes.append(route)
route.append(next_node)
current = next_node
routes
():
fig, (ax1, ax2) = plt.subplots(, , figsize=(, ))
colors = plt.cm.tab10(np.linspace(, , (solution[])))
ax1.plot(.depot[], .depot[], , markersize=,
label=, zorder=)
i (.n):
ax1.plot(.customer_locations[i, ],
.customer_locations[i, ],
, markersize=, zorder=)
ax1.text(.customer_locations[i, ],
.customer_locations[i, ],
, fontsize=)
route_idx, route (solution[]):
(route) > :
route_coords = np.vstack([
.depot node ==
.customer_locations[node - ]
node route
])
ax1.plot(route_coords[:, ], route_coords[:, ],
, color=colors[route_idx], linewidth=,
markersize=, label=,
alpha=)
ax1.set_xlabel()
ax1.set_ylabel()
ax1.set_title(, fontweight=)
ax1.legend()
ax1.grid(, alpha=)
customer_ids = np.arange(, .n + )
current_inv = .I
deliveries = [solution[][i] i customer_ids]
final_inv = current_inv + deliveries
capacity = .C
x_pos = np.arange(.n)
width =
ax2.bar(x_pos - width/, current_inv, width, label=,
alpha=, color=)
ax2.bar(x_pos + width/, final_inv, width, label=,
alpha=, color=)
ax2.plot(x_pos, capacity, , linewidth=, label=)
delivered_customers = [i i customer_ids deliveries[i - ] > ]
delivered_customers:
ax2.scatter([c - c delivered_customers],
[final_inv[c - ] c delivered_customers],
s=, marker=, color=, zorder=,
label=)
ax2.set_xlabel()
ax2.set_ylabel()
ax2.set_title(, fontweight=)
ax2.set_xticks(x_pos)
ax2.set_xticklabels(customer_ids)
ax2.legend()
ax2.grid(, alpha=, axis=)
plt.tight_layout()
plt
():
( + * )
()
( * )
np.random.seed()
num_customers =
depot = np.array([, ])
customer_locations = np.random.rand(num_customers, ) *
max_inventory = np.random.randint(, , num_customers)
current_inventory = max_inventory * np.random.uniform(, , num_customers)
consumption_rates = np.random.uniform(, , num_customers)
days_until_stockout = current_inventory / consumption_rates
()
()
()
()
()
()
(
)
( + * )
i (num_customers):
(
)
irp = SinglePeriodIRP(
num_customers=num_customers,
customer_locations=customer_locations,
depot_location=depot,
current_inventory=current_inventory,
consumption_rates=consumption_rates,
max_inventory=max_inventory,
vehicle_capacity=,
num_vehicles=,
holding_cost=,
routing_cost_per_km=
)
()
solution = irp.solve_mip(time_until_next_delivery=)
()
()
( * )
()
()
()
()
()
()
()
()
route_idx, route (solution[]):
(route) > :
(, end=)
(.join([ node ==
node route]))
route_delivery = (solution[][node]
node route node > )
()
node route:
node > :
delivery = solution[][node]
delivery > :
()
irp.plot_solution(solution)
plt.savefig(, dpi=, bbox_inches=)
()
irp, solution
__name__ == :
example_single_period_irp()
Multi-Period IRP
Rolling Horizon Approach
class MultiPeriodIRP:
"""
Multi-Period IRP using rolling horizon approach
Solve single-period IRP repeatedly, updating inventory levels
"""
def __init__(self, single_period_irp: SinglePeriodIRP,
num_periods: int, delivery_frequency: int = 2):
"""
Parameters:
-----------
single_period_irp : SinglePeriodIRP
Single-period IRP model
num_periods : int
Number of periods to plan
delivery_frequency : int
Minimum periods between deliveries to same customer
"""
self.irp = single_period_irp
self.T = num_periods
self.freq = delivery_frequency
self.inventory_history = np.zeros((num_periods + 1, self.irp.n))
self.inventory_history[0] = self.irp.I
self.delivery_history = []
self.route_history = []
def solve_rolling_horizon(self) -> Dict:
"""Solve multi-period IRP using rolling horizon"""
total_cost = 0
total_distance = 0
total_delivered = 0
for t in range(self.T):
print(f" Period {t+}/...", end=)
.irp.I = .inventory_history[t]
solution = .irp.solve_mip(time_until_next_delivery=.freq)
.route_history.append(solution[])
.delivery_history.append(solution[])
total_cost += solution[]
total_distance += solution[]
total_delivered += solution[]
i (, .irp.n + ):
delivered = solution[][i]
consumed = .irp.d[i - ]
.inventory_history[t + , i - ] = (
.inventory_history[t, i - ] + delivered - consumed
)
()
{
: total_cost,
: total_distance,
: total_delivered,
: total_cost / .T,
: .inventory_history,
: .delivery_history,
: .route_history
}
():
( + * )
()
( * )
np.random.seed()
num_customers =
depot = np.array([, ])
customer_locations = np.random.rand(num_customers, ) *
max_inventory = np.array([, , , , ])
current_inventory = np.array([, , , , ])
consumption_rates = np.array([, , , , ])
irp = SinglePeriodIRP(
num_customers=num_customers,
customer_locations=customer_locations,
depot_location=depot,
current_inventory=current_inventory,
consumption_rates=consumption_rates,
max_inventory=max_inventory,
vehicle_capacity=,
num_vehicles=,
holding_cost=,
routing_cost_per_km=
)
multi_irp = MultiPeriodIRP(irp, num_periods=, delivery_frequency=)
()
()
solution = multi_irp.solve_rolling_horizon()
()
()
( * )
()
()
()
()
fig, axes = plt.subplots(num_customers, , figsize=(, ))
i (num_customers):
axes[i].plot((), solution[][:, i],
marker=, linewidth=, color=)
axes[i].axhline(y=max_inventory[i], color=, linestyle=,
label=)
axes[i].axhline(y=, color=, linestyle=, linewidth=)
t ():
multi_irp.delivery_history[t][i + ] > :
axes[i].plot(t, solution[][t, i],
, markersize=, label= t == )
axes[i].set_ylabel()
axes[i].grid(, alpha=)
i == :
axes[i].legend()
axes[-].set_xlabel()
plt.suptitle(, fontsize=, fontweight=)
plt.tight_layout()
plt.savefig(, dpi=, bbox_inches=)
()
multi_irp, solution
__name__ == :
example_multi_period_irp()
Tools & Libraries
Python Libraries
pulp, pyomo: MIP modeling
ortools: Google OR-Tools for routing
numpy, scipy: Numerical computations
Commercial Software
- Blue Yonder TMS: Transportation with VMI
- Manhattan Associates: WMS/TMS integration with inventory
- SAP TM + EWM: Integrated transportation and warehouse management
- Oracle Transportation Management: Route optimization with inventory
- Descartes: Routing with inventory considerations
Common Challenges & Solutions
Challenge: Problem Size and Complexity
Problem: Combinatorial explosion with many customers and periods
Solutions:
- Rolling horizon approach
- Cluster-first, route-second heuristics
- Decomposition methods
- Limit optimization time, use good heuristics
Challenge: Demand Uncertainty
Problem: Stochastic consumption rates
Solutions:
- Safety stock at customers
- Robust optimization with demand scenarios
- Frequent replanning
- Risk pooling at depot
Challenge: Time Windows and Service Requirements
Problem: Customers have delivery windows, minimum frequencies
Solutions:
- Add time window constraints to MIP
- Multi-objective optimization (cost vs. service)
- Penalty costs for violations
- Contract-based service level agreements
Challenge: Heterogeneous Fleet
Problem: Different vehicle types (capacity, cost)
Solutions:
- Index vehicles by type in model
- Type-specific routing costs
- Preferential use of lower-cost vehicles
Related Skills
- vehicle-routing-problem: Pure routing optimization
- route-optimization: Transportation planning
- inventory-optimization: Inventory management
- multi-echelon-inventory: Network inventory
- network-design: Strategic distribution network
- fleet-management: Vehicle fleet operations
- demand-forecasting: Consumption rate prediction