A More Complex Ciw Example with Backgrounds

Note that this example is written using ciw 3.x, and is taken from the Ciw documentation here: https://ciw.readthedocs.io/en/latest/Tutorial/tutorial_iv.html

All credit for Ciw and the Ciw documentation go to Geraint Palmer.

import pandas as pd
# Import the wrapper objects for model interaction.
from examples.example_10_advanced_ciw.ex_10_ciw_model import N
from vidigi.ciw import event_log_from_ciw_recs
from vidigi.utils import EventPosition, create_event_position_df
from vidigi.animation import animate_activity_log
import plotly.io as pio
pio.renderers.default = "notebook"
import os
import ciw
import ciw

# From https://ciw.readthedocs.io/en/latest/Tutorial/tutorial_ii.html

N = ciw.create_network(

    arrival_distributions=[ciw.dists.Exponential(rate=0.3 / 60),

                           ciw.dists.Exponential(rate=0.2 / 60),

                           None],

    service_distributions=[ciw.dists.Exponential(rate=2.0 / 60),

                           ciw.dists.Exponential(rate=1.4 / 60),

                           ciw.dists.Exponential(rate=1.0 / 60)],

    routing=[[0.0, 0.3, 0.7],

             [0.0, 0.0, 1.0],

             [0.0, 0.0, 0.0]],

    number_of_servers=[1, 2, 2]

)

First, we set the seed and then run the simulation. We then use the .get_all_records() method on the simulation object to return the data in the format Vidigi requires.

ciw.seed(42)

Q = ciw.Simulation(N)

RESULTS_COLLECTION_PERIOD = 180 * 60 # 180 minutes x 60 seconds

Q.simulate_until_max_time(RESULTS_COLLECTION_PERIOD)

recs = Q.get_all_records()

pd.DataFrame(recs).sort_values(['id_number', 'service_start_date']).head(20)
id_number customer_class original_customer_class node arrival_date waiting_time service_start_date service_time service_end_date time_blocked exit_date destination queue_size_at_arrival queue_size_at_departure server_id record_type
0 1 Customer Customer 2 7.598652 0.000000 7.598652 13.783888 21.382540 0.0 21.382540 3 0 0 1 service
1 1 Customer Customer 3 21.382540 0.000000 21.382540 67.750381 89.132921 0.0 89.132921 -1 0 0 1 service
6 2 Customer Customer 2 83.374507 0.000000 83.374507 95.455204 178.829712 0.0 178.829712 3 0 0 1 service
7 2 Customer Customer 3 178.829712 0.000000 178.829712 53.388522 232.218234 0.0 232.218234 -1 1 1 1 service
2 3 Customer Customer 2 110.660229 0.000000 110.660229 23.487693 134.147921 0.0 134.147921 3 1 2 2 service
3 3 Customer Customer 3 134.147921 0.000000 134.147921 13.301500 147.449421 0.0 147.449421 -1 0 1 1 service
4 4 Customer Customer 2 119.735282 14.412639 134.147921 1.152617 135.300539 0.0 135.300539 3 2 1 2 service
5 4 Customer Customer 3 135.300539 0.000000 135.300539 47.239755 182.540294 0.0 182.540294 -1 1 1 2 service
16 5 Customer Customer 2 193.750291 0.000000 193.750291 71.045918 264.796209 0.0 264.796209 3 0 1 1 service
17 5 Customer Customer 3 264.796209 0.000000 264.796209 216.972911 481.769120 0.0 481.769120 -1 1 0 2 service
12 6 Customer Customer 2 195.706281 0.000000 195.706281 70.241394 265.947675 0.0 265.947675 3 1 0 2 service
13 6 Customer Customer 3 265.947675 30.320583 296.268259 48.183165 344.451423 0.0 344.451423 -1 2 2 1 service
8 7 Customer Customer 1 204.012057 0.000000 204.012057 12.476853 216.488910 0.0 216.488910 3 0 0 1 service
9 7 Customer Customer 3 216.488910 0.000000 216.488910 24.622136 241.111046 0.0 241.111046 -1 1 1 2 service
10 8 Customer Customer 1 237.809311 0.000000 237.809311 2.919980 240.729291 0.0 240.729291 3 0 0 1 service
11 8 Customer Customer 3 240.729291 0.000000 240.729291 55.538968 296.268259 0.0 296.268259 -1 1 2 1 service
14 9 Customer Customer 1 258.153048 0.000000 258.153048 49.371898 307.524946 0.0 307.524946 3 0 0 1 service
15 9 Customer Customer 3 307.524946 36.926477 344.451423 57.821772 402.273196 0.0 402.273196 -1 2 1 1 service
20 10 Customer Customer 1 519.821134 0.000000 519.821134 59.351398 579.172532 0.0 579.172532 2 0 0 1 service
21 10 Customer Customer 2 579.172532 1.027521 580.200053 4.563164 584.763217 0.0 584.763217 3 2 1 2 service

We then use Vidigi’s event_log_from_ciw_recs() function to convert the Ciw logs into the format the Vidigi animation functions will require.

This takes the output of .get_all_records(), as well as a list of node names that we choose that must match the order of nodes in the simulation.

We’ll create our Vidigi event log, and then view the rows that are created for a particular entity.

