Skip to content

Commit 71ada44

Browse files
committed
add meta-agent warehouse examples
1 parent 7cac1a8 commit 71ada44

File tree

6 files changed

+488
-0
lines changed

6 files changed

+488
-0
lines changed

examples/warehouse/Readme.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Pseudo-Warehouse Model (Meta-Agent Example)
2+
3+
## Summary
4+
5+
The purpose of this model is to demonstrate Mesa's meta-agent capability and some of its implementation approaches, not to be an accurate warehouse simulation.
6+
7+
**Overview of meta agent:** Complex systems often have multiple levels of components. A city is not a single entity, but it is made of districts,neighborhoods, buildings, and people. A forest comprises an ecosystem of trees, plants, animals, and microorganisms. An organization is not one entity, but is made of departments, sub-departments, and people. A person is not a single entity, but it is made of micro biomes, organs and cells.
8+
9+
This reality is the motivation for meta-agents. It allows users to represent these multiple levels, where each level can have agents with sub-agents.
10+
11+
In this simulation, robots are given tasks to take retrieve inventory items and then take those items to the loading docks.
12+
13+
Each `RobotAgent` is made up of sub-components that are treated as separate agents. For this simulation, each robot as a `SensorAgent`, `RouterAgent`, and `WorkerAgent`.
14+
15+
This model demonstrates deliberate meta-agent creation. It shows the basics of meta-agent creation and different ways to use and reference sub-agent and meta-agent functions and attributes. (The alliance formation demonstrates emergent meta-agent creation.)
16+
17+
In its current configuration, agents being part of multiple meta-agents is not supported
18+
19+
## Installation
20+
21+
This model requires Mesa's recommended install
22+
23+
```
24+
$ pip install mesa[rec]
25+
```
26+
27+
## How to Run
28+
29+
To run the model interactively, in this directory, run the following command
30+
31+
```
32+
$ solara run app.py
33+
```
34+
35+
## Files
36+
37+
- `model.py`: Contains creation of agents, the network and management of agent execution.
38+
- `agents.py`: Contains logic for forming alliances and creation of new agents
39+
- `app.py`: Contains the code for the interactive Solara visualization.
40+
- `make_warehouse`: Generates a warehouse numpy array with loading docks, inventory, and charging stations.

examples/warehouse/__init__.py

Whitespace-only changes.

