| name | wave-planning-optimization |
| description | When the user wants to optimize pick wave planning, schedule warehouse operations, or improve order fulfillment efficiency. Also use when the user mentions "wave management," "batch picking," "pick wave scheduling," "order release optimization," "wave design," or "pick wave strategy." For order batching, see order-batching-optimization. For workforce scheduling, see workforce-scheduling. |
Wave Planning Optimization
You are an expert in warehouse wave planning and order release optimization. Your goal is to help design and optimize pick waves to maximize picker productivity, balance workload, meet cutoff times, and improve overall fulfillment efficiency.
Initial Assessment
Before optimizing wave planning, understand:
-
Operational Characteristics
- Daily order volume (lines and units)?
- Order types (each-pick, case-pick, full-pallet)?
- Warehouse zones and pick methods?
- Shift structure and available labor?
- Current wave frequency and size?
-
Business Requirements
- Shipping cutoff times?
- Priority order types (same-day, next-day)?
- Customer SLAs and promises?
- Carrier pickup schedules?
- Order profile (single-line vs. multi-line)?
-
Constraints
- Equipment capacity (conveyors, sorters)?
- Packing station capacity?
- Shipping dock doors available?
- Labor availability by shift?
- WMS capabilities and limitations?
-
Performance Metrics
- Current picks per hour?
- Order cycle time (order → ship)?
- Labor utilization?
- On-time shipping performance?
- Wave completion rates?
Wave Planning Framework
Wave Design Principles
1. Wave Sizing
-
Small Waves (50-200 orders)
- Pros: Flexible, quick completion, easy re-wave
- Cons: More frequent releases, higher admin overhead
- Use: High variability, frequent cutoffs
-
Medium Waves (200-500 orders)
- Pros: Balanced workload, good equipment utilization
- Cons: Some idle time between waves
- Use: Standard operations, moderate volume
-
Large Waves (500-1000+ orders)
- Pros: Maximum efficiency, fewer releases
- Cons: Inflexible, longer cycle time
- Use: High volume, stable demand
2. Wave Frequency
- Continuous Waves: Release new wave when previous completes
- Fixed Schedule: Every 2-4 hours (e.g., 8am, 12pm, 4pm)
- Dynamic: Based on order accumulation threshold
- Just-in-Time: Aligned with carrier pickups
3. Wave Composition
- Zone-Based: All orders for a warehouse zone
- Order-Type Based: Priority, standard, bulk separately
- Customer-Based: Group by customer or ship-to region
- Carrier-Based: Group by shipping carrier
- Hybrid: Combination of above
Wave Optimization Objectives
Primary Goals:
1. Maximize picker productivity (picks/hour)
2. Balance workload across zones/pickers
3. Meet shipping cutoff times
4. Minimize labor cost
5. Maximize equipment utilization
Trade-offs:
- Large waves → Higher efficiency BUT Longer cycle time
- Small waves → Faster cycle time BUT Lower efficiency
- Balanced waves → Even workload BUT May miss optimal picking
Mathematical Formulation
Wave Planning Optimization Model
Decision Variables:
- x[o,w] = 1 if order o assigned to wave w, 0 otherwise
- y[w] = 1 if wave w is used, 0 otherwise
- t[w] = start time of wave w
- z[w,z] = workload (lines) in wave w for zone z
Parameters:
- L[o] = number of pick lines in order o
- Z[o,z] = number of lines in order o for zone z
- D[o] = deadline for order o
- P = picker productivity (lines/hour)
- W_min, W_max = min/max lines per wave
- N_pickers[z] = number of pickers in zone z
Objective Function:
Minimize:
α × (Number of waves) # Minimize wave releases
+ β × (Total completion time) # Minimize cycle time
+ γ × (Workload imbalance) # Balance zones
+ δ × (Late orders penalty) # Meet deadlines
Formally:
α × Σ y[w]
+ β × Σ (t[w] + duration[w])
+ γ × Σ (max_workload[w] - min_workload[w])
+ δ × Σ max(0, completion[o] - D[o])
Constraints:
for o in orders:
Σ x[o,w] = 1 for all w
for w in waves:
W_min × y[w] ≤ Σ (L[o] × x[o,w]) ≤ W_max × y[w] for all o
for o in orders:
for w in waves:
if x[o,w] = 1:
t[w] + processing_time[w] ≤ D[o]
for w in waves:
for z in zones:
z[w,z] = Σ (Z[o,z] × x[o,w]) for all o
for w in waves:
for z in zones:
z[w,z] / (N_pickers[z] × P) ≤ shift_duration
for w in 1..W-1:
t[w] + duration[w] ≤ t[w+1]
Wave Planning Algorithms
Greedy Wave Building
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
def greedy_wave_planning(orders, max_wave_size=500, max_waves=10):
"""
Build waves using greedy heuristic
Sort orders by priority/deadline, then fill waves
Parameters:
-----------
orders : DataFrame
Columns: order_id, lines, deadline, priority, zone
max_wave_size : int
Maximum lines per wave
max_waves : int
Maximum number of waves
Returns:
--------
Wave assignments
"""
orders_sorted = orders.sort_values(
['priority', 'deadline', 'lines'],
ascending=[False, True, False]
)
waves = []
current_wave = {
'wave_id': 1,
'orders': [],
'total_lines': 0,
'zones': {}
}
for idx, order in orders_sorted.iterrows():
order_lines = order['lines']
order_zone = order.get('zone', 'default')
if current_wave['total_lines'] + order_lines <= max_wave_size:
current_wave['orders'].append(order['order_id'])
current_wave['total_lines'] += order_lines
if order_zone current_wave[]:
current_wave[][order_zone] =
current_wave[][order_zone] += order_lines
:
waves.append(current_wave)
(waves) >= max_waves:
current_wave = {
: (waves) + ,
: [order[]],
: order_lines,
: {order_zone: order_lines}
}
current_wave[] (waves) < max_waves:
waves.append(current_wave)
pd.DataFrame(waves)
orders = pd.DataFrame({
: [ i (, )],
: np.random.randint(, , ),
: pd.date_range(, periods=, freq=),
: np.random.choice([, , ], ),
: np.random.choice([, , ], )
})
waves = greedy_wave_planning(orders, max_wave_size=, max_waves=)
()
()
()
_, wave waves.iterrows():
(
)
Balanced Wave Planning
def balanced_wave_planning(orders, num_waves, zones):
"""
Create balanced waves across zones to even workload
Parameters:
-----------
orders : DataFrame
Order data with zone distribution
num_waves : int
Number of waves to create
zones : list
Zone identifiers
Returns:
--------
Balanced wave assignments
"""
order_zone_lines = {}
for idx, order in orders.iterrows():
order_id = order['order_id']
zone_dist = {z: np.random.randint(0, order['lines'] // len(zones) + 1)
for z in zones}
order_zone_lines[order_id] = zone_dist
waves = [{
'wave_id': w + 1,
'orders': [],
'zone_lines': {z: 0 for z in zones},
'total_lines': 0
} for w in range(num_waves)]
orders_sorted = orders.sort_values('lines', ascending=False)
for idx, order in orders_sorted.iterrows():
order_id = order['order_id']
zone_dist = order_zone_lines[order_id]
best_wave_idx = None
best_balance_score = ()
w_idx, wave (waves):
new_zone_loads = {}
z zones:
new_zone_loads[z] = wave[][z] + zone_dist[z]
max_load = (new_zone_loads.values())
min_load = (new_zone_loads.values())
balance_score = max_load - min_load
balance_score < best_balance_score:
best_balance_score = balance_score
best_wave_idx = w_idx
waves[best_wave_idx][].append(order_id)
waves[best_wave_idx][] += order[]
z zones:
waves[best_wave_idx][][z] += zone_dist[order_id][z]
pd.DataFrame(waves)
zones = [, , ]
balanced_waves = balanced_wave_planning(orders, num_waves=, zones=zones)
()
_, wave balanced_waves.iterrows():
()
()
max_zone = (wave[].values())
min_zone = (wave[].values())
()
MIP-Based Wave Optimization
from pulp import *
def optimize_wave_planning(orders_df, num_waves, zone_productivity,
shift_hours=8, min_wave_size=100):
"""
Optimize wave planning using Mixed-Integer Programming
Parameters:
-----------
orders_df : DataFrame
Order data: order_id, lines, deadline, zone_breakdown
num_waves : int
Number of waves to plan
zone_productivity : dict
{zone: lines_per_hour_per_picker}
shift_hours : float
Hours available per shift
min_wave_size : int
Minimum lines per wave
Returns:
--------
Optimal wave assignments
"""
orders = orders_df['order_id'].tolist()
waves = list(range(num_waves))
zones = list(zone_productivity.keys())
prob = LpProblem("Wave_Planning", LpMinimize)
x = LpVariable.dicts("assign",
[(o, w) for o in orders for w in waves],
cat='Binary')
y = LpVariable.dicts("use_wave",
waves,
cat='Binary')
workload = LpVariable.dicts("workload",
[(w, z) for w in waves for z in zones],
lowBound=0,
cat='Continuous')
max_workload = LpVariable.dicts("max_load",
waves,
lowBound=,
cat=)
prob += (
* lpSum([y[w] w waves]) +
lpSum([max_workload[w] w waves])
),
o orders:
prob += lpSum([x[o, w] w waves]) == ,
w waves:
z zones:
prob += workload[w, z] == lpSum([
orders_df.loc[orders_df[] == o, ].values[] / (zones) * x[o, w]
o orders
]),
w waves:
z zones:
prob += max_workload[w] >= workload[w, z],
w waves:
total_lines = lpSum([
orders_df.loc[orders_df[] == o, ].values[] * x[o, w]
o orders
])
prob += total_lines >= min_wave_size * y[w],
prob += total_lines <= shift_hours * (zone_productivity.values()) * y[w], \
w waves:
o orders:
prob += x[o, w] <= y[w],
prob.solve(PULP_CBC_CMD(msg=))
wave_assignments = []
w waves:
y[w].varValue > :
wave_orders = [o o orders x[o, w].varValue > ]
total_lines = (
orders_df.loc[orders_df[] == o, ].values[]
o wave_orders
)
wave_workloads = {
z: workload[w, z].varValue z zones
}
wave_assignments.append({
: w + ,
: wave_orders,
: (wave_orders),
: total_lines,
: wave_workloads,
: max_workload[w].varValue
})
{
: LpStatus[prob.status],
: value(prob.objective),
: pd.DataFrame(wave_assignments)
}
zone_productivity = {: , : , : }
result = optimize_wave_planning(
orders.head(),
num_waves=,
zone_productivity=zone_productivity,
shift_hours=,
min_wave_size=
)
()
()
()
(result[][[, , , ]])
Advanced Wave Planning Techniques
Dynamic Wave Release Strategy
class DynamicWaveManager:
"""
Manage dynamic wave releases based on order accumulation
"""
def __init__(self, target_wave_size=400, min_wave_size=200,
release_threshold=0.9):
self.target_wave_size = target_wave_size
self.min_wave_size = min_wave_size
self.release_threshold = release_threshold
self.pending_orders = []
self.released_waves = []
def add_order(self, order):
"""Add new order to pending queue"""
self.pending_orders.append(order)
def should_release_wave(self):
"""
Determine if wave should be released
Release criteria:
1. Accumulated lines >= target size
2. Oldest order waiting > max_wait_time
3. Cutoff time approaching
"""
total_lines = sum(o['lines'] for o in self.pending_orders)
if total_lines >= self.target_wave_size * self.release_threshold:
return True, "Size threshold reached"
if len(self.pending_orders) > 0:
oldest_order_time = (o[] o .pending_orders)
wait_minutes = (datetime.now() - oldest_order_time).total_seconds() /
wait_minutes > :
,
total_lines >= .min_wave_size:
,
,
():
(.pending_orders) == :
wave_orders = []
total_lines =
sorted_orders = (
.pending_orders,
key= x: (x.get(, ), x.get(, datetime.)),
reverse=
)
order sorted_orders:
total_lines + order[] <= .target_wave_size:
wave_orders.append(order)
total_lines += order[]
order wave_orders:
.pending_orders.remove(order)
wave = {
: (.released_waves) + ,
: wave_orders,
: total_lines,
: datetime.now()
}
.released_waves.append(wave)
wave
():
{
: (.pending_orders),
: (o[] o .pending_orders),
: ((o[] o .pending_orders),
default=)
}
manager = DynamicWaveManager(target_wave_size=, min_wave_size=)
i ():
order = {
: ,
: np.random.randint(, ),
: np.random.choice([, , ]),
: datetime.now() + timedelta(hours=),
: datetime.now() - timedelta(minutes=np.random.randint(, ))
}
manager.add_order(order)
should_release, reason = manager.should_release_wave()
()
should_release:
wave = manager.release_wave()
()
()
()
pending = manager.get_pending_summary()
()
Multi-Shift Wave Planning
def plan_multi_shift_waves(orders, shifts, pickers_per_shift,
productivity=100):
"""
Plan waves across multiple shifts
Parameters:
-----------
orders : DataFrame
All orders to fulfill
shifts : list of dict
[{shift_id, start_time, end_time, hours}, ...]
pickers_per_shift : dict
{shift_id: num_pickers}
productivity : float
Lines per hour per picker
Returns:
--------
Wave plan with shift assignments
"""
shift_capacity = {}
for shift in shifts:
shift_id = shift['shift_id']
capacity = (pickers_per_shift[shift_id] *
shift['hours'] *
productivity)
shift_capacity[shift_id] = capacity
orders_sorted = orders.sort_values('deadline')
shift_assignments = {shift['shift_id']: [] for shift in shifts}
shift_loads = {shift['shift_id']: 0 for shift in shifts}
for idx, order in orders_sorted.iterrows():
order_lines = order['lines']
deadline = order['deadline']
assigned = False
for shift in shifts:
shift_id = shift['shift_id']
if shift['end_time'] <= deadline:
if shift_loads[shift_id] + order_lines <= shift_capacity[shift_id]:
shift_assignments[shift_id].append(order[])
shift_loads[shift_id] += order_lines
assigned =
assigned:
()
all_waves = []
wave_counter =
shift shifts:
shift_id = shift[]
shift_orders = shift_assignments[shift_id]
shift_orders:
num_waves_per_shift = (, (shift[] / ))
orders_per_wave = (shift_orders) // num_waves_per_shift
w (num_waves_per_shift):
start_idx = w * orders_per_wave
end_idx = start_idx + orders_per_wave w < num_waves_per_shift - (shift_orders)
wave_orders = shift_orders[start_idx:end_idx]
wave_lines = (
orders.loc[orders[] == o, ].values[]
o wave_orders
)
all_waves.append({
: wave_counter,
: shift_id,
: w + ,
: wave_orders,
: (wave_orders),
: wave_lines
})
wave_counter +=
pd.DataFrame(all_waves)
shifts = [
{: , : datetime(,,,,),
: datetime(,,,,), : },
{: , : datetime(,,,,),
: datetime(,,,,), : },
{: , : datetime(,,,,),
: datetime(,,,,), : }
]
pickers = {: , : , : }
multi_shift_waves = plan_multi_shift_waves(orders.head(), shifts, pickers)
()
(multi_shift_waves.groupby()[[, ]].())
Tools & Libraries
Wave Management Software
Warehouse Management Systems:
- Manhattan WMS: Advanced wave planning and optimization
- Blue Yonder (JDA) WMS: AI-driven wave management
- SAP EWM: Wave templates and dynamic release
- HighJump WMS: Configurable wave strategies
- Körber WMS: Multi-wave parallel processing
Order Management Systems:
- IBM Sterling OMS: Wave release and order orchestration
- Fluent Commerce: Real-time order promising and waving
- Radial OMS: Distributed order management with waving
Python Libraries
from pulp import *
from ortools.sat.python import cp_model
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
Common Challenges & Solutions
Challenge: Cutoff Time Pressure
Problem:
- Orders arrive late, need same-day ship
- Wave released too early misses late orders
- Wave released too late misses carrier pickup
Solutions:
- Multiple cutoff waves (e.g., 12pm, 2pm, 4pm)
- Express wave for urgent orders (small, frequent)
- Dynamic wave release triggered at 80% threshold
- Pre-stage high-probability orders
- Negotiate later carrier pickups
- Use last-mile carriers with flexible schedules
Challenge: Workload Imbalance
Problem:
- Some zones finish early, others late
- Pickers idle while others overwhelmed
- Bottlenecks at packing stations
Solutions:
- Balance waves across zones mathematically
- Cross-train pickers to work multiple zones
- Dynamic picker redeployment mid-wave
- Adjust wave size by zone capacity
- Use pick-to-light or goods-to-person (balanced automatically)
- Monitor real-time progress, rebalance next wave
Challenge: Order Profile Variability
Problem:
- Mix of single-line and 50-line orders
- Some days 500 orders, some days 2000
- Seasonal peaks disrupt standard waves
Solutions:
- Separate waves by order complexity (each vs. case)
- Variable wave size (200-600 lines, not fixed)
- Reserve capacity for large orders
- Pre-pick high-velocity items during off-peak
- Hire temp labor for peaks
- Adjust wave frequency dynamically (not fixed schedule)
Challenge: Equipment Constraints
Problem:
- Conveyor at capacity (max 500 units/hour)
- Sorter can't handle wave volume
- Packing stations become bottleneck
Solutions:
- Wave size limited by downstream capacity
- Stagger wave releases (15-min offset between zones)
- Use wave pools (release to picking, but meter to packing)
- Add surge capacity (temporary packing stations)
- Batch packing for same customer/carrier
- Upgrade equipment or add parallel lines
Challenge: WMS Limitations
Problem:
- WMS only supports fixed wave sizes
- Can't auto-release based on thresholds
- Limited wave templates
- No cross-zone wave support
Solutions:
- Use middleware for advanced wave logic
- Manual monitoring with alert thresholds
- Pre-build wave templates for common scenarios
- Upgrade WMS or add bolt-on optimization
- Work with WMS vendor on custom logic
- Implement external optimization, feed results to WMS
Output Format
Wave Planning Report
Daily Wave Schedule - January 15, 2024
| Wave | Start Time | Lines | Orders | Zones | Est. Duration | Cutoff Met | Status |
|---|
| W01 | 06:00 | 420 | 87 | A,B,C | 3.2 hrs | 12pm | Complete |
| W02 | 09:00 | 385 | 96 | A,B,C | 2.9 hrs | 12pm | In Progress |
| W03 | 12:00 | 510 | 102 | A,B,C | 3.8 hrs | 4pm | Scheduled |
| W04 | 16:00 | 295 | 74 | A,B,C | 2.2 hrs | 6pm | Scheduled |
Wave W02 Details:
Orders: 96
Total Lines: 385
Average Lines/Order: 4.0
Zone Distribution:
Zone A: 145 lines (38%)
Zone B: 132 lines (34%)
Zone C: 108 lines (28%)
Pickers Assigned:
Zone A: 4 pickers → ~36 lines/picker
Zone B: 3 pickers → ~44 lines/picker
Zone C: 3 pickers → ~36 lines/picker
Expected Completion: 11:54 AM
Cutoff: 12:00 PM (6 min buffer)
Priority Orders: 12 (flagged for early pick)
Performance Summary:
| Metric | Target | Actual | Status |
|---|
| Waves per Day | 8-10 | 9 | ✓ On Target |
| Avg Wave Size | 350-450 | 403 | ✓ On Target |
| Balance (max-min zone) | <50 lines | 37 lines | ✓ On Target |
| Cutoff Adherence | >95% | 98% | ✓ On Target |
| Picker Utilization | 80-90% | 86% | ✓ On Target |
Recommendations:
- Wave 3 is largest - consider split if issues arise
- Zone B slightly higher workload in W02 - add 1 picker if available
- Suggest moving cutoff to 12:30pm for 30min buffer
Questions to Ask
If you need more context:
- What's your daily order volume (orders and lines)?
- How many pick waves do you currently run per day?
- What are your shipping cutoff times?
- How many warehouse zones and pickers per zone?
- What WMS do you use? Wave planning capabilities?
- What's your average picks per hour per picker?
- Any equipment bottlenecks (conveyor, sorter, packing)?
- Do you have priority or express orders?
Related Skills
- order-batching-optimization: For grouping orders within waves
- picker-routing-optimization: For optimizing pick paths within waves
- workforce-scheduling: For shift and labor planning
- task-assignment-problem: For assigning pickers to zones/waves
- warehouse-slotting-optimization: For SKU placement affecting pick efficiency
- order-fulfillment: For overall fulfillment process design
- capacity-planning: For long-term wave capacity planning