A slightly more complex example with multiple servers

When dealing with multiple servers, we need to have a way to monitor which resource is in use. This is where vidigi’s custom resource classes come in, such as VidigiStore.

VidigiStore is designed to mimic the Resource class in Simpy - so you can continue to use the same patterns of resource requesting you are familiar with.

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
from vidigi.resources import VidigiStore

Step 2. Set up simulation parameters

# Simple simulation parameters
SIM_DURATION = 50
NUM_SERVERS = 3
ARRIVAL_RATE = 4.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. When using SimPy resources, it is not possible to track this information. However, if we instead use a VidigiStore, we get access to a resource ID, which can then be recorded and helps us visualise resource use clearly.

We also need to create a parameter class to store the number of resources/servers available at different steps. We create the resource name - which we choose ourselves - as an attribute of this class.

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
class Params: 
    def __init__(self): 
        self.num_servers = 3 

def patient_generator(env, servers, 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, servers, logger))

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

def patient_process(env, patient_id, servers, 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 servers.request() as request:
        server = yield request  

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

        # 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=server.id_attribute) 

    # Log departure 
    logger.log_departure(entity_id=patient_id)  

# Run the simulation
def run_simulation():
    params = Params()
    env = simpy.Environment()
    servers = VidigiStore(env=env, num_resources=params.num_servers) 
    logger = EventLogger(env=env) 

    # Start patient generator
    env.process(patient_generator(env, servers, 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 526 events

Step 5. Create event positions dataframe

Here, for the service_begins step, we pass in the ‘resource’ parameter, which needs to match one of the attributes in our parameter class. This will then pull back the correct number of resources/servers for that step and display them as icons.

# 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='num_servers', label="Being Served"),
    EventPosition(event='depart', x=250, y=50, label="Exit")
])

Step 6. Create 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)

We pass our Params() class in so that vidigi knows the number of resource icons it needs to generate.

# Create animation
animate_activity_log(
    event_log=event_log_df,
    event_position_df=event_positions,
    scenario=Params(),
    every_x_time_units=1,
    plotly_height=600,
    override_x_max=360,
    limit_duration=SIM_DURATION
)