Hospital Simulation with SimPy: Modelling Patient Flow

Hospitals are complex systems. Patients arrive unpredictably. Resources are constrained. Lives depend on getting it right. Simulation helps.

The Hospital Model

Key elements: - Patients - Arrive with varying urgency - Staff - Doctors, nurses, technicians - Beds - Limited and precious - Departments - A&E, wards, theatres - Pathways - Patient journeys through the system

Emergency Department Simulation

import simpy
import random

class EmergencyDepartment:
    def __init__(self, env, config):
        self.env = env
        self.triage_nurses = simpy.Resource(env, config['triage_nurses'])
        self.doctors = simpy.PriorityResource(env, config['doctors'])
        self.beds = simpy.Resource(env, config['beds'])
        self.stats = {
            'patients': [],
            'wait_times': [],
            'length_of_stay': []
        }

    def patient_pathway(self, patient_id, severity):
        arrival = self.env.now

        # Priority: 1 = critical, 5 = minor
        priority_map = {'critical': 1, 'urgent': 2, 'standard': 3, 'minor': 4}
        priority = priority_map.get(severity, 3)

        # Triage
        with self.triage_nurses.request() as req:
            yield req
            yield self.env.timeout(random.uniform(3, 8))

        triage_complete = self.env.now

        # Wait for doctor (priority queue)
        with self.doctors.request(priority=priority) as req:
            yield req
            wait_for_doctor = self.env.now - triage_complete
            self.stats['wait_times'].append({
                'patient': patient_id,
                'severity': severity,
                'wait': wait_for_doctor
            })

            # Treatment time varies by severity
            treatment_times = {
                'critical': (30, 60),
                'urgent': (20, 45),
                'standard': (15, 30),
                'minor': (10, 20)
            }
            min_t, max_t = treatment_times.get(severity, (15, 30))
            yield self.env.timeout(random.uniform(min_t, max_t))

        # Some patients need admission
        admission_rates = {'critical': 0.8, 'urgent': 0.5, 'standard': 0.2, 'minor': 0.05}
        if random.random() < admission_rates.get(severity, 0.2):
            with self.beds.request() as req:
                yield req
                yield self.env.timeout(random.uniform(60, 240))  # Ward stay

        departure = self.env.now
        self.stats['length_of_stay'].append(departure - arrival)
        self.stats['patients'].append({
            'id': patient_id,
            'severity': severity,
            'arrival': arrival,
            'departure': departure,
            'los': departure - arrival
        })

def patient_arrivals(env, ed, arrival_rate):
    patient_id = 0
    severity_probs = [0.05, 0.15, 0.40, 0.40]  # critical, urgent, standard, minor
    severities = ['critical', 'urgent', 'standard', 'minor']

    while True:
        yield env.timeout(random.expovariate(arrival_rate))
        severity = random.choices(severities, weights=severity_probs)[0]
        env.process(ed.patient_pathway(patient_id, severity))
        patient_id += 1

# Run simulation
env = simpy.Environment()
ed = EmergencyDepartment(env, {
    'triage_nurses': 2,
    'doctors': 3,
    'beds': 20
})
env.process(patient_arrivals(env, ed, arrival_rate=1/10))  # ~6 per hour
env.run(until=480)  # 8-hour shift

Ward Model

class HospitalWard:
    def __init__(self, env, name, beds, nurses_per_shift):
        self.env = env
        self.name = name
        self.beds = simpy.Resource(env, capacity=beds)
        self.nurses = simpy.Resource(env, capacity=nurses_per_shift)
        self.patients_discharged = 0

    def admit_patient(self, patient_id, expected_los):
        """Admit patient, occupy bed for length of stay."""
        with self.beds.request() as bed:
            yield bed
            print(f"Patient {patient_id} admitted to {self.name} at {self.env.now}")

            # Daily care rounds
            while self.env.now < expected_los:
                with self.nurses.request() as nurse:
                    yield nurse
                    yield self.env.timeout(random.uniform(10, 30))  # Care time
                yield self.env.timeout(random.uniform(60, 120))  # Time between rounds

        print(f"Patient {patient_id} discharged from {self.name} at {self.env.now}")
        self.patients_discharged += 1

Operating Theatre

class OperatingTheatre:
    def __init__(self, env, num_theatres):
        self.env = env
        self.theatres = simpy.Resource(env, capacity=num_theatres)
        self.surgeons = simpy.Resource(env, capacity=num_theatres)
        self.anaesthetists = simpy.Resource(env, capacity=num_theatres)
        self.surgeries_completed = 0

    def surgery(self, patient_id, surgery_type):
        """Perform surgery requiring theatre, surgeon, and anaesthetist."""
        surgery_times = {
            'minor': (30, 60),
            'major': (120, 240),
            'emergency': (60, 180)
        }
        min_t, max_t = surgery_times.get(surgery_type, (60, 120))

        # Need all three resources
        with self.theatres.request() as theatre:
            yield theatre
            with self.surgeons.request() as surgeon:
                yield surgeon
                with self.anaesthetists.request() as anaes:
                    yield anaes

                    # Pre-op
                    yield self.env.timeout(15)

                    # Surgery
                    yield self.env.timeout(random.uniform(min_t, max_t))

                    # Recovery (still in theatre)
                    yield self.env.timeout(30)

        self.surgeries_completed += 1
        print(f"Surgery for patient {patient_id} completed at {self.env.now}")

