Skip to content

Commit

Permalink
Update Agent and Model to increment automatically
Browse files Browse the repository at this point in the history
Co-Authored-By: HarshaNP <96897754+gittyharsha@users.noreply.github.com>
  • Loading branch information
2 people authored and EwoutH committed Aug 29, 2024
1 parent 3cf1b76 commit a49ed23
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 20 deletions.
36 changes: 32 additions & 4 deletions mesa/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 16 additions & 3 deletions mesa/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
44 changes: 42 additions & 2 deletions tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
19 changes: 10 additions & 9 deletions tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit a49ed23

Please sign in to comment.