Precalculated pathing

import os
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 plotly.io as pio
pio.renderers.default = "notebook"
import simpy
import random
from typing import Tuple
from vidigi.logging import EventLogger


# ---------------------------
# Warehouse Layout (scaled)
# ---------------------------
# Coordinates in pixels for animation space
LAYOUT = {
    # Packing & maintenance
    "packing": (270, 270),
    "maintenance": (600, 300),

    # 8 pickup points
    "pickup_1": (25, 500),
    "pickup_2": (180, 500),
    "pickup_3": (70, 500),
    "pickup_4": (550, 500),
    "pickup_5": (25, 100),
    "pickup_6": (180, 100),
    "pickup_7": (370, 100),
    "pickup_8": (550, 100),
}

PICKUP_POINTS = list(name for name in LAYOUT if name.startswith("pickup"))

SPEED = 100.0  # pixels per minute
PACKING_TIME = 5
PACKAGES_PER_BATCH = (1, 5)
OTHER_TASK_TIME = (2, 4)
OTHER_TASK_PROB = 0.3
SIM_DURATION = 60*24


def travel_time(pos_a: Tuple[int, int], pos_b: Tuple[int, int]) -> float:
    """Calculate travel time between two coordinates."""
    dist = ((pos_a[0] - pos_b[0]) ** 2 + (pos_a[1] - pos_b[1]) ** 2) ** 0.5
    return dist / SPEED


class PackingRobot:
    def __init__(self, env, name, logger):
        self.env = env
        self.name = name
        self.logger = logger
        self.pos = LAYOUT["packing"]  # start at packing station
        self.env.process(self.poll_position())
        self.logger.log_arrival(entity_id=self.name,
                                x=self.pos[0], y=self.pos[1])
        self.logger.log_queue(entity_id=self.name, event="packing",
                               x=self.pos[0], y=self.pos[1])

    def poll_position(self):
        """Logs position every 1 sim time unit, even if idle."""
        while True:
            self.logger.log_custom_event(entity_id=self.name,
                                         event_type="position_poll",
                                         event="position",
                                         x=self.pos[0], y=self.pos[1])
            yield self.env.timeout(1)

    def move_to(self, location_name, pathway, outbound=True):
        """Move robot to a location using Manhattan path.
        outbound=True: horizontal then vertical
        outbound=False: retrace return path (vertical then horizontal)
        """
        destination = LAYOUT[location_name]
        start_x, start_y = self.pos
        dest_x, dest_y = destination

        if outbound:
            sequence = [("x", dest_x - start_x), ("y", dest_y - start_y)]
        else:
            sequence = [("y", dest_y - start_y), ("x", dest_x - start_x)]

        for axis, delta in sequence:
            if delta != 0:
                travel_time = abs(delta) / SPEED
                steps = int(travel_time)
                remaining = travel_time - steps
                move_per_unit = delta / travel_time

                for _ in range(steps):
                    yield self.env.timeout(1)
                    if axis == "x":
                        self.pos = (self.pos[0] + move_per_unit, self.pos[1])
                    else:
                        self.pos = (self.pos[0], self.pos[1] + move_per_unit)
                if remaining > 0:
                    yield self.env.timeout(remaining)
                    if axis == "x":
                        self.pos = (self.pos[0] + move_per_unit * remaining, self.pos[1])
                    else:
                        self.pos = (self.pos[0], self.pos[1] + move_per_unit * remaining)


    def pickup_packages(self, count, pickup_name):
        yield self.env.process(self.move_to(pickup_name, "to_pickup", outbound=True))
        # self.logger.log_custom_event(entity_id=self.name, event_type="action",
        #                              event=f"picked_up_{count}_packages",
        #                              x=self.pos[0], y=self.pos[1],
        #                              location=pickup_name)

        yield self.env.process(self.move_to("packing", "to_packing", outbound=False))


        self.logger.log_queue(entity_id=self.name,
                                    event=pickup_name,
                                    x=self.pos[0], y=self.pos[1],
                                    package_count=count)
        for i in range(count):
            yield self.env.timeout(PACKING_TIME)

        if random.random() < OTHER_TASK_PROB:
            yield self.env.process(self.other_task())

        # Go back to the packing station
        self.logger.log_queue(entity_id=self.name,
                                    event="packing",
                                    x=self.pos[0], y=self.pos[1])



    def other_task(self):
        yield self.env.process(self.move_to("maintenance", "to_maintenance"))
        task_time = random.randint(*OTHER_TASK_TIME)
        self.logger.log_queue(entity_id=self.name,
                                     event="maintenance",
                                     x=self.pos[0], y=self.pos[1],
                                     task_duration_mins=task_time
                                     )
        yield self.env.timeout(task_time)
        yield self.env.process(self.move_to("packing", "return_from_maintenance"))
        # Logging of return to packing location will be handled in pickup_packages process


