From a49ed235ee86eee903f5963c03f819ad72fc703f Mon Sep 17 00:00:00 2001 From: GittyHarsha Date: Tue, 20 Aug 2024 08:19:40 +0530 Subject: [PATCH] Update Agent and Model to increment automatically Co-Authored-By: HarshaNP <96897754+gittyharsha@users.noreply.github.com> --- mesa/agent.py | 36 ++++++++++++++++++++++++++++++++---- mesa/model.py | 19 ++++++++++++++++--- tests/test_agent.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- tests/test_model.py | 2 -- tests/test_time.py | 19 ++++++++++--------- 5 files changed, 100 insertions(+), 20 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index e65b25f69c2..88a0f31208a 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -11,6 +11,7 @@ import contextlib import copy import operator +import warnings import weakref from collections.abc import Callable, Iterable, Iterator, MutableSet, Sequence from random import Random @@ -35,16 +36,43 @@ class Agent: self.pos: Position | None = None """ - def __init__(self, unique_id: int, model: Model) -> None: + def __init__( + self, + model: Model = None, + fallback: None = None, + ) -> None: """ Create a new agent. Args: - unique_id (int): A unique identifier for this agent. model (Model): The model instance in which the agent exists. + fallback (None): A fallback parameter for the old way of initializing agents. Deprecated. + + Note: + Previously, agents were initialized with a unique_id and a model instance. The unique_id is now + automatically assigned, so only the model instance is now required (as the first argument). """ - self.unique_id = unique_id - self.model = model + from .model import Model # avoid circular import + + # If the first argument is a model, that's the correct new way to initialize the agent. + if isinstance(model, Model): + self.model = model + else: + # If the fallback is a Mesa model, assign that to the model attribute + if isinstance(fallback, Model): + self.model = fallback + warnings.warn( + "The unique_id parameter is now generated automatically and doesn't need to be passed in Agent() anymore.\n" + "Please initialize agents with the model instance only: Agent(model, ...), instead of Agent(unique_id, model, ...)", + DeprecationWarning, + stacklevel=2, + ) + else: + raise ValueError( + "The model parameter is required to initialize an Agent object. Initialize the agent with Agent(model, ...)." + ) + + self.unique_id = self.model._next_id self.pos: Position | None = None # register agent diff --git a/mesa/model.py b/mesa/model.py index d998e112f26..1828bfcd801 100644 --- a/mesa/model.py +++ b/mesa/model.py @@ -123,10 +123,23 @@ def _advance_time(self, deltat: TimeT = 1): self._steps += 1 self._time += deltat - def next_id(self) -> int: - """Return the next unique ID for agents, increment current_id""" + @property + def _next_id(self) -> int: + """Return the next unique ID for agents. Starts at 0.""" + return_id = self.current_id self.current_id += 1 - return self.current_id + return return_id + + def next_id(self) -> int: + """Old version of next_id, kept for compatibility.""" + warnings.warn( + "Model.next_id() is deprecated and will be removed in a future version.\n" + "The Agent.unique_id parameter is now generated automatically and doesn't need to be passed in Agent() anymore.\n" + "Please initialize agents with the model instance only: Agent(model, ...), instead of Agent(unique_id, model, ...)", + DeprecationWarning, + stacklevel=2, + ) + return 0 # This is a dummy return value, but some functions might expect an int. It isn't used in agent creation anyway. def reset_randomizer(self, seed: int | None = None) -> None: """Reset the model random number generator. diff --git a/tests/test_agent.py b/tests/test_agent.py index f4e64ce5338..068397b2593 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -11,6 +11,46 @@ def get_unique_identifier(self): return self.unique_id +class PositionalAgent(Agent): + """Old behavior of Agent creation with unique_id and model as positional arguments. + Can be removed in the future.""" + + def __init__(self, unique_id, model): + super().__init__(unique_id, model) + + +class NewAgent(Agent): + def __init__(self, model, some_other, arguments=1): + super().__init__(model) + self.some_other = some_other + self.arguments = arguments + + +@pytest.fixture +def model(): + """Fixture to create a Model instance.""" + model = Model() + return model + + +def test_creation_with_positional_arguments(model): + """Old behavior of Agent creation with unique_id and model as positional arguments. + Can be removed/updated in the future.""" + agent = PositionalAgent(1, model) + assert isinstance(agent.unique_id, int) + assert agent.model == model + assert isinstance(agent.model, Model) + + +def test_creation_with_new_arguments(model): + """New behavior of Agent creation with model as the first argument and additional arguments.""" + agent = NewAgent(model, "some_other", 2) + assert agent.model == model + assert isinstance(agent.model, Model) + assert agent.some_other == "some_other" + assert agent.arguments == 2 + + class TestAgentDo(Agent): def __init__( self, @@ -45,7 +85,7 @@ def test_agent_removal(): def test_agentset(): # create agentset model = Model() - agents = [TestAgent(model.next_id(), model) for _ in range(10)] + agents = [TestAgent(model) for _ in range(10)] agentset = AgentSet(agents, model) @@ -57,7 +97,7 @@ def test_agentset(): assert a1 == a2 def test_function(agent): - return agent.unique_id > 5 + return agent.unique_id >= 5 assert len(agentset.select(test_function)) == 5 assert len(agentset.select(test_function, n=2)) == 2 diff --git a/tests/test_model.py b/tests/test_model.py index 874d45f935f..cfb236bfed9 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -7,8 +7,6 @@ def test_model_set_up(): assert model.running is True assert model.schedule is None assert model.current_id == 0 - assert model.current_id + 1 == model.next_id() - assert model.current_id == 1 model.step() diff --git a/tests/test_time.py b/tests/test_time.py index a9d8e2e6055..13b781f9876 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -25,8 +25,9 @@ class MockAgent(Agent): Minimalistic agent for testing purposes. """ - def __init__(self, unique_id, model): - super().__init__(unique_id, model) + def __init__(self, model, name=""): + super().__init__(model) + self.name = name self.steps = 0 self.advances = 0 @@ -38,10 +39,10 @@ def kill_other_agent(self): def stage_one(self): if self.model.enable_kill_other_agent: self.kill_other_agent() - self.model.log.append(self.unique_id + "_1") + self.model.log.append(f"{self.name}_1") def stage_two(self): - self.model.log.append(self.unique_id + "_2") + self.model.log.append(f"{self.name}_2") def advance(self): self.advances += 1 @@ -50,7 +51,7 @@ def step(self): if self.model.enable_kill_other_agent: self.kill_other_agent() self.steps += 1 - self.model.log.append(self.unique_id) + self.model.log.append(f"{self.name}") class MockModel(Model): @@ -90,7 +91,7 @@ def __init__(self, shuffle=False, activation=STAGED, enable_kill_other_agent=Fal # Make agents for name in ["A", "B"]: - agent = MockAgent(name, self) + agent = MockAgent(self, name) self.schedule.add(agent) def step(self): @@ -168,7 +169,7 @@ class TestRandomActivation(TestCase): def test_init(self): model = Model() - agents = [MockAgent(model.next_id(), model) for _ in range(10)] + agents = [MockAgent(model) for _ in range(10)] scheduler = RandomActivation(model, agents) assert all(agent in scheduler.agents for agent in agents) @@ -227,7 +228,7 @@ def test_not_sequential(self): model = MockModel(activation=RANDOM) # Create 10 agents for _ in range(10): - model.schedule.add(MockAgent(model.next_id(), model)) + model.schedule.add(MockAgent(model)) # Run 3 steps for _ in range(3): model.step() @@ -273,7 +274,7 @@ class TestRandomActivationByType(TestCase): def test_init(self): model = Model() - agents = [MockAgent(model.next_id(), model) for _ in range(10)] + agents = [MockAgent(model) for _ in range(10)] agents += [Agent(model.next_id(), model) for _ in range(10)] scheduler = RandomActivationByType(model, agents)