Highlighting different periods with a repeating overlay

from examples.feat_repeating_overlay.feat_repeating_overlay_model_classes import Trial, g
from vidigi.prep import reshape_for_animations, generate_animation_df
from vidigi.animation import generate_animation, add_repeating_overlay
from vidigi.utils import EventPosition, create_event_position_df
import os
import pandas as pd
import plotly.io as pio
pio.renderers.default = "notebook"
import random
import numpy as np
import pandas as pd
import simpy
from sim_tools.distributions import Exponential, Lognormal
from vidigi.resources import VidigiPriorityStore


# 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

    unav_time = 60 * 12 # 12 hours
    unav_freq = 60 * 12 # every 12 hours

    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 = []

        self.waiting_patients = []

        # Create our resources
        self.init_resources()

        # Store the passed in run number
        self.run_number = run_number

        # 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 = VidigiPriorityStore(self.env, num_resources=g.n_cubicles)

    # A generator function that represents the DES generator for patient arrivals
    def generator_patient_arrivals(self):
        while True:
            # Sample inter-arrival time
            sampled_inter = self.patient_inter_arrival_dist.sample()
            next_arrival_time = self.env.now + sampled_inter

            # Calculate time until next closure and reopening
            time_since_last_closure = self.env.now % g.unav_freq
            time_until_closing = g.unav_freq - time_since_last_closure

            unav_start = self.env.now + time_until_closing

            # Allow people to start arriving again before the clinic opens
            unav_end = unav_start + g.unav_time

            # If the next patient would arrive during the closure period, skip forward
            if next_arrival_time >= unav_start and next_arrival_time < unav_end:
                yield self.env.timeout(unav_end - self.env.now)
                continue  # Restart loop after skipping closure period

            # Wait for inter-arrival time before generating patient
            yield self.env.timeout(sampled_inter)

            self.patient_counter += 1
            p = Patient(self.patient_counter)
            self.patients.append(p)
            self.env.process(self.attend_clinic(p))

    # 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
        current_time = self.env.now
        time_since_last_closure = current_time % g.unav_freq
        time_until_closing = g.unav_freq - time_since_last_closure

        treatment_request_event = self.treatment_cubicles.get(priority=1)

        # Patients will give up and leave up to an hour before the clinic closes if they think they're
        # not going to get seen in time.
        result_of_queue = yield treatment_request_event | self.env.timeout(time_until_closing - min([random.randint(0, 60), time_until_closing]))

        if treatment_request_event in result_of_queue:
            cubicle = result_of_queue[treatment_request_event]

            # record the waiting time
            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': cubicle.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': cubicle.id_attribute}
            )

            self.treatment_cubicles.return_item(cubicle)

        else:

            self.treatment_cubicles.cancel_get(treatment_request_event)

        # 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}
        )

    # 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)

        self.event_log = pd.DataFrame(self.event_log)

        self.event_log["run"] = self.run_number

        return 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.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)
            event_log = my_model.run()

            self.all_event_logs.append(event_log)

        self.all_event_logs = pd.concat(self.all_event_logs)
g.sim_duration = 60 * 24 * 7 # 7 days
g.number_of_runs = 1

my_trial = Trial()

