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.

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
pio.renderers.default = "notebook"
import os
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

    '''
    random_number_set = 42

    n_cubicles = 4
    trauma_treat_mean = 40
    trauma_treat_var = 5

    arrival_rate = 5

    sim_duration = 600
    number_of_runs = 100

# 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,
                                                      random_seed = self.run_number*g.random_number_set)
        self.treat_dist = Lognormal(mean = g.trauma_treat_mean,
                                    stdev = g.trauma_treat_var,
                                    random_seed = self.run_number*g.random_number_set)

    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.
            p = Patient(self.patient_counter)

            # 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.
            sampled_inter = self.patient_inter_arrival_dist.sample()

            # 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
        start_wait = self.env.now
        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:
            treatment_resource = yield req
            # 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)

            my_model = Model(run)
            model_outputs = my_model.run()
            patient_level_results = model_outputs["results"]
            event_log = model_outputs["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)
g.sim_duration = 60 * 24 * 2 # 2 days (using unit of minutes)

my_trial = Trial()

my_trial.run_trial()
4 nurses
SIM_START_DATE = "2024-01-01"
SIM_START_TIME = "08:00:00"
STEP_SNAPSHOT_MAX = 45
LIMIT_DURATION = g.sim_duration
WRAP_QUEUES_AT = 15
# Create a list of EventPosition objects
event_position_df = create_event_position_df([
    EventPosition(event='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")
])

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(
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df= event_position_df,
        scenario=g(),
        entity_col_name="patient",
        every_x_time_units=10,
        gap_between_entities=6,
        plotly_height=700,
        frame_duration=200,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        limit_duration=g.sim_duration,
        wrap_queues_at=25,
        step_snapshot_max=100,
        display_stage_labels=False,
        add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
        debug_mode=True
    )
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(
        time_display_units="dhm",
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df= event_position_df,
        entity_col_name="patient",
        scenario=g(),
        every_x_time_units=10,
        gap_between_entities=6,
        plotly_height=700,
        frame_duration=200,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        limit_duration=g.sim_duration,
        wrap_queues_at=25,
        step_snapshot_max=100,
        display_stage_labels=False,
        debug_mode=True,
        add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
    )
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(
        start_date=SIM_START_DATE,
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df= event_position_df,
        scenario=g(),
        entity_col_name="patient",
        every_x_time_units=10,
        include_play_button=True,
        gap_between_entities=6,
        plotly_height=700,
        frame_duration=200,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        limit_duration=g.sim_duration,
        wrap_queues_at=25,
        step_snapshot_max=100,
        time_display_units="dhm",
        display_stage_labels=False,
        add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
    )

Finally, we’ll provide a start date for the simulation to display too.

animate_activity_log(
        entity_col_name="patient",
        time_display_units="dhm",
        start_date=SIM_START_DATE,
        start_time=SIM_START_TIME,
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df= event_position_df,
        scenario=g(),
        every_x_time_units=10,
        gap_between_entities=6,
        plotly_height=700,
        frame_duration=200,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        limit_duration=g.sim_duration,
        wrap_queues_at=25,
        step_snapshot_max=100,
        display_stage_labels=False,
        add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
    )

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(
        simulation_time_unit="minutes",
        time_display_units="day_clock",
        entity_col_name="patient",
        start_date=SIM_START_DATE,
        start_time=SIM_START_TIME,
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df=event_position_df,
        scenario=g(),
        every_x_time_units=10, # Every minute
        gap_between_entities=6,
        plotly_height=700,
        frame_duration=1000,
        frame_transition_duration=1000,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        limit_duration=g.sim_duration,
        wrap_queues_at=25,
        step_snapshot_max=100,
        display_stage_labels=False,
        add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
    )

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(
        simulation_time_unit="minutes",
        time_display_units="day_clock_ampm",
        entity_col_name="patient",
        start_date=SIM_START_DATE,
        start_time=SIM_START_TIME,
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df=event_position_df,
        scenario=g(),
        every_x_time_units=10, # Every minute
        gap_between_entities=6,
        plotly_height=700,
        frame_duration=1000,
        frame_transition_duration=1000,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        limit_duration=g.sim_duration,
        wrap_queues_at=25,
        step_snapshot_max=100,
        display_stage_labels=False,
        add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
    )

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(
        simulation_time_unit="days",
        time_display_units="d",
        start_date=SIM_START_DATE,
        entity_col_name="patient",
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df= event_position_df,
        scenario=g(),
        every_x_time_units=10,
        gap_between_entities=6,
        plotly_height=700,
        frame_duration=2000,
        frame_transition_duration=1000,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        limit_duration=g.sim_duration,
        wrap_queues_at=25,
        step_snapshot_max=100,
        display_stage_labels=False,
        add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
    )

Or per month…

animate_activity_log(
        simulation_time_unit="months",
        time_display_units="m",
        entity_col_name="patient",
        start_date=SIM_START_DATE,
        start_time=SIM_START_TIME,
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df=event_position_df,
        scenario=g(),
        every_x_time_units=10, # Every day
        gap_between_entities=6,
        plotly_height=700,
        frame_duration=1000,
        frame_transition_duration=1000,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        limit_duration=g.sim_duration,
        wrap_queues_at=25,
        step_snapshot_max=100,
        display_stage_labels=False,
        add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
    )

Or even per year (though we don’t have a simulation set up to demo this at this time!).