from vidigi.utils import EventPosition, create_event_position_df
from vidigi.prep import reshape_for_animations, generate_animation_df
from vidigi.animation import generate_animation, animate_activity_log
import pandas as pd
import os
import random
from plotly.subplots import make_subplots
import plotly.io as pio
import plotly.graph_objects as go
import plotly.express as px
= "notebook" pio.renderers.default
Visualising a more complex status alongside entity icons - gas station with individual fuel tank level
View Imported Code, which has had logging steps added at the appropriate points in the ‘model’ class
"""
Gas Station Refueling example
Covers:
- Resources: Resource
- Resources: Container
- Waiting for other processes
Scenario:
A gas station has a limited number of gas pumps that share a common
fuel reservoir. Cars randomly arrive at the gas station, request one
of the fuel pumps and start refueling from that reservoir.
A gas station control process observes the gas station's fuel level
and calls a tank truck for refueling if the station's level drops
below a threshold.
"""
import itertools
import random
from vidigi.resources import VidigiStore
from vidigi.animation import animate_activity_log
from vidigi.logging import EventLogger
from vidigi.utils import EventPosition, create_event_position_df
import simpy
# fmt: off
= 42
RANDOM_SEED = 600 # MODIFIED FROM EXAMPLE: Size of the gas station tank (liters)
STATION_TANK_SIZE = 25 # Station tank minimum level (% of full)
THRESHOLD = 50 # Size of car fuel tanks (liters)
CAR_TANK_SIZE = [5, 25] # Min/max levels of car fuel tanks (liters)
CAR_TANK_LEVEL = [30, 90] # MODIFICATION: Time it takes to pay
PAYMENT_TIME = 1 # MODIFIED FROM EXAMPLE: Rate of refuelling car fuel tank (liters / second)
REFUELING_SPEED = 300 # Time it takes tank truck to arrive (seconds)
TANK_TRUCK_ARRIVAL_TIME = 1000 # MODIFICATION: Time it takes tank truck to fill the station tank (seconds)
TANK_TRUCK_REFUEL_TIME = [30, 300] # Interval between car arrivals [min, max] (seconds)
T_INTER = 60*60*6 # Simulation duration (seconds)
SIM_TIME # fmt: on
def car(name, env, gas_station, station_tank, logger):
"""A car arrives at the gas station for refueling.
It requests one of the gas station's fuel pumps and tries to get the
desired amount of fuel from it. If the station's fuel tank is
depleted, the car has to wait for the tank truck to arrive.
"""
= random.randint(*CAR_TANK_LEVEL)
car_tank_level =name)
logger.log_arrival(entity_idprint(f'{env.now:6.1f} s: {name} arrived at gas station')
=name, event='pump_queue_wait_begins',
logger.log_queue(entity_id=car_tank_level, fuel_level_end=CAR_TANK_SIZE)
fuel_level_startwith gas_station.request() as req:
# Request one of the gas pumps
= yield req
gas_pump
# Get the required amount of fuel
= CAR_TANK_SIZE - car_tank_level
fuel_required yield station_tank.get(fuel_required)
=name, event="payment_begins",
logger.log_resource_use_start(entity_id=gas_pump.id_attribute,
resource_id=car_tank_level, fuel_level_end=CAR_TANK_SIZE)
fuel_level_start
yield env.timeout(random.randint(*PAYMENT_TIME))
=name, event="payment_ends",
logger.log_resource_use_end(entity_id=gas_pump.id_attribute,
resource_id=car_tank_level, fuel_level_end=CAR_TANK_SIZE)
fuel_level_start
=name, event="pumping_begins",
logger.log_resource_use_start(entity_id=gas_pump.id_attribute,
resource_id=car_tank_level, fuel_level_end=CAR_TANK_SIZE)
fuel_level_start
# The "actual" refueling process takes some time
yield env.timeout(fuel_required / REFUELING_SPEED)
=name, event="pumping_ends",
logger.log_resource_use_end(entity_id=gas_pump.id_attribute,
resource_id=car_tank_level, fuel_level_end=CAR_TANK_SIZE)
fuel_level_start
print(f'{env.now:6.1f} s: {name} refueled with {fuel_required:.1f}L')
=name)
logger.log_departure(entity_id
def gas_station_control(env, station_tank, logger):
"""Periodically check the level of the gas station tank and call the tank
truck if the level falls below a threshold."""
= 0
truck_call_id
while True:
if station_tank.level / station_tank.capacity * 100 < THRESHOLD:
# We need to call the tank truck now!
=f"Call {truck_call_id}")
logger.log_arrival(entity_id=f"Call {truck_call_id}", event="calling_truck")
logger.log_queue(entity_idprint(f'{env.now:6.1f} s: Calling tank truck')
# Wait for the tank truck to arrive and refuel the station tank
yield env.process(tank_truck(env, station_tank, logger, truck_call_id))
+= 1
truck_call_id
yield env.timeout(120) # Check every 120 seconds
# def tank_truck(env, station_tank, logger, truck_call_id):
# """Arrives at the gas station after a certain delay and refuels it."""
# yield env.timeout(TANK_TRUCK_ARRIVAL_TIME)
# logger.log_departure(entity_id=f"Call {truck_call_id}")
# logger.log_arrival(entity_id=f"Truck {truck_call_id}")
# amount = station_tank.capacity - station_tank.level
# logger.log_queue(entity_id=f"Truck {truck_call_id}", event="refueling")
# yield env.timeout(TANK_TRUCK_REFUEL_TIME)
# station_tank.put(amount)
# print(
# f'{env.now:6.1f} s: Tank truck arrived and refuelled station with {amount:.1f}L'
# )
# logger.log_departure(entity_id=f"Truck {truck_call_id}")
# Modification to make refuelling a smooth, loggable process
def tank_truck(env, station_tank, logger, truck_call_id):
"""Tank truck arrives and refuels the station tank for a fixed duration."""
yield env.timeout(TANK_TRUCK_ARRIVAL_TIME)
=f"Call {truck_call_id}")
logger.log_departure(entity_id=f"Truck {truck_call_id}")
logger.log_arrival(entity_id=f"Truck {truck_call_id}", event="refuelling")
logger.log_queue(entity_id
= TANK_TRUCK_REFUEL_TIME # total time truck stays
refuel_time = 10 # L/s (or adjust based on need)
refuel_rate = 1 # seconds between each refill step
step
= 0
total_refueled = 0
elapsed
while (elapsed < refuel_time) | station_tank.level < (STATION_TANK_SIZE - (STATION_TANK_SIZE*0.02)):
yield env.timeout(step)
+= step
elapsed
= refuel_rate * step
increment = station_tank.capacity - station_tank.level
space_available = min(increment, space_available)
actual_increment
if actual_increment > 0:
station_tank.put(actual_increment)+= actual_increment
total_refueled
print(f'{env.now:6.1f} s: Truck {truck_call_id} refueled station with {total_refueled:.1f}L')
=f"Truck {truck_call_id}")
logger.log_departure(entity_id
def car_generator(env, gas_station, station_tank, logger):
"""Generate new cars that arrive at the gas station."""
for i in itertools.count():
yield env.timeout(random.randint(*T_INTER))
f'Car {i}', env, gas_station, station_tank, logger))
env.process(car(
def fuel_monitor(env, station_tank, logger, interval=1):
"""Logs the fuel level at regular intervals."""
while True:
logger.log_queue(="StationTank",
entity_id="fuel_level_change",
event_type="fuel_level_change",
event=station_tank.level
value
)yield env.timeout(interval)
# Setup and start the simulation
print('Gas Station refuelling')
random.seed(RANDOM_SEED)
# Create environment and start processes
= simpy.Environment()
env = VidigiStore(env, num_resources=2)
gas_station = simpy.Container(env, capacity=STATION_TANK_SIZE, init=STATION_TANK_SIZE)
station_tank = EventLogger(env=env)
logger ="parameter", event_type="parameter", event="tank_size", value=STATION_TANK_SIZE)
logger.log_queue(entity_id
env.process(gas_station_control(env, station_tank, logger))
env.process(car_generator(env, gas_station, station_tank, logger))
env.process(fuel_monitor(env, station_tank, logger))
# Execute!
=SIM_TIME)
env.run(until
"gas_station_log.csv") logger.to_csv(
# Define positions for animation
= create_event_position_df([
event_positions ='arrival', x=0, y=350, label="Entrance"),
EventPosition(event='pump_queue_wait_begins', x=400, y=350, label="Queue"),
EventPosition(event='payment_begins', x=340, y=175, resource='num_pumps',
EventPosition(event="Pumping Gas"),
label='pumping_begins', x=340, y=175, resource='num_pumps',
EventPosition(event="Pumping Gas"),
label='calling_truck', x=140, y=50,
EventPosition(event="Calling Truck"),
label='refuelling', x=340, y=50,
EventPosition(event="Truck Filling Tank"),
label='depart', x=250, y=50, label="Exit")
EventPosition(event
])
class Params:
def __init__(self):
self.num_pumps = 2
= [ "🚗", "🚙", "🚓",
icon_list "🚗", "🚙", "🏍️", "🏍️",
"🚗", "🚙", "🚑",
"🚗", "🚙", "🛻",
"🚗", "🚙", "🚛",
"🚗", "🚙", "🚕",
"🚗", "🚙", "🚒",
"🚗", "🚙", "🚑"]
random.shuffle(icon_list)
= pd.read_csv("gas_station_log.csv") event_log_df
= 6
STEP_SNAPSHOT_MAX = 60*60*3
LIMIT_DURATION = 3 WRAP_QUEUES_AT
= reshape_for_animations(
full_entity_df =event_log_df,
event_log=5,
every_x_time_units=STEP_SNAPSHOT_MAX,
step_snapshot_max=LIMIT_DURATION,
limit_duration=True
debug_mode
)
= generate_animation_df(
full_entity_df_plus_pos =full_entity_df,
full_entity_df=event_positions,
event_position_df=WRAP_QUEUES_AT,
wrap_queues_at=STEP_SNAPSHOT_MAX,
step_snapshot_max=150,
gap_between_entities=180,
gap_between_resources=150,
gap_between_queue_rows# gap_between_resource_rows=60,
=True,
debug_mode=icon_list
custom_entity_icon_list )
Iteration through time-unit-by-time-unit logs complete 17:55:30
Snapshot df concatenation complete at 17:55:30
Placement dataframe finished construction at 17:55:30
def build_fuel_bar(value, max_value=50, length=10):
"""Create an ASCII bar to show fuel level."""
try:
if value is None or (isinstance(value, float) and (value != value)): # check for None or NaN
= 0
proportion else:
= min(max(value / max_value, 0), 1)
proportion except Exception:
= 0 # fallback
proportion
= int(proportion * length)
filled = length - filled
empty = "█"
filled_icon = "░"
empty_icon return "[" + filled_icon * filled + empty_icon * empty + "]"
def custom_icon_rules(row):
= row.get("icon", "")
icon = row.get("entity_id", "")
entity_id = row.get("event", "")
event = row.get("fuel_level_start", None) # Only for cars
fuel_level_start
if "more" not in str(icon):
if isinstance(entity_id, str):
if "Truck" in entity_id:
return "🚚 Truck is refilling the tank..."
elif "Call" in entity_id:
return "☎️ Calling Truck!"
elif "Car" in entity_id:
= ""
bar if (event == "arrival" or event == "pump_queue_wait_begins") and fuel_level_start is not None:
= " " + build_fuel_bar(fuel_level_start)
bar return icon + "<br>" + bar + "<br><br>"
elif event == "payment_begins" and fuel_level_start is not None:
= " " + build_fuel_bar(fuel_level_start)
bar return icon+ "<br>" + bar + "<br> Paying"
elif event == "pumping_begins" and fuel_level_start is not None:
= row["time"]
arrival_time = max(float(row["snapshot_time"]) - float(arrival_time), 0)
elapsed = min(fuel_level_start + elapsed * 1, 50)
current_fuel = " " + build_fuel_bar(current_fuel)
bar return icon+ "<br>" + bar + "<br> Pumping"
elif event == "departure" or event == "pumping_ends":
= " " + build_fuel_bar(50) # Car is full when it leaves
bar return icon+ "<br>" + bar + "<br> <br>"
else:
return icon
return icon
= full_entity_df_plus_pos.assign(
full_entity_df_plus_pos =full_entity_df_plus_pos.apply(custom_icon_rules, axis=1)
icon )
= generate_animation(
fig =full_entity_df_plus_pos.sort_values(['entity_id', 'snapshot_time']),
full_entity_df_plus_pos= event_positions,
event_position_df=Params(),
scenario="seconds",
simulation_time_unit=900,
plotly_height=1200,
plotly_width=500,
override_x_max=750,
override_y_max=30,
entity_icon_size=180,
gap_between_resources=False,
display_stage_labels# resource_opacity=1,
=0,
resource_opacity=False,
setup_mode# custom_resource_icon="⛽",
=40,
resource_icon_size="https://raw.githubusercontent.com/hsma-tools/vidigi/refs/heads/main/examples/example_15_gas_station_refuelling/gas_station.png",
add_background_image=1, # New parameter in 1.1.0
background_image_opacity="white", # New parameter in 1.1.0
overflow_text_color="09:00:00",
start_time="%H:%M:%S",
time_display_units=True,
debug_mode=100,
frame_duration=100
frame_transition_duration
)
fig
Output animation generation complete at 17:55:39
= event_log_df[(event_log_df["event_type"]=="fuel_level_change") &
fuel_level_change_df "time"] % 5 == 0) &
(event_log_df["time"] < LIMIT_DURATION)]
(event_log_df[
="entity_id", y="value", animation_frame="time", range_y=[0,400]) px.bar(fuel_level_change_df, x
Explore incorporating the fuel level bar plot as an additional synchronised plot
## Same as before, but increase the height to give space for some of it to be taken up by the bar plot later
= generate_animation(
fig =full_entity_df_plus_pos.sort_values(['entity_id', 'snapshot_time']),
full_entity_df_plus_pos= event_positions,
event_position_df=Params(),
scenario="seconds",
simulation_time_unit=1000,
plotly_height=1200,
plotly_width=500,
override_x_max=750,
override_y_max=30,
entity_icon_size=180,
gap_between_resources=False,
display_stage_labels# resource_opacity=1,
=0,
resource_opacity=False,
setup_mode# custom_resource_icon="⛽",
=40,
resource_icon_size="https://raw.githubusercontent.com/hsma-tools/vidigi/refs/heads/main/examples/example_15_gas_station_refuelling/gas_station.png",
add_background_image=1, # New parameter in 1.1.0
background_image_opacity="white", # New parameter in 1.1.0
overflow_text_color="09:00:00",
start_time="%H:%M:%S",
time_display_units=True,
debug_mode=100,
frame_duration=100
frame_transition_duration )
Output animation generation complete at 17:55:53
# Set up the desired subplot layout
= 2
ROWS
= make_subplots(
sp =ROWS,
rows=1,
cols=[0.75, 0.25],
row_heights=0.05,
vertical_spacing=(
subplot_titles"", # Original Animation
"Station Tank Fuel Level", # Fuel Tank Level
)
)
# Overwrite the domain of our original x and y axis with domain from the new axis
'xaxis']['domain'] = sp.layout['xaxis']['domain']
fig.layout['yaxis']['domain'] = sp.layout['yaxis']['domain']
fig.layout[
for i in range(2, ROWS+1):
# Add in the attributes for the secondary axis from our subplot
f'xaxis{i}'] = sp.layout[f'xaxis{i}']
fig.layout[f'yaxis{i}'] = sp.layout[f'yaxis{i}']
fig.layout[
= sp._grid_ref fig._grid_ref
# First, extract the trace containing the resource icons
# icon_trace = fig.data[1]
# Now keep our figure data as just the initial trace.
= (fig.data[0],)
fig.data
# 1. RESOURCE ICONS TRACE
# Readd the resource icons trace in a consistent manner
# Confusingly, when we start messing with the naimation frames, we lose the resource icon trace
# even though it appeared fine until this point - so we have to handle it here
# fig.add_trace(icon_trace)
# 2. BAR PLOT ON SECONDARY AXIS (animated barplot in subplot)
# Initialize with a single point and assign it to subplot axes (x2/y2)
# Get unique time points
= fuel_level_change_df["time"].unique()
time_points
# Initial frame (first time point)
= time_points[0]
initial_time = fuel_level_change_df[fuel_level_change_df["time"] == initial_time]
initial_df
# Create the initial bar trace
fig.add_trace(go.Bar(=[fuel_level_change_df["entity_id"].values[0]],
x=[fuel_level_change_df["value"].values[0]],
y=False
showlegend# We place it in our new subplot using the following line
=2, col=1) ), row
# # Now ensure we tell it which traces we are animating
# # (as per https://chart-studio.plotly.com/~empet/15243/animating-traces-in-subplotsbr/#/)
for i, frame in enumerate(fig.frames):
# Your original frame.data
# This will be a tuple
# We'll ensure we only take the first entry
# original_data = (frame.data[0], )
= frame.data
original_data
# The new data you want to add for this specific frame
= (
new_data # 0: resource icons
# icon_trace,
go.Bar(= [fuel_level_change_df.sort_values('time')['entity_id'].values[i]],
x= [fuel_level_change_df.sort_values('time')['value'].values[i]]
y# This needs to be a tuple even if we're only adding a single additional trace, hence the comma
) ,
)
= original_data + new_data frame.data
fig