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
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 arrivalspandas
- for managing dataframes
Step 2. Set up simulation parameters
# Simple simulation parameters
= 50
SIM_DURATION = 3
NUM_SERVERS = 4.0
ARRIVAL_RATE = 3.0 SERVICE_TIME
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"""
= 0
patient_id
while True:
+= 1
patient_id
# 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
= random.expovariate(1.0/SERVICE_TIME)
service_duration yield env.timeout(service_duration)
# Run the simulation
def run_simulation():
= simpy.Environment()
env = simpy.Resource(env, capacity=NUM_SERVERS)
server = []
event_log
# Start patient generator
env.process(patient_generator(env, server, event_log))
# Run simulation
=SIM_DURATION) env.run(until
With Vidigi Modifications
class Params:
def __init__(self):
self.num_servers = 3
def patient_generator(env, servers, logger):
"""Generate patients arriving"""
= 0
patient_id
while True:
+= 1
patient_id
# Log arrival
=patient_id)
logger.log_arrival(entity_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
=patient_id, event='queue_wait_begins')
logger.log_queue(entity_id
# Request server
with servers.request() as request:
= yield request
server
# Log service start
=patient_id, event="service_begins", resource_id=server.id_attribute)
logger.log_resource_use_start(entity_id
# Service time
= random.expovariate(1.0/SERVICE_TIME)
service_duration yield env.timeout(service_duration)
# Log service start
=patient_id, event="service_complete", resource_id=server.id_attribute)
logger.log_resource_use_end(entity_id
# Log departure
=patient_id)
logger.log_departure(entity_id
# Run the simulation
def run_simulation():
= Params()
params = simpy.Environment()
env = VidigiStore(env=env, num_resources=params.num_servers)
servers = EventLogger(env=env)
logger
# Start patient generator
env.process(patient_generator(env, servers, logger))
# Run simulation
=SIM_DURATION)
env.run(until
return logger.to_dataframe()
Step 4. Run simulation
# Run simulation and get event log
= run_simulation()
event_log_df 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
= create_event_position_df([
event_positions ='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")
EventPosition(event ])
Step 6. Create animation
plotly_height
and/orplotly_width
set the actual physical size of the resulting plotly canvas in pixelsoverride_x_max
and/oroverride_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 animationevery_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_df,
event_log=event_positions,
event_position_df=Params(),
scenario=1,
every_x_time_units=600,
plotly_height=360,
override_x_max=SIM_DURATION
limit_duration )