Skip to content

Commit

Permalink
track unique_id automatically (#2260)
Browse files Browse the repository at this point in the history
This PR ensures that the unique_id in Agent is assigned automatically. It draws on work done in #2226. In short, this PR makes 3 changes:

Agent.unique_id is assigned automatically and no longer used as an argument to Agent. A deprecation warning is issued if the Agent is instantiated with 2 arguments (unique_id and model). The value passed for unique_id is ignored in favor of the new system. This last point is in line with Automatically assign unique_id to Agents #2226.
Model.next_id() raises a deprecation warning and always return 0.
unique_id is unique relative to a Model instance. Internally, Agent has a class-level attribute _ids. This attribute is a dictionary with the Model instance as key and the "generator" for unique_id as value.
All unit tests and benchmarks work with the new structure. They raise no deprecation warnings.

examples, tutorials, etc., still need to be updated to be consistent with this change.
  • Loading branch information
quaquel authored Sep 4, 2024
1 parent cfa2635 commit 46a8752
Show file tree
Hide file tree
Showing 17 changed files with 163 additions and 158 deletions.
8 changes: 4 additions & 4 deletions benchmarks/BoltzmannWealth/boltzmann_wealth.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def __init__(self, seed=None, n=100, width=10, height=10):
model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"}
)
# Create agents
for i in range(self.num_agents):
a = MoneyAgent(i, self)
for _ in range(self.num_agents):
a = MoneyAgent(self)
# Add the agent to a random grid cell
x = self.random.randrange(self.grid.width)
y = self.random.randrange(self.grid.height)
Expand All @@ -50,8 +50,8 @@ def run_model(self, n):
class MoneyAgent(mesa.Agent):
"""An agent with fixed initial wealth."""

def __init__(self, unique_id, model):
super().__init__(unique_id, model)
def __init__(self, model):
super().__init__(model)
self.wealth = 1

def move(self):
Expand Down
7 changes: 2 additions & 5 deletions benchmarks/Flocking/flocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class Boid(mesa.Agent):

