Repository: projectmesa/mesa
Pull request #1757 (2023-08-07 13:23:29)

Docker: Update to use Solara viz
Not yet tested. Note that the current Docker setup is broken because the examples folder is now gone (moved to mesa-examples repo).

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/projectmesa/mesa/pull/1757?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) Report
Patch and project coverage have no change.
> Comparison is base [(`1ea0dc7`)](https://app.codecov.io/gh/projectmesa/mesa/commit/1ea0dc7960ceb7175d5f967de841fc2c101b9f73?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) 80.66% compared to head [(`24d7f4a`)](https://app.codecov.io/gh/projectmesa/mesa/pull/1757?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) 80.66%.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1757   +/-   ##
=======================================
  Coverage   80.66%   80.66%           
=======================================
  Files          15       15           
  Lines         869      869           
  Branches      183      183           
=======================================
  Hits          701      701           
  Misses        147      147           
  Partials       21       21           
```




</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/projectmesa/mesa/pull/1757?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).

tpike3: > Not yet tested. Note that the current Docker setup is broken because the examples folder is now gone (moved to mesa-examples repo).

You are absolutely correct, thanks for catching this!
rht: @tpike3 this is ready to merge. I have tested locally.
rht: @tpike3 ?

Patch:
diff --git a/Dockerfile b/Dockerfile
index 4d9fed52..61348e69 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,20 +1,25 @@
-FROM python:3.10-slim
+# We can't use slim because we need either git/wget/curl to
+# download mesa-examples, and so installing them requires
+# updating the system anyway.
+# We can't use alpine because NumPy doesn't support musllinux yet.
+# But it's in the RC version https://github.com/numpy/numpy/issues/20089.
+FROM python:bookworm
 LABEL maintainer="rht <rhtbot@protonmail.com>"
 
 # To use this Dockerfile:
 # 1. `docker build . -t mymesa_image`
-# 2. `docker run --name mymesa_instance -p 8521:8521 -it mymesa_image`
-# 3. In your browser, visit http://127.0.0.1:8521
+# 2. `docker run --name mymesa_instance -p 8765:8765 -it mymesa_image`
+# 3. In your browser, visit http://127.0.0.1:8765
 #
-# Currently, this Dockerfile defaults to running the Wolf-Sheep model, as an
+# Currently, this Dockerfile defaults to running the Schelling model, as an
 # illustration. If you want to run a different example, simply change the
 # MODEL_DIR variable below to point to another model, e.g.
-# examples/sugarscape_cg or path to your custom model.
+# /mesa-examples/examples/sugarscape_cg or path to your custom model.
 # You specify the MODEL_DIR (relative to this Git repo) by doing:
-# `docker run --name mymesa_instance -p 8521:8521 -e MODEL_DIR=examples/sugarscape_cg -it mymesa_image`
-# Note: the model directory MUST contain a run.py file.
+# `docker run --name mymesa_instance -p 8765:8765 -e MODEL_DIR=/mesa-examples/examples/sugarscape_cg -it mymesa_image`
+# Note: the model directory MUST contain an app.py file.
 
-ENV MODEL_DIR=examples/wolf_sheep
+ENV MODEL_DIR=/mesa-examples/examples/schelling_experimental
 
 # Don't buffer output:
 # https://docs.python.org/3.10/using/cmdline.html?highlight=pythonunbuffered#envvar-PYTHONUNBUFFERED
@@ -24,21 +29,10 @@ WORKDIR /opt/mesa
 
 COPY . /opt/mesa
 
-EXPOSE 8521/tcp
-
-# Important: we don't install python3-dev, python3-pip and so on because doing
-# so will install Python 3.9 instead of the already available Python 3.10 from
-# the base image.
-# The following RUN command is still provided for context.
-# RUN apt-get update && apt-get upgrade -y -o Dpkg::Options::="--force-confold" \
-#    && apt-get install -y --no-install-recommends \
-#    build-essential \
-#    python3-dev \
-#    python3-pip \
-#    python3-setuptools \
-#    python3-wheel \
-#    && rm -rf /var/lib/apt/lists/*
+RUN cd / && git clone https://github.com/projectmesa/mesa-examples.git
+
+EXPOSE 8765/tcp
 
 RUN pip3 install -e /opt/mesa
 
-CMD ["sh", "-c", "cd $MODEL_DIR && python3 run.py"]
+CMD ["sh", "-c", "cd $MODEL_DIR && solara run app.py --host=0.0.0.0"]
diff --git a/HISTORY.rst b/HISTORY.rst
index 929e0f5b..471e0947 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -37,7 +37,7 @@ Mesa 2.0 includes:
     * an improved `datacollector` that allows collection by agent type
     * several breaking changes that provide significant improvements to Mesa.
 
-.. _visualization tutorial: https://mesa.readthedocs.io/en/latest/tutorials/visualization_tutorial.html
+.. _visualization tutorial: https://mesa.readthedocs.io/en/stable/tutorials/visualization_tutorial.html
 **Breaking Changes:**
 
 * space: change `coord_iter` to return `(content,(x,y))` instead of `(content, x,y)`; this reduces known errors of scheduler to grid mismatch #1566, #1723
@@ -47,7 +47,7 @@ Mesa 2.0 includes:
     * `find_empty()`: convert this to `move_to_empty()`
     * `num_agents`: removed parameter from `move_to_empty()`
     * `position_agent()`: convert this to `place_agent`
-    * `neighbor_iter()`: convert this to `iter_neighborhood()`
+    * `neighbor_iter()`: convert this to `iter_neighbors()`
 * batchrunner: remove deprecations #1627
     * `class BatchRunner` and `class BatchRunnerMP`: convert these to `batch_run()`
     * Please see this `batch_run() example`_ if you would like to see an an implementation.
diff --git a/README.rst b/README.rst
index e4fffb3e..b3278af6 100644
--- a/README.rst
+++ b/README.rst
@@ -65,10 +65,10 @@ For resources or help on using Mesa, check out the following:
 * `Discussions`_ (GitHub threaded discussions about Mesa)
 * `Matrix Chat`_ (Chat Forum via Matrix to talk about Mesa)
 
-.. _`Intro to Mesa Tutorial` : http://mesa.readthedocs.org/en/main/tutorials/intro_tutorial.html
+.. _`Intro to Mesa Tutorial` : http://mesa.readthedocs.org/en/stable/tutorials/intro_tutorial.html
 .. _`Complexity Explorer Tutorial` : https://www.complexityexplorer.org/courses/172-agent-based-models-with-python-an-introduction-to-mesa
 .. _`Mesa Examples` : https://github.com/projectmesa/mesa-examples/tree/main/examples
-.. _`Docs` : http://mesa.readthedocs.org/en/main/
+.. _`Docs` : http://mesa.readthedocs.org/
 .. _`Discussions` : https://github.com/projectmesa/mesa/discussions
 .. _`Matrix Chat` : https://matrix.to/#/#project-mesa:matrix.org
 
@@ -85,21 +85,21 @@ If you are a Mesa developer, first `install Docker Compose <https://docs.docker.
     # If you want to make it run in the background, you instead run
     $ docker compose up -d
 
-This runs the wolf-sheep predation model, as an example.
+This runs the Schelling model, as an example.
 
 With the docker-compose.yml file in this Git repository, the `docker compose up` command does two important things:
 
 * It mounts the mesa root directory (relative to the docker-compose.yml file) into /opt/mesa and runs pip install -e on that directory so your changes to mesa should be reflected in the running container.
-* It binds the docker container's port 8521 to your host system's port 8521 so you can interact with the running model as usual by visiting localhost:8521 on your browser
+* It binds the docker container's port 8765 to your host system's port 8765 so you can interact with the running model as usual by visiting localhost:8765 on your browser
 
 
 If you are a model developer that wants to run Mesa on a model, you need to:
 
 * make sure that your model folder is inside the folder containing the docker-compose.yml file
 * change the ``MODEL_DIR`` variable in docker-compose.yml to point to the path of your model
-* make sure that the model folder contains a run.py file
+* make sure that the model folder contains an app.py file
 
-Then, you just need to run `docker compose up -d` to make it accessible from ``localhost:8521``.
+Then, you just need to run `docker compose up -d` to have it accessible from ``localhost:8765``.
 
 Contributing to Mesa
 ----------------------------
diff --git a/docker-compose.yml b/docker-compose.yml
index 22ab87d9..9b8e165f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,6 +12,10 @@ services:
     volumes:
       - .:/opt/mesa
     environment:
-      MODEL_DIR: examples/wolf_sheep
+      # You may replace this with any model directory located
+      # within the current directory.
+      # E.g. if it is at my-model, then you specify it as
+      # /opt/mesa/my-model.
+      MODEL_DIR: /mesa-examples/examples/schelling_experimental
     ports:
-      - "127.0.0.1:8521:8521"
+      - 8765:8765
diff --git a/docs/tutorials/adv_tutorial_legacy.ipynb b/docs/tutorials/adv_tutorial_legacy.ipynb
index c05d5819..c0c26e42 100644
--- a/docs/tutorials/adv_tutorial_legacy.ipynb
+++ b/docs/tutorials/adv_tutorial_legacy.ipynb
@@ -29,7 +29,7 @@
    "source": [
     "#### Grid Visualization\n",
     "\n",
-    "To start with, let's have a visualization where we can watch the agents moving around the grid. For this, you will need to put your model code in a separate Python source file. For now, let us use the `MoneyModel` created in the [Introductory Tutorial](https://mesa.readthedocs.io/en/main/tutorials/intro_tutorial.html) saved to `MoneyModel.py` file provided.\n",
+    "To start with, let's have a visualization where we can watch the agents moving around the grid. For this, you will need to put your model code in a separate Python source file. For now, let us use the `MoneyModel` created in the [Introductory Tutorial](https://mesa.readthedocs.io/en/stable/tutorials/intro_tutorial.html) saved to `MoneyModel.py` file provided.\n",
     "Next, in a new source file (e.g. `MoneyModel_Viz.py`) include the code shown in the following cells to run and avoid Jupyter compatibility issue."
    ]
   },
