Skip to content

Commit

Permalink
meta_agents
Browse files Browse the repository at this point in the history
- Add create meta-agents to experimental
- Add tests of meta-agents
- Add example with an alliance formation model in basic examples
  • Loading branch information
tpike3 committed Jan 10, 2025
1 parent 29d0f3b commit 01a1788
Show file tree
Hide file tree
Showing 9 changed files with 539 additions and 3 deletions.
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)

10 changes: 10 additions & 0 deletions mesa/examples/basic/alliance_formation_model/__init__.py
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.")
71 changes: 71 additions & 0 deletions mesa/examples/basic/alliance_formation_model/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import mesa
from mesa.experimental.meta_agents import create_meta_agent


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.hierarchy > calling_agent.hierarchy:
hierarchy = other_agent.hierarchy
elif other_agent.hierarchy == calling_agent.hierarchy:
hierarchy = calling_agent.hierarchy + 1
else:
hierarchy = calling_agent.hierarchy

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


class AllianceAgent(mesa.Agent):
"""
Agent has three attributes power (float), position (float) and hierarchy (int)
"""

def __init__(self, model, power, position, hierarchy=0):
super().__init__(model)
self.power = power
self.position = position
self.hierarchy = hierarchy

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"MetaAgentHierarchy{shapley_value[2]}"
meta = create_meta_agent(
self.model,
class_name,
{other_agent, self},
meta_attributes={
"hierarchy": shapley_value[2],
"power": shapley_value[0],
"position": shapley_value[1],
},
)

# Update the network if a new meta agent instance created
if meta:
self.model.network.add_node(
meta.unique_id,
size=(meta.hierarchy + 1) * 300,
hierarchy=meta.hierarchy,
)
74 changes: 74 additions & 0 deletions mesa/examples/basic/alliance_formation_model/app.py
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 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
39 changes: 39 additions & 0 deletions mesa/examples/basic/alliance_formation_model/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import networkx as nx
import numpy as np
from 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 = np.random.normal(mean, std_dev, n)
power = np.clip(power, 0, 1)
position = np.random.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, "hierarchy": 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.agents)
25 changes: 25 additions & 0 deletions mesa/experimental/meta_agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""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_agents import create_meta_agent

__all__ = ["create_meta_agent"]
Loading

0 comments on commit 01a1788

Please sign in to comment.