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
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 arrivalspandas
- for managing dataframes
Step 2. Set up simulation parameters
# Simple simulation parameters
= 50
SIM_DURATION = 1
NUM_SERVERS = 1.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. 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"""
= 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
def patient_generator(env, server, 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, 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
=patient_id, event='queue_wait_begins')
logger.log_queue(entity_id
# Request server
with server.request() as request:
yield request
# Log service start
=patient_id, event="service_begins", resource_id=1)
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=1)
logger.log_resource_use_end(entity_id
# Log departure
=patient_id)
logger.log_departure(entity_id
# Run the simulation
def run_simulation():
= simpy.Environment()
env = simpy.Resource(env, capacity=NUM_SERVERS)
server = EventLogger(env=env)
logger
# Start patient generator
env.process(patient_generator(env, server, 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 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
= 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='server', label="Being Served"),
EventPosition(event='depart', x=250, y=50, label="Exit")
EventPosition(event ])
Step 6. Create animation
Finally, we create our 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)
# Create animation
animate_activity_log(=event_log_df,
event_log=event_positions,
event_position_df=1,
every_x_time_units=600,
plotly_height=360,
override_x_max=SIM_DURATION
limit_duration )