diff --git a/docs/tutorials/visualization_tutorial.ipynb b/docs/tutorials/visualization_tutorial.ipynb
index 37564241..f19b2142 100644
--- a/docs/tutorials/visualization_tutorial.ipynb
+++ b/docs/tutorials/visualization_tutorial.ipynb
@@ -28,7 +28,7 @@
    "source": [
     "#### Grid Visualization\n",
     "\n",
-    "To start with, let's have a visualization where we can watch the agents moving around the grid. Let us use the same `MoneyModel` created in the [Introductory Tutorial](https://mesa.readthedocs.io/en/main/tutorials/intro_tutorial.html).\n"
+    "To start with, let's have a visualization where we can watch the agents moving around the grid. Let us use the same `MoneyModel` created in the [Introductory Tutorial](https://mesa.readthedocs.io/en/stable/tutorials/intro_tutorial.html).\n"
    ]
   },
   {
@@ -198,18 +198,16 @@
     "from matplotlib.figure import Figure\n",
     "\n",
     "\n",
-    "def make_histogram(viz):\n",
+    "def make_histogram(model):\n",
     "    # Note: you must initialize a figure using this method instead of\n",
     "    # plt.figure(), for thread safety purpose\n",
     "    fig = Figure()\n",
     "    ax = fig.subplots()\n",
-    "    wealth_vals = [agent.wealth for agent in viz.model.schedule.agents]\n",
+    "    wealth_vals = [agent.wealth for agent in model.schedule.agents]\n",
     "    # Note: you have to use Matplotlib's OOP API instead of plt.hist\n",
     "    # because plt.hist is not thread-safe.\n",
     "    ax.hist(wealth_vals, bins=10)\n",
-    "    # You have to specify the dependencies as follows, so that the figure\n",
-    "    # auto-updates when viz.model or viz.df is changed.\n",
-    "    solara.FigureMatplotlib(fig, dependencies=[viz.model, viz.df])"
+    "    solara.FigureMatplotlib(fig)"
    ]
   },
   {
diff --git a/mesa/datacollection.py b/mesa/datacollection.py
index d383f018..dbd52793 100644
--- a/mesa/datacollection.py
+++ b/mesa/datacollection.py
@@ -36,7 +36,6 @@ The default DataCollector here makes several assumptions:
 """
 import itertools
 import types
-from operator import attrgetter
 
 import pandas as pd
 
@@ -138,7 +137,7 @@ class DataCollector:
             reporter: Attribute string, or function object that returns the
                       variable when given a model instance.
         """
-        if type(reporter) is str:
+        if isinstance(reporter, str):
             attribute_name = reporter
 
             def reporter(agent):
@@ -175,17 +174,10 @@ class DataCollector:
             agent_records_without_none = (r for r in agent_records if r is not None)
             return agent_records_without_none
 
-        if all(hasattr(rep, "attribute_name") for rep in rep_funcs):
-            # This branch is for performance optimization purpose.
-            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
+        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)
         return agent_records