def package_arrival(env, robot):
    while True:
        yield env.timeout(random.randint(4, 8))
        num_packages = random.randint(*PACKAGES_PER_BATCH)
        pickup_name = random.choice(PICKUP_POINTS)
        yield env.process(robot.pickup_packages(num_packages, pickup_name))




# ---------------------------
# Running the simulation
# ---------------------------
if __name__ == "__main__":
    env = simpy.Environment()
    logger = EventLogger(env=env)
    robot = PackingRobot(env, "RoboPack-1", logger)
    env.process(package_arrival(env, robot))
    env.run(until=SIM_DURATION)
    logger.log_departure(entity_id=robot.name,
                            x=robot.pos[0], y=robot.pos[1])

    logger.to_csv("robot_log.csv")
# Define positions for animation
event_positions = create_event_position_df([
    EventPosition(event='arrival', x=0, y=550, label="Entrance"),

    EventPosition(event='pickup_1', x=40, y=500, label="Pickup 1"),
    EventPosition(event='pickup_2', x=170, y=500, label="Pickup 2"),
    EventPosition(event='pickup_3', x=350, y=500, label="Pickup 3"),
    EventPosition(event='pickup_4', x=500, y=500, label="Pickup 4"),
    EventPosition(event='pickup_5', x=40, y=60, label="Pickup 5"),
    EventPosition(event='pickup_6', x=170, y=60, label="Pickup 6"),
    EventPosition(event='pickup_7', x=350, y=60, label="Pickup 7"),
    EventPosition(event='pickup_8', x=500, y=60, label="Pickup 8"),

    EventPosition(event='packing', x=300, y=300, label="Packing"),
    EventPosition(event='maintenance', x=600, y=300, label="Maintenance"),

    EventPosition(event='depart', x=650, y=50, label="Exit")
])
event_log_df = pd.read_csv("robot_log.csv")
event_log_df.head()
entity_id event_type event time pathway run_number timestamp resource_id x y package_count task_duration_mins
0 RoboPack-1 arrival_departure arrival 0.0 NaN NaN NaN NaN 270.0 270.0 NaN NaN
1 RoboPack-1 queue packing 0.0 NaN NaN NaN NaN 270.0 270.0 NaN NaN
2 RoboPack-1 position_poll position 0.0 NaN NaN NaN NaN 270.0 270.0 NaN NaN
3 RoboPack-1 position_poll position 1.0 NaN NaN NaN NaN 270.0 270.0 NaN NaN
4 RoboPack-1 position_poll position 2.0 NaN NaN NaN NaN 270.0 270.0 NaN NaN
STEP_SNAPSHOT_MAX = 999
LIMIT_DURATION = int(max(event_log_df[event_log_df["event_type"]!="position_poll"]['time']))
WRAP_QUEUES_AT = 999
event_log_df_filtered = event_log_df[~event_log_df["event_type"].isin(["position_poll", "action"])][['entity_id', 'event_type', 'event', 'time', 'pathway']]
event_log_df_filtered.head(10)
entity_id event_type event time pathway
0 RoboPack-1 arrival_departure arrival 0.0 NaN
1 RoboPack-1 queue packing 0.0 NaN
13 RoboPack-1 queue pickup_2 10.4 NaN
23 RoboPack-1 queue maintenance 19.0 NaN
29 RoboPack-1 queue packing 24.6 NaN
46 RoboPack-1 queue pickup_8 40.6 NaN
67 RoboPack-1 queue packing 60.6 NaN
86 RoboPack-1 queue pickup_1 78.1 NaN
97 RoboPack-1 queue packing 88.1 NaN
114 RoboPack-1 queue pickup_8 104.1 NaN

