Skip to content

Commit 94c56a3

Browse files
authored
Meta agents (#2748)
* meta_agents - Add create meta-agents to experimental - Add tests of meta-agents - Add example with an alliance formation model in advanced examples
1 parent 1389837 commit 94c56a3

File tree

15 files changed

+1090
-12
lines changed

15 files changed

+1090
-12
lines changed

docs/apis/experimental.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,15 @@ This namespace contains experimental features. These are under development, and
2525
.. automodule:: experimental.continuous_space.continuous_space_agents
2626
:members:
2727
```
28+
29+
## Continuous Space
30+
31+
```{eval-rst}
32+
.. automodule:: experimental.continuous_space.continuous_space
33+
:members:
34+
```
35+
36+
```{eval-rst}
37+
.. automodule:: experimental.continuous_space.continuous_space_agents
38+
:members:
39+
```

mesa/agent.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,12 @@ def __init__(self, model: Model, *args, **kwargs) -> None:
5353
5454
Args:
5555
model (Model): The model instance in which the agent exists.
56-
args: passed on to super
57-
kwargs: passed on to super
56+
args: Passed on to super.
57+
kwargs: Passed on to super.
5858
5959
Notes:
6060
to make proper use of python's super, in each class remove the arguments and
6161
keyword arguments you need and pass on the rest to super
62-
6362
"""
6463
super().__init__(*args, **kwargs)
6564

@@ -103,7 +102,10 @@ def create_agents(cls, model: Model, n: int, *args, **kwargs) -> AgentSet[Agent]
103102
"""
104103

105104
class ListLike:
106-
"""Helper class to make default arguments act as if they are in a list of length N."""
105+
"""Make default arguments act as if they are in a list of length N.
106+
107+
This is a helper class.
108+
"""
107109

108110
def __init__(self, value):
109111
self.value = value

mesa/examples/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from mesa.examples.advanced.alliance_formation.model import MultiLevelAllianceModel
12
from mesa.examples.advanced.epstein_civil_violence.model import EpsteinCivilViolence
23
from mesa.examples.advanced.pd_grid.model import PdGrid
34
from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt
@@ -13,6 +14,7 @@
1314
"BoltzmannWealth",
1415
"ConwaysGameOfLife",
1516
"EpsteinCivilViolence",
17+
"MultiLevelAllianceModel",
1618
"PdGrid",
1719
"Schelling",
1820
"SugarscapeG1mt",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Alliance Formation Model (Meta-Agent Example)
2+
3+
## Summary
4+
5+
This model demonstrates Mesa's meta agent capability.
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+
This model demonstrates Mesa's ability to dynamically create new classes of agents that are composed of existing agents. These meta-agents inherits functions and attributes from their sub-agents and users can specify new functionality or attributes they want the meta agent to have. For example, if a user is doing a factory simulation with autonomous systems, each major component of that system can be a sub-agent of the overall robot agent. Or, if someone is doing a simulation of an organization, individuals can be part of different organizational units that are working for some purpose.
12+
13+
To provide a simple demonstration of this capability is an alliance formation model.
14+
15+
In this simulation n agents are created, who have two attributes (1) power and (2) preference. Each attribute is a number between 0 and 1 over a gaussian distribution. Agents then randomly select other agents and use the [bilateral shapley value](https://en.wikipedia.org/wiki/Shapley_value) to determine if they should form an alliance. If the expected utility support an alliances, the agent creates a meta-agent. Subsequent steps may add agents to the meta-agent, create new instances of similar hierarchy, or create a new hierarchy level where meta-agents form an alliance of meta-agents. In this visualization of this model a new meta-agent hierarchy will be a larger node and a new color.
16+
17+
In MetaAgents current configuration, agents being part of multiple meta-agents is not supported.
18+
19+
If you would like to see an example of explicit meta-agent formation see the [warehouse model in the Mesa example's repository](https://github.com/projectmesa/mesa-examples/tree/main/examples/warehouse)
20+
21+
22+
## Installation
23+
24+
This model requires Mesa's recommended install and scipy
25+
26+
```
27+
$ pip install mesa[rec]
28+
```
29+
30+
## How to Run
31+
32+
To run the model interactively, in this directory, run the following command
33+
34+
```
35+
$ solara run app.py
36+
```
37+
38+
## Files
39+
40+
- `model.py`: Contains creation of agents, the network and management of agent execution.
41+
- `agents.py`: Contains logic for forming alliances and creation of new agents
42+
- `app.py`: Contains the code for the interactive Solara visualization.
43+
44+
## Further Reading
45+
46+
The full tutorial describing how the model is built can be found at:
47+
https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html
48+
49+
An example of the bilateral shapley value in another model:
50+
[Techno-Social Energy Infrastructure Siting: Sustainable Energy Modeling Programming (SEMPro)](https://www.jasss.org/16/3/6.html)

mesa/examples/advanced/alliance_formation/__init__ .py

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import mesa
2+
3+
4+
class AllianceAgent(mesa.Agent):
5+
"""
6+
Agent has three attributes power (float), position (float) and level (int)
7+
8+
"""
9+
10+
def __init__(self, model, power, position, level=0):
11+
super().__init__(model)
12+
self.power = power
13+
self.position = position
14+
self.level = level
15+
16+
"""
17+
For this demo model agent only need attributes.
18+
19+
More complex models could have functions that define agent behavior.
20+
"""
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import matplotlib.pyplot as plt
2+
import networkx as nx
3+
import solara
4+
from matplotlib.figure import Figure
5+
6+
from mesa.examples.advanced.alliance_formation.model import MultiLevelAllianceModel
7+
from mesa.visualization import SolaraViz
8+
from mesa.visualization.utils import update_counter
9+
10+
model_params = {
11+
"seed": {
12+
"type": "InputText",
13+
"value": 42,
14+
"label": "Random Seed",
15+
},
16+
"n": {
17+
"type": "SliderInt",
18+
"value": 50,
19+
"label": "Number of agents:",
20+
"min": 10,
21+
"max": 100,
22+
"step": 1,
23+
},
24+
}
25+
26+
# Create visualization elements. The visualization elements are solara components
27+
# that receive the model instance as a "prop" and display it in a certain way.
28+
# Under the hood these are just classes that receive the model instance.
29+
# You can also author your own visualization elements, which can also be functions
30+
# that receive the model instance and return a valid solara component.
31+
32+
33+
@solara.component
34+
def plot_network(model):
35+
update_counter.get()
36+
g = model.network
37+
pos = nx.fruchterman_reingold_layout(g)
38+
fig = Figure()
39+
ax = fig.subplots()
40+
labels = {agent.unique_id: agent.unique_id for agent in model.agents}
41+
node_sizes = [g.nodes[node]["size"] for node in g.nodes]
42+
node_colors = [g.nodes[node]["size"] for node in g.nodes()]
43+
44+
nx.draw(
45+
g,
46+
pos,
47+
node_size=node_sizes,
48+
node_color=node_colors,
49+
cmap=plt.cm.coolwarm,
50+
labels=labels,
51+
ax=ax,
52+
)
53+
54+
solara.FigureMatplotlib(fig)
55+
56+
57+
# Create initial model instance
58+
model = MultiLevelAllianceModel(50)
59+
60+
# Create the SolaraViz page. This will automatically create a server and display the
61+
# visualization elements in a web browser.
62+
# Display it using the following command in the example directory:
63+
# solara run app.py
64+
# It will automatically update and display any changes made to this file
65+
page = SolaraViz(
66+
model,
67+
components=[plot_network],
68+
model_params=model_params,
69+
name="Alliance Formation Model",
70+
)
71+
page # noqa
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import networkx as nx
2+
import numpy as np
3+
4+
import mesa
5+
from mesa import Agent
6+
from mesa.examples.advanced.alliance_formation.agents import AllianceAgent
7+
from mesa.experimental.meta_agents.meta_agent import (
8+
create_meta_agent,
9+
find_combinations,
10+
)
11+
12+
13+
class MultiLevelAllianceModel(mesa.Model):
14+
"""
15+
Model for simulating multi-level alliances among agents.
16+
"""
17+
18+
def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42):
19+
"""
20+
Initialize the model.
21+
22+
Args:
23+
n (int): Number of agents.
24+
mean (float): Mean value for normal distribution.
25+
std_dev (float): Standard deviation for normal distribution.
26+
seed (int): Random seed.
27+
"""
28+
super().__init__(seed=seed)
29+
self.population = n
30+
self.network = nx.Graph() # Initialize the network
31+
self.datacollector = mesa.DataCollector(model_reporters={"Network": "network"})
32+
33+
# Create Agents
34+
power = self.rng.normal(mean, std_dev, n)
35+
power = np.clip(power, 0, 1)
36+
position = self.rng.normal(mean, std_dev, n)
37+
position = np.clip(position, 0, 1)
38+
AllianceAgent.create_agents(self, n, power, position)
39+
agent_ids = [
40+
(agent.unique_id, {"size": 300, "level": 0}) for agent in self.agents
41+
]
42+
self.network.add_nodes_from(agent_ids)
43+
44+
def add_link(self, meta_agent, agents):
45+
"""
46+
Add links between a meta agent and its constituent agents in the network.
47+
48+
Args:
49+
meta_agent (MetaAgent): The meta agent.
50+
agents (list): List of agents.
51+
"""
52+
for agent in agents:
53+
self.network.add_edge(meta_agent.unique_id, agent.unique_id)
54+
55+
def calculate_shapley_value(self, agents):
56+
"""
57+
Calculate the Shapley value of the two agents.
58+
59+
Args:
60+
agents (list): List of agents.
61+
62+
Returns:
63+
tuple: Potential utility, new position, and level.
64+
"""
65+
positions = agents.get("position")
66+
new_position = 1 - (max(positions) - min(positions))
67+
potential_utility = agents.agg("power", sum) * 1.2 * new_position
68+
69+
value_0 = 0.5 * agents[0].power + 0.5 * (potential_utility - agents[1].power)
70+
value_1 = 0.5 * agents[1].power + 0.5 * (potential_utility - agents[0].power)
71+
72+
if value_0 > agents[0].power and value_1 > agents[1].power:
73+
if agents[0].level > agents[1].level:
74+
level = agents[0].level
75+
elif agents[0].level == agents[1].level:
76+
level = agents[0].level + 1
77+
else:
78+
level = agents[1].level
79+
80+
return potential_utility, new_position, level
81+
82+
def only_best_combination(self, combinations):
83+
"""
84+
Filter to keep only the best combination for each agent.
85+
86+
Args:
87+
combinations (list): List of combinations.
88+
89+
Returns:
90+
dict: Unique combinations.
91+
"""
92+
best = {}
93+
# Determine best option for EACH agent
94+
for group, value in combinations:
95+
agent_ids = sorted(group.get("unique_id")) # by default is bilateral
96+
# Deal with all possibilities
97+
if (
98+
agent_ids[0] not in best and agent_ids[1] not in best
99+
): # if neither in add both
100+
best[agent_ids[0]] = [group, value, agent_ids]
101+
best[agent_ids[1]] = [group, value, agent_ids]
102+
elif (
103+
agent_ids[0] in best and agent_ids[1] in best
104+
): # if both in, see if both would be trading up
105+
if (
106+
value[0] > best[agent_ids[0]][1][0]
107+
and value[0] > best[agent_ids[1]][1][0]
108+
):
109+
# Remove the old alliances
110+
del best[best[agent_ids[0]][2][1]]
111+
del best[best[agent_ids[1]][2][0]]
112+
# Add the new alliance
113+
best[agent_ids[0]] = [group, value, agent_ids]
114+
best[agent_ids[1]] = [group, value, agent_ids]
115+
elif (
116+
agent_ids[0] in best
117+
): # if only agent_ids[0] in, see if it would be trading up
118+
if value[0] > best[agent_ids[0]][1][0]:
119+
# Remove the old alliance for agent_ids[0]
120+
del best[best[agent_ids[0]][2][1]]
121+
# Add the new alliance
122+
best[agent_ids[0]] = [group, value, agent_ids]
123+
best[agent_ids[1]] = [group, value, agent_ids]
124+
elif (
125+
agent_ids[1] in best
126+
): # if only agent_ids[1] in, see if it would be trading up
127+
if value[0] > best[agent_ids[1]][1][0]:
128+
# Remove the old alliance for agent_ids[1]
129+
del best[best[agent_ids[1]][2][0]]
130+
# Add the new alliance
131+
best[agent_ids[0]] = [group, value, agent_ids]
132+
best[agent_ids[1]] = [group, value, agent_ids]
133+
134+
# Create a unique dictionary of the best combinations
135+
unique_combinations = {}
136+
for group, value, agents_nums in best.values():
137+
unique_combinations[tuple(agents_nums)] = [group, value]
138+
139+
return unique_combinations.values()
140+
141+
def step(self):
142+
"""
143+
Execute one step of the model.
144+
"""
145+
# Get all other agents of the same type
146+
agent_types = list(self.agents_by_type.keys())
147+
148+
for agent_type in agent_types:
149+
similar_agents = self.agents_by_type[agent_type]
150+
151+
# Find the best combinations using find_combinations
152+
if (
153+
len(similar_agents) > 1
154+
): # only form alliances if there are more than 1 agent
155+
combinations = find_combinations(
156+
self,
157+
similar_agents,
158+
size=2,
159+
evaluation_func=self.calculate_shapley_value,
160+
filter_func=self.only_best_combination,
161+
)
162+
163+
for alliance, attributes in combinations:
164+
class_name = f"MetaAgentLevel{attributes[2]}"
165+
meta = create_meta_agent(
166+
self,
167+
class_name,
168+
alliance,
169+
Agent,
170+
meta_attributes={
171+
"level": attributes[2],
172+
"power": attributes[0],
173+
"position": attributes[1],
174+
},
175+
)
176+
177+
# Update the network if a new meta agent instance created
178+
if meta:
179+
self.network.add_node(
180+
meta.unique_id,
181+
size=(meta.level + 1) * 300,
182+
level=meta.level,
183+
)
184+
self.add_link(meta, meta.agents)

mesa/experimental/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
- Features graduate from experimental status once their APIs are stabilized
1616
"""
1717

18-
from mesa.experimental import continuous_space, devs, mesa_signals
18+
from mesa.experimental import continuous_space, devs, mesa_signals, meta_agents
1919

20-
__all__ = ["continuous_space", "devs", "mesa_signals"]
20+
__all__ = ["continuous_space", "devs", "mesa_signals", "meta_agents"]

0 commit comments

Comments
 (0)