Alliance Formation Model (Meta-Agent Example)#
Summary#
This model demonstrates Mesa’s meta agent capability.
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.
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.
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.
To provide a simple demonstration of this capability is an alliance formation model.
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 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.
In MetaAgents current configuration, agents being part of multiple meta-agents is not supported.
If you would like to see an example of explicit meta-agent formation see the warehouse model in the Mesa example’s repository
Installation#
This model requires Mesa’s recommended install and scipy
$ pip install mesa[rec]
How to Run#
To run the model interactively, in this directory, run the following command
$ solara run app.py
Files#
model.py: Contains creation of agents, the network and management of agent execution.agents.py: Contains logic for forming alliances and creation of new agentsapp.py: Contains the code for the interactive Solara visualization.
Further Reading#
The full tutorial describing how the model is built can be found at: https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html
An example of the bilateral shapley value in another model: Techno-Social Energy Infrastructure Siting: Sustainable Energy Modeling Programming (SEMPro)
Agents#
import mesa
class AllianceAgent(mesa.Agent):
"""
Agent has three attributes power (float), position (float) and level (int)
"""
def __init__(self, model, power, position, level=0):
super().__init__(model)
self.power = power
self.position = position
self.level = level
"""
For this demo model agent only need attributes.
More complex models could have functions that define agent behavior.
"""
Model#
import networkx as nx
import numpy as np
import mesa
from mesa import Agent
from mesa.examples.advanced.alliance_formation.agents import AllianceAgent
from mesa.experimental.meta_agents.meta_agent import (
create_meta_agent,
find_combinations,
)
class MultiLevelAllianceModel(mesa.Model):
"""
Model for simulating multi-level alliances among agents.
"""
def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42):
"""
Initialize the model.
Args:
n (int): Number of agents.
mean (float): Mean value for normal distribution.
std_dev (float): Standard deviation for normal distribution.
seed (int): Random seed.
"""
super().__init__(seed=seed)
self.population = n
self.network = nx.Graph() # Initialize the network
self.datacollector = mesa.DataCollector(model_reporters={"Network": "network"})
# Create Agents
power = self.rng.normal(mean, std_dev, n)
power = np.clip(power, 0, 1)
position = self.rng.normal(mean, std_dev, n)
position = np.clip(position, 0, 1)
AllianceAgent.create_agents(self, n, power, position)
agent_ids = [
(agent.unique_id, {"size": 300, "level": 0}) for agent in self.agents
]
self.network.add_nodes_from(agent_ids)
def add_link(self, meta_agent, agents):
"""
Add links between a meta agent and its constituent agents in the network.
Args:
meta_agent (MetaAgent): The meta agent.
agents (list): List of agents.
"""
for agent in agents:
self.network.add_edge(meta_agent.unique_id, agent.unique_id)
def calculate_shapley_value(self, agents):
"""
Calculate the Shapley value of the two agents.
Args:
agents (list): List of agents.
Returns:
tuple: Potential utility, new position, and level.
"""
positions = agents.get("position")
new_position = 1 - (max(positions) - min(positions))
potential_utility = agents.agg("power", sum) * 1.2 * new_position
value_0 = 0.5 * agents[0].power + 0.5 * (potential_utility - agents[1].power)
value_1 = 0.5 * agents[1].power + 0.5 * (potential_utility - agents[0].power)
if value_0 > agents[0].power and value_1 > agents[1].power:
if agents[0].level > agents[1].level:
level = agents[0].level
elif agents[0].level == agents[1].level:
level = agents[0].level + 1
else:
level = agents[1].level
return potential_utility, new_position, level
def only_best_combination(self, combinations):
"""
Filter to keep only the best combination for each agent.
Args:
combinations (list): List of combinations.
Returns:
dict: Unique combinations.
"""
best = {}
# Determine best option for EACH agent
for group, value in combinations:
agent_ids = sorted(group.get("unique_id")) # by default is bilateral
# Deal with all possibilities
if (
agent_ids[0] not in best and agent_ids[1] not in best
): # if neither in add both
best[agent_ids[0]] = [group, value, agent_ids]
best[agent_ids[1]] = [group, value, agent_ids]
elif (
agent_ids[0] in best and agent_ids[1] in best
): # if both in, see if both would be trading up
if (
value[0] > best[agent_ids[0]][1][0]
and value[0] > best[agent_ids[1]][1][0]
):
# Remove the old alliances
del best[best[agent_ids[0]][2][1]]
del best[best[agent_ids[1]][2][0]]
# Add the new alliance
best[agent_ids[0]] = [group, value, agent_ids]
best[agent_ids[1]] = [group, value, agent_ids]
elif (
agent_ids[0] in best
): # if only agent_ids[0] in, see if it would be trading up
if value[0] > best[agent_ids[0]][1][0]:
# Remove the old alliance for agent_ids[0]
del best[best[agent_ids[0]][2][1]]
# Add the new alliance
best[agent_ids[0]] = [group, value, agent_ids]
best[agent_ids[1]] = [group, value, agent_ids]
elif (
agent_ids[1] in best
): # if only agent_ids[1] in, see if it would be trading up
if value[0] > best[agent_ids[1]][1][0]:
# Remove the old alliance for agent_ids[1]
del best[best[agent_ids[1]][2][0]]
# Add the new alliance
best[agent_ids[0]] = [group, value, agent_ids]
best[agent_ids[1]] = [group, value, agent_ids]
# Create a unique dictionary of the best combinations
unique_combinations = {}
for group, value, agents_nums in best.values():
unique_combinations[tuple(agents_nums)] = [group, value]
return unique_combinations.values()
def step(self):
"""
Execute one step of the model.
"""
# Get all other agents of the same type
agent_types = list(self.agents_by_type.keys())
for agent_type in agent_types:
similar_agents = self.agents_by_type[agent_type]
# Find the best combinations using find_combinations
if (
len(similar_agents) > 1
): # only form alliances if there are more than 1 agent
combinations = find_combinations(
self,
similar_agents,
size=2,
evaluation_func=self.calculate_shapley_value,
filter_func=self.only_best_combination,
)
for alliance, attributes in combinations:
class_name = f"MetaAgentLevel{attributes[2]}"
meta = create_meta_agent(
self,
class_name,
alliance,
Agent,
meta_attributes={
"level": attributes[2],
"power": attributes[0],
"position": attributes[1],
},
)
# Update the network if a new meta agent instance created
if meta:
self.network.add_node(
meta.unique_id,
size=(meta.level + 1) * 300,
level=meta.level,
)
self.add_link(meta, meta.agents)
App#
import matplotlib.pyplot as plt
import networkx as nx
import solara
from matplotlib.figure import Figure
from mesa.examples.advanced.alliance_formation.model import MultiLevelAllianceModel
from mesa.visualization import SolaraViz
from mesa.visualization.utils import update_counter
model_params = {
"seed": {
"type": "InputText",
"value": 42,
"label": "Random Seed",
},
"n": {
"type": "SliderInt",
"value": 50,
"label": "Number of agents:",
"min": 10,
"max": 100,
"step": 1,
},
}
# Create visualization elements. The visualization elements are solara components
# that receive the model instance as a "prop" and display it in a certain way.
# Under the hood these are just classes that receive the model instance.
# You can also author your own visualization elements, which can also be functions
# that receive the model instance and return a valid solara component.
@solara.component
def plot_network(model):
update_counter.get()
g = model.network
pos = nx.fruchterman_reingold_layout(g)
fig = Figure()
ax = fig.subplots()
labels = {agent.unique_id: agent.unique_id for agent in model.agents}
node_sizes = [g.nodes[node]["size"] for node in g.nodes]
node_colors = [g.nodes[node]["size"] for node in g.nodes()]
nx.draw(
g,
pos,
node_size=node_sizes,
node_color=node_colors,
cmap=plt.cm.coolwarm,
labels=labels,
ax=ax,
)
solara.FigureMatplotlib(fig)
# Create initial model instance
model = MultiLevelAllianceModel(50)
# Create the SolaraViz page. This will automatically create a server and display the
# visualization elements in a web browser.
# Display it using the following command in the example directory:
# solara run app.py
# It will automatically update and display any changes made to this file
page = SolaraViz(
model,
components=[plot_network],
model_params=model_params,
name="Alliance Formation Model",
)
page # noqa