Project Scenario: GlobalParts Inc.
GlobalParts Inc. is a $2.1B industrial parts distributor operating 15 distribution centers across North America, managing 180,000 SKUs with 3,200 active suppliers. Their supply chain handles 45,000 order lines per day with a current fill rate of 91% (industry benchmark: 97%).
Supply Chain Challenges
- Fill Rate Gap: 91% vs. 97% benchmark — each 1% improvement = $4.2M recovered revenue
- Excess Inventory: $340M in slow-moving stock across DCs (18% of total inventory value)
- Demand Volatility: Post-pandemic demand swings of ±35% for key categories
- Manual Reordering: 80% of purchase orders created manually by 45 buyers using spreadsheets
- Blind Spots: No real-time visibility into in-transit inventory or warehouse conditions
Simulation Objectives
We'll build a Python simulation that models the key components of an intelligent supply chain, demonstrating how AI and IoT transform reactive operations into predictive, autonomous systems:
- IoT Sensor Layer: Generate realistic sensor data (temperature, humidity, vibration, fill levels)
- Demand Forecasting: Predict future demand using historical patterns and seasonality
- Inventory Optimization: Calculate optimal reorder points and quantities dynamically
- Autonomous Decisions: Auto-generate purchase orders when conditions are met, with safety guardrails
System Architecture
flowchart TB
subgraph Physical["Physical Layer"]
WH["Warehouses
(15 DCs)"]
SENS["IoT Sensors
(Temperature, Fill, Vibration)"]
FLEET["Fleet GPS
(In-Transit)"]
end
subgraph Digital["Digital Twin Layer"]
DT["Digital Twin
Engine"]
STATE["State Manager
(Real-time Snapshot)"]
SIM["What-If
Simulator"]
end
subgraph Intelligence["AI / ML Layer"]
DEMAND["Demand
Forecasting"]
OPTIM["Inventory
Optimizer"]
ANOMALY["Anomaly
Detection"]
end
subgraph Decision["Decision Layer"]
RULES["Business
Rules Engine"]
AUTO["Auto-Replenishment
Engine"]
ALERT["Alert &
Escalation"]
end
subgraph Output["Action Layer"]
PO["Purchase
Orders"]
TRANSFER["DC-to-DC
Transfers"]
DASH["Operations
Dashboard"]
end
SENS --> DT
FLEET --> DT
WH --> DT
DT --> STATE
STATE --> SIM
STATE --> DEMAND
STATE --> ANOMALY
DEMAND --> OPTIM
ANOMALY --> ALERT
OPTIM --> RULES
RULES --> AUTO
AUTO --> PO
AUTO --> TRANSFER
STATE --> DASH
style DT fill:#3B9797,color:#fff
style DEMAND fill:#16476A,color:#fff
style OPTIM fill:#16476A,color:#fff
style AUTO fill:#BF092F,color:#fff
style STATE fill:#132440,color:#fff
Digital Twin Data Flow
sequenceDiagram
participant Sensor as IoT Sensor
participant Stream as Event Stream
participant Twin as Digital Twin
participant ML as ML Models
participant Decision as Decision Engine
participant ERP as ERP System
Sensor->>Stream: Temperature: 4.2°C
Sensor->>Stream: Fill Level: 34%
Stream->>Twin: Update State
Twin->>Twin: Validate Against Thresholds
Twin->>ML: Request Forecast (current state)
ML->>Twin: Demand Forecast: 450 units/week
Twin->>Decision: State + Forecast Package
Decision->>Decision: Evaluate Rules
Decision->>ERP: Auto-Generate PO #4521
ERP-->>Decision: PO Confirmed
Decision->>Twin: Update Expected Receipts
IoT Sensor Simulation
The IoT layer generates realistic sensor events simulating warehouse conditions. Each sensor produces readings at configurable intervals with realistic noise, drift, and occasional anomalies.
"""
IoT Sensor Event Generator for Supply Chain Digital Twin.
Simulates temperature, humidity, fill-level, and vibration sensors.
"""
import random
import math
from datetime import datetime, timedelta
class IoTSensorSimulator:
"""Generates realistic IoT sensor events for warehouse monitoring."""
def __init__(self, warehouse_id, num_zones=4):
self.warehouse_id = warehouse_id
self.num_zones = num_zones
self.sensors = self._initialize_sensors()
self.anomaly_probability = 0.02 # 2% chance per reading
def _initialize_sensors(self):
"""Create sensor configurations for each warehouse zone."""
sensors = []
for zone in range(1, self.num_zones + 1):
sensors.extend([
{
"id": f"{self.warehouse_id}_Z{zone}_TEMP",
"type": "temperature",
"zone": zone,
"baseline": 18.0 + random.uniform(-2, 2),
"noise_std": 0.3,
"unit": "°C",
"alert_min": 2.0,
"alert_max": 30.0
},
{
"id": f"{self.warehouse_id}_Z{zone}_HUMID",
"type": "humidity",
"zone": zone,
"baseline": 45.0 + random.uniform(-5, 5),
"noise_std": 2.0,
"unit": "%RH",
"alert_min": 20.0,
"alert_max": 70.0
},
{
"id": f"{self.warehouse_id}_Z{zone}_FILL",
"type": "fill_level",
"zone": zone,
"baseline": 65.0 + random.uniform(-15, 15),
"noise_std": 1.5,
"unit": "%",
"alert_min": 15.0,
"alert_max": 95.0
}
])
return sensors
def generate_reading(self, sensor, timestamp):
"""Generate a single sensor reading with realistic noise."""
# Base value with daily cyclical pattern
hour = timestamp.hour
daily_cycle = math.sin(2 * math.pi * hour / 24) * 1.5
# Random noise
noise = random.gauss(0, sensor["noise_std"])
# Occasional anomaly injection
anomaly = 0
is_anomaly = False
if random.random() < self.anomaly_probability:
anomaly = random.choice([-1, 1]) * random.uniform(5, 15)
is_anomaly = True
value = sensor["baseline"] + daily_cycle + noise + anomaly
value = round(value, 2)
# Check alert thresholds
alert = None
if value < sensor["alert_min"]:
alert = "LOW"
elif value > sensor["alert_max"]:
alert = "HIGH"
return {
"sensor_id": sensor["id"],
"warehouse": self.warehouse_id,
"zone": sensor["zone"],
"type": sensor["type"],
"value": value,
"unit": sensor["unit"],
"timestamp": timestamp.isoformat(),
"is_anomaly": is_anomaly,
"alert": alert
}
def generate_batch(self, duration_minutes=60, interval_seconds=30):
"""Generate a batch of sensor readings over a time period."""
events = []
start = datetime(2026, 4, 30, 8, 0, 0)
num_readings = (duration_minutes * 60) // interval_seconds
for i in range(num_readings):
ts = start + timedelta(seconds=i * interval_seconds)
for sensor in self.sensors:
events.append(self.generate_reading(sensor, ts))
return events
# === Demo: Generate 1 hour of sensor data ===
simulator = IoTSensorSimulator("DC_CHICAGO", num_zones=3)
events = simulator.generate_batch(duration_minutes=60, interval_seconds=60)
print(f"=== IoT SENSOR SIMULATION: DC_CHICAGO ===")
print(f"Sensors: {len(simulator.sensors)} | Events generated: {len(events)}\n")
# Show sample and statistics
anomalies = [e for e in events if e["is_anomaly"]]
alerts = [e for e in events if e["alert"] is not None]
print(f"Anomalies detected: {len(anomalies)}")
print(f"Alert conditions: {len(alerts)}\n")
print("Sample readings (first 5):")
for event in events[:5]:
status = f" ⚠️ ALERT:{event['alert']}" if event["alert"] else ""
status += " 🔴 ANOMALY" if event["is_anomaly"] else ""
print(f" [{event['timestamp'][-8:]}] {event['sensor_id']}: "
f"{event['value']}{event['unit']}{status}")
Anomaly Detection
Demand Forecasting
The demand forecasting model predicts future demand for each SKU using historical sales patterns. We implement a simple but effective approach: linear regression with seasonal decomposition, suitable for the stable demand patterns typical of industrial parts.
"""
Demand Forecasting with Linear Regression + Seasonality.
Predicts weekly demand for inventory optimization.
"""
import math
import random
class DemandForecaster:
"""Simple linear regression forecaster with seasonal adjustment."""
def __init__(self, sku_id, base_demand=100, trend=0.5, seasonality_amplitude=20):
self.sku_id = sku_id
self.base_demand = base_demand
self.trend = trend # Units per week increase
self.seasonality_amplitude = seasonality_amplitude
self.noise_std = base_demand * 0.1 # 10% noise
def generate_historical(self, weeks=52):
"""Generate 52 weeks of historical demand data."""
history = []
for week in range(weeks):
# Trend component
trend_val = self.base_demand + self.trend * week
# Seasonal component (annual cycle)
seasonal = self.seasonality_amplitude * math.sin(
2 * math.pi * week / 52
)
# Random noise
noise = random.gauss(0, self.noise_std)
demand = max(0, round(trend_val + seasonal + noise))
history.append({
"week": week + 1,
"demand": demand,
"trend": round(trend_val, 1),
"seasonal": round(seasonal, 1)
})
return history
def fit_linear_regression(self, history):
"""Fit simple linear regression: demand = a + b*week."""
n = len(history)
x = [h["week"] for h in history]
y = [h["demand"] for h in history]
# Calculate regression coefficients
x_mean = sum(x) / n
y_mean = sum(y) / n
numerator = sum((x[i] - x_mean) * (y[i] - y_mean) for i in range(n))
denominator = sum((x[i] - x_mean) ** 2 for i in range(n))
b = numerator / denominator # Slope
a = y_mean - b * x_mean # Intercept
# R-squared
y_pred = [a + b * x[i] for i in range(n)]
ss_res = sum((y[i] - y_pred[i]) ** 2 for i in range(n))
ss_tot = sum((y[i] - y_mean) ** 2 for i in range(n))
r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
return {"intercept": a, "slope": b, "r_squared": r_squared}
def forecast(self, model, weeks_ahead=8, current_week=52):
"""Generate demand forecast for future weeks."""
forecasts = []
for i in range(1, weeks_ahead + 1):
week = current_week + i
# Linear prediction
point_forecast = model["intercept"] + model["slope"] * week
# Add seasonal adjustment
seasonal = self.seasonality_amplitude * math.sin(
2 * math.pi * week / 52
)
adjusted_forecast = point_forecast + seasonal
# Confidence interval (±2σ)
lower = max(0, adjusted_forecast - 2 * self.noise_std)
upper = adjusted_forecast + 2 * self.noise_std
forecasts.append({
"week": week,
"forecast": round(adjusted_forecast),
"lower_bound": round(lower),
"upper_bound": round(upper)
})
return forecasts
# === Demo: Forecast demand for SKU-7842 ===
random.seed(42)
forecaster = DemandForecaster(
sku_id="SKU-7842",
base_demand=120,
trend=0.8,
seasonality_amplitude=25
)
# Generate and fit
history = forecaster.generate_historical(weeks=52)
model = forecaster.fit_linear_regression(history)
print(f"=== DEMAND FORECAST: SKU-7842 ===\n")
print(f"Historical data: {len(history)} weeks")
print(f"Model: demand = {model['intercept']:.1f} + {model['slope']:.2f} × week")
print(f"R²: {model['r_squared']:.4f}\n")
# Generate 8-week forecast
forecast = forecaster.forecast(model, weeks_ahead=8)
print("8-Week Demand Forecast:")
print(f"{'Week':<6} {'Forecast':>9} {'Lower':>8} {'Upper':>8}")
print("-" * 35)
for f in forecast:
print(f" W{f['week']:<4} {f['forecast']:>7} units "
f"[{f['lower_bound']}, {f['upper_bound']}]")
total_forecast = sum(f["forecast"] for f in forecast)
print(f"\n{'Total 8-week demand forecast:':<32} {total_forecast} units")
print(f"{'Average weekly demand:':<32} {total_forecast // 8} units")
Seasonality Handling
The forecasting model uses sinusoidal seasonal decomposition:
$$\hat{D}(t) = \underbrace{\alpha + \beta t}_{\text{Linear Trend}} + \underbrace{A \sin\left(\frac{2\pi t}{52}\right)}_{\text{Seasonal Component}} + \epsilon$$
Where $A$ is the seasonality amplitude, $t$ is the week number, and $\epsilon \sim \mathcal{N}(0, \sigma^2)$ is Gaussian noise.
Inventory Optimization
The inventory optimizer converts demand forecasts into actionable reorder decisions. It calculates Reorder Points (ROP) and Economic Order Quantities (EOQ) that balance holding costs against stockout risk.
"""
Supply Chain Inventory Optimization Simulation.
Simulates daily operations with autonomous reorder decisions.
"""
import math
import random
class InventoryOptimizer:
"""Calculates optimal inventory parameters and simulates operations."""
def __init__(self, sku_config):
self.sku_id = sku_config["sku_id"]
self.avg_daily_demand = sku_config["avg_daily_demand"]
self.demand_std = sku_config["demand_std"]
self.lead_time_days = sku_config["lead_time_days"]
self.lead_time_std = sku_config.get("lead_time_std", 1)
self.unit_cost = sku_config["unit_cost"]
self.holding_cost_pct = sku_config.get("holding_cost_pct", 0.25)
self.order_cost = sku_config.get("order_cost", 150)
self.service_level = sku_config.get("service_level", 0.97)
def calculate_safety_stock(self):
"""Safety stock using probabilistic model."""
# Z-score for target service level (97% → Z ≈ 1.88)
z_scores = {0.90: 1.28, 0.95: 1.65, 0.97: 1.88, 0.99: 2.33}
z = z_scores.get(self.service_level, 1.88)
# Safety stock = Z × √(LT × σ_d² + d² × σ_LT²)
variance = (self.lead_time_days * self.demand_std**2 +
self.avg_daily_demand**2 * self.lead_time_std**2)
safety_stock = z * math.sqrt(variance)
return round(safety_stock)
def calculate_reorder_point(self):
"""ROP = average demand during lead time + safety stock."""
avg_demand_during_lt = self.avg_daily_demand * self.lead_time_days
safety_stock = self.calculate_safety_stock()
return round(avg_demand_during_lt + safety_stock)
def calculate_eoq(self):
"""Economic Order Quantity (Wilson formula)."""
annual_demand = self.avg_daily_demand * 365
holding_cost = self.unit_cost * self.holding_cost_pct
eoq = math.sqrt(
(2 * annual_demand * self.order_cost) / holding_cost
)
return round(eoq)
def simulate_operations(self, days=90):
"""Simulate daily inventory operations with auto-reorder."""
rop = self.calculate_reorder_point()
eoq = self.calculate_eoq()
safety_stock = self.calculate_safety_stock()
# Initial state
inventory = rop + eoq // 2 # Start above ROP
pending_orders = [] # (arrival_day, quantity)
results = []
total_demand = 0
total_fulfilled = 0
stockout_days = 0
orders_placed = 0
for day in range(1, days + 1):
# Receive incoming orders
arrivals = [o for o in pending_orders if o[0] == day]
for arrival in arrivals:
inventory += arrival[1]
pending_orders = [o for o in pending_orders if o[0] > day]
# Generate daily demand (normal distribution, clipped to 0)
demand = max(0, round(random.gauss(
self.avg_daily_demand, self.demand_std
)))
total_demand += demand
# Fulfill demand
fulfilled = min(demand, inventory)
total_fulfilled += fulfilled
inventory -= fulfilled
if fulfilled < demand:
stockout_days += 1
# Auto-reorder decision
ordered = False
if inventory <= rop and not any(o for o in pending_orders):
# Place order with variable lead time
lt = max(1, round(random.gauss(
self.lead_time_days, self.lead_time_std
)))
pending_orders.append((day + lt, eoq))
orders_placed += 1
ordered = True
results.append({
"day": day,
"demand": demand,
"fulfilled": fulfilled,
"inventory": inventory,
"ordered": ordered
})
# Calculate metrics
fill_rate = total_fulfilled / total_demand if total_demand > 0 else 1.0
avg_inventory = sum(r["inventory"] for r in results) / days
inventory_turns = (total_demand * 365 / days) / avg_inventory if avg_inventory > 0 else 0
return {
"sku_id": self.sku_id,
"parameters": {
"reorder_point": rop,
"eoq": eoq,
"safety_stock": safety_stock
},
"metrics": {
"fill_rate": round(fill_rate, 4),
"stockout_days": stockout_days,
"orders_placed": orders_placed,
"avg_inventory": round(avg_inventory),
"inventory_turns": round(inventory_turns, 1),
"total_demand": total_demand,
"total_fulfilled": total_fulfilled
},
"daily_results": results
}
# === Demo: Simulate 90 days for 3 SKUs ===
random.seed(123)
skus = [
{"sku_id": "SKU-7842", "avg_daily_demand": 18, "demand_std": 5,
"lead_time_days": 7, "lead_time_std": 2, "unit_cost": 45.00,
"service_level": 0.97},
{"sku_id": "SKU-2091", "avg_daily_demand": 45, "demand_std": 12,
"lead_time_days": 4, "lead_time_std": 1, "unit_cost": 12.50,
"service_level": 0.95},
{"sku_id": "SKU-5563", "avg_daily_demand": 8, "demand_std": 3,
"lead_time_days": 14, "lead_time_std": 3, "unit_cost": 220.00,
"service_level": 0.99}
]
print("=== SUPPLY CHAIN INVENTORY SIMULATION (90 Days) ===\n")
print(f"{'SKU':<12} {'ROP':>5} {'EOQ':>5} {'SS':>5} | "
f"{'Fill%':>6} {'Stockouts':>9} {'Orders':>7} {'Turns':>6}")
print("-" * 72)
for config in skus:
optimizer = InventoryOptimizer(config)
result = optimizer.simulate_operations(days=90)
p = result["parameters"]
m = result["metrics"]
fill_ok = "✓" if m["fill_rate"] >= config["service_level"] else "✗"
print(f"{config['sku_id']:<12} {p['reorder_point']:>5} {p['eoq']:>5} "
f"{p['safety_stock']:>5} | {m['fill_rate']:>5.1%} {fill_ok} "
f"{m['stockout_days']:>6}d {m['orders_placed']:>5} "
f"{m['inventory_turns']:>5.1f}")
print(f"\nTarget service levels: SKU-7842=97%, SKU-2091=95%, SKU-5563=99%")
Autonomous Decision Framework
The autonomous replenishment engine converts optimization outputs into actionable purchase orders without human intervention. However, it operates within carefully defined guardrails that escalate edge cases to human buyers.
Safety Guardrails
Autonomous Decision Tiers
| Tier | Condition | Action | Human Review |
|---|---|---|---|
| Auto-Execute | Standard reorder within EOQ ±20%, existing supplier | Place PO immediately | None (audit log only) |
| Auto-Propose | Order value > $50K, or new supplier, or quantity > 2× EOQ | Draft PO + notify buyer | 24-hour approval window |
| Escalate | Supplier lead time anomaly, demand spike > 3σ, budget threshold | Alert + recommendation | Mandatory before action |
| Halt | System anomaly, data quality issue, model drift detected | Freeze auto-ordering | Engineering investigation |
Conclusion & Simulation Results
This capstone built a complete intelligent supply chain simulation demonstrating how AI, IoT, and autonomous decisions transform traditional supply chain operations:
- IoT Sensor Layer: Generated realistic sensor data for 12 sensors across 3 warehouse zones, with anomaly injection and threshold alerting
- Demand Forecasting: Linear regression with seasonal decomposition achieving strong fit on simulated industrial demand patterns
- Inventory Optimization: Probabilistic safety stock + EOQ calculation maintaining 97%+ fill rates across diverse SKU profiles
- Autonomous Decisions: 4-tier guardrail framework ensuring safe automation with appropriate human oversight for edge cases