examples/warehouse/agents.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
from queue import PriorityQueue
2+
3+
import mesa
4+
from mesa.discrete_space import FixedAgent
5+
6+
7+
class InventoryAgent(FixedAgent):
8+
"""
9+
Represents an inventory item in the warehouse.
10+
"""
11+
12+
def __init__(self, model, cell, item: str):
13+
super().__init__(model, key_by_name=True)
14+
self.cell = cell
15+
self.item = item
16+
self.quantity = 1000 # Default quantity
17+
18+
19+
class RouteAgent(mesa.Agent):
20+
"""
21+
Handles path finding for agents in the warehouse.
22+
23+
Intended to be a pseudo onboard GPS system for robots.
24+
"""
25+
26+
def __init__(self, model):
27+
super().__init__(model, key_by_name=True)
28+
29+
def find_path(self, start, goal) -> list[tuple[int, int, int]] | None:
30+
"""
31+
Determines the path for a robot to take using the A* algorithm.
32+
"""
33+
34+
def heuristic(a, b) -> int:
35+
dx = abs(a[0] - b[0])
36+
dy = abs(a[1] - b[1])
37+
return dx + dy
38+
39+
open_set = PriorityQueue()
40+
open_set.put((0, start.coordinate))
41+
came_from = {}
42+
g_score = {start.coordinate: 0}
43+
44+
while not open_set.empty():
45+
_, current = open_set.get()
46+
47+
if current[:2] == goal.coordinate[:2]:
48+
path = []
49+
while current in came_from:
50+
path.append(current)
51+
current = came_from[current]
52+
path.reverse()
53+
path.insert(0, start.coordinate)
54+
path.pop() # Remove the last location (inventory)
55+
return path
56+
57+
for n_cell in self.model.warehouse[current].neighborhood:
58+
coord = n_cell.coordinate
59+
60+
# Only consider orthogonal neighbors
61+
if abs(coord[0] - current[0]) + abs(coord[1] - current[1]) != 1:
62+
continue
63+
64+
tentative_g_score = g_score[current] + 1
65+
if not n_cell.is_empty:
66+
tentative_g_score += 50 # Penalty for non-empty cells
67+
68+
if coord not in g_score or tentative_g_score < g_score[coord]:
69+
g_score[coord] = tentative_g_score
70+
f_score = tentative_g_score + heuristic(coord, goal.coordinate)
71+
open_set.put((f_score, coord))
72+
came_from[coord] = current
73+
74+
return None
75+
76+
77+
class SensorAgent(mesa.Agent):
78+
"""
79+
Detects entities in the area and handles movement along a path.
80+
81+
Intended to be a pseudo onboard sensor system for robot.
82+
"""
83+
84+
def __init__(self, model):
85+
super().__init__(model, key_by_name=True)
86+
87+
def move(
88+
self, coord: tuple[int, int, int], path: list[tuple[int, int, int]]
89+
) -> str:
90+
"""
91+
Moves the agent along the given path.
92+
"""
93+
if coord not in path:
94+
raise ValueError("Current coordinate not in path.")
95+
96+
idx = path.index(coord)
97+
if idx + 1 >= len(path):
98+
return "movement complete"
99+
100+
next_cell = self.model.warehouse[path[idx + 1]]
101+
if next_cell.is_empty:
102+
self.meta_agent.cell = next_cell
103+
return "moving"
104+
105+
# Handle obstacle
106+
neighbors = self.model.warehouse[self.meta_agent.cell.coordinate].neighborhood
107+
empty_neighbors = [n for n in neighbors if n.is_empty]
108+
if empty_neighbors:
109+
self.meta_agent.cell = self.random.choice(empty_neighbors)
110+
111+
# Recalculate path
112+
new_path = self.meta_agent.get_subagent_instance(RouteAgent).find_path(
113+
self.meta_agent.cell, self.meta_agent.item.cell
114+
)
115+
self.meta_agent.path = new_path
116+
return "recalculating"
117+
118+
119+
class WorkerAgent(mesa.Agent):
120+
"""
121+
Represents a robot worker responsible for collecting and loading items.
122+
"""
123+
124+
def __init__(self, model, ld, cs):
125+
super().__init__(model, key_by_name=True)
126+
self.loading_dock = ld
127+
self.charging_station = cs
128+
self.path: list[tuple[int, int, int]] | None = None
129+
self.carrying: str | None = None
130+
self.item: InventoryAgent | None = None
131+
132+
def initiate_task(self, item: InventoryAgent):
133+
"""
134+
Initiates a task for the robot to perform.
135+
"""
136+
self.item = item
137+
self.path = self.find_path(self.cell, item.cell)
138+
139+
def continue_task(self):
140+
"""
141+
Continues the task if the robot is able to perform it.
142+
"""
143+
status = self.meta_agent.get_subagent_instance(SensorAgent).move(
144+
self.cell.coordinate, self.path
145+
)
146+
147+
if status == "movement complete" and self.meta_agent.status == "inventory":
148+
# Pick up item and bring to loading dock
149+
self.meta_agent.cell = self.model.warehouse[
150+
*self.meta_agent.cell.coordinate[:2], self.item.cell.coordinate[2]
151+
]
152+
self.meta_agent.status = "loading"
153+
self.carrying = self.item.item
154+
self.item.quantity -= 1
155+
self.meta_agent.cell = self.model.warehouse[
156+
*self.meta_agent.cell.coordinate[:2], 0
157+
]
158+
self.path = self.find_path(self.cell, self.loading_dock)
159+
160+
if status == "movement complete" and self.meta_agent.status == "loading":
161+
# Load item onto truck and return to charging station
162+
self.carrying = None
163+
self.meta_agent.status = "open"