my_trial.run_trial()
4 nurses
my_trial.all_event_logs.head(50)
patient pathway event_type event time resource_id run
0 1 Simplest arrival_departure arrival 3.399660 NaN 0
1 1 Simplest queue treatment_wait_begins 3.399660 NaN 0
2 1 Simplest resource_use treatment_begins 3.399660 1.0 0
3 2 Simplest arrival_departure arrival 8.497645 NaN 0
4 2 Simplest queue treatment_wait_begins 8.497645 NaN 0
5 2 Simplest resource_use treatment_begins 8.497645 2.0 0
6 3 Simplest arrival_departure arrival 8.596678 NaN 0
7 3 Simplest queue treatment_wait_begins 8.596678 NaN 0
8 3 Simplest resource_use treatment_begins 8.596678 3.0 0
9 4 Simplest arrival_departure arrival 8.608025 NaN 0
10 4 Simplest queue treatment_wait_begins 8.608025 NaN 0
11 4 Simplest resource_use treatment_begins 8.608025 4.0 0
12 5 Simplest arrival_departure arrival 11.359739 NaN 0
13 5 Simplest queue treatment_wait_begins 11.359739 NaN 0
14 6 Simplest arrival_departure arrival 19.509442 NaN 0
15 6 Simplest queue treatment_wait_begins 19.509442 NaN 0
16 7 Simplest arrival_departure arrival 22.877356 NaN 0
17 7 Simplest queue treatment_wait_begins 22.877356 NaN 0
18 8 Simplest arrival_departure arrival 26.653863 NaN 0
19 8 Simplest queue treatment_wait_begins 26.653863 NaN 0
20 9 Simplest arrival_departure arrival 40.737793 NaN 0
21 9 Simplest queue treatment_wait_begins 40.737793 NaN 0
22 1 Simplest resource_use_end treatment_complete 43.717044 1.0 0
23 1 Simplest arrival_departure depart 43.717044 NaN 0
24 5 Simplest resource_use treatment_begins 43.717044 1.0 0
25 2 Simplest resource_use_end treatment_complete 47.541216 2.0 0
26 2 Simplest arrival_departure depart 47.541216 NaN 0
27 6 Simplest resource_use treatment_begins 47.541216 2.0 0
28 4 Simplest resource_use_end treatment_complete 48.820975 4.0 0
29 4 Simplest arrival_departure depart 48.820975 NaN 0
30 7 Simplest resource_use treatment_begins 48.820975 4.0 0
31 3 Simplest resource_use_end treatment_complete 51.582490 3.0 0
32 3 Simplest arrival_departure depart 51.582490 NaN 0
33 8 Simplest resource_use treatment_begins 51.582490 3.0 0
34 10 Simplest arrival_departure arrival 71.026558 NaN 0
35 10 Simplest queue treatment_wait_begins 71.026558 NaN 0
36 5 Simplest resource_use_end treatment_complete 80.847148 1.0 0
37 5 Simplest arrival_departure depart 80.847148 NaN 0
38 9 Simplest resource_use treatment_begins 80.847148 1.0 0
39 11 Simplest arrival_departure arrival 87.458700 NaN 0
40 11 Simplest queue treatment_wait_begins 87.458700 NaN 0
41 12 Simplest arrival_departure arrival 87.465138 NaN 0
42 12 Simplest queue treatment_wait_begins 87.465138 NaN 0
43 6 Simplest resource_use_end treatment_complete 89.060237 2.0 0
44 6 Simplest arrival_departure depart 89.060237 NaN 0
45 10 Simplest resource_use treatment_begins 89.060237 2.0 0
46 7 Simplest resource_use_end treatment_complete 95.509386 4.0 0
47 7 Simplest arrival_departure depart 95.509386 NaN 0
48 11 Simplest resource_use treatment_begins 95.509386 4.0 0
49 8 Simplest resource_use_end treatment_complete 96.241403 3.0 0
# 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")
])
my_trial.all_event_logs[my_trial.all_event_logs['run']==0]
patient pathway event_type event time resource_id run
0 1 Simplest arrival_departure arrival 3.399660 NaN 0
1 1 Simplest queue treatment_wait_begins 3.399660 NaN 0
2 1 Simplest resource_use treatment_begins 3.399660 1.0 0
3 2 Simplest arrival_departure arrival 8.497645 NaN 0
4 2 Simplest queue treatment_wait_begins 8.497645 NaN 0
... ... ... ... ... ... ... ...
3981 914 Simplest arrival_departure depart 9374.860981 NaN 0
3982 915 Simplest resource_use_end treatment_complete 9377.867245 2.0 0
3983 915 Simplest arrival_departure depart 9377.867245 NaN 0
3984 912 Simplest resource_use_end treatment_complete 9381.404322 4.0 0
3985 912 Simplest arrival_departure depart 9381.404322 NaN 0

3986 rows × 7 columns

LIMIT_DURATION = g.sim_duration
WRAP_QUEUES_AT = 15
STEP_SNAPSHOT_MAX = WRAP_QUEUES_AT * 4

full_patient_df = reshape_for_animations(
    event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==0],
    every_x_time_units=10,
    entity_col_name="patient",
    step_snapshot_max=STEP_SNAPSHOT_MAX,
    limit_duration=LIMIT_DURATION,
    debug_mode=True
    )

