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-agent creation #2561

Closed
wants to merge 5 commits into from
Closed
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
111 changes: 111 additions & 0 deletions mesa/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
self.unique_id: int = next(self._ids[model])
self.pos: Position | None = None
self.model.register_agent(self)
# Private attribute to track parent agent if metaagents are created
# Uses name mangling to prevent name clashes
self.__metaagent = None # Currently restricted to one parent agent

def remove(self) -> None:
"""Remove and delete the agent from the model.
Expand Down Expand Up @@ -133,6 +136,114 @@
agents.append(agent)
return AgentSet(agents, random=model.random)

def create_metaagent(
self,
new_agent_class: str,
agents: Iterable[Agent],
**unique_attributes_functions,
) -> None:
"""Dynamically create a new meta-agent class and instantiate agents in that class.

Args:
new_agent_class (str): The name of the new meta-agent class.
agents (Iterable[Agent]): The agents to be grouped into the new meta-agent class.
**unique_attributes_functions: A dictionary of unique attributes for that class.

Returns:
Adds the new meta-agent instance to agentset if adding agent to new class or new agent instance
- None if adding agent to existing class
- New class instance if created a new instance of a dynamically created agent type
- New class instance if created a new dynamically created agent type

Notes:
This method is useful for creating meta-agents that represent groups of agents with interdependent characteristics.
The new meta-agent class is created dynamically using the provided name and unique attributes and functions.

Currently restricted to one parent agent and one meta-agent per agent. Goal is to assess usage and expand functionality.

Method has three paths of execution:
1. Add agents to existing metaagent
2. Create new meta-agent instance of existing metaagent class
3. Create new meta-agent class

See alliance formation example for usage.

"""
# Convert agents to set to ensure uniqueness
agents = set(agents)

# Helper function to update agents __metaagent attribute and store agent's metaagent
def update_agents_metaagent(agents, metaagent):
for agent in agents:
agent._Agent__metaagent = metaagent

# Path 1 - Add agents to existing meta-agent
subagents = [agent for agent in agents if agent._Agent__metaagent is not None]

if len(subagents) > 0:
if len(subagents) == 1:
# Update metaagents agent set with new agents
subagents[0]._Agent__metaagent.agents.update(agents)
# Update subagents with metaagent
update_agents_metaagent(agents, subagents[0]._Agent__metaagent)
else:
# If there are multiple subagents, one is chosen at random to be the parent metaagent
subagent = self.random.choice(subagents)

Check warning on line 191 in mesa/agent.py

View check run for this annotation

Codecov / codecov/patch

mesa/agent.py#L191

Added line #L191 was not covered by tests
# Remove agent who are already part of metaagent
agents = set(agents) - set(subagents)
subagent._Agent__metaagent.agents.update(agents)
update_agents_metaagent(agents, subagent._Agent__metaagent)

Check warning on line 195 in mesa/agent.py

View check run for this annotation

Codecov / codecov/patch

mesa/agent.py#L193-L195

Added lines #L193 - L195 were not covered by tests
# TODO: Add way for user to add function to specify how agents join metaagent

else:
# Path 2 - Create a new instance of an existing meta-agent class
agent_class = next(
(
agent_type
for agent_type in self.model.agent_types
if agent_type.__name__ == new_agent_class
),
None,
)
if agent_class:
# Create an instance of the meta-agent class
meta_agent_instance = agent_class(

Check warning on line 210 in mesa/agent.py

View check run for this annotation

Codecov / codecov/patch

mesa/agent.py#L210

Added line #L210 was not covered by tests
self.model, **unique_attributes_functions
)
# Add agents to meta-agent instance
meta_agent_instance.agents = agents

Check warning on line 214 in mesa/agent.py

View check run for this annotation

Codecov / codecov/patch

mesa/agent.py#L214

Added line #L214 was not covered by tests
# Update subagents Agent.__metaagent attribute
update_agents_metaagent(agents, meta_agent_instance)

Check warning on line 216 in mesa/agent.py

View check run for this annotation

Codecov / codecov/patch

mesa/agent.py#L216

Added line #L216 was not covered by tests
# Register the new meta-agent instance
self.model.register_agent(meta_agent_instance)

Check warning on line 218 in mesa/agent.py

View check run for this annotation

Codecov / codecov/patch

mesa/agent.py#L218

Added line #L218 was not covered by tests

return meta_agent_instance

Check warning on line 220 in mesa/agent.py

View check run for this annotation

Codecov / codecov/patch

mesa/agent.py#L220

Added line #L220 was not covered by tests

# Path 3 - Create a new meta-agent class
else:
# Get agent types of subagents to create the new meta-agent class
agent_types = tuple({type(agent) for agent in agents})

meta_agent_class = type(
new_agent_class,
agent_types,
{
"unique_id": None,
"agents": agents,
},
)

# Create an instance of the meta-agent class
meta_agent_instance = meta_agent_class(
self.model, **unique_attributes_functions
)
# Register the new meta-agent instance
self.model.register_agent(meta_agent_instance)
# Update subagents Agent.__metaagent attribute
update_agents_metaagent(agents, meta_agent_instance)

return meta_agent_instance

@property
def random(self) -> Random:
"""Return a seeded stdlib rng."""
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
```
$ 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.
68 changes: 68 additions & 0 deletions mesa/examples/basic/alliance_formation_model/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import mesa

Check warning on line 1 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L1

Added line #L1 was not covered by tests


def calculate_shapley_value(self, other_agent):

Check warning on line 4 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L4

Added line #L4 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems a function, so don't use self or make it a method on your agent.

"""
Calculate the Shapley value of the two agents
"""
other_agent.hierarchy = other_agent.hierarchy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line makes no sense to me.

self.hierarchy = self.hierarchy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, you set self.hierarchy to itself.

new_position = 1 - abs(self.position - other_agent.position)
potential_utility = (self.power + other_agent.power) * 1.1 * new_position
value_me = 0.5 * self.power + 0.5 * (potential_utility - other_agent.power)
value_other = 0.5 * other_agent.power + 0.5 * (potential_utility - self.power)

Check warning on line 13 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L8-L13

Added lines #L8 - L13 were not covered by tests

# Determine ig there is value in the alliance
if value_me > self.power and value_other > other_agent.power:
if other_agent.hierarchy > self.hierarchy:
hierarchy = other_agent.hierarchy

Check warning on line 18 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L18

Added line #L18 was not covered by tests
elif other_agent.hierarchy == self.hierarchy:
hierarchy = self.hierarchy + 1

Check warning on line 20 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L20

Added line #L20 was not covered by tests
else:
hierarchy = self.hierarchy

Check warning on line 22 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L22

Added line #L22 was not covered by tests

return (potential_utility, new_position, hierarchy)

Check warning on line 24 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L24

Added line #L24 was not covered by tests
else:
return None

Check warning on line 26 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L26

Added line #L26 was not covered by tests


class AllianceAgent(mesa.Agent):

Check warning on line 29 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L29

Added line #L29 was not covered by tests
"""
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

