Back to Digital Transformation Series

Capstone: Simulate a Real-Time Intelligent Supply Chain

April 30, 2026 Wasil Zafar 22 min read

Build a simulation of an AI-powered supply chain for GlobalParts Inc. — featuring digital twins, IoT sensor networks, demand forecasting, inventory optimization, and autonomous replenishment decisions. This capstone integrates concepts from Parts 5, 7, 13, and 20.

Table of Contents

  1. Project Scenario
  2. System Architecture
  3. IoT Sensor Simulation
  4. Demand Forecasting
  5. Inventory Optimization
  6. Autonomous Decisions
  7. Conclusion & Results

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%).

Company Profile Revenue: $2.1B | DCs: 15 | SKUs: 180K
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
Distribution B2B IoT

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:

  1. IoT Sensor Layer: Generate realistic sensor data (temperature, humidity, vibration, fill levels)
  2. Demand Forecasting: Predict future demand using historical patterns and seasonality
  3. Inventory Optimization: Calculate optimal reorder points and quantities dynamically
  4. Autonomous Decisions: Auto-generate purchase orders when conditions are met, with safety guardrails

System Architecture

Intelligent Supply Chain Simulation 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

Digital Twin Synchronization Pattern
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

Anomaly Detection Strategy: The simulation uses a simple threshold-based approach, but production systems layer three detection methods: (1) Statistical — Z-score > 3σ from rolling mean, (2) Pattern — deviation from expected daily/weekly cycle, (3) Contextual — normal value but wrong for current context (e.g., 25°C in a cold storage zone).

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.

Production Enhancement: This linear model works well for stable industrial demand. For volatile consumer goods, upgrade to: (1) Prophet for multiple seasonalities + holidays, (2) LSTM for complex non-linear patterns, (3) Ensemble combining statistical + ML models. The key insight: start simple, add complexity only when accuracy metrics demand it.

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

Decision Framework Autonomy Levels by Risk
Autonomous Decision Tiers
TierConditionActionHuman 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
Autonomy Guardrails Risk Tiers
Autonomy Principle: Start with Tier 1 (auto-execute for routine reorders only) and expand autonomy gradually as the system proves reliable. Most failures in autonomous systems come from expanding autonomy too fast before edge cases are understood. GlobalParts targets: 80% auto-execute, 15% auto-propose, 4% escalate, 1% halt.

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
Projected Impact: GlobalParts' simulation predicts: fill rate improvement from 91% → 97.3% ($25M recovered revenue), inventory reduction of $48M (14% of excess stock), and buyer productivity gain of 65% (from 45 manual buyers to 16 exception handlers). Combined annual benefit: $31M against $8M system investment.
Series Complete: You've completed all 4 capstone projects, integrating concepts across the entire 20-part Digital Transformation series. From enterprise architecture blueprints to intelligent supply chain simulations, these projects demonstrate how modern organizations combine strategy, technology, data, and AI into coherent transformation programs. Return to the Series Hub to revisit any part or capstone.