Skip to content

Commit de01c3f

Browse files
authored
Performance enhancements for Model.agents (#2251)
This PR is a performance enhancement for Model.agents. It emerged from a discussion on [the weird scaling performance of the Boltzman wealth model](#2224). # Key changes model.agents now returns the agentset as maintained by the model, rather than a new copy based on the hard references agent registration and deregistration have been moved from the Agent into the model. The agent now calls model.register and model.deregister. This encapsulates everything cleanly inside the model class and makes Agent less dependent on the inner details of how Model manages the hard references to agents the setup of the relevant datastructures is moved into its own helper method, again, this cleans up code.
1 parent de58ddb commit de01c3f

File tree

2 files changed

+86
-28
lines changed

2 files changed

+86
-28
lines changed

mesa/agent.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,12 @@ def __init__(self, unique_id: int, model: Model) -> None:
4949
self.model = model
5050
self.pos: Position | None = None
5151

52-
# register agent
53-
try:
54-
self.model.agents_[type(self)][self] = None
55-
except AttributeError as err:
56-
# model super has not been called
57-
raise RuntimeError(
58-
"The Mesa Model class was not initialized. You must explicitly initialize the Model by calling super().__init__() on initialization."
59-
) from err
52+
self.model.register_agent(self)
6053

6154
def remove(self) -> None:
6255
"""Remove and delete the agent from the model."""
6356
with contextlib.suppress(KeyError):
64-
self.model.agents_[type(self)].pop(self)
57+
self.model.deregister_agent(self)
6558

6659
def step(self) -> None:
6760
"""A single step of the agent."""

mesa/model.py

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
# Remove this __future__ import once the oldest supported Python is 3.10
99
from __future__ import annotations
1010

11-
import itertools
1211
import random
13-
from collections import defaultdict
12+
import warnings
1413

1514
# mypy
1615
from typing import Any
@@ -32,11 +31,9 @@ class Model:
3231
running: A boolean indicating if the model should continue running.
3332
schedule: An object to manage the order and execution of agent steps.
3433
current_id: A counter for assigning unique IDs to agents.
35-
agents_: A defaultdict mapping each agent type to a dict of its instances.
36-
This private attribute is used internally to manage agents.
3734
3835
Properties:
39-
agents: An AgentSet containing all agents in the model, generated from the _agents attribute.
36+
agents: An AgentSet containing all agents in the model
4037
agent_types: A list of different agent types present in the model.
4138
4239
Methods:
@@ -46,6 +43,14 @@ class Model:
4643
next_id: Generates and returns the next unique identifier for an agent.
4744
reset_randomizer: Resets the model's random number generator with a new or existing seed.
4845
initialize_data_collector: Sets up the data collector for the model, requiring an initialized scheduler and agents.
46+
register_agent : register an agent with the model
47+
deregister_agent : remove an agent from the model
48+
49+
Notes:
50+
Model.agents returns the AgentSet containing all agents registered with the model. Changing
51+
the content of the AgentSet directly can result in strange behavior. If you want change the
52+
composition of this AgentSet, ensure you operate on a copy.
53+
4954
"""
5055

5156
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
@@ -57,6 +62,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Any:
5762
# advance.
5863
obj._seed = random.random()
5964
obj.random = random.Random(obj._seed)
65+
6066
# TODO: Remove these 2 lines just before Mesa 3.0
6167
obj._steps = 0
6268
obj._time = 0
@@ -71,39 +77,98 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
7177
self.running = True
7278
self.schedule = None
7379
self.current_id = 0
74-
self.agents_: defaultdict[type, dict] = defaultdict(dict)
80+
81+
self._setup_agent_registration()
7582

7683
self._steps: int = 0
7784
self._time: TimeT = 0 # the model's clock
7885

7986
@property
8087
def agents(self) -> AgentSet:
8188
"""Provides an AgentSet of all agents in the model, combining agents from all types."""
82-
83-
if hasattr(self, "_agents"):
84-
return self._agents
85-
else:
86-
all_agents = itertools.chain.from_iterable(self.agents_.values())
87-
return AgentSet(all_agents, self)
89+
return self._all_agents
8890

8991
@agents.setter
9092
def agents(self, agents: Any) -> None:
9193
raise AttributeError(
92-
"You are trying to set model.agents. In Mesa 3.0 and higher, this attribute will be "
94+
"You are trying to set model.agents. In Mesa 3.0 and higher, this attribute is "
9395
"used by Mesa itself, so you cannot use it directly anymore."
9496
"Please adjust your code to use a different attribute name for custom agent storage."
9597
)
9698

97-
self._agents = agents
98-
9999
@property
100100
def agent_types(self) -> list[type]:
101-
"""Return a list of different agent types."""
102-
return list(self.agents_.keys())
101+
"""Return a list of all unique agent types registered with the model."""
102+
return list(self._agents_by_type.keys())
103103

104104
def get_agents_of_type(self, agenttype: type[Agent]) -> AgentSet:
105-
"""Retrieves an AgentSet containing all agents of the specified type."""
106-
return AgentSet(self.agents_[agenttype].keys(), self)
105+
"""Retrieves an AgentSet containing all agents of the specified type.
106+
107+
Args:
108+
agenttype: The type of agent to retrieve.
109+
110+
Raises:
111+
KeyError: If agenttype does not exist
112+
113+
114+
"""
115+
return self._agents_by_type[agenttype]
116+
117+
def _setup_agent_registration(self):
118+
"""helper method to initialize the agent registration datastructures"""
119+
self._agents = {} # the hard references to all agents in the model
120+
self._agents_by_type: dict[
121+
type, AgentSet
122+
] = {} # a dict with an agentset for each class of agents
123+
self._all_agents = AgentSet([], self) # an agenset with all agents
124+
125+
def register_agent(self, agent):
126+
"""Register the agent with the model
127+
128+
Args:
129+
agent: The agent to register.
130+
131+
Notes:
132+
This method is called automatically by ``Agent.__init__``, so there is no need to use this
133+
if you are subclassing Agent and calling its super in the ``__init__`` method.
134+
135+
"""
136+
if not hasattr(self, "_agents"):
137+
self._setup_agent_registration()
138+
139+
warnings.warn(
140+
"The Mesa Model class was not initialized. In the future, you need to explicitly initialize "
141+
"the Model by calling super().__init__() on initialization.",
142+
FutureWarning,
143+
stacklevel=2,
144+
)
145+
146+
self._agents[agent] = None
147+
148+
# because AgentSet requires model, we cannot use defaultdict
149+
# tricks with a function won't work because model then cannot be pickled
150+
try:
151+
self._agents_by_type[type(agent)].add(agent)
152+
except KeyError:
153+
self._agents_by_type[type(agent)] = AgentSet(
154+
[
155+
agent,
156+
],
157+
self,
158+
)
159+
160+
self._all_agents.add(agent)
161+
162+
def deregister_agent(self, agent):
163+
"""Deregister the agent with the model
164+
165+
Notes::
166+
This method is called automatically by ``Agent.remove``
167+
168+
"""
169+
del self._agents[agent]
170+
self._agents_by_type[type(agent)].remove(agent)
171+
self._all_agents.remove(agent)
107172

108173
def run_model(self) -> None:
109174
"""Run the model until the end condition is reached. Overload as

0 commit comments

Comments
 (0)