We’ll first run this while allowing vidigi to handle the pathing. This means that the path between each step will be interpolated.

animate_activity_log(
    event_log=event_log_df_filtered,
    event_position_df=event_positions,
    wrap_queues_at=WRAP_QUEUES_AT,
    limit_duration=LIMIT_DURATION,
    step_snapshot_max=STEP_SNAPSHOT_MAX,
    every_x_time_units=1,
    debug_mode=True,
    custom_entity_icon_list=["🤖"]
)
Animation function called at 13:45:39
Iteration through time-unit-by-time-unit logs complete 13:45:44
Snapshot df concatenation complete at 13:45:45
Reshaped animation dataframe finished construction at 13:45:45
Placement dataframe finished construction at 13:45:45
Output animation generation complete at 13:45:50
Total Time Elapsed: 11.08 seconds

If we were to put in a background illustrating the paths the robots should follow, this will look bad:

animate_activity_log(
    event_log=event_log_df_filtered,
    event_position_df=event_positions,
    wrap_queues_at=WRAP_QUEUES_AT,
    limit_duration=LIMIT_DURATION,
    step_snapshot_max=STEP_SNAPSHOT_MAX,
    every_x_time_units=1,
    debug_mode=True,
    display_stage_labels=False,
    custom_entity_icon_list=["🤖"],
    add_background_image="https://raw.githubusercontent.com/hsma-tools/vidigi/refs/heads/main/examples/example_16_packing_robot/warehouse.png",
    background_image_opacity=1, # New parameter in 1.1.0
    override_x_max=650,
    override_y_max=550,
    plotly_width=1300,
    plotly_height=800,
)
Animation function called at 13:45:51
Iteration through time-unit-by-time-unit logs complete 13:45:56
Snapshot df concatenation complete at 13:45:56
Reshaped animation dataframe finished construction at 13:45:56
Placement dataframe finished construction at 13:45:57
Output animation generation complete at 13:46:02
Total Time Elapsed: 11.19 seconds

However, in this particular model, we have been polling the location of the robot after every step.

This might be imporant to visualise accurately in certain models - for example, to demonstrate why a robot might have to wait for another robot to move out of the way, and to assure stakeholders that such a thing has been recorded accurately.

Let’s see how we can combine vidigi with these polled locations.

full_entity_df = reshape_for_animations(
    event_log=event_log_df[~event_log_df["event_type"].isin(["position_poll", "action"])][['entity_id', 'event_type', 'event', 'time', 'pathway']],
    limit_duration=LIMIT_DURATION,
    step_snapshot_max=STEP_SNAPSHOT_MAX,
    every_x_time_units=1,
    debug_mode=True
    )

full_entity_df
Iteration through time-unit-by-time-unit logs complete 13:46:08
Snapshot df concatenation complete at 13:46:08
index entity_id event_type event time rank snapshot_time
0 1 RoboPack-1 queue packing 0.0 1.0 0
1 1 RoboPack-1 queue packing 0.0 1.0 1
2 1 RoboPack-1 queue packing 0.0 1.0 2
3 1 RoboPack-1 queue packing 0.0 1.0 3
4 1 RoboPack-1 queue packing 0.0 1.0 4
... ... ... ... ... ... ... ...
1436 1535 RoboPack-1 queue packing 1428.4 1.0 1436
1437 1535 RoboPack-1 queue packing 1428.4 1.0 1437
1438 1535 RoboPack-1 queue packing 1428.4 1.0 1438
1439 1535 RoboPack-1 queue packing 1428.4 1.0 1439
1440 1547 RoboPack-1 arrival_departure depart 1440.0 1.0 1440

1441 rows × 7 columns

full_entity_df_plus_pos = generate_animation_df(
    full_entity_df=full_entity_df,
    event_position_df=event_positions,
    wrap_queues_at=WRAP_QUEUES_AT,
    step_snapshot_max=STEP_SNAPSHOT_MAX,
    debug_mode=True,
    custom_entity_icon_list=["🤖"]
    )

