Restaurant Simulation with SimPy: From Seating to Serving

Restaurants are fascinating systems. Multiple resources. Parallel processes. Variable service times. Customer behaviour. All in one simulation.

The Restaurant Model

Components: - Customers - Arrive in groups - Tables - Limited seating - Waiters - Take orders, serve food - Kitchen - Prepares meals - Bill - Payment process

Basic Restaurant

import simpy
import random

class Restaurant:
    def __init__(self, env, num_tables, num_waiters, num_cooks):
        self.env = env
        self.tables = simpy.Resource(env, capacity=num_tables)
        self.waiters = simpy.Resource(env, capacity=num_waiters)
        self.kitchen = simpy.Resource(env, capacity=num_cooks)
        self.stats = []

    def customer_group(self, group_id, group_size):
        arrival = self.env.now

        # Wait for table
        with self.tables.request() as table:
            yield table
            seated = self.env.now

            # Waiter takes order
            with self.waiters.request() as waiter:
                yield waiter
                yield self.env.timeout(random.uniform(2, 5))  # Ordering

            # Kitchen prepares food
            with self.kitchen.request() as cook:
                yield cook
                yield self.env.timeout(random.uniform(10, 25))  # Cooking

            # Waiter serves food
            with self.waiters.request() as waiter:
                yield waiter
                yield self.env.timeout(1)  # Serving

            # Eating
            yield self.env.timeout(random.uniform(20, 40))

            # Waiter brings bill
            with self.waiters.request() as waiter:
                yield waiter
                yield self.env.timeout(random.uniform(3, 8))  # Bill and payment

        departure = self.env.now
        self.stats.append({
            'group': group_id,
            'size': group_size,
            'arrival': arrival,
            'seated': seated,
            'departure': departure,
            'wait_for_table': seated - arrival,
            'total_time': departure - arrival
        })

def arrivals(env, restaurant):
    group_id = 0
    while True:
        yield env.timeout(random.expovariate(1/10))  # ~6 groups/hour
        group_size = random.choices([1, 2, 3, 4, 5, 6], weights=[10, 30, 25, 20, 10, 5])[0]
        env.process(restaurant.customer_group(group_id, group_size))
        group_id += 1

env = simpy.Environment()
restaurant = Restaurant(env, num_tables=10, num_waiters=3, num_cooks=2)
env.process(arrivals(env, restaurant))
env.run(until=240)  # 4 hours

Table Management

Different table sizes:

class TableManager:
    def __init__(self, env, table_config):
        """
        table_config = {2: 5, 4: 8, 6: 3}  # 5 two-tops, 8 four-tops, 3 six-tops
        """
        self.env = env
        self.tables = {}
        for size, count in table_config.items():
            self.tables[size] = simpy.Resource(env, capacity=count)

    def get_table(self, group_size):
        """Find smallest table that fits the group."""
        suitable = [s for s in sorted(self.tables.keys()) if s >= group_size]

        if not suitable:
            return None  # No table big enough

        for size in suitable:
            if self.tables[size].count < self.tables[size].capacity:
                return self.tables[size], size

        # All suitable tables busy - wait for smallest that fits
        return self.tables[suitable[0]], suitable[0]

def customer_group(env, table_manager, group_id, group_size):
    result = table_manager.get_table(group_size)
    if result is None:
        print(f"Group {group_id} (size {group_size}) left - no suitable table")
        return

    table_resource, table_size = result

    with table_resource.request() as req:
        yield req
        print(f"Group {group_id} seated at {table_size}-top")
        yield env.timeout(random.uniform(30, 60))  # Dining time

Kitchen with Multiple Stations

class Kitchen:
    def __init__(self, env, config):
        self.env = env
        self.stations = {
            'grill': simpy.Resource(env, capacity=config.get('grill', 2)),
            'fryer': simpy.Resource(env, capacity=config.get('fryer', 1)),
            'salad': simpy.Resource(env, capacity=config.get('salad', 1)),
            'dessert': simpy.Resource(env, capacity=config.get('dessert', 1))
        }
        self.orders_completed = 0

    def prepare_order(self, order):
        """Prepare all items in an order."""
        # Group items by station
        station_items = {}
        for item in order['items']:
            station = item['station']
            if station not in station_items:
                station_items[station] = []
            station_items[station].append(item)

        # Prepare at each station (can be parallel across stations)
        processes = []
        for station, items in station_items.items():
            proc = self.env.process(self.prepare_at_station(station, items))
            processes.append(proc)

        # Wait for all stations to complete
        for proc in processes:
            yield proc

        self.orders_completed += 1

    def prepare_at_station(self, station_name, items):
        with self.stations[station_name].request() as req:
            yield req
            for item in items:
                yield self.env.timeout(item['prep_time'])

Order Queue

