A very simple example with one server

In this example, we’ll create a very simply SimPy model where only one server/resource is present.

In a later example with multiple servers, we’ll bring in vidigi’s custom classes for tracking resource use.

If you’re working with ciw, you may find it more useful to look at this cafe example.

Step 1. Import required libraries

  • vidigi
  • simpy - for simulation model (or see the Ciw functions and examples elsewhere in this documentation)
  • random - for generating random arrivals
  • pandas - for managing dataframes
import simpy
import pandas as pd
import random
from vidigi.animation import animate_activity_log
from vidigi.logging import EventLogger
from vidigi.utils import EventPosition, create_event_position_df

Step 2. Set up simulation parameters

# Simple simulation parameters
SIM_DURATION = 50
NUM_SERVERS = 1
ARRIVAL_RATE = 1.0
SERVICE_TIME = 3.0

Step 3. Write model code with event logs

Here we create a simple simulation model using simpy.

On the left is a basic simpy model. If you’re not familiar with this notation, check out the simpy documentation for an introduction to SimPy, or the DES book from the Health Service Modelling Associate programme.

On the right is how we incorporate vidigi.

We set up a logger using the EventLogger class. This then gives us methods for recording arrivals, departures and queueing steps.

Every entity must have a single arrival and a single departure event. If this isn’t true, unexpected behaviour will occur in the animation.

They may have as many queueing events as desired (and they don’t have to just be what you’re traditionally think of as a ‘queue’ - they can be any point where an individual is waiting for something or doing something).

We can also record the points at which an entity starts using/blocking a resource/server, and when this ends. For this we also need to pass in an identifier that tracks which resource they are using. Here we only have 1 resource, so this will always be 1 - for more complex examples, we can use vidigi’s custom resource classes, which expose this detail, and are covered in the next chapter.

Simple SimPy Model

def patient_generator(env, server, event_log):
    """Generate patients arriving"""
    patient_id = 0

    while True:
        patient_id += 1

        # Start the patient process
        env.process(patient_process(env, patient_id, server, event_log))

        # Wait for next arrival
        yield env.timeout(random.expovariate(ARRIVAL_RATE))

def patient_process(env, patient_id, server, event_log):
    """Process a single patient through the system"""

    # Request server
    with server.request() as request:
        yield request

        # Service time
        service_duration = random.expovariate(1.0/SERVICE_TIME)
        yield env.timeout(service_duration)

# Run the simulation
def run_simulation():
    env = simpy.Environment()
    server = simpy.Resource(env, capacity=NUM_SERVERS)
    event_log = []

    # Start patient generator
    env.process(patient_generator(env, server, event_log))

    # Run simulation
    env.run(until=SIM_DURATION)
With Vidigi Modifications
def patient_generator(env, server, logger):
    """Generate patients arriving"""
    patient_id = 0

    while True:
        patient_id += 1

        # Log arrival
        logger.log_arrival(entity_id=patient_id) 

        # Start the patient process
        env.process(patient_process(env, patient_id, server, logger))

        # Wait for next arrival
        yield env.timeout(random.expovariate(ARRIVAL_RATE))

def patient_process(env, patient_id, server, logger):
    """Process a single patient through the system"""

    # Log start of queue wait 
    logger.log_queue(entity_id=patient_id, event='queue_wait_begins') 

    # Request server
    with server.request() as request:
        yield request

        # Log service start
        logger.log_resource_use_start(entity_id=patient_id, event="service_begins", resource_id=1) 

        # Service time
        service_duration = random.expovariate(1.0/SERVICE_TIME)
        yield env.timeout(service_duration)

        # Log service start
        logger.log_resource_use_end(entity_id=patient_id, event="service_complete", resource_id=1) 

    # Log departure 
    logger.log_departure(entity_id=patient_id)  

# Run the simulation
def run_simulation():
    env = simpy.Environment()
    server = simpy.Resource(env, capacity=NUM_SERVERS)
    logger = EventLogger(env=env) 

    # Start patient generator
    env.process(patient_generator(env, server, logger))

    # Run simulation
    env.run(until=SIM_DURATION)

    return logger.to_dataframe() 

Step 4. Run simulation

# Run simulation and get event log
event_log_df = run_simulation()
print(f"Generated {len(event_log_df)} events")
Generated 124 events

Step 5. Create event positions dataframe

We need to tell vidigi where the bottom-right-hand corner of each queue should be on the page.

This gives it a starting point to work from for laying out the events within the animation canvas. We provide coordinates to it to do this.

# Define positions for animation
event_positions = create_event_position_df([
    EventPosition(event='arrival', x=0, y=350, label="Entrance"),
    EventPosition(event='queue_wait_begins', x=250, y=250, label="Queue"),
    EventPosition(event='service_begins', x=250, y=150, resource='server', label="Being Served"),
    EventPosition(event='depart', x=250, y=50, label="Exit")
])

Step 6. Create animation

Finally, we create our animation.

  • plotly_height and/or plotly_width set the actual physical size of the resulting plotly canvas in pixels
  • override_x_max and/or override_y_max controls the limits of the grid within the plot. If not specified, it will set it to a value that’s just larger than the maximum coordinate that is generated within the animation. However, manually setting the extents of the plot makes it more consistent and can help if we want to later add a background image.
  • setup_mode turns on and off the gridlines and coordinate grid values, which are useful when setting up the animation but may not be wanted once you are ready to share the animation
  • every_x_time_units defines how often the animation will poll the position of each entity - so e.g. a value of 10 would mean that the animation would progress in jumps of 10 minutes (or whatever the relevant time unit of your simulation is)
# Create animation
animate_activity_log(
    event_log=event_log_df,
    event_position_df=event_positions,
    every_x_time_units=1,
    plotly_height=600,
    override_x_max=360,
    limit_duration=SIM_DURATION
)