full_entity_df_plus_pos
Placement dataframe finished construction at 13:46:08
index entity_id event_type event time rank snapshot_time x y_final label x_final row icon opacity
109 1 RoboPack-1 queue packing 0.0 1.0 0 300 300.0 Packing 300.0 0.0 🤖 1.0
110 1 RoboPack-1 queue packing 0.0 1.0 1 300 300.0 Packing 300.0 0.0 🤖 1.0
111 1 RoboPack-1 queue packing 0.0 1.0 2 300 300.0 Packing 300.0 0.0 🤖 1.0
112 1 RoboPack-1 queue packing 0.0 1.0 3 300 300.0 Packing 300.0 0.0 🤖 1.0
113 1 RoboPack-1 queue packing 0.0 1.0 4 300 300.0 Packing 300.0 0.0 🤖 1.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
739 1535 RoboPack-1 queue packing 1428.4 1.0 1435 300 300.0 Packing 300.0 0.0 🤖 1.0
740 1535 RoboPack-1 queue packing 1428.4 1.0 1436 300 300.0 Packing 300.0 0.0 🤖 1.0
741 1535 RoboPack-1 queue packing 1428.4 1.0 1437 300 300.0 Packing 300.0 0.0 🤖 1.0
742 1535 RoboPack-1 queue packing 1428.4 1.0 1438 300 300.0 Packing 300.0 0.0 🤖 1.0
743 1535 RoboPack-1 queue packing 1428.4 1.0 1439 300 300.0 Packing 300.0 0.0 🤖 1.0

1440 rows × 14 columns

Now we can replace our calculated x_final and y_final coordinates with the values from the polling.

First, we pull this data out of our event logs and rename the columns to match the relevant columns in our transformed event logging dataset.

entity_position_df = event_log_df[event_log_df["event_type"]=="position_poll"][['entity_id', 'time', 'x', 'y']].reset_index(drop=True)
entity_position_df = entity_position_df.rename(columns={"x": "x_final", "y": "y_final", "time": "snapshot_time"})
entity_position_df
entity_id snapshot_time x_final y_final
0 RoboPack-1 0.0 270.0 270.0
1 RoboPack-1 1.0 270.0 270.0
2 RoboPack-1 2.0 270.0 270.0
3 RoboPack-1 3.0 270.0 270.0
4 RoboPack-1 4.0 270.0 270.0
... ... ... ... ...
1435 RoboPack-1 1435.0 25.0 270.0
1436 RoboPack-1 1436.0 25.0 370.0
1437 RoboPack-1 1437.0 25.0 470.0
1438 RoboPack-1 1438.0 25.0 500.0
1439 RoboPack-1 1439.0 25.0 400.0

1440 rows × 4 columns

We then drop our original x_final and y_final columns from our transformed dataset, replacing them with those from the polling in our model.

full_entity_df_plus_pos_manual_locations = (
    full_entity_df_plus_pos
    .drop(columns=["x_final", "y_final"])
    .merge(entity_position_df, on=["entity_id", "snapshot_time"])
)

full_entity_df_plus_pos_manual_locations
index entity_id event_type event time rank snapshot_time x label row icon opacity x_final y_final
0 1 RoboPack-1 queue packing 0.0 1.0 0 300 Packing 0.0 🤖 1.0 270.0 270.0
1 1 RoboPack-1 queue packing 0.0 1.0 1 300 Packing 0.0 🤖 1.0 270.0 270.0
2 1 RoboPack-1 queue packing 0.0 1.0 2 300 Packing 0.0 🤖 1.0 270.0 270.0
3 1 RoboPack-1 queue packing 0.0 1.0 3 300 Packing 0.0 🤖 1.0 270.0 270.0
4 1 RoboPack-1 queue packing 0.0 1.0 4 300 Packing 0.0 🤖 1.0 270.0 270.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1435 1535 RoboPack-1 queue packing 1428.4 1.0 1435 300 Packing 0.0 🤖 1.0 25.0 270.0
1436 1535 RoboPack-1 queue packing 1428.4 1.0 1436 300 Packing 0.0 🤖 1.0 25.0 370.0
1437 1535 RoboPack-1 queue packing 1428.4 1.0 1437 300 Packing 0.0 🤖 1.0 25.0 470.0
1438 1535 RoboPack-1 queue packing 1428.4 1.0 1438 300 Packing 0.0 🤖 1.0 25.0 500.0
1439 1535 RoboPack-1 queue packing 1428.4 1.0 1439 300 Packing 0.0 🤖 1.0 25.0 400.0

