Warm-Up Period in SimPy: Avoiding Initialisation Bias

Your simulation starts empty. Reality doesn't. This mismatch corrupts your results unless you handle it properly.

The Problem

At time 0, your simulation has: - Empty queues - Idle servers - No entities in transit

Reality has: - Existing queues - Busy servers - Work in progress

The early part of your simulation is unrepresentative. Including it in your statistics biases your results.

The Solution: Warm-Up Period

Run the simulation for a while before collecting statistics:

def run_simulation_with_warmup(warmup_time, collection_time):
    env = simpy.Environment()
    # ... setup

    # Phase 1: Warm-up (no statistics collection)
    env.run(until=warmup_time)

    # Reset statistics here
    stats.reset()

    # Phase 2: Data collection
    env.run(until=warmup_time + collection_time)

    return stats

Detecting When Warm-Up Is Complete

Visual Method (Welch's Method)

Plot the metric over time. Look for when it stabilises:

def plot_warmup_detection(time_series_df, metric='queue_length'):
    """Visual inspection for warm-up period."""
    plt.figure(figsize=(12, 5))
    plt.plot(time_series_df['time'], time_series_df[metric])
    plt.xlabel('Time')
    plt.ylabel(metric)
    plt.title('Identify where the metric stabilises')
    plt.show()

The warm-up period ends when the metric stops trending and fluctuates around a stable mean.

Moving Average Method

def detect_warmup_moving_average(time_series, window_size=50, threshold=0.05):
    """Detect warm-up using moving average stability."""
    ma = pd.Series(time_series).rolling(window=window_size).mean()

    # Calculate relative change in moving average
    changes = ma.diff().abs() / ma

    # Find where changes fall below threshold
    stable_index = (changes < threshold).idxmax()

    return stable_index

MSER (Marginal Standard Error Rule)

A more rigorous approach:

def mser_warmup(data, max_truncation=0.5):
    """
    Find optimal truncation point using MSER-5.

    Truncates where mean squared error is minimised.
    """
    n = len(data)
    max_d = int(n * max_truncation)

    best_d = 0
    best_mser = float('inf')

    for d in range(0, max_d, 5):  # MSER-5 uses batches of 5
        remaining = data[d:]
        if len(remaining) < 10:
            break
        mser = np.var(remaining, ddof=1) / len(remaining)
        if mser < best_mser:
            best_mser = mser
            best_d = d

    return best_d

Implementing Warm-Up in SimPy

Method 1: Reset Statistics

class Statistics:
    def __init__(self):
        self.wait_times = []
        self.collection_enabled = False

    def reset(self):
        self.wait_times = []
        self.collection_enabled = True

    def record_wait(self, wait):
        if self.collection_enabled:
            self.wait_times.append(wait)

# Usage
stats = Statistics()
env.run(until=warmup_time)
stats.reset()  # Start collecting
env.run(until=warmup_time + collection_time)

Method 2: Flag in Customer Process

def customer(env, resource, stats, warmup_complete_event):
    arrival = env.now

    with resource.request() as req:
        yield req
        wait = env.now - arrival
        yield env.timeout(service_time())

        # Only record if warm-up is complete
        if warmup_complete_event.triggered:
            stats.record_wait(wait)

# Set flag after warm-up
def warmup_controller(env, warmup_time, warmup_event):
    yield env.timeout(warmup_time)
    warmup_event.succeed()
    print(f"Warm-up complete at {env.now}")

env = simpy.Environment()
warmup_event = env.event()
env.process(warmup_controller(env, 100, warmup_event))

Method 3: Timestamp Filtering

Collect all data, filter later:

def analyse_results(all_stats, warmup_time):
    """Filter out warm-up period after simulation."""
    return [
        record for record in all_stats
        if record['arrival_time'] >= warmup_time
    ]

How Long Should Warm-Up Be?

Depends on your system. Heuristics:

  1. 5-10x the longest cycle time - Time for an entity to traverse the system
  2. Visual inspection - Plot and look
  3. Multiple runs - See when statistics stabilise
  4. Err on the long side - Better too much warm-up than bias
def estimate_warmup_length(pilot_run_stats, target_stability=0.05):
    """
    Estimate required warm-up based on pilot run.

    target_stability: maximum coefficient of variation in rolling mean
    """
    means = pd.Series(pilot_run_stats).expanding().mean()
    cv = means.std() / means.mean()

    # Find where CV stabilises
    for i, val in enumerate(cv):
        if val < target_stability:
            return i

    return len(pilot_run_stats)  # Didn't stabilise

Complete Example

import simpy
import random
import numpy as np
import matplotlib.pyplot as plt

class WarmupAwareSimulation:
    def __init__(self, warmup_time, collection_time):
        self.warmup_time = warmup_time
        self.collection_time = collection_time
        self.wait_times = []
        self.all_wait_times = []  # Including warm-up for analysis
        self.warmup_complete = False

    def run(self, seed=42):
        random.seed(seed)
        env = simpy.Environment()
        server = simpy.Resource(env, capacity=2)

        env.process(self.arrivals(env, server))
        env.process(self.warmup_controller(env))

        env.run(until=self.warmup_time + self.collection_time)

        return self.wait_times

    def warmup_controller(self, env):
        yield env.timeout(self.warmup_time)
        self.warmup_complete = True
        print(f"Warm-up complete at time {env.now}")

    def arrivals(self, env, server):
        i = 0
        while True:
            yield env.timeout(random.expovariate(1/5))
            env.process(self.customer(env, f"C{i}", server))
            i += 1

    def customer(self, env, name, server):
        arrival = env.now
        with server.request() as req:
            yield req
            wait = env.now - arrival
            yield env.timeout(random.expovariate(1/4))

            self.all_wait_times.append({'time': arrival, 'wait': wait})
            if self.warmup_complete:
                self.wait_times.append(wait)

    def plot_warmup_analysis(self):
        times = [r['time'] for r in self.all_wait_times]
        waits = [r['wait'] for r in self.all_wait_times]

        plt.figure(figsize=(12, 5))
        plt.scatter(times, waits, alpha=0.3, s=5)
        plt.axvline(self.warmup_time, color='red', linestyle='--',
                    label='Warm-up end')
        plt.xlabel('Arrival Time')
        plt.ylabel('Wait Time')
        plt.title('Wait Times Over Simulation')
        plt.legend()
        plt.show()

# Run
sim = WarmupAwareSimulation(warmup_time=200, collection_time=1000)
results = sim.run()
sim.plot_warmup_analysis()

print(f"Customers in warm-up: {len(sim.all_wait_times) - len(sim.wait_times)}")
print(f"Customers analysed: {len(sim.wait_times)}")
print(f"Mean wait (steady-state): {np.mean(sim.wait_times):.2f}")

Summary

Warm-up period: - Necessary to avoid initialisation bias - Detect visually or algorithmically - Reset statistics after warm-up completes - Err on the side of longer warm-up

Starting empty is not steady state. Don't pretend it is.

Next Steps


Discover the Power of Simulation

Want to become a go-to expert in simulation with Python? The Complete Simulation Bootcamp will show you how simulation can transform your career and your projects.

Explore the Bootcamp