From 75134fb98ed20c1a35595c78ae2d834b68b7abb5 Mon Sep 17 00:00:00 2001 From: rht Date: Tue, 5 Mar 2024 03:04:51 -0500 Subject: [PATCH 1/6] feat: Show model seed in render_in_browser output --- mesa/experimental/jupyter_viz.py | 44 +++++++++++++++++++++++--------- tests/test_jupyter_viz.py | 3 ++- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py index 9d29e08fb60..e13038c60ee 100644 --- a/mesa/experimental/jupyter_viz.py +++ b/mesa/experimental/jupyter_viz.py @@ -17,7 +17,7 @@ # TODO: Turn this function into a Solara component once the current_step.value # dependency is passed to measure() def Card( - model, measures, agent_portrayal, space_drawer, current_step, color, layout_type + model, measures, agent_portrayal, space_drawer, dependencies, color, layout_type ): with rv.Card( style_=f"background-color: {color}; width: 100%; height: 100%" @@ -27,11 +27,11 @@ def Card( if space_drawer == "default": # draw with the default implementation components_matplotlib.SpaceMatplotlib( - model, agent_portrayal, dependencies=[current_step.value] + model, agent_portrayal, dependencies=dependencies ) elif space_drawer == "altair": components_altair.SpaceAltair( - model, agent_portrayal, dependencies=[current_step.value] + model, agent_portrayal, dependencies=dependencies ) elif space_drawer: # if specified, draw agent space with an alternate renderer @@ -44,7 +44,7 @@ def Card( measure(model) else: components_matplotlib.PlotMatplotlib( - model, measure, dependencies=[current_step.value] + model, measure, dependencies=dependencies ) return main @@ -58,6 +58,7 @@ def JupyterViz( agent_portrayal=None, space_drawer="default", play_interval=150, + seed=None, ): """Initialize a component to visualize a model. Args: @@ -71,6 +72,7 @@ def JupyterViz( simulations with no space to visualize should specify `space_drawer=False` play_interval: play interval (default: 150) + seed: the random seed used to initialize the model """ if name is None: name = model_class.__name__ @@ -78,6 +80,7 @@ def JupyterViz( current_step = solara.use_reactive(0) # 1. Set up model parameters + reactive_seed = solara.use_reactive(0) user_params, fixed_params = split_model_params(model_params) model_parameters, set_model_parameters = solara.use_state( {**fixed_params, **{k: v.get("value") for k, v in user_params.items()}} @@ -85,13 +88,18 @@ def JupyterViz( # 2. Set up Model def make_model(): - model = model_class(**model_parameters) + model = model_class(**model_parameters, seed=reactive_seed.value) current_step.value = 0 return model reset_counter = solara.use_reactive(0) model = solara.use_memo( - make_model, dependencies=[*list(model_parameters.values()), reset_counter.value] + make_model, + dependencies=[ + *list(model_parameters.values()), + reset_counter.value, + reactive_seed.value, + ], ) def handle_change_model_params(name: str, value: any): @@ -103,8 +111,12 @@ def handle_change_model_params(name: str, value: any): solara.AppBarTitle(name) # render layout and plot + def do_reseed(): + reactive_seed.value = model.random.random() # jupyter + dependencies = [current_step.value, reactive_seed.value] + def render_in_jupyter(): with solara.GridFixed(columns=2): UserInputs(user_params, on_change=handle_change_model_params) @@ -116,11 +128,11 @@ def render_in_jupyter(): if space_drawer == "default": # draw with the default implementation components_matplotlib.SpaceMatplotlib( - model, agent_portrayal, dependencies=[current_step.value] + model, agent_portrayal, dependencies=dependencies ) elif space_drawer == "altair": components_altair.SpaceAltair( - model, agent_portrayal, dependencies=[current_step.value] + model, agent_portrayal, dependencies=dependencies ) elif space_drawer: # if specified, draw agent space with an alternate renderer @@ -134,7 +146,7 @@ def render_in_jupyter(): measure(model) else: components_matplotlib.PlotMatplotlib( - model, measure, dependencies=[current_step.value] + model, measure, dependencies=dependencies ) def render_in_browser(): @@ -149,10 +161,18 @@ def render_in_browser(): with solara.Sidebar(): with solara.Card("Controls", margin=1, elevation=2): + solara.InputText( + label="Seed", + value=reactive_seed, + continuous_update=True, + ) UserInputs(user_params, on_change=handle_change_model_params) ModelController(model, play_interval, current_step, reset_counter) - with solara.Card("Progress", margin=1, elevation=2): - solara.Markdown(md_text=f"####Step - {current_step}") + solara.Button(label="Reseed", color="primary", on_click=do_reseed) + with solara.Card("Information", margin=1, elevation=2): + solara.Markdown( + md_text=f"Step - {current_step}" + ) items = [ Card( @@ -160,7 +180,7 @@ def render_in_browser(): measures, agent_portrayal, space_drawer, - current_step, + dependencies, color="white", layout_type=layout_types[i], ) diff --git a/tests/test_jupyter_viz.py b/tests/test_jupyter_viz.py index 248e8c319d5..512c6a4aaaf 100644 --- a/tests/test_jupyter_viz.py +++ b/tests/test_jupyter_viz.py @@ -91,7 +91,8 @@ def test_call_space_drawer(self, mock_space_matplotlib): "color": "gray", } current_step = 0 - dependencies = [current_step] + seed = 0 + dependencies = [current_step, seed] # initialize with space drawer unspecified (use default) # component must be rendered for code to run solara.render( From e9fb378d4c06425f8b751293b01c46eccb76ce30 Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 7 Mar 2024 03:34:52 -0500 Subject: [PATCH 2/6] Apply workaround for model_class __new__ not being executed by default --- mesa/experimental/jupyter_viz.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py index e13038c60ee..1662711a2bb 100644 --- a/mesa/experimental/jupyter_viz.py +++ b/mesa/experimental/jupyter_viz.py @@ -88,7 +88,8 @@ def JupyterViz( # 2. Set up Model def make_model(): - model = model_class(**model_parameters, seed=reactive_seed.value) + model = model_class.__new__(model_class, **model_parameters, seed=reactive_seed.value) + model.__init__(**model_parameters) current_step.value = 0 return model From f6452263fcf39f043762a729923567fae614a3ab Mon Sep 17 00:00:00 2001 From: rht Date: Wed, 6 Mar 2024 00:14:28 -0500 Subject: [PATCH 3/6] Restyle play+stop button to be Solara blue color --- mesa/experimental/jupyter_viz.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py index 1662711a2bb..7d8babe6316 100644 --- a/mesa/experimental/jupyter_viz.py +++ b/mesa/experimental/jupyter_viz.py @@ -259,10 +259,13 @@ def do_set_playing(value): solara.Button(label="Step", color="primary", on_click=do_step) # This style is necessary so that the play widget has almost the same # height as typical Solara buttons. - solara.Style( - """ + solara.Style(""" .widget-play { - height: 30px; + height: 35px; + } + .widget-play button { + color: white; + background-color: #1976D2; // Solara blue color } """ ) From 29f619416e7d451fc5075b398028a859ae6b7b9f Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 7 Mar 2024 03:30:44 -0500 Subject: [PATCH 4/6] Simplify initial grid layout creation --- mesa/experimental/jupyter_viz.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py index 7d8babe6316..611b31bd23b 100644 --- a/mesa/experimental/jupyter_viz.py +++ b/mesa/experimental/jupyter_viz.py @@ -157,7 +157,7 @@ def render_in_browser(): if measures: layout_types += [{"Measure": elem} for elem in range(len(measures))] - grid_layout_initial = get_initial_grid_layout(layout_types=layout_types) + grid_layout_initial = make_initial_grid_layout(layout_types=layout_types) grid_layout, set_grid_layout = solara.use_state(grid_layout_initial) with solara.Sidebar(): @@ -385,20 +385,15 @@ def function(model): return function -def get_initial_grid_layout(layout_types): - grid_lay = [] - y_coord = 0 - for ii in range(len(layout_types)): - template_layout = {"h": 10, "i": 0, "moved": False, "w": 6, "y": 0, "x": 0} - if ii == 0: - grid_lay.append(template_layout) - else: - template_layout.update({"i": ii}) - if ii % 2 == 0: - template_layout.update({"x": 0}) - y_coord += 16 - else: - template_layout.update({"x": 6}) - template_layout.update({"y": y_coord}) - grid_lay.append(template_layout) - return grid_lay +def make_initial_grid_layout(layout_types): + return [ + { + "i": i, + "w": 6, + "h": 10, + "moved": False, + "x": 6 * (i % 2), + "y": 16 * (i - i % 2), + } + for i in range(len(layout_types)) + ] From c6bcc6ca37f9861adf7e1c09470df10c4a69c40b Mon Sep 17 00:00:00 2001 From: rht Date: Thu, 14 Mar 2024 20:34:30 -0400 Subject: [PATCH 5/6] Switch test_call_space_drawer to using pytest This is because the unittest version is not sufficient to handle patching the `__new__` method. --- pyproject.toml | 1 + tests/test_jupyter_viz.py | 93 ++++++++++++++++++++------------------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0233bc5874b..087bf4cb5d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ dev = [ "pytest >= 4.6", "pytest-cov", "sphinx", + "pytest-mock", ] docs = [ "sphinx", diff --git a/tests/test_jupyter_viz.py b/tests/test_jupyter_viz.py index 512c6a4aaaf..47315f0b22a 100644 --- a/tests/test_jupyter_viz.py +++ b/tests/test_jupyter_viz.py @@ -4,6 +4,7 @@ import ipyvuetify as vw import solara +import mesa from mesa.experimental.jupyter_viz import JupyterViz, Slider, UserInputs @@ -81,59 +82,61 @@ def Test(user_params): assert slider_int.step is None -class TestJupyterViz(unittest.TestCase): - @patch("mesa.experimental.components.matplotlib.SpaceMatplotlib") - def test_call_space_drawer(self, mock_space_matplotlib): - mock_model_class = Mock() - mock_model_class.__name__ = "MockModelClass" - agent_portrayal = { - "Shape": "circle", - "color": "gray", - } - current_step = 0 - seed = 0 - dependencies = [current_step, seed] - # initialize with space drawer unspecified (use default) - # component must be rendered for code to run - solara.render( - JupyterViz( - model_class=mock_model_class, - model_params={}, - agent_portrayal=agent_portrayal, - ) +def test_call_space_drawer(mocker): + mock_space_matplotlib = mocker.patch("mesa.experimental.components.matplotlib.SpaceMatplotlib") + + model = mesa.Model() + mocker.patch.object(mesa.Model, "__new__", return_value=model) + mocker.patch.object(mesa.Model, "__init__", return_value=None) + + agent_portrayal = { + "Shape": "circle", + "color": "gray", + } + current_step = 0 + seed = 0 + dependencies = [current_step, seed] + # initialize with space drawer unspecified (use default) + # component must be rendered for code to run + solara.render( + JupyterViz( + model_class=mesa.Model, + model_params={}, + agent_portrayal=agent_portrayal, ) - # should call default method with class instance and agent portrayal - mock_space_matplotlib.assert_called_with( - mock_model_class.return_value, agent_portrayal, dependencies=dependencies - ) - - # specify no space should be drawn; any false value should work - for falsy_value in [None, False, 0]: - mock_space_matplotlib.reset_mock() - solara.render( - JupyterViz( - model_class=mock_model_class, - model_params={}, - agent_portrayal=agent_portrayal, - space_drawer=falsy_value, - ) - ) - # should call default method with class instance and agent portrayal - assert mock_space_matplotlib.call_count == 0 - - # specify a custom space method - altspace_drawer = Mock() + ) + # should call default method with class instance and agent portrayal + mock_space_matplotlib.assert_called_with( + model, agent_portrayal, dependencies=dependencies + ) + + # specify no space should be drawn; any false value should work + for falsy_value in [None, False, 0]: + mock_space_matplotlib.reset_mock() solara.render( JupyterViz( - model_class=mock_model_class, + model_class=mesa.Model, model_params={}, agent_portrayal=agent_portrayal, - space_drawer=altspace_drawer, + space_drawer=falsy_value, ) ) - altspace_drawer.assert_called_with( - mock_model_class.return_value, agent_portrayal + # should call default method with class instance and agent portrayal + assert mock_space_matplotlib.call_count == 0 + + # specify a custom space method + altspace_drawer = Mock() + solara.render( + JupyterViz( + model_class=mesa.Model, + model_params={}, + agent_portrayal=agent_portrayal, + space_drawer=altspace_drawer, ) + ) + altspace_drawer.assert_called_with( + model, agent_portrayal + ) def test_slider(): From 9e30ab508ab75f8b39d32d22ac74b09dd2b82e40 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 00:40:26 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/jupyter_viz.py | 11 ++++++----- tests/test_jupyter_viz.py | 10 +++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py index 611b31bd23b..a4dec7f6a9a 100644 --- a/mesa/experimental/jupyter_viz.py +++ b/mesa/experimental/jupyter_viz.py @@ -88,7 +88,9 @@ def JupyterViz( # 2. Set up Model def make_model(): - model = model_class.__new__(model_class, **model_parameters, seed=reactive_seed.value) + model = model_class.__new__( + model_class, **model_parameters, seed=reactive_seed.value + ) model.__init__(**model_parameters) current_step.value = 0 return model @@ -171,9 +173,7 @@ def render_in_browser(): ModelController(model, play_interval, current_step, reset_counter) solara.Button(label="Reseed", color="primary", on_click=do_reseed) with solara.Card("Information", margin=1, elevation=2): - solara.Markdown( - md_text=f"Step - {current_step}" - ) + solara.Markdown(md_text=f"Step - {current_step}") items = [ Card( @@ -259,7 +259,8 @@ def do_set_playing(value): solara.Button(label="Step", color="primary", on_click=do_step) # This style is necessary so that the play widget has almost the same # height as typical Solara buttons. - solara.Style(""" + solara.Style( + """ .widget-play { height: 35px; } diff --git a/tests/test_jupyter_viz.py b/tests/test_jupyter_viz.py index 47315f0b22a..f67daa09518 100644 --- a/tests/test_jupyter_viz.py +++ b/tests/test_jupyter_viz.py @@ -1,5 +1,5 @@ import unittest -from unittest.mock import Mock, patch +from unittest.mock import Mock import ipyvuetify as vw import solara @@ -83,7 +83,9 @@ def Test(user_params): def test_call_space_drawer(mocker): - mock_space_matplotlib = mocker.patch("mesa.experimental.components.matplotlib.SpaceMatplotlib") + mock_space_matplotlib = mocker.patch( + "mesa.experimental.components.matplotlib.SpaceMatplotlib" + ) model = mesa.Model() mocker.patch.object(mesa.Model, "__new__", return_value=model) @@ -134,9 +136,7 @@ def test_call_space_drawer(mocker): space_drawer=altspace_drawer, ) ) - altspace_drawer.assert_called_with( - model, agent_portrayal - ) + altspace_drawer.assert_called_with(model, agent_portrayal) def test_slider():