88# Remove this __future__ import once the oldest supported Python is 3.10
99from __future__ import annotations
1010
11- import itertools
1211import random
13- from collections import defaultdict
12+ import warnings
1413
1514# mypy
1615from 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