class OrderSystem:
    def __init__(self, env, kitchen):
        self.env = env
        self.kitchen = kitchen
        self.order_queue = simpy.Store(env)
        self.ready_orders = simpy.FilterStore(env)
        env.process(self.kitchen_process())

    def place_order(self, table_id, items):
        order = {
            'id': f"{table_id}_{self.env.now:.0f}",
            'table': table_id,
            'items': items,
            'placed_at': self.env.now
        }
        yield self.order_queue.put(order)
        return order['id']

    def kitchen_process(self):
        while True:
            order = yield self.order_queue.get()
            yield from self.kitchen.prepare_order(order)
            order['ready_at'] = self.env.now
            yield self.ready_orders.put(order)

    def collect_order(self, order_id):
        order = yield self.ready_orders.get(
            lambda o: o['id'] == order_id
        )
        return order

Complete Restaurant Simulation

import simpy
import random
import numpy as np

class FullRestaurantSimulation:
    def __init__(self, env, config):
        self.env = env
        self.config = config

        # Resources
        self.tables = simpy.Resource(env, capacity=config['tables'])
        self.waiters = simpy.Resource(env, capacity=config['waiters'])
        self.kitchen = simpy.Resource(env, capacity=config['cooks'])

        # Stats
        self.stats = {
            'groups': [],
            'turnaways': 0
        }

    def customer_group(self, group_id, group_size):
        arrival = self.env.now

        # Check if willing to wait
        if len(self.tables.queue) > self.config['max_wait_queue']:
            self.stats['turnaways'] += 1
            return

        record = {
            'id': group_id,
            'size': group_size,
            'arrival': arrival
        }

        # Get table
        with self.tables.request() as table:
            result = yield table | self.env.timeout(self.config['max_wait_time'])
            if table not in result:
                self.stats['turnaways'] += 1
                return

            record['seated_at'] = self.env.now
            record['wait_for_table'] = self.env.now - arrival

            # Order
            with self.waiters.request() as waiter:
                yield waiter
                yield self.env.timeout(random.uniform(3, 6))
            record['ordered_at'] = self.env.now

            # Cook
            with self.kitchen.request() as cook:
                yield cook
                cook_time = self.config['cook_time_base'] + group_size * 2
                yield self.env.timeout(random.uniform(cook_time * 0.8, cook_time * 1.2))
            record['food_ready'] = self.env.now

            # Serve
            with self.waiters.request() as waiter:
                yield waiter
                yield self.env.timeout(random.uniform(1, 2))

            # Eat
            eat_time = random.uniform(20, 40)
            yield self.env.timeout(eat_time)

            # Bill
            with self.waiters.request() as waiter:
                yield waiter
                yield self.env.timeout(random.uniform(3, 8))

        record['departure'] = self.env.now
        record['total_time'] = self.env.now - arrival
        self.stats['groups'].append(record)

    def arrivals(self):
        group_id = 0
        while True:
            # Time-varying arrivals
            hour = (self.env.now / 60) % 24
            if 12 <= hour <= 14 or 18 <= hour <= 21:
                rate = self.config['peak_rate']
            else:
                rate = self.config['off_peak_rate']

            yield self.env.timeout(random.expovariate(rate))

            # Group size distribution
            sizes = [1, 2, 3, 4, 5, 6]
            probs = [0.10, 0.35, 0.25, 0.20, 0.07, 0.03]
            group_size = random.choices(sizes, weights=probs)[0]

            self.env.process(self.customer_group(group_id, group_size))
            group_id += 1

    def run(self, duration):
        self.env.process(self.arrivals())
        self.env.run(until=duration)

    def report(self):
        groups = self.stats['groups']

        print("\n=== Restaurant Report ===")
        print(f"Groups served: {len(groups)}")
        print(f"Groups turned away: {self.stats['turnaways']}")

        if groups:
            waits = [g['wait_for_table'] for g in groups]
            totals = [g['total_time'] for g in groups]

            print(f"\nWait for table:")
            print(f"  Mean: {np.mean(waits):.1f} min")
            print(f"  Max: {max(waits):.1f} min")

            print(f"\nTotal time in restaurant:")
            print(f"  Mean: {np.mean(totals):.1f} min")

            # Table turnover
            turnover = len(groups) / (self.config['tables'] * (self.env.now / 60))
            print(f"\nTable turnover: {turnover:.2f} per hour")

# Config
config = {
    'tables': 15,
    'waiters': 4,
    'cooks': 3,
    'peak_rate': 0.2,        # Groups per minute
    'off_peak_rate': 0.05,
    'max_wait_queue': 5,
    'max_wait_time': 15,
    'cook_time_base': 12
}

random.seed(42)
env = simpy.Environment()
sim = FullRestaurantSimulation(env, config)
sim.run(duration=240)  # 4 hours
sim.report()

Summary

Restaurant simulation teaches: - Multiple resource types - Sequential processes with parallel elements - Customer behaviour and patience - Time-varying demand - Table management optimisation

Every restaurant is a system. Simulate yours.

Next Steps


Build Professional Simulations

Break free from commercial software and learn how to build powerful, industry-standard simulations in Python. The Complete Simulation in Python with SimPy Bootcamp gives you everything you need.

Explore the Bootcamp