From 4a3d75731b661b93a5ccdd54c4cf7a2347bf5eb1 Mon Sep 17 00:00:00 2001 From: rht Date: Sat, 23 Sep 2023 20:21:49 -0400 Subject: [PATCH 01/20] perf: Speed up Solara space render --- mesa/experimental/jupyter_viz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py index 96970b8c56b..265bb9f44c2 100644 --- a/mesa/experimental/jupyter_viz.py +++ b/mesa/experimental/jupyter_viz.py @@ -270,7 +270,7 @@ def portray(g): else: space_ax.scatter(**portray(model.grid)) space_ax.set_axis_off() - solara.FigureMatplotlib(space_fig) + solara.FigureMatplotlib(space_fig, format="png") def _draw_network_grid(model, space_ax, agent_portrayal): From a75a353f1bf6e53ef3823ef267b0a694fac811ee Mon Sep 17 00:00:00 2001 From: rht Date: Sat, 23 Sep 2023 20:28:42 -0400 Subject: [PATCH 02/20] Solara: Reduce default interval to 150 --- mesa/experimental/jupyter_viz.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py index 265bb9f44c2..d3bc9c0d676 100644 --- a/mesa/experimental/jupyter_viz.py +++ b/mesa/experimental/jupyter_viz.py @@ -21,7 +21,7 @@ def JupyterViz( name="Mesa Model", agent_portrayal=None, space_drawer="default", - play_interval=400, + play_interval=150, ): """Initialize a component to visualize a model. Args: @@ -34,7 +34,7 @@ def JupyterViz( the model; default implementation is :meth:`make_space`; simulations with no space to visualize should specify `space_drawer=False` - play_interval: play interval (default: 400) + play_interval: play interval (default: 150) """ current_step, set_current_step = solara.use_state(0) From 6a39efd15ecacf8f44cc21e75228eb4f723199b6 Mon Sep 17 00:00:00 2001 From: rht Date: Wed, 20 Sep 2023 09:29:53 -0400 Subject: [PATCH 03/20] model: Ensure the seed is initialized with current timestamp when it is None --- mesa/model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mesa/model.py b/mesa/model.py index 12446b5df23..1ac0edd6460 100644 --- a/mesa/model.py +++ b/mesa/model.py @@ -21,7 +21,11 @@ class Model: def __new__(cls, *args: Any, **kwargs: Any) -> Any: """Create a new model object and instantiate its RNG automatically.""" obj = object.__new__(cls) - obj._seed = kwargs.get("seed", None) + obj._seed = kwargs.get("seed") + if obj._seed is None: + # We explicitly specify the seed here so that we know its value in + # advance. + obj._seed = random.random() # noqa: S311 obj.random = random.Random(obj._seed) return obj From b2f642fd1a986f3114ba6f6f6e9bc119c037062f Mon Sep 17 00:00:00 2001 From: Wang Boyu Date: Tue, 26 Sep 2023 10:03:39 -0400 Subject: [PATCH 04/20] update ruff version in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3ab90619d9b..73e82f80da8 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ extras_require = { "dev": [ "black", - "ruff==0.0.254", + "ruff==0.0.275", "coverage", "pytest >= 4.6", "pytest-cov", From d888a8cab397286c8fb3b496f1af8e79bb1b844d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:35:14 +0000 Subject: [PATCH 05/20] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1) - [github.com/asottile/pyupgrade: v3.10.1 → v3.14.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.14.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 772620fc72d..f97b4c7298d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,12 +4,12 @@ ci: repos: - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black-jupyter exclude: ^mesa/cookiecutter-mesa/ - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.14.0 hooks: - id: pyupgrade args: [--py38-plus] From 3cbe56f27fc1f9461876ad7f08514701950636e9 Mon Sep 17 00:00:00 2001 From: maskarb Date: Sat, 7 Oct 2023 18:23:03 -0400 Subject: [PATCH 06/20] Fix issue #1831: check if positions values are tuples --- mesa/space.py | 7 +++---- tests/test_space.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/mesa/space.py b/mesa/space.py index 286f63c8fa3..6ed7c09697e 100644 --- a/mesa/space.py +++ b/mesa/space.py @@ -68,10 +68,9 @@ def accept_tuple_argument(wrapped_function: F) -> F: single-item list rather than forcing user to do it.""" def wrapper(grid_instance, positions) -> Any: - if isinstance(positions, tuple) and len(positions) == 2: - return wrapped_function(grid_instance, [positions]) - else: - return wrapped_function(grid_instance, positions) + if len(positions) == 2 and not isinstance(positions[0], tuple): + positions = [positions] + return wrapped_function(grid_instance, positions) return cast(F, wrapper) diff --git a/tests/test_space.py b/tests/test_space.py index 8691dc45f0a..f8f2cc9440c 100644 --- a/tests/test_space.py +++ b/tests/test_space.py @@ -327,6 +327,28 @@ def move_agent(self): assert self.space[initial_pos[0]][initial_pos[1]] is None assert self.space[final_pos[0]][final_pos[1]] == _agent + def test_iter_cell_list_contents(self): + """ + Test neighborhood retrieval + """ + cell_list_1 = list(self.space.iter_cell_list_contents(TEST_AGENTS_GRID[0])) + assert len(cell_list_1) == 1 + + cell_list_2 = list( + self.space.iter_cell_list_contents( + (TEST_AGENTS_GRID[0], TEST_AGENTS_GRID[1]) + ) + ) + assert len(cell_list_2) == 2 + + cell_list_3 = list(self.space.iter_cell_list_contents(tuple(TEST_AGENTS_GRID))) + assert len(cell_list_3) == 3 + + cell_list_4 = list( + self.space.iter_cell_list_contents((TEST_AGENTS_GRID[0], (0, 0))) + ) + assert len(cell_list_4) == 1 + class TestSingleNetworkGrid(unittest.TestCase): GRAPH_SIZE = 10 From 477778c2cc7dcaa4c70b735f8bdc1b788b1657f3 Mon Sep 17 00:00:00 2001 From: rht Date: Sat, 7 Oct 2023 04:36:25 -0400 Subject: [PATCH 07/20] solara: Implement drawer for continuous space --- mesa/experimental/jupyter_viz.py | 41 ++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py index d3bc9c0d676..87459dcfa30 100644 --- a/mesa/experimental/jupyter_viz.py +++ b/mesa/experimental/jupyter_viz.py @@ -265,16 +265,22 @@ def portray(g): space_fig = Figure() space_ax = space_fig.subplots() - if isinstance(model.grid, mesa.space.NetworkGrid): - _draw_network_grid(model, space_ax, agent_portrayal) + space = getattr(model, "grid", None) + if space is None: + # Sometimes the space is defined as model.space instead of model.grid + space = model.space + if isinstance(space, mesa.space.NetworkGrid): + _draw_network_grid(space, space_ax, agent_portrayal) + elif isinstance(space, mesa.space.ContinuousSpace): + _draw_continuous_space(space, space_ax, agent_portrayal) else: - space_ax.scatter(**portray(model.grid)) + space_ax.scatter(**portray(space)) space_ax.set_axis_off() solara.FigureMatplotlib(space_fig, format="png") -def _draw_network_grid(model, space_ax, agent_portrayal): - graph = model.grid.G +def _draw_network_grid(space, space_ax, agent_portrayal): + graph = space.G pos = nx.spring_layout(graph, seed=0) nx.draw( graph, @@ -284,6 +290,31 @@ def _draw_network_grid(model, space_ax, agent_portrayal): ) +def _draw_continuous_space(space, space_ax, agent_portrayal): + def portray(space): + x = [] + y = [] + s = [] # size + c = [] # color + for agent in space._agent_to_index: + data = agent_portrayal(agent) + _x, _y = agent.pos + x.append(_x) + y.append(_y) + if "size" in data: + s.append(data["size"]) + if "color" in data: + c.append(data["color"]) + out = {"x": x, "y": y} + if len(s) > 0: + out["s"] = s + if len(c) > 0: + out["c"] = c + return out + + space_ax.scatter(**portray(space)) + + def make_plot(model, measure): fig = Figure() ax = fig.subplots() From 44ab8cd8c364e3aae4ecd37af07d853b9af34239 Mon Sep 17 00:00:00 2001 From: rht Date: Sat, 7 Oct 2023 06:07:48 -0400 Subject: [PATCH 08/20] refactor: Move default grid drawer to separate function --- mesa/experimental/jupyter_viz.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py index 87459dcfa30..3408ba11c6a 100644 --- a/mesa/experimental/jupyter_viz.py +++ b/mesa/experimental/jupyter_viz.py @@ -235,6 +235,23 @@ def change_handler(value, name=name): def make_space(model, agent_portrayal): + space_fig = Figure() + space_ax = space_fig.subplots() + space = getattr(model, "grid", None) + if space is None: + # Sometimes the space is defined as model.space instead of model.grid + space = model.space + if isinstance(space, mesa.space.NetworkGrid): + _draw_network_grid(space, space_ax, agent_portrayal) + elif isinstance(space, mesa.space.ContinuousSpace): + _draw_continuous_space(space, space_ax, agent_portrayal) + else: + _draw_grid(space, space_ax, agent_portrayal) + space_ax.set_axis_off() + solara.FigureMatplotlib(space_fig, format="png") + + +def _draw_grid(space, space_ax, agent_portrayal): def portray(g): x = [] y = [] @@ -263,20 +280,7 @@ def portray(g): out["c"] = c return out - space_fig = Figure() - space_ax = space_fig.subplots() - space = getattr(model, "grid", None) - if space is None: - # Sometimes the space is defined as model.space instead of model.grid - space = model.space - if isinstance(space, mesa.space.NetworkGrid): - _draw_network_grid(space, space_ax, agent_portrayal) - elif isinstance(space, mesa.space.ContinuousSpace): - _draw_continuous_space(space, space_ax, agent_portrayal) - else: - space_ax.scatter(**portray(space)) - space_ax.set_axis_off() - solara.FigureMatplotlib(space_fig, format="png") + space_ax.scatter(**portray(space)) def _draw_network_grid(space, space_ax, agent_portrayal): From d40bc5b475f667f65efc3c6dd256f56b1fe3f724 Mon Sep 17 00:00:00 2001 From: rht Date: Sun, 15 Oct 2023 01:20:34 -0400 Subject: [PATCH 09/20] intro tutorial: Explain how to plot reporter of multiple agents This is a section taken from #1717. Co-authored-by: Ewout ter Hoeven --- docs/tutorials/intro_tutorial.ipynb | 58 ++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/intro_tutorial.ipynb b/docs/tutorials/intro_tutorial.ipynb index 404ae8d25bf..1aa8d4d1c4b 100644 --- a/docs/tutorials/intro_tutorial.ipynb +++ b/docs/tutorials/intro_tutorial.ipynb @@ -1059,6 +1059,60 @@ "g.set(title=\"Wealth of agent 14 over time\");" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also plot a reporter of multiple agents over time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent_list = [3, 14, 25]\n", + "\n", + "# Get the wealth of multiple agents over time\n", + "multiple_agents_wealth = agent_wealth[\n", + " agent_wealth.index.get_level_values(\"AgentID\").isin(agent_list)\n", + "]\n", + "# Plot the wealth of multiple agents over time\n", + "g = sns.lineplot(data=multiple_agents_wealth, x=\"Step\", y=\"Wealth\", hue=\"AgentID\")\n", + "g.set(title=\"Wealth of agents 3, 14 and 25 over time\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also plot the average of all agents, with a 95% confidence interval for that average." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Transform the data to a long format\n", + "agent_wealth_long = agent_wealth.T.unstack().reset_index()\n", + "agent_wealth_long.columns = [\"Step\", \"AgentID\", \"Variable\", \"Value\"]\n", + "agent_wealth_long.head(3)\n", + "\n", + "# Plot the average wealth over time\n", + "g = sns.lineplot(data=agent_wealth_long, x=\"Step\", y=\"Value\", errorbar=(\"ci\", 95))\n", + "g.set(title=\"Average wealth over time\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Which is exactly 1, as expected in this model, since each agent starts with one wealth unit, and each agent gives one wealth unit to another agent at each step." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1369,7 +1423,7 @@ "metadata": { "anaconda-cloud": {}, "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1383,7 +1437,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.10.12" }, "widgets": { "state": {}, From 7343401312f915c81a93566ef91c20957d2784c7 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Wed, 18 Oct 2023 16:24:29 +0200 Subject: [PATCH 10/20] DataCollector: Allow agent reporters to take class methods and functions with parameter lists Modify the `DataCollector` class to allow agent reporters to take methods of a class/instance and functions with parameters placed in a list (like model reporters), by extending the `_new_agent_reporter` method. This implementation starts by checking if the reporter is an attribute string. If so, it creates a function to retrieve the attribute from an agent. Next, it checks if the reporter is a list. If it is, this indicates that we have a function with parameters, so it wraps that function to pass those parameters when called. For any other type (like lambdas or methods), we assume they're directly suitable to be used as reporters. Now, with this modification, agent reporters in the `DataCollector` class can take: 1. Attribute strings 2. Function objects (like lambdas) 3. Methods of a class/instance 4. Functions with parameters placed in a list This approach ensures backward compatibility because the existing checks for attribute strings and function objects remain unchanged. The added functionality only extends the capabilities of the class without altering the existing behavior. --- mesa/datacollection.py | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/mesa/datacollection.py b/mesa/datacollection.py index 7f3dc847111..be3b23905a2 100644 --- a/mesa/datacollection.py +++ b/mesa/datacollection.py @@ -125,22 +125,37 @@ def _new_model_reporter(self, name, reporter): self.model_reporters[name] = reporter self.model_vars[name] = [] - def _new_agent_reporter(self, name, reporter): - """Add a new agent-level reporter to collect. +def _new_agent_reporter(self, name, reporter): + """Add a new agent-level reporter to collect. + + Args: + name: Name of the agent-level variable to collect. + reporter: Attribute string, function object, method of a class/instance, or + function with parameters placed in a list that returns the + variable when given an agent instance. + """ + # Check if the reporter is an attribute string + if isinstance(reporter, str): + attribute_name = reporter - Args: - name: Name of the agent-level variable to collect. - reporter: Attribute string, or function object that returns the - variable when given a model instance. - """ - if isinstance(reporter, str): - attribute_name = reporter + def attr_reporter(agent): + return getattr(agent, attribute_name, None) + + reporter = attr_reporter + + # Check if the reporter is a function with arguments placed in a list + elif isinstance(reporter, list): + func, params = reporter[0], reporter[1] + + def func_with_params(agent): + return func(agent, *params) + + reporter = func_with_params - def reporter(agent): - return getattr(agent, attribute_name, None) + # For other types (like lambda functions, method of a class/instance), + # it's already suitable to be used as a reporter directly. - reporter.attribute_name = attribute_name - self.agent_reporters[name] = reporter + self.agent_reporters[name] = reporter def _new_table(self, table_name, table_columns): """Add a new table that objects can write to. From 28d7ec2e33a62a97d5068f024209c9ce813a90bf Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Wed, 18 Oct 2023 16:41:13 +0200 Subject: [PATCH 11/20] DataCollector: Add tests for class instance method and function lists --- tests/test_datacollector.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/test_datacollector.py b/tests/test_datacollector.py index 8cdee4401e5..d2f2d36a2e6 100644 --- a/tests/test_datacollector.py +++ b/tests/test_datacollector.py @@ -24,6 +24,9 @@ def step(self): self.val += 1 self.val2 += 1 + def double_val(self): + return self.val * 2 + def write_final_values(self): """ Write the final value to the appropriate table. @@ -31,6 +34,8 @@ def write_final_values(self): row = {"agent_id": self.unique_id, "final_value": self.val} self.model.datacollector.add_table_row("Final_Values", row) +def agent_function_with_params(agent, multiplier, offset): + return (agent.val * multiplier) + offset class DifferentMockAgent(MockAgent): # We define a different MockAgent to test for attributes that are present @@ -56,17 +61,20 @@ def __init__(self): self.schedule.add(MockAgent(i, self, val=i)) agent_reporters = {"value": lambda a: a.val, "value2": "val2"} self.initialize_data_collector( - { + model_reporters={ "total_agents": lambda m: m.schedule.get_agent_count(), "model_value": "model_val", "model_calc": self.schedule.get_agent_count, "model_calc_comp": [self.test_model_calc_comp, [3, 4]], "model_calc_fail": [self.test_model_calc_comp, [12, 0]], }, - agent_reporters, - {"Final_Values": ["agent_id", "final_value"]}, + agent_reporters={ + "value": lambda a: a.val, + "value2": "val2", + "double_value": MockAgent.double_val, + "value_with_params": [agent_function_with_params, [2, 3]] + } ) - def test_model_calc_comp(self, input1, input2): if input2 > 0: return (self.model_val * input1) / input2 @@ -132,6 +140,19 @@ def test_agent_records(self): data_collector = self.model.datacollector agent_table = data_collector.get_agent_vars_dataframe() + assert "double_value" in list(agent_table.columns) + assert "value_with_params" in list(agent_table.columns) + + # Check the double_value column + for step, agent_id, value in agent_table["double_value"].items(): + expected_value = agent_id * 2 + self.assertEqual(value, expected_value) + + # Check the value_with_params column + for step, agent_id, value in agent_table["value_with_params"].items(): + expected_value = (agent_id * 2) + 3 + self.assertEqual(value, expected_value) + assert len(data_collector._agent_records) == 8 for step, records in data_collector._agent_records.items(): if step < 5: From f8ad79bd44e9a40c28111d143268bef26af14fdc Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Thu, 19 Oct 2023 14:22:39 +0200 Subject: [PATCH 12/20] datacollection: Fix _new_agent_reporter indentation _new_agent_reporter wasn't intended into the DataCollector class, so thus not seen as a method. --- mesa/datacollection.py | 48 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/mesa/datacollection.py b/mesa/datacollection.py index be3b23905a2..4aa4533a698 100644 --- a/mesa/datacollection.py +++ b/mesa/datacollection.py @@ -125,37 +125,37 @@ def _new_model_reporter(self, name, reporter): self.model_reporters[name] = reporter self.model_vars[name] = [] -def _new_agent_reporter(self, name, reporter): - """Add a new agent-level reporter to collect. - - Args: - name: Name of the agent-level variable to collect. - reporter: Attribute string, function object, method of a class/instance, or - function with parameters placed in a list that returns the - variable when given an agent instance. - """ - # Check if the reporter is an attribute string - if isinstance(reporter, str): - attribute_name = reporter + def _new_agent_reporter(self, name, reporter): + """Add a new agent-level reporter to collect. + + Args: + name: Name of the agent-level variable to collect. + reporter: Attribute string, function object, method of a class/instance, or + function with parameters placed in a list that returns the + variable when given an agent instance. + """ + # Check if the reporter is an attribute string + if isinstance(reporter, str): + attribute_name = reporter - def attr_reporter(agent): - return getattr(agent, attribute_name, None) + def attr_reporter(agent): + return getattr(agent, attribute_name, None) - reporter = attr_reporter + reporter = attr_reporter - # Check if the reporter is a function with arguments placed in a list - elif isinstance(reporter, list): - func, params = reporter[0], reporter[1] + # Check if the reporter is a function with arguments placed in a list + elif isinstance(reporter, list): + func, params = reporter[0], reporter[1] - def func_with_params(agent): - return func(agent, *params) + def func_with_params(agent): + return func(agent, *params) - reporter = func_with_params + reporter = func_with_params - # For other types (like lambda functions, method of a class/instance), - # it's already suitable to be used as a reporter directly. + # For other types (like lambda functions, method of a class/instance), + # it's already suitable to be used as a reporter directly. - self.agent_reporters[name] = reporter + self.agent_reporters[name] = reporter def _new_table(self, table_name, table_columns): """Add a new table that objects can write to. From 17bc8dec5b4c3f6f2b36fda42100a42b1c466324 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Thu, 19 Oct 2023 14:26:02 +0200 Subject: [PATCH 13/20] Fix DataCollector tests for new agent reporter types - Move agent_reporters specification into initialize_data_collector() - Add back the tables argument (accidentally deleted in previous commit) - Use parentheses to parse step and agent_id from agent records dataframe, since those are the multi-index key - Update expected values for new agent reporter types - Update length values of new agent table and vars (both increase by 2 due to 2 new agent reporter columns) --- tests/test_datacollector.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/test_datacollector.py b/tests/test_datacollector.py index d2f2d36a2e6..a45d8891971 100644 --- a/tests/test_datacollector.py +++ b/tests/test_datacollector.py @@ -34,9 +34,11 @@ def write_final_values(self): row = {"agent_id": self.unique_id, "final_value": self.val} self.model.datacollector.add_table_row("Final_Values", row) + def agent_function_with_params(agent, multiplier, offset): return (agent.val * multiplier) + offset + class DifferentMockAgent(MockAgent): # We define a different MockAgent to test for attributes that are present # only in 1 type of agent, but not the other. @@ -59,7 +61,6 @@ def __init__(self): self.n = 10 for i in range(self.n): self.schedule.add(MockAgent(i, self, val=i)) - agent_reporters = {"value": lambda a: a.val, "value2": "val2"} self.initialize_data_collector( model_reporters={ "total_agents": lambda m: m.schedule.get_agent_count(), @@ -72,9 +73,11 @@ def __init__(self): "value": lambda a: a.val, "value2": "val2", "double_value": MockAgent.double_val, - "value_with_params": [agent_function_with_params, [2, 3]] - } + "value_with_params": [agent_function_with_params, [2, 3]], + }, + tables={"Final_Values": ["agent_id", "final_value"]}, ) + def test_model_calc_comp(self, input1, input2): if input2 > 0: return (self.model_val * input1) / input2 @@ -144,13 +147,13 @@ def test_agent_records(self): assert "value_with_params" in list(agent_table.columns) # Check the double_value column - for step, agent_id, value in agent_table["double_value"].items(): - expected_value = agent_id * 2 + for (step, agent_id), value in agent_table["double_value"].items(): + expected_value = (step + agent_id) * 2 self.assertEqual(value, expected_value) # Check the value_with_params column - for step, agent_id, value in agent_table["value_with_params"].items(): - expected_value = (agent_id * 2) + 3 + for (step, agent_id), value in agent_table["value_with_params"].items(): + expected_value = ((step + agent_id) * 2) + 3 self.assertEqual(value, expected_value) assert len(data_collector._agent_records) == 8 @@ -161,7 +164,7 @@ def test_agent_records(self): assert len(records) == 9 for values in records: - assert len(values) == 4 + assert len(values) == 6 assert "value" in list(agent_table.columns) assert "value2" in list(agent_table.columns) @@ -196,7 +199,7 @@ def test_exports(self): agent_vars = data_collector.get_agent_vars_dataframe() table_df = data_collector.get_table_dataframe("Final_Values") assert model_vars.shape == (8, 5) - assert agent_vars.shape == (77, 2) + assert agent_vars.shape == (77, 4) assert table_df.shape == (9, 2) with self.assertRaises(Exception): From 6616cdae8f5c1cfded7609100a2a691137923cd7 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Thu, 19 Oct 2023 14:40:18 +0200 Subject: [PATCH 14/20] DataCollector: Update docs with new agent reporter syntax --- mesa/datacollection.py | 59 +++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/mesa/datacollection.py b/mesa/datacollection.py index 4aa4533a698..8bddfa23da2 100644 --- a/mesa/datacollection.py +++ b/mesa/datacollection.py @@ -56,44 +56,49 @@ def __init__( agent_reporters=None, tables=None, ): - """Instantiate a DataCollector with lists of model and agent reporters. + """ + 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. - For example, if there was only one model-level reporter for number of - agents, it might look like: - {"agent_count": lambda m: m.schedule.get_agent_count() } - If there was only one agent-level reporter (e.g. the agent's energy), - it might look like this: - {"energy": "energy"} - or like this: - {"energy": lambda a: a.energy} + variable name to either an attribute name, a function, a method of a class/instance, + or a function with parameters placed in a list. + + Model reporters can take four types of arguments: + 1. Lambda function: + {"agent_count": lambda m: m.schedule.get_agent_count()} + 2. Method of a class/instance: + {"agent_count": self.get_agent_count} # self here is a class instance + {"agent_count": Model.get_agent_count} # Model here is a class + 3. Class attributes of a model: + {"model_attribute": "model_attribute"} + 4. Functions with parameters that have been placed in a list: + {"Model_Function": [function, [param_1, param_2]]} + + Agent reporters can similarly take: + 1. Attribute name (string) referring to agent's attribute: + {"energy": "energy"} + 2. Lambda function: + {"energy": lambda a: a.energy} + 3. Method of an agent class/instance: + {"agent_action": self.do_action} # self here is an agent class instance + {"agent_action": Agent.do_action} # Agent here is a class + 4. Functions with parameters placed in a list: + {"Agent_Function": [function, [param_1, param_2]]} The tables arg accepts a dictionary mapping names of tables to lists of columns. For example, if we want to allow agents to write their age when they are destroyed (to keep track of lifespans), it might look like: - {"Lifespan": ["unique_id", "age"]} + {"Lifespan": ["unique_id", "age"]} Args: - model_reporters: Dictionary of reporter names and attributes/funcs - agent_reporters: Dictionary of reporter names and attributes/funcs. + model_reporters: Dictionary of reporter names and attributes/funcs/methods. + agent_reporters: Dictionary of reporter names and attributes/funcs/methods. tables: Dictionary of table names to lists of column names. Notes: - If you want to pickle your model you must not use lambda functions. - If your model includes a large number of agents, you should *only* - use attribute names for the agent reporter, it will be much faster. - - Model reporters can take four types of arguments: - lambda like above: - {"agent_count": lambda m: m.schedule.get_agent_count() } - method of a class/instance: - {"agent_count": self.get_agent_count} # self here is a class instance - {"agent_count": Model.get_agent_count} # Model here is a class - class attributes of a model - {"model_attribute": "model_attribute"} - functions with parameters that have placed in a list - {"Model_Function":[function, [param_1, param_2]]} + - If you want to pickle your model you must not use lambda functions. + - If your model includes a large number of agents, it is recommended to + use attribute names for the agent reporter, as it will be faster. """ self.model_reporters = {} self.agent_reporters = {} From cd18cdf440736039be4e5816230d660577c5b36b Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Mon, 23 Oct 2023 11:58:18 +0200 Subject: [PATCH 15/20] CI: Update GHA workflows to Python 3.12 Start testing Python 3.12 in CI --- .github/workflows/build_lint.yml | 12 +++++++----- .github/workflows/release.yml | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_lint.yml b/.github/workflows/build_lint.yml index 9e89ad99b9c..919c07f267c 100644 --- a/.github/workflows/build_lint.yml +++ b/.github/workflows/build_lint.yml @@ -30,8 +30,10 @@ jobs: fail-fast: False matrix: os: [windows, ubuntu, macos] - python-version: ["3.11"] + python-version: ["3.12"] include: + - os: ubuntu + python-version: "3.11" - os: ubuntu python-version: "3.10" - os: ubuntu @@ -64,10 +66,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python 3.10 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - run: pip install ruff==0.0.275 - name: Lint with ruff # Include `--format=github` to enable automatic inline annotations. @@ -78,10 +80,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python 3.10 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - run: pip install black[jupyter] - name: Lint with black run: black --check --exclude=mesa/cookiecutter-mesa/* . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a16e39209af..7600a15de93 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: "3.12" - name: Install dependencies run: pip install -U pip wheel setuptools - name: Build package From d92b7427eb6e5033b4b182c33b71e590f40401b8 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Mon, 23 Oct 2023 16:36:59 +0200 Subject: [PATCH 16/20] Update to Ruff 0.1.1 (#1841) * Update to Ruff 0.1.1 Note that Ruff has adopted a new version policy (https://docs.astral.sh/ruff/versioning/), similar to SemVer. So we can now do >=0.1.1,<0.2.0 to get bugfixes and deprecation warnings, but don't get new (syntax) features. * CI: Use --output-format=github for Ruff in CLI Update the CLI syntax to --output-format=github for Ruff See https://github.com/astral-sh/ruff/pull/8014 * Use ~= syntax for Ruff version --- .github/workflows/build_lint.yml | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_lint.yml b/.github/workflows/build_lint.yml index 919c07f267c..ab9f22cdade 100644 --- a/.github/workflows/build_lint.yml +++ b/.github/workflows/build_lint.yml @@ -70,11 +70,11 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.12" - - run: pip install ruff==0.0.275 + - run: pip install ruff~=0.1.1 # Update periodically - name: Lint with ruff # Include `--format=github` to enable automatic inline annotations. # Use settings from pyproject.toml. - run: ruff . --format=github --extend-exclude 'mesa/cookiecutter-mesa/*' + run: ruff . --output-format=github --extend-exclude 'mesa/cookiecutter-mesa/*' lint-black: runs-on: ubuntu-latest diff --git a/setup.py b/setup.py index 73e82f80da8..6024165dc50 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ extras_require = { "dev": [ "black", - "ruff==0.0.275", + "ruff~=0.1.1", # Update periodically "coverage", "pytest >= 4.6", "pytest-cov", From b58c63400c44bb6d93d6e6ef46a270a49399b7d2 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 19 Oct 2023 00:14:19 -0400 Subject: [PATCH 17/20] docs: Rename useful-snippets to how-to guide --- docs/{useful-snippets/snippets.rst => howto.rst} | 6 +++--- docs/index.rst | 2 +- docs/tutorials/intro_tutorial.ipynb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename docs/{useful-snippets/snippets.rst => howto.rst} (94%) diff --git a/docs/useful-snippets/snippets.rst b/docs/howto.rst similarity index 94% rename from docs/useful-snippets/snippets.rst rename to docs/howto.rst index 728c4e65f19..09792af5bbd 100644 --- a/docs/useful-snippets/snippets.rst +++ b/docs/howto.rst @@ -1,7 +1,7 @@ -Useful Snippets -=============== +How-to Guide +============ -A collection of useful code snippets. Here you can find code that allows you to get to get started on common tasks in Mesa. +Here you can find code that allows you to get to get started on common tasks in Mesa. Models with Discrete Time ------------------------- diff --git a/docs/index.rst b/docs/index.rst index 6b2e4831ccd..83382e1bdaa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -98,7 +98,7 @@ ABM features users have shared that you may want to use in your model tutorials/intro_tutorial tutorials/visualization_tutorial Best Practices - Useful Snippets + How-to Guide API Documentation Mesa Packages tutorials/adv_tutorial_legacy.ipynb diff --git a/docs/tutorials/intro_tutorial.ipynb b/docs/tutorials/intro_tutorial.ipynb index 1aa8d4d1c4b..a77f4a13610 100644 --- a/docs/tutorials/intro_tutorial.ipynb +++ b/docs/tutorials/intro_tutorial.ipynb @@ -1263,7 +1263,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**Note for Windows OS users:** If you are running this tutorial in Jupyter, make sure that you set `number_processes = 1` (single process). If `number_processes` is greater than 1, it is less straightforward to set up. You can read [Mesa's collection of useful snippets](https://github.com/projectmesa/mesa/blob/main/docs/useful-snippets/snippets.rst), in 'Using multi-process `batch_run` on Windows' section for how to do it." + "**Note for Windows OS users:** If you are running this tutorial in Jupyter, make sure that you set `number_processes = 1` (single process). If `number_processes` is greater than 1, it is less straightforward to set up. You can read [Mesa's how-to guide](https://github.com/projectmesa/mesa/blob/main/docs/howto.rst), in 'Using multi-process `batch_run` on Windows' section for how to do it." ] }, { From 3b580a38690d72a5b7e9fcaeab5151950a004b0b Mon Sep 17 00:00:00 2001 From: Rebecca Sutton Koeser Date: Wed, 25 Oct 2023 02:47:35 -0400 Subject: [PATCH 18/20] fix: Configure change handler for checkbox input (#1844) --- mesa/experimental/jupyter_viz.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py index 3408ba11c6a..de207bf2926 100644 --- a/mesa/experimental/jupyter_viz.py +++ b/mesa/experimental/jupyter_viz.py @@ -228,6 +228,7 @@ def change_handler(value, name=name): elif input_type == "Checkbox": solara.Checkbox( label=label, + on_value=change_handler, value=options.get("value"), ) else: From 9cac22d37158c2c94ef77546c966241880574952 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Sat, 29 Oct 2022 15:21:22 +0200 Subject: [PATCH 19/20] release CI: Run on all pushes and PRs, only publish on tag Currently the release CI only run when a GitHub Release is created. This PR modifies that is runs on each PR and push, and uploads. It uses the official action from PyPI: https://github.com/pypa/gh-action-pypi-publish It also now uses build to build the wheel, instead of calling setup.py directly, which is deprecated. --- .github/workflows/release.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7600a15de93..0f35c891e85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,5 @@ name: Release -on: - release: - types: - - published +on: [push, pull_request, workflow_dispatch] permissions: id-token: write @@ -19,16 +16,17 @@ jobs: with: python-version: "3.12" - name: Install dependencies - run: pip install -U pip wheel setuptools - - name: Build package - run: python setup.py sdist bdist_wheel + run: pip install -U pip build wheel setuptools + - name: Build distributions + run: python -m build - name: Upload package as artifact to GitHub + if: github.repository == 'projectmesa/mesa' && startsWith(github.ref, 'refs/tags') uses: actions/upload-artifact@v3 with: name: package path: dist/ - - name: Upload packages to PyPI + - name: Publish package to PyPI + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ - password: ${{ secrets.pypi_password }} + password: ${{ secrets.PYPI_API_TOKEN }} From adc55499874bc648e56e0ba4d2d087c75f195a73 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Sat, 28 Oct 2023 12:04:18 +0200 Subject: [PATCH 20/20] Release CI: Run on pushes only to main and release branches Same configuration as in build_lint.yml --- .github/workflows/release.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f35c891e85..1b401a050b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,17 @@ name: Release -on: [push, pull_request, workflow_dispatch] +on: + push: + branches: + - main + - release** + paths-ignore: + - '**.md' + - '**.rst' + pull_request: + paths-ignore: + - '**.md' + - '**.rst' + workflow_dispatch: permissions: id-token: write