Clinic Appointments

class OutpatientClinic:
    def __init__(self, env, num_consultants, appointment_slots_per_hour):
        self.env = env
        self.consultants = simpy.Resource(env, capacity=num_consultants)
        self.scheduled_appointments = simpy.Store(env)
        self.patients_seen = 0
        self.no_shows = 0

    def schedule_appointment(self, patient_id, appointment_time):
        """Schedule an appointment."""
        yield self.scheduled_appointments.put({
            'patient_id': patient_id,
            'time': appointment_time
        })

    def run_clinic(self, start_time, end_time):
        """Run clinic for a session."""
        yield self.env.timeout(start_time - self.env.now)

        while self.env.now < end_time:
            try:
                appt = yield self.scheduled_appointments.get()

                # No-show probability
                if random.random() < 0.1:
                    self.no_shows += 1
                    continue

                with self.consultants.request() as req:
                    yield req
                    yield self.env.timeout(random.uniform(10, 25))
                    self.patients_seen += 1

            except simpy.Interrupt:
                break

Ambulance and A&E Integration

class AmbulanceService:
    def __init__(self, env, num_ambulances, hospital_ed):
        self.env = env
        self.ambulances = simpy.Resource(env, capacity=num_ambulances)
        self.hospital_ed = hospital_ed
        self.calls_responded = 0

    def respond_to_call(self, call_id, location_distance):
        with self.ambulances.request() as req:
            yield req

            # Travel to scene
            travel_time = location_distance / 30  # 30 km/h average
            yield self.env.timeout(travel_time)

            # On-scene time
            yield self.env.timeout(random.uniform(10, 30))

            # Travel to hospital
            yield self.env.timeout(travel_time)

            # Handover to ED
            severity = random.choice(['critical', 'urgent', 'standard'])
            self.env.process(
                self.hospital_ed.patient_pathway(f"amb_{call_id}", severity)
            )

            # Turnaround time
            yield self.env.timeout(15)

        self.calls_responded += 1

Complete Hospital Simulation

import simpy
import random
import pandas as pd

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

        # Resources
        self.ed_doctors = simpy.PriorityResource(env, config['ed_doctors'])
        self.ed_nurses = simpy.Resource(env, config['ed_nurses'])
        self.ed_beds = simpy.Resource(env, config['ed_beds'])
        self.ward_beds = simpy.Resource(env, config['ward_beds'])

        # Statistics
        self.patient_records = []

    def ed_patient(self, patient_id, severity, arrival_time):
        record = {
            'id': patient_id,
            'severity': severity,
            'arrival': arrival_time
        }

        # Triage
        with self.ed_nurses.request() as req:
            yield req
            yield self.env.timeout(random.uniform(5, 10))
        record['triage_complete'] = self.env.now

        # Doctor
        priority = {'critical': 1, 'urgent': 2, 'standard': 3, 'minor': 4}[severity]
        with self.ed_doctors.request(priority=priority) as req:
            yield req
            treatment = random.uniform(15, 60)
            yield self.env.timeout(treatment)
        record['treatment_complete'] = self.env.now

        # Admission decision
        admission_prob = {'critical': 0.7, 'urgent': 0.4, 'standard': 0.15, 'minor': 0.05}
        if random.random() < admission_prob[severity]:
            with self.ward_beds.request() as req:
                yield req
                los = random.uniform(24*60, 72*60)  # 1-3 days in minutes
                yield self.env.timeout(los)
            record['admitted'] = True
            record['ward_discharge'] = self.env.now
        else:
            record['admitted'] = False

        record['departure'] = self.env.now
        self.patient_records.append(record)

    def arrivals(self):
        patient_id = 0
        severities = ['critical', 'urgent', 'standard', 'minor']
        probs = [0.05, 0.20, 0.35, 0.40]

        while True:
            # Time-varying arrival rate
            hour = (self.env.now / 60) % 24
            if 8 <= hour <= 20:
                rate = 1/8  # Busier during day
            else:
                rate = 1/15  # Quieter at night

            yield self.env.timeout(random.expovariate(rate))
            severity = random.choices(severities, weights=probs)[0]
            self.env.process(self.ed_patient(patient_id, severity, self.env.now))
            patient_id += 1

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

    def analyse(self):
        df = pd.DataFrame(self.patient_records)
        df['wait_for_doctor'] = df['treatment_complete'] - df['triage_complete']
        df['ed_los'] = df['treatment_complete'] - df['arrival']

        print("\n=== Hospital Simulation Results ===")
        print(f"Total patients: {len(df)}")
        print(f"Admissions: {df['admitted'].sum()}")

        print("\nWait for doctor (minutes):")
        print(df.groupby('severity')['wait_for_doctor'].describe())

        return df

# Run
random.seed(42)
env = simpy.Environment()
hospital = Hospital(env, {
    'ed_doctors': 3,
    'ed_nurses': 4,
    'ed_beds': 15,
    'ward_beds': 50
})
hospital.run(duration=24*60)  # 24 hours
results = hospital.analyse()

Summary

Hospital simulation models: - Patient pathways through departments - Resource constraints (staff, beds, equipment) - Priority-based treatment (triage) - Admission and discharge dynamics - Time-varying demand patterns

Simulate to save lives. Optimise to save more.

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