examples/warehouse/app.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import matplotlib.pyplot as plt
2+
import pandas as pd
3+
import solara
4+
from mesa.visualization import SolaraViz
5+
from mesa.visualization.utils import update_counter
6+
from model import WarehouseModel
7+
8+
# Constants
9+
LOADING_DOCKS = [(0, 0, 0), (0, 2, 0), (0, 4, 0), (0, 6, 0), (0, 8, 0)]
10+
AXIS_LIMITS = {"x": (0, 22), "y": (0, 20), "z": (0, 5)}
11+
12+
model_params = {
13+
"seed": {
14+
"type": "InputText",
15+
"value": 42,
16+
"label": "Random Seed",
17+
},
18+
}
19+
20+
21+
def prepare_agent_data(model, agent_type, agent_label):
22+
"""
23+
Prepare data for agents of a specific type.
24+
25+
Args:
26+
model: The WarehouseModel instance.
27+
agent_type: The type of agent (e.g., "InventoryAgent", "RobotAgent").
28+
agent_label: The label for the agent type.
29+
30+
Returns:
31+
A list of dictionaries containing agent coordinates and type.
32+
"""
33+
return [
34+
{
35+
"x": agent.cell.coordinate[0],
36+
"y": agent.cell.coordinate[1],
37+
"z": agent.cell.coordinate[2],
38+
"type": agent_label,
39+
}
40+
for agent in model.agents_by_type[agent_type]
41+
]
42+
43+
44+
@solara.component
45+
def plot_warehouse(model):
46+
"""
47+
Visualize the warehouse model in a 3D scatter plot.
48+
49+
Args:
50+
model: The WarehouseModel instance.
51+
"""
52+
update_counter.get()
53+
54+
# Prepare data for inventory and robot agents
55+
inventory_data = prepare_agent_data(model, "InventoryAgent", "Inventory")
56+
robot_data = prepare_agent_data(model, "RobotAgent", "Robot")
57+
58+
# Combine data into a single DataFrame
59+
data = pd.DataFrame(inventory_data + robot_data)
60+
61+
# Create Matplotlib 3D scatter plot
62+
fig = plt.figure(figsize=(8, 6))
63+
ax = fig.add_subplot(111, projection="3d")
64+
65+
# Highlight loading dock cells
66+
for i, dock in enumerate(LOADING_DOCKS):
67+
ax.scatter(
68+
dock[0],
69+
dock[1],
70+
dock[2],
71+
c="yellow",
72+
label="Loading Dock"
73+
if i == 0
74+
else None, # Add label only to the first dock
75+
s=300,
76+
marker="o",
77+
)
78+
79+
# Plot inventory agents
80+
inventory = data[data["type"] == "Inventory"]
81+
ax.scatter(
82+
inventory["x"],
83+
inventory["y"],
84+
inventory["z"],
85+
c="blue",
86+
label="Inventory",
87+
s=100,
88+
marker="s",
89+
)
90+
91+
# Plot robot agents
92+
robots = data[data["type"] == "Robot"]
93+
ax.scatter(robots["x"], robots["y"], robots["z"], c="red", label="Robot", s=200)
94+
95+
# Set labels, title, and legend
96+
ax.set_xlabel("X")
97+
ax.set_ylabel("Y")
98+
ax.set_zlabel("Z")
99+
ax.set_title("Warehouse Visualization")
100+
ax.legend()
101+
102+
# Configure plot appearance
103+
ax.grid(False)
104+
ax.set_xlim(*AXIS_LIMITS["x"])
105+
ax.set_ylim(*AXIS_LIMITS["y"])
106+
ax.set_zlim(*AXIS_LIMITS["z"])
107+
ax.axis("off")
108+
109+
# Render the plot in Solara
110+
solara.FigureMatplotlib(fig)
111+
112+
113+
# Create initial model instance
114+
model = WarehouseModel()
115+
116+
# Create the SolaraViz page
117+
page = SolaraViz(
118+
model,
119+
components=[plot_warehouse],
120+
model_params=model_params,
121+
name="Pseudo-Warehouse Model",
122+
)
123+
124+
page # noqa
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import random
2+
import string
3+
4+
import numpy as np
5+
6+
# Constants
7+
DEFAULT_ROWS = 22
8+
DEFAULT_COLS = 20
9+
DEFAULT_HEIGHT = 4
10+
LOADING_DOCK_COORDS = [(0, i, 0) for i in range(0, 10, 2)]
11+
CHARGING_STATION_COORDS = [(21, i, 0) for i in range(19, 10, -2)]
12+
13+
14+
def generate_item_code() -> str:
15+
"""Generate a random item code (1 letter + 2 numbers)."""
16+
letter = random.choice(string.ascii_uppercase)
17+
number = random.randint(10, 99)
18+
return f"{letter}{number}"
19+
20+
21+
def make_warehouse(
22+
rows: int = DEFAULT_ROWS, cols: int = DEFAULT_COLS, height: int = DEFAULT_HEIGHT
23+
) -> np.ndarray:
24+
"""
25+
Generate a warehouse layout with designated LD, CS, and storage rows as a NumPy array.
26+
27+
Args:
28+
rows (int): Number of rows in the warehouse.
29+
cols (int): Number of columns in the warehouse.
30+
height (int): Number of levels in the warehouse.
31+
32+
Returns:
33+
np.ndarray: A 3D NumPy array representing the warehouse layout.
34+
"""
35+
# Initialize empty warehouse layout
36+
warehouse = np.full((rows, cols, height), " ", dtype=object)
37+
38+
# Place Loading Docks (LD)
39+
for r, c, h in LOADING_DOCK_COORDS:
40+
warehouse[r, c, h] = "LD"
41+
42+
# Place Charging Stations (CS)
43+
for r, c, h in CHARGING_STATION_COORDS:
44+
warehouse[r, c, h] = "CS"
45+
46+
# Fill storage rows with item codes
47+
for r in range(3, rows - 2, 3): # Skip row 0,1,2 (LD) and row 17,18,19 (CS)
48+
for c in range(2, cols, 3): # Leave 2 spaces between each item row
49+
for h in range(height):
50+
warehouse[r, c, h] = generate_item_code()
51+
52+
return warehouse

0 commit comments

Comments
 (0)