Repository: projectmesa/mesa
Pull request #2510 (2024-11-16 22:08:32)

Updated docs and check_model param
## Summary
This PR adds documentation for the SolaraViz instantiation and adds a new test case to handle arguments and keyword arguments in solaraviz.
## Bug / Issue
 SolaraViz docs #2494 

## Implementation
changes in docs and explicit check in _check_model_params.

## Testing
<!-- Detail the testing performed to verify the fix. Include information on test cases, steps taken, and any relevant results.

If you're fixing the visualisation, add before/after screenshots. -->
![Screenshot from 2024-11-17 16-30-08](https://github.com/user-attachments/assets/ae09074c-61b4-4cf0-899e-194c9e62891a)


## Additional Notes
<!-- Add any additional information that may be relevant for the reviewers, su


github-actions[bot]: Performance benchmarks:

| Model           | Size   | Init time [95% CI]      | Run time [95% CI]       |
|-----------------|--------|-------------------------|-------------------------|
| BoltzmannWealth | small  | 🔵 -0.9% [-2.9%, +1.2%] | 🔵 -0.2% [-0.3%, +0.0%] |
| BoltzmannWealth | large  | 🔵 -0.4% [-0.9%, +0.2%] | 🔵 +0.1% [-0.5%, +0.8%] |
| Schelling       | small  | 🔵 -1.3% [-1.6%, -1.0%] | 🔵 -0.4% [-0.7%, -0.1%] |
| Schelling       | large  | 🔵 -1.2% [-1.6%, -0.7%] | 🔵 -0.6% [-1.1%, -0.0%] |
| WolfSheep       | small  | 🔵 -1.0% [-1.3%, -0.7%] | 🔵 -0.4% [-0.5%, -0.2%] |
| WolfSheep       | large  | 🔵 -1.1% [-1.6%, -0.6%] | 🔵 -0.2% [-1.1%, +0.9%] |
| BoidFlockers    | small  | 🔵 -0.2% [-0.7%, +0.4%] | 🔵 -0.3% [-1.4%, +0.8%] |
| BoidFlockers    | large  | 🔵 -0.8% [-1.2%, -0.4%] | 🔵 +0.5% [+0.0%, +1.0%] |

EwoutH: Thanks for your PR!

Could you format your PR following our [bug template](https://github.com/projectmesa/mesa/blob/main/.github/PULL_REQUEST_TEMPLATE/bug.md?plain=1)? That makes it readable on its own, helps us review and serves as a  historical reference if we ever need to revisit it.
nissu99: @quaquel ,i have added the test case.
tpike3: @nissu99 Thanks for ding this and the fact you updated tests, migration guide, and tutorial is very cool!

I had one comment and will defer to @quaquel for final review. 
EwoutH: @tpike3 @quaquel what's the state of this PR, and what's steps are needed to get it merged?
quaquel: I think all that is left is the last point on the failing read the docs built. It should be an easy fix that I had hoped to do today (but life intervened) 
nissu99: @quaquel  @EwoutH  will make the required changes today .
nissu99: @tpike3  thanks for all the instructive comments ,i have made the changes.


Patch:
diff --git a/docs/getting_started.md b/docs/getting_started.md
index b928599b..83b83eae 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -180,6 +180,16 @@ Mesa now uses a new browser-based visualization system called SolaraViz. This al
 
 > **Note:** SolaraViz is experimental and still in active development for Mesa 3.0. While we attempt to minimize them, there might be API breaking changes between Mesa 3.0 and 3.1. There won't be breaking changes between Mesa 3.0.x patch releases.
 
+> **Note:** SolaraViz instantiates new models using `**model_parameters.value`, so all model inputs must be keyword arguments.
+
+Ensure your model's `__init__` method accepts keyword arguments matching the `model_params` keys.
+
+```python
+class MyModel(Model):
+    def __init__(self, n_agents=10, seed=None):
+        super().__init__(seed=seed)
+        # Initialize the model with N agents
+
 The core functionality for building your own visualizations resides in the [`mesa.visualization`](apis/visualization) namespace
 
 Here's a basic example of how to set up a visualization:
diff --git a/docs/migration_guide.md b/docs/migration_guide.md
index 1422d272..28bd90c4 100644
--- a/docs/migration_guide.md
+++ b/docs/migration_guide.md
@@ -259,6 +259,17 @@ the import from mesa.experimental. Otherwise here is a list of things you need t
 
 Previously SolaraViz was initialized by providing a `model_cls` and a `model_params`. This has changed to expect a model instance `model`. You can still provide (user-settable) `model_params`, but only if users should be able to change them. It is now also possible to pass in a "reactive model" by first calling `model = solara.reactive(model)`. This is useful for notebook environments. It allows you to pass the model to the SolaraViz Module, but continue to use the model. For example calling `model.value.step()` (notice the extra .value) will automatically update the plots. This currently only automatically works for the step method, you can force visualization updates by calling `model.value.force_update()`.
 
+### Model Initialization with Keyword Arguments
+
+With the introduction of SolaraViz in Mesa 3.0, models are now instantiated using `**model_parameters.value`. This means all inputs for initializing a new model must be keyword arguments. Ensure your model's `__init__` method accepts keyword arguments matching the keys in `model_params`.
+
+```python
+class MyModel(mesa.Model):
+    def __init__(self, n_agents=10, seed=None):
+        super().__init__(seed=seed)
+        # Initialize the model with N agents
+```
+
 #### Default space visualization
 
 Previously we included a default space drawer that you could configure with an `agent_portrayal` function. You now have to explicitly create a space drawer with the `agent_portrayal` function
diff --git a/docs/tutorials/visualization_tutorial.ipynb b/docs/tutorials/visualization_tutorial.ipynb
index f28ece08..48794f15 100644
--- a/docs/tutorials/visualization_tutorial.ipynb
+++ b/docs/tutorials/visualization_tutorial.ipynb
@@ -3,7 +3,9 @@
   {
    "cell_type": "markdown",
    "metadata": {},
-   "source": "# Visualization Tutorial"
+   "source": [
+    "# Visualization Tutorial"
+   ]
   },
   {
    "cell_type": "markdown",
@@ -51,10 +53,10 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "import mesa\n",
     "print(f\"Mesa version: {mesa.__version__}\")\n",
@@ -66,10 +68,10 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "def agent_portrayal(agent):\n",
     "    return {\n",
@@ -79,15 +81,17 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "markdown",
-   "source": "In addition to the portrayal method, we instantiate the model parameters, some of which are modifiable by user inputs. In this case, the number of agents, N, is specified as a slider of integers."
+   "metadata": {},
+   "source": [
+    "In addition to the portrayal method, we instantiate the model parameters, some of which are modifiable by user inputs. In this case, the number of agents, N, is specified as a slider of integers."
+   ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "model_params = {\n",
     "    \"n\": {\n",
@@ -104,8 +108,8 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "markdown",
+   "metadata": {},
    "source": [
     "Next, we instantiate the visualization object which (by default) displays the grid containing the agents, and timeseries of values computed by the model's data collector. In this example, we specify the Gini coefficient.\n",
     "\n",
@@ -118,13 +122,13 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "# Create initial model instance\n",
-    "model1 = MoneyModel(50, 10, 10)\n",
+    "model1 = MoneyModel(n=50, width=10, height=10) #keyword arguments\n",
     "\n",
     "SpaceGraph = make_space_component(agent_portrayal)\n",
     "GiniPlot = make_plot_component(\"Gini\")\n",
@@ -140,8 +144,8 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "markdown",
+   "metadata": {},
    "source": [
     "## Part 2 - Dynamic Agent Representation \n",
     "\n",
@@ -155,10 +159,10 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "import mesa\n",
     "print(f\"Mesa version: {mesa.__version__}\")\n",
@@ -169,10 +173,10 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "def agent_portrayal(agent):\n",
     "    size = 10\n",
@@ -197,13 +201,13 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "# Create initial model instance\n",
-    "model1 = MoneyModel(50, 10, 10)\n",
+    "model = MoneyModel(n=50, width=10, height=10)\n",
     "\n",
     "SpaceGraph = make_space_component(agent_portrayal)\n",
     "GiniPlot = make_plot_component(\"Gini\")\n",
@@ -219,8 +223,8 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "markdown",
+   "metadata": {},
    "source": [
     "## Part 3 - Custom Components \n",
     "\n",
@@ -236,10 +240,10 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "import mesa\n",
     "print(f\"Mesa version: {mesa.__version__}\")\n",
@@ -253,10 +257,10 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "def agent_portrayal(agent):\n",
     "    size = 10\n",
@@ -281,15 +285,17 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "markdown",
-   "source": "Next, we update our solara frontend to use this new component"
+   "metadata": {},
+   "source": [
+    "Next, we update our solara frontend to use this new component"
+   ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "@solara.component\n",
     "def Histogram(model):\n",
@@ -306,13 +312,13 @@
    ]
   },
   {
-   "metadata": {},
    "cell_type": "code",
-   "outputs": [],
    "execution_count": null,
+   "metadata": {},
+   "outputs": [],
    "source": [
     "# Create initial model instance\n",
-    "model1 = MoneyModel(50, 10, 10)\n",
+    "model = MoneyModel(n=50, width=10, height=10)\n",
     "\n",
     "SpaceGraph = make_space_component(agent_portrayal)\n",
     "GiniPlot = make_plot_component(\"Gini\")"
@@ -320,42 +326,42 @@
   },
   {
    "cell_type": "code",
+   "execution_count": 12,
    "metadata": {
     "ExecuteTime": {
      "end_time": "2024-10-29T19:38:49.471838Z",
      "start_time": "2024-10-29T19:38:47.897295Z"
     }
    },
-   "source": [
-    "page = SolaraViz(\n",
-    "    model1,\n",
-    "    components=[SpaceGraph, GiniPlot, Histogram],\n",
-    "    model_params=model_params,\n",
-    "    name=\"Boltzmann Wealth Model\",\n",
-    ")\n",
-    "# This is required to render the visualization in the Jupyter notebook\n",
-    "page"
-   ],
    "outputs": [
     {
      "data": {
-      "text/plain": [
-       "Cannot show ipywidgets in text"
-      ],
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "bc71b89ee5684038a194eee4c36f4a4c",
+       "version_major": 2,
+       "version_minor": 0
+      },
       "text/html": [
        "Cannot show widget. You probably want to rerun the code cell above (<i>Click in the code cell, and press Shift+Enter <kbd>⇧</kbd>+<kbd>↩</kbd></i>)."
       ],
-      "application/vnd.jupyter.widget-view+json": {
-       "version_major": 2,
-       "version_minor": 0,
-       "model_id": "bc71b89ee5684038a194eee4c36f4a4c"
-      }
+      "text/plain": [
+       "Cannot show ipywidgets in text"
+      ]
      },
      "metadata": {},
      "output_type": "display_data"
     }
    ],
-   "execution_count": 12
+   "source": [
+    "page = SolaraViz(\n",
+    "    model,\n",
+    "    components=[SpaceGraph, GiniPlot, Histogram],\n",
+    "    model_params=model_params,\n",
+    "    name=\"Boltzmann Wealth Model\",\n",
+    ")\n",
+    "# This is required to render the visualization in the Jupyter notebook\n",
+    "page"
+   ]
   },
   {
    "cell_type": "markdown",
@@ -366,35 +372,35 @@
   },
   {
    "cell_type": "code",
+   "execution_count": 13,
    "metadata": {
     "ExecuteTime": {
      "end_time": "2024-10-29T19:38:49.505725Z",
      "start_time": "2024-10-29T19:38:49.472599Z"
     }
    },
-   "source": [
-    "Histogram(model1)"
-   ],
    "outputs": [
     {
      "data": {
-      "text/plain": [
-       "Cannot show ipywidgets in text"
-      ],
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "0491f167a1434a92b78535078bd082a8",
+       "version_major": 2,
+       "version_minor": 0
+      },
       "text/html": [
        "Cannot show widget. You probably want to rerun the code cell above (<i>Click in the code cell, and press Shift+Enter <kbd>⇧</kbd>+<kbd>↩</kbd></i>)."
       ],
-      "application/vnd.jupyter.widget-view+json": {
-       "version_major": 2,
-       "version_minor": 0,
-       "model_id": "0491f167a1434a92b78535078bd082a8"
-      }
+      "text/plain": [
+       "Cannot show ipywidgets in text"
+      ]
      },
      "metadata": {},
      "output_type": "display_data"
     }
    ],
-   "execution_count": 13
+   "source": [
+    "Histogram(model)"
+   ]
   },
   {
    "cell_type": "markdown",
diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py
index 294dab34..ee62c8b0 100644
--- a/mesa/visualization/solara_viz.py
+++ b/mesa/visualization/solara_viz.py
@@ -423,6 +423,17 @@ def _check_model_params(init_func, model_params):
         ValueError: If a parameter is not valid for the model's initialization function
     """
     model_parameters = inspect.signature(init_func).parameters
+
+    has_var_positional = any(
+        param.kind == inspect.Parameter.VAR_POSITIONAL
+        for param in model_parameters.values()
+    )
+
+    if has_var_positional:
+        raise ValueError(
+            "Mesa's visualization requires the use of keyword arguments to ensure the parameters are passed to Solara correctly. Please ensure all model parameters are of form param=value"
+        )
+
     for name in model_parameters:
         if (
             model_parameters[name].default == inspect.Parameter.empty


Test patch:
diff --git a/tests/test_solara_viz.py b/tests/test_solara_viz.py
index 67a72dcb..3b8d82fb 100644
--- a/tests/test_solara_viz.py
+++ b/tests/test_solara_viz.py
@@ -213,3 +213,20 @@ def test_model_param_checks():  # noqa: D103
     # Test empty params dict raises ValueError if required params
     with pytest.raises(ValueError, match="Missing required model parameter"):
         _check_model_params(ModelWithOnlyRequired.__init__, {})
+
+
+# test that _check_model_params raises ValueError when *args are present
+def test_check_model_params_with_args_only():
+    """Test that _check_model_params raises ValueError when *args are present."""
+
+    class ModelWithArgsOnly:
+        def __init__(self, param1, *args):
+            pass
+
+    model_params = {"param1": 1}
+
+    with pytest.raises(
+        ValueError,
+        match="Mesa's visualization requires the use of keyword arguments to ensure the parameters are passed to Solara correctly. Please ensure all model parameters are of form param=value",
+    ):
+        _check_model_params(ModelWithArgsOnly.__init__, model_params)