event_log = event_log_from_ciw_recs(
    recs,
    node_name_list=["cold_food", "hot_food", "till"]
    )

event_log[event_log["entity_id"]==13]
entity_id pathway event_type event time resource_id
99 13 Model arrival_departure arrival 671.604123 NaN
100 13 Model queue hot_food_wait_begins 671.604123 NaN
101 13 Model resource_use hot_food_begins 671.604123 1.0
102 13 Model resource_use_end hot_food_ends 684.914665 1.0
103 13 Model queue till_wait_begins 684.914665 NaN
104 13 Model resource_use till_begins 684.914665 1.0
105 13 Model resource_use_end till_ends 741.277635 1.0
106 13 Model arrival_departure depart 741.277635 NaN

So that we can visualise the number of resources available at each step, we need to create a simple class we can pass to the animation function, where the attributes are related to the number of resources.

We’ll use these attribute names when we set up our event positioning dataframe shortly.

# Create a suitable class to pass in the resource numbers to the animation function
class model_params():
    def __init__(self):
        self.cold_food_servers = 1
        self.hot_food_servers = 2
        self.tills = 2

params = model_params()

We then set up an event positioning dataframe that reflects the bottom-right corner of each event. Queues will build up to the left of this point, and then wrap onto a new line when they reach a limit we specify in the animation step, at which point the entity icons will wrap onto a new line, which will appear above (i.e. higher up in the animation) the first row of the queue.

event_position_df = create_event_position_df( [
    EventPosition(event='arrival', x=30, y=550, label="Arrival"),
    EventPosition(event='cold_food_wait_begins', x=200, y=510, label="Waiting for Cold Food"),
    EventPosition(event='cold_food_begins', x=210, y=370, resource='cold_food_servers', label="Being Served Cold Food"),
    EventPosition(event='hot_food_wait_begins', x=505, y=510, label="Waiting for Hot Food"),
    EventPosition(event='hot_food_begins', x=505, y=370, resource='hot_food_servers', label="Being Served Hot Food"),
    EventPosition(event='till_wait_begins', x=350, y=170, label="Waiting for Till"),
    EventPosition(event='till_begins', x=350, y=120, resource='tills', label="Being Served at Till"),
    EventPosition(event='depart', x=600, y=10, label="Exit")
]
)

Finally, we create our animation. Here, we’ve used our all-in-one function as we don’t need to do anything special to the entity icons or pathing.

animate_activity_log(
    # pass in the log of events we created using the helper function
    event_log=event_log,
    # pass in our initial event positioning dataframe
    event_position_df=event_position_df,
    # pass in our model parameters class so that resources have icons that persist when not in use
    scenario=model_params(),
    # this sim was set up in seconds - vidigi defaults to minutes, so its important we overwrite this
    simulation_time_unit="seconds",
    # display the time below the bar in the format 'Day Month Year Hours:Minutes:Seconds'.
    # because we don't specify a start date for the animation to run from, it will default to 6
    # months from when the animation is created
    time_display_units="dhms",
    # print out messages to help us spot where any possible issues occur in the animation
    # generation process
    debug_mode=True,
    # hide gridlines and axis labels
    setup_mode=False,
    # poll the position of entities every 5 seconds
    every_x_time_units=5,
    # Include a play button (so the viewer can click play and leave the animation to run, rather
    # than only having the option to scrub through)
    include_play_button=True,
    # set icon to 20 units in size
    entity_icon_size=20,
    # set the horizontal gap between entities to be 25 units in size
    gap_between_entities=15,
    # set the vertical gap between rows of entities when queueing to be 25 units
    gap_between_queue_rows=25,
    # set the gap between resource icons - and the entities using them in resource use steps -
    # to be 30 units
    gap_between_resources=30,
    # set the height and width of the plot in pixels
    plotly_height=525,
    plotly_width=900,
    # set the duration of each frame in milliseconds
    frame_duration=200,
    # set the duration of the transition (movement phase) between each frame in milliseconds
    frame_transition_duration=600,
    # set the internal coordinate grid limits
    override_x_max=700,
    override_y_max=600,
    # stop the animation after the length of time we simulated for has elapsed
    limit_duration=RESULTS_COLLECTION_PERIOD,
    # if a queue reaches 25 entities, wrap it onto a new line so it doesn't just extend off the page
    wrap_queues_at=25,
    # if the number of resources exceeds 50, wrap them onto a new line
    # (not really needed in this example)
    wrap_resources_at=50,
    # if the number of people queueing in a step exceeds 75, replace the final entity with text
    # identifying the number of additional people in the queue
    step_snapshot_max=75,
    # specify the start time that will be used for time 0 in the time display
    start_time="12:00:00",
    # define the size of text layers in the animation
    text_size=20,
    # hide the layer of text that displays the 'label' field from the event positioning dataframe
    display_stage_labels=False,
    # add a background image created in the free draw.io tool that is a floor plan of the cafe
    add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_10_advanced_ciw/cafe_floorplan.drawio%20(1).png"
)
Animation function called at 13:21:26
Iteration through time-unit-by-time-unit logs complete 13:21:30
Snapshot df concatenation complete at 13:21:31
Reshaped animation dataframe finished construction at 13:21:31
Placement dataframe finished construction at 13:21:31
Output animation generation complete at 13:21:39
Total Time Elapsed: 13.04 seconds