diff --git a/mesa/experimental/jupyter_viz.py b/mesa/experimental/jupyter_viz.py
index a7eefb51..96970b8c 100644
--- a/mesa/experimental/jupyter_viz.py
+++ b/mesa/experimental/jupyter_viz.py
@@ -89,15 +89,25 @@ def ModelController(
 ):
     playing = solara.use_reactive(False)
     thread = solara.use_reactive(None)
+    # We track the previous step to detect if user resets the model via
+    # clicking the reset button or changing the parameters. If previous_step >
+    # current_step, it means a model reset happens while the simulation is
+    # still playing.
+    previous_step = solara.use_reactive(0)
 
     def on_value_play(change):
-        if model.running:
+        if previous_step.value > current_step and current_step == 0:
+            # We add extra checks for current_step == 0, just to be sure.
+            # We automatically stop the playing if a model is reset.
+            playing.value = False
+        elif model.running:
             do_step()
         else:
             playing.value = False
 
     def do_step():
         model.step()
+        previous_step.value = current_step
         set_current_step(model.schedule.steps)
 
     def do_play():
@@ -174,7 +184,7 @@ def check_param_is_fixed(param):
 def UserInputs(user_params, on_change=None):
     """Initialize user inputs for configurable model parameters.
     Currently supports :class:`solara.SliderInt`, :class:`solara.SliderFloat`,
-    and :class:`solara.Select`.
+    :class:`solara.Select`, and :class:`solara.Checkbox`.
 
     Props:
         user_params: dictionary with options for the input, including label,
@@ -215,6 +225,11 @@ def UserInputs(user_params, on_change=None):
                 on_value=change_handler,
                 values=options.get("values"),
             )
+        elif input_type == "Checkbox":
+            solara.Checkbox(
+                label=label,
+                value=options.get("value"),
+            )
         else:
             raise ValueError(f"{input_type} is not a supported input type")
 


Test patch:
diff --git a/tests/test_jupyter_viz.py b/tests/test_jupyter_viz.py
index f1d7371c..97021252 100644
--- a/tests/test_jupyter_viz.py
+++ b/tests/test_jupyter_viz.py
@@ -45,6 +45,19 @@ class TestMakeUserInput(unittest.TestCase):
         assert slider_int.max == options["max"]
         assert slider_int.step == options["step"]
 
+    def test_checkbox(self):
+        @solara.component
+        def Test(user_params):
+            UserInputs(user_params)
+
+        options = {"type": "Checkbox", "value": True, "label": "On"}
+        user_params = {"num_agents": options}
+        _, rc = solara.render(Test(user_params), handle_error=False)
+        checkbox = rc.find(vw.Checkbox).widget
+
+        assert checkbox.v_model == options["value"]
+        assert checkbox.label == options["label"]
+
     def test_label_fallback(self):
         """name should be used as fallback label"""