Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Meta agents #2575

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions mesa/examples/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from multi_level_alliance.model import AllianceModel

from mesa.examples.advanced.epstein_civil_violence.model import EpsteinCivilViolence
from mesa.examples.advanced.pd_grid.model import PdGrid
from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt
from mesa.examples.advanced.wolf_sheep.model import WolfSheep
from mesa.examples.basic.alliance_formation_model.model import AllianceModel
from mesa.examples.basic.boid_flockers.model import BoidFlockers
from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealth
from mesa.examples.basic.conways_game_of_life.model import ConwaysGameOfLife
from mesa.examples.basic.schelling.model import Schelling
from mesa.examples.basic.virus_on_network.model import VirusOnNetwork

__all__ = [
"AllianceModel",
"BoidFlockers",
"BoltzmannWealth",
"ConwaysGameOfLife",
Expand Down
40 changes: 40 additions & 0 deletions mesa/examples/basic/alliance_formation_model/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Alliance Formation Model

## Summary
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](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.

In its current configuration, agents being part of multiple meta-agents is not supported

## Installation

This model requires Mesa's recommended install and scipy
```
$ pip install mesa[rec] scipy
```

## 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 agents
* ``app.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)](https://www.jasss.org/16/3/6.html)

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import logging

# Configure logging
logging.basicConfig(
level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)

# Example usage of logging
logger = logging.getLogger(__name__)
logger.info("Logging is configured and ready to use.")
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import mesa
from mesa.experimental.meta_agents import create_multi_levels


def calculate_shapley_value(calling_agent, other_agent):
"""
Calculate the Shapley value of the two agents
"""
new_position = 1 - abs(calling_agent.position - other_agent.position)
potential_utility = (calling_agent.power + other_agent.power) * 1.1 * new_position
value_me = 0.5 * calling_agent.power + 0.5 * (potential_utility - other_agent.power)
value_other = 0.5 * other_agent.power + 0.5 * (
potential_utility - calling_agent.power
)

# Determine if there is value in the alliance
if value_me > calling_agent.power and value_other > other_agent.power:
if other_agent.level > calling_agent.level:
level = other_agent.level
elif other_agent.level == calling_agent.level:
level = calling_agent.level + 1
else:
level = calling_agent.level

return (potential_utility, new_position, level)
else:
return None


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

def form_alliance(self):
# Randomly select another agent of the same type
other_agents = [
agent for agent in self.model.agents_by_type[type(self)] if agent != self
]

# Determine if there is a beneficial alliance
if other_agents:
other_agent = self.random.choice(other_agents)
shapley_value = calculate_shapley_value(self, other_agent)
if shapley_value:
class_name = f"MetaAgentLevel{shapley_value[2]}"
meta = create_multi_levels(
self.model,
class_name,
{other_agent, self},
meta_attributes={
"level": shapley_value[2],
"power": shapley_value[0],
"position": shapley_value[1],
},
retain_subagent_functions=True,
)

# Update the network if a new meta agent instance created
if meta:
self.model.network.add_node(
meta.unique_id,
size=(meta.level + 1) * 300,
level=meta.level,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import matplotlib.pyplot as plt
import networkx as nx
import solara
from matplotlib.figure import Figure
from multi_level_alliance.model import AllianceModel

from mesa.mesa_logging import DEBUG, log_to_stderr
from mesa.visualization import SolaraViz
from mesa.visualization.utils import update_counter

log_to_stderr(DEBUG)

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.kamada_kawai_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 = AllianceModel(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
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import networkx as nx
import numpy as np
from multi_level_alliance.agents import AllianceAgent

import mesa


class AllianceModel(mesa.Model):
def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42):
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):
for agent in agents:
self.network.add_edge(meta_agent.unique_id, agent.unique_id)

def step(self):
for agent_class in list(
self.agent_types
): # Convert to list to avoid modification during iteration
self.agents_by_type[agent_class].shuffle_do("form_alliance")

# Update graph
if agent_class is not AllianceAgent:
for meta_agent in self.agents_by_type[agent_class]:
self.add_link(meta_agent, meta_agent.subset)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import logging

# Configure logging
logging.basicConfig(
level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)

# Example usage of logging
logger = logging.getLogger(__name__)
logger.info("Logging is configured and ready to use.")
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import mesa
from mesa.experimental.meta_agents import create_multi_levels


def calculate_shapley_value(calling_agent, other_agent):
"""
Calculate the Shapley value of the two agents
"""
new_position = 1 - abs(calling_agent.position - other_agent.position)
potential_utility = (calling_agent.power + other_agent.power) * 1.1 * new_position
value_me = 0.5 * calling_agent.power + 0.5 * (potential_utility - other_agent.power)
value_other = 0.5 * other_agent.power + 0.5 * (
potential_utility - calling_agent.power
)

# Determine if there is value in the alliance
if value_me > calling_agent.power and value_other > other_agent.power:
if other_agent.level > calling_agent.level:
level = other_agent.level
elif other_agent.level == calling_agent.level:
level = calling_agent.level + 1
else:
level = calling_agent.level

return (potential_utility, new_position, level)
else:
return None


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

def form_alliance(self):
# Randomly select another agent of the same type
other_agents = [
agent for agent in self.model.agents_by_type[type(self)] if agent != self
]

# Determine if there is a beneficial alliance
if other_agents:
other_agent = self.random.choice(other_agents)
shapley_value = calculate_shapley_value(self, other_agent)
if shapley_value:
class_name = f"MetaAgentLevel{shapley_value[2]}"
meta = create_multi_levels(
self.model,
class_name,
{other_agent, self},
meta_attributes={
"level": shapley_value[2],
"power": shapley_value[0],
"position": shapley_value[1],
},
retain_subagent_functions=True,
)

# Update the network if a new meta agent instance created
if meta:
self.model.network.add_node(
meta.unique_id,
size=(meta.level + 1) * 300,
level=meta.level,
)
Loading
Loading