| name | dynamic-lot-sizing |
| description | When the user wants to solve lot-sizing problems with time-varying costs or demand, handle non-stationary inventory systems, or optimize replenishment with changing parameters over time. Also use when the user mentions "time-varying lot sizing," "finite horizon lot sizing," "dynamic EOQ," "price changes over time," "seasonal lot sizing," "Wagner-Whitin with time-varying costs," or "rolling horizon planning." For stationary systems, see economic-order-quantity or lot-sizing-problems. For stochastic demand, see stochastic-inventory-models. |
Dynamic Lot-Sizing
You are an expert in dynamic lot-sizing models for non-stationary inventory systems with time-varying parameters. Your goal is to help optimize inventory replenishment decisions when demand, costs, or prices change over time, using finite-horizon planning approaches.
Initial Assessment
Before solving dynamic lot-sizing problems, understand:
-
Time Horizon
- Planning horizon length? (months, quarters, years)
- Finite or rolling horizon?
- Frequency of replanning?
-
Time-Varying Parameters
- What changes over time? (demand, costs, prices, capacity)
- Demand: seasonal patterns, trends, known changes?
- Costs: price increases, seasonal holding costs?
- Capacity: time-varying production/storage limits?
-
Problem Type
- Deterministic or stochastic time variation?
- Known price changes (announced) or forecasted?
- Must account for inflation?
-
Decision Flexibility
- Can adjust decisions at each period?
- Committed orders vs. flexible replenishment?
- Lead times and their impact?
-
Special Considerations
- End-of-horizon effects (salvage value, terminal inventory)?
- Price speculation opportunities?
- Obsolescence or product lifecycle considerations?
Dynamic Lot-Sizing Fundamentals
Problem Characteristics
Key Differences from Static EOQ:
- Demand varies by period: D₁, D₂, ..., D_T
- Costs may vary: setup S_t, holding h_t, purchase c_t
- Finite planning horizon T (not infinite)
- Must account for end-of-horizon effects
Decision: Order quantities Q_t in each period t to minimize total cost
Applications:
- Seasonal demand planning
- Price speculation (buy before price increase)
- Product lifecycle management (new/declining products)
- Fashion/perishable goods
- Promotional planning
Python Implementation: Dynamic Lot-Sizing
Time-Varying Demand and Costs
import numpy as np
import pandas as pd
from typing import List, Dict, Optional
import matplotlib.pyplot as plt
from scipy.optimize import minimize
class DynamicLotSizing:
"""
Dynamic lot-sizing with time-varying parameters
Handles changes in demand, costs, and prices over time
"""
def __init__(self, demands: List[float], setup_costs: List[float],
holding_costs: List[float], unit_costs: List[float],
initial_inventory: float = 0, salvage_value: float = 0):
"""
Parameters:
-----------
demands : list
Demand in each period [D₁, D₂, ..., D_T]
setup_costs : list
Setup cost in each period [S₁, S₂, ..., S_T]
holding_costs : list
Holding cost per unit per period [h₁, h₂, ..., h_T]
unit_costs : list
Purchase cost per unit [c₁, c₂, ..., c_T]
initial_inventory : float
Starting inventory
salvage_value : float
Value per unit of leftover inventory at end
"""
self.T = len(demands)
self.demands = np.array(demands)
self.setup_costs = np.array(setup_costs)
self.holding_costs = np.array(holding_costs)
self.unit_costs = np.array(unit_costs)
self.initial_inventory = initial_inventory
.salvage_value = salvage_value
() -> :
cum_demand = np.zeros(.T + )
t (.T - , -, -):
cum_demand[t] = cum_demand[t + ] + .demands[t]
max_inventory = (cum_demand[])
INF =
F = [[INF] * (max_inventory + ) _ (.T + )]
decision = [[] * (max_inventory + ) _ (.T)]
i (max_inventory + ):
F[.T][i] = -.salvage_value * i
t (.T - , -, -):
d_t = .demands[t]
S_t = .setup_costs[t]
h_t = .holding_costs[t]
c_t = .unit_costs[t]
I_t (max_inventory + ):
I_t >= d_t:
I_next = I_t - d_t
cost_no_order = h_t * I_next + F[t + ][(I_next)]
:
cost_no_order = INF
best_order_cost = INF
best_order_qty =
max_order = (cum_demand[t] - I_t)
Q (, max_order + ):
I_after_order = I_t + Q
I_after_order >= d_t:
I_next = I_after_order - d_t
cost = S_t + c_t * Q + h_t * I_next + F[t + ][(I_next)]
cost < best_order_cost:
best_order_cost = cost
best_order_qty = Q
cost_no_order < best_order_cost:
F[t][I_t] = cost_no_order
decision[t][I_t] =
:
F[t][I_t] = best_order_cost
decision[t][I_t] = best_order_qty
orders = np.zeros(.T)
inventory = np.zeros(.T + )
inventory[] = .initial_inventory
t (.T):
I_t = (inventory[t])
Q_t = decision[t][I_t] I_t <= max_inventory
orders[t] = Q_t
inventory[t + ] = inventory[t] + Q_t - .demands[t]
setup_cost = (.setup_costs[t] t (.T) orders[t] > )
purchase_cost = (.unit_costs[t] * orders[t] t (.T))
holding_cost = (.holding_costs[t] * inventory[t + ]
t (.T))
salvage = .salvage_value * inventory[.T]
total_cost = setup_cost + purchase_cost + holding_cost - salvage
{
: ,
: orders,
: inventory[:-],
: total_cost,
: setup_cost,
: purchase_cost,
: holding_cost,
: salvage
}
() -> :
speculation_opportunities = []
t (.T - ):
current_price = .unit_costs[t]
future_price = .unit_costs[t + ]
price_increase = future_price - current_price
holding_cost = .holding_costs[t]
net_savings = price_increase - holding_cost
net_savings > :
speculation_opportunities.append({
: t + ,
: current_price,
: future_price,
: price_increase,
: holding_cost,
: net_savings
})
speculation_opportunities
():
periods = np.arange(, .T + )
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(, , figsize=(, ))
ax1.plot(periods, .demands, marker=, linewidth=, color=)
ax1.set_xlabel()
ax1.set_ylabel()
ax1.set_title(, fontweight=)
ax1.grid(, alpha=)
ax2.plot(periods, .unit_costs, marker=, linewidth=, color=)
ax2.set_xlabel()
ax2.set_ylabel()
ax2.set_title(, fontweight=)
ax2.grid(, alpha=)
ax3.plot(periods, .setup_costs, marker=, linewidth=, color=)
ax3.set_xlabel()
ax3.set_ylabel()
ax3.set_title(, fontweight=)
ax3.grid(, alpha=)
ax4.plot(periods, .holding_costs, marker=, linewidth=, color=)
ax4.set_xlabel()
ax4.set_ylabel()
ax4.set_title(, fontweight=)
ax4.grid(, alpha=)
plt.tight_layout()
plt
() -> :
actual_demands :
actual_demands = .demands
actual_demands = np.array(actual_demands)
orders = np.zeros(.T)
inventory = np.zeros(.T + )
inventory[] = .initial_inventory
total_cost =
t (.T):
horizon_end = (t + horizon_length, .T)
subproblem = DynamicLotSizing(
demands=(.demands[t:horizon_end]),
setup_costs=(.setup_costs[t:horizon_end]),
holding_costs=(.holding_costs[t:horizon_end]),
unit_costs=(.unit_costs[t:horizon_end]),
initial_inventory=inventory[t],
salvage_value=.salvage_value
)
solution = subproblem.dynamic_programming()
orders[t] = solution[][]
inventory[t + ] = inventory[t] + orders[t] - actual_demands[t]
orders[t] > :
total_cost += .setup_costs[t]
total_cost += .unit_costs[t] * orders[t]
total_cost += .holding_costs[t] * inventory[t + ]
{
: ,
: orders,
: inventory[:-],
: total_cost
}
():
( + * )
()
( * )
demands = [, , , , , , , , , , , ]
unit_costs = [, , , , , , , , , , , ]
holding_costs = [, , , , , , , , , , , ]
setup_costs = [] *
problem = DynamicLotSizing(
demands=demands,
setup_costs=setup_costs,
holding_costs=holding_costs,
unit_costs=unit_costs,
initial_inventory=,
salvage_value=
)
()
()
()
()
spec_opps = problem.price_speculation_analysis()
spec_opps:
()
opp spec_opps:
(
)
()
solution = problem.dynamic_programming()
()
()
( * )
()
()
()
()
()
()
(
)
( + * )
t (problem.T):
setup_indicator = solution[][t] >
(
)
()
solution[][] > demands[]:
(
)
()
problem.plot_time_varying_parameters()
plt.savefig(, dpi=, bbox_inches=)
()
problem, solution
():
( + * )
()
( * )
forecast_demands = [, , , , , , , , , , , ]
np.random.seed()
actual_demands = forecast_demands + np.random.normal(, , )
actual_demands = np.maximum(actual_demands, )
problem = DynamicLotSizing(
demands=forecast_demands,
setup_costs=[] * ,
holding_costs=[] * ,
unit_costs=[] * ,
initial_inventory=
)
()
()
()
()
full_horizon = problem.dynamic_programming()
rolling_6 = problem.rolling_horizon_simulation(horizon_length=,
actual_demands=actual_demands)
rolling_3 = problem.rolling_horizon_simulation(horizon_length=,
actual_demands=actual_demands)
()
()
( * )
comparison = pd.DataFrame([
{: , : full_horizon[]},
{: , : rolling_6[]},
{: , : rolling_3[]}
])
( + comparison.to_string(index=))
()
problem, rolling_6
__name__ == :
problem1, solution1 = example_seasonal_with_price_change()
problem2, solution2 = example_rolling_horizon()
Tools & Libraries
Python Libraries
numpy, scipy: Numerical computations
pulp, pyomo: Optimization modeling
- Dynamic programming implementations
Commercial Software
- SAP APO: Advanced planning with time-varying parameters
- Blue Yonder: Dynamic planning and optimization
- Kinaxis: RapidResponse with scenario planning
- o9 Solutions: Time-phased planning
Common Challenges & Solutions
Challenge: Computational Complexity
Problem: DP state space grows large
Solutions:
- Discretize inventory levels
- Use approximate DP
- Rolling horizon approach
- Heuristics for large problems
Challenge: Forecast Uncertainty
Problem: Future demands/prices uncertain
Solutions:
- Rolling horizon with frequent replanning
- Stochastic DP for uncertainty
- Scenario-based planning
- Robust optimization
Challenge: End-of-Horizon Effects
Problem: Artificial terminal behavior
Solutions:
- Use appropriate salvage values
- Extend horizon beyond decision period
- Rolling horizon mitigates this
- Terminal inventory targets
Related Skills
- economic-order-quantity: Static EOQ models
- lot-sizing-problems: Multi-period deterministic lot-sizing
- stochastic-inventory-models: Uncertainty in demand
- demand-forecasting: Forecast time-varying demand
- seasonal-planning: Seasonal demand patterns
- price-optimization: Dynamic pricing strategies