def __init__(
self,
unique_id,
model,
speed,
direction,
Expand All @@ -41,7 +40,6 @@ def __init__(
Create a new Boid flocker agent.
Args:
unique_id: Unique agent identifier.
speed: Distance to move per step.
direction: numpy vector for the Boid's direction of movement.
vision: Radius to look around for nearby Boids.
Expand All @@ -51,7 +49,7 @@ def __init__(
match: the relative importance of matching neighbors' directions
"""
super().__init__(unique_id, model)
super().__init__(model)
self.speed = speed
self.direction = direction
self.vision = vision
Expand Down Expand Up @@ -131,13 +129,12 @@ def __init__(
"match": match,
}

for i in range(self.population):
for _ in range(self.population):
x = self.random.random() * self.space.x_max
y = self.random.random() * self.space.y_max
pos = np.array((x, y))
direction = np.random.random(2) * 2 - 1
boid = Boid(
unique_id=i,
model=self,
speed=speed,
direction=direction,
Expand Down
9 changes: 3 additions & 6 deletions benchmarks/Schelling/schelling.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ class SchellingAgent(CellAgent):
Schelling segregation agent
"""

def __init__(self, unique_id, model, agent_type, radius, homophily):
def __init__(self, model, agent_type, radius, homophily):
"""
Create a new Schelling agent.
Args:
unique_id: Unique identifier for the agent.
x, y: Agent initial location.
agent_type: Indicator for the agent's type (minority=1, majority=0)
"""
super().__init__(unique_id, model)
super().__init__(model)
self.type = agent_type
self.radius = radius
self.homophily = homophily
Expand Down Expand Up @@ -81,9 +80,7 @@ def __init__(
for cell in self.grid:
if self.random.random() < density:
agent_type = 1 if self.random.random() < self.minority_pc else 0
agent = SchellingAgent(
self.next_id(), self, agent_type, radius, homophily
)
agent = SchellingAgent(self, agent_type, radius, homophily)
agent.move_to(cell)
self.schedule.add(agent)

Expand Down
15 changes: 5 additions & 10 deletions benchmarks/WolfSheep/wolf_sheep.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@


class Animal(CellAgent):
def __init__(self, unique_id, model, energy, p_reproduce, energy_from_food):
super().__init__(unique_id, model)
def __init__(self, model, energy, p_reproduce, energy_from_food):
super().__init__(model)
self.energy = energy
self.p_reproduce = p_reproduce
self.energy_from_food = energy_from_food
Expand All @@ -29,7 +29,6 @@ def random_move(self):
def spawn_offspring(self):
self.energy /= 2
offspring = self.__class__(
self.model.next_id(),
self.model,
self.energy,
self.p_reproduce,
Expand Down Expand Up @@ -107,7 +106,7 @@ def fully_grown(self, value: bool) -> None:
function_args=[self, "fully_grown", True],
)

def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time):
def __init__(self, model, fully_grown, countdown, grass_regrowth_time):
"""
TODO:: fully grown can just be an int --> so one less param (i.e. countdown)
Expand All @@ -119,7 +118,7 @@ def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time
grass_regrowth_time : time to fully regrow grass
countdown : Time for the patch of grass to be fully regrown if fully grown is False
"""
super().__init__(unique_id, model)
super().__init__(model)
self._fully_grown = fully_grown
self.grass_regrowth_time = grass_regrowth_time

Expand Down Expand Up @@ -189,7 +188,6 @@ def __init__(
)
energy = self.random.randrange(2 * sheep_gain_from_food)
sheep = Sheep(
self.next_id(),
self,
energy,
sheep_reproduce,
Expand All @@ -205,7 +203,6 @@ def __init__(
)
energy = self.random.randrange(2 * wolf_gain_from_food)
wolf = Wolf(
self.next_id(),
self,
energy,
wolf_reproduce,
Expand All @@ -221,9 +218,7 @@ def __init__(
countdown = grass_regrowth_time
else:
countdown = self.random.randrange(grass_regrowth_time)
patch = GrassPatch(
self.next_id(), self, fully_grown, countdown, grass_regrowth_time
)
patch = GrassPatch(self, fully_grown, countdown, grass_regrowth_time)
patch.move_to(cell)

def step(self):
Expand Down
4 changes: 2 additions & 2 deletions docs/tutorials/MoneyModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ def compute_gini(model):
class MoneyAgent(mesa.Agent):
"""An agent with fixed initial wealth."""

def __init__(self, unique_id, model):
super().__init__(unique_id, model)
def __init__(self, model):
super().__init__(model)
self.wealth = 1

def move(self):
Expand Down
42 changes: 36 additions & 6 deletions mesa/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import contextlib
import copy
import functools
import itertools
import operator
import warnings
import weakref
Expand All @@ -32,21 +34,49 @@ class Agent:
Base class for a model agent in Mesa.
Attributes:
unique_id (int): A unique identifier for this agent.
model (Model): A reference to the model instance.
self.pos: Position | None = None
unique_id (int): A unique identifier for this agent.
pos (Position): A reference to the position where this agent is located.
Notes:
unique_id is unique relative to a model instance and starts from 1
"""

def __init__(self, unique_id: int, model: Model) -> None:
# this is a class level attribute
# it is a dictionary, indexed by model instance
# so, unique_id is unique relative to a model, and counting starts from 1
_ids = defaultdict(functools.partial(itertools.count, 1))

def __init__(self, *args, **kwargs) -> 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.
"""
self.unique_id = unique_id
self.model = model
# TODO: Cleanup in future Mesa version (3.1+)
match args:
# Case 1: Only the model is provided. The new correct behavior.
case [model]:
self.model = model
self.unique_id = next(self._ids[model])
# Case 2: Both unique_id and model are provided, deprecated
case [_, model]:
warnings.warn(
"unique ids are assigned automatically to Agents in Mesa 3. The use of custom unique_id is "
"deprecated. Only input a model when calling `super()__init__(model)`. The unique_id inputted is not used.",
DeprecationWarning,
stacklevel=2,
)
self.model = model
self.unique_id = next(self._ids[model])
# Case 3: Anything else, raise an error
case _:
raise ValueError(
"Invalid arguments provided to initialize the Agent. Only input a model: `super()__init__(model)`."
)

self.pos: Position | None = None

self.model.register_agent(self)
Expand Down
4 changes: 2 additions & 2 deletions mesa/experimental/cell_space/cell_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ class CellAgent(Agent):
cell: (Cell | None): the cell which the agent occupies
"""

def __init__(self, unique_id: int, model: Model) -> None:
def __init__(self, model: Model) -> 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.
"""
super().__init__(unique_id, model)
super().__init__(model)
self.cell: Cell | None = None

def move_to(self, cell) -> None:
Expand Down
16 changes: 6 additions & 10 deletions mesa/experimental/devs/examples/epstein_civil_violence.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@


class EpsteinAgent(Agent):
def __init__(self, unique_id, model, vision, movement):
super().__init__(unique_id, model)
def __init__(self, model, vision, movement):
super().__init__(model)
self.vision = vision
self.movement = movement

Expand Down Expand Up @@ -46,7 +46,6 @@ class Citizen(EpsteinAgent):

def __init__(
self,
unique_id,
model,
vision,
movement,
Expand All @@ -59,7 +58,6 @@ def __init__(
"""
Create a new Citizen.
Args:
unique_id: unique int
model : model instance
hardship: Agent's 'perceived hardship (i.e., physical or economic
privation).' Exogenous, drawn from U(0,1).
Expand All @@ -71,7 +69,7 @@ def __init__(
vision: number of cells in each direction (N, S, E and W) that
agent can inspect. Exogenous.
"""
super().__init__(unique_id, model, vision, movement)
super().__init__(model, vision, movement)
self.hardship = hardship
self.regime_legitimacy = regime_legitimacy
self.risk_aversion = risk_aversion
Expand Down Expand Up @@ -144,8 +142,8 @@ class Cop(EpsteinAgent):
able to inspect
"""

def __init__(self, unique_id, model, vision, movement, max_jail_term):
super().__init__(unique_id, model, vision, movement)
def __init__(self, model, vision, movement, max_jail_term):
super().__init__(model, vision, movement)
self.max_jail_term = max_jail_term

def step(self):
Expand Down Expand Up @@ -236,15 +234,13 @@ def __init__(
for _, pos in self.grid.coord_iter():
if self.random.random() < self.cop_density:
agent = Cop(
self.next_id(),
self,
cop_vision,
movement,
max_jail_term,
)
elif self.random.random() < (self.cop_density + self.citizen_density):
agent = Citizen(
self.next_id(),
self,
citizen_vision,
movement,
Expand All @@ -270,4 +266,4 @@ def step(self):

simulator.setup(model)

simulator.run(time_delta=100)
simulator.run_for(time_delta=100)
15 changes: 5 additions & 10 deletions mesa/experimental/devs/examples/wolf_sheep.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@


class Animal(mesa.Agent):
def __init__(self, unique_id, model, moore, energy, p_reproduce, energy_from_food):
super().__init__(unique_id, model)
def __init__(self, model, moore, energy, p_reproduce, energy_from_food):
super().__init__(model)
self.energy = energy
self.p_reproduce = p_reproduce
self.energy_from_food = energy_from_food
Expand All @@ -30,7 +30,6 @@ def random_move(self):
def spawn_offspring(self):
self.energy /= 2
offspring = self.__class__(
self.model.next_id(),
self.model,
self.moore,
self.energy,
Expand Down Expand Up @@ -109,15 +108,15 @@ def fully_grown(self, value: bool):
function_args=[self, "fully_grown", True],
)

def __init__(self, unique_id, model, fully_grown, countdown, grass_regrowth_time):
def __init__(self, model, fully_grown, countdown, grass_regrowth_time):
"""
Creates a new patch of grass
Args:
grown: (boolean) Whether the patch of grass is fully grown or not
countdown: Time for the patch of grass to be fully grown again
"""
super().__init__(unique_id, model)
super().__init__(model)
self._fully_grown = fully_grown
self.grass_regrowth_time = grass_regrowth_time

Expand Down Expand Up @@ -191,7 +190,6 @@ def __init__(
)
energy = self.random.randrange(2 * sheep_gain_from_food)
sheep = Sheep(
self.next_id(),
self,
moore,
energy,
Expand All @@ -208,7 +206,6 @@ def __init__(
)
energy = self.random.randrange(2 * wolf_gain_from_food)
wolf = Wolf(
self.next_id(),
self,
moore,
energy,
Expand All @@ -225,9 +222,7 @@ def __init__(
countdown = grass_regrowth_time
else:
countdown = self.random.randrange(grass_regrowth_time)
patch = GrassPatch(
self.next_id(), self, fully_grown, countdown, grass_regrowth_time
)
patch = GrassPatch(self, fully_grown, countdown, grass_regrowth_time)
self.grid.place_agent(patch, pos)

def step(self):
Expand Down
Loading

0 comments on commit 46a8752

Please sign in to comment.