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
= "notebook"
pio.renderers.default import os
import ciw
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 ciw
# From https://ciw.readthedocs.io/en/latest/Tutorial/tutorial_ii.html
= ciw.create_network(
N
=[ciw.dists.Exponential(rate=0.3 / 60),
arrival_distributions
=0.2 / 60),
ciw.dists.Exponential(rate
None],
=[ciw.dists.Exponential(rate=2.0 / 60),
service_distributions
=1.4 / 60),
ciw.dists.Exponential(rate
=1.0 / 60)],
ciw.dists.Exponential(rate
=[[0.0, 0.3, 0.7],
routing
0.0, 0.0, 1.0],
[
0.0, 0.0, 0.0]],
[
=[1, 2, 2]
number_of_servers
)
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.
42)
ciw.seed(
= ciw.Simulation(N)
Q
= 180 * 60 # 180 minutes x 60 seconds
RESULTS_COLLECTION_PERIOD
Q.simulate_until_max_time(RESULTS_COLLECTION_PERIOD)
= Q.get_all_records()
recs
'id_number', 'service_start_date']).head(20) pd.DataFrame(recs).sort_values([
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_from_ciw_recs(
event_log
recs,=["cold_food", "hot_food", "till"]
node_name_list
)
"entity_id"]==13] event_log[event_log[
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
= model_params() 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.
= create_event_position_df( [
event_position_df ='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")
EventPosition(event
] )
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
=model_params(),
scenario# this sim was set up in seconds - vidigi defaults to minutes, so its important we overwrite this
="seconds",
simulation_time_unit# 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
="dhms",
time_display_units# print out messages to help us spot where any possible issues occur in the animation
# generation process
=True,
debug_mode# hide gridlines and axis labels
=False,
setup_mode# poll the position of entities every 5 seconds
=5,
every_x_time_units# 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)
=True,
include_play_button# set icon to 20 units in size
=20,
entity_icon_size# set the horizontal gap between entities to be 25 units in size
=15,
gap_between_entities# set the vertical gap between rows of entities when queueing to be 25 units
=25,
gap_between_queue_rows# set the gap between resource icons - and the entities using them in resource use steps -
# to be 30 units
=30,
gap_between_resources# set the height and width of the plot in pixels
=525,
plotly_height=900,
plotly_width# set the duration of each frame in milliseconds
=200,
frame_duration# set the duration of the transition (movement phase) between each frame in milliseconds
=600,
frame_transition_duration# set the internal coordinate grid limits
=700,
override_x_max=600,
override_y_max# stop the animation after the length of time we simulated for has elapsed
=RESULTS_COLLECTION_PERIOD,
limit_duration# if a queue reaches 25 entities, wrap it onto a new line so it doesn't just extend off the page
=25,
wrap_queues_at# if the number of resources exceeds 50, wrap them onto a new line
# (not really needed in this example)
=50,
wrap_resources_at# 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
=75,
step_snapshot_max# specify the start time that will be used for time 0 in the time display
="12:00:00",
start_time# define the size of text layers in the animation
=20,
text_size# hide the layer of text that displays the 'label' field from the event positioning dataframe
=False,
display_stage_labels# add a background image created in the free draw.io tool that is a floor plan of the cafe
="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_10_advanced_ciw/cafe_floorplan.drawio%20(1).png"
add_background_image )
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