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 16:38:11
Iteration through time-unit-by-time-unit logs complete 16:38:17
Snapshot df concatenation complete at 16:38:17
Reshaped animation dataframe finished construction at 16:38:17
Placement dataframe finished construction at 16:38:17
Output animation generation complete at 16:38:22
Total Time Elapsed: 10.60 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 16:38:22
Iteration through time-unit-by-time-unit logs complete 16:38:28
Snapshot df concatenation complete at 16:38:28
Reshaped animation dataframe finished construction at 16:38:28
Placement dataframe finished construction at 16:38:28
Output animation generation complete at 16:38:32
Total Time Elapsed: 10.22 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 16:38:38
Snapshot df concatenation complete at 16:38:38
index entity_id event_type event time pathway rank snapshot_time
0 1 RoboPack-1 queue packing 0.0 NaN 1.0 0
1 1 RoboPack-1 queue packing 0.0 NaN 1.0 1
2 1 RoboPack-1 queue packing 0.0 NaN 1.0 2
3 1 RoboPack-1 queue packing 0.0 NaN 1.0 3
4 1 RoboPack-1 queue packing 0.0 NaN 1.0 4
... ... ... ... ... ... ... ... ...
1436 1535 RoboPack-1 queue packing 1428.4 NaN 1.0 1436
1437 1535 RoboPack-1 queue packing 1428.4 NaN 1.0 1437
1438 1535 RoboPack-1 queue packing 1428.4 NaN 1.0 1438
1439 1535 RoboPack-1 queue packing 1428.4 NaN 1.0 1439
1440 1547 RoboPack-1 arrival_departure depart 1440.0 NaN 1.0 1440

1441 rows × 8 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 16:38:38
index entity_id event_type event time pathway rank snapshot_time x y_final label resource x_final row y icon opacity
0 23 RoboPack-1 queue maintenance 19.0 NaN 1.0 19 600 300.0 Maintenance None 600.0 0.0 NaN 🤖 1.0
1 23 RoboPack-1 queue maintenance 19.0 NaN 1.0 20 600 300.0 Maintenance None 600.0 0.0 NaN 🤖 1.0
2 23 RoboPack-1 queue maintenance 19.0 NaN 1.0 21 600 300.0 Maintenance None 600.0 0.0 NaN 🤖 1.0
3 23 RoboPack-1 queue maintenance 19.0 NaN 1.0 22 600 300.0 Maintenance None 600.0 0.0 NaN 🤖 1.0
4 23 RoboPack-1 queue maintenance 19.0 NaN 1.0 23 600 300.0 Maintenance None 600.0 0.0 NaN 🤖 1.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1435 1472 RoboPack-1 queue pickup_8 1370.7 NaN 1.0 1385 500 60.0 Pickup 8 None 500.0 0.0 NaN 🤖 1.0
1436 1472 RoboPack-1 queue pickup_8 1370.7 NaN 1.0 1386 500 60.0 Pickup 8 None 500.0 0.0 NaN 🤖 1.0
1437 1472 RoboPack-1 queue pickup_8 1370.7 NaN 1.0 1387 500 60.0 Pickup 8 None 500.0 0.0 NaN 🤖 1.0
1438 1472 RoboPack-1 queue pickup_8 1370.7 NaN 1.0 1388 500 60.0 Pickup 8 None 500.0 0.0 NaN 🤖 1.0
1439 1472 RoboPack-1 queue pickup_8 1370.7 NaN 1.0 1389 500 60.0 Pickup 8 None 500.0 0.0 NaN 🤖 1.0

1440 rows × 17 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 pathway rank snapshot_time x label resource row y icon opacity x_final y_final
0 23 RoboPack-1 queue maintenance 19.0 NaN 1.0 19 600 Maintenance None 0.0 NaN 🤖 1.0 600.0 270.0
1 23 RoboPack-1 queue maintenance 19.0 NaN 1.0 20 600 Maintenance None 0.0 NaN 🤖 1.0 600.0 300.0
2 23 RoboPack-1 queue maintenance 19.0 NaN 1.0 21 600 Maintenance None 0.0 NaN 🤖 1.0 600.0 300.0
3 23 RoboPack-1 queue maintenance 19.0 NaN 1.0 22 600 Maintenance None 0.0 NaN 🤖 1.0 500.0 300.0
4 23 RoboPack-1 queue maintenance 19.0 NaN 1.0 23 600 Maintenance None 0.0 NaN 🤖 1.0 400.0 300.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1435 1472 RoboPack-1 queue pickup_8 1370.7 NaN 1.0 1385 500 Pickup 8 None 0.0 NaN 🤖 1.0 270.0 270.0
1436 1472 RoboPack-1 queue pickup_8 1370.7 NaN 1.0 1386 500 Pickup 8 None 0.0 NaN 🤖 1.0 270.0 270.0
1437 1472 RoboPack-1 queue pickup_8 1370.7 NaN 1.0 1387 500 Pickup 8 None 0.0 NaN 🤖 1.0 370.0 270.0
1438 1472 RoboPack-1 queue pickup_8 1370.7 NaN 1.0 1388 500 Pickup 8 None 0.0 NaN 🤖 1.0 470.0 270.0
1439 1472 RoboPack-1 queue pickup_8 1370.7 NaN 1.0 1389 500 Pickup 8 None 0.0 NaN 🤖 1.0 600.0 270.0

