Skip to content

Commit c7c8311

Browse files
committed
refactor meta-agent
- allow for deliberate meta-agents creation - allow for combinatorics - allow for dynamic agent creation fix methods-functions; add tests
1 parent a87d382 commit c7c8311

File tree

12 files changed

+639
-268
lines changed

12 files changed

+639
-268
lines changed

mesa/examples/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22
from mesa.examples.advanced.pd_grid.model import PdGrid
33
from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt
44
from mesa.examples.advanced.wolf_sheep.model import WolfSheep
5-
from mesa.examples.basic.alliance_formation_model.model import AllianceModel
5+
from mesa.examples.basic.alliance_formation_model.model import MultiLevelAllianceModel
66
from mesa.examples.basic.boid_flockers.model import BoidFlockers
77
from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealth
88
from mesa.examples.basic.conways_game_of_life.model import ConwaysGameOfLife
99
from mesa.examples.basic.schelling.model import Schelling
1010
from mesa.examples.basic.virus_on_network.model import VirusOnNetwork
1111

1212
__all__ = [
13-
"AllianceModel",
1413
"BoidFlockers",
1514
"BoltzmannWealth",
1615
"ConwaysGameOfLife",
1716
"EpsteinCivilViolence",
17+
"MultiLevelAllianceModel",
1818
"PdGrid",
1919
"Schelling",
2020
"SugarscapeG1mt",

mesa/examples/basic/alliance_formation_model/Readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ In its current configuration, agents being part of multiple meta-agents is not s
1313

1414
This model requires Mesa's recommended install and scipy
1515
```
16-
$ pip install mesa[rec] scipy
16+
$ pip install mesa[rec]
1717
```
1818

1919
## How to Run
Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +0,0 @@
1-
import logging
2-
3-
# Configure logging
4-
logging.basicConfig(
5-
level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
6-
)
7-
8-
# Example usage of logging
9-
logger = logging.getLogger(__name__)
10-
logger.info("Logging is configured and ready to use.")
Lines changed: 7 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,20 @@
11
import mesa
2-
from mesa.experimental.meta_agents import create_meta_agent
3-
4-
5-
def calculate_shapley_value(calling_agent, other_agent):
6-
"""
7-
Calculate the Shapley value of the two agents
8-
"""
9-
new_position = 1 - abs(calling_agent.position - other_agent.position)
10-
potential_utility = (calling_agent.power + other_agent.power) * 1.1 * new_position
11-
value_me = 0.5 * calling_agent.power + 0.5 * (potential_utility - other_agent.power)
12-
value_other = 0.5 * other_agent.power + 0.5 * (
13-
potential_utility - calling_agent.power
14-
)
15-
16-
# Determine if there is value in the alliance
17-
if value_me > calling_agent.power and value_other > other_agent.power:
18-
if other_agent.hierarchy > calling_agent.hierarchy:
19-
hierarchy = other_agent.hierarchy
20-
elif other_agent.hierarchy == calling_agent.hierarchy:
21-
hierarchy = calling_agent.hierarchy + 1
22-
else:
23-
hierarchy = calling_agent.hierarchy
24-
25-
return (potential_utility, new_position, hierarchy)
26-
else:
27-
return None
282

293

304
class AllianceAgent(mesa.Agent):
315
"""
32-
Agent has three attributes power (float), position (float) and hierarchy (int)
6+
Agent has three attributes power (float), position (float) and level (int)
337
348
"""
359

36-
def __init__(self, model, power, position, hierarchy=0):
10+
def __init__(self, model, power, position, level=0):
3711
super().__init__(model)
3812
self.power = power
3913
self.position = position
40-
self.hierarchy = hierarchy
41-
42-
def form_alliance(self):
43-
# Randomly select another agent of the same type
44-
other_agents = [
45-
agent for agent in self.model.agents_by_type[type(self)] if agent != self
46-
]
14+
self.level = level
4715

48-
# Determine if there is a beneficial alliance
49-
if other_agents:
50-
other_agent = self.random.choice(other_agents)
51-
shapley_value = calculate_shapley_value(self, other_agent)
52-
if shapley_value:
53-
class_name = f"MetaAgentHierarchy{shapley_value[2]}"
54-
meta = create_meta_agent(
55-
self.model,
56-
class_name,
57-
{other_agent, self},
58-
meta_attributes={
59-
"hierarchy": shapley_value[2],
60-
"power": shapley_value[0],
61-
"position": shapley_value[1],
62-
},
63-
)
16+
"""
17+
For this demo model agent only need attributes.
6418
65-
# Update the network if a new meta agent instance created
66-
if meta:
67-
self.model.network.add_node(
68-
meta.unique_id,
69-
size=(meta.hierarchy + 1) * 300,
70-
hierarchy=meta.hierarchy,
71-
)
19+
More complex models could have functions that define agent behavior.
20+
"""

mesa/examples/basic/alliance_formation_model/app.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@
33
import solara
44
from matplotlib.figure import Figure
55

6-
from mesa.examples.basic.alliance_formation_model.model import AllianceModel
7-
from mesa.mesa_logging import DEBUG, log_to_stderr
6+
from mesa.examples.basic.alliance_formation_model.model import MultiLevelAllianceModel
87
from mesa.visualization import SolaraViz
98
from mesa.visualization.utils import update_counter
109

11-
log_to_stderr(DEBUG)
12-
1310
model_params = {
1411
"seed": {
1512
"type": "InputText",
@@ -37,7 +34,7 @@
3734
def plot_network(model):
3835
update_counter.get()
3936
g = model.network
40-
pos = nx.kamada_kawai_layout(g)
37+
pos = nx.fruchterman_reingold_layout(g)
4138
fig = Figure()
4239
ax = fig.subplots()
4340
labels = {agent.unique_id: agent.unique_id for agent in model.agents}
@@ -58,13 +55,14 @@ def plot_network(model):
5855

5956

6057
# Create initial model instance
61-
model = AllianceModel(50)
58+
model = MultiLevelAllianceModel(50)
6259

6360
# Create the SolaraViz page. This will automatically create a server and display the
6461
# visualization elements in a web browser.
6562
# Display it using the following command in the example directory:
6663
# solara run app.py
6764
# It will automatically update and display any changes made to this file
65+
6866
page = SolaraViz(
6967
model,
7068
components=[plot_network],

mesa/examples/basic/alliance_formation_model/model.py

Lines changed: 152 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,25 @@
33

44
import mesa
55
from mesa.examples.basic.alliance_formation_model.agents import AllianceAgent
6+
from mesa.experimental.meta_agents.meta_agent import find_combinations
7+
from mesa.experimental.meta_agents.multi_levels import multi_level_agents
68

79

8-
class AllianceModel(mesa.Model):
10+
class MultiLevelAllianceModel(mesa.Model):
11+
"""
12+
Model for simulating multi-level alliances among agents.
13+
"""
14+
915
def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42):
16+
"""
17+
Initialize the model.
18+
19+
Args:
20+
n (int): Number of agents.
21+
mean (float): Mean value for normal distribution.
22+
std_dev (float): Standard deviation for normal distribution.
23+
seed (int): Random seed.
24+
"""
1025
super().__init__(seed=seed)
1126
self.population = n
1227
self.network = nx.Graph() # Initialize the network
@@ -19,21 +34,147 @@ def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42):
1934
position = np.clip(position, 0, 1)
2035
AllianceAgent.create_agents(self, n, power, position)
2136
agent_ids = [
22-
(agent.unique_id, {"size": 300, "hierarchy": 0}) for agent in self.agents
37+
(agent.unique_id, {"size": 300, "level": 0}) for agent in self.agents
2338
]
2439
self.network.add_nodes_from(agent_ids)
2540

2641
def add_link(self, meta_agent, agents):
42+
"""
43+
Add links between a meta agent and its constituent agents in the network.
44+
45+
Args:
46+
meta_agent (MetaAgent): The meta agent.
47+
agents (list): List of agents.
48+
"""
2749
for agent in agents:
2850
self.network.add_edge(meta_agent.unique_id, agent.unique_id)
2951

52+
def calculate_shapley_value(self, agents):
53+
"""
54+
Calculate the Shapley value of the two agents.
55+
56+
Args:
57+
agents (list): List of agents.
58+
59+
Returns:
60+
tuple: Potential utility, new position, and level.
61+
"""
62+
positions = agents.get("position")
63+
new_position = 1 - (max(positions) - min(positions))
64+
potential_utility = agents.agg("power", sum) * 1.2 * new_position
65+
66+
value_0 = 0.5 * agents[0].power + 0.5 * (potential_utility - agents[1].power)
67+
value_1 = 0.5 * agents[1].power + 0.5 * (potential_utility - agents[0].power)
68+
69+
if value_0 > agents[0].power and value_1 > agents[1].power:
70+
if agents[0].level > agents[1].level:
71+
level = agents[0].level
72+
elif agents[0].level == agents[1].level:
73+
level = agents[0].level + 1
74+
else:
75+
level = agents[1].level
76+
77+
return potential_utility, new_position, level
78+
79+
def only_best_combination(self, combinations):
80+
"""
81+
Filter to keep only the best combination for each agent.
82+
83+
Args:
84+
combinations (list): List of combinations.
85+
86+
Returns:
87+
dict: Unique combinations.
88+
"""
89+
best = {}
90+
# Determine best option for EACH agent
91+
for group, value in combinations:
92+
agent_ids = sorted(group.get("unique_id")) # by default is bilateral
93+
# Deal with all possibilities
94+
if (
95+
agent_ids[0] not in best and agent_ids[1] not in best
96+
): # if neither in add both
97+
best[agent_ids[0]] = [group, value, agent_ids]
98+
best[agent_ids[1]] = [group, value, agent_ids]
99+
elif (
100+
agent_ids[0] in best and agent_ids[1] in best
101+
): # if both in, see if both would be trading up
102+
if (
103+
value[0] > best[agent_ids[0]][1][0]
104+
and value[0] > best[agent_ids[1]][1][0]
105+
):
106+
# Remove the old alliances
107+
del best[best[agent_ids[0]][2][1]]
108+
del best[best[agent_ids[1]][2][0]]
109+
# Add the new alliance
110+
best[agent_ids[0]] = [group, value, agent_ids]
111+
best[agent_ids[1]] = [group, value, agent_ids]
112+
elif (
113+
agent_ids[0] in best
114+
): # if only agent_ids[0] in, see if it would be trading up
115+
if value[0] > best[agent_ids[0]][1][0]:
116+
# Remove the old alliance for agent_ids[0]
117+
del best[best[agent_ids[0]][2][1]]
118+
# Add the new alliance
119+
best[agent_ids[0]] = [group, value, agent_ids]
120+
best[agent_ids[1]] = [group, value, agent_ids]
121+
elif (
122+
agent_ids[1] in best
123+
): # if only agent_ids[1] in, see if it would be trading up
124+
if value[0] > best[agent_ids[1]][1][0]:
125+
# Remove the old alliance for agent_ids[1]
126+
del best[best[agent_ids[1]][2][0]]
127+
# Add the new alliance
128+
best[agent_ids[0]] = [group, value, agent_ids]
129+
best[agent_ids[1]] = [group, value, agent_ids]
130+
131+
# Create a unique dictionary of the best combinations
132+
unique_combinations = {}
133+
for group, value, agents_nums in best.values():
134+
unique_combinations[tuple(agents_nums)] = [group, value]
135+
136+
return unique_combinations.values()
137+
30138
def step(self):
31-
for agent_class in list(
32-
self.agent_types
33-
): # Convert to list to avoid modification during iteration
34-
self.agents_by_type[agent_class].shuffle_do("form_alliance")
35-
36-
# Update graph
37-
if agent_class is not AllianceAgent:
38-
for meta_agent in self.agents_by_type[agent_class]:
39-
self.add_link(meta_agent, meta_agent.agents)
139+
"""
140+
Execute one step of the model.
141+
"""
142+
# Get all other agents of the same type
143+
agent_types = list(self.agents_by_type.keys())
144+
145+
for agent_type in agent_types:
146+
similar_agents = self.agents_by_type[agent_type]
147+
148+
# Find the best combinations using find_combinations
149+
if (
150+
len(similar_agents) > 1
151+
): # only form alliances if there are more than 1 agent
152+
combinations = find_combinations(
153+
self,
154+
similar_agents,
155+
size=2,
156+
evaluation_func=self.calculate_shapley_value,
157+
filter_func=self.only_best_combination,
158+
)
159+
160+
for alliance, attributes in combinations:
161+
class_name = f"MetaAgentLevel{attributes[2]}"
162+
meta = multi_level_agents(
163+
self,
164+
class_name,
165+
alliance,
166+
meta_attributes={
167+
"level": attributes[2],
168+
"power": attributes[0],
169+
"position": attributes[1],
170+
},
171+
)
172+
173+
# Update the network if a new meta agent instance created
174+
if meta:
175+
self.network.add_node(
176+
meta.unique_id,
177+
size=(meta.level + 1) * 300,
178+
level=meta.level,
179+
)
180+
self.add_link(meta, meta.subset)

mesa/experimental/meta_agents/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
2121
"""
2222

23-
from .meta_agents import create_meta_agent
23+
from .meta_agent import MetaAgent
24+
from .multi_levels import multi_level_agents
2425

25-
__all__ = ["create_meta_agent"]
26+
__all__ = ["MetaAgent", "multi_level_agents"]

0 commit comments

Comments
 (0)