Check warning on line 39 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L35-L39

Added lines #L35 - L39 were not covered by tests

def form_alliance(self):

Check warning on line 41 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L41

Added line #L41 was not covered by tests
# Randomly select another agent of the same type
other_agents = [

Check warning on line 43 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L43

Added line #L43 was not covered by tests
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)

Check warning on line 50 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L49-L50

Added lines #L49 - L50 were not covered by tests
if shapley_value:
class_name = f"MetaAgentHierarchy{shapley_value[2]}"
meta = self.create_metaagent(

Check warning on line 53 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L52-L53

Added lines #L52 - L53 were not covered by tests
class_name,
{other_agent, self},
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(

Check warning on line 63 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L63

Added line #L63 was not covered by tests
meta.unique_id, size=(meta.hierarchy + 1) * 200
)
self.model.new_agents = True

Check warning on line 66 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L66

Added line #L66 was not covered by tests
else:
self.model.new_agents = False

Check warning on line 68 in mesa/examples/basic/alliance_formation_model/agents.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/basic/alliance_formation_model/agents.py#L68

Added line #L68 was not covered by tests
75 changes: 75 additions & 0 deletions mesa/examples/basic/alliance_formation_model/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import matplotlib.pyplot as plt
import networkx as nx
import solara
from matplotlib.figure import Figure

Check warning on line 4 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#L1-L4

Added lines #L1 - L4 were not covered by tests

from mesa.examples.basic.alliance_formation.model import AllianceModel
from mesa.mesa_logging import DEBUG, log_to_stderr
from mesa.visualization import SolaraViz
from mesa.visualization.utils import update_counter

Check warning on line 9 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#L6-L9

Added lines #L6 - L9 were not covered by tests

log_to_stderr(DEBUG)

Check warning on line 11 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#L11

Added line #L11 was not covered by tests


model_params = {

Check warning on line 14 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#L14

Added line #L14 was not covered by tests
"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.spring_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 46 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#L37-L46

Added lines #L37 - L46 were not covered by tests

nx.draw(

Check warning on line 48 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#L48

Added line #L48 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 58 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#L58

Added line #L58 was not covered by tests


# Create initial model instance
model = AllianceModel(50)

Check warning on line 62 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#L62

Added line #L62 was not covered by tests

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

Check warning on line 69 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#L69

Added line #L69 was not covered by tests
model,
components=[plot_network],
model_params=model_params,
name="Alliance Formation Model",
)
page # noqa

Check warning on line 75 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#L75

Added line #L75 was not covered by tests
37 changes: 37 additions & 0 deletions mesa/examples/basic/alliance_formation_model/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import networkx as nx
import numpy as np
from agents import AllianceAgent

Check warning on line 3 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#L1-L3

Added lines #L1 - L3 were not covered by tests

import mesa

Check warning on line 5 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#L5

Added line #L5 was not covered by tests


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"})

Check warning on line 13 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#L8-L13

Added lines #L8 - L13 were not covered by tests

# Create Agents
power = np.random.normal(mean, std_dev, n)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change this to use self.rng to get the seeded numpy random number generator

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": 100}) for agent in self.agents]
self.network.add_nodes_from(agent_ids)

Check warning on line 22 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#L16-L22

Added lines #L16 - L22 were not covered by tests

def add_link(self, metaagent, agents):

Check warning on line 24 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#L24

Added line #L24 was not covered by tests
for agent in agents:
self.network.add_edge(metaagent.unique_id, agent.unique_id)

Check warning on line 26 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#L26

Added line #L26 was not covered by tests

def step(self):

Check warning on line 28 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#L28

Added line #L28 was not covered by tests
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")

Check warning on line 32 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#L32

Added line #L32 was not covered by tests

# Update graph
if agent_class is not AllianceAgent:
for metaagent in self.agents_by_type[agent_class]:
self.add_link(metaagent, metaagent.agents)

Check warning on line 37 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#L37

Added line #L37 was not covered by tests
Loading
Loading