1440 rows × 17 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 16:38:43

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 16:38:51
Snapshot df concatenation complete at 16:38:51
Placement dataframe finished construction at 16:38:51
index entity_id event_type event time pathway rank snapshot_time x y_final label resource x_final row y icon opacity
0 224 RoboPack-8 queue maintenance 24.9 NaN 1.0 25 600 300.0 Maintenance None 600.0 0.0 NaN 🤖 1.0
1 224 RoboPack-8 queue maintenance 24.9 NaN 1.0 26 600 300.0 Maintenance None 600.0 0.0 NaN 🤖 1.0
2 224 RoboPack-8 queue maintenance 24.9 NaN 1.0 27 600 300.0 Maintenance None 600.0 0.0 NaN 🤖 1.0
3 224 RoboPack-8 queue maintenance 24.9 NaN 1.0 28 600 300.0 Maintenance None 600.0 0.0 NaN 🤖 1.0
4 224 RoboPack-8 queue maintenance 24.9 NaN 1.0 29 600 300.0 Maintenance None 600.0 0.0 NaN 🤖 1.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
11515 4934 RoboPack-1 queue pickup_7 576.6 NaN 1.0 597 350 60.0 Pickup 7 None 350.0 0.0 NaN 🤖 1.0
11516 4934 RoboPack-1 queue pickup_7 576.6 NaN 1.0 598 350 60.0 Pickup 7 None 350.0 0.0 NaN 🤖 1.0
11517 4934 RoboPack-1 queue pickup_7 576.6 NaN 1.0 599 350 60.0 Pickup 7 None 350.0 0.0 NaN 🤖 1.0
11518 4934 RoboPack-1 queue pickup_7 576.6 NaN 1.0 600 350 60.0 Pickup 7 None 350.0 0.0 NaN 🤖 1.0
11519 4934 RoboPack-1 queue pickup_7 576.6 NaN 1.0 601 350 60.0 Pickup 7 None 350.0 0.0 NaN 🤖 1.0

11520 rows × 17 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 pathway rank snapshot_time x label resource row y icon opacity x_final y_final
0 224 RoboPack-8 queue maintenance 24.9 NaN 1.0 25 600 Maintenance None 0.0 NaN 🤖 1.0 600.0 300.0
1 224 RoboPack-8 queue maintenance 24.9 NaN 1.0 26 600 Maintenance None 0.0 NaN 🤖 1.0 600.0 300.0
2 224 RoboPack-8 queue maintenance 24.9 NaN 1.0 27 600 Maintenance None 0.0 NaN 🤖 1.0 600.0 300.0
3 224 RoboPack-8 queue maintenance 24.9 NaN 1.0 28 600 Maintenance None 0.0 NaN 🤖 1.0 600.0 300.0
4 224 RoboPack-8 queue maintenance 24.9 NaN 1.0 29 600 Maintenance None 0.0 NaN 🤖 1.0 600.0 300.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
11515 4934 RoboPack-1 queue pickup_7 576.6 NaN 1.0 597 350 Pickup 7 None 0.0 NaN 🤖 1.0 270.0 270.0
11516 4934 RoboPack-1 queue pickup_7 576.6 NaN 1.0 598 350 Pickup 7 None 0.0 NaN 🤖 1.0 270.0 270.0
11517 4934 RoboPack-1 queue pickup_7 576.6 NaN 1.0 599 350 Pickup 7 None 0.0 NaN 🤖 1.0 270.0 270.0
11518 4934 RoboPack-1 queue pickup_7 576.6 NaN 1.0 600 350 Pickup 7 None 0.0 NaN 🤖 1.0 270.0 270.0
11519 4934 RoboPack-1 queue pickup_7 576.6 NaN 1.0 601 350 Pickup 7 None 0.0 NaN 🤖 1.0 270.0 270.0

11520 rows × 17 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 16:38:56