full_patient_df.head(15)
Iteration through time-unit-by-time-unit logs complete 14:23:52
Snapshot df concatenation complete at 14:23:52
snapshot_time index patient pathway event_type event time resource_id run rank additional
0 0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 10 2.0 1.0 Simplest resource_use treatment_begins 3.399660 1.0 0.0 1.0 NaN
2 10 5.0 2.0 Simplest resource_use treatment_begins 8.497645 2.0 0.0 2.0 NaN
3 10 8.0 3.0 Simplest resource_use treatment_begins 8.596678 3.0 0.0 3.0 NaN
4 10 11.0 4.0 Simplest resource_use treatment_begins 8.608025 4.0 0.0 4.0 NaN
5 20 2.0 1.0 Simplest resource_use treatment_begins 3.399660 1.0 0.0 1.0 NaN
6 20 5.0 2.0 Simplest resource_use treatment_begins 8.497645 2.0 0.0 2.0 NaN
7 20 8.0 3.0 Simplest resource_use treatment_begins 8.596678 3.0 0.0 3.0 NaN
8 20 11.0 4.0 Simplest resource_use treatment_begins 8.608025 4.0 0.0 4.0 NaN
9 20 13.0 5.0 Simplest queue treatment_wait_begins 11.359739 NaN 0.0 1.0 NaN
10 20 15.0 6.0 Simplest queue treatment_wait_begins 19.509442 NaN 0.0 2.0 NaN
11 30 2.0 1.0 Simplest resource_use treatment_begins 3.399660 1.0 0.0 1.0 NaN
12 30 5.0 2.0 Simplest resource_use treatment_begins 8.497645 2.0 0.0 2.0 NaN
13 30 8.0 3.0 Simplest resource_use treatment_begins 8.596678 3.0 0.0 3.0 NaN
14 30 11.0 4.0 Simplest resource_use treatment_begins 8.608025 4.0 0.0 4.0 NaN
full_patient_df_plus_pos = generate_animation_df(
    full_entity_df=full_patient_df,
    event_position_df=event_position_df,
    entity_col_name="patient",
    wrap_queues_at=WRAP_QUEUES_AT,
    step_snapshot_max=STEP_SNAPSHOT_MAX,
    gap_between_entities=10,
    gap_between_resources=10,
    gap_between_resource_rows=30,
    gap_between_queue_rows=30,
    debug_mode=True
    )

