Skip to content

Commit d1baeeb

Browse files
committed
meta-agent creation
- adds ability to dynamically create meta-agents - adds alliance formation model to demonstrate - adds tests to agent.py for meta agent creation
1 parent 03ce5f0 commit d1baeeb

File tree

7 files changed

+339
-1
lines changed

7 files changed

+339
-1
lines changed

mesa/agent.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ def __init__(self, model: Model, *args, **kwargs) -> None:
6767
self.unique_id: int = next(self._ids[model])
6868
self.pos: Position | None = None
6969
self.model.register_agent(self)
70-
70+
# Private attribute to track parent agent if metaagents are created
71+
# Uses name mangling to prevent name clashes
72+
self.__metaagent = None # Currently restricted to one parent agent
73+
7174
def remove(self) -> None:
7275
"""Remove and delete the agent from the model.
7376
@@ -132,7 +135,100 @@ def __getitem__(self, i):
132135
agent = cls(model, *instance_args, **instance_kwargs)
133136
agents.append(agent)
134137
return AgentSet(agents, random=model.random)
138+
139+
def create_metaagent(self, new_agent_class: str, agents: Iterable[Agent], **unique_attributes_functions) -> None:
140+
141+
"""Dynamically create a new meta-agent class and instantiate agents in that class.
142+
143+
Args:
144+
new_agent_class (str): The name of the new meta-agent class.
145+
agents (Iterable[Agent]): The agents to be grouped into the new meta-agent class.
146+
**unique_attributes_functions: A dictionary of unique attributes for that class.
147+
148+
Returns:
149+
Adds the new meta-agent instance to agentset if adding agent to new class or new agent instance
150+
- None if adding agent to exsiting class
151+
- New class instance if created a new instance of a dynamically created agent type
152+
- New class instance if created a new dynamically created agent type
135153
154+
Notes:
155+
This method is useful for creating meta-agents that represent groups of agents with interdependent characteristics.
156+
The new meta-agent class is created dynamically using the provided name and unique attributes and functions.
157+
158+
Currently restricted to one parent agent and one meta-agent per agent. Goal is to assess usage and expand functionality.
159+
160+
Method has three paths of execution:
161+
1. Add agents to existing metaagent
162+
2. Create new meta-agent instance of existing metaagent class
163+
3. Create new meta-agent class
164+
165+
See alliance formation example for usage.
166+
167+
"""
168+
# Convert agents to set to ensure uniqueness
169+
agents = set(agents)
170+
171+
# Helper function to update agents __metaagent attribute and store agent's metaagent
172+
def update_agents_metaagent(agents, metaagent):
173+
for agent in agents:
174+
agent._Agent__metaagent = metaagent
175+
176+
# Path 1 - Add agents to existing meta-agent
177+
subagents = [agent for agent in agents if agent._Agent__metaagent is not None]
178+
179+
if len(subagents) > 0:
180+
if len(subagents) == 1:
181+
# Update metaagents agent set with new agents
182+
subagents[0]._Agent__metaagent.agents.update(agents)
183+
# Update subagents with metaagent
184+
update_agents_metaagent(agents, subagents[0]._Agent__metaagent)
185+
else:
186+
# If there are multiple subagents, one is chosen at random to be the parent metaagent
187+
subagent = self.random.choice(subagents)
188+
# Remove agent who are already part of metaagent
189+
agents = set(agents) - set(subagents)
190+
subagent._Agent__metaagent.agents.update(agents)
191+
update_agents_metaagent(agents, subagent._Agent__metaagent)
192+
# TODO: Add way for user to add function to specify how agents join metaagent
193+
194+
else:
195+
# Path 2 - Create a new instance of an exsiting meta-agent class
196+
agent_class = next((agent_type for agent_type in self.model.agent_types if agent_type.__name__ == new_agent_class), None)
197+
if agent_class:
198+
# Create an instance of the meta-agent class
199+
meta_agent_instance = agent_class(self.model, **unique_attributes_functions)
200+
# Add agents to meta-agent instance
201+
meta_agent_instance.agents = agents
202+
# Update subagents Agent.__metaagent attribute
203+
update_agents_metaagent(agents, meta_agent_instance)
204+
# Register the new meta-agent instance
205+
self.model.register_agent(meta_agent_instance)
206+
207+
return meta_agent_instance
208+
209+
# Path 3 - Create a new meta-agent class
210+
else:
211+
# Get agent types of subagents to create the new meta-agent class
212+
agent_types = tuple(set((type(agent) for agent in agents)))
213+
214+
meta_agent_class = type(
215+
new_agent_class,
216+
agent_types,
217+
{
218+
"unique_id": None,
219+
"agents": agents,
220+
}
221+
)
222+
223+
# Create an instance of the meta-agent class
224+
meta_agent_instance = meta_agent_class(self.model, **unique_attributes_functions)
225+
# Register the new meta-agent instance
226+
self.model.register_agent(meta_agent_instance)
227+
# Update subagents Agent.__metaagent attribute
228+
update_agents_metaagent(agents, meta_agent_instance)
229+
230+
return meta_agent_instance
231+
136232
@property
137233
def random(self) -> Random:
138234
"""Return a seeded stdlib rng."""
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Alliance Formation Model
2+
3+
## Summary
4+
5+
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.
6+
7+
To provide a simple demonstration of this capability is an alliance formation model.
8+
9+
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.
10+
11+
In its current configuration, agents being part of multiple meta-agents is not supported
12+
13+
## Installation
14+
15+
This model requires Mesa's recommended install
16+
```
17+
$ pip install mesa[rec]
18+
```
19+
20+
## How to Run
21+
22+
To run the model interactively, in this directory, run the following command
23+
24+
```
25+
$ solara run app.py
26+
```
27+
28+
## Files
29+
30+
* ``model.py``: Contains creation of agents, the network and management of agent execution.
31+
* ``agents.py``: Contains logic for forming alliances and creation of new agents
32+
* ``app.py``: Contains the code for the interactive Solara visualization.
33+
34+
## Further Reading
35+
36+
The full tutorial describing how the model is built can be found at:
37+
https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html
38+
39+
An example of the bilateral shapley value in another model:
40+
[Techno-Social Energy Infrastructure Siting: Sustainable Energy Modeling Programming (SEMPro)](https://www.jasss.org/16/3/6.html)
41+

mesa/examples/basic/alliance_formation_model/__init__.py

Whitespace-only changes.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import mesa
2+
3+
def calculate_shapley_value(self, other_agent):
4+
"""
5+
Calculate the Shapley value of the two agents
6+
"""
7+
other_agent.hierarchy = other_agent.hierarchy
8+
self.hierarchy = self.hierarchy
9+
new_position = 1-abs(self.position - other_agent.position)
10+
potential_utility = (self.power+other_agent.power)*1.1 * new_position
11+
value_me = 0.5 * self.power + 0.5 * (potential_utility - other_agent.power)
12+
value_other = 0.5 * other_agent.power + 0.5 * (potential_utility - self.power)
13+
14+
15+
# Determine ig there is value in the alliance
16+
if value_me > self.power and value_other > other_agent.power:
17+
if other_agent.hierarchy>self.hierarchy:
18+
hierarchy = other_agent.hierarchy
19+
elif other_agent.hierarchy==self.hierarchy:
20+
hierarchy = self.hierarchy+1
21+
else:
22+
hierarchy = self.hierarchy
23+
24+
return (potential_utility, new_position, hierarchy)
25+
else:
26+
return None
27+
28+
class alliance_agent(mesa.Agent):
29+
"""
30+
Agent has three attirbutes power (float), position (float) and hierarchy (int)
31+
32+
"""
33+
def __init__(self, model, power, position, hierarchy=0):
34+
super().__init__(model)
35+
self.power = power
36+
self.position = position
37+
self.hierarchy = hierarchy
38+
39+
40+
def form_alliance(self):
41+
# Randomly select another agent of the same type
42+
other_agents = [agent for agent in self.model.agents_by_type[type(self)] if agent != self]
43+
44+
# Determine if there is a beneficial alliance
45+
if other_agents:
46+
other_agent = self.random.choice(other_agents)
47+
shapley_value = calculate_shapley_value(self, other_agent)
48+
if shapley_value:
49+
class_name = f"MetaAgentHierarchy{shapley_value[2]}"
50+
meta = self.create_metaagent(class_name, {other_agent, self}, hierarchy=shapley_value[2],
51+
power=shapley_value[0],position=shapley_value[1])
52+
53+
# Update the network if a new meta agent instance created
54+
if meta:
55+
self.model.network.add_node(meta.unique_id, size =(meta.hierarchy+1)*200)
56+
self.model.new_agents=True
57+
else:
58+
self.model.new_agents=False
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import networkx as nx
2+
import matplotlib.pyplot as plt
3+
from matplotlib.figure import Figure
4+
from mesa.examples.basic.alliance_formation.model import alliance_model
5+
import solara
6+
from mesa.mesa_logging import DEBUG, log_to_stderr
7+
from mesa.visualization.utils import update_counter
8+
from mesa.visualization import (SolaraViz)
9+
10+
log_to_stderr(DEBUG)
11+
12+
13+
14+
model_params = {
15+
"seed": {
16+
"type": "InputText",
17+
"value": 42,
18+
"label": "Random Seed",
19+
},
20+
"n": {
21+
"type": "SliderInt",
22+
"value": 50,
23+
"label": "Number of agents:",
24+
"min": 10,
25+
"max": 100,
26+
"step": 1,
27+
},
28+
}
29+
30+
# Create visualization elements. The visualization elements are solara components
31+
# that receive the model instance as a "prop" and display it in a certain way.
32+
# Under the hood these are just classes that receive the model instance.
33+
# You can also author your own visualization elements, which can also be functions
34+
# that receive the model instance and return a valid solara component.
35+
36+
@solara.component
37+
def plot_network(model):
38+
update_counter.get()
39+
G = model.network
40+
pos = nx.spring_layout(G)
41+
fig = Figure()
42+
ax = fig.subplots()
43+
labels = {agent.unique_id: agent.unique_id for agent in model.agents}
44+
node_sizes = [G.nodes[node]['size'] for node in G.nodes]
45+
node_colors = [G.nodes[node]['size'] for node in G.nodes()]
46+
47+
nx.draw(G,
48+
pos,
49+
node_size = node_sizes,
50+
node_color = node_colors,
51+
cmap=plt.cm.coolwarm,
52+
labels=labels,
53+
ax=ax)
54+
55+
solara.FigureMatplotlib(fig)
56+
57+
# Create initial model instance
58+
model = alliance_model(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="Allliance Formation Model",
70+
)
71+
page # noqa
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import numpy as np
2+
import mesa
3+
from agent import alliance_agent
4+
import networkx as nx
5+
6+
class alliance_model(mesa.Model):
7+
def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42):
8+
super().__init__(seed=seed)
9+
self.population = n
10+
self.network = nx.Graph() # Initialize the network
11+
self.datacollector = mesa.DataCollector(model_reporters={"Network": "network"})
12+
13+
14+
# Create Agents
15+
power = np.random.normal(mean, std_dev, n)
16+
power = np.clip(power, 0, 1)
17+
position = np.random.normal(mean, std_dev, n)
18+
position = np.clip(position, 0, 1)
19+
alliance_agent.create_agents(self, n, power, position)
20+
agent_ids = [(agent.unique_id, {"size": 100}) for agent in self.agents]
21+
self.network.add_nodes_from(agent_ids)
22+
23+
def add_link(self, metaagent, agents):
24+
for agent in agents:
25+
self.network.add_edge(metaagent.unique_id, agent.unique_id)
26+
27+
def step(self):
28+
for agent_class in list(self.agent_types): # Convert to list to avoid modification during iteration
29+
self.agents_by_type[agent_class].shuffle_do("form_alliance")
30+
31+
# Update graph
32+
if agent_class is not alliance_agent:
33+
for metaagent in self.agents_by_type[agent_class]:
34+
self.add_link(metaagent, metaagent.agents)
35+

tests/test_agent.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,3 +682,40 @@ def custom_agg(values):
682682
assert custom_result[False] == custom_agg(
683683
[agent.value for agent in agents if not agent.even]
684684
)
685+
686+
687+
class TestMetaAgent(Agent):
688+
def __init__(self, model, power, position, hierarchy=0):
689+
super().__init__(model)
690+
self.power = power
691+
self.position = position
692+
self.hierarchy = hierarchy
693+
694+
def test_create_metaagent():
695+
model = Model()
696+
agent1 = TestMetaAgent(model, power=0.5, position=0.5)
697+
agent2 = TestMetaAgent(model, power=0.6, position=0.6)
698+
agent3 = TestMetaAgent(model, power=0.7, position=0.7)
699+
agent4 = TestMetaAgent(model, power=0.8, position=0.8)
700+
701+
# Test creating a new meta-agent class
702+
meta = agent1.create_metaagent("MetaAgentClass1", {agent1, agent2}, power=1.1, position=0.55, hierarchy=1)
703+
assert len(model.agent_types) == 2
704+
assert len(model.agents) == 5
705+
assert meta.power == 1.1
706+
assert meta.position == 0.55
707+
assert meta.hierarchy == 1
708+
709+
# Test adding agents to an existing meta-agent
710+
agent1.create_metaagent("MetaAgentClass1", {agent3, agent2}, power=1.8, position=0.65, hierarchy=1)
711+
assert len(model.agent_types) == 2
712+
assert len(model.agents) == 5
713+
assert len(meta.agents) == 3
714+
715+
# Test creating a new instance of an existing meta-agent class
716+
meta2 = agent4.create_metaagent("MetaAgentClass2", {agent4}, power=0.8, position=0.8, hierarchy=2)
717+
assert len(model.agent_types) == 3
718+
assert len(model.agents) == 6
719+
assert meta2.power == 0.8
720+
assert meta2.position == 0.8
721+
assert meta2.hierarchy == 2

0 commit comments

Comments
 (0)