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
2 changes: 2 additions & 0 deletions mesa/examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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 MultiLevelAllianceModel
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
Expand All @@ -13,6 +14,7 @@
"BoltzmannWealth",
"ConwaysGameOfLife",
"EpsteinCivilViolence",
"MultiLevelAllianceModel",
"PdGrid",
"Schelling",
"SugarscapeG1mt",
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]
```

## 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)

Empty file.
20 changes: 20 additions & 0 deletions mesa/examples/basic/alliance_formation_model/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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.
"""
72 changes: 72 additions & 0 deletions mesa/examples/basic/alliance_formation_model/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import matplotlib.pyplot as plt
import networkx as nx
import solara
from matplotlib.figure import Figure

from mesa.examples.basic.alliance_formation_model.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()]

Check warning on line 42 in mesa/examples/basic/alliance_formation_model/app.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/app.py#L35-L42

Added lines #L35 - L42 were not covered by tests

nx.draw(

Check warning on line 44 in mesa/examples/basic/alliance_formation_model/app.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/app.py#L44

Added line #L44 was not covered by tests
g,
pos,
node_size=node_sizes,
node_color=node_colors,
cmap=plt.cm.coolwarm,
labels=labels,
ax=ax,
)

solara.FigureMatplotlib(fig)

Check warning on line 54 in mesa/examples/basic/alliance_formation_model/app.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/app.py#L54

Added line #L54 was not covered by tests


# 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
180 changes: 180 additions & 0 deletions mesa/examples/basic/alliance_formation_model/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import networkx as nx
import numpy as np

import mesa
from mesa.examples.basic.alliance_formation_model.agents import AllianceAgent
from mesa.experimental.meta_agents.meta_agent import find_combinations
from mesa.experimental.meta_agents.multi_levels import multi_level_agents


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

Check warning on line 71 in mesa/examples/basic/alliance_formation_model/model.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/model.py#L71

Added line #L71 was not covered by tests
elif agents[0].level == agents[1].level:
level = agents[0].level + 1
else:
level = agents[1].level

Check warning on line 75 in mesa/examples/basic/alliance_formation_model/model.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/model.py#L75

Added line #L75 was not covered by tests

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 = multi_level_agents(
self,
class_name,
alliance,
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.subset)
4 changes: 2 additions & 2 deletions mesa/experimental/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
- Features graduate from experimental status once their APIs are stabilized
"""

from mesa.experimental import cell_space, devs, mesa_signals
from mesa.experimental import cell_space, devs, mesa_signals, meta_agents

__all__ = ["cell_space", "devs", "mesa_signals"]
__all__ = ["cell_space", "devs", "mesa_signals", "meta_agents"]
26 changes: 26 additions & 0 deletions mesa/experimental/meta_agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""This method is for dynamically creating new agents (meta-agents).

Meta-agents are defined as agents composed of existing agents.

Meta-agents are created dynamically with a pointer to the model, name of the meta-agent,,
iterable of agents to belong to the new meta-agents, any new functions for the meta-agent,
any new attributes for the meta-agent, whether to retain sub-agent functions,
whether to retain sub-agent attributes.

Examples of meta-agents:
- An autonomous car where the subagents are the wheels, sensors,
battery, computer etc. and the meta-agent is the car itself.
- A company where the subagents are employees, departments, buildings, etc.
- A city where the subagents are people, buildings, streets, etc.

Currently meta-agents are restricted to one parent agent for each subagent/
one meta-agent per subagent.

Goal is to assess usage and expand functionality.

"""

from .meta_agent import MetaAgent
from .multi_levels import multi_level_agents

__all__ = ["MetaAgent", "multi_level_agents"]
Loading
Loading