From cd3305758e091c7037a43282d5f6e1cc5fdefe32 Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Fri, 27 Dec 2024 20:00:59 -0500 Subject: [PATCH 01/31] meta_agents - Add create meta-agents to experimental - Add tests of meta-agents - Add example with an alliance formation model in basic examples --- .../basic/alliance_formation_model/Readme.md | 40 +++++ .../alliance_formation_model/__init__.py | 10 ++ .../basic/alliance_formation_model/agents.py | 71 ++++++++ .../basic/alliance_formation_model/app.py | 74 ++++++++ .../basic/alliance_formation_model/model.py | 39 +++++ mesa/experimental/__init__.py | 4 +- mesa/experimental/meta_agents/__init__.py | 25 +++ mesa/experimental/meta_agents/meta_agents.py | 159 ++++++++++++++++++ tests/test_meta_agents.py | 112 ++++++++++++ tests/test_solara_viz.py | 10 +- 10 files changed, 539 insertions(+), 5 deletions(-) create mode 100644 mesa/examples/basic/alliance_formation_model/Readme.md create mode 100644 mesa/examples/basic/alliance_formation_model/__init__.py create mode 100644 mesa/examples/basic/alliance_formation_model/agents.py create mode 100644 mesa/examples/basic/alliance_formation_model/app.py create mode 100644 mesa/examples/basic/alliance_formation_model/model.py create mode 100644 mesa/experimental/meta_agents/__init__.py create mode 100644 mesa/experimental/meta_agents/meta_agents.py create mode 100644 tests/test_meta_agents.py diff --git a/mesa/examples/basic/alliance_formation_model/Readme.md b/mesa/examples/basic/alliance_formation_model/Readme.md new file mode 100644 index 00000000000..4cd0edab7b7 --- /dev/null +++ b/mesa/examples/basic/alliance_formation_model/Readme.md @@ -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) + diff --git a/mesa/examples/basic/alliance_formation_model/__init__.py b/mesa/examples/basic/alliance_formation_model/__init__.py new file mode 100644 index 00000000000..49a80b627ee --- /dev/null +++ b/mesa/examples/basic/alliance_formation_model/__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/agents.py b/mesa/examples/basic/alliance_formation_model/agents.py new file mode 100644 index 00000000000..38ff5ff92f8 --- /dev/null +++ b/mesa/examples/basic/alliance_formation_model/agents.py @@ -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, + ) diff --git a/mesa/examples/basic/alliance_formation_model/app.py b/mesa/examples/basic/alliance_formation_model/app.py new file mode 100644 index 00000000000..cfdadcf2da9 --- /dev/null +++ b/mesa/examples/basic/alliance_formation_model/app.py @@ -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 diff --git a/mesa/examples/basic/alliance_formation_model/model.py b/mesa/examples/basic/alliance_formation_model/model.py new file mode 100644 index 00000000000..44e6cb709fd --- /dev/null +++ b/mesa/examples/basic/alliance_formation_model/model.py @@ -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 = 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, "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) diff --git a/mesa/experimental/__init__.py b/mesa/experimental/__init__.py index 946b2ba53fc..2f73264608e 100644 --- a/mesa/experimental/__init__.py +++ b/mesa/experimental/__init__.py @@ -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"] diff --git a/mesa/experimental/meta_agents/__init__.py b/mesa/experimental/meta_agents/__init__.py new file mode 100644 index 00000000000..43f238bf253 --- /dev/null +++ b/mesa/experimental/meta_agents/__init__.py @@ -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"] diff --git a/mesa/experimental/meta_agents/meta_agents.py b/mesa/experimental/meta_agents/meta_agents.py new file mode 100644 index 00000000000..75048c46533 --- /dev/null +++ b/mesa/experimental/meta_agents/meta_agents.py @@ -0,0 +1,159 @@ +"""This method is for dynamically 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 meta-agent +2. Create new meta-agent instance of existing meta-agent class +3. Create new meta-agent class + +See alliance formation model in basic examples for usage. + +""" + +from types import MethodType + + +def create_meta_agent( + model, + new_agent_class: str, + agents, + meta_attributes=dict(), # noqa B006 + meta_functions=dict(), # noqa B006 + retain_subagent_functions=True, + retain_subagent_attributes=False, +): + """Dynamically create a new meta-agent class and instantiate agents in that class. + + Parameters: + model (Model): The model instance. + new_agent_class (str): The name of the new meta-agent class. + agents (Iterable[Agent]): The agents to be included in the meta-agent. + meta_attributes (dict): Attributes to be added to the meta-agent. + meta_functions (dict): Functions to be added to the meta-agent. + retain_subagent_functions (bool): Whether to retain functions from sub-agents. + retain_subagent_attributes (bool): Whether to retain attributes from sub-agents. + + Returns: + - None if adding agent(s) 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 + """ + 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. + + Parameters: + meta_agent_instance (MetaAgent): The meta-agent instance. + agents (Iterable[Agent]): The agents to derive functions from. + meta_functions (dict): Functions to be added to the meta-agent. + """ + if retain_subagent_functions: + agent_classes = {type(agent) for agent in agents} + for agent_class in agent_classes: + for name in dir(agent_class): + 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) + + def add_attributes(meta_agent_instance, agents, meta_attributes): + """Add attributes to the meta-agent instance. + + Parameters: + meta_agent_instance (MetaAgent): The meta-agent instance. + agents (Iterable[Agent]): The agents to derive attributes from. + meta_attributes (dict): Attributes to be added to the meta-agent. + """ + if retain_subagent_attributes: + for agent in agents: + for name, value in agent.__dict__.items(): + if not callable(value): + meta_attributes[name] = value + + if meta_attributes: + 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")] + if len(subagents) > 0: + 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) + 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) + # 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 + agent_class = next( + ( + agent_type + for agent_type in model.agent_types + if agent_type.__name__ == new_agent_class + ), + None, + ) + + if agent_class: + 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,), + { + "unique_id": None, + "agents": None, + }, + ) + + meta_agent_instance = meta_agent_class(model=model, agents=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_meta_agents.py b/tests/test_meta_agents.py new file mode 100644 index 00000000000..62284ff7177 --- /dev/null +++ b/tests/test_meta_agents.py @@ -0,0 +1,112 @@ +"""Tests for the meta_agents module.""" + +import pytest + +from mesa import Agent, Model +from mesa.experimental.meta_agents.meta_agents import create_meta_agent + + +@pytest.fixture +def setup_agents(): + """Set up the model and agents for testing. + + Returns: + tuple: A tuple containing the model and a list of agents. + """ + model = Model() + agent1 = Agent(model) + agent2 = Agent(model) + agent3 = Agent(model) + agent4 = Agent(model) + agent4.custom_attribute = "custom_value" + agents = [agent1, agent2, agent3, agent4] + return model, agents + + +def test_create_meta_agent_new_class(setup_agents): + """Test creating a new meta-agent class and test inclusion of attributes and functions. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + meta_agent = create_meta_agent( + model, + "MetaAgentClass", + agents, + meta_attributes={"attribute1": "value1"}, + meta_functions={"function1": lambda self: "function1"}, + retain_subagent_attributes=True, + ) + assert meta_agent is not None + assert meta_agent.attribute1 == "value1" + assert meta_agent.function1() == "function1" + assert meta_agent.agents == set(agents) + assert hasattr(meta_agent, "custom_attribute") + assert meta_agent.custom_attribute == "custom_value" + + +def test_create_meta_agent_existing_class(setup_agents): + """Test creating new meta-agent instance with an existing meta-agent class. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + + # Create Met Agent Class + meta_agent = create_meta_agent( + model, + "MetaAgentClass", + [agents[0], agents[2]], + meta_attributes={"attribute1": "value1"}, + meta_functions={"function1": lambda self: "function1"}, + ) + + # Create new meta-agent instance with existing class + meta_agent2 = create_meta_agent( + model, + "MetaAgentClass", + [agents[1], agents[3]], + meta_attributes={"attribute2": "value2"}, + meta_functions={"function2": lambda self: "function2"}, + retain_subagent_attributes=True, + ) + assert meta_agent is not None + assert meta_agent2.attribute2 == "value2" + assert meta_agent.function1() == "function1" + assert meta_agent.agents == {agents[2], agents[0]} + assert meta_agent2.function2() == "function2" + assert meta_agent2.agents == {agents[1], agents[3]} + assert hasattr(meta_agent2, "custom_attribute") + assert meta_agent2.custom_attribute == "custom_value" + + +def test_add_agents_to_existing_meta_agent(setup_agents): + """Test adding agents to an existing meta-agent instance. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + + meta_agent1 = create_meta_agent( + model, + "MetaAgentClass", + [agents[0], agents[3]], + meta_attributes={"attribute1": "value1"}, + meta_functions={"function1": lambda self: "function1"}, + retain_subagent_attributes=True, + ) + + create_meta_agent( + model, + "MetaAgentClass", + [agents[1], agents[0], agents[2]], + retain_subagent_attributes=True, + ) + assert meta_agent1.agents == {agents[0], agents[1], agents[2], agents[3]} + assert meta_agent1.function1() == "function1" + assert meta_agent1.attribute1 == "value1" + assert hasattr(meta_agent1, "custom_attribute") + assert meta_agent1.custom_attribute == "custom_value" diff --git a/tests/test_solara_viz.py b/tests/test_solara_viz.py index 3b8d82fb7bc..a84fc910364 100644 --- a/tests/test_solara_viz.py +++ b/tests/test_solara_viz.py @@ -92,7 +92,8 @@ def Test(user_params): assert slider_int.step is None -def test_call_space_drawer(mocker): # noqa: D103 +def test_call_space_drawer(mocker): + """Test the call to space drawer.""" mock_space_matplotlib = mocker.spy( mesa.visualization.components.matplotlib_components, "SpaceMatplotlib" ) @@ -153,7 +154,8 @@ def drawer(model): ) -def test_slider(): # noqa: D103 +def test_slider(): + """Test the Slider component.""" slider_float = Slider("Agent density", 0.8, 0.1, 1.0, 0.1) assert slider_float.is_float_slider assert slider_float.value == 0.8 @@ -167,7 +169,9 @@ def test_slider(): # noqa: D103 assert slider_dtype_float.is_float_slider -def test_model_param_checks(): # noqa: D103 +def test_model_param_checks(): + """Test the model parameter checks.""" + class ModelWithOptionalParams: def __init__(self, required_param, optional_param=10): pass From 5bc7b95ce499e3a146d665e100710a915afe7e8e Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Sun, 29 Dec 2024 12:11:24 -0500 Subject: [PATCH 02/31] Meta-Agents - add alliance formation model to example_tests --- mesa/examples/__init__.py | 2 ++ mesa/examples/basic/alliance_formation_model/app.py | 2 +- mesa/examples/basic/alliance_formation_model/model.py | 2 +- tests/test_examples.py | 11 +++++++++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/mesa/examples/__init__.py b/mesa/examples/__init__.py index beecd6a1b6a..d15e448ecbd 100644 --- a/mesa/examples/__init__.py +++ b/mesa/examples/__init__.py @@ -7,8 +7,10 @@ from mesa.examples.basic.conways_game_of_life.model import ConwaysGameOfLife from mesa.examples.basic.schelling.model import Schelling from mesa.examples.basic.virus_on_network.model import VirusOnNetwork +from mesa.examples.basic.alliance_formation_model.model import AllianceModel __all__ = [ + "AllianceModel", "BoidFlockers", "BoltzmannWealth", "ConwaysGameOfLife", diff --git a/mesa/examples/basic/alliance_formation_model/app.py b/mesa/examples/basic/alliance_formation_model/app.py index cfdadcf2da9..4ea70f17541 100644 --- a/mesa/examples/basic/alliance_formation_model/app.py +++ b/mesa/examples/basic/alliance_formation_model/app.py @@ -2,7 +2,7 @@ import networkx as nx import solara from matplotlib.figure import Figure -from model import AllianceModel +from mesa.examples.basic.alliance_formation_model.model import AllianceModel from mesa.mesa_logging import DEBUG, log_to_stderr from mesa.visualization import SolaraViz diff --git a/mesa/examples/basic/alliance_formation_model/model.py b/mesa/examples/basic/alliance_formation_model/model.py index 44e6cb709fd..50dc1ba5004 100644 --- a/mesa/examples/basic/alliance_formation_model/model.py +++ b/mesa/examples/basic/alliance_formation_model/model.py @@ -1,6 +1,6 @@ import networkx as nx import numpy as np -from agents import AllianceAgent +from mesa.examples.basic.alliance_formation_model.agents import AllianceAgent import mesa diff --git a/tests/test_examples.py b/tests/test_examples.py index 0e8a7edce42..b9536c588ef 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,5 +1,6 @@ # noqa: D100 from mesa.examples import ( + AllianceModel, BoidFlockers, BoltzmannWealth, ConwaysGameOfLife, @@ -106,3 +107,13 @@ def test_wolf_sheep(): # noqa: D103 simulator = ABMSimulator() WolfSheep(seed=42, simulator=simulator) simulator.run_for(10) + +def test_alliance_formation_model(): # noqa: D103 + from mesa.examples.basic.alliance_formation_model import app + + app.page # noqa: B018 + + model = AllianceModel(50, seed=42) + + for _i in range(10): + model.step() From a87d382df66324ae8cbb2347f7caeaebee28865e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:11:44 +0000 Subject: [PATCH 03/31] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/examples/__init__.py | 2 +- mesa/examples/basic/alliance_formation_model/app.py | 2 +- mesa/examples/basic/alliance_formation_model/model.py | 2 +- tests/test_examples.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mesa/examples/__init__.py b/mesa/examples/__init__.py index d15e448ecbd..a2b9ff9080c 100644 --- a/mesa/examples/__init__.py +++ b/mesa/examples/__init__.py @@ -2,12 +2,12 @@ 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 AllianceModel 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 from mesa.examples.basic.schelling.model import Schelling from mesa.examples.basic.virus_on_network.model import VirusOnNetwork -from mesa.examples.basic.alliance_formation_model.model import AllianceModel __all__ = [ "AllianceModel", diff --git a/mesa/examples/basic/alliance_formation_model/app.py b/mesa/examples/basic/alliance_formation_model/app.py index 4ea70f17541..ba536a92533 100644 --- a/mesa/examples/basic/alliance_formation_model/app.py +++ b/mesa/examples/basic/alliance_formation_model/app.py @@ -2,8 +2,8 @@ import networkx as nx import solara from matplotlib.figure import Figure -from mesa.examples.basic.alliance_formation_model.model import AllianceModel +from mesa.examples.basic.alliance_formation_model.model import AllianceModel from mesa.mesa_logging import DEBUG, log_to_stderr from mesa.visualization import SolaraViz from mesa.visualization.utils import update_counter diff --git a/mesa/examples/basic/alliance_formation_model/model.py b/mesa/examples/basic/alliance_formation_model/model.py index 50dc1ba5004..a3b05156813 100644 --- a/mesa/examples/basic/alliance_formation_model/model.py +++ b/mesa/examples/basic/alliance_formation_model/model.py @@ -1,8 +1,8 @@ import networkx as nx import numpy as np -from mesa.examples.basic.alliance_formation_model.agents import AllianceAgent import mesa +from mesa.examples.basic.alliance_formation_model.agents import AllianceAgent class AllianceModel(mesa.Model): diff --git a/tests/test_examples.py b/tests/test_examples.py index b9536c588ef..8e923e64419 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -108,6 +108,7 @@ def test_wolf_sheep(): # noqa: D103 WolfSheep(seed=42, simulator=simulator) simulator.run_for(10) + def test_alliance_formation_model(): # noqa: D103 from mesa.examples.basic.alliance_formation_model import app From c7c8311909b83ff0fb8d8b83cfac6d04a7625ae3 Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Mon, 30 Dec 2024 19:00:32 -0500 Subject: [PATCH 04/31] refactor meta-agent - allow for deliberate meta-agents creation - allow for combinatorics - allow for dynamic agent creation fix methods-functions; add tests --- mesa/examples/__init__.py | 4 +- .../basic/alliance_formation_model/Readme.md | 2 +- .../alliance_formation_model/__init__.py | 10 - .../basic/alliance_formation_model/agents.py | 65 +----- .../basic/alliance_formation_model/app.py | 10 +- .../basic/alliance_formation_model/model.py | 163 ++++++++++++++- mesa/experimental/meta_agents/__init__.py | 5 +- mesa/experimental/meta_agents/meta_agent.py | 148 ++++++++++++++ mesa/experimental/meta_agents/meta_agents.py | 159 --------------- mesa/experimental/meta_agents/multi_levels.py | 144 +++++++++++++ tests/test_examples.py | 6 +- tests/test_meta_agents.py | 191 ++++++++++++++++-- 12 files changed, 639 insertions(+), 268 deletions(-) create mode 100644 mesa/experimental/meta_agents/meta_agent.py delete mode 100644 mesa/experimental/meta_agents/meta_agents.py create mode 100644 mesa/experimental/meta_agents/multi_levels.py diff --git a/mesa/examples/__init__.py b/mesa/examples/__init__.py index a2b9ff9080c..0422b232994 100644 --- a/mesa/examples/__init__.py +++ b/mesa/examples/__init__.py @@ -2,7 +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 AllianceModel +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 @@ -10,11 +10,11 @@ from mesa.examples.basic.virus_on_network.model import VirusOnNetwork __all__ = [ - "AllianceModel", "BoidFlockers", "BoltzmannWealth", "ConwaysGameOfLife", "EpsteinCivilViolence", + "MultiLevelAllianceModel", "PdGrid", "Schelling", "SugarscapeG1mt", diff --git a/mesa/examples/basic/alliance_formation_model/Readme.md b/mesa/examples/basic/alliance_formation_model/Readme.md index 4cd0edab7b7..cf60ab4a2f7 100644 --- a/mesa/examples/basic/alliance_formation_model/Readme.md +++ b/mesa/examples/basic/alliance_formation_model/Readme.md @@ -13,7 +13,7 @@ In its current configuration, agents being part of multiple meta-agents is not s This model requires Mesa's recommended install and scipy ``` - $ pip install mesa[rec] scipy + $ pip install mesa[rec] ``` ## How to Run diff --git a/mesa/examples/basic/alliance_formation_model/__init__.py b/mesa/examples/basic/alliance_formation_model/__init__.py index 49a80b627ee..e69de29bb2d 100644 --- a/mesa/examples/basic/alliance_formation_model/__init__.py +++ b/mesa/examples/basic/alliance_formation_model/__init__.py @@ -1,10 +0,0 @@ -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/agents.py b/mesa/examples/basic/alliance_formation_model/agents.py index 38ff5ff92f8..4f33bb5b9a3 100644 --- a/mesa/examples/basic/alliance_formation_model/agents.py +++ b/mesa/examples/basic/alliance_formation_model/agents.py @@ -1,71 +1,20 @@ 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) + 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 - - 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 - ] + self.level = level - # 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], - }, - ) + """ + For this demo model agent only need attributes. - # 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, - ) + More complex models could have functions that define agent behavior. + """ diff --git a/mesa/examples/basic/alliance_formation_model/app.py b/mesa/examples/basic/alliance_formation_model/app.py index ba536a92533..9df3116dc54 100644 --- a/mesa/examples/basic/alliance_formation_model/app.py +++ b/mesa/examples/basic/alliance_formation_model/app.py @@ -3,13 +3,10 @@ import solara from matplotlib.figure import Figure -from mesa.examples.basic.alliance_formation_model.model import AllianceModel -from mesa.mesa_logging import DEBUG, log_to_stderr +from mesa.examples.basic.alliance_formation_model.model import MultiLevelAllianceModel from mesa.visualization import SolaraViz from mesa.visualization.utils import update_counter -log_to_stderr(DEBUG) - model_params = { "seed": { "type": "InputText", @@ -37,7 +34,7 @@ def plot_network(model): update_counter.get() g = model.network - pos = nx.kamada_kawai_layout(g) + pos = nx.fruchterman_reingold_layout(g) fig = Figure() ax = fig.subplots() labels = {agent.unique_id: agent.unique_id for agent in model.agents} @@ -58,13 +55,14 @@ def plot_network(model): # Create initial model instance -model = AllianceModel(50) +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], diff --git a/mesa/examples/basic/alliance_formation_model/model.py b/mesa/examples/basic/alliance_formation_model/model.py index a3b05156813..d1f8e891881 100644 --- a/mesa/examples/basic/alliance_formation_model/model.py +++ b/mesa/examples/basic/alliance_formation_model/model.py @@ -3,10 +3,25 @@ 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 AllianceModel(mesa.Model): +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 @@ -19,21 +34,147 @@ 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) 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 + elif agents[0].level == agents[1].level: + level = agents[0].level + 1 + else: + level = agents[1].level + + 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): - 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) + """ + 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) diff --git a/mesa/experimental/meta_agents/__init__.py b/mesa/experimental/meta_agents/__init__.py index 43f238bf253..420f4ad34b1 100644 --- a/mesa/experimental/meta_agents/__init__.py +++ b/mesa/experimental/meta_agents/__init__.py @@ -20,6 +20,7 @@ """ -from .meta_agents import create_meta_agent +from .meta_agent import MetaAgent +from .multi_levels import multi_level_agents -__all__ = ["create_meta_agent"] +__all__ = ["MetaAgent", "multi_level_agents"] diff --git a/mesa/experimental/meta_agents/meta_agent.py b/mesa/experimental/meta_agents/meta_agent.py new file mode 100644 index 00000000000..b1882c51195 --- /dev/null +++ b/mesa/experimental/meta_agents/meta_agent.py @@ -0,0 +1,148 @@ +"""Implementation of Mesa's meta agent capability. + +This contains two helper functions and a MetaAgent class that can be used to create agents that contain other agents as components. + +Helper functions: +1 - find_combinations: Find combinations of agents to create a meta-agent subset. +2- evaluate_combination: Evaluate combinations of agents by some user based criteria to determine if it should be a subset. + +Meta-Agent class (MetaAgent): An agent that contains other agents as components. +""" + +import itertools +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor + +from mesa.agent import Agent, AgentSet + + +def evaluate_combination( + candidate_group: tuple[Agent, ...], + model, + evaluation_func: Callable[[AgentSet], float] | None, +) -> tuple[AgentSet, float] | None: + """Evaluate a combination of agents. + + Args: + candidate_group (Tuple[Agent, ...]): The group of agents to evaluate. + model: The model instance. + evaluation_func (Optional[Callable[[AgentSet], float]]): The function to evaluate the group. + + Returns: + Optional[Tuple[AgentSet, float]]: The evaluated group and its value, or None. + """ + group_set = AgentSet(candidate_group, random=model.random) + if evaluation_func: + value = evaluation_func(group_set) + return group_set, value + return None + + +def find_combinations( + model, + group: AgentSet, + size: int | tuple[int, int] = (2, 5), + evaluation_func: Callable[[AgentSet], float] | None = None, + filter_func: Callable[[list[tuple[AgentSet, float]]], list[tuple[AgentSet, float]]] + | None = None, +) -> list[tuple[AgentSet, float]]: + """Find valuable combinations of agents in this set. + + Args: + model: The model instance. + group (AgentSet): The set of agents to find combinations in. + size (Union[int, Tuple[int, int]], optional): The size or range of sizes for combinations. Defaults to (2, 5). + evaluation_func (Optional[Callable[[AgentSet], float]], optional): The function to evaluate combinations. Defaults to None. + filter_func (Optional[Callable[[List[Tuple[AgentSet, float]]], List[Tuple[AgentSet, float]]]], optional): The function to filter combinations. Defaults to None. + + Returns: + List[Tuple[AgentSet, float]]: The list of valuable combinations. + """ + combinations = [] + with ThreadPoolExecutor() as executor: + futures = [] + # Allow one size or range of sizes to be passed + size_range = (size, size + 1) if isinstance(size, int) else size + + candidate_groups = itertools.chain.from_iterable( + itertools.combinations(group, size) for size in range(*size_range) + ) + for candidate_group in candidate_groups: + futures.append( + executor.submit( + evaluate_combination, candidate_group, model, evaluation_func + ) + ) + + for future in futures: + group_set, result = future.result() + if result: + combinations.append((group_set, result)) + + if len(combinations) > 0 and filter_func: + filtered_combinations = filter_func(combinations) + return filtered_combinations + + return combinations + + +class MetaAgent(Agent): + """A MetaAgent is an agent that contains other agents as components.""" + + def __init__(self, model, agents: set[Agent] | None = None): + """Create a new MetaAgent. + + Args: + model: The model instance. + agents (Optional[set[Agent]], optional): The set of agents to include in the MetaAgent. Defaults to None. + """ + 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 + + def __len__(self) -> int: + """Return the number of components.""" + return len(self._subset) + + def __iter__(self): + """Iterate over components.""" + return iter(self._subset) + + def __contains__(self, agent: Agent) -> bool: + """Check if an agent is a component.""" + return agent in self._subset + + @property + def subset(self) -> AgentSet: + """Read-only access to components as an AgentSet.""" + return self._subset + + def add_subagents(self, new_agents: set[Agent]): + """Add agents as components. + + Args: + new_agents (set[Agent]): The agents to add to MetaAgent subset. + """ + for agent in new_agents: + self._subset.add(agent) + agent.meta_agent = self # TODO: Make a set for meta_agents + + def remove_subagents(self, remove_agents: set[Agent]): + """Remove agents as components. + + Args: + remove_agents (set[Agent]): The agents to remove from MetaAgent. + """ + for agent in remove_agents: + self._subset.discard(agent) + 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. + """ diff --git a/mesa/experimental/meta_agents/meta_agents.py b/mesa/experimental/meta_agents/meta_agents.py deleted file mode 100644 index 75048c46533..00000000000 --- a/mesa/experimental/meta_agents/meta_agents.py +++ /dev/null @@ -1,159 +0,0 @@ -"""This method is for dynamically 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 meta-agent -2. Create new meta-agent instance of existing meta-agent class -3. Create new meta-agent class - -See alliance formation model in basic examples for usage. - -""" - -from types import MethodType - - -def create_meta_agent( - model, - new_agent_class: str, - agents, - meta_attributes=dict(), # noqa B006 - meta_functions=dict(), # noqa B006 - retain_subagent_functions=True, - retain_subagent_attributes=False, -): - """Dynamically create a new meta-agent class and instantiate agents in that class. - - Parameters: - model (Model): The model instance. - new_agent_class (str): The name of the new meta-agent class. - agents (Iterable[Agent]): The agents to be included in the meta-agent. - meta_attributes (dict): Attributes to be added to the meta-agent. - meta_functions (dict): Functions to be added to the meta-agent. - retain_subagent_functions (bool): Whether to retain functions from sub-agents. - retain_subagent_attributes (bool): Whether to retain attributes from sub-agents. - - Returns: - - None if adding agent(s) 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 - """ - 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. - - Parameters: - meta_agent_instance (MetaAgent): The meta-agent instance. - agents (Iterable[Agent]): The agents to derive functions from. - meta_functions (dict): Functions to be added to the meta-agent. - """ - if retain_subagent_functions: - agent_classes = {type(agent) for agent in agents} - for agent_class in agent_classes: - for name in dir(agent_class): - 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) - - def add_attributes(meta_agent_instance, agents, meta_attributes): - """Add attributes to the meta-agent instance. - - Parameters: - meta_agent_instance (MetaAgent): The meta-agent instance. - agents (Iterable[Agent]): The agents to derive attributes from. - meta_attributes (dict): Attributes to be added to the meta-agent. - """ - if retain_subagent_attributes: - for agent in agents: - for name, value in agent.__dict__.items(): - if not callable(value): - meta_attributes[name] = value - - if meta_attributes: - 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")] - if len(subagents) > 0: - 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) - 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) - # 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 - agent_class = next( - ( - agent_type - for agent_type in model.agent_types - if agent_type.__name__ == new_agent_class - ), - None, - ) - - if agent_class: - 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,), - { - "unique_id": None, - "agents": None, - }, - ) - - meta_agent_instance = meta_agent_class(model=model, agents=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/mesa/experimental/meta_agents/multi_levels.py b/mesa/experimental/meta_agents/multi_levels.py new file mode 100644 index 00000000000..6238c53b9d6 --- /dev/null +++ b/mesa/experimental/meta_agents/multi_levels.py @@ -0,0 +1,144 @@ +"""This function is for dynamically growing multiple levels of meta-agents. + +Each new level creates a new meta-agent class that is created dynamically using the provided name and +unique attributes and methods. + +Currently restricted to one parent agent and one meta-agent per agent. +Goal is to assess usage and expand functionality. + +Function has three paths of execution: +1. Add agents to existing meta-agent +2. Create new meta-agent instance of existing meta-agent class +3. Create new meta-agent class + +See multi-level alliance formation model in basic examples for usage. + +""" + +from collections.abc import Callable, Iterable +from types import MethodType +from typing import Any + +from mesa.experimental.meta_agents.meta_agent import MetaAgent + + +def multi_level_agents( + model: Any, + new_agent_class: str, + agents: Iterable[Any], + meta_attributes: dict[str, Any] = dict(), # noqa B006 + meta_methods: dict[str, Callable] = dict(), # noqa B006 + retain_subagent_methods: bool = False, + retain_subagent_attributes: bool = False, +) -> Any | None: + """Dynamically create a new meta-agent class and instantiate agents in that class. + + Parameters: + model (Any): The model instance. + new_agent_class (str): The name of the new meta-agent class. + agents (Iterable[Any]): The agents to be included in the meta-agent. + meta_attributes (Dict[str, Any]): Attributes to be added to the meta-agent. + meta_methods (Dict[str, Callable]): Methods to be added to the meta-agent. + retain_subagent_functions (bool): Whether to retain functions from sub-agents. + retain_subagent_attributes (bool): Whether to retain attributes from sub-agents. + + Returns: + Optional[Any]: + - None if adding agent(s) 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 + """ + # Convert agents to set to ensure uniqueness + agents = set(agents) + + def add_methods( + meta_agent_instance: Any, + agents: Iterable[Any], + meta_methods: dict[str, Callable], + ) -> None: + """Add functions to the meta-agent instance. + + Parameters: + meta_agent_instance (Any): The meta-agent instance. + agents (Iterable[Any]): The agents to derive functions from. + meta_methods (Dict[str, Callable]): Functions to be added to the meta-agent. + """ + if retain_subagent_methods: + agent_classes = {type(agent) for agent in agents} + for agent_class in agent_classes: + for name in agent_class.__dict__: + if callable(getattr(agent_class, name)) and not name.startswith( + "__" + ): + original_method = getattr(agent_class, name) + meta_methods[name] = original_method + + for name, meth in meta_methods.items(): + bound_method = MethodType(meth, meta_agent_instance) + setattr(meta_agent_instance, name, bound_method) + + def add_attributes( + meta_agent_instance: Any, agents: Iterable[Any], meta_attributes: dict[str, Any] + ) -> None: + """Add attributes to the meta-agent instance. + + Parameters: + meta_agent_instance (Any): The meta-agent instance. + agents (Iterable[Any]): The agents to derive attributes from. + meta_attributes (Dict[str, Any]): Attributes to be added to the meta-agent. + """ + if retain_subagent_attributes: + for agent in agents: + for name, value in agent.__dict__.items(): + if not callable(value): + meta_attributes[name] = 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")] + if len(subagents) > 0: + if len(subagents) == 1: + add_attributes(subagents[0].meta_agent, agents, meta_attributes) + add_methods(subagents[0].meta_agent, agents, meta_methods) + 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_methods(subagent.meta_agent, agents, meta_methods) + 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 + agent_class = next( + ( + agent_type + for agent_type in model.agent_types + if agent_type.__name__ == new_agent_class + ), + None, + ) + + if agent_class: + meta_agent_instance = agent_class(model, agents) + add_attributes(meta_agent_instance, agents, meta_attributes) + add_methods(meta_agent_instance, agents, meta_methods) + return meta_agent_instance + else: + # Path 3 - Create a new meta-agent class + meta_agent_class = type( + new_agent_class, + (MetaAgent,), + { + "unique_id": None, + "_subset": None, + }, + ) + + meta_agent_instance = meta_agent_class(model, agents) + add_attributes(meta_agent_instance, agents, meta_attributes) + add_methods(meta_agent_instance, agents, meta_methods) + return meta_agent_instance diff --git a/tests/test_examples.py b/tests/test_examples.py index 8e923e64419..f9e020cbde2 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,10 +1,10 @@ # noqa: D100 from mesa.examples import ( - AllianceModel, BoidFlockers, BoltzmannWealth, ConwaysGameOfLife, EpsteinCivilViolence, + MultiLevelAllianceModel, PdGrid, Schelling, SugarscapeG1mt, @@ -114,7 +114,9 @@ def test_alliance_formation_model(): # noqa: D103 app.page # noqa: B018 - model = AllianceModel(50, seed=42) + model = MultiLevelAllianceModel(50, seed=42) for _i in range(10): model.step() + + assert len(model.agents) == len(model.network.nodes) diff --git a/tests/test_meta_agents.py b/tests/test_meta_agents.py index 62284ff7177..6f8dff8208e 100644 --- a/tests/test_meta_agents.py +++ b/tests/test_meta_agents.py @@ -3,7 +3,25 @@ import pytest from mesa import Agent, Model -from mesa.experimental.meta_agents.meta_agents import create_meta_agent +from mesa.experimental.meta_agents.meta_agent import ( + MetaAgent, + evaluate_combination, + find_combinations, +) +from mesa.experimental.meta_agents.multi_levels import multi_level_agents + + +class CustomAgent(Agent): + """A custom agent with additional attributes and methods.""" + + def __init__(self, model): + """A custom agent constructor.""" + super().__init__(model) + self.custom_attribute = "custom_value" + + def custom_method(self): + """A custom agent method.""" + return "custom_method_value" @pytest.fixture @@ -17,8 +35,7 @@ def setup_agents(): agent1 = Agent(model) agent2 = Agent(model) agent3 = Agent(model) - agent4 = Agent(model) - agent4.custom_attribute = "custom_value" + agent4 = CustomAgent(model) agents = [agent1, agent2, agent3, agent4] return model, agents @@ -30,20 +47,23 @@ def test_create_meta_agent_new_class(setup_agents): setup_agents (tuple): The model and agents fixture. """ model, agents = setup_agents - meta_agent = create_meta_agent( + meta_agent = multi_level_agents( model, "MetaAgentClass", agents, meta_attributes={"attribute1": "value1"}, - meta_functions={"function1": lambda self: "function1"}, + meta_methods={"function1": lambda self: "function1"}, retain_subagent_attributes=True, + retain_subagent_methods=True, ) assert meta_agent is not None assert meta_agent.attribute1 == "value1" assert meta_agent.function1() == "function1" - assert meta_agent.agents == set(agents) + assert meta_agent._subset == set(agents) assert hasattr(meta_agent, "custom_attribute") assert meta_agent.custom_attribute == "custom_value" + assert hasattr(meta_agent, "custom_method") + assert meta_agent.custom_method() == "custom_method_value" def test_create_meta_agent_existing_class(setup_agents): @@ -54,32 +74,35 @@ def test_create_meta_agent_existing_class(setup_agents): """ model, agents = setup_agents - # Create Met Agent Class - meta_agent = create_meta_agent( + # Create Meta Agent Class + meta_agent = multi_level_agents( model, "MetaAgentClass", [agents[0], agents[2]], meta_attributes={"attribute1": "value1"}, - meta_functions={"function1": lambda self: "function1"}, + meta_methods={"function1": lambda self: "function1"}, ) # Create new meta-agent instance with existing class - meta_agent2 = create_meta_agent( + meta_agent2 = multi_level_agents( model, "MetaAgentClass", [agents[1], agents[3]], meta_attributes={"attribute2": "value2"}, - meta_functions={"function2": lambda self: "function2"}, + meta_methods={"function2": lambda self: "function2"}, retain_subagent_attributes=True, + retain_subagent_methods=True, ) assert meta_agent is not None assert meta_agent2.attribute2 == "value2" assert meta_agent.function1() == "function1" - assert meta_agent.agents == {agents[2], agents[0]} + assert meta_agent._subset == {agents[2], agents[0]} assert meta_agent2.function2() == "function2" - assert meta_agent2.agents == {agents[1], agents[3]} + assert meta_agent2._subset == {agents[1], agents[3]} assert hasattr(meta_agent2, "custom_attribute") assert meta_agent2.custom_attribute == "custom_value" + assert hasattr(meta_agent2, "custom_method") + assert meta_agent2.custom_method() == "custom_method_value" def test_add_agents_to_existing_meta_agent(setup_agents): @@ -90,23 +113,157 @@ def test_add_agents_to_existing_meta_agent(setup_agents): """ model, agents = setup_agents - meta_agent1 = create_meta_agent( + meta_agent1 = multi_level_agents( model, "MetaAgentClass", [agents[0], agents[3]], meta_attributes={"attribute1": "value1"}, - meta_functions={"function1": lambda self: "function1"}, + meta_methods={"function1": lambda self: "function1"}, retain_subagent_attributes=True, + retain_subagent_methods=True, ) - create_meta_agent( + multi_level_agents( model, "MetaAgentClass", [agents[1], agents[0], agents[2]], retain_subagent_attributes=True, + retain_subagent_methods=True, ) - assert meta_agent1.agents == {agents[0], agents[1], agents[2], agents[3]} + assert meta_agent1._subset == {agents[0], agents[1], agents[2], agents[3]} assert meta_agent1.function1() == "function1" assert meta_agent1.attribute1 == "value1" assert hasattr(meta_agent1, "custom_attribute") assert meta_agent1.custom_attribute == "custom_value" + assert hasattr(meta_agent1, "custom_method") + assert meta_agent1.custom_method() == "custom_method_value" + + +def test_meta_agent_integration(setup_agents): + """Test the integration of MetaAgent with the model. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + + meta_agent = multi_level_agents( + model, + "MetaAgentClass", + agents, + meta_attributes={"attribute1": "value1"}, + meta_methods={"function1": lambda self: "function1"}, + retain_subagent_attributes=True, + retain_subagent_methods=True, + ) + + model.step() + + assert meta_agent in model.agents + assert meta_agent.function1() == "function1" + assert meta_agent.attribute1 == "value1" + assert hasattr(meta_agent, "custom_attribute") + assert meta_agent.custom_attribute == "custom_value" + assert hasattr(meta_agent, "custom_method") + assert meta_agent.custom_method() == "custom_method_value" + + +def test_evaluate_combination(setup_agents): + """Test the evaluate_combination function. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + + def evaluation_func(agent_set): + return len(agent_set) + + result = evaluate_combination(tuple(agents), model, evaluation_func) + assert result is not None + assert result[1] == len(agents) + + +def test_find_combinations(setup_agents): + """Test the find_combinations function. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + agent_set = set(agents) + + def evaluation_func(agent_set): + return len(agent_set) + + def filter_func(combinations): + return [combo for combo in combinations if combo[1] > 2] + + combinations = find_combinations( + model, + agent_set, + size=(2, 4), + evaluation_func=evaluation_func, + filter_func=filter_func, + ) + assert len(combinations) > 0 + for combo in combinations: + assert combo[1] > 2 + + +def test_meta_agent_len(setup_agents): + """Test the __len__ method of MetaAgent. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + meta_agent = MetaAgent(model, set(agents)) + assert len(meta_agent) == len(agents) + + +def test_meta_agent_iter(setup_agents): + """Test the __iter__ method of MetaAgent. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + meta_agent = MetaAgent(model, set(agents)) + assert list(iter(meta_agent)) == list(meta_agent._subset) + + +def test_meta_agent_contains(setup_agents): + """Test the __contains__ method of MetaAgent. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + meta_agent = MetaAgent(model, set(agents)) + for agent in agents: + assert agent in meta_agent + + +def test_meta_agent_add_subagents(setup_agents): + """Test the add_subagents method of MetaAgent. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + meta_agent = MetaAgent(model, {agents[0], agents[1]}) + meta_agent.add_subagents({agents[2], agents[3]}) + assert meta_agent._subset == set(agents) + + +def test_meta_agent_remove_subagents(setup_agents): + """Test the remove_subagents method of MetaAgent. + + Args: + setup_agents (tuple): The model and agents fixture. + """ + model, agents = setup_agents + meta_agent = MetaAgent(model, set(agents)) + meta_agent.remove_subagents({agents[2], agents[3]}) + assert meta_agent._subset == {agents[0], agents[1]} From 3529b7d326c5f083c8e3444d60c54751a5a668ba Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 6 Jan 2025 16:24:23 +0100 Subject: [PATCH 05/31] Basic bugfix for bug ContinuousSpace.get_neighbors (#2599) Co-authored-by: Ewout ter Hoeven --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ewout ter Hoeven --- mesa/examples/basic/boid_flockers/agents.py | 3 ++- mesa/space.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mesa/examples/basic/boid_flockers/agents.py b/mesa/examples/basic/boid_flockers/agents.py index 2ff00cbaef2..a8f23f915aa 100644 --- a/mesa/examples/basic/boid_flockers/agents.py +++ b/mesa/examples/basic/boid_flockers/agents.py @@ -58,7 +58,8 @@ def __init__( def step(self): """Get the Boid's neighbors, compute the new vector, and move accordingly.""" - self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False) + neighbors = self.model.space.get_neighbors(self.pos, self.vision, True) + self.neighbors = [n for n in neighbors if n is not self] # If no neighbors, maintain current direction if not self.neighbors: diff --git a/mesa/space.py b/mesa/space.py index 6e15426bf34..d503d42e41b 100644 --- a/mesa/space.py +++ b/mesa/space.py @@ -1415,6 +1415,13 @@ def get_neighbors( coordinates. i.e. if you are searching for the neighbors of a given agent, True will include that agent in the results. + + Notes: + If 1 or more agents are located on pos, include_center=False will remove all these agents + from the results. So, if you really want to get the neighbors of a given agent, + you should set include_center=True, and then filter the list of agents to remove + the given agent (i.e., self when calling it from an agent). + """ if self._agent_points is None: self._build_agent_cache() From 1b0ad153ae7537d5ae750ec1465927e5911cdd71 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:01:41 +0100 Subject: [PATCH 06/31] [pre-commit.ci] pre-commit autoupdate (#2601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.8.1 → v0.8.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.1...v0.8.6) - [github.com/asottile/pyupgrade: v3.19.0 → v3.19.1](https://github.com/asottile/pyupgrade/compare/v3.19.0...v3.19.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7209ceebcac..308e345a6a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.8.1 + rev: v0.8.6 hooks: # Run the linter. - id: ruff @@ -14,7 +14,7 @@ repos: - id: ruff-format types_or: [ python, pyi, jupyter ] - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py311-plus] From 70375d1c0a0cc94b0d5de08ca01458e3bd18d06a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 10 Jan 2025 08:41:06 +0100 Subject: [PATCH 07/31] Reimplementation of Continuous Space (#2584) --- benchmarks/configurations.py | 9 +- mesa/examples/basic/boid_flockers/agents.py | 63 +-- mesa/examples/basic/boid_flockers/app.py | 7 +- mesa/examples/basic/boid_flockers/model.py | 67 ++- mesa/experimental/__init__.py | 4 +- .../experimental/continuous_space/__init__.py | 8 + .../continuous_space/continuous_space.py | 273 +++++++++++ .../continuous_space_agents.py | 101 ++++ mesa/visualization/mpl_space_drawing.py | 5 +- pyproject.toml | 1 + tests/test_continuous_space.py | 432 ++++++++++++++++++ 11 files changed, 889 insertions(+), 81 deletions(-) create mode 100644 mesa/experimental/continuous_space/__init__.py create mode 100644 mesa/experimental/continuous_space/continuous_space.py create mode 100644 mesa/experimental/continuous_space/continuous_space_agents.py create mode 100644 tests/test_continuous_space.py diff --git a/benchmarks/configurations.py b/benchmarks/configurations.py index e1e193c4e6d..62f52bf3181 100644 --- a/benchmarks/configurations.py +++ b/benchmarks/configurations.py @@ -90,14 +90,19 @@ "seeds": 25, "replications": 3, "steps": 20, - "parameters": {"population": 200, "width": 100, "height": 100, "vision": 5}, + "parameters": { + "population_size": 200, + "width": 100, + "height": 100, + "vision": 5, + }, }, "large": { "seeds": 10, "replications": 3, "steps": 10, "parameters": { - "population": 400, + "population_size": 400, "width": 150, "height": 150, "vision": 15, diff --git a/mesa/examples/basic/boid_flockers/agents.py b/mesa/examples/basic/boid_flockers/agents.py index a8f23f915aa..10f31cdc530 100644 --- a/mesa/examples/basic/boid_flockers/agents.py +++ b/mesa/examples/basic/boid_flockers/agents.py @@ -6,10 +6,10 @@ import numpy as np -from mesa import Agent +from mesa.experimental.continuous_space import ContinuousSpaceAgent -class Boid(Agent): +class Boid(ContinuousSpaceAgent): """A Boid-style flocker agent. The agent follows three behaviors to flock: @@ -26,10 +26,12 @@ class Boid(Agent): def __init__( self, model, - speed, - direction, - vision, - separation, + space, + position=(0, 0), + speed=1, + direction=(1, 1), + vision=1, + separation=1, cohere=0.03, separate=0.015, match=0.05, @@ -46,7 +48,8 @@ def __init__( separate: Relative importance of avoiding close neighbors (default: 0.015) match: Relative importance of matching neighbors' directions (default: 0.05) """ - super().__init__(model) + super().__init__(space, model) + self.position = position self.speed = speed self.direction = direction self.vision = vision @@ -58,47 +61,31 @@ def __init__( def step(self): """Get the Boid's neighbors, compute the new vector, and move accordingly.""" - neighbors = self.model.space.get_neighbors(self.pos, self.vision, True) + neighbors, distances = self.get_neighbors_in_radius(radius=self.vision) self.neighbors = [n for n in neighbors if n is not self] # If no neighbors, maintain current direction - if not self.neighbors: - new_pos = self.pos + self.direction * self.speed - self.model.space.move_agent(self, new_pos) + if not neighbors: + self.position += self.direction * self.speed return - # Initialize vectors for the three flocking behaviors - cohere = np.zeros(2) # Cohesion vector - match_vector = np.zeros(2) # Alignment vector - separation_vector = np.zeros(2) # Separation vector + delta = self.space.calculate_difference_vector(self.position, agents=neighbors) - # Calculate the contribution of each neighbor to the three behaviors - for neighbor in self.neighbors: - heading = self.model.space.get_heading(self.pos, neighbor.pos) - distance = self.model.space.get_distance(self.pos, neighbor.pos) - - # Cohesion - steer towards the average position of neighbors - cohere += heading - - # Separation - avoid getting too close - if distance < self.separation: - separation_vector -= heading - - # Alignment - match neighbors' flying direction - match_vector += neighbor.direction - - # Weight each behavior by its factor and normalize by number of neighbors - n = len(self.neighbors) - cohere = cohere * self.cohere_factor - separation_vector = separation_vector * self.separate_factor - match_vector = match_vector * self.match_factor + cohere_vector = delta.sum(axis=0) * self.cohere_factor + separation_vector = ( + -1 * delta[distances < self.separation].sum(axis=0) * self.separate_factor + ) + match_vector = ( + np.asarray([n.direction for n in neighbors]).sum(axis=0) * self.match_factor + ) # Update direction based on the three behaviors - self.direction += (cohere + separation_vector + match_vector) / n + self.direction += (cohere_vector + separation_vector + match_vector) / len( + neighbors + ) # Normalize direction vector self.direction /= np.linalg.norm(self.direction) # Move boid - new_pos = self.pos + self.direction * self.speed - self.model.space.move_agent(self, new_pos) + self.position += self.direction * self.speed diff --git a/mesa/examples/basic/boid_flockers/app.py b/mesa/examples/basic/boid_flockers/app.py index 16b1ccf7f68..174e2736a2a 100644 --- a/mesa/examples/basic/boid_flockers/app.py +++ b/mesa/examples/basic/boid_flockers/app.py @@ -1,3 +1,8 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath("../../../..")) + from mesa.examples.basic.boid_flockers.model import BoidFlockers from mesa.visualization import Slider, SolaraViz, make_space_component @@ -17,7 +22,7 @@ def boid_draw(agent): "value": 42, "label": "Random Seed", }, - "population": Slider( + "population_size": Slider( label="Number of boids", value=100, min=10, diff --git a/mesa/examples/basic/boid_flockers/model.py b/mesa/examples/basic/boid_flockers/model.py index 5b4974f3a20..c5d46a12b85 100644 --- a/mesa/examples/basic/boid_flockers/model.py +++ b/mesa/examples/basic/boid_flockers/model.py @@ -5,11 +5,17 @@ Uses numpy arrays to represent vectors. """ +import os +import sys + +sys.path.insert(0, os.path.abspath("../../../..")) + + import numpy as np from mesa import Model from mesa.examples.basic.boid_flockers.agents import Boid -from mesa.space import ContinuousSpace +from mesa.experimental.continuous_space import ContinuousSpace class BoidFlockers(Model): @@ -17,7 +23,7 @@ class BoidFlockers(Model): def __init__( self, - population=100, + population_size=100, width=100, height=100, speed=1, @@ -31,7 +37,7 @@ def __init__( """Create a new Boids Flocking model. Args: - population: Number of Boids in the simulation (default: 100) + population_size: Number of Boids in the simulation (default: 100) width: Width of the space (default: 100) height: Height of the space (default: 100) speed: How fast the Boids move (default: 1) @@ -44,48 +50,35 @@ def __init__( """ super().__init__(seed=seed) - # Model Parameters - self.population = population - self.vision = vision - self.speed = speed - self.separation = separation - # Set up the space - self.space = ContinuousSpace(width, height, torus=True) - - # Store flocking weights - self.factors = {"cohere": cohere, "separate": separate, "match": match} + self.space = ContinuousSpace( + [[0, width], [0, height]], + torus=True, + random=self.random, + n_agents=population_size, + ) # Create and place the Boid agents - self.make_agents() + positions = self.rng.random(size=(population_size, 2)) * self.space.size + directions = self.rng.uniform(-1, 1, size=(population_size, 2)) + Boid.create_agents( + self, + population_size, + self.space, + position=positions, + direction=directions, + cohere=cohere, + separate=separate, + match=match, + speed=speed, + vision=vision, + separation=separation, + ) # For tracking statistics self.average_heading = None self.update_average_heading() - def make_agents(self): - """Create and place all Boid agents randomly in the space.""" - for _ in range(self.population): - # Random position - x = self.random.random() * self.space.x_max - y = self.random.random() * self.space.y_max - pos = np.array((x, y)) - - # Random initial direction - direction = np.random.random(2) * 2 - 1 # Random vector between -1 and 1 - direction /= np.linalg.norm(direction) # Normalize - - # Create and place the Boid - boid = Boid( - model=self, - speed=self.speed, - direction=direction, - vision=self.vision, - separation=self.separation, - **self.factors, - ) - self.space.place_agent(boid, pos) - def update_average_heading(self): """Calculate the average heading (direction) of all Boids.""" if not self.agents: diff --git a/mesa/experimental/__init__.py b/mesa/experimental/__init__.py index 2f73264608e..194cd1c95d4 100644 --- a/mesa/experimental/__init__.py +++ b/mesa/experimental/__init__.py @@ -15,6 +15,6 @@ - Features graduate from experimental status once their APIs are stabilized """ -from mesa.experimental import cell_space, devs, mesa_signals, meta_agents +from mesa.experimental import cell_space, continuous_space, devs, mesa_signals, meta_agents -__all__ = ["cell_space", "devs", "mesa_signals", "meta_agents"] +__all__ = ["cell_space", "continuous_space", "devs", "mesa_signals", "meta_agents"] diff --git a/mesa/experimental/continuous_space/__init__.py b/mesa/experimental/continuous_space/__init__.py new file mode 100644 index 00000000000..13425cc0b8c --- /dev/null +++ b/mesa/experimental/continuous_space/__init__.py @@ -0,0 +1,8 @@ +"""Continuous space support.""" + +from mesa.experimental.continuous_space.continuous_space import ContinuousSpace +from mesa.experimental.continuous_space.continuous_space_agents import ( + ContinuousSpaceAgent, +) + +__all__ = ["ContinuousSpace", "ContinuousSpaceAgent"] diff --git a/mesa/experimental/continuous_space/continuous_space.py b/mesa/experimental/continuous_space/continuous_space.py new file mode 100644 index 00000000000..232eb38414b --- /dev/null +++ b/mesa/experimental/continuous_space/continuous_space.py @@ -0,0 +1,273 @@ +"""A Continuous Space class.""" + +import warnings +from collections.abc import Iterable +from itertools import compress +from random import Random + +import numpy as np +from numpy.typing import ArrayLike +from scipy.spatial.distance import cdist + +from mesa.agent import Agent, AgentSet + + +class ContinuousSpace: + """Continuous space where each agent can have an arbitrary position.""" + + @property + def x_min(self): # noqa: D102 + # compatibility with solara_viz + return self.dimensions[0, 0] + + @property + def x_max(self): # noqa: D102 + # compatibility with solara_viz + return self.dimensions[0, 1] + + @property + def y_min(self): # noqa: D102 + # compatibility with solara_viz + return self.dimensions[1, 0] + + @property + def y_max(self): # noqa: D102 + # compatibility with solara_viz + return self.dimensions[1, 1] + + @property + def width(self): # noqa: D102 + # compatibility with solara_viz + return self.size[0] + + @property + def height(self): # noqa: D102 + # compatibility with solara_viz + return self.size[1] + + def __init__( + self, + dimensions: ArrayLike, + torus: bool = False, + random: Random | None = None, + n_agents: int = 100, + ) -> None: + """Create a new continuous space. + + Args: + dimensions: a numpy array like object where each row specifies the minimum and maximum value of that dimension. + torus: boolean for whether the space wraps around or not + random: a seeded stdlib random.Random instance + n_agents: the expected number of agents in the space + + Internally, a numpy array is used to store the positions of all agents. This is resized if needed, + but you can control the initial size explicitly by passing n_agents. + + + """ + if random is None: + warnings.warn( + "Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly", + UserWarning, + stacklevel=2, + ) + random = Random() + self.random = random + + self.dimensions: np.array = np.asanyarray(dimensions) + self.ndims: int = self.dimensions.shape[0] + self.size: np.array = self.dimensions[:, 1] - self.dimensions[:, 0] + self.center: np.array = np.sum(self.dimensions, axis=1) / 2 + + self.torus: bool = torus + + # self._agent_positions is the array containing all agent positions + # plus potential extra empty rows + # agent_positions is a view into _agent_positions containing only the filled rows + self._agent_positions: np.array = np.empty( + (n_agents, self.dimensions.shape[0]), dtype=float + ) + self.agent_positions: ( + np.array + ) # a view on _agent_positions containing all active positions + + # the list of agents in the space + self.active_agents = [] + self._n_agents = 0 # the number of active agents in the space + + # a mapping from agents to index and vice versa + self._index_to_agent: dict[int, Agent] = {} + self._agent_to_index: dict[Agent, int | None] = {} + + @property + def agents(self) -> AgentSet: + """Return an AgentSet with the agents in the space.""" + return AgentSet(self.active_agents, random=self.random) + + def _add_agent(self, agent: Agent) -> int: + """Helper method for adding an agent to the space. + + This method manages the numpy array with the agent positions and ensuring it is + enlarged if and when needed. It is called automatically by ContinousSpaceAgent when created. + + """ + index = self._n_agents + self._n_agents += 1 + + if self._agent_positions.shape[0] <= index: + # we are out of space + fraction = 0.2 # we add 20% Fixme + n = int(round(fraction * self._n_agents)) + self._agent_positions = np.vstack( + [ + self._agent_positions, + np.empty( + (n, self.dimensions.shape[0]), + ), + ] + ) + + self._agent_to_index[agent] = index + self._index_to_agent[index] = agent + + # we want to maintain a view rather than a copy on the active agents and positions + # this is essential for the performance of the rest of this code + self.active_agents.append(agent) + self.agent_positions = self._agent_positions[0 : self._n_agents] + + return index + + def _remove_agent(self, agent: Agent) -> None: + """Remove an agent from the space. + + This method is automatically called by ContinuousSpaceAgent.remove. + + """ + index = self._agent_to_index[agent] + self._agent_to_index.pop(agent, None) + self._index_to_agent.pop(index, None) + del self.active_agents[index] + + # we update all indices + for agent in self.active_agents[index::]: + old_index = self._agent_to_index[agent] + self._agent_to_index[agent] = old_index - 1 + self._index_to_agent[old_index - 1] = agent + + # we move all data below the removed agent one row up + self._agent_positions[index : self._n_agents - 1] = self._agent_positions[ + index + 1 : self._n_agents + ] + self._n_agents -= 1 + self.agent_positions = self._agent_positions[0 : self._n_agents] + + def calculate_difference_vector(self, point: np.ndarray, agents=None) -> np.ndarray: + """Calculate the difference vector between the point and all agenents. + + Args: + point: the point to calculate the difference vector for + agents: the agents to calculate the difference vector of point with. By default, + all agents are considered. + + + """ + point = np.asanyarray(point) + positions = ( + self.agent_positions + if agents is None + else self._agent_positions[[self._agent_to_index[a] for a in agents]] + ) + + delta = positions - point + + if self.torus: + inverse_delta = delta - np.sign(delta) * self.size + + # we need to use the lowest absolute value from delta and inverse delta + logical = np.abs(delta) < np.abs(inverse_delta) + + out = np.zeros(delta.shape) + out[logical] = delta[logical] + out[~logical] = inverse_delta[~logical] + + delta = out + + return delta + + def calculate_distances( + self, point: ArrayLike, agents: Iterable[Agent] | None = None, **kwargs + ) -> tuple[np.ndarray, list]: + """Calculate the distance between the point and all agents. + + Args: + point: the point to calculate the difference vector for + agents: the agents to calculate the difference vector of point with. By default, + all agents are considered. + kwargs: any additional keyword arguments are passed to scipy's cdist, which is used + only if torus is False. This allows for non-Euclidian distance measures. + + """ + point = np.asanyarray(point) + + if agents is None: + positions = self.agent_positions + agents = self.active_agents + else: + positions = self._agent_positions[[self._agent_to_index[a] for a in agents]] + agents = np.asarray(agents) + + if self.torus: + delta = np.abs(point - positions) + delta = np.minimum(delta, self.size - delta, out=delta) + + # + is much faster than np.sum or array.sum + dists = delta[:, 0] ** 2 + for i in range(1, self.ndims): + dists += delta[:, i] ** 2 + dists = np.sqrt(dists) + else: + dists = cdist(point[np.newaxis, :], positions, **kwargs)[0, :] + return dists, agents + + def get_agents_in_radius( + self, point: ArrayLike, radius: float | int = 1 + ) -> tuple[list, np.ndarray]: + """Return the agents and their distances within a radius for the point.""" + distances, agents = self.calculate_distances(point) + logical = distances <= radius + agents = list(compress(agents, logical)) + return ( + agents, + distances[logical], + ) + + def get_k_nearest_agents( + self, point: ArrayLike, k: int = 1 + ) -> tuple[list, np.ndarray]: + """Return the k nearest agents and their distances to the point. + + Notes: + This method returns exactly k agents, ignoring ties. In case of ties, the + earlier an agent is inserted the higher it will rank. + + """ + dists, agents = self.calculate_distances(point) + + indices = np.argpartition(dists, k)[:k] + agents = [agents[i] for i in indices] + return agents, dists[indices] + + def in_bounds(self, point: ArrayLike) -> bool: + """Check if point is inside the bounds of the space.""" + return bool( + ( + (np.asanyarray(point) >= self.dimensions[:, 0]) + & (point <= self.dimensions[:, 1]) + ).all() + ) + + def torus_correct(self, point: ArrayLike) -> np.ndarray: + """Apply a torus correction to the point.""" + return self.dimensions[:, 0] + np.mod( + np.asanyarray(point) - self.dimensions[:, 0], self.size + ) diff --git a/mesa/experimental/continuous_space/continuous_space_agents.py b/mesa/experimental/continuous_space/continuous_space_agents.py new file mode 100644 index 00000000000..f4ea9c2885f --- /dev/null +++ b/mesa/experimental/continuous_space/continuous_space_agents.py @@ -0,0 +1,101 @@ +"""Continuous space agents.""" + +from __future__ import annotations + +from itertools import compress +from typing import Protocol + +import numpy as np + +from mesa.agent import Agent +from mesa.experimental.continuous_space import ContinuousSpace + + +class HasPositionProtocol(Protocol): + """Protocol for continuous space position holders.""" + + position: np.ndarray + + +class ContinuousSpaceAgent(Agent): + """A continuous space agent. + + Attributes: + space (ContinuousSpace): the continuous space in which the agent is located + position (np.ndarray): the position of the agent + + """ + + __slots__ = ["_mesa_index", "space"] + + @property + def position(self) -> np.ndarray: + """Position of the agent.""" + return self.space.agent_positions[self.space._agent_to_index[self]] + + @position.setter + def position(self, value: np.ndarray) -> None: + if not self.space.in_bounds(value): + if self.space.torus: + value = self.space.torus_correct(value) + else: + raise ValueError(f"point {value} is outside the bounds of the space") + + self.space.agent_positions[self.space._agent_to_index[self]] = value + + @property + def pos(self): # noqa: D102 + # just here for compatibility with solara_viz. + return self.position + + @pos.setter + def pos(self, value): + # just here for compatibility solara_viz. + pass + + def __init__(self, space: ContinuousSpace, model): + """Initialize a continuous space agent. + + Args: + space: the continuous space in which the agent is located + model: the model to which the agent belongs + + """ + super().__init__(model) + self.space: ContinuousSpace = space + self.space._add_agent(self) + # self.position[:] = np.nan + + def remove(self) -> None: + """Remove and delete the agent from the model and continuous space.""" + super().remove() + self.space._remove_agent(self) + self._mesa_index = None + self.space = None + + def get_neighbors_in_radius( + self, radius: float | int = 1 + ) -> tuple[list, np.ndarray]: + """Get neighbors within radius. + + Args: + radius: radius within which to look for neighbors + + """ + agents, dists = self.space.get_agents_in_radius(self.position, radius=radius) + logical = np.asarray([agent is not self for agent in agents]) + agents = list(compress(agents, logical)) + return agents, dists[logical] + + def get_nearest_neighbors(self, k: int = 1) -> tuple[list, np.ndarray]: + """Get neighbors within radius. + + Args: + k: the number of nearest neighbors to return + + """ + # return includes self, so we need to get k+1 + agents, dists = self.space.get_k_nearest_agents(self.position, k=k + 1) + logical = np.asarray([agent is not self for agent in agents]) + agents = list(compress(agents, logical)) + return agents, dists[logical] diff --git a/mesa/visualization/mpl_space_drawing.py b/mesa/visualization/mpl_space_drawing.py index 784d370eba0..6e7fa9a05d5 100644 --- a/mesa/visualization/mpl_space_drawing.py +++ b/mesa/visualization/mpl_space_drawing.py @@ -143,7 +143,10 @@ def draw_space( draw_orthogonal_grid(space, agent_portrayal, ax=ax, **space_drawing_kwargs) case mesa.space.NetworkGrid() | mesa.experimental.cell_space.Network(): draw_network(space, agent_portrayal, ax=ax, **space_drawing_kwargs) - case mesa.space.ContinuousSpace(): + case ( + mesa.space.ContinuousSpace() + | mesa.experimental.continuous_space.ContinuousSpace() + ): draw_continuous_space(space, agent_portrayal, ax=ax) case VoronoiGrid(): draw_voronoi_grid(space, agent_portrayal, ax=ax) diff --git a/pyproject.toml b/pyproject.toml index 51265df9301..db68f3986c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "numpy", "pandas", "tqdm", + "scipy" ] dynamic = ["version"] diff --git a/tests/test_continuous_space.py b/tests/test_continuous_space.py new file mode 100644 index 00000000000..41d7ca33186 --- /dev/null +++ b/tests/test_continuous_space.py @@ -0,0 +1,432 @@ +"""Tests for continuous space.""" + +import numpy as np +import pytest + +from mesa import Model +from mesa.experimental.continuous_space import ContinuousSpace, ContinuousSpaceAgent + + +def test_continuous_space(): + """Test ContinuousSpace class.""" + model = Model(seed=42) + + dimensions = np.asarray([[0, 1], [-1, 0]]) + space = ContinuousSpace(dimensions, torus=False, random=model.random) + + # check some default fields + assert space.ndims == 2 + assert np.all(space.size == [1, 1]) + assert np.all(space.center == [0.5, -0.5]) + assert len(space.agents) == 0 + + # check in_bounds + assert space.in_bounds([0.5, -0.5]) + assert not space.in_bounds([-0.5, -0.5]) + assert not space.in_bounds([1.5, -0.5]) + assert not space.in_bounds([0.5, 0.5]) + assert not space.in_bounds([0.5, -1.5]) + + # check torus correction + space = ContinuousSpace(dimensions, torus=True, random=model.random) + assert np.all(space.torus_correct([-0.5, 0.5]) == [0.5, -0.5]) + assert np.all(space.torus_correct([0.5, -0.5]) == [0.5, -0.5]) + assert np.all(space.torus_correct([0.5, -0.5]) == [0.5, -0.5]) + assert np.all(space.torus_correct([1.5, -0.5]) == [0.5, -0.5]) + assert np.all(space.torus_correct([0.5, -1.5]) == [0.5, -0.5]) + + # check 3d + dimensions = np.asarray([[0, 2], [-2, 0], [-2, 2]]) + space = ContinuousSpace(dimensions, torus=False, random=model.random) + + # check some default fields + assert space.ndims == 3 + assert np.all(space.size == [2, 2, 4]) + assert np.all(space.center == [1, -1, 0]) + + # check in_bounds + assert space.in_bounds([1, -1, 0]) + assert not space.in_bounds([-0.5, -1, 0]) + assert not space.in_bounds([2.5, -1, 0]) + assert not space.in_bounds([1, 0.5, 0]) + assert not space.in_bounds([1, -2.5, 0]) + assert not space.in_bounds([1, -1, -3]) + assert not space.in_bounds([1, -1, 3]) + assert not space.in_bounds([-0.5, -1, 3]) + assert not space.in_bounds([1, 0.5, 3]) + + +def test_continuous_agent(): + """Test ContinuousSpaceAgent class.""" + model = Model(seed=42) + + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=False, random=model.random) + + for _ in range(10): + agent = ContinuousSpaceAgent(space, model) + position = [agent.random.random(), agent.random.random()] + agent.position = position + agent.coordinate = position + + assert space.agent_positions.shape == (10, 2) + for agent in space.agents: + a = agent.position + b = space._agent_positions[space._agent_to_index[agent]] + assert np.all(a == b) + assert np.all(agent.position == agent.coordinate) + + # add more agents, triggering a resizeing of the array + for _ in range(100): + agent = ContinuousSpaceAgent(space, model) + position = [agent.random.random(), agent.random.random()] + agent.position = position + agent.coordinate = position + + assert space.agent_positions.shape == (110, 2) + for agent in space.agents: + a = agent.position + b = space._agent_positions[space._agent_to_index[agent]] + assert np.all(a == b) + assert np.all(agent.position == agent.coordinate) + + # remove all agents and check if the view is updated throughout correctly + for i, agent in enumerate(space.agents): + assert np.all( + agent.position == agent.coordinate + ) ## check if updates of indices is correctly done + agent.remove() + assert space.agent_positions.shape == (110 - 1 - i, 2) + + model = Model(seed=42) + + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=True, random=model.random) + agent = ContinuousSpaceAgent(space, model) + agent.position = [1.1, 1.1] + assert np.allclose(agent.position, [0.1, 0.1]) + + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=False, random=model.random) + agent = ContinuousSpaceAgent(space, model) + with pytest.raises(ValueError): + agent.position = [1.1, 1.1] + + +def test_continous_space_calculate_distances(): + """Test ContinuousSpace.distance method.""" + # non torus + model = Model(seed=42) + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=False, random=model.random) + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.1, 0.1] + + distances, agents = space.calculate_distances([0.1, 0.9]) + assert np.all( + distances + == [ + 0.8, + ] + ) + assert np.all( + agents + == [ + agent, + ] + ) + + distances, agents = space.calculate_distances([0.9, 0.1]) + assert np.all( + distances + == [ + 0.8, + ] + ) + assert np.all( + agents + == [ + agent, + ] + ) + + # torus + model = Model(seed=42) + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=True, random=model.random) + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.1, 0.1] + + distances, agents = space.calculate_distances([0.1, 0.9]) + assert np.all( + np.isclose( + distances, + [ + 0.2, + ], + ) + ) + assert np.all( + agents + == [ + agent, + ] + ) + + distances, agents = space.calculate_distances([0.9, 0.1]) + assert np.all( + np.isclose( + distances, + [ + 0.2, + ], + ) + ) + assert np.all( + agents + == [ + agent, + ] + ) + + distances, agents = space.calculate_distances([0.9, 0.9]) + assert np.all( + np.isclose( + distances, + [ + 0.2 * 2**0.5, + ], + ) + ) + assert np.all( + agents + == [ + agent, + ] + ) + + distances, agents = space.calculate_distances( + [0.9, 0.9], + agents=[ + agent, + ], + ) + assert np.all( + np.isclose( + distances, + [ + 0.2 * 2**0.5, + ], + ) + ) + assert np.all( + agents + == [ + agent, + ] + ) + + +def test_continous_space_difference_vector(): + """Test ContinuousSpace.get_difference_vector method.""" + # non torus + model = Model(seed=42) + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=False, random=model.random) + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.1, 0.1] + + vector = space.calculate_difference_vector([0.1, 0.9]) + assert np.all(vector == [0, -0.8]) + + vector = space.calculate_difference_vector([0.9, 0.1]) + assert np.all(vector == [-0.8, 0]) + + # torus + model = Model(seed=42) + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=True, random=model.random) + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.1, 0.1] + + vector = space.calculate_difference_vector([0.1, 0.9]) + assert np.allclose(vector, [0, 0.2]) + + vector = space.calculate_difference_vector([0.9, 0.1]) + assert np.allclose(vector, [0.2, 0]) + + vector = space.calculate_difference_vector([0.9, 0.9]) + assert np.allclose(vector, [0.2, 0.2]) + + +def test_continuous_space_get_k_nearest_agents(): # noqa: D103 + # non torus + model = Model(seed=42) + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=False, random=model.random) + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.1, 0.1] + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.1, 0.9] + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.9, 0.1] + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.9, 0.9] + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.5, 0.5] + + agents, distances = space.get_k_nearest_agents([0.1, 0.1], k=1) + assert len(agents) == 1 + assert np.allclose( + distances, + [ + 0, + ], + ) + + agents, distances = space.get_k_nearest_agents([0.5, 0.1], k=1) + assert len(agents) == 1 + assert np.allclose(distances, [0.4, 0.4]) + + agents, distances = space.get_k_nearest_agents([0.5, 0.1], k=2) + assert len(agents) == 2 + assert np.allclose(distances, [0.4, 0.4]) + + # torus + model = Model(seed=42) + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=True, random=model.random) + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.1, 0.1] + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.9, 0.1] + + agent = ContinuousSpaceAgent(space, model) + agent.position = [0.9, 0.9] + + agents, distances = space.get_k_nearest_agents([0.0, 0.1], k=2) + assert len(agents) == 2 + assert np.allclose(distances, [0.1, 0.1]) + + +def test_continuous_space_get_agents_in_radius(): # noqa: D103 + # non torus + model = Model(seed=42) + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=False, random=model.random) + + positions = [ + [0.1, 0.1], + [0.1, 0.9], + [0.9, 0.1], + [0.9, 0.9], + [0.5, 0.5], + ] + + for position in positions: + agent = ContinuousSpaceAgent(space, model) + agent.position = position + + agents, distances = space.get_agents_in_radius([0.1, 0.1], radius=0.1) + assert len(agents) == 1 + assert np.allclose( + distances, + [ + 0, + ], + ) + + agents, distances = space.get_agents_in_radius([0.5, 0.1], radius=0.4) + assert len(agents) == 3 + assert np.allclose(distances, [0.4, 0.4, 0.4]) + + agents, distances = space.get_agents_in_radius([0.5, 0.5], radius=1) + assert len(agents) == 5 + + # torus + model = Model(seed=42) + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=True, random=model.random) + + positions = [ + [0.1, 0.1], + [0.1, 0.9], + [0.9, 0.1], + [0.9, 0.9], + [0.5, 0.5], + ] + + for position in positions: + agent = ContinuousSpaceAgent(space, model) + agent.position = position + + agents, distances = space.get_agents_in_radius([0.0, 0.1], radius=0.1) + assert len(agents) == 2 + assert np.allclose(distances, [0.1, 0.1]) + + +def test_get_neighbor_methos(): # noqa: D103 + # non torus + model = Model(seed=42) + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=False, random=model.random) + + positions = [ + [0.1, 0.1], + [0.1, 0.9], + [0.9, 0.1], + [0.9, 0.9], + [0.5, 0.5], + ] + + for position in positions: + agent = ContinuousSpaceAgent(space, model) + agent.position = position + + agent: ContinuousSpaceAgent = model.agents[-1] # 0.5, 0.5 + agents, distances = agent.get_neighbors_in_radius(1) + assert len(agents) == 4 + + agents, distances = agent.get_neighbors_in_radius(0.1) + assert len(agents) == 0 + + agent: ContinuousSpaceAgent = model.agents[0] # 0.1, 0.1 + agents, distances = agent.get_nearest_neighbors(k=2) + assert len(agents) == 2 + + # torus + model = Model(seed=42) + dimensions = np.asarray([[0, 1], [0, 1]]) + space = ContinuousSpace(dimensions, torus=True, random=model.random) + + positions = [ + [0.1, 0.1], + [0.1, 0.9], + [0.9, 0.1], + [0.9, 0.9], + [0.5, 0.5], + ] + + for position in positions: + agent = ContinuousSpaceAgent(space, model) + agent.position = position + + agent: ContinuousSpaceAgent = model.agents[-1] # 0.5, 0.5 + agents, distances = agent.get_neighbors_in_radius(1) + assert len(agents) == 4 + + agent: ContinuousSpaceAgent = model.agents[0] # 0.1, 0.1 + agents, distances = agent.get_nearest_neighbors(k=2) + assert len(agents) == 2 + assert np.allclose(distances, [0.2, 0.2]) From 804ea9559833e755538c1e44369a3751fa63df32 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 10 Jan 2025 12:50:27 +0100 Subject: [PATCH 08/31] reimplementation of draw_voroinoi (#2608) * reimplementation of draw_voroinoi * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- mesa/experimental/cell_space/voronoi.py | 5 +---- mesa/visualization/mpl_space_drawing.py | 29 +++++++++++++++---------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/mesa/experimental/cell_space/voronoi.py b/mesa/experimental/cell_space/voronoi.py index 92080d353cd..5c48f53af8a 100644 --- a/mesa/experimental/cell_space/voronoi.py +++ b/mesa/experimental/cell_space/voronoi.py @@ -186,7 +186,6 @@ def __init__( random: Random | None = None, cell_klass: type[Cell] = Cell, capacity_function: callable = round_float, - cell_coloring_property: str | None = None, ) -> None: """A Voronoi Tessellation Grid. @@ -200,7 +199,7 @@ def __init__( random (Random): random number generator cell_klass (type[Cell]): type of cell class capacity_function (Callable): function to compute (int) capacity according to (float) area - cell_coloring_property (str): voronoi visualization polygon fill property + """ super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) self.centroids_coordinates = centroids_coordinates @@ -215,7 +214,6 @@ def __init__( self.triangulation = None self.voronoi_coordinates = None self.capacity_function = capacity_function - self.cell_coloring_property = cell_coloring_property self._connect_cells() self._build_cell_polygons() @@ -266,4 +264,3 @@ def _build_cell_polygons(self): polygon_area = self._compute_polygon_area(polygon) self._cells[region].properties["area"] = polygon_area self._cells[region].capacity = self.capacity_function(polygon_area) - self._cells[region].properties[self.cell_coloring_property] = 0 diff --git a/mesa/visualization/mpl_space_drawing.py b/mesa/visualization/mpl_space_drawing.py index 6e7fa9a05d5..b0e10d039ae 100644 --- a/mesa/visualization/mpl_space_drawing.py +++ b/mesa/visualization/mpl_space_drawing.py @@ -20,7 +20,7 @@ from matplotlib.cm import ScalarMappable from matplotlib.collections import PatchCollection from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba -from matplotlib.patches import RegularPolygon +from matplotlib.patches import Polygon, RegularPolygon import mesa from mesa.experimental.cell_space import ( @@ -501,7 +501,11 @@ def draw_continuous_space( def draw_voronoi_grid( - space: VoronoiGrid, agent_portrayal: Callable, ax: Axes | None = None, **kwargs + space: VoronoiGrid, + agent_portrayal: Callable, + ax: Axes | None = None, + draw_grid: bool = True, + **kwargs, ): """Visualize a voronoi grid. @@ -509,6 +513,7 @@ def draw_voronoi_grid( space: the space to visualize agent_portrayal: a callable that is called with the agent and returns a dict ax: a Matplotlib Axes instance. If none is provided a new figure and ax will be created using plt.subplots + draw_grid: whether to draw the grid or not kwargs: additional keyword arguments passed to ax.scatter Returns: @@ -541,16 +546,18 @@ def draw_voronoi_grid( _scatter(ax, arguments, **kwargs) - for cell in space.all_cells: - polygon = cell.properties["polygon"] - ax.fill( - *zip(*polygon), - alpha=min(1, cell.properties[space.cell_coloring_property]), - c="red", - zorder=0, - ) # Plot filled polygon - ax.plot(*zip(*polygon), color="black") # Plot polygon edges in black + def setup_voroinoimesh(cells): + patches = [] + for cell in cells: + patch = Polygon(cell.properties["polygon"]) + patches.append(patch) + mesh = PatchCollection( + patches, edgecolor="k", facecolor=(1, 1, 1, 0), linestyle="dotted", lw=1 + ) + return mesh + if draw_grid: + ax.add_collection(setup_voroinoimesh(space.all_cells.cells)) return ax From 16261f54652c6c8e80766fb9498b02215a0fa8b7 Mon Sep 17 00:00:00 2001 From: Himanshu Rai <166975398+HMNS19@users.noreply.github.com> Date: Fri, 10 Jan 2025 18:30:14 +0530 Subject: [PATCH 09/31] Add render interval slider to control visualization update frequency (#2596) This PR adds a render interval slider to the SolaraViz visualization system, allowing users to control how frequently plots and visualizations are updated during model simulation. This feature significantly improves performance when working with complex visualizations or multiple plots. --- mesa/visualization/solara_viz.py | 37 ++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index d01c66e5f94..f5fde84b1a3 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -52,6 +52,7 @@ def SolaraViz( | Literal["default"] = "default", *, play_interval: int = 100, + render_interval: int = 1, simulator: Simulator | None = None, model_params=None, name: str | None = None, @@ -72,6 +73,8 @@ def SolaraViz( Defaults to "default", which uses the default Altair space visualization. play_interval (int, optional): Interval for playing the model steps in milliseconds. This controls the speed of the model's automatic stepping. Defaults to 100 ms. + render_interval (int, optional): Controls how often plots are updated during a simulation, + allowing users to skip intermediate steps and update graphs less frequently. simulator: A simulator that controls the model (optional) model_params (dict, optional): Parameters for (re-)instantiating a model. Can include user-adjustable parameters and fixed parameters. Defaults to None. @@ -90,6 +93,8 @@ def SolaraViz( model instance is provided, it will be converted to a reactive model using `solara.use_reactive`. - The `play_interval` argument controls the speed of the model's automatic stepping. A lower value results in faster stepping, while a higher value results in slower stepping. + - The `render_interval` argument determines how often plots are updated during simulation. Higher values + reduce update frequency,resulting in faster execution. """ if components == "default": components = [components_altair.make_altair_space()] @@ -103,7 +108,7 @@ def SolaraViz( # set up reactive model_parameters shared by ModelCreator and ModelController reactive_model_parameters = solara.use_reactive({}) reactive_play_interval = solara.use_reactive(play_interval) - + reactive_render_interval = solara.use_reactive(render_interval) with solara.AppBar(): solara.AppBarTitle(name if name else model.value.__class__.__name__) @@ -117,11 +122,20 @@ def SolaraViz( max=500, step=10, ) + solara.SliderInt( + label="Render Interval (steps)", + value=reactive_render_interval, + on_value=lambda v: reactive_render_interval.set(v), + min=1, + max=100, + step=2, + ) if not isinstance(simulator, Simulator): ModelController( model, model_parameters=reactive_model_parameters, play_interval=reactive_play_interval, + render_interval=reactive_render_interval, ) else: SimulatorController( @@ -129,6 +143,7 @@ def SolaraViz( simulator, model_parameters=reactive_model_parameters, play_interval=reactive_play_interval, + render_interval=reactive_render_interval, ) with solara.Card("Model Parameters"): ModelCreator( @@ -189,6 +204,7 @@ def ModelController( *, model_parameters: dict | solara.Reactive[dict] = None, play_interval: int | solara.Reactive[int] = 100, + render_interval: int | solara.Reactive[int] = 1, ): """Create controls for model execution (step, play, pause, reset). @@ -196,7 +212,7 @@ def ModelController( model: Reactive model instance model_parameters: Reactive parameters for (re-)instantiating a model. play_interval: Interval for playing the model steps in milliseconds. - + render_interval: Controls how often the plots are updated during simulation steps.Higher value reduce update frequency. """ playing = solara.use_reactive(False) running = solara.use_reactive(True) @@ -215,9 +231,12 @@ async def step(): @function_logger(__name__) def do_step(): - """Advance the model by one step.""" - model.value.step() + """Advance the model by the number of steps specified by the render_interval slider.""" + for _ in range(render_interval.value): + model.value.step() + running.value = model.value.running + force_update() @function_logger(__name__) @@ -259,6 +278,7 @@ def SimulatorController( *, model_parameters: dict | solara.Reactive[dict] = None, play_interval: int | solara.Reactive[int] = 100, + render_interval: int | solara.Reactive[int] = 1, ): """Create controls for model execution (step, play, pause, reset). @@ -267,7 +287,11 @@ def SimulatorController( simulator: Simulator instance model_parameters: Reactive parameters for (re-)instantiating a model. play_interval: Interval for playing the model steps in milliseconds. + render_interval: Controls how often the plots are updated during simulation steps.Higher values reduce update frequency. + Notes: + The `step button` increments the step by the value specified in the `render_interval` slider. + This behavior ensures synchronization between simulation steps and plot updates. """ playing = solara.use_reactive(False) running = solara.use_reactive(True) @@ -285,8 +309,8 @@ async def step(): ) def do_step(): - """Advance the model by one step.""" - simulator.run_for(1) + """Advance the model by the number of steps specified by the render_interval slider.""" + simulator.run_for(render_interval.value) running.value = model.value.running force_update() @@ -390,7 +414,6 @@ def ModelCreator( or are dictionaries containing parameter details such as type, value, min, and max. - The `seed` argument ensures reproducibility by setting the initial seed for the model's random number generator. - The component provides an interface for adjusting user-defined parameters and reseeding the model. - """ if model_parameters is None: model_parameters = {} From 80a4f2beeada9251f75858840b290211bd379936 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 11 Jan 2025 16:41:52 +0100 Subject: [PATCH 10/31] Add docs for continuous space (#2613) --- docs/apis/experimental.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/apis/experimental.md b/docs/apis/experimental.md index 2eb4c302aaf..701d6a3e00f 100644 --- a/docs/apis/experimental.md +++ b/docs/apis/experimental.md @@ -54,3 +54,15 @@ This namespace contains experimental features. These are under development, and .. automodule:: experimental.devs.simulator :members: ``` + +## Continuous Space + +```{eval-rst} +.. automodule:: experimental.continuous_space.continuous_space + :members: +``` + +```{eval-rst} +.. automodule:: experimental.continuous_space.continuous_space_agents + :members: +``` From 3f460cfcd584f86af4230d9fc10ee16d84e66963 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Sat, 11 Jan 2025 16:57:01 +0100 Subject: [PATCH 11/31] Update release notes and version for 3.1.3 (#2612) Update version and release notes for the next feature release, Mesa 3.1.3. --- HISTORY.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ mesa/__init__.py | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 11894509106..ae1de07e948 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,60 @@ --- title: Release History --- +# 3.1.3 (2025-01-11) +## Highlights +Mesa 3.1.3 introduces a major experimental reimplementation of Mesa's continuous space, providing an intuitive agent-centric API and significant performance improvements. The new implementation supports n-dimensional spaces and offers streamlined methods for agent movement and neighbor calculations. + +### New Continuous Space Features +- Agent-centric movement API similar to cell spaces +- Efficient neighbor calculations and position updates +- Support for n-dimensional spaces +- Improved memory management with dynamic array resizing + +Here's a quick look at the new API: + +```python +# Create a 2D continuous space +space = ContinuousSpace( + dimensions=[[0, 1], [0, 1]], + torus=True, + random=model.random +) + +# Create and position an agent +agent = ContinuousSpaceAgent(space, model) +agent.position = [0.5, 0.5] + +# Move agent using vector arithmetic +agent.position += [0.1, 0.1] + +# Get neighbors within radius +neighbors, distances = agent.get_neighbors_in_radius(radius=0.2) + +# Find k nearest neighbors +nearest, distances = agent.get_nearest_neighbors(k=5) +``` + +The new implementation particularly benefits models requiring frequent position updates and neighbor queries, such as flocking simulations or particle systems. See [#2584](https://github.com/projectmesa/mesa/pull/2584) for more details. We would love to get feedback on the new Continuous Space in [#2611](https://github.com/projectmesa/mesa/discussions/2611). + +Other improvements in this release include consistent visualization behavior across space types with the reimplementation of `draw_voronoi` [#2608](https://github.com/projectmesa/mesa/pull/2608), and a new render interval slider for controlling visualization update frequency in SolaraViz, which helps improve performance when working with complex visualizations [#2596](https://github.com/projectmesa/mesa/pull/2596). We've also fixed a bug affecting random number generation determinism when using `Model(seed=something)`, ensuring both `model.random` and `model.rng` now behave consistently when seeded with the same initial value [#2598](https://github.com/projectmesa/mesa/pull/2598). + +## What's Changed +### 🧪 Experimental features +* Reimplementation of Continuous Space by @quaquel in https://github.com/projectmesa/mesa/pull/2584 +### 🛠 Enhancements made +* reimplementation of draw_voroinoi by @quaquel in https://github.com/projectmesa/mesa/pull/2608 +* Add render interval slider to control visualization update frequency by @HMNS19 in https://github.com/projectmesa/mesa/pull/2596 +### 🐛 Bugs fixed +* Bugfix for non deterministic rng behavior by @quaquel in https://github.com/projectmesa/mesa/pull/2598 +### 🔍 Examples updated +* Clarify ContinuousSpace.get_neighbors behavior with multiple agents at same position by @quaquel in https://github.com/projectmesa/mesa/pull/2599 + +## New Contributors +* @HMNS19 made their first contribution in https://github.com/projectmesa/mesa/pull/2596 + +**Full Changelog**: https://github.com/projectmesa/mesa/compare/v3.1.2...v3.1.3 + # 3.1.2 (2025-01-04) ## Highlights Mesa v3.1.2 is a patch release containing updates to our wolf-sheep, shelling and prisoner's dilemma example models and improving documentation in the tutorials and visualisation docstring. No functional changes to the core library were made. diff --git a/mesa/__init__.py b/mesa/__init__.py index 368f46f0440..ff8f002f2b1 100644 --- a/mesa/__init__.py +++ b/mesa/__init__.py @@ -22,7 +22,7 @@ ] __title__ = "mesa" -__version__ = "3.1.2" +__version__ = "3.1.3" __license__ = "Apache 2.0" _this_year = datetime.datetime.now(tz=datetime.UTC).date().year __copyright__ = f"Copyright {_this_year} Project Mesa Team" From 291da0eb5ddcc7a1e07eace53c9c5697e366ba18 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 14 Jan 2025 10:47:20 +0100 Subject: [PATCH 12/31] remove remnants of mesa cli (#2617) --- docs/getting_started.md | 1 - pyproject.toml | 3 --- 2 files changed, 4 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index 83b83eaee40..3345521df4d 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -68,7 +68,6 @@ model.step() You should see agents 1-5, activated in random order. See the [tutorial](tutorials/intro_tutorial) or API documentation for more detail on how to add model functionality. -To bootstrap a new model install mesa and run `mesa startproject` ### AgentSet and model.agents Mesa 3.0 makes `model.agents` and the AgentSet class central in managing and activating agents. diff --git a/pyproject.toml b/pyproject.toml index db68f3986c6..5950d0714b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,9 +82,6 @@ docs = [ homepage = "https://github.com/projectmesa/mesa" repository = "https://github.com/projectmesa/mesa" -[project.scripts] -mesa = "mesa.main:cli" - [tool.hatch.build.targets.wheel] packages = ["mesa"] From 7eacc724546f2b1a13bec13c84173ed63dc42adf Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 14 Jan 2025 10:47:42 +0100 Subject: [PATCH 13/31] remove any reference to using --pre (#2618) --- README.md | 11 +++-------- docs/index.md | 8 +------- docs/tutorials/intro_tutorial.ipynb | 10 +--------- 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index cc03d1bd144..94634eb0763 100644 --- a/README.md +++ b/README.md @@ -34,21 +34,16 @@ To install our latest stable release (3.0.x), run: pip install -U mesa ``` -To install our latest pre-release, run: - -``` bash -pip install -U --pre mesa -``` Starting with Mesa 3.0, we don't install all our dependencies anymore by default. ```bash # You can customize the additional dependencies you need, if you want. Available are: -pip install -U --pre mesa[network,viz] +pip install -U mesa[network,viz] # This is equivalent to our recommended dependencies: -pip install -U --pre mesa[rec] +pip install -U mesa[rec] # To install all, including developer, dependencies: -pip install -U --pre mesa[all] +pip install -U mesa[all] ``` You can also use `pip` to install the latest GitHub version: diff --git a/docs/index.md b/docs/index.md index eecaef2839d..b4ee4bb3316 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,7 +33,7 @@ Mesa allows users to quickly create agent-based models using built-in core compo ## Using Mesa ### Installation Options -To install our latest stable release (3.0.x), run: +To install our latest stable release (3.1.x), run: ```bash pip install -U mesa @@ -47,12 +47,6 @@ On a Mac, this command might cause an error stating `zsh: no matches found: mesa In that case, change the command to `pip install -U "mesa[rec]"`. -To install our latest pre-release: - -```bash -pip install -U --pre mesa[rec] -``` - ### Resources For help getting started with Mesa, check out these resources: diff --git a/docs/tutorials/intro_tutorial.ipynb b/docs/tutorials/intro_tutorial.ipynb index 95d363ea403..a99bd24acfd 100644 --- a/docs/tutorials/intro_tutorial.ipynb +++ b/docs/tutorials/intro_tutorial.ipynb @@ -85,12 +85,6 @@ "pip install --upgrade mesa[rec] \n", "```\n", "\n", - "If you want to use our newest features, you can also opt to install our latest pre-release version:\n", - "\n", - "```bash\n", - "pip install --upgrade --pre mesa[rec]\n", - "```\n", - "\n", "Install Jupyter notebook (optional):\n", "\n", "```bash\n", @@ -117,9 +111,7 @@ { "cell_type": "raw", "metadata": {}, - "source": [ - "pip install --quiet --upgrade --pre mesa[rec] #installs Mesa 3.0" - ] + "source": "pip install --quiet --upgrade mesa[rec] #installs Mesa 3.1.3" }, { "cell_type": "markdown", From 6e44ad4808ad34ae4da18fed7b9d3f82ae5fb08a Mon Sep 17 00:00:00 2001 From: sanika Date: Thu, 16 Jan 2025 13:25:34 +0530 Subject: [PATCH 14/31] added color-bar --- mesa/examples/advanced/sugarscape_g1mt/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mesa/examples/advanced/sugarscape_g1mt/app.py b/mesa/examples/advanced/sugarscape_g1mt/app.py index f00619b9641..678485ae33d 100644 --- a/mesa/examples/advanced/sugarscape_g1mt/app.py +++ b/mesa/examples/advanced/sugarscape_g1mt/app.py @@ -29,13 +29,14 @@ def portray(g): cmap="spring", origin="lower", ) - fig.colorbar(im, orientation="vertical") + fig.colorbar(im, ax=ax, orientation="vertical", pad=0.1, fraction=0.046) # Spice - ax.imshow( + im_spice = ax.imshow( np.ma.masked_where(model.grid.spice.data <= 1, model.grid.spice.data), cmap="winter", origin="lower", ) + fig.colorbar(im_spice, ax=ax, orientation="vertical", fraction=0.046, pad=0.04) # Trader ax.scatter(**out["trader"]) ax.set_axis_off() From 4aa9513d9573f041ee82b54a704e072a548ab3cc Mon Sep 17 00:00:00 2001 From: Anish Dabhane Date: Mon, 20 Jan 2025 00:22:30 +0530 Subject: [PATCH 15/31] Documentation (#2630) * chore: formatting * fix: changed zsh: command not found: self.rng.randint to zsh: command not found: self.rng.integers --- docs/getting_started.md | 4 ++-- docs/tutorials/intro_tutorial.ipynb | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index 3345521df4d..125c902bcdb 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -188,8 +188,8 @@ class MyModel(Model): def __init__(self, n_agents=10, seed=None): super().__init__(seed=seed) # Initialize the model with N agents - -The core functionality for building your own visualizations resides in the [`mesa.visualization`](apis/visualization) namespace +``` +The core functionality for building your own visualizations resides in the [`mesa.visualization`](apis/visualization) namespace. Here's a basic example of how to set up a visualization: diff --git a/docs/tutorials/intro_tutorial.ipynb b/docs/tutorials/intro_tutorial.ipynb index a99bd24acfd..02e0993387f 100644 --- a/docs/tutorials/intro_tutorial.ipynb +++ b/docs/tutorials/intro_tutorial.ipynb @@ -111,7 +111,9 @@ { "cell_type": "raw", "metadata": {}, - "source": "pip install --quiet --upgrade mesa[rec] #installs Mesa 3.1.3" + "source": [ + "pip install --quiet --upgrade mesa[rec] #installs Mesa 3.1.3" + ] }, { "cell_type": "markdown", @@ -607,8 +609,8 @@ " # Create agents\n", " agents = MoneyAgent.create_agents(model=self, n=n)\n", " # Create x and y positions for agents\n", - " x = self.rng.randint(0, self.grid.width, size=(n,))\n", - " y = self.rng.randint(0, self.grid.height, size=(n,))\n", + " x = self.rng.integers(0, self.grid.width, size=(n,))\n", + " y = self.rng.integers(0, self.grid.height, size=(n,))\n", " for a, i, j in zip(agents, x, y):\n", " # Add the agent to a random grid cell\n", " self.grid.place_agent(a, (i, j))" From 910e99b43a1c0634626e1ed8776d3006f53eba27 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Mon, 20 Jan 2025 14:38:55 +0100 Subject: [PATCH 16/31] benchmarks.yml: Install SciPy and use uv for pip install (#2633) - Install SciPy, since that's now a dependency - Use uv for pip install, which is way faster --- .github/workflows/benchmarks.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 82608c28f9e..c8a1434fc8a 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -28,8 +28,10 @@ jobs: python-version: '3.13' - name: Add project directory to PYTHONPATH run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)" >> $GITHUB_ENV + - name: Install uv + run: pip install uv - name: Install dependencies - run: pip install numpy pandas tqdm tabulate matplotlib solara networkx + run: uv pip install --system numpy pandas tqdm tabulate matplotlib solara networkx scipy # Benchmarks on the projectmesa main branch - name: Checkout main branch uses: actions/checkout@v4 From c24f13cad34fe9a1e588f8025af208f099c8456a Mon Sep 17 00:00:00 2001 From: PrashantChoudhary13579 <143925781+PrashantChoudhary13579@users.noreply.github.com> Date: Sat, 18 Jan 2025 15:34:20 +0530 Subject: [PATCH 17/31] Adding a copy option at the top of the code written in the docs --- docs/README.md | 3 ++- docs/conf.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index edd25eadaf4..65f3cb66308 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ Updating docs can be confusing. Here are the basic setups. 1. Build the docs, from the inside of the docs folder. * **Requires** sphinx: `pip install sphinx` * **Requires** nbsphinx: `pip install nbsphinx` (this will render the images from jupyter in the docs) + * **Requires** sphinx-copybutton: `pip install sphinx-copybutton` (this will enable a copy option at the top of the code written in the docs) * `make html` 1. Commit the changes. If there are new files, you will have to explicit add them. * `git commit -am "Updating docs."` @@ -32,5 +33,5 @@ From this point, you will need to find someone that has access to readthedocs. C ### Helpful Sphnix tips * Build html from docs: * `make html` -* Autogenerate / update sphninx from docstrings (replace your name as the author: +* Autogenerate / update sphninx from docstrings (replace your name as the author): * `sphinx-apidoc -A "Jackie Kazil" -F -o docs mesa/` \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 24dfa07b786..84d0f087eab 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,6 +47,7 @@ "sphinx.ext.viewcode", "sphinx.ext.napoleon", # for google style docstrings "myst_nb", # For Markdown and Jupyter notebooks + "sphinx_copybutton" # For copying the code in the documentation ] # Add any paths that contain templates here, relative to this directory. From fbb3e58059a1008e9f793311acc87a3166ffaacf Mon Sep 17 00:00:00 2001 From: PrashantChoudhary13579 <143925781+PrashantChoudhary13579@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:46:41 +0530 Subject: [PATCH 18/31] Add sphinx-copybutton to documentation dependencies in pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 5950d0714b3..be0dc4a3139 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ docs = [ "seaborn", "myst-nb", "myst-parser", # Markdown in Sphinx + "sphinx-copybutton", # Add a copy option in docs ] [project.urls] From 8477af0852a98403df9843aea4a1e55bcfaea20c Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 20 Jan 2025 15:31:41 +0100 Subject: [PATCH 19/31] Change Hexgrid._connect_cells_2d to use x,y coordinates (#2632) --- mesa/experimental/cell_space/grid.py | 16 ++++++------- tests/test_cell_space.py | 34 ++++++++++++++-------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 5e2b993e847..6078a038401 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -273,20 +273,20 @@ class HexGrid(Grid[T]): def _connect_cells_2d(self) -> None: # fmt: off even_offsets = [ - (-1, -1), (-1, 0), - ( 0, -1), ( 0, 1), - ( 1, -1), ( 1, 0), + (-1, -1), (0, -1), + ( -1, 0), ( 1, 0), + ( -1, 1), (0, 1), ] odd_offsets = [ - (-1, 0), (-1, 1), - ( 0, -1), ( 0, 1), - ( 1, 0), ( 1, 1), + (0, -1), (1, -1), + ( -1, 0), ( 1, 0), + ( 0, 1), ( 1, 1), ] # fmt: on for cell in self.all_cells: - i = cell.coordinate[0] - offsets = even_offsets if i % 2 == 0 else odd_offsets + i = cell.coordinate[1] + offsets = even_offsets if i % 2 else odd_offsets self._connect_single_cell_2d(cell, offsets=offsets) def _connect_cells_nd(self) -> None: diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 53fb737d5a7..7f4dd820b65 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -354,7 +354,7 @@ def test_cell_neighborhood(): grid = HexGrid( (width, height), torus=False, capacity=None, random=random.Random(42) ) - for radius, n in zip(range(1, 4), [2, 6, 11]): + for radius, n in zip(range(1, 4), [3, 7, 13]): if radius == 1: neighborhood = grid._cells[(0, 0)].neighborhood else: @@ -366,7 +366,7 @@ def test_cell_neighborhood(): grid = HexGrid( (width, height), torus=False, capacity=None, random=random.Random(42) ) - for radius, n in zip(range(1, 4), [5, 10, 17]): + for radius, n in zip(range(1, 4), [4, 10, 17]): if radius == 1: neighborhood = grid._cells[(1, 0)].neighborhood else: @@ -385,25 +385,25 @@ def test_hexgrid(): assert len(grid._cells) == width * height # first row - assert len(grid._cells[(0, 0)].connections.values()) == 2 + assert len(grid._cells[(0, 0)].connections.values()) == 3 for connection in grid._cells[(0, 0)].connections.values(): - assert connection.coordinate in {(0, 1), (1, 0)} + assert connection.coordinate in {(0, 1), (1, 0), (1, 1)} # second row - assert len(grid._cells[(1, 0)].connections.values()) == 5 + assert len(grid._cells[(1, 0)].connections.values()) == 4 for connection in grid._cells[(1, 0)].connections.values(): # fmt: off - assert connection.coordinate in {(0, 0), (0, 1), - (1, 1), - (2, 0), (2, 1)} + assert connection.coordinate in { (1, 1), (2, 1), + (0, 0), (2, 0),} + # fmt: on # middle odd row assert len(grid._cells[(5, 5)].connections.values()) == 6 for connection in grid._cells[(5, 5)].connections.values(): # fmt: off - assert connection.coordinate in {(4, 5), (4, 6), - (5, 4), (5, 6), - (6, 5), (6, 6)} + assert connection.coordinate in { (4, 4), (5, 4), + (4, 5), (6, 5), + (4, 6), (5, 6)} # fmt: on @@ -411,9 +411,9 @@ def test_hexgrid(): assert len(grid._cells[(4, 4)].connections.values()) == 6 for connection in grid._cells[(4, 4)].connections.values(): # fmt: off - assert connection.coordinate in {(3, 3), (3, 4), - (4, 3), (4, 5), - (5, 3), (5, 4)} + assert connection.coordinate in {(4, 3), (5, 3), + (3, 4), (5, 4), + (4, 5), (5, 5)} # fmt: on @@ -424,9 +424,9 @@ def test_hexgrid(): assert len(grid._cells[(0, 0)].connections.values()) == 6 for connection in grid._cells[(0, 0)].connections.values(): # fmt: off - assert connection.coordinate in {(9, 9), (9, 0), - (0, 9), (0, 1), - (1, 9), (1, 0)} + assert connection.coordinate in {(0, 9), (1, 9), + (9, 0), (1, 0), + (0, 1), (1, 1)} # fmt: on From 802aa6391f414e65ed24b604de1548a6517a578b Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 23 Jan 2025 11:40:31 +0100 Subject: [PATCH 20/31] bugfix for draw_property_layer (#2639) --- mesa/visualization/mpl_space_drawing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mesa/visualization/mpl_space_drawing.py b/mesa/visualization/mpl_space_drawing.py index b0e10d039ae..11100e6d104 100644 --- a/mesa/visualization/mpl_space_drawing.py +++ b/mesa/visualization/mpl_space_drawing.py @@ -191,11 +191,10 @@ def draw_property_layers( continue data = layer.data.astype(float) if layer.data.dtype == bool else layer.data - width, height = data.shape # if space is None else (space.width, space.height) - if space and data.shape != (width, height): + if (space.width, space.height) != data.shape: warnings.warn( - f"Layer {layer_name} dimensions ({data.shape}) do not match space dimensions ({width}, {height}).", + f"Layer {layer_name} dimensions ({data.shape}) do not match space dimensions ({space.width}, {space.height}).", UserWarning, stacklevel=2, ) @@ -208,6 +207,7 @@ def draw_property_layers( # Draw the layer if "color" in portrayal: + data = data.T rgba_color = to_rgba(portrayal["color"]) normalized_data = (data - vmin) / (vmax - vmin) rgba_data = np.full((*data.shape, 4), rgba_color) @@ -231,7 +231,7 @@ def draw_property_layers( if isinstance(cmap, list): cmap = LinearSegmentedColormap.from_list(layer_name, cmap) im = ax.imshow( - data, + data.T, cmap=cmap, alpha=alpha, vmin=vmin, From 76b0bc4524b4ab490362e10eac6ebe8802e75c4a Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Fri, 27 Dec 2024 20:00:59 -0500 Subject: [PATCH 21/31] meta_agents - Add create meta-agents to experimental - Add tests of meta-agents - Add example with an alliance formation model in basic examples --- .../basic/alliance_formation_model/Readme.md | 11 +- .../alliance_formation_model/__init__.py | 10 ++ .../basic/alliance_formation_model/app.py | 1 - mesa/experimental/meta_agents/meta_agents.py | 159 ++++++++++++++++++ tests/test_meta_agents.py | 39 ++--- 5 files changed, 191 insertions(+), 29 deletions(-) create mode 100644 mesa/experimental/meta_agents/meta_agents.py diff --git a/mesa/examples/basic/alliance_formation_model/Readme.md b/mesa/examples/basic/alliance_formation_model/Readme.md index cf60ab4a2f7..00a9a19adfc 100644 --- a/mesa/examples/basic/alliance_formation_model/Readme.md +++ b/mesa/examples/basic/alliance_formation_model/Readme.md @@ -1,7 +1,8 @@ # 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. + +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. @@ -12,6 +13,7 @@ In its current configuration, agents being part of multiple meta-agents is not s ## Installation This model requires Mesa's recommended install and scipy + ``` $ pip install mesa[rec] ``` @@ -26,9 +28,9 @@ To run the model interactively, in this directory, run the following command ## 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. +- `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 @@ -37,4 +39,3 @@ 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) - diff --git a/mesa/examples/basic/alliance_formation_model/__init__.py b/mesa/examples/basic/alliance_formation_model/__init__.py index e69de29bb2d..49a80b627ee 100644 --- a/mesa/examples/basic/alliance_formation_model/__init__.py +++ b/mesa/examples/basic/alliance_formation_model/__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/app.py b/mesa/examples/basic/alliance_formation_model/app.py index 9df3116dc54..8e5a9050317 100644 --- a/mesa/examples/basic/alliance_formation_model/app.py +++ b/mesa/examples/basic/alliance_formation_model/app.py @@ -62,7 +62,6 @@ def plot_network(model): # 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], diff --git a/mesa/experimental/meta_agents/meta_agents.py b/mesa/experimental/meta_agents/meta_agents.py new file mode 100644 index 00000000000..75048c46533 --- /dev/null +++ b/mesa/experimental/meta_agents/meta_agents.py @@ -0,0 +1,159 @@ +"""This method is for dynamically 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 meta-agent +2. Create new meta-agent instance of existing meta-agent class +3. Create new meta-agent class + +See alliance formation model in basic examples for usage. + +""" + +from types import MethodType + + +def create_meta_agent( + model, + new_agent_class: str, + agents, + meta_attributes=dict(), # noqa B006 + meta_functions=dict(), # noqa B006 + retain_subagent_functions=True, + retain_subagent_attributes=False, +): + """Dynamically create a new meta-agent class and instantiate agents in that class. + + Parameters: + model (Model): The model instance. + new_agent_class (str): The name of the new meta-agent class. + agents (Iterable[Agent]): The agents to be included in the meta-agent. + meta_attributes (dict): Attributes to be added to the meta-agent. + meta_functions (dict): Functions to be added to the meta-agent. + retain_subagent_functions (bool): Whether to retain functions from sub-agents. + retain_subagent_attributes (bool): Whether to retain attributes from sub-agents. + + Returns: + - None if adding agent(s) 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 + """ + 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. + + Parameters: + meta_agent_instance (MetaAgent): The meta-agent instance. + agents (Iterable[Agent]): The agents to derive functions from. + meta_functions (dict): Functions to be added to the meta-agent. + """ + if retain_subagent_functions: + agent_classes = {type(agent) for agent in agents} + for agent_class in agent_classes: + for name in dir(agent_class): + 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) + + def add_attributes(meta_agent_instance, agents, meta_attributes): + """Add attributes to the meta-agent instance. + + Parameters: + meta_agent_instance (MetaAgent): The meta-agent instance. + agents (Iterable[Agent]): The agents to derive attributes from. + meta_attributes (dict): Attributes to be added to the meta-agent. + """ + if retain_subagent_attributes: + for agent in agents: + for name, value in agent.__dict__.items(): + if not callable(value): + meta_attributes[name] = value + + if meta_attributes: + 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")] + if len(subagents) > 0: + 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) + 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) + # 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 + agent_class = next( + ( + agent_type + for agent_type in model.agent_types + if agent_type.__name__ == new_agent_class + ), + None, + ) + + if agent_class: + 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,), + { + "unique_id": None, + "agents": None, + }, + ) + + meta_agent_instance = meta_agent_class(model=model, agents=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_meta_agents.py b/tests/test_meta_agents.py index 6f8dff8208e..c14d3f05472 100644 --- a/tests/test_meta_agents.py +++ b/tests/test_meta_agents.py @@ -35,7 +35,8 @@ def setup_agents(): agent1 = Agent(model) agent2 = Agent(model) agent3 = Agent(model) - agent4 = CustomAgent(model) + agent4 = Agent(model) + agent4.custom_attribute = "custom_value" agents = [agent1, agent2, agent3, agent4] return model, agents @@ -47,23 +48,20 @@ def test_create_meta_agent_new_class(setup_agents): setup_agents (tuple): The model and agents fixture. """ model, agents = setup_agents - meta_agent = multi_level_agents( + meta_agent = create_meta_agent( model, "MetaAgentClass", agents, meta_attributes={"attribute1": "value1"}, - meta_methods={"function1": lambda self: "function1"}, + meta_functions={"function1": lambda self: "function1"}, retain_subagent_attributes=True, - retain_subagent_methods=True, ) assert meta_agent is not None assert meta_agent.attribute1 == "value1" assert meta_agent.function1() == "function1" - assert meta_agent._subset == set(agents) + assert meta_agent.agents == set(agents) assert hasattr(meta_agent, "custom_attribute") assert meta_agent.custom_attribute == "custom_value" - assert hasattr(meta_agent, "custom_method") - assert meta_agent.custom_method() == "custom_method_value" def test_create_meta_agent_existing_class(setup_agents): @@ -74,35 +72,32 @@ def test_create_meta_agent_existing_class(setup_agents): """ model, agents = setup_agents - # Create Meta Agent Class - meta_agent = multi_level_agents( + # Create Met Agent Class + meta_agent = create_meta_agent( model, "MetaAgentClass", [agents[0], agents[2]], meta_attributes={"attribute1": "value1"}, - meta_methods={"function1": lambda self: "function1"}, + meta_functions={"function1": lambda self: "function1"}, ) # Create new meta-agent instance with existing class - meta_agent2 = multi_level_agents( + meta_agent2 = create_meta_agent( model, "MetaAgentClass", [agents[1], agents[3]], meta_attributes={"attribute2": "value2"}, - meta_methods={"function2": lambda self: "function2"}, + meta_functions={"function2": lambda self: "function2"}, retain_subagent_attributes=True, - retain_subagent_methods=True, ) assert meta_agent is not None assert meta_agent2.attribute2 == "value2" assert meta_agent.function1() == "function1" - assert meta_agent._subset == {agents[2], agents[0]} + assert meta_agent.agents == {agents[2], agents[0]} assert meta_agent2.function2() == "function2" - assert meta_agent2._subset == {agents[1], agents[3]} + assert meta_agent2.agents == {agents[1], agents[3]} assert hasattr(meta_agent2, "custom_attribute") assert meta_agent2.custom_attribute == "custom_value" - assert hasattr(meta_agent2, "custom_method") - assert meta_agent2.custom_method() == "custom_method_value" def test_add_agents_to_existing_meta_agent(setup_agents): @@ -113,24 +108,22 @@ def test_add_agents_to_existing_meta_agent(setup_agents): """ model, agents = setup_agents - meta_agent1 = multi_level_agents( + meta_agent1 = create_meta_agent( model, "MetaAgentClass", [agents[0], agents[3]], meta_attributes={"attribute1": "value1"}, - meta_methods={"function1": lambda self: "function1"}, + meta_functions={"function1": lambda self: "function1"}, retain_subagent_attributes=True, - retain_subagent_methods=True, ) - multi_level_agents( + create_meta_agent( model, "MetaAgentClass", [agents[1], agents[0], agents[2]], retain_subagent_attributes=True, - retain_subagent_methods=True, ) - assert meta_agent1._subset == {agents[0], agents[1], agents[2], agents[3]} + assert meta_agent1.agents == {agents[0], agents[1], agents[2], agents[3]} assert meta_agent1.function1() == "function1" assert meta_agent1.attribute1 == "value1" assert hasattr(meta_agent1, "custom_attribute") From f231add503592f91052af4f32846da93adb8aa31 Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Thu, 6 Mar 2025 05:40:46 -0500 Subject: [PATCH 22/31] Meta-Agents IP --- mesa/examples/__init__.py | 2 +- .../dynamic_creation}/Readme.md | 0 .../meta_agents/dynamic_creation/__init__.py | 0 .../dynamic_creation}/agents.py | 0 .../dynamic_creation}/app.py | 2 +- .../dynamic_creation}/model.py | 2 +- .../meta_agents/explicit_creation/__init__.py | 0 .../meta_agents/explicit_creation/agents.py | 72 +++++++++++++++++ .../meta_agents/explicit_creation/app.py | 77 +++++++++++++++++++ .../meta_agents/explicit_creation/model.py | 27 +++++++ .../explicit_creation/warehouse_layout.csv | 15 ++++ 11 files changed, 194 insertions(+), 3 deletions(-) rename mesa/examples/basic/{alliance_formation_model => meta_agents/dynamic_creation}/Readme.md (100%) create mode 100644 mesa/examples/basic/meta_agents/dynamic_creation/__init__.py rename mesa/examples/basic/{alliance_formation_model => meta_agents/dynamic_creation}/agents.py (100%) rename mesa/examples/basic/{alliance_formation_model => meta_agents/dynamic_creation}/app.py (95%) rename mesa/examples/basic/{alliance_formation_model => meta_agents/dynamic_creation}/model.py (98%) create mode 100644 mesa/examples/basic/meta_agents/explicit_creation/__init__.py create mode 100644 mesa/examples/basic/meta_agents/explicit_creation/agents.py create mode 100644 mesa/examples/basic/meta_agents/explicit_creation/app.py create mode 100644 mesa/examples/basic/meta_agents/explicit_creation/model.py create mode 100644 mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv diff --git a/mesa/examples/__init__.py b/mesa/examples/__init__.py index 0422b232994..15b82669846 100644 --- a/mesa/examples/__init__.py +++ b/mesa/examples/__init__.py @@ -2,7 +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.meta_agents.dynamic_creation.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 diff --git a/mesa/examples/basic/alliance_formation_model/Readme.md b/mesa/examples/basic/meta_agents/dynamic_creation/Readme.md similarity index 100% rename from mesa/examples/basic/alliance_formation_model/Readme.md rename to mesa/examples/basic/meta_agents/dynamic_creation/Readme.md diff --git a/mesa/examples/basic/meta_agents/dynamic_creation/__init__.py b/mesa/examples/basic/meta_agents/dynamic_creation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mesa/examples/basic/alliance_formation_model/agents.py b/mesa/examples/basic/meta_agents/dynamic_creation/agents.py similarity index 100% rename from mesa/examples/basic/alliance_formation_model/agents.py rename to mesa/examples/basic/meta_agents/dynamic_creation/agents.py diff --git a/mesa/examples/basic/alliance_formation_model/app.py b/mesa/examples/basic/meta_agents/dynamic_creation/app.py similarity index 95% rename from mesa/examples/basic/alliance_formation_model/app.py rename to mesa/examples/basic/meta_agents/dynamic_creation/app.py index 8e5a9050317..053e2ad3dac 100644 --- a/mesa/examples/basic/alliance_formation_model/app.py +++ b/mesa/examples/basic/meta_agents/dynamic_creation/app.py @@ -3,7 +3,7 @@ import solara from matplotlib.figure import Figure -from mesa.examples.basic.alliance_formation_model.model import MultiLevelAllianceModel +from mesa.examples.basic.meta_agents.dynamic_creation.model import MultiLevelAllianceModel from mesa.visualization import SolaraViz from mesa.visualization.utils import update_counter diff --git a/mesa/examples/basic/alliance_formation_model/model.py b/mesa/examples/basic/meta_agents/dynamic_creation/model.py similarity index 98% rename from mesa/examples/basic/alliance_formation_model/model.py rename to mesa/examples/basic/meta_agents/dynamic_creation/model.py index d1f8e891881..909325e12f0 100644 --- a/mesa/examples/basic/alliance_formation_model/model.py +++ b/mesa/examples/basic/meta_agents/dynamic_creation/model.py @@ -2,7 +2,7 @@ import numpy as np import mesa -from mesa.examples.basic.alliance_formation_model.agents import AllianceAgent +from mesa.examples.basic.meta_agents.dynamic_creation.agents import AllianceAgent from mesa.experimental.meta_agents.meta_agent import find_combinations from mesa.experimental.meta_agents.multi_levels import multi_level_agents diff --git a/mesa/examples/basic/meta_agents/explicit_creation/__init__.py b/mesa/examples/basic/meta_agents/explicit_creation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mesa/examples/basic/meta_agents/explicit_creation/agents.py b/mesa/examples/basic/meta_agents/explicit_creation/agents.py new file mode 100644 index 00000000000..6e22382436d --- /dev/null +++ b/mesa/examples/basic/meta_agents/explicit_creation/agents.py @@ -0,0 +1,72 @@ +from queue import PriorityQueue + +import mesa + + + +class RouteAgent(mesa.Agent): + """ + Psuedo of on board routing entity that the agent will take + """ + + def __init__(self, model): + super().__init__(model) + + + def find_path(self, start, goal): + """ + Determines path for robot to take + """ + # A* path finding algorithm + def heuristic(a, b): + return abs(a[0] - b[0]) + abs(a[1] - b[1]) + + def _get_neighbors(self, position): + row, col = position + potential_moves = [(row - 1, col), (row + 1, col), (row, col - 1), (row, col + 1)] + return [move for move in potential_moves if self.warehouse.is_walkable(move)] + + open_set = PriorityQueue() + open_set.put((0, start)) + came_from = {} + g_score = {start: 0} + + while not open_set.empty(): + _, current = open_set.get() + + if current == goal: + path = [] + while current in came_from: + path.append(current) + current = came_from[current] + path.reverse() + return path + + for neighbor in self._get_neighbors(current): + tentative_g_score = g_score[current] + 1 + + if neighbor not in g_score or tentative_g_score < g_score[neighbor]: + g_score[neighbor] = tentative_g_score + f_score = tentative_g_score + heuristic(neighbor, goal) + open_set.put((f_score, neighbor)) + came_from[neighbor] = current + + return None + + + +class SensorAgent(mesa.Agent): + """ + Psuedo sensor that detects other entities in the area + + """ + + def __init__(self, model): + super().__init__(model) + + + def check_path(self, next_position): + """ + Detects obstacles in the area + """ + pass diff --git a/mesa/examples/basic/meta_agents/explicit_creation/app.py b/mesa/examples/basic/meta_agents/explicit_creation/app.py new file mode 100644 index 00000000000..db10360e99f --- /dev/null +++ b/mesa/examples/basic/meta_agents/explicit_creation/app.py @@ -0,0 +1,77 @@ +import matplotlib.pyplot as plt +import networkx as nx +import solara +from matplotlib.figure import Figure + +from mesa.examples.basic.meta_agents.explicit_creation.model import WarehouseModel +from mesa.visualization import SolaraViz +from mesa.visualization.utils import update_counter + +warehouse_model = WarehouseModel() + + +''' +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()] + + 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 = 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 +''' \ No newline at end of file diff --git a/mesa/examples/basic/meta_agents/explicit_creation/model.py b/mesa/examples/basic/meta_agents/explicit_creation/model.py new file mode 100644 index 00000000000..a4ed7c7eb78 --- /dev/null +++ b/mesa/examples/basic/meta_agents/explicit_creation/model.py @@ -0,0 +1,27 @@ +import networkx as nx +import numpy as np +import pandas as pd +import os + +import mesa +import mesa.examples.basic.meta_agents.explicit_creation.agents as agents +from mesa.experimental.meta_agents.meta_agent import MetaAgent + + +class WarehouseModel(mesa.Model): + """ + Model for simulating multi-level alliances among agents. + """ + + def __init__(self, seed=42): + """ + Initialize the model. + + Args: + seed (int): Random seed. + """ + super().__init__(seed=42) + self.map = pd.read_csv(os.path.join(os.path.dirname(__file__), "warehouse_layout.csv"), + header=None).values + + print(self.map) diff --git a/mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv b/mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv new file mode 100644 index 00000000000..5572a2aa19b --- /dev/null +++ b/mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv @@ -0,0 +1,15 @@ +LD,LD,LD,LD,LD, , , , , , , , , , , , , , , + , , , , , , , , , , , , , , , , , , , + , , , , ,A41, , ,X91,C62, , ,O88,C52, , , , , , + , , , , ,T96, , ,W33,X8, , ,S25,T11, , , , , , + , , , , ,O8, , ,S80,Q77, , ,U12,A2, , , , , , + , , , , ,D6, , ,I77,M12, , ,X96,G31, , , , , , + , , , , ,F74, , ,J69,O2, , ,J43,I65, , , , , , + , , , , ,B53, , ,Y8,Y11, , ,X62,N26, , , , , , + , , , , ,H41, , ,T30,J69, , ,P18,S93, , , , , , + , , , , ,H8, , ,C35,V1, , ,V12,K9, , , , , , + , , , , ,H30, , ,Z41,X54, , ,A49,P44, , , , , , + , , , , ,V65, , ,A25,M86, , ,T38,P38, , , , , , + , , , , ,B45, , ,K93,M12, , ,S18,P21, , , , , , + , , , , , , , , , , , , , , , , , , , +CH, ,CH, ,CH, ,CH, ,CH, , , , , , , , , , , From 09fd68494f6e15ba7635be4e00143d53b9a54e86 Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Thu, 6 Mar 2025 05:41:17 -0500 Subject: [PATCH 23/31] Meta-Agents IP2 --- mesa/examples/__init__.py | 4 ++- .../basic/meta_agents/dynamic_creation/app.py | 4 ++- .../meta_agents/explicit_creation/agents.py | 23 +++++++------- .../meta_agents/explicit_creation/app.py | 11 ++----- .../meta_agents/explicit_creation/model.py | 12 ++++---- .../explicit_creation/warehouse_layout.csv | 30 +++++++++---------- 6 files changed, 41 insertions(+), 43 deletions(-) diff --git a/mesa/examples/__init__.py b/mesa/examples/__init__.py index 15b82669846..3f7bd1528fd 100644 --- a/mesa/examples/__init__.py +++ b/mesa/examples/__init__.py @@ -2,10 +2,12 @@ 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.meta_agents.dynamic_creation.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 +from mesa.examples.basic.meta_agents.dynamic_creation.model import ( + MultiLevelAllianceModel, +) from mesa.examples.basic.schelling.model import Schelling from mesa.examples.basic.virus_on_network.model import VirusOnNetwork diff --git a/mesa/examples/basic/meta_agents/dynamic_creation/app.py b/mesa/examples/basic/meta_agents/dynamic_creation/app.py index 053e2ad3dac..28c1b28c983 100644 --- a/mesa/examples/basic/meta_agents/dynamic_creation/app.py +++ b/mesa/examples/basic/meta_agents/dynamic_creation/app.py @@ -3,7 +3,9 @@ import solara from matplotlib.figure import Figure -from mesa.examples.basic.meta_agents.dynamic_creation.model import MultiLevelAllianceModel +from mesa.examples.basic.meta_agents.dynamic_creation.model import ( + MultiLevelAllianceModel, +) from mesa.visualization import SolaraViz from mesa.visualization.utils import update_counter diff --git a/mesa/examples/basic/meta_agents/explicit_creation/agents.py b/mesa/examples/basic/meta_agents/explicit_creation/agents.py index 6e22382436d..932c898e2ac 100644 --- a/mesa/examples/basic/meta_agents/explicit_creation/agents.py +++ b/mesa/examples/basic/meta_agents/explicit_creation/agents.py @@ -3,7 +3,6 @@ import mesa - class RouteAgent(mesa.Agent): """ Psuedo of on board routing entity that the agent will take @@ -11,20 +10,27 @@ class RouteAgent(mesa.Agent): def __init__(self, model): super().__init__(model) - - def find_path(self, start, goal): + def find_path(self, start, goal): """ Determines path for robot to take """ + # A* path finding algorithm def heuristic(a, b): return abs(a[0] - b[0]) + abs(a[1] - b[1]) - + def _get_neighbors(self, position): row, col = position - potential_moves = [(row - 1, col), (row + 1, col), (row, col - 1), (row, col + 1)] - return [move for move in potential_moves if self.warehouse.is_walkable(move)] + potential_moves = [ + (row - 1, col), + (row + 1, col), + (row, col - 1), + (row, col + 1), + ] + return [ + move for move in potential_moves if self.warehouse.is_walkable(move) + ] open_set = PriorityQueue() open_set.put((0, start)) @@ -54,7 +60,6 @@ def _get_neighbors(self, position): return None - class SensorAgent(mesa.Agent): """ Psuedo sensor that detects other entities in the area @@ -63,10 +68,8 @@ class SensorAgent(mesa.Agent): def __init__(self, model): super().__init__(model) - - def check_path(self, next_position): + def check_path(self, next_position): """ Detects obstacles in the area """ - pass diff --git a/mesa/examples/basic/meta_agents/explicit_creation/app.py b/mesa/examples/basic/meta_agents/explicit_creation/app.py index db10360e99f..2bc7e136368 100644 --- a/mesa/examples/basic/meta_agents/explicit_creation/app.py +++ b/mesa/examples/basic/meta_agents/explicit_creation/app.py @@ -1,16 +1,9 @@ -import matplotlib.pyplot as plt -import networkx as nx -import solara -from matplotlib.figure import Figure - from mesa.examples.basic.meta_agents.explicit_creation.model import WarehouseModel -from mesa.visualization import SolaraViz -from mesa.visualization.utils import update_counter warehouse_model = WarehouseModel() -''' +""" model_params = { "seed": { "type": "InputText", @@ -74,4 +67,4 @@ def plot_network(model): name="Alliance Formation Model", ) page # noqa -''' \ No newline at end of file +""" diff --git a/mesa/examples/basic/meta_agents/explicit_creation/model.py b/mesa/examples/basic/meta_agents/explicit_creation/model.py index a4ed7c7eb78..fbef676c435 100644 --- a/mesa/examples/basic/meta_agents/explicit_creation/model.py +++ b/mesa/examples/basic/meta_agents/explicit_creation/model.py @@ -1,11 +1,8 @@ -import networkx as nx -import numpy as np -import pandas as pd import os +import pandas as pd + import mesa -import mesa.examples.basic.meta_agents.explicit_creation.agents as agents -from mesa.experimental.meta_agents.meta_agent import MetaAgent class WarehouseModel(mesa.Model): @@ -21,7 +18,8 @@ def __init__(self, seed=42): seed (int): Random seed. """ super().__init__(seed=42) - self.map = pd.read_csv(os.path.join(os.path.dirname(__file__), "warehouse_layout.csv"), - header=None).values + self.map = pd.read_csv( + os.path.join(os.path.dirname(__file__), "warehouse_layout.csv"), header=None + ).values print(self.map) diff --git a/mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv b/mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv index 5572a2aa19b..de887cc2ac9 100644 --- a/mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv +++ b/mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv @@ -1,15 +1,15 @@ -LD,LD,LD,LD,LD, , , , , , , , , , , , , , , - , , , , , , , , , , , , , , , , , , , - , , , , ,A41, , ,X91,C62, , ,O88,C52, , , , , , - , , , , ,T96, , ,W33,X8, , ,S25,T11, , , , , , - , , , , ,O8, , ,S80,Q77, , ,U12,A2, , , , , , - , , , , ,D6, , ,I77,M12, , ,X96,G31, , , , , , - , , , , ,F74, , ,J69,O2, , ,J43,I65, , , , , , - , , , , ,B53, , ,Y8,Y11, , ,X62,N26, , , , , , - , , , , ,H41, , ,T30,J69, , ,P18,S93, , , , , , - , , , , ,H8, , ,C35,V1, , ,V12,K9, , , , , , - , , , , ,H30, , ,Z41,X54, , ,A49,P44, , , , , , - , , , , ,V65, , ,A25,M86, , ,T38,P38, , , , , , - , , , , ,B45, , ,K93,M12, , ,S18,P21, , , , , , - , , , , , , , , , , , , , , , , , , , -CH, ,CH, ,CH, ,CH, ,CH, , , , , , , , , , , +LD,LD,LD,LD,LD, , , , , , , , , , , , , , , + , , , , , , , , , , , , , , , , , , , + , , , , ,A41, , ,X91,C62, , ,O88,C52, , , , , , + , , , , ,T96, , ,W33,X8, , ,S25,T11, , , , , , + , , , , ,O8, , ,S80,Q77, , ,U12,A2, , , , , , + , , , , ,D6, , ,I77,M12, , ,X96,G31, , , , , , + , , , , ,F74, , ,J69,O2, , ,J43,I65, , , , , , + , , , , ,B53, , ,Y8,Y11, , ,X62,N26, , , , , , + , , , , ,H41, , ,T30,J69, , ,P18,S93, , , , , , + , , , , ,H8, , ,C35,V1, , ,V12,K9, , , , , , + , , , , ,H30, , ,Z41,X54, , ,A49,P44, , , , , , + , , , , ,V65, , ,A25,M86, , ,T38,P38, , , , , , + , , , , ,B45, , ,K93,M12, , ,S18,P21, , , , , , + , , , , , , , , , , , , , , , , , , , +CH, ,CH, ,CH, ,CH, ,CH, , , , , , , , , , , From e444a6eb0e5ac13d8573fd851c597c8f5e7f8433 Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Tue, 18 Mar 2025 16:33:26 -0400 Subject: [PATCH 24/31] update meta agents - update meta-agent to make more functional - add warehouse model - update alliance model -update tests --- docs/tutorials/visualization_tutorial.ipynb | 212 +++++++++++++-- mesa/agent.py | 20 +- mesa/examples/__init__.py | 6 +- .../alliance_formation_model/__init__.py | 10 - .../Readme.md | 0 .../__init__.py | 0 .../agents.py | 0 .../app.py | 2 +- .../model.py | 12 +- .../meta_agents/explicit_creation/agents.py | 75 ------ .../meta_agents/explicit_creation/app.py | 70 ----- .../meta_agents/explicit_creation/model.py | 25 -- .../explicit_creation/warehouse_layout.csv | 15 -- .../basic/meta_agents/warehouse/Readme.md | 34 +++ .../__init__.py | 0 .../basic/meta_agents/warehouse/agents.py | 163 ++++++++++++ .../basic/meta_agents/warehouse/app.py | 125 +++++++++ .../meta_agents/warehouse/make_warehouse.py | 52 ++++ .../basic/meta_agents/warehouse/model.py | 109 ++++++++ mesa/experimental/meta_agents/__init__.py | 3 +- mesa/experimental/meta_agents/meta_agent.py | 251 ++++++++++++++++-- mesa/experimental/meta_agents/meta_agents.py | 159 ----------- mesa/experimental/meta_agents/multi_levels.py | 144 ---------- mesa/model.py | 26 +- tests/test_examples.py | 14 +- tests/test_meta_agents.py | 67 ++++- 26 files changed, 1011 insertions(+), 583 deletions(-) delete mode 100644 mesa/examples/basic/alliance_formation_model/__init__.py rename mesa/examples/basic/meta_agents/{dynamic_creation => alliance_formation}/Readme.md (100%) rename mesa/examples/basic/meta_agents/{dynamic_creation => alliance_formation}/__init__.py (100%) rename mesa/examples/basic/meta_agents/{dynamic_creation => alliance_formation}/agents.py (100%) rename mesa/examples/basic/meta_agents/{dynamic_creation => alliance_formation}/app.py (96%) rename mesa/examples/basic/meta_agents/{dynamic_creation => alliance_formation}/model.py (95%) delete mode 100644 mesa/examples/basic/meta_agents/explicit_creation/agents.py delete mode 100644 mesa/examples/basic/meta_agents/explicit_creation/app.py delete mode 100644 mesa/examples/basic/meta_agents/explicit_creation/model.py delete mode 100644 mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv create mode 100644 mesa/examples/basic/meta_agents/warehouse/Readme.md rename mesa/examples/basic/meta_agents/{explicit_creation => warehouse}/__init__.py (100%) create mode 100644 mesa/examples/basic/meta_agents/warehouse/agents.py create mode 100644 mesa/examples/basic/meta_agents/warehouse/app.py create mode 100644 mesa/examples/basic/meta_agents/warehouse/make_warehouse.py create mode 100644 mesa/examples/basic/meta_agents/warehouse/model.py delete mode 100644 mesa/experimental/meta_agents/meta_agents.py delete mode 100644 mesa/experimental/meta_agents/multi_levels.py diff --git a/docs/tutorials/visualization_tutorial.ipynb b/docs/tutorials/visualization_tutorial.ipynb index 9675e24570e..f92f279d979 100644 --- a/docs/tutorials/visualization_tutorial.ipynb +++ b/docs/tutorials/visualization_tutorial.ipynb @@ -52,9 +52,115 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + " window.jupyter_python_executable = '\\r\\r';\n", + " window.jupyter_widget_checks_silent = true;\n", + " window.jupyter_widget_checks_libraries = [{\"python\": \"ipyvuetify\", \"classic\": \"jupyter-vuetify/extension\", \"lab\": \"jupyter-vuetify\"}, {\"python\": \"ipyvue\", \"classic\": \"jupyter-vue/extension\", \"lab\": \"jupyter-vue\"}];\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + " \n", + "
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mesa version: 3.2.0.dev\n" + ] + } + ], "source": [ "import mesa\n", "from mesa.visualization import SolaraViz, make_plot_component, make_space_component\n", @@ -179,7 +285,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -199,7 +305,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -233,9 +339,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e5937d177a5e49a18c1fc57a2cbbbf23", + "version_major": 2, + "version_minor": 0 + }, + "text/html": [ + "Cannot show widget. You probably want to rerun the code cell above (Click in the code cell, and press Shift+Enter +)." + ], + "text/plain": [ + "Cannot show ipywidgets in text" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Create initial model instance\n", "money_model = MoneyModel(n=50, width=10, height=10) # keyword arguments\n", @@ -270,7 +394,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -299,9 +423,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7c0c0f82c1ca462abafd48f3bee60099", + "version_major": 2, + "version_minor": 0 + }, + "text/html": [ + "Cannot show widget. You probably want to rerun the code cell above (Click in the code cell, and press Shift+Enter +)." + ], + "text/plain": [ + "Cannot show ipywidgets in text" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Create initial model instance\n", "money_model = MoneyModel(n=50, width=10, height=10)\n", @@ -338,7 +480,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -350,7 +492,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -386,7 +528,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -406,7 +548,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -419,14 +561,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "ExecuteTime": { "end_time": "2024-10-29T19:38:49.471838Z", "start_time": "2024-10-29T19:38:47.897295Z" } }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b16003c82a72456b932cdcd89da268e5", + "version_major": 2, + "version_minor": 0 + }, + "text/html": [ + "Cannot show widget. You probably want to rerun the code cell above (Click in the code cell, and press Shift+Enter +)." + ], + "text/plain": [ + "Cannot show ipywidgets in text" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "page = SolaraViz(\n", " money_model,\n", @@ -447,14 +607,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": { "ExecuteTime": { "end_time": "2024-10-29T19:38:49.505725Z", "start_time": "2024-10-29T19:38:49.472599Z" } }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ab1835364c534d56800ce8eae7699c70", + "version_major": 2, + "version_minor": 0 + }, + "text/html": [ + "Cannot show widget. You probably want to rerun the code cell above (Click in the code cell, and press Shift+Enter +)." + ], + "text/plain": [ + "Cannot show ipywidgets in text" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "Histogram(money_model)" ] diff --git a/mesa/agent.py b/mesa/agent.py index 5cfc1131e2c..019c8966ea8 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -48,25 +48,23 @@ class Agent: # so, unique_id is unique relative to a model, and counting starts from 1 _ids = defaultdict(functools.partial(itertools.count, 1)) - def __init__(self, model: Model, *args, **kwargs) -> None: + def __init__( + self, model: Model, *args, key_by_name: bool = False, **kwargs + ) -> None: """Create a new agent. Args: model (Model): The model instance in which the agent exists. - args: passed on to super - kwargs: passed on to super - - Notes: - to make proper use of python's super, in each class remove the arguments and - keyword arguments you need and pass on the rest to super - + key_by_name (bool): If True, use Agent.__name__ as the key in the model's agents_by_type dictionary. + args: Passed on to super. + kwargs: Passed on to super. """ super().__init__(*args, **kwargs) self.model: Model = model self.unique_id: int = next(self._ids[model]) self.pos: Position | None = None - self.model.register_agent(self) + self.model.register_agent(self, key_by_name=key_by_name) def remove(self) -> None: """Remove and delete the agent from the model. @@ -103,7 +101,9 @@ def create_agents(cls, model: Model, n: int, *args, **kwargs) -> AgentSet[Agent] """ class ListLike: - """Helper class to make default arguments act as if they are in a list of length N.""" + """Helper class to make default arguments act as if they are in a + list of length N. + """ def __init__(self, value): self.value = value diff --git a/mesa/examples/__init__.py b/mesa/examples/__init__.py index 3f7bd1528fd..404715b5e80 100644 --- a/mesa/examples/__init__.py +++ b/mesa/examples/__init__.py @@ -5,9 +5,12 @@ 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 -from mesa.examples.basic.meta_agents.dynamic_creation.model import ( +from mesa.examples.basic.meta_agents.alliance_formation.model import ( MultiLevelAllianceModel, ) +from mesa.examples.basic.meta_agents.warehouse.model import ( + WarehouseModel, +) from mesa.examples.basic.schelling.model import Schelling from mesa.examples.basic.virus_on_network.model import VirusOnNetwork @@ -21,5 +24,6 @@ "Schelling", "SugarscapeG1mt", "VirusOnNetwork", + "WarehouseModel", "WolfSheep", ] diff --git a/mesa/examples/basic/alliance_formation_model/__init__.py b/mesa/examples/basic/alliance_formation_model/__init__.py deleted file mode 100644 index 49a80b627ee..00000000000 --- a/mesa/examples/basic/alliance_formation_model/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -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/meta_agents/dynamic_creation/Readme.md b/mesa/examples/basic/meta_agents/alliance_formation/Readme.md similarity index 100% rename from mesa/examples/basic/meta_agents/dynamic_creation/Readme.md rename to mesa/examples/basic/meta_agents/alliance_formation/Readme.md diff --git a/mesa/examples/basic/meta_agents/dynamic_creation/__init__.py b/mesa/examples/basic/meta_agents/alliance_formation/__init__.py similarity index 100% rename from mesa/examples/basic/meta_agents/dynamic_creation/__init__.py rename to mesa/examples/basic/meta_agents/alliance_formation/__init__.py diff --git a/mesa/examples/basic/meta_agents/dynamic_creation/agents.py b/mesa/examples/basic/meta_agents/alliance_formation/agents.py similarity index 100% rename from mesa/examples/basic/meta_agents/dynamic_creation/agents.py rename to mesa/examples/basic/meta_agents/alliance_formation/agents.py diff --git a/mesa/examples/basic/meta_agents/dynamic_creation/app.py b/mesa/examples/basic/meta_agents/alliance_formation/app.py similarity index 96% rename from mesa/examples/basic/meta_agents/dynamic_creation/app.py rename to mesa/examples/basic/meta_agents/alliance_formation/app.py index 28c1b28c983..06aa94d6966 100644 --- a/mesa/examples/basic/meta_agents/dynamic_creation/app.py +++ b/mesa/examples/basic/meta_agents/alliance_formation/app.py @@ -3,7 +3,7 @@ import solara from matplotlib.figure import Figure -from mesa.examples.basic.meta_agents.dynamic_creation.model import ( +from mesa.examples.basic.meta_agents.alliance_formation.model import ( MultiLevelAllianceModel, ) from mesa.visualization import SolaraViz diff --git a/mesa/examples/basic/meta_agents/dynamic_creation/model.py b/mesa/examples/basic/meta_agents/alliance_formation/model.py similarity index 95% rename from mesa/examples/basic/meta_agents/dynamic_creation/model.py rename to mesa/examples/basic/meta_agents/alliance_formation/model.py index 909325e12f0..36a48ae0784 100644 --- a/mesa/examples/basic/meta_agents/dynamic_creation/model.py +++ b/mesa/examples/basic/meta_agents/alliance_formation/model.py @@ -2,9 +2,11 @@ import numpy as np import mesa -from mesa.examples.basic.meta_agents.dynamic_creation.agents import AllianceAgent -from mesa.experimental.meta_agents.meta_agent import find_combinations -from mesa.experimental.meta_agents.multi_levels import multi_level_agents +from mesa.examples.basic.meta_agents.alliance_formation.agents import AllianceAgent +from mesa.experimental.meta_agents.meta_agent import ( + create_meta_agent, + find_combinations, +) class MultiLevelAllianceModel(mesa.Model): @@ -159,7 +161,7 @@ def step(self): for alliance, attributes in combinations: class_name = f"MetaAgentLevel{attributes[2]}" - meta = multi_level_agents( + meta = create_meta_agent( self, class_name, alliance, @@ -177,4 +179,4 @@ def step(self): size=(meta.level + 1) * 300, level=meta.level, ) - self.add_link(meta, meta.subset) + self.add_link(meta, meta.agents) diff --git a/mesa/examples/basic/meta_agents/explicit_creation/agents.py b/mesa/examples/basic/meta_agents/explicit_creation/agents.py deleted file mode 100644 index 932c898e2ac..00000000000 --- a/mesa/examples/basic/meta_agents/explicit_creation/agents.py +++ /dev/null @@ -1,75 +0,0 @@ -from queue import PriorityQueue - -import mesa - - -class RouteAgent(mesa.Agent): - """ - Psuedo of on board routing entity that the agent will take - """ - - def __init__(self, model): - super().__init__(model) - - def find_path(self, start, goal): - """ - Determines path for robot to take - """ - - # A* path finding algorithm - def heuristic(a, b): - return abs(a[0] - b[0]) + abs(a[1] - b[1]) - - def _get_neighbors(self, position): - row, col = position - potential_moves = [ - (row - 1, col), - (row + 1, col), - (row, col - 1), - (row, col + 1), - ] - return [ - move for move in potential_moves if self.warehouse.is_walkable(move) - ] - - open_set = PriorityQueue() - open_set.put((0, start)) - came_from = {} - g_score = {start: 0} - - while not open_set.empty(): - _, current = open_set.get() - - if current == goal: - path = [] - while current in came_from: - path.append(current) - current = came_from[current] - path.reverse() - return path - - for neighbor in self._get_neighbors(current): - tentative_g_score = g_score[current] + 1 - - if neighbor not in g_score or tentative_g_score < g_score[neighbor]: - g_score[neighbor] = tentative_g_score - f_score = tentative_g_score + heuristic(neighbor, goal) - open_set.put((f_score, neighbor)) - came_from[neighbor] = current - - return None - - -class SensorAgent(mesa.Agent): - """ - Psuedo sensor that detects other entities in the area - - """ - - def __init__(self, model): - super().__init__(model) - - def check_path(self, next_position): - """ - Detects obstacles in the area - """ diff --git a/mesa/examples/basic/meta_agents/explicit_creation/app.py b/mesa/examples/basic/meta_agents/explicit_creation/app.py deleted file mode 100644 index 2bc7e136368..00000000000 --- a/mesa/examples/basic/meta_agents/explicit_creation/app.py +++ /dev/null @@ -1,70 +0,0 @@ -from mesa.examples.basic.meta_agents.explicit_creation.model import WarehouseModel - -warehouse_model = WarehouseModel() - - -""" -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()] - - 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 = 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 -""" diff --git a/mesa/examples/basic/meta_agents/explicit_creation/model.py b/mesa/examples/basic/meta_agents/explicit_creation/model.py deleted file mode 100644 index fbef676c435..00000000000 --- a/mesa/examples/basic/meta_agents/explicit_creation/model.py +++ /dev/null @@ -1,25 +0,0 @@ -import os - -import pandas as pd - -import mesa - - -class WarehouseModel(mesa.Model): - """ - Model for simulating multi-level alliances among agents. - """ - - def __init__(self, seed=42): - """ - Initialize the model. - - Args: - seed (int): Random seed. - """ - super().__init__(seed=42) - self.map = pd.read_csv( - os.path.join(os.path.dirname(__file__), "warehouse_layout.csv"), header=None - ).values - - print(self.map) diff --git a/mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv b/mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv deleted file mode 100644 index de887cc2ac9..00000000000 --- a/mesa/examples/basic/meta_agents/explicit_creation/warehouse_layout.csv +++ /dev/null @@ -1,15 +0,0 @@ -LD,LD,LD,LD,LD, , , , , , , , , , , , , , , - , , , , , , , , , , , , , , , , , , , - , , , , ,A41, , ,X91,C62, , ,O88,C52, , , , , , - , , , , ,T96, , ,W33,X8, , ,S25,T11, , , , , , - , , , , ,O8, , ,S80,Q77, , ,U12,A2, , , , , , - , , , , ,D6, , ,I77,M12, , ,X96,G31, , , , , , - , , , , ,F74, , ,J69,O2, , ,J43,I65, , , , , , - , , , , ,B53, , ,Y8,Y11, , ,X62,N26, , , , , , - , , , , ,H41, , ,T30,J69, , ,P18,S93, , , , , , - , , , , ,H8, , ,C35,V1, , ,V12,K9, , , , , , - , , , , ,H30, , ,Z41,X54, , ,A49,P44, , , , , , - , , , , ,V65, , ,A25,M86, , ,T38,P38, , , , , , - , , , , ,B45, , ,K93,M12, , ,S18,P21, , , , , , - , , , , , , , , , , , , , , , , , , , -CH, ,CH, ,CH, ,CH, ,CH, , , , , , , , , , , diff --git a/mesa/examples/basic/meta_agents/warehouse/Readme.md b/mesa/examples/basic/meta_agents/warehouse/Readme.md new file mode 100644 index 00000000000..0b7db453643 --- /dev/null +++ b/mesa/examples/basic/meta_agents/warehouse/Readme.md @@ -0,0 +1,34 @@ +# Pseudo-Warehouse Model + +## Summary + +The purpose of this model is to demonstrate Mesa's meta-agent capability and some of its implementation approaches, not to be an accurate warehouse simulation. In this simulations, robots are given tasks to take retrieve inventory items and then take those items to the loading docks. + +Each `RobotAgent` is made up of sub-components that are treated as separate agents. FOr this simulation, each robot as a `SensorAgent`, `RouterAgent`, and `WorkerAgent`. + +This model demonstrates deliberate meta-agent creation. It shows the basics of meta-agent creation and different ways to use and reference sub-agent and meta-agent functions and attributes. (The alliance formation demonstrates emergent meta-agent creation.) + +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. +- `make_warehouse`: Generates a warehouse numpy array with loading docks, inventory, and charging stations. \ No newline at end of file diff --git a/mesa/examples/basic/meta_agents/explicit_creation/__init__.py b/mesa/examples/basic/meta_agents/warehouse/__init__.py similarity index 100% rename from mesa/examples/basic/meta_agents/explicit_creation/__init__.py rename to mesa/examples/basic/meta_agents/warehouse/__init__.py diff --git a/mesa/examples/basic/meta_agents/warehouse/agents.py b/mesa/examples/basic/meta_agents/warehouse/agents.py new file mode 100644 index 00000000000..039a0003338 --- /dev/null +++ b/mesa/examples/basic/meta_agents/warehouse/agents.py @@ -0,0 +1,163 @@ +from queue import PriorityQueue + +import mesa +from mesa.discrete_space import FixedAgent + + +class InventoryAgent(FixedAgent): + """ + Represents an inventory item in the warehouse. + """ + + def __init__(self, model, cell, item: str): + super().__init__(model, key_by_name=True) + self.cell = cell + self.item = item + self.quantity = 1000 # Default quantity + + +class RouteAgent(mesa.Agent): + """ + Handles path finding for agents in the warehouse. + + Intended to be a pseudo onboard GPS system for robots. + """ + + def __init__(self, model): + super().__init__(model, key_by_name=True) + + def find_path(self, start, goal) -> list[tuple[int, int, int]] | None: + """ + Determines the path for a robot to take using the A* algorithm. + """ + + def heuristic(a, b) -> int: + dx = abs(a[0] - b[0]) + dy = abs(a[1] - b[1]) + return dx + dy + + open_set = PriorityQueue() + open_set.put((0, start.coordinate)) + came_from = {} + g_score = {start.coordinate: 0} + + while not open_set.empty(): + _, current = open_set.get() + + if current[:2] == goal.coordinate[:2]: + path = [] + while current in came_from: + path.append(current) + current = came_from[current] + path.reverse() + path.insert(0, start.coordinate) + path.pop() # Remove the last location (inventory) + return path + + for n_cell in self.model.warehouse[current].neighborhood: + coord = n_cell.coordinate + + # Only consider orthogonal neighbors + if abs(coord[0] - current[0]) + abs(coord[1] - current[1]) != 1: + continue + + tentative_g_score = g_score[current] + 1 + if not n_cell.is_empty: + tentative_g_score += 50 # Penalty for non-empty cells + + if coord not in g_score or tentative_g_score < g_score[coord]: + g_score[coord] = tentative_g_score + f_score = tentative_g_score + heuristic(coord, goal.coordinate) + open_set.put((f_score, coord)) + came_from[coord] = current + + return None + + +class SensorAgent(mesa.Agent): + """ + Detects entities in the area and handles movement along a path. + + Intended to be a pseudo onboard sensor system for robot. + """ + + def __init__(self, model): + super().__init__(model, key_by_name=True) + + def move( + self, coord: tuple[int, int, int], path: list[tuple[int, int, int]] + ) -> str: + """ + Moves the agent along the given path. + """ + if coord not in path: + raise ValueError("Current coordinate not in path.") + + idx = path.index(coord) + if idx + 1 >= len(path): + return "movement complete" + + next_cell = self.model.warehouse[path[idx + 1]] + if next_cell.is_empty: + self.meta_agent.cell = next_cell + return "moving" + + # Handle obstacle + neighbors = self.model.warehouse[self.meta_agent.cell.coordinate].neighborhood + empty_neighbors = [n for n in neighbors if n.is_empty] + if empty_neighbors: + self.meta_agent.cell = self.random.choice(empty_neighbors) + + # Recalculate path + new_path = self.meta_agent.get_subagent_instance(RouteAgent).find_path( + self.meta_agent.cell, self.meta_agent.item.cell + ) + self.meta_agent.path = new_path + return "recalculating" + + +class WorkerAgent(mesa.Agent): + """ + Represents a robot worker responsible for collecting and loading items. + """ + + def __init__(self, model, ld, cs): + super().__init__(model, key_by_name=True) + self.loading_dock = ld + self.charging_station = cs + self.path: list[tuple[int, int, int]] | None = None + self.carrying: str | None = None + self.item: InventoryAgent | None = None + + def initiate_task(self, item: InventoryAgent): + """ + Initiates a task for the robot to perform. + """ + self.item = item + self.path = self.find_path(self.cell, item.cell) + + def continue_task(self): + """ + Continues the task if the robot is able to perform it. + """ + status = self.meta_agent.get_subagent_instance(SensorAgent).move( + self.cell.coordinate, self.path + ) + + if status == "movement complete" and self.meta_agent.status == "inventory": + # Pick up item and bring to loading dock + self.meta_agent.cell = self.model.warehouse[ + *self.meta_agent.cell.coordinate[:2], self.item.cell.coordinate[2] + ] + self.meta_agent.status = "loading" + self.carrying = self.item.item + self.item.quantity -= 1 + self.meta_agent.cell = self.model.warehouse[ + *self.meta_agent.cell.coordinate[:2], 0 + ] + self.path = self.find_path(self.cell, self.loading_dock) + + if status == "movement complete" and self.meta_agent.status == "loading": + # Load item onto truck and return to charging station + self.carrying = None + self.meta_agent.status = "open" diff --git a/mesa/examples/basic/meta_agents/warehouse/app.py b/mesa/examples/basic/meta_agents/warehouse/app.py new file mode 100644 index 00000000000..c95f2669804 --- /dev/null +++ b/mesa/examples/basic/meta_agents/warehouse/app.py @@ -0,0 +1,125 @@ +import matplotlib.pyplot as plt +import pandas as pd +import solara + +from mesa.examples.basic.meta_agents.warehouse.model import WarehouseModel +from mesa.visualization import SolaraViz +from mesa.visualization.utils import update_counter + +# Constants +LOADING_DOCKS = [(0, 0, 0), (0, 2, 0), (0, 4, 0), (0, 6, 0), (0, 8, 0)] +AXIS_LIMITS = {"x": (0, 22), "y": (0, 20), "z": (0, 5)} + +model_params = { + "seed": { + "type": "InputText", + "value": 42, + "label": "Random Seed", + }, +} + + +def prepare_agent_data(model, agent_type, agent_label): + """ + Prepare data for agents of a specific type. + + Args: + model: The WarehouseModel instance. + agent_type: The type of agent (e.g., "InventoryAgent", "RobotAgent"). + agent_label: The label for the agent type. + + Returns: + A list of dictionaries containing agent coordinates and type. + """ + return [ + { + "x": agent.cell.coordinate[0], + "y": agent.cell.coordinate[1], + "z": agent.cell.coordinate[2], + "type": agent_label, + } + for agent in model.agents_by_type[agent_type] + ] + + +@solara.component +def plot_warehouse(model): + """ + Visualize the warehouse model in a 3D scatter plot. + + Args: + model: The WarehouseModel instance. + """ + update_counter.get() + + # Prepare data for inventory and robot agents + inventory_data = prepare_agent_data(model, "InventoryAgent", "Inventory") + robot_data = prepare_agent_data(model, "RobotAgent", "Robot") + + # Combine data into a single DataFrame + data = pd.DataFrame(inventory_data + robot_data) + + # Create Matplotlib 3D scatter plot + fig = plt.figure(figsize=(8, 6)) + ax = fig.add_subplot(111, projection="3d") + + # Highlight loading dock cells + for i, dock in enumerate(LOADING_DOCKS): + ax.scatter( + dock[0], + dock[1], + dock[2], + c="yellow", + label="Loading Dock" + if i == 0 + else None, # Add label only to the first dock + s=300, + marker="o", + ) + + # Plot inventory agents + inventory = data[data["type"] == "Inventory"] + ax.scatter( + inventory["x"], + inventory["y"], + inventory["z"], + c="blue", + label="Inventory", + s=100, + marker="s", + ) + + # Plot robot agents + robots = data[data["type"] == "Robot"] + ax.scatter(robots["x"], robots["y"], robots["z"], c="red", label="Robot", s=200) + + # Set labels, title, and legend + ax.set_xlabel("X") + ax.set_ylabel("Y") + ax.set_zlabel("Z") + ax.set_title("Warehouse Visualization") + ax.legend() + + # Configure plot appearance + ax.grid(False) + ax.set_xlim(*AXIS_LIMITS["x"]) + ax.set_ylim(*AXIS_LIMITS["y"]) + ax.set_zlim(*AXIS_LIMITS["z"]) + ax.axis("off") + + # Render the plot in Solara + solara.FigureMatplotlib(fig) + + +# Create initial model instance +model = WarehouseModel() + +# Create the SolaraViz page +page = SolaraViz( + model, + components=[plot_warehouse], + model_params=model_params, + name="Pseudo-Warehouse Model", +) + +page # noqa diff --git a/mesa/examples/basic/meta_agents/warehouse/make_warehouse.py b/mesa/examples/basic/meta_agents/warehouse/make_warehouse.py new file mode 100644 index 00000000000..bbd5a8d39d4 --- /dev/null +++ b/mesa/examples/basic/meta_agents/warehouse/make_warehouse.py @@ -0,0 +1,52 @@ +import random +import string + +import numpy as np + +# Constants +DEFAULT_ROWS = 22 +DEFAULT_COLS = 20 +DEFAULT_HEIGHT = 4 +LOADING_DOCK_COORDS = [(0, i, 0) for i in range(0, 10, 2)] +CHARGING_STATION_COORDS = [(21, i, 0) for i in range(19, 10, -2)] + + +def generate_item_code() -> str: + """Generate a random item code (1 letter + 2 numbers).""" + letter = random.choice(string.ascii_uppercase) + number = random.randint(10, 99) + return f"{letter}{number}" + + +def make_warehouse( + rows: int = DEFAULT_ROWS, cols: int = DEFAULT_COLS, height: int = DEFAULT_HEIGHT +) -> np.ndarray: + """ + Generate a warehouse layout with designated LD, CS, and storage rows as a NumPy array. + + Args: + rows (int): Number of rows in the warehouse. + cols (int): Number of columns in the warehouse. + height (int): Number of levels in the warehouse. + + Returns: + np.ndarray: A 3D NumPy array representing the warehouse layout. + """ + # Initialize empty warehouse layout + warehouse = np.full((rows, cols, height), " ", dtype=object) + + # Place Loading Docks (LD) + for r, c, h in LOADING_DOCK_COORDS: + warehouse[r, c, h] = "LD" + + # Place Charging Stations (CS) + for r, c, h in CHARGING_STATION_COORDS: + warehouse[r, c, h] = "CS" + + # Fill storage rows with item codes + for r in range(3, rows - 2, 3): # Skip row 0,1,2 (LD) and row 17,18,19 (CS) + for c in range(2, cols, 3): # Leave 2 spaces between each item row + for h in range(height): + warehouse[r, c, h] = generate_item_code() + + return warehouse diff --git a/mesa/examples/basic/meta_agents/warehouse/model.py b/mesa/examples/basic/meta_agents/warehouse/model.py new file mode 100644 index 00000000000..cb5224e48ea --- /dev/null +++ b/mesa/examples/basic/meta_agents/warehouse/model.py @@ -0,0 +1,109 @@ +import mesa +from mesa.discrete_space import OrthogonalMooreGrid +from mesa.discrete_space.cell_agent import CellAgent +from mesa.examples.basic.meta_agents.warehouse.agents import ( + InventoryAgent, + RouteAgent, + SensorAgent, + WorkerAgent, +) +from mesa.examples.basic.meta_agents.warehouse.make_warehouse import make_warehouse +from mesa.experimental.meta_agents.meta_agent import create_meta_agent + +# Constants for configuration +LOADING_DOCKS = [(0, 0, 0), (0, 2, 0), (0, 4, 0), (0, 6, 0), (0, 8, 0)] +CHARGING_STATIONS = [ + (21, 19, 0), + (21, 17, 0), + (21, 15, 0), + (21, 13, 0), + (21, 11, 0), +] +INVENTORY_START_ROW_OFFSET = 3 + + +class WarehouseModel(mesa.Model): + """ + Model for simulating warehouse management with autonomous systems where + each autonomous system (e.g., robot) is made of numerous smaller agents + (e.g., routing, sensors, etc.). + """ + + def __init__(self, seed=42): + """ + Initialize the model. + + Args: + seed (int): Random seed. + """ + super().__init__(seed=seed) + self.inventory = {} + self.loading_docks = LOADING_DOCKS + self.charging_stations = CHARGING_STATIONS + + # Create warehouse and instantiate grid + layout = make_warehouse() + self.warehouse = OrthogonalMooreGrid( + (layout.shape[0], layout.shape[1], layout.shape[2]), + torus=False, + capacity=1, + random=self.random, + ) + + # Create Inventory Agents + for row in range( + INVENTORY_START_ROW_OFFSET, layout.shape[0] - INVENTORY_START_ROW_OFFSET + ): + for col in range(layout.shape[1]): + for height in range(layout.shape[2]): + if layout[row][col][height].strip(): + item = layout[row][col][height] + InventoryAgent(self, self.warehouse[row, col, height], item) + + # Create Robot Agents + for idx in range(len(self.loading_docks)): + # Create sub-agents + router = RouteAgent(self) + sensor = SensorAgent(self) + worker = WorkerAgent( + self, + self.warehouse[self.loading_docks[idx]], + self.warehouse[self.charging_stations[idx]], + ) + + # Create meta-agent and place in warehouse + create_meta_agent( + self, + "RobotAgent", + [router, sensor, worker], + mesa_agent_type=CellAgent, + meta_attributes={ + "cell": self.warehouse[self.charging_stations[idx]], + "status": "open", + }, + assume_subagent_attributes=True, + assume_subagent_methods=True, + ) + + def central_move(self, robot): + """ + Consolidates meta-agent behavior in the model class. + + Args: + robot: The robot meta-agent to move. + """ + robot.move(robot.cell.coordinate, robot.path) + + def step(self): + """ + Advance the model by one step. + """ + for robot in self.agents_by_type["RobotAgent"]: + if robot.status == "open": # Assign a task to the robot + item = self.random.choice(self.agents_by_type["InventoryAgent"]) + if item.quantity > 0: + robot.initiate_task(item) + robot.status = "inventory" + self.central_move(robot) + else: + robot.continue_task() diff --git a/mesa/experimental/meta_agents/__init__.py b/mesa/experimental/meta_agents/__init__.py index 420f4ad34b1..f4f3231295b 100644 --- a/mesa/experimental/meta_agents/__init__.py +++ b/mesa/experimental/meta_agents/__init__.py @@ -21,6 +21,5 @@ """ from .meta_agent import MetaAgent -from .multi_levels import multi_level_agents -__all__ = ["MetaAgent", "multi_level_agents"] +__all__ = ["MetaAgent"] diff --git a/mesa/experimental/meta_agents/meta_agent.py b/mesa/experimental/meta_agents/meta_agent.py index b1882c51195..570f47347c4 100644 --- a/mesa/experimental/meta_agents/meta_agent.py +++ b/mesa/experimental/meta_agents/meta_agent.py @@ -1,17 +1,25 @@ """Implementation of Mesa's meta agent capability. -This contains two helper functions and a MetaAgent class that can be used to create agents that contain other agents as components. +This contains two helper methods and a MetaAgent class that can be used to +create agents that contain other agents as components. -Helper functions: -1 - find_combinations: Find combinations of agents to create a meta-agent subset. -2- evaluate_combination: Evaluate combinations of agents by some user based criteria to determine if it should be a subset. +Helper methods: +1 - find_combinations: Find combinations of agents to create a meta-agent +subset. +2- evaluate_combination: Evaluate combinations of agents by some user based +criteria to determine if it should be a subset. -Meta-Agent class (MetaAgent): An agent that contains other agents as components. +Meta-Agent class (MetaAgent): An agent that contains other agents +as components. + +See basic examples >> meta_agents for dynamic creation and explicit creation """ import itertools -from collections.abc import Callable +from collections.abc import Callable, Iterable from concurrent.futures import ThreadPoolExecutor +from types import MethodType +from typing import Any from mesa.agent import Agent, AgentSet @@ -26,10 +34,12 @@ def evaluate_combination( Args: candidate_group (Tuple[Agent, ...]): The group of agents to evaluate. model: The model instance. - evaluation_func (Optional[Callable[[AgentSet], float]]): The function to evaluate the group. + evaluation_func (Optional[Callable[[AgentSet], float]]): The function + to evaluate the group. Returns: - Optional[Tuple[AgentSet, float]]: The evaluated group and its value, or None. + Optional[Tuple[AgentSet, float]]: The evaluated group and its value, + or None. """ group_set = AgentSet(candidate_group, random=model.random) if evaluation_func: @@ -51,9 +61,15 @@ def find_combinations( Args: model: The model instance. group (AgentSet): The set of agents to find combinations in. - size (Union[int, Tuple[int, int]], optional): The size or range of sizes for combinations. Defaults to (2, 5). - evaluation_func (Optional[Callable[[AgentSet], float]], optional): The function to evaluate combinations. Defaults to None. - filter_func (Optional[Callable[[List[Tuple[AgentSet, float]]], List[Tuple[AgentSet, float]]]], optional): The function to filter combinations. Defaults to None. + size (Union[int, Tuple[int, int]], optional): The size or range of + sizes for combinations. Defaults to (2, 5). + evaluation_func (Optional[Callable[[AgentSet], float]], optional): The + function to evaluate combinations. Defaults to None. + filter_func (Optional[Callable[[List[Tuple[AgentSet, float]]]): Allows + the user to specify how agents are filtered to form groups. + Defaults to None. + List[Tuple[AgentSet, float]]]], optional): The function to filter + combinations. Defaults to None. Returns: List[Tuple[AgentSet, float]]: The list of valuable combinations. @@ -86,18 +102,163 @@ def find_combinations( return combinations +def extract_class(agents_by_type: dict, new_agent_class: str): + """Helper function for create_meta_agents extracts the type of agent + being created to create a new instance of that agent type. + + Args: + agents_by_type (dict): The dictionary of agents by type. + new_agent_class (str): The name of the agent class to be created + + Returns: + type(Agent) if it agent type exists + None otherwise + """ + + if new_agent_class in agents_by_type: + return type(agents_by_type[new_agent_class][0]) + return None + + +def create_meta_agent( + model: Any, + new_agent_class: str, + agents: Iterable[Any], + mesa_agent_type=Agent, + meta_attributes: dict[str, Any] = dict(), # noqa B006 + meta_methods: dict[str, Callable] = dict(), # noqa B006 + assume_subagent_methods: bool = False, + assume_subagent_attributes: bool = False, +) -> Any | None: + + """Dynamically create a new meta-agent class and instantiate agents + in that class. + + Parameters: + model (Any): The model instance. + new_agent_class (str): The name of the new meta-agent class. + agents (Iterable[Any]): The agents to be included in the meta-agent. + meta_attributes (Dict[str, Any]): Attributes to be added to the meta-agent. + meta_methods (Dict[str, Callable]): Methods to be added to the meta-agent. + assume_subagent_methods (bool): Whether to assume methods from + sub-agents as meta_agent methods. + assume_subagent_attributes (bool): Whether to retain attributes + from sub-agents. + + Returns: + Optional[Any]: + - None if adding agent(s) 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 + """ + + # Convert agents to set to ensure uniqueness + agents = set(agents) + + def add_methods( + meta_agent_instance: Any, + agents: Iterable[Any], + meta_methods: dict[str, Callable], + ) -> None: + """Add methods to the meta-agent instance. + + Parameters: + meta_agent_instance (Any): The meta-agent instance. + agents (Iterable[Any]): The agents to derive methods from. + meta_methods (Dict[str, Callable]): methods to be added to the meta-agent. + """ + if assume_subagent_methods: + agent_classes = {type(agent) for agent in agents} + for agent_class in agent_classes: + for name in agent_class.__dict__: + if callable(getattr(agent_class, name)) and not name.startswith( + "__" + ): + original_method = getattr(agent_class, name) + meta_methods[name] = original_method + + for name, meth in meta_methods.items(): + bound_method = MethodType(meth, meta_agent_instance) + setattr(meta_agent_instance, name, bound_method) + + def add_attributes( + meta_agent_instance: Any, agents: Iterable[Any], meta_attributes: dict[str, Any] + ) -> None: + """Add attributes to the meta-agent instance. + + Parameters: + meta_agent_instance (Any): The meta-agent instance. + agents (Iterable[Any]): The agents to derive attributes from. + meta_attributes (Dict[str, Any]): Attributes to be added to the + meta-agent. + """ + if assume_subagent_attributes: + for agent in agents: + for name, value in agent.__dict__.items(): + if not callable(value): + meta_attributes[name] = 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")] + if len(subagents) > 0: + if len(subagents) == 1: + add_attributes(subagents[0].meta_agent, agents, meta_attributes) + add_methods(subagents[0].meta_agent, agents, meta_methods) + 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_methods(subagent.meta_agent, agents, meta_methods) + 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 + agent_class = extract_class(model.agents_by_type, new_agent_class) + if agent_class: + meta_agent_instance = agent_class(model, agents) + add_attributes(meta_agent_instance, agents, meta_attributes) + add_methods(meta_agent_instance, agents, meta_methods) + return meta_agent_instance + else: + # Path 3 - Create a new meta-agent class + meta_agent_class = type( + new_agent_class, + (MetaAgent, mesa_agent_type), # Use the meta_agent_type parameter here + { + "unique_id": None, + "_subset": None, + }, + ) + meta_agent_instance = meta_agent_class(model, agents) + add_attributes(meta_agent_instance, agents, meta_attributes) + add_methods(meta_agent_instance, agents, meta_methods) + return meta_agent_instance + + class MetaAgent(Agent): """A MetaAgent is an agent that contains other agents as components.""" - def __init__(self, model, agents: set[Agent] | None = None): + def __init__( + self, model, agents: set[Agent] | None = None, name: str = "MetaAgent" + ): """Create a new MetaAgent. Args: model: The model instance. - agents (Optional[set[Agent]], optional): The set of agents to include in the MetaAgent. Defaults to None. + agents (Optional[set[Agent]], optional): The set of agents to + include in the MetaAgent. Defaults to None. + name (str, optional): The name of the MetaAgent. Defaults to "MetaAgent". """ - super().__init__(model) + super().__init__(model, key_by_name=True) self._subset = AgentSet(agents or [], random=model.random) + self.name = name # Add ref to meta_agent in subagents for agent in self._subset: @@ -116,11 +277,55 @@ def __contains__(self, agent: Agent) -> bool: return agent in self._subset @property - def subset(self) -> AgentSet: - """Read-only access to components as an AgentSet.""" - return self._subset + def agents(self) -> AgentSet: + """Get list of Meta-Agent subagents.""" + return set(self._subset) - def add_subagents(self, new_agents: set[Agent]): + @property + def subagents_by_type(self) -> dict[type, list[Agent]]: + """Get the subagents grouped by type. + + Returns: + dict[type, list[Agent]]: A dictionary of subagents grouped by type. + """ + subagents_by_type = {} + for agent in self._subset: + agent_type = type(agent) + if agent_type not in subagents_by_type: + subagents_by_type[agent_type] = [] + subagents_by_type[agent_type].append(agent) + return subagents_by_type + + @property + def subagent_types(self) -> set[type]: + """Get the types of all subagents. + + Returns: + set[type]: A set of unique types of the subagents. + """ + return {type(agent) for agent in self._subset} + + def get_subagent_instance(self, agent_type) -> set[type]: + """Get the instance of a subagent of the specified type. + + Args: + agent_type: The type of the subagent to retrieve. + + Returns: + The first instance of the specified subagent type. + + Raises: + ValueError: If no subagent of the specified type is found. + """ + try: + return self.subagents_by_type[agent_type][0] + except KeyError: + raise ValueError(f"No subagent of type {agent_type} found.") + + def add_subagents( + self, + new_agents: set[Agent], + ): """Add agents as components. Args: @@ -129,6 +334,7 @@ def add_subagents(self, new_agents: set[Agent]): for agent in new_agents: self._subset.add(agent) agent.meta_agent = self # TODO: Make a set for meta_agents + self.model.register_agent(agent) def remove_subagents(self, remove_agents: set[Agent]): """Remove agents as components. @@ -139,6 +345,15 @@ def remove_subagents(self, remove_agents: set[Agent]): for agent in remove_agents: self._subset.discard(agent) agent.meta_agent = None # TODO: Remove meta_agent from set + self.model.deregister_agent(agent) + + def get_subagent_type(self) -> set[type]: + """Get the types of all subagents. + + Returns: + set[type]: A set of unique types of the subagents. + """ + return {type(agent) for agent in self._subset} def step(self): """Perform the agent's step. diff --git a/mesa/experimental/meta_agents/meta_agents.py b/mesa/experimental/meta_agents/meta_agents.py deleted file mode 100644 index 75048c46533..00000000000 --- a/mesa/experimental/meta_agents/meta_agents.py +++ /dev/null @@ -1,159 +0,0 @@ -"""This method is for dynamically 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 meta-agent -2. Create new meta-agent instance of existing meta-agent class -3. Create new meta-agent class - -See alliance formation model in basic examples for usage. - -""" - -from types import MethodType - - -def create_meta_agent( - model, - new_agent_class: str, - agents, - meta_attributes=dict(), # noqa B006 - meta_functions=dict(), # noqa B006 - retain_subagent_functions=True, - retain_subagent_attributes=False, -): - """Dynamically create a new meta-agent class and instantiate agents in that class. - - Parameters: - model (Model): The model instance. - new_agent_class (str): The name of the new meta-agent class. - agents (Iterable[Agent]): The agents to be included in the meta-agent. - meta_attributes (dict): Attributes to be added to the meta-agent. - meta_functions (dict): Functions to be added to the meta-agent. - retain_subagent_functions (bool): Whether to retain functions from sub-agents. - retain_subagent_attributes (bool): Whether to retain attributes from sub-agents. - - Returns: - - None if adding agent(s) 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 - """ - 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. - - Parameters: - meta_agent_instance (MetaAgent): The meta-agent instance. - agents (Iterable[Agent]): The agents to derive functions from. - meta_functions (dict): Functions to be added to the meta-agent. - """ - if retain_subagent_functions: - agent_classes = {type(agent) for agent in agents} - for agent_class in agent_classes: - for name in dir(agent_class): - 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) - - def add_attributes(meta_agent_instance, agents, meta_attributes): - """Add attributes to the meta-agent instance. - - Parameters: - meta_agent_instance (MetaAgent): The meta-agent instance. - agents (Iterable[Agent]): The agents to derive attributes from. - meta_attributes (dict): Attributes to be added to the meta-agent. - """ - if retain_subagent_attributes: - for agent in agents: - for name, value in agent.__dict__.items(): - if not callable(value): - meta_attributes[name] = value - - if meta_attributes: - 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")] - if len(subagents) > 0: - 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) - 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) - # 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 - agent_class = next( - ( - agent_type - for agent_type in model.agent_types - if agent_type.__name__ == new_agent_class - ), - None, - ) - - if agent_class: - 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,), - { - "unique_id": None, - "agents": None, - }, - ) - - meta_agent_instance = meta_agent_class(model=model, agents=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/mesa/experimental/meta_agents/multi_levels.py b/mesa/experimental/meta_agents/multi_levels.py deleted file mode 100644 index 6238c53b9d6..00000000000 --- a/mesa/experimental/meta_agents/multi_levels.py +++ /dev/null @@ -1,144 +0,0 @@ -"""This function is for dynamically growing multiple levels of meta-agents. - -Each new level creates a new meta-agent class that is created dynamically using the provided name and -unique attributes and methods. - -Currently restricted to one parent agent and one meta-agent per agent. -Goal is to assess usage and expand functionality. - -Function has three paths of execution: -1. Add agents to existing meta-agent -2. Create new meta-agent instance of existing meta-agent class -3. Create new meta-agent class - -See multi-level alliance formation model in basic examples for usage. - -""" - -from collections.abc import Callable, Iterable -from types import MethodType -from typing import Any - -from mesa.experimental.meta_agents.meta_agent import MetaAgent - - -def multi_level_agents( - model: Any, - new_agent_class: str, - agents: Iterable[Any], - meta_attributes: dict[str, Any] = dict(), # noqa B006 - meta_methods: dict[str, Callable] = dict(), # noqa B006 - retain_subagent_methods: bool = False, - retain_subagent_attributes: bool = False, -) -> Any | None: - """Dynamically create a new meta-agent class and instantiate agents in that class. - - Parameters: - model (Any): The model instance. - new_agent_class (str): The name of the new meta-agent class. - agents (Iterable[Any]): The agents to be included in the meta-agent. - meta_attributes (Dict[str, Any]): Attributes to be added to the meta-agent. - meta_methods (Dict[str, Callable]): Methods to be added to the meta-agent. - retain_subagent_functions (bool): Whether to retain functions from sub-agents. - retain_subagent_attributes (bool): Whether to retain attributes from sub-agents. - - Returns: - Optional[Any]: - - None if adding agent(s) 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 - """ - # Convert agents to set to ensure uniqueness - agents = set(agents) - - def add_methods( - meta_agent_instance: Any, - agents: Iterable[Any], - meta_methods: dict[str, Callable], - ) -> None: - """Add functions to the meta-agent instance. - - Parameters: - meta_agent_instance (Any): The meta-agent instance. - agents (Iterable[Any]): The agents to derive functions from. - meta_methods (Dict[str, Callable]): Functions to be added to the meta-agent. - """ - if retain_subagent_methods: - agent_classes = {type(agent) for agent in agents} - for agent_class in agent_classes: - for name in agent_class.__dict__: - if callable(getattr(agent_class, name)) and not name.startswith( - "__" - ): - original_method = getattr(agent_class, name) - meta_methods[name] = original_method - - for name, meth in meta_methods.items(): - bound_method = MethodType(meth, meta_agent_instance) - setattr(meta_agent_instance, name, bound_method) - - def add_attributes( - meta_agent_instance: Any, agents: Iterable[Any], meta_attributes: dict[str, Any] - ) -> None: - """Add attributes to the meta-agent instance. - - Parameters: - meta_agent_instance (Any): The meta-agent instance. - agents (Iterable[Any]): The agents to derive attributes from. - meta_attributes (Dict[str, Any]): Attributes to be added to the meta-agent. - """ - if retain_subagent_attributes: - for agent in agents: - for name, value in agent.__dict__.items(): - if not callable(value): - meta_attributes[name] = 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")] - if len(subagents) > 0: - if len(subagents) == 1: - add_attributes(subagents[0].meta_agent, agents, meta_attributes) - add_methods(subagents[0].meta_agent, agents, meta_methods) - 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_methods(subagent.meta_agent, agents, meta_methods) - 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 - agent_class = next( - ( - agent_type - for agent_type in model.agent_types - if agent_type.__name__ == new_agent_class - ), - None, - ) - - if agent_class: - meta_agent_instance = agent_class(model, agents) - add_attributes(meta_agent_instance, agents, meta_attributes) - add_methods(meta_agent_instance, agents, meta_methods) - return meta_agent_instance - else: - # Path 3 - Create a new meta-agent class - meta_agent_class = type( - new_agent_class, - (MetaAgent,), - { - "unique_id": None, - "_subset": None, - }, - ) - - meta_agent_instance = meta_agent_class(model, agents) - add_attributes(meta_agent_instance, agents, meta_attributes) - add_methods(meta_agent_instance, agents, meta_methods) - return meta_agent_instance diff --git a/mesa/model.py b/mesa/model.py index c507ed73a10..95e079431a3 100644 --- a/mesa/model.py +++ b/mesa/model.py @@ -144,25 +144,20 @@ def agents_by_type(self) -> dict[type[Agent], AgentSet]: """A dictionary where the keys are agent types and the values are the corresponding AgentSets.""" return self._agents_by_type - def register_agent(self, agent): + def register_agent(self, agent, key_by_name: bool = False): """Register the agent with the model. Args: agent: The agent to register. - - Notes: - This method is called automatically by ``Agent.__init__``, so there is no need to use this - if you are subclassing Agent and calling its super in the ``__init__`` method. - + key_by_name (bool): If True, use Agent.__name__ as the key in the agents_by_type dictionary. """ self._agents[agent] = None - # because AgentSet requires model, we cannot use defaultdict - # tricks with a function won't work because model then cannot be pickled + key = agent.__class__.__name__ if key_by_name else type(agent) try: - self._agents_by_type[type(agent)].add(agent) + self._agents_by_type[key].add(agent) except KeyError: - self._agents_by_type[type(agent)] = AgentSet( + self._agents_by_type[key] = AgentSet( [ agent, ], @@ -174,18 +169,17 @@ def register_agent(self, agent): f"registered {agent.__class__.__name__} with agent_id {agent.unique_id}" ) - def deregister_agent(self, agent): + def deregister_agent(self, agent, key_by_name: bool = False): """Deregister the agent with the model. Args: agent: The agent to deregister. - - Notes: - This method is called automatically by ``Agent.remove`` - + key_by_name (bool): If True, use Agent.__name__ as the key in the agents_by_type dictionary. """ del self._agents[agent] - self._agents_by_type[type(agent)].remove(agent) + + key = agent.__class__.__name__ if key_by_name else type(agent) + self._agents_by_type[key].remove(agent) self._all_agents.remove(agent) _mesa_logger.debug(f"deregistered agent with agent_id {agent.unique_id}") diff --git a/tests/test_examples.py b/tests/test_examples.py index f9e020cbde2..d18834d908e 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -9,6 +9,7 @@ Schelling, SugarscapeG1mt, VirusOnNetwork, + WarehouseModel, WolfSheep, ) @@ -110,7 +111,7 @@ def test_wolf_sheep(): # noqa: D103 def test_alliance_formation_model(): # noqa: D103 - from mesa.examples.basic.alliance_formation_model import app + from mesa.examples.basic.meta_agents.alliance_formation import app app.page # noqa: B018 @@ -120,3 +121,14 @@ def test_alliance_formation_model(): # noqa: D103 model.step() assert len(model.agents) == len(model.network.nodes) + + +def test_warehouse_model(): # noqa: D103 + from mesa.examples.basic.meta_agents.warehouse import app + + app.page # noqa: B018 + + model = WarehouseModel(seed=42) + + for _i in range(10): + model.step() diff --git a/tests/test_meta_agents.py b/tests/test_meta_agents.py index c14d3f05472..388ca4e5724 100644 --- a/tests/test_meta_agents.py +++ b/tests/test_meta_agents.py @@ -5,10 +5,10 @@ from mesa import Agent, Model from mesa.experimental.meta_agents.meta_agent import ( MetaAgent, + create_meta_agent, evaluate_combination, find_combinations, ) -from mesa.experimental.meta_agents.multi_levels import multi_level_agents class CustomAgent(Agent): @@ -32,7 +32,7 @@ def setup_agents(): tuple: A tuple containing the model and a list of agents. """ model = Model() - agent1 = Agent(model) + agent1 = CustomAgent(model) agent2 = Agent(model) agent3 = Agent(model) agent4 = Agent(model) @@ -42,7 +42,7 @@ def setup_agents(): def test_create_meta_agent_new_class(setup_agents): - """Test creating a new meta-agent class and test inclusion of attributes and functions. + """Test creating a new meta-agent class and test inclusion of attributes and methods. Args: setup_agents (tuple): The model and agents fixture. @@ -53,8 +53,8 @@ def test_create_meta_agent_new_class(setup_agents): "MetaAgentClass", agents, meta_attributes={"attribute1": "value1"}, - meta_functions={"function1": lambda self: "function1"}, - retain_subagent_attributes=True, + meta_methods={"function1": lambda self: "function1"}, + assume_subagent_attributes=True, ) assert meta_agent is not None assert meta_agent.attribute1 == "value1" @@ -78,7 +78,7 @@ def test_create_meta_agent_existing_class(setup_agents): "MetaAgentClass", [agents[0], agents[2]], meta_attributes={"attribute1": "value1"}, - meta_functions={"function1": lambda self: "function1"}, + meta_methods={"function1": lambda self: "function1"}, ) # Create new meta-agent instance with existing class @@ -87,8 +87,8 @@ def test_create_meta_agent_existing_class(setup_agents): "MetaAgentClass", [agents[1], agents[3]], meta_attributes={"attribute2": "value2"}, - meta_functions={"function2": lambda self: "function2"}, - retain_subagent_attributes=True, + meta_methods={"function2": lambda self: "function2"}, + assume_subagent_attributes=True, ) assert meta_agent is not None assert meta_agent2.attribute2 == "value2" @@ -113,15 +113,16 @@ def test_add_agents_to_existing_meta_agent(setup_agents): "MetaAgentClass", [agents[0], agents[3]], meta_attributes={"attribute1": "value1"}, - meta_functions={"function1": lambda self: "function1"}, - retain_subagent_attributes=True, + meta_methods={"function1": lambda self: "function1"}, + assume_subagent_attributes=True, ) create_meta_agent( model, "MetaAgentClass", [agents[1], agents[0], agents[2]], - retain_subagent_attributes=True, + assume_subagent_attributes=True, + assume_subagent_methods=True, ) assert meta_agent1.agents == {agents[0], agents[1], agents[2], agents[3]} assert meta_agent1.function1() == "function1" @@ -140,14 +141,14 @@ def test_meta_agent_integration(setup_agents): """ model, agents = setup_agents - meta_agent = multi_level_agents( + meta_agent = create_meta_agent( model, "MetaAgentClass", agents, meta_attributes={"attribute1": "value1"}, meta_methods={"function1": lambda self: "function1"}, - retain_subagent_attributes=True, - retain_subagent_methods=True, + assume_subagent_attributes=True, + assume_subagent_methods=True, ) model.step() @@ -260,3 +261,41 @@ def test_meta_agent_remove_subagents(setup_agents): meta_agent = MetaAgent(model, set(agents)) meta_agent.remove_subagents({agents[2], agents[3]}) assert meta_agent._subset == {agents[0], agents[1]} + + +def test_meta_agent_subagents_by_type(setup_agents): + """Test the subagents_by_type property of MetaAgent.""" + model, agents = setup_agents + meta_agent = MetaAgent(model, set(agents)) + subagents_by_type = meta_agent.subagents_by_type + assert isinstance(subagents_by_type, dict) + for agent_type, agent_list in subagents_by_type.items(): + assert all(isinstance(agent, agent_type) for agent in agent_list) + + +def test_meta_agent_subagent_types(setup_agents): + """Test the subagent_types property of MetaAgent.""" + model, agents = setup_agents + meta_agent = MetaAgent(model, set(agents)) + subagent_types = meta_agent.subagent_types + assert isinstance(subagent_types, set) + assert all(isinstance(agent_type, type) for agent_type in subagent_types) + + +def test_meta_agent_get_subagent_instance(setup_agents): + """Test the get_subagent_instance method of MetaAgent.""" + model, agents = setup_agents + meta_agent = MetaAgent(model, set(agents)) + agent_type = type(agents[0]) + subagent_instance = meta_agent.get_subagent_instance(agent_type) + assert isinstance(subagent_instance, agent_type) + with pytest.raises(ValueError): + meta_agent.get_subagent_instance(str) # Invalid type + + +def test_meta_agent_step(setup_agents): + """Test the step method of MetaAgent.""" + model, agents = setup_agents + meta_agent = MetaAgent(model, set(agents)) + meta_agent.step() # Ensure no errors occur during step + # Add additional assertions if step behavior is defined in the future From f73205109f919ad8bf2f010a55b5f67f9a7b87af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:48:20 +0000 Subject: [PATCH 25/31] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/meta_agents/meta_agent.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mesa/experimental/meta_agents/meta_agent.py b/mesa/experimental/meta_agents/meta_agent.py index 570f47347c4..03a98513da1 100644 --- a/mesa/experimental/meta_agents/meta_agent.py +++ b/mesa/experimental/meta_agents/meta_agent.py @@ -114,7 +114,6 @@ def extract_class(agents_by_type: dict, new_agent_class: str): type(Agent) if it agent type exists None otherwise """ - if new_agent_class in agents_by_type: return type(agents_by_type[new_agent_class][0]) return None @@ -130,7 +129,6 @@ def create_meta_agent( assume_subagent_methods: bool = False, assume_subagent_attributes: bool = False, ) -> Any | None: - """Dynamically create a new meta-agent class and instantiate agents in that class. @@ -152,7 +150,6 @@ def create_meta_agent( created agent type - New class instance if created a new dynamically created agent type """ - # Convert agents to set to ensure uniqueness agents = set(agents) From ffd45075b4f6670faee385430f392c9a6e64d48a Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Wed, 26 Mar 2025 05:22:01 -0400 Subject: [PATCH 26/31] Updates - increase test coverage - fix pre-commit issues - fix examples autobuild --- mesa/agent.py | 5 ++-- mesa/examples/__init__.py | 12 ++++----- .../alliance_formation/Readme.md | 2 +- .../__init__ .py} | 0 .../alliance_formation/agents.py | 0 .../alliance_formation/app.py | 2 +- .../alliance_formation/model.py | 2 +- .../{meta_agents => }/warehouse/Readme.md | 2 +- .../{meta_agents => }/warehouse/__init__.py | 0 .../{meta_agents => }/warehouse/agents.py | 0 .../basic/{meta_agents => }/warehouse/app.py | 2 +- .../warehouse/make_warehouse.py | 0 .../{meta_agents => }/warehouse/model.py | 4 +-- mesa/experimental/meta_agents/meta_agent.py | 27 ++++++++----------- tests/test_examples.py | 7 ++--- 15 files changed, 31 insertions(+), 34 deletions(-) rename mesa/examples/basic/{meta_agents => }/alliance_formation/Readme.md (97%) rename mesa/examples/basic/{meta_agents/alliance_formation/__init__.py => alliance_formation/__init__ .py} (100%) rename mesa/examples/basic/{meta_agents => }/alliance_formation/agents.py (100%) rename mesa/examples/basic/{meta_agents => }/alliance_formation/app.py (96%) rename mesa/examples/basic/{meta_agents => }/alliance_formation/model.py (98%) rename mesa/examples/basic/{meta_agents => }/warehouse/Readme.md (96%) rename mesa/examples/basic/{meta_agents => }/warehouse/__init__.py (100%) rename mesa/examples/basic/{meta_agents => }/warehouse/agents.py (100%) rename mesa/examples/basic/{meta_agents => }/warehouse/app.py (97%) rename mesa/examples/basic/{meta_agents => }/warehouse/make_warehouse.py (100%) rename mesa/examples/basic/{meta_agents => }/warehouse/model.py (95%) diff --git a/mesa/agent.py b/mesa/agent.py index 019c8966ea8..df466333758 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -101,8 +101,9 @@ def create_agents(cls, model: Model, n: int, *args, **kwargs) -> AgentSet[Agent] """ class ListLike: - """Helper class to make default arguments act as if they are in a - list of length N. + """Make default arguments act as if they are in a list of length N. + + This is a helper class. """ def __init__(self, value): diff --git a/mesa/examples/__init__.py b/mesa/examples/__init__.py index 404715b5e80..e04ccb5dfb9 100644 --- a/mesa/examples/__init__.py +++ b/mesa/examples/__init__.py @@ -2,17 +2,17 @@ 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 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 -from mesa.examples.basic.meta_agents.alliance_formation.model import ( - MultiLevelAllianceModel, -) -from mesa.examples.basic.meta_agents.warehouse.model import ( - WarehouseModel, -) from mesa.examples.basic.schelling.model import Schelling from mesa.examples.basic.virus_on_network.model import VirusOnNetwork +from mesa.examples.basic.warehouse.model import ( + WarehouseModel, +) __all__ = [ "BoidFlockers", diff --git a/mesa/examples/basic/meta_agents/alliance_formation/Readme.md b/mesa/examples/basic/alliance_formation/Readme.md similarity index 97% rename from mesa/examples/basic/meta_agents/alliance_formation/Readme.md rename to mesa/examples/basic/alliance_formation/Readme.md index 00a9a19adfc..6c28f28d218 100644 --- a/mesa/examples/basic/meta_agents/alliance_formation/Readme.md +++ b/mesa/examples/basic/alliance_formation/Readme.md @@ -1,4 +1,4 @@ -# Alliance Formation Model +# Alliance Formation Model (Meta-Agent Example) ## Summary diff --git a/mesa/examples/basic/meta_agents/alliance_formation/__init__.py b/mesa/examples/basic/alliance_formation/__init__ .py similarity index 100% rename from mesa/examples/basic/meta_agents/alliance_formation/__init__.py rename to mesa/examples/basic/alliance_formation/__init__ .py diff --git a/mesa/examples/basic/meta_agents/alliance_formation/agents.py b/mesa/examples/basic/alliance_formation/agents.py similarity index 100% rename from mesa/examples/basic/meta_agents/alliance_formation/agents.py rename to mesa/examples/basic/alliance_formation/agents.py diff --git a/mesa/examples/basic/meta_agents/alliance_formation/app.py b/mesa/examples/basic/alliance_formation/app.py similarity index 96% rename from mesa/examples/basic/meta_agents/alliance_formation/app.py rename to mesa/examples/basic/alliance_formation/app.py index 06aa94d6966..29086cde456 100644 --- a/mesa/examples/basic/meta_agents/alliance_formation/app.py +++ b/mesa/examples/basic/alliance_formation/app.py @@ -3,7 +3,7 @@ import solara from matplotlib.figure import Figure -from mesa.examples.basic.meta_agents.alliance_formation.model import ( +from mesa.examples.basic.alliance_formation.model import ( MultiLevelAllianceModel, ) from mesa.visualization import SolaraViz diff --git a/mesa/examples/basic/meta_agents/alliance_formation/model.py b/mesa/examples/basic/alliance_formation/model.py similarity index 98% rename from mesa/examples/basic/meta_agents/alliance_formation/model.py rename to mesa/examples/basic/alliance_formation/model.py index 36a48ae0784..1616d74aa5b 100644 --- a/mesa/examples/basic/meta_agents/alliance_formation/model.py +++ b/mesa/examples/basic/alliance_formation/model.py @@ -2,7 +2,7 @@ import numpy as np import mesa -from mesa.examples.basic.meta_agents.alliance_formation.agents import AllianceAgent +from mesa.examples.basic.alliance_formation.agents import AllianceAgent from mesa.experimental.meta_agents.meta_agent import ( create_meta_agent, find_combinations, diff --git a/mesa/examples/basic/meta_agents/warehouse/Readme.md b/mesa/examples/basic/warehouse/Readme.md similarity index 96% rename from mesa/examples/basic/meta_agents/warehouse/Readme.md rename to mesa/examples/basic/warehouse/Readme.md index 0b7db453643..a7080c0ccae 100644 --- a/mesa/examples/basic/meta_agents/warehouse/Readme.md +++ b/mesa/examples/basic/warehouse/Readme.md @@ -1,4 +1,4 @@ -# Pseudo-Warehouse Model +# Pseudo-Warehouse Model (Meta-Agent Example) ## Summary diff --git a/mesa/examples/basic/meta_agents/warehouse/__init__.py b/mesa/examples/basic/warehouse/__init__.py similarity index 100% rename from mesa/examples/basic/meta_agents/warehouse/__init__.py rename to mesa/examples/basic/warehouse/__init__.py diff --git a/mesa/examples/basic/meta_agents/warehouse/agents.py b/mesa/examples/basic/warehouse/agents.py similarity index 100% rename from mesa/examples/basic/meta_agents/warehouse/agents.py rename to mesa/examples/basic/warehouse/agents.py diff --git a/mesa/examples/basic/meta_agents/warehouse/app.py b/mesa/examples/basic/warehouse/app.py similarity index 97% rename from mesa/examples/basic/meta_agents/warehouse/app.py rename to mesa/examples/basic/warehouse/app.py index c95f2669804..87858f81f30 100644 --- a/mesa/examples/basic/meta_agents/warehouse/app.py +++ b/mesa/examples/basic/warehouse/app.py @@ -2,7 +2,7 @@ import pandas as pd import solara -from mesa.examples.basic.meta_agents.warehouse.model import WarehouseModel +from mesa.examples.basic.warehouse.model import WarehouseModel from mesa.visualization import SolaraViz from mesa.visualization.utils import update_counter diff --git a/mesa/examples/basic/meta_agents/warehouse/make_warehouse.py b/mesa/examples/basic/warehouse/make_warehouse.py similarity index 100% rename from mesa/examples/basic/meta_agents/warehouse/make_warehouse.py rename to mesa/examples/basic/warehouse/make_warehouse.py diff --git a/mesa/examples/basic/meta_agents/warehouse/model.py b/mesa/examples/basic/warehouse/model.py similarity index 95% rename from mesa/examples/basic/meta_agents/warehouse/model.py rename to mesa/examples/basic/warehouse/model.py index cb5224e48ea..9ebab92f97d 100644 --- a/mesa/examples/basic/meta_agents/warehouse/model.py +++ b/mesa/examples/basic/warehouse/model.py @@ -1,13 +1,13 @@ import mesa from mesa.discrete_space import OrthogonalMooreGrid from mesa.discrete_space.cell_agent import CellAgent -from mesa.examples.basic.meta_agents.warehouse.agents import ( +from mesa.examples.basic.warehouse.agents import ( InventoryAgent, RouteAgent, SensorAgent, WorkerAgent, ) -from mesa.examples.basic.meta_agents.warehouse.make_warehouse import make_warehouse +from mesa.examples.basic.warehouse.make_warehouse import make_warehouse from mesa.experimental.meta_agents.meta_agent import create_meta_agent # Constants for configuration diff --git a/mesa/experimental/meta_agents/meta_agent.py b/mesa/experimental/meta_agents/meta_agent.py index 03a98513da1..4bec1b1d3ff 100644 --- a/mesa/experimental/meta_agents/meta_agent.py +++ b/mesa/experimental/meta_agents/meta_agent.py @@ -1,18 +1,23 @@ """Implementation of Mesa's meta agent capability. -This contains two helper methods and a MetaAgent class that can be used to +This contains four helper functions and a MetaAgent class that can be used to create agents that contain other agents as components. Helper methods: 1 - find_combinations: Find combinations of agents to create a meta-agent subset. 2- evaluate_combination: Evaluate combinations of agents by some user based -criteria to determine if it should be a subset. +criteria to determine if it should be a subset of agents. +3- extract_class: Helper function for create_meta-agent. Extracts the types of +agent being created to create a new instance of that agent type. +4- create_meta_agent: Create a new meta-agent class and instantiate +agents in that class. Meta-Agent class (MetaAgent): An agent that contains other agents as components. -See basic examples >> meta_agents for dynamic creation and explicit creation +See basic examples >> meta_agents for deliberate meta-agent creation (warehouse) +and emergent meta-agent creation (alliance formation) of meta-agents. """ import itertools @@ -103,8 +108,7 @@ def find_combinations( def extract_class(agents_by_type: dict, new_agent_class: str): - """Helper function for create_meta_agents extracts the type of agent - being created to create a new instance of that agent type. + """Helper function for create_meta_agents extracts the types of agents. Args: agents_by_type (dict): The dictionary of agents by type. @@ -129,8 +133,7 @@ def create_meta_agent( assume_subagent_methods: bool = False, assume_subagent_attributes: bool = False, ) -> Any | None: - """Dynamically create a new meta-agent class and instantiate agents - in that class. + """Create a new meta-agent class and instantiate agents. Parameters: model (Any): The model instance. @@ -317,7 +320,7 @@ def get_subagent_instance(self, agent_type) -> set[type]: try: return self.subagents_by_type[agent_type][0] except KeyError: - raise ValueError(f"No subagent of type {agent_type} found.") + raise ValueError(f"No subagent of type {agent_type} found.") from None def add_subagents( self, @@ -344,14 +347,6 @@ def remove_subagents(self, remove_agents: set[Agent]): agent.meta_agent = None # TODO: Remove meta_agent from set self.model.deregister_agent(agent) - def get_subagent_type(self) -> set[type]: - """Get the types of all subagents. - - Returns: - set[type]: A set of unique types of the subagents. - """ - return {type(agent) for agent in self._subset} - def step(self): """Perform the agent's step. diff --git a/tests/test_examples.py b/tests/test_examples.py index d18834d908e..bea02a2d25b 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -111,7 +111,7 @@ def test_wolf_sheep(): # noqa: D103 def test_alliance_formation_model(): # noqa: D103 - from mesa.examples.basic.meta_agents.alliance_formation import app + from mesa.examples.basic.alliance_formation import app app.page # noqa: B018 @@ -124,11 +124,12 @@ def test_alliance_formation_model(): # noqa: D103 def test_warehouse_model(): # noqa: D103 - from mesa.examples.basic.meta_agents.warehouse import app + from mesa.examples.basic.warehouse import app app.page # noqa: B018 model = WarehouseModel(seed=42) - for _i in range(10): + # More steps needed to hit coverage difference + for _i in range(30): model.step() From 0378bb53632ad26673f34ed6f05dc8d9a283aa9f Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Tue, 8 Apr 2025 04:38:00 -0400 Subject: [PATCH 27/31] update meta-agent --- mesa/experimental/meta_agents/meta_agent.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/mesa/experimental/meta_agents/meta_agent.py b/mesa/experimental/meta_agents/meta_agent.py index 4bec1b1d3ff..0eef89441b5 100644 --- a/mesa/experimental/meta_agents/meta_agent.py +++ b/mesa/experimental/meta_agents/meta_agent.py @@ -128,8 +128,8 @@ def create_meta_agent( new_agent_class: str, agents: Iterable[Any], mesa_agent_type=Agent, - meta_attributes: dict[str, Any] = dict(), # noqa B006 - meta_methods: dict[str, Callable] = dict(), # noqa B006 + meta_attributes: dict[str, Any] = None, + meta_methods: dict[str, Callable] = None, # noqa B006 assume_subagent_methods: bool = False, assume_subagent_attributes: bool = False, ) -> Any | None: @@ -178,9 +178,10 @@ def add_methods( original_method = getattr(agent_class, name) meta_methods[name] = original_method - for name, meth in meta_methods.items(): - bound_method = MethodType(meth, meta_agent_instance) - setattr(meta_agent_instance, name, bound_method) + if meta_methods is not None: + for name, meth in meta_methods.items(): + bound_method = MethodType(meth, meta_agent_instance) + setattr(meta_agent_instance, name, bound_method) def add_attributes( meta_agent_instance: Any, agents: Iterable[Any], meta_attributes: dict[str, Any] @@ -199,8 +200,9 @@ def add_attributes( if not callable(value): meta_attributes[name] = value - for key, value in meta_attributes.items(): - setattr(meta_agent_instance, key, value) + if meta_attributes is not None: + 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")] From 29988fc9952bc1f6410edd69ad91ce5182c3840a Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Mon, 21 Apr 2025 04:56:32 -0400 Subject: [PATCH 28/31] Requested updates --- mesa/examples/__init__.py | 8 +- .../alliance_formation/Readme.md | 11 +- .../alliance_formation/__init__ .py | 0 .../alliance_formation/agents.py | 0 .../alliance_formation/app.py | 4 +- .../alliance_formation/model.py | 2 +- mesa/examples/basic/warehouse/Readme.md | 34 ---- mesa/examples/basic/warehouse/__init__.py | 0 mesa/examples/basic/warehouse/agents.py | 163 ------------------ mesa/examples/basic/warehouse/app.py | 125 -------------- .../basic/warehouse/make_warehouse.py | 52 ------ mesa/examples/basic/warehouse/model.py | 109 ------------ mesa/experimental/meta_agents/meta_agent.py | 34 +++- tests/test_examples.py | 15 +- 14 files changed, 43 insertions(+), 514 deletions(-) rename mesa/examples/{basic => advanced}/alliance_formation/Readme.md (70%) rename mesa/examples/{basic => advanced}/alliance_formation/__init__ .py (100%) rename mesa/examples/{basic => advanced}/alliance_formation/agents.py (100%) rename mesa/examples/{basic => advanced}/alliance_formation/app.py (95%) rename mesa/examples/{basic => advanced}/alliance_formation/model.py (98%) delete mode 100644 mesa/examples/basic/warehouse/Readme.md delete mode 100644 mesa/examples/basic/warehouse/__init__.py delete mode 100644 mesa/examples/basic/warehouse/agents.py delete mode 100644 mesa/examples/basic/warehouse/app.py delete mode 100644 mesa/examples/basic/warehouse/make_warehouse.py delete mode 100644 mesa/examples/basic/warehouse/model.py diff --git a/mesa/examples/__init__.py b/mesa/examples/__init__.py index e04ccb5dfb9..90e6cd1e2a3 100644 --- a/mesa/examples/__init__.py +++ b/mesa/examples/__init__.py @@ -1,18 +1,13 @@ +from mesa.examples.advanced.alliance_formation.model import MultiLevelAllianceModel 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 from mesa.examples.advanced.wolf_sheep.model import WolfSheep -from mesa.examples.basic.alliance_formation.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 from mesa.examples.basic.schelling.model import Schelling from mesa.examples.basic.virus_on_network.model import VirusOnNetwork -from mesa.examples.basic.warehouse.model import ( - WarehouseModel, -) __all__ = [ "BoidFlockers", @@ -24,6 +19,5 @@ "Schelling", "SugarscapeG1mt", "VirusOnNetwork", - "WarehouseModel", "WolfSheep", ] diff --git a/mesa/examples/basic/alliance_formation/Readme.md b/mesa/examples/advanced/alliance_formation/Readme.md similarity index 70% rename from mesa/examples/basic/alliance_formation/Readme.md rename to mesa/examples/advanced/alliance_formation/Readme.md index 6c28f28d218..1933294bc3f 100644 --- a/mesa/examples/basic/alliance_formation/Readme.md +++ b/mesa/examples/advanced/alliance_formation/Readme.md @@ -2,13 +2,22 @@ ## Summary +This model demonstrates Mesa's meta agent capability. + +**Overview of mate agent:** Complex systems often have multiple levels of components. A city is not a single entity, but it is made of districts,neighborhoods, buildings, and people. A forest comprises an ecosystem of trees, plants, animals, and microorganisms. An organization is not one entity, but is made of departments, sub-departments, and people. A person is not a single entity, but it is made of micro biomes, organs and cells. + +This reality is the motivation for meta-agents. It allows users to represent these multiple levels, where each level can have agents with sub-agents. + 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 +In MetaAgents current configuration, agents being part of multiple meta-agents is not supported. + +If you would like to see an example of explicit meta-agent formation see the [warehouse model in the Mesa example's repository](https://github.com/projectmesa/mesa-examples/tree/main/examples/warehouse) + ## Installation diff --git a/mesa/examples/basic/alliance_formation/__init__ .py b/mesa/examples/advanced/alliance_formation/__init__ .py similarity index 100% rename from mesa/examples/basic/alliance_formation/__init__ .py rename to mesa/examples/advanced/alliance_formation/__init__ .py diff --git a/mesa/examples/basic/alliance_formation/agents.py b/mesa/examples/advanced/alliance_formation/agents.py similarity index 100% rename from mesa/examples/basic/alliance_formation/agents.py rename to mesa/examples/advanced/alliance_formation/agents.py diff --git a/mesa/examples/basic/alliance_formation/app.py b/mesa/examples/advanced/alliance_formation/app.py similarity index 95% rename from mesa/examples/basic/alliance_formation/app.py rename to mesa/examples/advanced/alliance_formation/app.py index 29086cde456..d9faff91c40 100644 --- a/mesa/examples/basic/alliance_formation/app.py +++ b/mesa/examples/advanced/alliance_formation/app.py @@ -3,9 +3,7 @@ import solara from matplotlib.figure import Figure -from mesa.examples.basic.alliance_formation.model import ( - MultiLevelAllianceModel, -) +from mesa.examples.advanced.alliance_formation.model import MultiLevelAllianceModel from mesa.visualization import SolaraViz from mesa.visualization.utils import update_counter diff --git a/mesa/examples/basic/alliance_formation/model.py b/mesa/examples/advanced/alliance_formation/model.py similarity index 98% rename from mesa/examples/basic/alliance_formation/model.py rename to mesa/examples/advanced/alliance_formation/model.py index 1616d74aa5b..bd16d5ee98b 100644 --- a/mesa/examples/basic/alliance_formation/model.py +++ b/mesa/examples/advanced/alliance_formation/model.py @@ -2,7 +2,7 @@ import numpy as np import mesa -from mesa.examples.basic.alliance_formation.agents import AllianceAgent +from mesa.examples.advanced.alliance_formation.agents import AllianceAgent from mesa.experimental.meta_agents.meta_agent import ( create_meta_agent, find_combinations, diff --git a/mesa/examples/basic/warehouse/Readme.md b/mesa/examples/basic/warehouse/Readme.md deleted file mode 100644 index a7080c0ccae..00000000000 --- a/mesa/examples/basic/warehouse/Readme.md +++ /dev/null @@ -1,34 +0,0 @@ -# Pseudo-Warehouse Model (Meta-Agent Example) - -## Summary - -The purpose of this model is to demonstrate Mesa's meta-agent capability and some of its implementation approaches, not to be an accurate warehouse simulation. In this simulations, robots are given tasks to take retrieve inventory items and then take those items to the loading docks. - -Each `RobotAgent` is made up of sub-components that are treated as separate agents. FOr this simulation, each robot as a `SensorAgent`, `RouterAgent`, and `WorkerAgent`. - -This model demonstrates deliberate meta-agent creation. It shows the basics of meta-agent creation and different ways to use and reference sub-agent and meta-agent functions and attributes. (The alliance formation demonstrates emergent meta-agent creation.) - -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. -- `make_warehouse`: Generates a warehouse numpy array with loading docks, inventory, and charging stations. \ No newline at end of file diff --git a/mesa/examples/basic/warehouse/__init__.py b/mesa/examples/basic/warehouse/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/mesa/examples/basic/warehouse/agents.py b/mesa/examples/basic/warehouse/agents.py deleted file mode 100644 index 039a0003338..00000000000 --- a/mesa/examples/basic/warehouse/agents.py +++ /dev/null @@ -1,163 +0,0 @@ -from queue import PriorityQueue - -import mesa -from mesa.discrete_space import FixedAgent - - -class InventoryAgent(FixedAgent): - """ - Represents an inventory item in the warehouse. - """ - - def __init__(self, model, cell, item: str): - super().__init__(model, key_by_name=True) - self.cell = cell - self.item = item - self.quantity = 1000 # Default quantity - - -class RouteAgent(mesa.Agent): - """ - Handles path finding for agents in the warehouse. - - Intended to be a pseudo onboard GPS system for robots. - """ - - def __init__(self, model): - super().__init__(model, key_by_name=True) - - def find_path(self, start, goal) -> list[tuple[int, int, int]] | None: - """ - Determines the path for a robot to take using the A* algorithm. - """ - - def heuristic(a, b) -> int: - dx = abs(a[0] - b[0]) - dy = abs(a[1] - b[1]) - return dx + dy - - open_set = PriorityQueue() - open_set.put((0, start.coordinate)) - came_from = {} - g_score = {start.coordinate: 0} - - while not open_set.empty(): - _, current = open_set.get() - - if current[:2] == goal.coordinate[:2]: - path = [] - while current in came_from: - path.append(current) - current = came_from[current] - path.reverse() - path.insert(0, start.coordinate) - path.pop() # Remove the last location (inventory) - return path - - for n_cell in self.model.warehouse[current].neighborhood: - coord = n_cell.coordinate - - # Only consider orthogonal neighbors - if abs(coord[0] - current[0]) + abs(coord[1] - current[1]) != 1: - continue - - tentative_g_score = g_score[current] + 1 - if not n_cell.is_empty: - tentative_g_score += 50 # Penalty for non-empty cells - - if coord not in g_score or tentative_g_score < g_score[coord]: - g_score[coord] = tentative_g_score - f_score = tentative_g_score + heuristic(coord, goal.coordinate) - open_set.put((f_score, coord)) - came_from[coord] = current - - return None - - -class SensorAgent(mesa.Agent): - """ - Detects entities in the area and handles movement along a path. - - Intended to be a pseudo onboard sensor system for robot. - """ - - def __init__(self, model): - super().__init__(model, key_by_name=True) - - def move( - self, coord: tuple[int, int, int], path: list[tuple[int, int, int]] - ) -> str: - """ - Moves the agent along the given path. - """ - if coord not in path: - raise ValueError("Current coordinate not in path.") - - idx = path.index(coord) - if idx + 1 >= len(path): - return "movement complete" - - next_cell = self.model.warehouse[path[idx + 1]] - if next_cell.is_empty: - self.meta_agent.cell = next_cell - return "moving" - - # Handle obstacle - neighbors = self.model.warehouse[self.meta_agent.cell.coordinate].neighborhood - empty_neighbors = [n for n in neighbors if n.is_empty] - if empty_neighbors: - self.meta_agent.cell = self.random.choice(empty_neighbors) - - # Recalculate path - new_path = self.meta_agent.get_subagent_instance(RouteAgent).find_path( - self.meta_agent.cell, self.meta_agent.item.cell - ) - self.meta_agent.path = new_path - return "recalculating" - - -class WorkerAgent(mesa.Agent): - """ - Represents a robot worker responsible for collecting and loading items. - """ - - def __init__(self, model, ld, cs): - super().__init__(model, key_by_name=True) - self.loading_dock = ld - self.charging_station = cs - self.path: list[tuple[int, int, int]] | None = None - self.carrying: str | None = None - self.item: InventoryAgent | None = None - - def initiate_task(self, item: InventoryAgent): - """ - Initiates a task for the robot to perform. - """ - self.item = item - self.path = self.find_path(self.cell, item.cell) - - def continue_task(self): - """ - Continues the task if the robot is able to perform it. - """ - status = self.meta_agent.get_subagent_instance(SensorAgent).move( - self.cell.coordinate, self.path - ) - - if status == "movement complete" and self.meta_agent.status == "inventory": - # Pick up item and bring to loading dock - self.meta_agent.cell = self.model.warehouse[ - *self.meta_agent.cell.coordinate[:2], self.item.cell.coordinate[2] - ] - self.meta_agent.status = "loading" - self.carrying = self.item.item - self.item.quantity -= 1 - self.meta_agent.cell = self.model.warehouse[ - *self.meta_agent.cell.coordinate[:2], 0 - ] - self.path = self.find_path(self.cell, self.loading_dock) - - if status == "movement complete" and self.meta_agent.status == "loading": - # Load item onto truck and return to charging station - self.carrying = None - self.meta_agent.status = "open" diff --git a/mesa/examples/basic/warehouse/app.py b/mesa/examples/basic/warehouse/app.py deleted file mode 100644 index 87858f81f30..00000000000 --- a/mesa/examples/basic/warehouse/app.py +++ /dev/null @@ -1,125 +0,0 @@ -import matplotlib.pyplot as plt -import pandas as pd -import solara - -from mesa.examples.basic.warehouse.model import WarehouseModel -from mesa.visualization import SolaraViz -from mesa.visualization.utils import update_counter - -# Constants -LOADING_DOCKS = [(0, 0, 0), (0, 2, 0), (0, 4, 0), (0, 6, 0), (0, 8, 0)] -AXIS_LIMITS = {"x": (0, 22), "y": (0, 20), "z": (0, 5)} - -model_params = { - "seed": { - "type": "InputText", - "value": 42, - "label": "Random Seed", - }, -} - - -def prepare_agent_data(model, agent_type, agent_label): - """ - Prepare data for agents of a specific type. - - Args: - model: The WarehouseModel instance. - agent_type: The type of agent (e.g., "InventoryAgent", "RobotAgent"). - agent_label: The label for the agent type. - - Returns: - A list of dictionaries containing agent coordinates and type. - """ - return [ - { - "x": agent.cell.coordinate[0], - "y": agent.cell.coordinate[1], - "z": agent.cell.coordinate[2], - "type": agent_label, - } - for agent in model.agents_by_type[agent_type] - ] - - -@solara.component -def plot_warehouse(model): - """ - Visualize the warehouse model in a 3D scatter plot. - - Args: - model: The WarehouseModel instance. - """ - update_counter.get() - - # Prepare data for inventory and robot agents - inventory_data = prepare_agent_data(model, "InventoryAgent", "Inventory") - robot_data = prepare_agent_data(model, "RobotAgent", "Robot") - - # Combine data into a single DataFrame - data = pd.DataFrame(inventory_data + robot_data) - - # Create Matplotlib 3D scatter plot - fig = plt.figure(figsize=(8, 6)) - ax = fig.add_subplot(111, projection="3d") - - # Highlight loading dock cells - for i, dock in enumerate(LOADING_DOCKS): - ax.scatter( - dock[0], - dock[1], - dock[2], - c="yellow", - label="Loading Dock" - if i == 0 - else None, # Add label only to the first dock - s=300, - marker="o", - ) - - # Plot inventory agents - inventory = data[data["type"] == "Inventory"] - ax.scatter( - inventory["x"], - inventory["y"], - inventory["z"], - c="blue", - label="Inventory", - s=100, - marker="s", - ) - - # Plot robot agents - robots = data[data["type"] == "Robot"] - ax.scatter(robots["x"], robots["y"], robots["z"], c="red", label="Robot", s=200) - - # Set labels, title, and legend - ax.set_xlabel("X") - ax.set_ylabel("Y") - ax.set_zlabel("Z") - ax.set_title("Warehouse Visualization") - ax.legend() - - # Configure plot appearance - ax.grid(False) - ax.set_xlim(*AXIS_LIMITS["x"]) - ax.set_ylim(*AXIS_LIMITS["y"]) - ax.set_zlim(*AXIS_LIMITS["z"]) - ax.axis("off") - - # Render the plot in Solara - solara.FigureMatplotlib(fig) - - -# Create initial model instance -model = WarehouseModel() - -# Create the SolaraViz page -page = SolaraViz( - model, - components=[plot_warehouse], - model_params=model_params, - name="Pseudo-Warehouse Model", -) - -page # noqa diff --git a/mesa/examples/basic/warehouse/make_warehouse.py b/mesa/examples/basic/warehouse/make_warehouse.py deleted file mode 100644 index bbd5a8d39d4..00000000000 --- a/mesa/examples/basic/warehouse/make_warehouse.py +++ /dev/null @@ -1,52 +0,0 @@ -import random -import string - -import numpy as np - -# Constants -DEFAULT_ROWS = 22 -DEFAULT_COLS = 20 -DEFAULT_HEIGHT = 4 -LOADING_DOCK_COORDS = [(0, i, 0) for i in range(0, 10, 2)] -CHARGING_STATION_COORDS = [(21, i, 0) for i in range(19, 10, -2)] - - -def generate_item_code() -> str: - """Generate a random item code (1 letter + 2 numbers).""" - letter = random.choice(string.ascii_uppercase) - number = random.randint(10, 99) - return f"{letter}{number}" - - -def make_warehouse( - rows: int = DEFAULT_ROWS, cols: int = DEFAULT_COLS, height: int = DEFAULT_HEIGHT -) -> np.ndarray: - """ - Generate a warehouse layout with designated LD, CS, and storage rows as a NumPy array. - - Args: - rows (int): Number of rows in the warehouse. - cols (int): Number of columns in the warehouse. - height (int): Number of levels in the warehouse. - - Returns: - np.ndarray: A 3D NumPy array representing the warehouse layout. - """ - # Initialize empty warehouse layout - warehouse = np.full((rows, cols, height), " ", dtype=object) - - # Place Loading Docks (LD) - for r, c, h in LOADING_DOCK_COORDS: - warehouse[r, c, h] = "LD" - - # Place Charging Stations (CS) - for r, c, h in CHARGING_STATION_COORDS: - warehouse[r, c, h] = "CS" - - # Fill storage rows with item codes - for r in range(3, rows - 2, 3): # Skip row 0,1,2 (LD) and row 17,18,19 (CS) - for c in range(2, cols, 3): # Leave 2 spaces between each item row - for h in range(height): - warehouse[r, c, h] = generate_item_code() - - return warehouse diff --git a/mesa/examples/basic/warehouse/model.py b/mesa/examples/basic/warehouse/model.py deleted file mode 100644 index 9ebab92f97d..00000000000 --- a/mesa/examples/basic/warehouse/model.py +++ /dev/null @@ -1,109 +0,0 @@ -import mesa -from mesa.discrete_space import OrthogonalMooreGrid -from mesa.discrete_space.cell_agent import CellAgent -from mesa.examples.basic.warehouse.agents import ( - InventoryAgent, - RouteAgent, - SensorAgent, - WorkerAgent, -) -from mesa.examples.basic.warehouse.make_warehouse import make_warehouse -from mesa.experimental.meta_agents.meta_agent import create_meta_agent - -# Constants for configuration -LOADING_DOCKS = [(0, 0, 0), (0, 2, 0), (0, 4, 0), (0, 6, 0), (0, 8, 0)] -CHARGING_STATIONS = [ - (21, 19, 0), - (21, 17, 0), - (21, 15, 0), - (21, 13, 0), - (21, 11, 0), -] -INVENTORY_START_ROW_OFFSET = 3 - - -class WarehouseModel(mesa.Model): - """ - Model for simulating warehouse management with autonomous systems where - each autonomous system (e.g., robot) is made of numerous smaller agents - (e.g., routing, sensors, etc.). - """ - - def __init__(self, seed=42): - """ - Initialize the model. - - Args: - seed (int): Random seed. - """ - super().__init__(seed=seed) - self.inventory = {} - self.loading_docks = LOADING_DOCKS - self.charging_stations = CHARGING_STATIONS - - # Create warehouse and instantiate grid - layout = make_warehouse() - self.warehouse = OrthogonalMooreGrid( - (layout.shape[0], layout.shape[1], layout.shape[2]), - torus=False, - capacity=1, - random=self.random, - ) - - # Create Inventory Agents - for row in range( - INVENTORY_START_ROW_OFFSET, layout.shape[0] - INVENTORY_START_ROW_OFFSET - ): - for col in range(layout.shape[1]): - for height in range(layout.shape[2]): - if layout[row][col][height].strip(): - item = layout[row][col][height] - InventoryAgent(self, self.warehouse[row, col, height], item) - - # Create Robot Agents - for idx in range(len(self.loading_docks)): - # Create sub-agents - router = RouteAgent(self) - sensor = SensorAgent(self) - worker = WorkerAgent( - self, - self.warehouse[self.loading_docks[idx]], - self.warehouse[self.charging_stations[idx]], - ) - - # Create meta-agent and place in warehouse - create_meta_agent( - self, - "RobotAgent", - [router, sensor, worker], - mesa_agent_type=CellAgent, - meta_attributes={ - "cell": self.warehouse[self.charging_stations[idx]], - "status": "open", - }, - assume_subagent_attributes=True, - assume_subagent_methods=True, - ) - - def central_move(self, robot): - """ - Consolidates meta-agent behavior in the model class. - - Args: - robot: The robot meta-agent to move. - """ - robot.move(robot.cell.coordinate, robot.path) - - def step(self): - """ - Advance the model by one step. - """ - for robot in self.agents_by_type["RobotAgent"]: - if robot.status == "open": # Assign a task to the robot - item = self.random.choice(self.agents_by_type["InventoryAgent"]) - if item.quantity > 0: - robot.initiate_task(item) - robot.status = "inventory" - self.central_move(robot) - else: - robot.continue_task() diff --git a/mesa/experimental/meta_agents/meta_agent.py b/mesa/experimental/meta_agents/meta_agent.py index 0eef89441b5..74b8f1cee25 100644 --- a/mesa/experimental/meta_agents/meta_agent.py +++ b/mesa/experimental/meta_agents/meta_agent.py @@ -1,6 +1,25 @@ """Implementation of Mesa's meta agent capability. -This contains four helper functions and a MetaAgent class that can be used to +Overview: Complex systems often have multiple levels of components. An +organization is not one entity, but is made of departments, sub-departments, +and people. A person is not a single entity, but it is made of micro biomes, +organs and cells. A city is not a single entity, but it is made of districts, +neighborhoods, buildings, and people. A forest comprises an ecosystem of +trees, plants, animals, and microorganisms. + +This reality is the motivation for meta-agents. It allows users to represent +these multiple levels, where each level can have agents with sub-agents. + +To demonstrate meta-agents capability there are two examples: +1 - Alliance formation which shows emergent meta-agent formation in +advanced examples: +https://github.com/projectmesa/mesa/tree/main/mesa/examples/advanced/alliance_formation +2 - Warehouse model in the Mesa example's repository +https://github.com/projectmesa/mesa-examples/tree/main/examples/warehouse + +To accomplish this the MetaAgent module is as follows: + +This contains four helper functions and a MetaAgent class that can be used to create agents that contain other agents as components. Helper methods: @@ -16,8 +35,7 @@ Meta-Agent class (MetaAgent): An agent that contains other agents as components. -See basic examples >> meta_agents for deliberate meta-agent creation (warehouse) -and emergent meta-agent creation (alliance formation) of meta-agents. +. """ import itertools @@ -128,8 +146,8 @@ def create_meta_agent( new_agent_class: str, agents: Iterable[Any], mesa_agent_type=Agent, - meta_attributes: dict[str, Any] = None, - meta_methods: dict[str, Callable] = None, # noqa B006 + meta_attributes: dict[str, Any] | None = None, + meta_methods: dict[str, Callable] | None = None, assume_subagent_methods: bool = False, assume_subagent_attributes: bool = False, ) -> Any | None: @@ -170,6 +188,9 @@ def add_methods( """ if assume_subagent_methods: agent_classes = {type(agent) for agent in agents} + if meta_methods is None: + # Initialize meta_methods if not provided + meta_methods = {} for agent_class in agent_classes: for name in agent_class.__dict__: if callable(getattr(agent_class, name)) and not name.startswith( @@ -195,6 +216,9 @@ def add_attributes( meta-agent. """ if assume_subagent_attributes: + if meta_attributes is None: + # Initialize meta_attributes if not provided + meta_attributes = {} for agent in agents: for name, value in agent.__dict__.items(): if not callable(value): diff --git a/tests/test_examples.py b/tests/test_examples.py index bea02a2d25b..6a901e49122 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -9,7 +9,6 @@ Schelling, SugarscapeG1mt, VirusOnNetwork, - WarehouseModel, WolfSheep, ) @@ -111,7 +110,7 @@ def test_wolf_sheep(): # noqa: D103 def test_alliance_formation_model(): # noqa: D103 - from mesa.examples.basic.alliance_formation import app + from mesa.examples.advanced.alliance_formation import app app.page # noqa: B018 @@ -121,15 +120,3 @@ def test_alliance_formation_model(): # noqa: D103 model.step() assert len(model.agents) == len(model.network.nodes) - - -def test_warehouse_model(): # noqa: D103 - from mesa.examples.basic.warehouse import app - - app.page # noqa: B018 - - model = WarehouseModel(seed=42) - - # More steps needed to hit coverage difference - for _i in range(30): - model.step() From d66515f6a0bb269a6280e0d9d53cfcb553ad7a79 Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Mon, 21 Apr 2025 05:11:29 -0400 Subject: [PATCH 29/31] Update Readme.md --- mesa/examples/advanced/alliance_formation/Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/examples/advanced/alliance_formation/Readme.md b/mesa/examples/advanced/alliance_formation/Readme.md index 1933294bc3f..bb04dea94d0 100644 --- a/mesa/examples/advanced/alliance_formation/Readme.md +++ b/mesa/examples/advanced/alliance_formation/Readme.md @@ -4,7 +4,7 @@ This model demonstrates Mesa's meta agent capability. -**Overview of mate agent:** Complex systems often have multiple levels of components. A city is not a single entity, but it is made of districts,neighborhoods, buildings, and people. A forest comprises an ecosystem of trees, plants, animals, and microorganisms. An organization is not one entity, but is made of departments, sub-departments, and people. A person is not a single entity, but it is made of micro biomes, organs and cells. +**Overview of meta agent:** Complex systems often have multiple levels of components. A city is not a single entity, but it is made of districts,neighborhoods, buildings, and people. A forest comprises an ecosystem of trees, plants, animals, and microorganisms. An organization is not one entity, but is made of departments, sub-departments, and people. A person is not a single entity, but it is made of micro biomes, organs and cells. This reality is the motivation for meta-agents. It allows users to represent these multiple levels, where each level can have agents with sub-agents. From d9a67b4a3afa520686447fda13de80f211669685 Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Sat, 26 Apr 2025 07:20:41 -0400 Subject: [PATCH 30/31] Remove key_by_name Update model.py Update tests Update meta_agent.py debating names --- mesa/agent.py | 11 +- .../advanced/alliance_formation/model.py | 2 + mesa/experimental/meta_agents/meta_agent.py | 180 +++++++++--------- tests/test_meta_agents.py | 68 ++++--- 4 files changed, 138 insertions(+), 123 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index eb3286e8afc..432b06b441e 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -48,23 +48,24 @@ class Agent: # so, unique_id is unique relative to a model, and counting starts from 1 _ids = defaultdict(functools.partial(itertools.count, 1)) - def __init__( - self, model: Model, *args, key_by_name: bool = False, **kwargs - ) -> None: + def __init__(self, model: Model, *args, **kwargs) -> None: """Create a new agent. Args: model (Model): The model instance in which the agent exists. - key_by_name (bool): If True, use Agent.__name__ as the key in the model's agents_by_type dictionary. args: Passed on to super. kwargs: Passed on to super. + + Notes: + to make proper use of python's super, in each class remove the arguments and + keyword arguments you need and pass on the rest to super """ super().__init__(*args, **kwargs) self.model: Model = model self.unique_id: int = next(self._ids[model]) self.pos: Position | None = None - self.model.register_agent(self, key_by_name=key_by_name) + self.model.register_agent(self) def remove(self) -> None: """Remove and delete the agent from the model. diff --git a/mesa/examples/advanced/alliance_formation/model.py b/mesa/examples/advanced/alliance_formation/model.py index bd16d5ee98b..6eaa21ab414 100644 --- a/mesa/examples/advanced/alliance_formation/model.py +++ b/mesa/examples/advanced/alliance_formation/model.py @@ -2,6 +2,7 @@ import numpy as np import mesa +from mesa import Agent from mesa.examples.advanced.alliance_formation.agents import AllianceAgent from mesa.experimental.meta_agents.meta_agent import ( create_meta_agent, @@ -165,6 +166,7 @@ def step(self): self, class_name, alliance, + Agent, meta_attributes={ "level": attributes[2], "power": attributes[0], diff --git a/mesa/experimental/meta_agents/meta_agent.py b/mesa/experimental/meta_agents/meta_agent.py index 74b8f1cee25..7a5ffe049cf 100644 --- a/mesa/experimental/meta_agents/meta_agent.py +++ b/mesa/experimental/meta_agents/meta_agent.py @@ -8,7 +8,7 @@ trees, plants, animals, and microorganisms. This reality is the motivation for meta-agents. It allows users to represent -these multiple levels, where each level can have agents with sub-agents. +these multiple levels, where each level can have agents with constituting_agents. To demonstrate meta-agents capability there are two examples: 1 - Alliance formation which shows emergent meta-agent formation in @@ -24,9 +24,9 @@ Helper methods: 1 - find_combinations: Find combinations of agents to create a meta-agent -subset. +constituting_set. 2- evaluate_combination: Evaluate combinations of agents by some user based -criteria to determine if it should be a subset of agents. +criteria to determine if it should be a constituting_set of agents. 3- extract_class: Helper function for create_meta-agent. Extracts the types of agent being created to create a new instance of that agent type. 4- create_meta_agent: Create a new meta-agent class and instantiate @@ -40,7 +40,6 @@ import itertools from collections.abc import Callable, Iterable -from concurrent.futures import ThreadPoolExecutor from types import MethodType from typing import Any @@ -95,28 +94,22 @@ def find_combinations( combinations. Defaults to None. Returns: - List[Tuple[AgentSet, float]]: The list of valuable combinations. + List[Tuple[AgentSet, float]]: The list of valuable combinations, in + a tuple first agentset of valuable combination and then the value of + the combination. """ combinations = [] - with ThreadPoolExecutor() as executor: - futures = [] - # Allow one size or range of sizes to be passed - size_range = (size, size + 1) if isinstance(size, int) else size + # Allow one size or range of sizes to be passed + size_range = (size, size + 1) if isinstance(size, int) else size - candidate_groups = itertools.chain.from_iterable( - itertools.combinations(group, size) for size in range(*size_range) + for candidate_group in itertools.chain.from_iterable( + itertools.combinations(group, size) for size in range(*size_range) + ): + group_set, result = evaluate_combination( + candidate_group, model, evaluation_func ) - for candidate_group in candidate_groups: - futures.append( - executor.submit( - evaluate_combination, candidate_group, model, evaluation_func - ) - ) - - for future in futures: - group_set, result = future.result() - if result: - combinations.append((group_set, result)) + if result: + combinations.append((group_set, result)) if len(combinations) > 0 and filter_func: filtered_combinations = filter_func(combinations) @@ -125,7 +118,7 @@ def find_combinations( return combinations -def extract_class(agents_by_type: dict, new_agent_class: str): +def extract_class(agents_by_type: dict, new_agent_class: object) -> type[Agent] | None: """Helper function for create_meta_agents extracts the types of agents. Args: @@ -133,11 +126,15 @@ def extract_class(agents_by_type: dict, new_agent_class: str): new_agent_class (str): The name of the agent class to be created Returns: - type(Agent) if it agent type exists + type(Agent) if agent type exists None otherwise """ - if new_agent_class in agents_by_type: - return type(agents_by_type[new_agent_class][0]) + agent_type_names = {} + for agent in agents_by_type: + agent_type_names[agent.__name__] = agent + + if new_agent_class in agent_type_names: + return type(agents_by_type[agent_type_names[new_agent_class]][0]) return None @@ -145,11 +142,11 @@ def create_meta_agent( model: Any, new_agent_class: str, agents: Iterable[Any], - mesa_agent_type=Agent, + mesa_agent_type: type[Agent] | None, meta_attributes: dict[str, Any] | None = None, meta_methods: dict[str, Callable] | None = None, - assume_subagent_methods: bool = False, - assume_subagent_attributes: bool = False, + assume_constituting_agent_methods: bool = False, + assume_constituting_agent_attributes: bool = False, ) -> Any | None: """Create a new meta-agent class and instantiate agents. @@ -159,21 +156,23 @@ def create_meta_agent( agents (Iterable[Any]): The agents to be included in the meta-agent. meta_attributes (Dict[str, Any]): Attributes to be added to the meta-agent. meta_methods (Dict[str, Callable]): Methods to be added to the meta-agent. - assume_subagent_methods (bool): Whether to assume methods from - sub-agents as meta_agent methods. - assume_subagent_attributes (bool): Whether to retain attributes - from sub-agents. + assume_constituting_agent_methods (bool): Whether to assume methods from + constituting_-agents as meta_agent methods. + assume_constituting_agent_attributes (bool): Whether to retain attributes + from constituting_-agents. Returns: - Optional[Any]: - - None if adding agent(s) 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 + - MetaAgent Instance """ # Convert agents to set to ensure uniqueness agents = set(agents) + # Ensure there is at least one agent base class + if not mesa_agent_type: + mesa_agent_type = (Agent,) + elif not isinstance(mesa_agent_type, tuple): + mesa_agent_type = (mesa_agent_type,) + def add_methods( meta_agent_instance: Any, agents: Iterable[Any], @@ -186,7 +185,7 @@ def add_methods( agents (Iterable[Any]): The agents to derive methods from. meta_methods (Dict[str, Callable]): methods to be added to the meta-agent. """ - if assume_subagent_methods: + if assume_constituting_agent_methods: agent_classes = {type(agent) for agent in agents} if meta_methods is None: # Initialize meta_methods if not provided @@ -215,7 +214,7 @@ def add_attributes( meta_attributes (Dict[str, Any]): Attributes to be added to the meta-agent. """ - if assume_subagent_attributes: + if assume_constituting_agent_attributes: if meta_attributes is None: # Initialize meta_attributes if not provided meta_attributes = {} @@ -229,24 +228,29 @@ def add_attributes( 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")] - if len(subagents) > 0: - if len(subagents) == 1: - add_attributes(subagents[0].meta_agent, agents, meta_attributes) - add_methods(subagents[0].meta_agent, agents, meta_methods) - subagents[0].meta_agent.add_subagents(agents) + constituting_agents = [a for a in agents if hasattr(a, "meta_agent")] + if len(constituting_agents) > 0: + if len(constituting_agents) == 1: + add_attributes(constituting_agents[0].meta_agent, agents, meta_attributes) + add_methods(constituting_agents[0].meta_agent, agents, meta_methods) + constituting_agents[0].meta_agent.add_constituting_agents(agents) + + return constituting_agents[0].meta_agent # Return the existing meta-agent else: - subagent = model.random.choice(subagents) - agents = set(agents) - set(subagents) - add_attributes(subagent.meta_agent, agents, meta_attributes) - add_methods(subagent.meta_agent, agents, meta_methods) - subagent.meta_agent.add_subagents(agents) + constituting_agent = model.random.choice(constituting_agents) + agents = set(agents) - set(constituting_agents) + add_attributes(constituting_agent.meta_agent, agents, meta_attributes) + add_methods(constituting_agent.meta_agent, agents, meta_methods) + constituting_agent.meta_agent.add_constituting_agents(agents) # TODO: Add way for user to specify how agents join meta-agent # instead of random choice + return constituting_agent.meta_agent + else: # Path 2 - Create a new instance of an existing meta-agent class agent_class = extract_class(model.agents_by_type, new_agent_class) + if agent_class: meta_agent_instance = agent_class(model, agents) add_attributes(meta_agent_instance, agents, meta_attributes) @@ -256,10 +260,10 @@ def add_attributes( # Path 3 - Create a new meta-agent class meta_agent_class = type( new_agent_class, - (MetaAgent, mesa_agent_type), # Use the meta_agent_type parameter here + (MetaAgent, *mesa_agent_type), # Inherit Mesa Agent Classes { "unique_id": None, - "_subset": None, + "_constituting_set": None, }, ) meta_agent_instance = meta_agent_class(model, agents) @@ -282,94 +286,96 @@ def __init__( include in the MetaAgent. Defaults to None. name (str, optional): The name of the MetaAgent. Defaults to "MetaAgent". """ - super().__init__(model, key_by_name=True) - self._subset = AgentSet(agents or [], random=model.random) + super().__init__(model) + self._constituting_set = AgentSet(agents or [], random=model.random) self.name = name - # Add ref to meta_agent in subagents - for agent in self._subset: + # Add ref to meta_agent in constituting_agents + for agent in self._constituting_set: agent.meta_agent = self # TODO: Make a set for meta_agents def __len__(self) -> int: """Return the number of components.""" - return len(self._subset) + return len(self._constituting_set) def __iter__(self): """Iterate over components.""" - return iter(self._subset) + return iter(self._constituting_set) def __contains__(self, agent: Agent) -> bool: """Check if an agent is a component.""" - return agent in self._subset + return agent in self._constituting_set @property def agents(self) -> AgentSet: - """Get list of Meta-Agent subagents.""" - return set(self._subset) + """Get list of Meta-Agent constituting_agents.""" + return self._constituting_set @property - def subagents_by_type(self) -> dict[type, list[Agent]]: - """Get the subagents grouped by type. + def constituting_agents_by_type(self) -> dict[type, list[Agent]]: + """Get the constituting_agents grouped by type. Returns: - dict[type, list[Agent]]: A dictionary of subagents grouped by type. + dict[type, list[Agent]]: A dictionary of constituting_agents grouped by type. """ - subagents_by_type = {} - for agent in self._subset: + constituting_agents_by_type = {} + for agent in self._constituting_set: agent_type = type(agent) - if agent_type not in subagents_by_type: - subagents_by_type[agent_type] = [] - subagents_by_type[agent_type].append(agent) - return subagents_by_type + if agent_type not in constituting_agents_by_type: + constituting_agents_by_type[agent_type] = [] + constituting_agents_by_type[agent_type].append(agent) + return constituting_agents_by_type @property - def subagent_types(self) -> set[type]: - """Get the types of all subagents. + def constituting_agent_types(self) -> set[type]: + """Get the types of all constituting_agents. Returns: - set[type]: A set of unique types of the subagents. + set[type]: A set of unique types of the constituting_agents. """ - return {type(agent) for agent in self._subset} + return {type(agent) for agent in self._constituting_set} - def get_subagent_instance(self, agent_type) -> set[type]: - """Get the instance of a subagent of the specified type. + def get_constituting_agent_instance(self, agent_type) -> set[type]: + """Get the instance of a constituting_agent of the specified type. Args: - agent_type: The type of the subagent to retrieve. + agent_type: The type of the constituting_agent to retrieve. Returns: - The first instance of the specified subagent type. + The first instance of the specified constituting_agent type. Raises: - ValueError: If no subagent of the specified type is found. + ValueError: If no constituting_agent of the specified type is found. """ try: - return self.subagents_by_type[agent_type][0] + return self.constituting_agents_by_type[agent_type][0] except KeyError: - raise ValueError(f"No subagent of type {agent_type} found.") from None + raise ValueError( + f"No constituting_agent of type {agent_type} found." + ) from None - def add_subagents( + def add_constituting_agents( self, new_agents: set[Agent], ): """Add agents as components. Args: - new_agents (set[Agent]): The agents to add to MetaAgent subset. + new_agents (set[Agent]): The agents to add to MetaAgent constituting_set. """ for agent in new_agents: - self._subset.add(agent) + self._constituting_set.add(agent) agent.meta_agent = self # TODO: Make a set for meta_agents self.model.register_agent(agent) - def remove_subagents(self, remove_agents: set[Agent]): + def remove_constituting_agents(self, remove_agents: set[Agent]): """Remove agents as components. Args: remove_agents (set[Agent]): The agents to remove from MetaAgent. """ for agent in remove_agents: - self._subset.discard(agent) + self._constituting_set.discard(agent) agent.meta_agent = None # TODO: Remove meta_agent from set self.model.deregister_agent(agent) diff --git a/tests/test_meta_agents.py b/tests/test_meta_agents.py index 388ca4e5724..564c669979c 100644 --- a/tests/test_meta_agents.py +++ b/tests/test_meta_agents.py @@ -52,9 +52,10 @@ def test_create_meta_agent_new_class(setup_agents): model, "MetaAgentClass", agents, + Agent, meta_attributes={"attribute1": "value1"}, meta_methods={"function1": lambda self: "function1"}, - assume_subagent_attributes=True, + assume_constituting_agent_attributes=True, ) assert meta_agent is not None assert meta_agent.attribute1 == "value1" @@ -77,6 +78,7 @@ def test_create_meta_agent_existing_class(setup_agents): model, "MetaAgentClass", [agents[0], agents[2]], + Agent, meta_attributes={"attribute1": "value1"}, meta_methods={"function1": lambda self: "function1"}, ) @@ -86,9 +88,10 @@ def test_create_meta_agent_existing_class(setup_agents): model, "MetaAgentClass", [agents[1], agents[3]], + Agent, meta_attributes={"attribute2": "value2"}, meta_methods={"function2": lambda self: "function2"}, - assume_subagent_attributes=True, + assume_constituting_agent_attributes=True, ) assert meta_agent is not None assert meta_agent2.attribute2 == "value2" @@ -112,17 +115,19 @@ def test_add_agents_to_existing_meta_agent(setup_agents): model, "MetaAgentClass", [agents[0], agents[3]], + Agent, meta_attributes={"attribute1": "value1"}, meta_methods={"function1": lambda self: "function1"}, - assume_subagent_attributes=True, + assume_constituting_agent_attributes=True, ) create_meta_agent( model, "MetaAgentClass", [agents[1], agents[0], agents[2]], - assume_subagent_attributes=True, - assume_subagent_methods=True, + Agent, + assume_constituting_agent_attributes=True, + assume_constituting_agent_methods=True, ) assert meta_agent1.agents == {agents[0], agents[1], agents[2], agents[3]} assert meta_agent1.function1() == "function1" @@ -145,10 +150,11 @@ def test_meta_agent_integration(setup_agents): model, "MetaAgentClass", agents, + Agent, meta_attributes={"attribute1": "value1"}, meta_methods={"function1": lambda self: "function1"}, - assume_subagent_attributes=True, - assume_subagent_methods=True, + assume_constituting_agent_attributes=True, + assume_constituting_agent_methods=True, ) model.step() @@ -224,7 +230,7 @@ def test_meta_agent_iter(setup_agents): """ model, agents = setup_agents meta_agent = MetaAgent(model, set(agents)) - assert list(iter(meta_agent)) == list(meta_agent._subset) + assert list(iter(meta_agent)) == list(meta_agent._constituting_set) def test_meta_agent_contains(setup_agents): @@ -239,58 +245,58 @@ def test_meta_agent_contains(setup_agents): assert agent in meta_agent -def test_meta_agent_add_subagents(setup_agents): - """Test the add_subagents method of MetaAgent. +def test_meta_agent_add_constituting_agents(setup_agents): + """Test the add_constituting_agents method of MetaAgent. Args: setup_agents (tuple): The model and agents fixture. """ model, agents = setup_agents meta_agent = MetaAgent(model, {agents[0], agents[1]}) - meta_agent.add_subagents({agents[2], agents[3]}) - assert meta_agent._subset == set(agents) + meta_agent.add_constituting_agents({agents[2], agents[3]}) + assert meta_agent._constituting_set == set(agents) -def test_meta_agent_remove_subagents(setup_agents): - """Test the remove_subagents method of MetaAgent. +def test_meta_agent_remove_constituting_agents(setup_agents): + """Test the remove_constituting_agents method of MetaAgent. Args: setup_agents (tuple): The model and agents fixture. """ model, agents = setup_agents meta_agent = MetaAgent(model, set(agents)) - meta_agent.remove_subagents({agents[2], agents[3]}) - assert meta_agent._subset == {agents[0], agents[1]} + meta_agent.remove_constituting_agents({agents[2], agents[3]}) + assert meta_agent._constituting_set == {agents[0], agents[1]} -def test_meta_agent_subagents_by_type(setup_agents): - """Test the subagents_by_type property of MetaAgent.""" +def test_meta_agent_constituting_agents_by_type(setup_agents): + """Test the constituting_agents_by_type property of MetaAgent.""" model, agents = setup_agents meta_agent = MetaAgent(model, set(agents)) - subagents_by_type = meta_agent.subagents_by_type - assert isinstance(subagents_by_type, dict) - for agent_type, agent_list in subagents_by_type.items(): + constituting_agents_by_type = meta_agent.constituting_agents_by_type + assert isinstance(constituting_agents_by_type, dict) + for agent_type, agent_list in constituting_agents_by_type.items(): assert all(isinstance(agent, agent_type) for agent in agent_list) -def test_meta_agent_subagent_types(setup_agents): - """Test the subagent_types property of MetaAgent.""" +def test_meta_agent_constituting_agent_types(setup_agents): + """Test the constituting_agent_types property of MetaAgent.""" model, agents = setup_agents meta_agent = MetaAgent(model, set(agents)) - subagent_types = meta_agent.subagent_types - assert isinstance(subagent_types, set) - assert all(isinstance(agent_type, type) for agent_type in subagent_types) + constituting_agent_types = meta_agent.constituting_agent_types + assert isinstance(constituting_agent_types, set) + assert all(isinstance(agent_type, type) for agent_type in constituting_agent_types) -def test_meta_agent_get_subagent_instance(setup_agents): - """Test the get_subagent_instance method of MetaAgent.""" +def test_meta_agent_get_constituting_agent_instance(setup_agents): + """Test the get_constituting_agent_instance method of MetaAgent.""" model, agents = setup_agents meta_agent = MetaAgent(model, set(agents)) agent_type = type(agents[0]) - subagent_instance = meta_agent.get_subagent_instance(agent_type) - assert isinstance(subagent_instance, agent_type) + constituting_agent_instance = meta_agent.get_constituting_agent_instance(agent_type) + assert isinstance(constituting_agent_instance, agent_type) with pytest.raises(ValueError): - meta_agent.get_subagent_instance(str) # Invalid type + meta_agent.get_constituting_agent_instance(str) # Invalid type def test_meta_agent_step(setup_agents): From 70a4abf6ebc8440ff7dc3a874f84354b6d8fcc01 Mon Sep 17 00:00:00 2001 From: Tom Pike Date: Sun, 27 Apr 2025 13:57:00 -0400 Subject: [PATCH 31/31] Update model.py Remove all traces of key_by_name --- mesa/model.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/mesa/model.py b/mesa/model.py index 95e079431a3..f53d92b1633 100644 --- a/mesa/model.py +++ b/mesa/model.py @@ -144,20 +144,25 @@ def agents_by_type(self) -> dict[type[Agent], AgentSet]: """A dictionary where the keys are agent types and the values are the corresponding AgentSets.""" return self._agents_by_type - def register_agent(self, agent, key_by_name: bool = False): + def register_agent(self, agent): """Register the agent with the model. Args: agent: The agent to register. - key_by_name (bool): If True, use Agent.__name__ as the key in the agents_by_type dictionary. + + Notes: + This method is called automatically by ``Agent.__init__``, so there + is no need to use this if you are subclassing Agent and calling its + super in the ``__init__`` method. """ self._agents[agent] = None - key = agent.__class__.__name__ if key_by_name else type(agent) + # because AgentSet requires model, we cannot use defaultdict + # tricks with a function won't work because model then cannot be pickled try: - self._agents_by_type[key].add(agent) + self._agents_by_type[type(agent)].add(agent) except KeyError: - self._agents_by_type[key] = AgentSet( + self._agents_by_type[type(agent)] = AgentSet( [ agent, ], @@ -169,17 +174,18 @@ def register_agent(self, agent, key_by_name: bool = False): f"registered {agent.__class__.__name__} with agent_id {agent.unique_id}" ) - def deregister_agent(self, agent, key_by_name: bool = False): + def deregister_agent(self, agent): """Deregister the agent with the model. Args: agent: The agent to deregister. - key_by_name (bool): If True, use Agent.__name__ as the key in the agents_by_type dictionary. + + Notes: + This method is called automatically by ``Agent.remove`` + """ del self._agents[agent] - - key = agent.__class__.__name__ if key_by_name else type(agent) - self._agents_by_type[key].remove(agent) + self._agents_by_type[type(agent)].remove(agent) self._all_agents.remove(agent) _mesa_logger.debug(f"deregistered agent with agent_id {agent.unique_id}")