From f81be13695225e9d36ba4c271503b97fe95d8821 Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Sat, 29 Oct 2022 14:10:22 -0700 Subject: [PATCH 01/11] Fix #1419. DataCollector accepts an arbitrary schedule at creation (defaults to model.schedule otherwise) and will return None if an attribute is not found instead of throwing an AttributeError. --- mesa/datacollection.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/mesa/datacollection.py b/mesa/datacollection.py index 695742ad33b..afc7c00f250 100644 --- a/mesa/datacollection.py +++ b/mesa/datacollection.py @@ -53,7 +53,7 @@ class DataCollector: """ - def __init__(self, model_reporters=None, agent_reporters=None, tables=None): + def __init__(self, model_reporters=None, agent_reporters=None, tables=None, schedule=None): """Instantiate a DataCollector with lists of model and agent reporters. Both model_reporters and agent_reporters accept a dictionary mapping a variable name to either an attribute name, or a method. @@ -76,6 +76,7 @@ def __init__(self, model_reporters=None, agent_reporters=None, tables=None): model_reporters: Dictionary of reporter names and attributes/funcs agent_reporters: Dictionary of reporter names and attributes/funcs. tables: Dictionary of table names to lists of column names. + schedule: A scheduler from the mesa.time module. If not supplied, this defaults to `model.schedule`. Notes: If you want to pickle your model you must not use lambda functions. @@ -100,6 +101,8 @@ class attributes of model self._agent_records = {} self.tables = {} + self.schedule = schedule + if model_reporters is not None: for name, reporter in model_reporters.items(): self._new_model_reporter(name, reporter) @@ -151,28 +154,29 @@ def _new_table(self, table_name, table_columns): new_table = {column: [] for column in table_columns} self.tables[table_name] = new_table - def _record_agents(self, model): + def _record_agents(self, model, schedule): """Record agents data in a mapping of functions and agents.""" - rep_funcs = self.agent_reporters.values() - if all(hasattr(rep, "attribute_name") for rep in rep_funcs): - prefix = ["model.schedule.steps", "unique_id"] - attributes = [func.attribute_name for func in rep_funcs] - get_reports = attrgetter(*prefix + attributes) - else: - - def get_reports(agent): - _prefix = (agent.model.schedule.steps, agent.unique_id) - reports = tuple(rep(agent) for rep in rep_funcs) - return _prefix + reports - - agent_records = map(get_reports, model.schedule.agents) + agent_records = map(partial(self._get_reports, self, schedule.steps), schedule.agents) return agent_records + @staticmethod + def _get_reports(collector, steps, agent): + """Get the agent reports for a given agent and return them in a tuple. """ + rep_funcs = collector.agent_reporters.values() + _prefix = (steps, agent.unique_id) + reports = tuple(rep(agent) for rep in rep_funcs) + return _prefix + reports + def _reporter_decorator(self, reporter): return reporter() def collect(self, model): """Collect all the data for the given model object.""" + if self.schedule is None: + schedule = model.schedule + else: + schedule = self.schedule + if self.model_reporters: for var, reporter in self.model_reporters.items(): @@ -189,8 +193,8 @@ def collect(self, model): self.model_vars[var].append(self._reporter_decorator(reporter)) if self.agent_reporters: - agent_records = self._record_agents(model) - self._agent_records[model.schedule.steps] = list(agent_records) + agent_records = self._record_agents(model, schedule) + self._agent_records[schedule.steps] = list(agent_records) def add_table_row(self, table_name, row, ignore_missing=False): """Add a row dictionary to a specific table. From 1918e0c109ca0dc3b75e6dd3812b8468577d191a Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Sat, 29 Oct 2022 14:20:48 -0700 Subject: [PATCH 02/11] Removed unused attrgetter --- mesa/datacollection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mesa/datacollection.py b/mesa/datacollection.py index afc7c00f250..1f5587920ca 100644 --- a/mesa/datacollection.py +++ b/mesa/datacollection.py @@ -37,7 +37,6 @@ """ from functools import partial import itertools -from operator import attrgetter import pandas as pd import types From f9622c8848fb82901a01db3645937c80037ff4bb Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Sat, 29 Oct 2022 20:14:16 -0700 Subject: [PATCH 03/11] Fixed formatting with black --- mesa/datacollection.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mesa/datacollection.py b/mesa/datacollection.py index 1f5587920ca..c50346f42d1 100644 --- a/mesa/datacollection.py +++ b/mesa/datacollection.py @@ -52,7 +52,9 @@ class DataCollector: """ - def __init__(self, model_reporters=None, agent_reporters=None, tables=None, schedule=None): + def __init__( + self, model_reporters=None, agent_reporters=None, tables=None, schedule=None + ): """Instantiate a DataCollector with lists of model and agent reporters. Both model_reporters and agent_reporters accept a dictionary mapping a variable name to either an attribute name, or a method. @@ -155,12 +157,14 @@ def _new_table(self, table_name, table_columns): def _record_agents(self, model, schedule): """Record agents data in a mapping of functions and agents.""" - agent_records = map(partial(self._get_reports, self, schedule.steps), schedule.agents) + agent_records = map( + partial(self._get_reports, self, schedule.steps), schedule.agents + ) return agent_records @staticmethod def _get_reports(collector, steps, agent): - """Get the agent reports for a given agent and return them in a tuple. """ + """Get the agent reports for a given agent and return them in a tuple.""" rep_funcs = collector.agent_reporters.values() _prefix = (steps, agent.unique_id) reports = tuple(rep(agent) for rep in rep_funcs) From 4fcf6a00f044ea50d2b005a6427531320e475680 Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Tue, 1 Nov 2022 22:10:36 -0700 Subject: [PATCH 04/11] Fix #1419 as discussed in #1481. _record_agents() now accepts a scheudle instead of a model. Attributes that do not exist return None instead of an AttributeError. --- mesa/datacollection.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/mesa/datacollection.py b/mesa/datacollection.py index c50346f42d1..6357ff799d0 100644 --- a/mesa/datacollection.py +++ b/mesa/datacollection.py @@ -155,20 +155,17 @@ def _new_table(self, table_name, table_columns): new_table = {column: [] for column in table_columns} self.tables[table_name] = new_table - def _record_agents(self, model, schedule): + def _record_agents(self, schedule): """Record agents data in a mapping of functions and agents.""" - agent_records = map( - partial(self._get_reports, self, schedule.steps), schedule.agents - ) - return agent_records + rep_funcs = self.agent_reporters.values() - @staticmethod - def _get_reports(collector, steps, agent): - """Get the agent reports for a given agent and return them in a tuple.""" - rep_funcs = collector.agent_reporters.values() - _prefix = (steps, agent.unique_id) - reports = tuple(rep(agent) for rep in rep_funcs) - return _prefix + reports + def get_reports(agent): + _prefix = (schedule.steps, agent.unique_id) + reports = tuple(rep(agent) for rep in rep_funcs) + return _prefix + reports + + agent_records = map(get_reports, schedule.agents) + return agent_records def _reporter_decorator(self, reporter): return reporter() @@ -196,7 +193,7 @@ def collect(self, model): self.model_vars[var].append(self._reporter_decorator(reporter)) if self.agent_reporters: - agent_records = self._record_agents(model, schedule) + agent_records = self._record_agents(schedule) self._agent_records[schedule.steps] = list(agent_records) def add_table_row(self, table_name, row, ignore_missing=False): From bf0f3a1c2abb1463fe1172522462522e3471e558 Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Tue, 1 Nov 2022 23:19:47 -0700 Subject: [PATCH 05/11] Added sample snippet code for data collecting on multiple schedules in the same model. --- docs/useful-snippets/snippets.rst | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/useful-snippets/snippets.rst b/docs/useful-snippets/snippets.rst index 4c7e8e418f1..624c37d0bd7 100644 --- a/docs/useful-snippets/snippets.rst +++ b/docs/useful-snippets/snippets.rst @@ -58,3 +58,48 @@ code as follows. If you would still like to run your code in Jupyter you will need to adjust the cell as noted above. Then you can you can add the `nbmultitask library <(https://nbviewer.org/github/micahscopes/nbmultitask/blob/39b6f31b047e8a51a0fcb5c93ae4572684f877ce/examples.ipynb)>`__ or look at this `stackoverflow `__. + +Using multiple schedules and data collectors +------------- + +One may occasionally want multiple schedules with distinct sets of agents and collect data on each of those schedules independently while allowing the agents to interact in the same model. This would be particularly useful if the agents in your model do not all have the same attributes and you want to collect data on them in groups. You can create a simple scheduler class to mix the agents in each schedule and data collect on each schedule independently. + +.. code:: python + from random import shuffle + + # simple schedule mixer + class ScheduleMixerRandom: + def __init__(self, schedules: list): + self.schedules = schedules + + def step(self): + agents = [] + for s in self.schedules: + agents.extend(s.agents) + agents = shuffle(agents) # step through them randomly + for a in agents: + a.step() + + class MyModel(Model): + def __init__(self, ...): + super().__init__() + self.schedule_one = BaseScheduler() + self.schedule_two = BaseScheduler() + + # each DataCollector instance is associated with a specific schedule (and therefore its agents) + self.datacollector_one = DataCollector(agent_reporters = {'value_one': 'value_one'}, schedule = self.schedule_one) + self.datacollector_two = DataCollector(agent_reporters = {'value_two': 'value_two'}, schedule = self.schedule_two) + + self.schedule_mixer = ScheduleMixerRandom(schedules=[self.schedule_one, self.schedule_two]) + + def step(): + self.schedule_mixer.step() # calls step() on each agent in both schedules, mixed together + self.datacollector_one.collect(self) # collects data on the agents in schedule_one + self.datacollector_two.collect(self) # collects data on the agents in schedule_two + + +To get the data at the end of the model, simply call: + +.. code:: python + schedule_one_agent_data = model.datacollector_one.get_agent_vars_dataframe() + schedule_two_agent_data = model.datacollector_two.get_agent_vars_dataframe() From fcfa5e6483128f7890af50c2ac8bb4876b820c0a Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Wed, 2 Nov 2022 11:11:56 -0700 Subject: [PATCH 06/11] Added newline for proper rendering --- docs/useful-snippets/snippets.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/useful-snippets/snippets.rst b/docs/useful-snippets/snippets.rst index 624c37d0bd7..9701df05dbd 100644 --- a/docs/useful-snippets/snippets.rst +++ b/docs/useful-snippets/snippets.rst @@ -101,5 +101,6 @@ One may occasionally want multiple schedules with distinct sets of agents and co To get the data at the end of the model, simply call: .. code:: python + schedule_one_agent_data = model.datacollector_one.get_agent_vars_dataframe() schedule_two_agent_data = model.datacollector_two.get_agent_vars_dataframe() From 93832956d5e0e4cbecceda7cd869967345d28f76 Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Wed, 2 Nov 2022 12:26:06 -0700 Subject: [PATCH 07/11] Added another newline for proper rendering --- docs/useful-snippets/snippets.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/useful-snippets/snippets.rst b/docs/useful-snippets/snippets.rst index 9701df05dbd..1421c3614ff 100644 --- a/docs/useful-snippets/snippets.rst +++ b/docs/useful-snippets/snippets.rst @@ -65,6 +65,7 @@ Using multiple schedules and data collectors One may occasionally want multiple schedules with distinct sets of agents and collect data on each of those schedules independently while allowing the agents to interact in the same model. This would be particularly useful if the agents in your model do not all have the same attributes and you want to collect data on them in groups. You can create a simple scheduler class to mix the agents in each schedule and data collect on each schedule independently. .. code:: python + from random import shuffle # simple schedule mixer From 8bf31eef171579892418748c804a97e6cc6eb50c Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Thu, 3 Nov 2022 21:05:55 -0700 Subject: [PATCH 08/11] Add documenting/precautionary sentences to the ScheduleMixerRandom custom class snippet. --- docs/useful-snippets/snippets.rst | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/useful-snippets/snippets.rst b/docs/useful-snippets/snippets.rst index 1421c3614ff..ae41aa6fc70 100644 --- a/docs/useful-snippets/snippets.rst +++ b/docs/useful-snippets/snippets.rst @@ -70,16 +70,20 @@ One may occasionally want multiple schedules with distinct sets of agents and co # simple schedule mixer class ScheduleMixerRandom: - def __init__(self, schedules: list): - self.schedules = schedules + """ + Note that ScheduleMixerRandom as written here would not capture changes to + the agent lists in the sub-schedules. The user would need to be mindful if + this functionality were to be included. + """ + + def __init__(self, schedules): + self._agents = [] + for s in schedules: + self._agents.extends(s.agents) def step(self): - agents = [] - for s in self.schedules: - agents.extend(s.agents) - agents = shuffle(agents) # step through them randomly - for a in agents: - a.step() + # do stuff with self._agents + ... class MyModel(Model): def __init__(self, ...): @@ -99,6 +103,8 @@ One may occasionally want multiple schedules with distinct sets of agents and co self.datacollector_two.collect(self) # collects data on the agents in schedule_two + + To get the data at the end of the model, simply call: .. code:: python From ab94920614e140619a93d96ec92b7ea3d35828c5 Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Fri, 4 Nov 2022 10:25:49 -0700 Subject: [PATCH 09/11] Edited language, snippet for ScheduleMixerRandom uses a dict instead of list. --- docs/useful-snippets/snippets.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/useful-snippets/snippets.rst b/docs/useful-snippets/snippets.rst index ae41aa6fc70..01e2834ef5c 100644 --- a/docs/useful-snippets/snippets.rst +++ b/docs/useful-snippets/snippets.rst @@ -71,15 +71,16 @@ One may occasionally want multiple schedules with distinct sets of agents and co # simple schedule mixer class ScheduleMixerRandom: """ - Note that ScheduleMixerRandom as written here would not capture changes to + Be careful: ScheduleMixerRandom as written here would not capture changes to the agent lists in the sub-schedules. The user would need to be mindful if this functionality were to be included. """ def __init__(self, schedules): - self._agents = [] + self._agents = {} for s in schedules: - self._agents.extends(s.agents) + for a in s.agents: + self._agents[a.unique_id] = a # each agent must have a unique id across models def step(self): # do stuff with self._agents From 3d336d2131cacec9602f5e9e90544f19228086cd Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Mon, 7 Nov 2022 12:46:04 -0800 Subject: [PATCH 10/11] Reformatted snippet code --- docs/useful-snippets/snippets.rst | 86 +++++++++++++++++-------------- docs/useful-snippets/tmp.py | 49 ++++++++++++++++++ 2 files changed, 97 insertions(+), 38 deletions(-) create mode 100644 docs/useful-snippets/tmp.py diff --git a/docs/useful-snippets/snippets.rst b/docs/useful-snippets/snippets.rst index 01e2834ef5c..ebaf9f19c87 100644 --- a/docs/useful-snippets/snippets.rst +++ b/docs/useful-snippets/snippets.rst @@ -66,44 +66,54 @@ One may occasionally want multiple schedules with distinct sets of agents and co .. code:: python - from random import shuffle - - # simple schedule mixer - class ScheduleMixerRandom: - """ - Be careful: ScheduleMixerRandom as written here would not capture changes to - the agent lists in the sub-schedules. The user would need to be mindful if - this functionality were to be included. - """ - - def __init__(self, schedules): - self._agents = {} - for s in schedules: - for a in s.agents: - self._agents[a.unique_id] = a # each agent must have a unique id across models - - def step(self): - # do stuff with self._agents - ... - - class MyModel(Model): - def __init__(self, ...): - super().__init__() - self.schedule_one = BaseScheduler() - self.schedule_two = BaseScheduler() - - # each DataCollector instance is associated with a specific schedule (and therefore its agents) - self.datacollector_one = DataCollector(agent_reporters = {'value_one': 'value_one'}, schedule = self.schedule_one) - self.datacollector_two = DataCollector(agent_reporters = {'value_two': 'value_two'}, schedule = self.schedule_two) - - self.schedule_mixer = ScheduleMixerRandom(schedules=[self.schedule_one, self.schedule_two]) - - def step(): - self.schedule_mixer.step() # calls step() on each agent in both schedules, mixed together - self.datacollector_one.collect(self) # collects data on the agents in schedule_one - self.datacollector_two.collect(self) # collects data on the agents in schedule_two - - + from random import shuffle + + # simple schedule mixer + class ScheduleMixerRandom: + """ + Be careful: ScheduleMixerRandom as written here would not capture changes to + the agent lists in the sub-schedules. The user would need to be mindful if + this functionality were to be included. + """ + + def __init__(self, schedules): + self._agents = {} + for s in schedules: + for a in s.agents: + self._agents[ + a.unique_id + ] = a # each agent must have a unique id across models + + def step(self): + # do stuff with self._agents + ... + + class MyModel(Model): + def __init__(self, ...): + super().__init__() + self.schedule_one = BaseScheduler() + self.schedule_two = BaseScheduler() + + # each DataCollector instance is associated with a specific schedule (and therefore its agents) + self.datacollector_one = DataCollector( + agent_reporters={"value_one": "value_one"}, schedule=self.schedule_one + ) + self.datacollector_two = DataCollector( + agent_reporters={"value_two": "value_two"}, schedule=self.schedule_two + ) + + self.schedule_mixer = ScheduleMixerRandom( + schedules=[self.schedule_one, self.schedule_two] + ) + + def step(): + self.schedule_mixer.step() # calls step() on each agent in both schedules, mixed together + self.datacollector_one.collect( + self + ) # collects data on the agents in schedule_one + self.datacollector_two.collect( + self + ) # collects data on the agents in schedule_two To get the data at the end of the model, simply call: diff --git a/docs/useful-snippets/tmp.py b/docs/useful-snippets/tmp.py new file mode 100644 index 00000000000..6716c844e54 --- /dev/null +++ b/docs/useful-snippets/tmp.py @@ -0,0 +1,49 @@ +from random import shuffle + +# simple schedule mixer +class ScheduleMixerRandom: + """ + Be careful: ScheduleMixerRandom as written here would not capture changes to + the agent lists in the sub-schedules. The user would need to be mindful if + this functionality were to be included. + """ + + def __init__(self, schedules): + self._agents = {} + for s in schedules: + for a in s.agents: + self._agents[ + a.unique_id + ] = a # each agent must have a unique id across models + + def step(self): + # do stuff with self._agents + pass + + +class MyModel(Model): + def __init__(self, args): + super().__init__() + self.schedule_one = BaseScheduler() + self.schedule_two = BaseScheduler() + + # each DataCollector instance is associated with a specific schedule (and therefore its agents) + self.datacollector_one = DataCollector( + agent_reporters={"value_one": "value_one"}, schedule=self.schedule_one + ) + self.datacollector_two = DataCollector( + agent_reporters={"value_two": "value_two"}, schedule=self.schedule_two + ) + + self.schedule_mixer = ScheduleMixerRandom( + schedules=[self.schedule_one, self.schedule_two] + ) + + def step(): + self.schedule_mixer.step() # calls step() on each agent in both schedules, mixed together + self.datacollector_one.collect( + self + ) # collects data on the agents in schedule_one + self.datacollector_two.collect( + self + ) # collects data on the agents in schedule_two From d486f2e7e79d048ce8731219a018df0d2e3b708b Mon Sep 17 00:00:00 2001 From: Jacob Sundstrom Date: Tue, 8 Nov 2022 10:24:25 -0800 Subject: [PATCH 11/11] Removed accidentally commited file. --- docs/useful-snippets/tmp.py | 49 ------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 docs/useful-snippets/tmp.py diff --git a/docs/useful-snippets/tmp.py b/docs/useful-snippets/tmp.py deleted file mode 100644 index 6716c844e54..00000000000 --- a/docs/useful-snippets/tmp.py +++ /dev/null @@ -1,49 +0,0 @@ -from random import shuffle - -# simple schedule mixer -class ScheduleMixerRandom: - """ - Be careful: ScheduleMixerRandom as written here would not capture changes to - the agent lists in the sub-schedules. The user would need to be mindful if - this functionality were to be included. - """ - - def __init__(self, schedules): - self._agents = {} - for s in schedules: - for a in s.agents: - self._agents[ - a.unique_id - ] = a # each agent must have a unique id across models - - def step(self): - # do stuff with self._agents - pass - - -class MyModel(Model): - def __init__(self, args): - super().__init__() - self.schedule_one = BaseScheduler() - self.schedule_two = BaseScheduler() - - # each DataCollector instance is associated with a specific schedule (and therefore its agents) - self.datacollector_one = DataCollector( - agent_reporters={"value_one": "value_one"}, schedule=self.schedule_one - ) - self.datacollector_two = DataCollector( - agent_reporters={"value_two": "value_two"}, schedule=self.schedule_two - ) - - self.schedule_mixer = ScheduleMixerRandom( - schedules=[self.schedule_one, self.schedule_two] - ) - - def step(): - self.schedule_mixer.step() # calls step() on each agent in both schedules, mixed together - self.datacollector_one.collect( - self - ) # collects data on the agents in schedule_one - self.datacollector_two.collect( - self - ) # collects data on the agents in schedule_two