1440 rows × 14 columns

Finally, we then generate our animation as usual - making sure to refer to our updated dataframe.

fig = generate_animation(
        full_entity_df_plus_pos=full_entity_df_plus_pos_manual_locations.sort_values(['entity_id', 'snapshot_time']),
        event_position_df= event_positions,
        simulation_time_unit="seconds",
        display_stage_labels=False,
        setup_mode=False,
        start_time="07:00:00",
        time_display_units="%H:%M:%S",
        debug_mode=True,
        add_background_image="https://raw.githubusercontent.com/hsma-tools/vidigi/refs/heads/main/examples/example_16_packing_robot/warehouse.png",
        background_image_opacity=1, # New parameter in 1.1.0
        override_x_max=650,
        override_y_max=550,
        plotly_width=1300,
        plotly_height=800,
        entity_icon_size=50,
        frame_duration=200,
        frame_transition_duration=300

    )

fig
Output animation generation complete at 13:46:13

Multiple Packers

Let’s finally repeat this with an instance with multiple packers.

In this very simplistic model, blocking of corridors is not implemented - but is the kind of thing that would visualise well with this kind of pre-calculated movement.

event_log_df_multiple = pd.read_csv("robot_log_multiple.csv")

full_entity_df_multiple = reshape_for_animations(
    event_log=event_log_df_multiple[~event_log_df_multiple["event_type"].isin(["position_poll", "action"])][['entity_id', 'event_type', 'event', 'time', 'pathway']],
    limit_duration=LIMIT_DURATION,
    step_snapshot_max=STEP_SNAPSHOT_MAX,
    every_x_time_units=1,
    debug_mode=True
    )

full_entity_df_plus_pos_multiple = generate_animation_df(
    full_entity_df=full_entity_df_multiple,
    event_position_df=event_positions,
    wrap_queues_at=WRAP_QUEUES_AT,
    step_snapshot_max=STEP_SNAPSHOT_MAX,
    debug_mode=True,
    custom_entity_icon_list=["🤖"]
    # custom_entity_icon_list=["🟦", "🟫", "🟪", "🟧", "🟥", "🟨", "🟩", "◻️"]
    )

full_entity_df_plus_pos_multiple
Iteration through time-unit-by-time-unit logs complete 13:46:22
Snapshot df concatenation complete at 13:46:22
Placement dataframe finished construction at 13:46:22
index entity_id event_type event time rank snapshot_time x y_final label x_final row icon opacity
10222 1 RoboPack-1 queue packing 0.00 1.0 0 300 300.0 Packing 300.0 0.0 🤖 1.0
10223 1 RoboPack-1 queue packing 0.00 1.0 1 300 300.0 Packing 300.0 0.0 🤖 1.0
10224 1 RoboPack-1 queue packing 0.00 1.0 2 300 300.0 Packing 300.0 0.0 🤖 1.0
10225 1 RoboPack-1 queue packing 0.00 1.0 3 300 300.0 Packing 300.0 0.0 🤖 1.0
10226 1 RoboPack-1 queue packing 0.00 1.0 4 300 300.0 Packing 300.0 0.0 🤖 1.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1106 12159 RoboPack-8 queue pickup_4 1423.15 1.0 1435 500 500.0 Pickup 4 500.0 0.0 🤖 1.0
1107 12159 RoboPack-8 queue pickup_4 1423.15 1.0 1436 500 500.0 Pickup 4 500.0 0.0 🤖 1.0
1108 12159 RoboPack-8 queue pickup_4 1423.15 1.0 1437 500 500.0 Pickup 4 500.0 0.0 🤖 1.0
1109 12159 RoboPack-8 queue pickup_4 1423.15 1.0 1438 500 500.0 Pickup 4 500.0 0.0 🤖 1.0
1110 12159 RoboPack-8 queue pickup_4 1423.15 1.0 1439 500 500.0 Pickup 4 500.0 0.0 🤖 1.0

