Skip to content

Commit

Permalink
meta-agent plus
Browse files Browse the repository at this point in the history
  • Loading branch information
tpike3 committed Dec 31, 2024
1 parent a87d382 commit 0fa42a8
Show file tree
Hide file tree
Showing 14 changed files with 303 additions and 59 deletions.
2 changes: 2 additions & 0 deletions mesa/examples/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import mesa
from mesa.experimental.meta_agents import create_meta_agent
from mesa.experimental.meta_agents import create_multi_levels


def calculate_shapley_value(calling_agent, other_agent):
Expand All @@ -15,29 +15,29 @@ def calculate_shapley_value(calling_agent, other_agent):

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

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


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

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

def form_alliance(self):
# Randomly select another agent of the same type
Expand All @@ -50,22 +50,23 @@ def form_alliance(self):
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(
class_name = f"MetaAgentLevel{shapley_value[2]}"
meta = create_multi_levels(
self.model,
class_name,
{other_agent, self},
meta_attributes={
"hierarchy": shapley_value[2],
"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.hierarchy + 1) * 300,
hierarchy=meta.hierarchy,
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
@@ -1,8 +1,8 @@
import networkx as nx
import numpy as np
from multi_level_alliance.agents import AllianceAgent

import mesa
from mesa.examples.basic.alliance_formation_model.agents import AllianceAgent


class AllianceModel(mesa.Model):
Expand All @@ -19,7 +19,7 @@ def __init__(self, n=50, mean=0.5, std_dev=0.1, seed=42):
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
(agent.unique_id, {"size": 300, "level": 0}) for agent in self.agents
]
self.network.add_nodes_from(agent_ids)

Expand All @@ -36,4 +36,4 @@ def step(self):
# 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)
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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from mesa.mesa_logging import DEBUG, log_to_stderr
from mesa.visualization import SolaraViz
from mesa.visualization.utils import update_counter
from multi_level_alliance.model import AllianceModel

log_to_stderr(DEBUG)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import networkx as nx
import numpy as np

import mesa
from mesa.examples.basic.alliance_formation_model.multi_level_alliance.agents import AllianceAgent


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)
4 changes: 2 additions & 2 deletions mesa/experimental/meta_agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
"""

from .meta_agents import create_meta_agent
from .multi_levels import create_multi_levels

__all__ = ["create_meta_agent"]
__all__ = ["create_multi_levels"]
66 changes: 66 additions & 0 deletions mesa/experimental/meta_agents/meta_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Implementation of Mesa's meta agent capability."""

from mesa.agent import Agent, AgentSet


class MetaAgent(Agent):
"""A MetaAgent is an agent that contains other agents as components."""

def __init__(self, model, agents):
"""Create a new MetaAgent."""
super().__init__(model)
self._subset = AgentSet(agents or [], random=model.random)

# Add ref to meta_agent in subagents
for agent in self._subset:
agent.meta_agent = self # TODO: Make a set for meta_agents

@property
def subset(self):
"""Read-only access to components as an AgentSet."""
return self._subset

def add_subagents(self, new_agents: set[Agent]):
"""Add an agent as a component.
Args:
new_agents (Agent): The agents to add to MetaAgent subset
"""
for agent in new_agents:
self._subset.add(agent)

for agent in new_agents:
agent.meta_agent = self # TODO: Make a set for meta_agents

def remove_subagents(self, remove_agents: set[Agent]):
"""Remove an agent component.
Args:
remove_agents (Agent): The agents to remove from MetAgents
"""
for agent in remove_agents:
self._subset.discard(agent)

for agent in remove_agents:
agent.meta_agent = None # TODO: Remove meta_agent from set

def step(self):
"""Perform the agent's step.
Override this method to define the meta agent's behavior.
By default, does nothing.
"""

def __len__(self):
"""Return the number of components."""
return len(self._subset)

def __iter__(self):
"""Iterate over components."""
return iter(self._subset)

def __contains__(self, agent):
"""Check if an agent is a component."""
return agent in self._subset
Loading

0 comments on commit 0fa42a8

Please sign in to comment.