44import pandas as pd
55
66from mesa .agent import AgentSet
7+ from mesa import Model
78
89
910class BaseCollector :
10- def __init__ (self , name :str , obj : Any , attributes : str | List [str ]= None , callable : Callable = None , column_names : List [ str ] = None ):
11+ def __init__ (self , name :str , obj : Any , attributes : str | List [str ]= None , callable : Callable = None ):
1112 """
1213
1314 Args
1415 name : name of the collector
1516 obj : object
1617 attributes :
1718 callable : callable
18- column_names : list of column names, optional, defaults to attributes
19+
20+ Note::
21+ if a callable is passed, it is assumed that there will only be a single return value
1922
2023 """
2124 super ().__init__ ()
25+ if attributes is None :
26+ attributes = [name ,]
2227
2328 if isinstance (attributes , str ):
2429 attributes = [attributes ,]
@@ -27,7 +32,6 @@ def __init__(self, name:str, obj: Any, attributes: str|List[str]=None, callable:
2732 self .obj = obj
2833 self .attributes = attributes
2934 self .callable = callable
30- self .column_names = column_names
3135 self .data_over_time = {}
3236
3337 def collect (self , time ):
@@ -54,9 +58,7 @@ def to_dataframe(self):
5458 # or name, or even something else if callable does funky stuff
5559 # so we need meaningful defaults and a way to override them
5660
57- if self .column_names is not None :
58- columns = self .column_names
59- elif self .callable is not None :
61+ if self .callable is not None :
6062 columns = [self .name ]
6163 else :
6264 columns = self .attributes
@@ -65,8 +67,8 @@ def to_dataframe(self):
6567
6668
6769class AgentSetCollector (BaseCollector ):
68- def __init__ (self , name , obj , attributes = None , callable = None , column_names = None ):
69- super ().__init__ (name , obj , attributes , callable , column_names )
70+ def __init__ (self , name , obj , attributes = None , callable = None ):
71+ super ().__init__ (name , obj , attributes = attributes , callable = callable )
7072 self .attributes .append ("unique_id" )
7173
7274 def collect (self , time ):
@@ -163,9 +165,51 @@ def collect_all(self):
163165 collector .collect (time )
164166
165167
168+ def collect (name :str , obj :Any , attributes : str | List [str ]= None , callable : Callable = None ):
169+ """
170+
171+ Args
172+ name : name of the collector
173+ obj : object form which to collect information
174+ attributes : attributes to collect, option. If not provided, attributes defaults to name
175+ callable : callable to apply to collected data.
176+
177+ FIXME:: what about callable to object directly? or simply not allow for it and solve this
178+ FIXME:: through measures?
179+
180+ """
166181
167- def collect_from (name , object , attributes , callable = None ):
168- if isinstance (object , AgentSet ):
169- return AgentSetCollector (name , object , attributes , callable )
182+ if isinstance (obj , AgentSet ):
183+ return AgentSetCollector (name , obj , attributes , callable )
170184 else :
171- return BaseCollector (name , object , attributes , callable )
185+ return BaseCollector (name , obj , attributes , callable )
186+
187+
188+
189+
190+ class Measure :
191+ # FIXME:: do we want AgentSet based measures?
192+ # FIXME:: can we play some property trick to enable attribute retrieval
193+ # FIXME:: doing so would turn measure into a descriptor
194+ # FIXME:: what about callable vs. attribute based Measures?
195+
196+ def __init__ (self , model :Model , obj : Any , callable : Callable ):
197+ super ().__init__ ()
198+ self .obj = obj
199+ self .callable = callable
200+ self .model = model
201+ self ._update_step = - 1
202+ self ._cached_value = None
203+
204+ def get_value (self , force_update : bool = False ):
205+ """
206+
207+ Args:
208+ force_update (bool): force recalculation of measure.
209+
210+ """
211+
212+ if force_update or (self .model .step != self ._update_step ):
213+ self ._cached_value = self .callable (self .obj )
214+ return self ._cached_value
215+
0 commit comments