full_patient_df_plus_pos.sort_values(['patient', 'snapshot_time']).head(15)
Placement dataframe finished construction at 14:23:53
snapshot_time index patient pathway event_type event time resource_id run rank additional x y_final label resource x_final row y icon
17207 10 2.0 1.0 Simplest resource_use treatment_begins 3.399660 1.0 0.0 1.0 NaN 205.0 175.0 Being Treated n_cubicles 205.0 0.0 NaN 🧔🏼
17208 20 2.0 1.0 Simplest resource_use treatment_begins 3.399660 1.0 0.0 1.0 NaN 205.0 175.0 Being Treated n_cubicles 205.0 0.0 NaN 🧔🏼
17209 30 2.0 1.0 Simplest resource_use treatment_begins 3.399660 1.0 0.0 1.0 NaN 205.0 175.0 Being Treated n_cubicles 205.0 0.0 NaN 🧔🏼
17210 40 2.0 1.0 Simplest resource_use treatment_begins 3.399660 1.0 0.0 1.0 NaN 205.0 175.0 Being Treated n_cubicles 205.0 0.0 NaN 🧔🏼
17206 50 2.0 1.0 Simplest resource_use depart 3.399660 1.0 0.0 1.0 NaN 270.0 70.0 Exit None 270.0 0.0 NaN 🧔🏼
17212 10 5.0 2.0 Simplest resource_use treatment_begins 8.497645 2.0 0.0 2.0 NaN 205.0 175.0 Being Treated n_cubicles 195.0 0.0 NaN 👨🏿‍🦯
17213 20 5.0 2.0 Simplest resource_use treatment_begins 8.497645 2.0 0.0 2.0 NaN 205.0 175.0 Being Treated n_cubicles 195.0 0.0 NaN 👨🏿‍🦯
17214 30 5.0 2.0 Simplest resource_use treatment_begins 8.497645 2.0 0.0 2.0 NaN 205.0 175.0 Being Treated n_cubicles 195.0 0.0 NaN 👨🏿‍🦯
17215 40 5.0 2.0 Simplest resource_use treatment_begins 8.497645 2.0 0.0 2.0 NaN 205.0 175.0 Being Treated n_cubicles 195.0 0.0 NaN 👨🏿‍🦯
17211 50 5.0 2.0 Simplest resource_use depart 8.497645 2.0 0.0 2.0 NaN 270.0 70.0 Exit None 260.0 0.0 NaN 👨🏿‍🦯
17222 10 8.0 3.0 Simplest resource_use treatment_begins 8.596678 3.0 0.0 3.0 NaN 205.0 175.0 Being Treated n_cubicles 185.0 0.0 NaN 👨🏻‍🦰
17223 20 8.0 3.0 Simplest resource_use treatment_begins 8.596678 3.0 0.0 3.0 NaN 205.0 175.0 Being Treated n_cubicles 185.0 0.0 NaN 👨🏻‍🦰
17224 30 8.0 3.0 Simplest resource_use treatment_begins 8.596678 3.0 0.0 3.0 NaN 205.0 175.0 Being Treated n_cubicles 185.0 0.0 NaN 👨🏻‍🦰
17225 40 8.0 3.0 Simplest resource_use treatment_begins 8.596678 3.0 0.0 3.0 NaN 205.0 175.0 Being Treated n_cubicles 185.0 0.0 NaN 👨🏻‍🦰
17226 50 8.0 3.0 Simplest resource_use treatment_begins 8.596678 3.0 0.0 1.0 NaN 205.0 175.0 Being Treated n_cubicles 185.0 0.0 NaN 👨🏻‍🦰
full_patient_df_plus_pos[full_patient_df_plus_pos["patient"]=="overnight_closure"]
snapshot_time index patient pathway event_type event time resource_id run rank additional x y_final label resource x_final row y icon
fig = generate_animation(
        full_entity_df_plus_pos=full_patient_df_plus_pos.sort_values(['patient', 'snapshot_time']),
        event_position_df= event_position_df,
        scenario=g(),
        entity_col_name="patient",
        debug_mode=True,
        setup_mode=False,
        include_play_button=True,
        start_time="08:00:00",
        entity_icon_size=20,
        resource_icon_size=20,
        gap_between_resource_rows=30,
        plotly_height=700,
        frame_duration=800,
        frame_transition_duration=200,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        time_display_units="day_clock_ampm",
        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",
    )

fig
Output animation generation complete at 14:24:04

Adding the overlay

If you drag the slider through to 8pm, you will see an overlay showing the clinic is closed. This will continue through until 8am the next day.

add_repeating_overlay(
    fig,
    "🌙 Clinic Closed",
    first_start_frame=int((60 * 12) / 10), # After 12 hours, but we only have a frame every 10 minutes
    on_duration_frames=int((60 * 12) / 10),
    off_duration_frames=int((60 * 12) / 10),
    rect_color="navy",
    rect_opacity=0.1
    )

Text overlay in a different position with no overall overlay

Let’s first generate the basic fig again.

fig = generate_animation(
        full_entity_df_plus_pos=full_patient_df_plus_pos.sort_values(['patient', 'snapshot_time']),
        event_position_df= event_position_df,
        scenario=g(),
        entity_col_name="patient",
        debug_mode=True,
        setup_mode=False,
        include_play_button=True,
        start_time="08:00:00",
        entity_icon_size=20,
        resource_icon_size=20,
        gap_between_resource_rows=30,
        plotly_height=700,
        frame_duration=800,
        frame_transition_duration=200,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        time_display_units="day_clock_ampm",
        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",
    )

fig
Output animation generation complete at 14:24:17
add_repeating_overlay(
    fig,
    "🌙<br>Clinic<br>Closed",
    first_start_frame=int((60 * 12) / 10), # After 12 hours, but we only have a frame every 10 minutes
    on_duration_frames=int((60 * 12) / 10),
    off_duration_frames=int((60 * 12) / 10),
    rect_opacity=0,
    relative_text_position_x=0.85,
    relative_text_position_y=0.7,
    text_font_color="black"
    )