import pandas as pd
# Import the wrapper objects for model interaction.
from examples.example_4_ciw.ex_4_ciw_model import Experiment, multiple_replications
from vidigi.ciw import event_log_from_ciw_recs
from vidigi.animation import animate_activity_log
import plotly.io as pio
= "notebook"
pio.renderers.default import os
A Simple Ciw Model
Note that this example is written using ciw 2.x
It will not run with 3.x - but could theoretically be adapted to do so
The ‘logs’ object is the result of running
sim_engine.get_all_records()
However, note that while we run multiple replications, we only pass the records for a single replication to the event_log_from_ciw_recs
function.
The underlying model code is from Monks, T., Harper, A., & Heather, A. (2023). Towards Sharing Tools, Artefacts, and Reproducible Simulation: a ciw model example (v1.0.1). Zenodo. https://doi.org/10.5281/zenodo.10051494
See here for the adaptation embedded within that repo: https://github.com/Bergam0t/ciw-example-animation/tree/main
'''
CiW Implementation of the 111 call centre
Time units of the simulation model are in minutes.
'''
# Imports
import numpy as np
import pandas as pd
import ciw
# Module level variables, constants, and default values
= 13
N_OPERATORS = 9
N_NURSES = 100.0 / 60.0
MEAN_IAT
= 5.0
CALL_LOW = 7.0
CALL_MODE = 10.0
CALL_HIGH
= 10.0
NURSE_CALL_LOW = 20.0
NURSE_CALL_HIGH
= 0.4
CHANCE_CALLBACK = 1000
RESULTS_COLLECTION_PERIOD
# Experiment class
class Experiment:
def __init__(self, n_operators=N_OPERATORS, n_nurses=N_NURSES,
=MEAN_IAT, call_low=CALL_LOW,
mean_iat=CALL_MODE, call_high=CALL_HIGH,
call_mode=CHANCE_CALLBACK,
chance_callback=NURSE_CALL_LOW,
nurse_call_low=NURSE_CALL_HIGH,
nurse_call_high=None):
random_seedself.n_operators = n_operators
self.n_nurses = n_nurses
self.arrival_dist = ciw.dists.Exponential(mean_iat)
self.call_dist = ciw.dists.Triangular(call_low, call_mode, call_high)
self.nurse_dist = ciw.dists.Uniform(nurse_call_low, nurse_call_high)
self.chance_callback = chance_callback
self.init_results_variables()
def init_results_variables(self):
self.results = {
'waiting_times': [],
'total_call_duration': 0.0,
'nurse_waiting_times': [],
'total_nurse_call_duration': 0.0,
}
# Model code
def get_model(args):
'''
Build a CiW model using the arguments provided.
'''
= ciw.create_network(
network =[args.arrival_dist, None],
arrival_distributions=[args.call_dist, args.nurse_dist],
service_distributions=[[0.0, args.chance_callback], [0.0, 0.0]],
routing=[args.n_operators, args.n_nurses]
number_of_servers
)return network
# Model wrapper functions
def single_run(experiment, rc_period=RESULTS_COLLECTION_PERIOD, random_seed=None):
= {}
run_results
ciw.seed(random_seed)
= get_model(experiment)
model
= ciw.Simulation(model)
sim_engine
sim_engine.simulate_until_max_time(rc_period)
= sim_engine.get_all_records()
recs
= [r.service_time for r in recs if r.node == 1]
op_servicetimes = [r.service_time for r in recs if r.node == 2]
nurse_servicetimes
= [r.waiting_time for r in recs if r.node == 1]
op_waits = [r.waiting_time for r in recs if r.node == 2]
nurse_waits
'01_mean_waiting_time'] = np.mean(op_waits)
run_results['02_operator_util'] = (
run_results[sum(op_servicetimes) / (rc_period * experiment.n_operators)
* 100.0
) '03_mean_nurse_waiting_time'] = np.mean(nurse_waits)
run_results['04_nurse_util'] = (
run_results[sum(nurse_servicetimes) / (rc_period * experiment.n_nurses)
* 100.0
)
return run_results, recs
def multiple_replications(experiment, rc_period=RESULTS_COLLECTION_PERIOD, n_reps=5):
= []
results = []
logs
for rep in range(n_reps):
= single_run(experiment, rc_period)
run_result, log
results.append(run_result)
logs.append(log)
= pd.DataFrame(results)
df_results = np.arange(1, len(df_results) + 1)
df_results.index = 'rep'
df_results.index.name
return df_results, logs
= 18
N_OPERATORS = 9
N_NURSES = 1000
RESULTS_COLLECTION_PERIOD
= Experiment(n_operators=N_OPERATORS,
user_experiment =N_NURSES,
n_nurses=0.4)
chance_callback
# run multiple replications
= multiple_replications(user_experiment, n_reps=10) results, logs
While we’ve done multiple replications, for the purpose of the animation we want only a single set of logs, so we will extract those from the logs variable we created.
# the 'logs' object contains a list, where each entry is the recs object for that run
= logs[0]
logs_run_1
print(len(logs_run_1))
2277
# let's print all of the outputs for a single individual
print(log) for log in logs_run_1 if log.id_number==500] [
Record(id_number=500, customer_class='Customer', original_customer_class='Customer', node=1, arrival_date=285.15199278675664, waiting_time=0.0, service_start_date=285.15199278675664, service_time=8.53886189681026, service_end_date=293.6908546835669, time_blocked=0.0, exit_date=293.6908546835669, destination=2, queue_size_at_arrival=14, queue_size_at_departure=9, server_id=2, record_type='service')
Record(id_number=500, customer_class='Customer', original_customer_class='Customer', node=2, arrival_date=293.6908546835669, waiting_time=49.99518525413794, service_start_date=343.68603993770483, service_time=15.877553023839369, service_end_date=359.5635929615442, time_blocked=0.0, exit_date=359.5635929615442, destination=-1, queue_size_at_arrival=40, queue_size_at_departure=45, server_id=4, record_type='service')
[None, None]
Unlike SimPy, where we have to manually add our event logs at various points, we instead can make use of the event_log_from_ciw_recs
helper function from vidigi.utils
to automatically reshape ciw logs into the correct format for vidigi to work with.
For each node, we pass in an appropriate name. Vidigi will use these and append ’_begins’ and ’_ends’, as well as calculating arrivals and departures from the model and creating resource IDs to allow it to correctly show the utilisation of a resource at each step.
# let's now try turning this into an event log
= event_log_from_ciw_recs(logs_run_1, node_name_list=["operator", "nurse"])
event_log_test
25) event_log_test.head(
entity_id | pathway | event_type | event | time | resource_id | |
---|---|---|---|---|---|---|
0 | 1 | Model | arrival_departure | arrival | 0.217233 | NaN |
1 | 1 | Model | queue | operator_wait_begins | 0.217233 | NaN |
2 | 1 | Model | resource_use | operator_begins | 0.217233 | 1.0 |
3 | 1 | Model | resource_use | operator_ends | 9.223846 | 1.0 |
4 | 1 | Model | queue | nurse_wait_begins | 9.223846 | NaN |
5 | 1 | Model | resource_use | nurse_begins | 9.223846 | 2.0 |
6 | 1 | Model | resource_use | nurse_ends | 20.429835 | 2.0 |
7 | 1 | Model | arrival_departure | depart | 20.429835 | NaN |
8 | 2 | Model | arrival_departure | arrival | 0.876985 | NaN |
9 | 2 | Model | queue | operator_wait_begins | 0.876985 | NaN |
10 | 2 | Model | resource_use | operator_begins | 0.876985 | 2.0 |
11 | 2 | Model | resource_use | operator_ends | 7.146672 | 2.0 |
12 | 2 | Model | queue | nurse_wait_begins | 7.146672 | NaN |
13 | 2 | Model | resource_use | nurse_begins | 7.146672 | 1.0 |
14 | 2 | Model | resource_use | nurse_ends | 18.146884 | 1.0 |
15 | 2 | Model | arrival_departure | depart | 18.146884 | NaN |
16 | 3 | Model | arrival_departure | arrival | 2.301595 | NaN |
17 | 3 | Model | queue | operator_wait_begins | 2.301595 | NaN |
18 | 3 | Model | resource_use | operator_begins | 2.301595 | 3.0 |
19 | 3 | Model | resource_use | operator_ends | 10.313719 | 3.0 |
20 | 3 | Model | arrival_departure | depart | 10.313719 | NaN |
21 | 4 | Model | arrival_departure | arrival | 2.806043 | NaN |
22 | 4 | Model | queue | operator_wait_begins | 2.806043 | NaN |
23 | 4 | Model | resource_use | operator_begins | 2.806043 | 4.0 |
24 | 4 | Model | resource_use | operator_ends | 10.160622 | 4.0 |
Now we need to create a suitable class to pass in the resource numbers to the animation function.
# Create a suitable class to pass in the resource numbers to the animation function
class model_params():
def __init__(self):
self.n_operators = N_OPERATORS
self.n_nurses = N_NURSES
= model_params()
params
print(f"Number of operators: {params.n_operators}")
print(f"Number of nurses: {params.n_nurses}")
Number of operators: 18
Number of nurses: 9
Like with SimPy, we need to tell vidigi where to put each step on our plot. We will refer to the names we used - so as we named our nodes ‘operator’ and ‘nurse’, we will want
- arrival
- operator_wait_begins (to show queueing for the operator)
- operator_begins (to show resource use of the operator)
- nurse_wait_begins (to show queuing for the nurse after finishing being seen by the operator)
- nurse_begins (to show resource use of the nurse)
For the _begins steps, which relat to resource use, we will also pass in a name that relates to the number of resources we need, which we defined in our model_params class above.
So, for the operator_begins step, for example, we tell t to look for n_operators, whch is one of the parameters in our model_params class. We pass the params class into the animation function.
# Create required event_position_df for vidigi animation
= pd.DataFrame([
event_position_df 'event': 'arrival',
{'x': 30, 'y': 350,
'label': "Arrival"},
'event': 'operator_wait_begins',
{'x': 205, 'y': 270,
'label': "Waiting for Operator"},
'event': 'operator_begins',
{'x': 210, 'y': 210,
'resource':'n_operators',
'label': "Speaking to operator"},
'event': 'nurse_wait_begins',
{'x': 205, 'y': 110,
'label': "Waiting for Nurse"},
'event': 'nurse_begins',
{'x': 210, 'y': 50,
'resource':'n_nurses',
'label': "Speaking to Nurse"},
'event': 'depart',
{'x': 270, 'y': 10,
'label': "Exit"}
])
event_position_df
event | x | y | label | resource | |
---|---|---|---|---|---|
0 | arrival | 30 | 350 | Arrival | NaN |
1 | operator_wait_begins | 205 | 270 | Waiting for Operator | NaN |
2 | operator_begins | 210 | 210 | Speaking to operator | n_operators |
3 | nurse_wait_begins | 205 | 110 | Waiting for Nurse | NaN |
4 | nurse_begins | 210 | 50 | Speaking to Nurse | n_nurses |
5 | exit | 270 | 10 | Exit | NaN |
Finally, we can create the animation.
# Create animation
= model_params()
params
animate_activity_log(=event_log_test,
event_log=event_position_df,
event_position_df=model_params(),
scenario=True,
debug_mode=False,
setup_mode=1,
every_x_time_units=True,
include_play_button=20,
entity_icon_size=8,
gap_between_entities=25,
gap_between_queue_rows=700,
plotly_height=200,
frame_duration=1200,
plotly_width=300,
override_x_max=300,
override_y_max=RESULTS_COLLECTION_PERIOD,
limit_duration=25,
wrap_queues_at=50,
wrap_resources_at=75,
step_snapshot_max="dhm",
time_display_units=True,
display_stage_labels )
Animation function called at 15:52:26
Iteration through time-unit-by-time-unit logs complete 15:52:31
Snapshot df concatenation complete at 15:52:31
Reshaped animation dataframe finished construction at 15:52:31
Placement dataframe finished construction at 15:52:31
Output animation generation complete at 15:52:35
Total Time Elapsed: 9.56 seconds