from examples.example_6_simplest_case_storewrapper.ex_6_model_classes_with_vidigi_logging import Trial, g
from vidigi.animation import animate_activity_log
from vidigi.utils import EventPosition, create_event_position_df
import pandas as pd
import plotly.io as pio
= "notebook"
pio.renderers.default import os
Feature Breakdown: Changing how the date and time are displayed
We can change the start time of our simulation to best reflect our simulation’s parameters.
import random
import numpy as np
import pandas as pd
import simpy
from sim_tools.distributions import Exponential, Lognormal
from vidigi.resources import VidigiStore
# Class to store global parameter values. We don't create an instance of this
# class - we just refer to the class blueprint itself to access the numbers
# inside.
class g:
'''
Create a scenario to parameterise the simulation model
Parameters:
-----------
random_number_set: int, optional (default=DEFAULT_RNG_SET)
Set to control the initial seeds of each stream of pseudo
random numbers used in the model.
n_cubicles: int
The number of treatment cubicles
trauma_treat_mean: float
Mean of the trauma cubicle treatment distribution (Lognormal)
trauma_treat_var: float
Variance of the trauma cubicle treatment distribution (Lognormal)
arrival_rate: float
Set the mean of the exponential distribution that is used to sample the
inter-arrival time of patients
sim_duration: int
The number of time units the simulation will run for
number_of_runs: int
The number of times the simulation will be run with different random number streams
'''
= 42
random_number_set
= 4
n_cubicles = 40
trauma_treat_mean = 5
trauma_treat_var
= 5
arrival_rate
= 600
sim_duration = 100
number_of_runs
# Class representing patients coming in to the clinic.
class Patient:
'''
Class defining details for a patient entity
'''
def __init__(self, p_id):
'''
Constructor method
Params:
-----
identifier: int
a numeric identifier for the patient.
'''
self.identifier = p_id
self.arrival = -np.inf
self.wait_treat = -np.inf
self.total_time = -np.inf
self.treat_duration = -np.inf
# Class representing our model of the clinic.
class Model:
'''
Simulates the simplest minor treatment process for a patient
1. Arrive
2. Examined/treated by nurse when one available
3. Discharged
'''
# Constructor to set up the model for a run. We pass in a run number when
# we create a new model.
def __init__(self, run_number):
# Create a SimPy environment in which everything will live
self.env = simpy.Environment()
self.event_log = []
# Create a patient counter (which we'll use as a patient ID)
self.patient_counter = 0
self.patients = []
# Create our resources
self.init_resources()
# Store the passed in run number
self.run_number = run_number
# Create a new Pandas DataFrame that will store some results against
# the patient ID (which we'll use as the index).
self.results_df = pd.DataFrame()
self.results_df["Patient ID"] = [1]
self.results_df["Queue Time Cubicle"] = [0.0]
self.results_df["Time with Nurse"] = [0.0]
self.results_df.set_index("Patient ID", inplace=True)
# Create an attribute to store the mean queuing times across this run of
# the model
self.mean_q_time_cubicle = 0
self.patient_inter_arrival_dist = Exponential(mean = g.arrival_rate,
= self.run_number*g.random_number_set)
random_seed self.treat_dist = Lognormal(mean = g.trauma_treat_mean,
= g.trauma_treat_var,
stdev = self.run_number*g.random_number_set)
random_seed
def init_resources(self):
'''
Init the number of resources
and store in the arguments container object
Resource list:
1. Nurses/treatment bays (same thing in this model)
'''
self.treatment_cubicles = VidigiStore(self.env, num_resources=g.n_cubicles)
# A generator function that represents the DES generator for patient
# arrivals
def generator_patient_arrivals(self):
# We use an infinite loop here to keep doing this indefinitely whilst
# the simulation runs
while True:
# Increment the patient counter by 1 (this means our first patient
# will have an ID of 1)
self.patient_counter += 1
# Create a new patient - an instance of the Patient Class we
# defined above. Remember, we pass in the ID when creating a
# patient - so here we pass the patient counter to use as the ID.
= Patient(self.patient_counter)
p
# Store patient in list for later easy access
self.patients.append(p)
# Tell SimPy to start up the attend_clinic generator function with
# this patient (the generator function that will model the
# patient's journey through the system)
self.env.process(self.attend_clinic(p))
# Randomly sample the time to the next patient arriving. Here, we
# sample from an exponential distribution (common for inter-arrival
# times), and pass in a lambda value of 1 / mean. The mean
# inter-arrival time is stored in the g class.
= self.patient_inter_arrival_dist.sample()
sampled_inter
# Freeze this instance of this function in place until the
# inter-arrival time we sampled above has elapsed. Note - time in
# SimPy progresses in "Time Units", which can represent anything
# you like (just make sure you're consistent within the model)
yield self.env.timeout(sampled_inter)
# A generator function that represents the pathway for a patient going
# through the clinic.
# The patient object is passed in to the generator function so we can
# extract information from / record information to it
def attend_clinic(self, patient):
self.arrival = self.env.now
self.event_log.append(
'patient': patient.identifier,
{'pathway': 'Simplest',
'event_type': 'arrival_departure',
'event': 'arrival',
'time': self.env.now}
)
# request examination resource
= self.env.now
start_wait self.event_log.append(
'patient': patient.identifier,
{'pathway': 'Simplest',
'event': 'treatment_wait_begins',
'event_type': 'queue',
'time': self.env.now}
)
# Seize a treatment resource when available
with self.treatment_cubicles.request() as req:
= yield req
treatment_resource # record the waiting time for registration
self.wait_treat = self.env.now - start_wait
self.event_log.append(
'patient': patient.identifier,
{'pathway': 'Simplest',
'event': 'treatment_begins',
'event_type': 'resource_use',
'time': self.env.now,
'resource_id': treatment_resource.id_attribute
}
)
# sample treatment duration
self.treat_duration = self.treat_dist.sample()
yield self.env.timeout(self.treat_duration)
self.event_log.append(
'patient': patient.identifier,
{'pathway': 'Simplest',
'event': 'treatment_complete',
'event_type': 'resource_use_end',
'time': self.env.now,
'resource_id': treatment_resource.id_attribute}
)
# total time in system
self.total_time = self.env.now - self.arrival
self.event_log.append(
'patient': patient.identifier,
{'pathway': 'Simplest',
'event': 'depart',
'event_type': 'arrival_departure',
'time': self.env.now}
)
# This method calculates results over a single run. Here we just calculate
# a mean, but in real world models you'd probably want to calculate more.
def calculate_run_results(self):
# Take the mean of the queuing times across patients in this run of the
# model.
self.mean_q_time_cubicle = self.results_df["Queue Time Cubicle"].mean()
# The run method starts up the DES entity generators, runs the simulation,
# and in turns calls anything we need to generate results for the run
def run(self):
# Start up our DES entity generators that create new patients. We've
# only got one in this model, but we'd need to do this for each one if
# we had multiple generators.
self.env.process(self.generator_patient_arrivals())
# Run the model for the duration specified in g class
self.env.run(until=g.sim_duration)
# Now the simulation run has finished, call the method that calculates
# run results
self.calculate_run_results()
self.event_log = pd.DataFrame(self.event_log)
self.event_log["run"] = self.run_number
return {'results': self.results_df, 'event_log': self.event_log}
# Class representing a Trial for our simulation - a batch of simulation runs.
class Trial:
# The constructor sets up a pandas dataframe that will store the key
# results from each run against run number, with run number as the index.
def __init__(self):
self.df_trial_results = pd.DataFrame()
self.df_trial_results["Run Number"] = [0]
self.df_trial_results["Arrivals"] = [0]
self.df_trial_results["Mean Queue Time Cubicle"] = [0.0]
self.df_trial_results.set_index("Run Number", inplace=True)
self.all_event_logs = []
# Method to run a trial
def run_trial(self):
print(f"{g.n_cubicles} nurses")
print("") ## Print a blank line
# Run the simulation for the number of runs specified in g class.
# For each run, we create a new instance of the Model class and call its
# run method, which sets everything else in motion. Once the run has
# completed, we grab out the stored run results (just mean queuing time
# here) and store it against the run number in the trial results
# dataframe.
for run in range(g.number_of_runs):
random.seed(run)
= Model(run)
my_model = my_model.run()
model_outputs = model_outputs["results"]
patient_level_results = model_outputs["event_log"]
event_log
self.df_trial_results.loc[run] = [
len(patient_level_results),
my_model.mean_q_time_cubicle,
]
# print(event_log)
self.all_event_logs.append(event_log)
self.all_event_logs = pd.concat(self.all_event_logs)
= 60 * 24 * 2 # 2 days (using unit of minutes)
g.sim_duration
= Trial()
my_trial
my_trial.run_trial()
4 nurses
= "2024-01-01"
SIM_START_DATE = "08:00:00"
SIM_START_TIME = 45
STEP_SNAPSHOT_MAX = g.sim_duration
LIMIT_DURATION = 15 WRAP_QUEUES_AT
# Create a list of EventPosition objects
= create_event_position_df([
event_position_df ='arrival', x=50, y=300, label="Arrival"),
EventPosition(event='treatment_wait_begins', x=205, y=275, label="Waiting for Treatment"),
EventPosition(event='treatment_begins', x=205, y=175, resource='n_cubicles', label="Being Treated"),
EventPosition(event='depart', x=270, y=70, label="Exit")
EventPosition(event ])
Run using the all-in-one animation function
Let’s first look at the output if we don’t use the start date or ask for the data to displayed in days, hours and minutes.
animate_activity_log(=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_log= event_position_df,
event_position_df=g(),
scenario="patient",
entity_col_name=10,
every_x_time_units=6,
gap_between_entities=700,
plotly_height=200,
frame_duration=1200,
plotly_width=300,
override_x_max=500,
override_y_max=g.sim_duration,
limit_duration=25,
wrap_queues_at=100,
step_snapshot_max=False,
display_stage_labels="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
add_background_image=True
debug_mode )
Animation function called at 12:32:48
Iteration through time-unit-by-time-unit logs complete 12:32:49
Snapshot df concatenation complete at 12:32:50
Reshaped animation dataframe finished construction at 12:32:50
Placement dataframe finished construction at 12:32:50
Output animation generation complete at 12:32:51
Total Time Elapsed: 3.06 seconds
Next, let’s see what happens when we request it in days, hours and minutes using the time_display_units
parameter, but don’t specify a simulation start date.
The default behaviour is that it will choose a date 6 months from the date the code is run, and set the starting time to 00:00:00 (midnight).
animate_activity_log(="dhm",
time_display_units=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_log= event_position_df,
event_position_df="patient",
entity_col_name=g(),
scenario=10,
every_x_time_units=6,
gap_between_entities=700,
plotly_height=200,
frame_duration=1200,
plotly_width=300,
override_x_max=500,
override_y_max=g.sim_duration,
limit_duration=25,
wrap_queues_at=100,
step_snapshot_max=False,
display_stage_labels=True,
debug_mode="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
add_background_image )
Animation function called at 12:32:52
Iteration through time-unit-by-time-unit logs complete 12:32:53
Snapshot df concatenation complete at 12:32:53
Reshaped animation dataframe finished construction at 12:32:53
Placement dataframe finished construction at 12:32:53
Output animation generation complete at 12:32:55
Total Time Elapsed: 3.19 seconds
Let’s now provide a start date of the 1st of January 2024.
We can see that with a start date provided but no start time, the simulation clock will start at 00:00.
animate_activity_log(=SIM_START_DATE,
start_date=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_log= event_position_df,
event_position_df=g(),
scenario="patient",
entity_col_name=10,
every_x_time_units=True,
include_play_button=6,
gap_between_entities=700,
plotly_height=200,
frame_duration=1200,
plotly_width=300,
override_x_max=500,
override_y_max=g.sim_duration,
limit_duration=25,
wrap_queues_at=100,
step_snapshot_max="dhm",
time_display_units=False,
display_stage_labels="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
add_background_image )
Finally, we’ll provide a start date for the simulation to display too.
animate_activity_log(="patient",
entity_col_name="dhm",
time_display_units=SIM_START_DATE,
start_date=SIM_START_TIME,
start_time=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_log= event_position_df,
event_position_df=g(),
scenario=10,
every_x_time_units=6,
gap_between_entities=700,
plotly_height=200,
frame_duration=1200,
plotly_width=300,
override_x_max=500,
override_y_max=g.sim_duration,
limit_duration=25,
wrap_queues_at=100,
step_snapshot_max=False,
display_stage_labels="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
add_background_image )
There are a wide range of different acceptable parameters for the time_display_units
parameter.
These include
- ‘dhms’ : Day Month Year + HH:MM:SS (e.g., “06 June 2025:23:45”)
- ‘dhms_ampm’ : Same as ‘dhms’, but in 12-hour format with AM/PM (e.g., “06 June 2025:23:45 PM”)
- ‘dhm’ : Day Month Year + HH:MM (e.g., “06 June 2025:23”)
- ‘dhm_ampm’ : 12-hour format with AM/PM (e.g., “06 June 2025:23 PM”)
- ‘dh’ : Day Month Year + HH (e.g., “06 June 2025”)
- ‘dh_ampm’ : 12-hour format with AM/PM (e.g., “06 June 2025 PM”)
- ‘d’ : Full weekday and date (e.g., “Friday 06 June 2025”)
- ‘m’ : Month and year (e.g., “June 2025”)
- ‘y’ : Year only (e.g., “2025”)
- ‘day_clock’ or ‘simulation_day_clock’: Show simulation-relative day and time (e.g., “Simulation Day 3:15”)
- ‘day_clock_ampm’ or ‘simulation_day_clock_ampm’: Same as above, but time is shown in 12-hour clock with AM/PM (e.g., “Simulation Day 3:15 PM”)
Alternatively, you can supply a custom strftime format string (e.g., ‘%Y-%m-%d %H’) to control the display manually.
Let’s have a look at day_clock:
animate_activity_log(="minutes",
simulation_time_unit="day_clock",
time_display_units="patient",
entity_col_name=SIM_START_DATE,
start_date=SIM_START_TIME,
start_time=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_log=event_position_df,
event_position_df=g(),
scenario=10, # Every minute
every_x_time_units=6,
gap_between_entities=700,
plotly_height=1000,
frame_duration=1000,
frame_transition_duration=1200,
plotly_width=300,
override_x_max=500,
override_y_max=g.sim_duration,
limit_duration=25,
wrap_queues_at=100,
step_snapshot_max=False,
display_stage_labels="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
add_background_image )
And ‘day_clock_ampm’.
Note that the suffix _ampm
can be added to any of dhms
, dhm
, dh
, and day_clock
so that you display the time in a 12 hour format with AM/PM instead of the default 24 hour clock.
animate_activity_log(="minutes",
simulation_time_unit="day_clock_ampm",
time_display_units="patient",
entity_col_name=SIM_START_DATE,
start_date=SIM_START_TIME,
start_time=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_log=event_position_df,
event_position_df=g(),
scenario=10, # Every minute
every_x_time_units=6,
gap_between_entities=700,
plotly_height=1000,
frame_duration=1000,
frame_transition_duration=1200,
plotly_width=300,
override_x_max=500,
override_y_max=g.sim_duration,
limit_duration=25,
wrap_queues_at=100,
step_snapshot_max=False,
display_stage_labels="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
add_background_image )
Changing the timescale
We could also display the simulation at a daily level. Let’s imagine our simulation represents the number of patients being seen per day instead of per minute.
NOTE: These example animations won’t make a lot of sense as the underlying simulation doesn’t really make sense at a higher resolution than daily! but it gives you some idea of how these parameters can change and their impact on how dates and times are displayed.
animate_activity_log(="days",
simulation_time_unit="d",
time_display_units=SIM_START_DATE,
start_date="patient",
entity_col_name=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_log= event_position_df,
event_position_df=g(),
scenario=10,
every_x_time_units=6,
gap_between_entities=700,
plotly_height=2000,
frame_duration=1000,
frame_transition_duration=1200,
plotly_width=300,
override_x_max=500,
override_y_max=g.sim_duration,
limit_duration=25,
wrap_queues_at=100,
step_snapshot_max=False,
display_stage_labels="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
add_background_image )
Or per month…
animate_activity_log(="months",
simulation_time_unit="m",
time_display_units="patient",
entity_col_name=SIM_START_DATE,
start_date=SIM_START_TIME,
start_time=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
event_log=event_position_df,
event_position_df=g(),
scenario=10, # Every day
every_x_time_units=6,
gap_between_entities=700,
plotly_height=1000,
frame_duration=1000,
frame_transition_duration=1200,
plotly_width=300,
override_x_max=500,
override_y_max=g.sim_duration,
limit_duration=25,
wrap_queues_at=100,
step_snapshot_max=False,
display_stage_labels="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
add_background_image )
Or even per year (though we don’t have a simulation set up to demo this at this time!).