11520 rows × 14 columns

entity_position_df_multiple = (
    event_log_df_multiple[event_log_df_multiple["event_type"]=="position_poll"]
    [['entity_id', 'time', 'x', 'y']]
    .reset_index(drop=True)
    .rename(columns={"x": "x_final", "y": "y_final", "time": "snapshot_time"})
    )

# entity_position_df_multiple["snapshot_time"] = entity_position_df_multiple["snapshot_time"].astype('int')

entity_position_df_multiple
entity_id snapshot_time x_final y_final
0 RoboPack-1 0.0 270.0 270.0
1 RoboPack-2 0.0 285.0 270.0
2 RoboPack-3 0.0 300.0 270.0
3 RoboPack-4 0.0 315.0 270.0
4 RoboPack-5 0.0 270.0 300.0
... ... ... ... ...
11515 RoboPack-4 1439.0 300.0 300.0
11516 RoboPack-5 1439.0 270.0 300.0
11517 RoboPack-6 1439.0 300.0 300.0
11518 RoboPack-7 1439.0 300.0 300.0
11519 RoboPack-8 1439.0 315.0 300.0

11520 rows × 4 columns

full_entity_df_plus_pos_manual_locations_multiple = (
    full_entity_df_plus_pos_multiple
    .drop(columns=["x_final", "y_final"])
    .merge(entity_position_df_multiple, on=["entity_id", "snapshot_time"])
)

full_entity_df_plus_pos_manual_locations_multiple
index entity_id event_type event time rank snapshot_time x label row icon opacity x_final y_final
0 1 RoboPack-1 queue packing 0.00 1.0 0 300 Packing 0.0 🤖 1.0 270.0 270.0
1 1 RoboPack-1 queue packing 0.00 1.0 1 300 Packing 0.0 🤖 1.0 270.0 270.0
2 1 RoboPack-1 queue packing 0.00 1.0 2 300 Packing 0.0 🤖 1.0 270.0 270.0
3 1 RoboPack-1 queue packing 0.00 1.0 3 300 Packing 0.0 🤖 1.0 270.0 270.0
4 1 RoboPack-1 queue packing 0.00 1.0 4 300 Packing 0.0 🤖 1.0 270.0 270.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
11515 12159 RoboPack-8 queue pickup_4 1423.15 1.0 1435 500 Pickup 4 0.0 🤖 1.0 315.0 300.0
11516 12159 RoboPack-8 queue pickup_4 1423.15 1.0 1436 500 Pickup 4 0.0 🤖 1.0 315.0 300.0
11517 12159 RoboPack-8 queue pickup_4 1423.15 1.0 1437 500 Pickup 4 0.0 🤖 1.0 315.0 300.0
11518 12159 RoboPack-8 queue pickup_4 1423.15 1.0 1438 500 Pickup 4 0.0 🤖 1.0 315.0 300.0
11519 12159 RoboPack-8 queue pickup_4 1423.15 1.0 1439 500 Pickup 4 0.0 🤖 1.0 315.0 300.0

11520 rows × 14 columns

fig_multiple = generate_animation(
        full_entity_df_plus_pos=full_entity_df_plus_pos_manual_locations_multiple.sort_values(['entity_id', 'snapshot_time']),
        event_position_df= event_positions,
        simulation_time_unit="seconds",
        display_stage_labels=False,
        setup_mode=False,
        start_time="07:00:00",
        time_display_units="%H:%M:%S",
        debug_mode=True,
        add_background_image="https://raw.githubusercontent.com/hsma-tools/vidigi/refs/heads/main/examples/example_16_packing_robot/warehouse.png",
        background_image_opacity=1, # New parameter in 1.1.0
        override_x_max=650,
        override_y_max=550,
        plotly_width=1300,
        plotly_height=800,
        entity_icon_size=30

    )

fig_multiple
Output animation generation complete at 13:46:27