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