From 0fa42a8c793410b62ad7771e4017f69d770e0bc3 Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Mon, 30 Dec 2024 19:00:32 -0500 Subject: [PATCH] meta-agent plus --- mesa/examples/__init__.py | 2 + .../{ => alliance_formation}/__init__.py | 0 .../{ => alliance_formation}/agents.py | 31 ++++---- .../alliance_formation/app.py | 74 +++++++++++++++++++ .../{ => alliance_formation}/model.py | 6 +- .../multi_level_alliance/__init__.py | 10 +++ .../multi_level_alliance/agents.py | 72 ++++++++++++++++++ .../{ => multi_level_alliance}/app.py | 1 + .../multi_level_alliance/model.py | 39 ++++++++++ mesa/experimental/meta_agents/__init__.py | 4 +- mesa/experimental/meta_agents/meta_agent.py | 66 +++++++++++++++++ .../{meta_agents.py => multi_levels.py} | 53 ++++--------- tests/test_examples.py | 2 +- tests/test_meta_agents.py | 2 +- 14 files changed, 303 insertions(+), 59 deletions(-) rename mesa/examples/basic/alliance_formation_model/{ => alliance_formation}/__init__.py (100%) rename mesa/examples/basic/alliance_formation_model/{ => alliance_formation}/agents.py (70%) create mode 100644 mesa/examples/basic/alliance_formation_model/alliance_formation/app.py rename mesa/examples/basic/alliance_formation_model/{ => alliance_formation}/model.py (84%) create mode 100644 mesa/examples/basic/alliance_formation_model/multi_level_alliance/__init__.py create mode 100644 mesa/examples/basic/alliance_formation_model/multi_level_alliance/agents.py rename mesa/examples/basic/alliance_formation_model/{ => multi_level_alliance}/app.py (97%) create mode 100644 mesa/examples/basic/alliance_formation_model/multi_level_alliance/model.py create mode 100644 mesa/experimental/meta_agents/meta_agent.py rename mesa/experimental/meta_agents/{meta_agents.py => multi_levels.py} (76%) diff --git a/mesa/examples/__init__.py b/mesa/examples/__init__.py index a2b9ff9080c..0cb1814a1e3 100644 --- a/mesa/examples/__init__.py +++ b/mesa/examples/__init__.py @@ -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 diff --git a/mesa/examples/basic/alliance_formation_model/__init__.py b/mesa/examples/basic/alliance_formation_model/alliance_formation/__init__.py similarity index 100% rename from mesa/examples/basic/alliance_formation_model/__init__.py rename to mesa/examples/basic/alliance_formation_model/alliance_formation/__init__.py diff --git a/mesa/examples/basic/alliance_formation_model/agents.py b/mesa/examples/basic/alliance_formation_model/alliance_formation/agents.py similarity index 70% rename from mesa/examples/basic/alliance_formation_model/agents.py rename to mesa/examples/basic/alliance_formation_model/alliance_formation/agents.py index 38ff5ff92f8..7a2e1f88c42 100644 --- a/mesa/examples/basic/alliance_formation_model/agents.py +++ b/mesa/examples/basic/alliance_formation_model/alliance_formation/agents.py @@ -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): @@ -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 @@ -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, ) diff --git a/mesa/examples/basic/alliance_formation_model/alliance_formation/app.py b/mesa/examples/basic/alliance_formation_model/alliance_formation/app.py new file mode 100644 index 00000000000..c981f5caebd --- /dev/null +++ b/mesa/examples/basic/alliance_formation_model/alliance_formation/app.py @@ -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 diff --git a/mesa/examples/basic/alliance_formation_model/model.py b/mesa/examples/basic/alliance_formation_model/alliance_formation/model.py similarity index 84% rename from mesa/examples/basic/alliance_formation_model/model.py rename to mesa/examples/basic/alliance_formation_model/alliance_formation/model.py index a3b05156813..349946de0a2 100644 --- a/mesa/examples/basic/alliance_formation_model/model.py +++ b/mesa/examples/basic/alliance_formation_model/alliance_formation/model.py @@ -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): @@ -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) @@ -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) diff --git a/mesa/examples/basic/alliance_formation_model/multi_level_alliance/__init__.py b/mesa/examples/basic/alliance_formation_model/multi_level_alliance/__init__.py new file mode 100644 index 00000000000..49a80b627ee --- /dev/null +++ b/mesa/examples/basic/alliance_formation_model/multi_level_alliance/__init__.py @@ -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.") diff --git a/mesa/examples/basic/alliance_formation_model/multi_level_alliance/agents.py b/mesa/examples/basic/alliance_formation_model/multi_level_alliance/agents.py new file mode 100644 index 00000000000..7a2e1f88c42 --- /dev/null +++ b/mesa/examples/basic/alliance_formation_model/multi_level_alliance/agents.py @@ -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, + ) diff --git a/mesa/examples/basic/alliance_formation_model/app.py b/mesa/examples/basic/alliance_formation_model/multi_level_alliance/app.py similarity index 97% rename from mesa/examples/basic/alliance_formation_model/app.py rename to mesa/examples/basic/alliance_formation_model/multi_level_alliance/app.py index ba536a92533..a2a4bead137 100644 --- a/mesa/examples/basic/alliance_formation_model/app.py +++ b/mesa/examples/basic/alliance_formation_model/multi_level_alliance/app.py @@ -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) diff --git a/mesa/examples/basic/alliance_formation_model/multi_level_alliance/model.py b/mesa/examples/basic/alliance_formation_model/multi_level_alliance/model.py new file mode 100644 index 00000000000..d66b5090ece --- /dev/null +++ b/mesa/examples/basic/alliance_formation_model/multi_level_alliance/model.py @@ -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) diff --git a/mesa/experimental/meta_agents/__init__.py b/mesa/experimental/meta_agents/__init__.py index 43f238bf253..aa3f4b4fc9d 100644 --- a/mesa/experimental/meta_agents/__init__.py +++ b/mesa/experimental/meta_agents/__init__.py @@ -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"] diff --git a/mesa/experimental/meta_agents/meta_agent.py b/mesa/experimental/meta_agents/meta_agent.py new file mode 100644 index 00000000000..5b9719815ec --- /dev/null +++ b/mesa/experimental/meta_agents/meta_agent.py @@ -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 diff --git a/mesa/experimental/meta_agents/meta_agents.py b/mesa/experimental/meta_agents/multi_levels.py similarity index 76% rename from mesa/experimental/meta_agents/meta_agents.py rename to mesa/experimental/meta_agents/multi_levels.py index 75048c46533..a56d40b8aa8 100644 --- a/mesa/experimental/meta_agents/meta_agents.py +++ b/mesa/experimental/meta_agents/multi_levels.py @@ -17,14 +17,16 @@ from types import MethodType +from mesa.experimental.meta_agents.meta_agent import MetaAgent -def create_meta_agent( + +def create_multi_levels( model, new_agent_class: str, agents, meta_attributes=dict(), # noqa B006 meta_functions=dict(), # noqa B006 - retain_subagent_functions=True, + retain_subagent_functions=False, retain_subagent_attributes=False, ): """Dynamically create a new meta-agent class and instantiate agents in that class. @@ -44,24 +46,9 @@ def create_meta_agent( created agent type - New class instance if created a new dynamically created agent type """ - from mesa import ( - Agent, # Import the Agent class from Mesa locally to avoid circular import - ) - # Convert agents to set to ensure uniqueness agents = set(agents) - def add_agents(meta_agent, new_agents: set[Agent]): - """Update agents' meta-agent attribute and store agent's meta-agent. - - Parameters: - meta_agent (MetaAgent): The meta-agent instance. - new_agents (Set[Agent]): The new agents to be added. - """ - meta_agent.agents.update(new_agents) - for agent in new_agents: - agent.meta_agent = meta_agent - def add_functions(meta_agent_instance, agents, meta_functions): """Add functions to the meta-agent instance. @@ -73,17 +60,16 @@ def add_functions(meta_agent_instance, agents, meta_functions): if retain_subagent_functions: agent_classes = {type(agent) for agent in agents} for agent_class in agent_classes: - for name in dir(agent_class): + for name in agent_class.__dict__: if callable(getattr(agent_class, name)) and not name.startswith( "__" ): original_method = getattr(agent_class, name) meta_functions[name] = original_method - if meta_functions: - for name, func in meta_functions.items(): - bound_method = MethodType(func, meta_agent_instance) - setattr(meta_agent_instance, name, bound_method) + for name, func in meta_functions.items(): + bound_method = MethodType(func, meta_agent_instance) + setattr(meta_agent_instance, name, bound_method) def add_attributes(meta_agent_instance, agents, meta_attributes): """Add attributes to the meta-agent instance. @@ -99,9 +85,8 @@ def add_attributes(meta_agent_instance, agents, meta_attributes): if not callable(value): meta_attributes[name] = value - if meta_attributes: - for key, value in meta_attributes.items(): - setattr(meta_agent_instance, key, value) + for key, value in meta_attributes.items(): + setattr(meta_agent_instance, key, value) # Path 1 - Add agents to existing meta-agent subagents = [a for a in agents if hasattr(a, "meta_agent")] @@ -109,13 +94,14 @@ def add_attributes(meta_agent_instance, agents, meta_attributes): if len(subagents) == 1: add_attributes(subagents[0].meta_agent, agents, meta_attributes) add_functions(subagents[0].meta_agent, agents, meta_functions) - add_agents(subagents[0].meta_agent, agents) + subagents[0].meta_agent.add_subagents(agents) + else: subagent = model.random.choice(subagents) agents = set(agents) - set(subagents) add_attributes(subagent.meta_agent, agents, meta_attributes) add_functions(subagent.meta_agent, agents, meta_functions) - add_agents(subagent.meta_agent, agents) + subagent.meta_agent.add_subagents(agents) # TODO: Add way for user to specify how agents join meta-agent instead of random choice else: # Path 2 - Create a new instance of an existing meta-agent class @@ -132,28 +118,21 @@ def add_attributes(meta_agent_instance, agents, meta_attributes): meta_agent_instance = agent_class(model, agents) add_attributes(meta_agent_instance, agents, meta_attributes) add_functions(meta_agent_instance, agents, meta_functions) - add_agents(meta_agent_instance, agents) model.register_agent(meta_agent_instance) return meta_agent_instance else: # Path 3 - Create a new meta-agent class - class MetaAgentClass(Agent): - def __init__(self, model, agents): - super().__init__(model) - self.agents = agents - meta_agent_class = type( new_agent_class, - (MetaAgentClass,), + (MetaAgent,), { "unique_id": None, - "agents": None, + "_subset": None, }, ) - meta_agent_instance = meta_agent_class(model=model, agents=agents) + meta_agent_instance = meta_agent_class(model, agents) add_attributes(meta_agent_instance, agents, meta_attributes) add_functions(meta_agent_instance, agents, meta_functions) model.register_agent(meta_agent_instance) - add_agents(meta_agent_instance, agents) return meta_agent_instance diff --git a/tests/test_examples.py b/tests/test_examples.py index 8e923e64419..48b52f3427e 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -110,7 +110,7 @@ def test_wolf_sheep(): # noqa: D103 def test_alliance_formation_model(): # noqa: D103 - from mesa.examples.basic.alliance_formation_model import app + from multi_level_alliance import app app.page # noqa: B018 diff --git a/tests/test_meta_agents.py b/tests/test_meta_agents.py index 62284ff7177..7b805327f2b 100644 --- a/tests/test_meta_agents.py +++ b/tests/test_meta_agents.py @@ -3,7 +3,7 @@ import pytest from mesa import Agent, Model -from mesa.experimental.meta_agents.meta_agents import create_meta_agent +from mesa.experimental.meta_agents.multi_levels import create_meta_agent @pytest.fixture