Repository: projectmesa/mesa

Issue:
Bugs in SolaraViz?
I started working on #2427 and have run into three issues:

1. If the model takes one or more arguments and those arguments are not part of the user-controllable parameters that are set from the UI, stuff will crash immediately 
2. Strangely, it crashes immediately even though we started SolaraViz with a valid model instance, so this points to a second bug: at the moment, the model shown after starting the UI is not the same as the original model with which the UI was initialized. Again, this is easy to reproduce. In the boltzman wealth model, in app.py, start the model with say a width and height of 20. Next, start Solara, and in the GUI, you'll see that the actual model has reverted back to 10 by 10 (as specified in `model_params`). 
3. Seed is currently handled differently from other parameters. With #1940 merged, this is no longer needed, and seed can just be integrated into the user-specifiable parameters.

Hints from issue comments:
@Corvince: Yes from looking at the code I can confirm these bugs. If user-setable parameters are provided the ModelCreator is set up and initializes a new model. This should not happen initially. But this raises the question how to work with models that need parameters, but those are not provided. Should reset just be disabled or should we create a helpful error message?@quaquel: 1. We should add to the docs that it is recommended to only use keyword argument in `Model.__ini__`
2. We should have an informative error. It might even be possible to use reflection to check the signature of the `__init__` and raise the exception immediately. I would advocate against aligning inputs from the UI with the order of arguments in the `__init__`: when in doubt refuse the temptation to guess.

Solution patch:
diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py
index d5d83f437ea..fefbfb32a04 100644
--- a/mesa/visualization/solara_viz.py
+++ b/mesa/visualization/solara_viz.py
@@ -25,6 +25,7 @@
 
 import asyncio
 import copy
+import inspect
 from collections.abc import Callable
 from typing import TYPE_CHECKING, Literal
 
@@ -299,9 +300,12 @@ def ModelCreator(model, model_params, seed=1):
         - The component provides an interface for adjusting user-defined parameters and reseeding the model.
 
     """
-    user_params, fixed_params = split_model_params(model_params)
+    solara.use_effect(
+        lambda: _check_model_params(model.value.__class__.__init__, fixed_params),
+        [model.value],
+    )
 
-    reactive_seed = solara.use_reactive(seed)
+    user_params, fixed_params = split_model_params(model_params)
 
     model_parameters, set_model_parameters = solara.use_state(
         {
@@ -310,29 +314,35 @@ def ModelCreator(model, model_params, seed=1):
         }
     )
 
-    def do_reseed():
-        """Update the random seed for the model."""
-        reactive_seed.value = model.value.random.random()
-
     def on_change(name, value):
-        set_model_parameters({**model_parameters, name: value})
+        new_model_parameters = {**model_parameters, name: value}
+        model.value = model.value.__class__(**new_model_parameters)
+        set_model_parameters(new_model_parameters)
 
-    def create_model():
-        model.value = model.value.__class__(**model_parameters)
-        model.value._seed = reactive_seed.value
+    UserInputs(user_params, on_change=on_change)
 
-    solara.use_effect(create_model, [model_parameters, reactive_seed.value])
 
-    with solara.Row(justify="space-between"):
-        solara.InputText(
-            label="Seed",
-            value=reactive_seed,
-            continuous_update=True,
-        )
+def _check_model_params(init_func, model_params):
+    """Check if model parameters are valid for the model's initialization function.
 
-        solara.Button(label="Reseed", color="primary", on_click=do_reseed)
+    Args:
+        init_func: Model initialization function
+        model_params: Dictionary of model parameters
 
-    UserInputs(user_params, on_change=on_change)
+    Raises:
+        ValueError: If a parameter is not valid for the model's initialization function
+    """
+    model_parameters = inspect.signature(init_func).parameters
+    for name in model_parameters:
+        if (
+            model_parameters[name].default == inspect.Parameter.empty
+            and name not in model_params
+            and name != "self"
+        ):
+            raise ValueError(f"Missing required model parameter: {name}")
+    for name in model_params:
+        if name not in model_parameters:
+            raise ValueError(f"Invalid model parameter: {name}")
 
 
 @solara.component


Test patch:
diff --git a/tests/test_solara_viz.py b/tests/test_solara_viz.py
index 9276696676f..7dfeae722e0 100644
--- a/tests/test_solara_viz.py
+++ b/tests/test_solara_viz.py
@@ -3,13 +3,19 @@
 import unittest
 
 import ipyvuetify as vw
+import pytest
 import solara
 
 import mesa
 import mesa.visualization.components.altair_components
 import mesa.visualization.components.matplotlib_components
 from mesa.visualization.components.matplotlib_components import make_mpl_space_component
-from mesa.visualization.solara_viz import Slider, SolaraViz, UserInputs
+from mesa.visualization.solara_viz import (
+    Slider,
+    SolaraViz,
+    UserInputs,
+    _check_model_params,
+)
 
 
 class TestMakeUserInput(unittest.TestCase):  # noqa: D101
@@ -152,3 +158,41 @@ def test_slider():  # noqa: D103
     assert not slider_int.is_float_slider
     slider_dtype_float = Slider("Homophily", 3, 0, 8, 1, dtype=float)
     assert slider_dtype_float.is_float_slider
+
+
+def test_model_param_checks():  # noqa: D103
+    class ModelWithOptionalParams:
+        def __init__(self, required_param, optional_param=10):
+            pass
+
+    class ModelWithOnlyRequired:
+        def __init__(self, param1, param2):
+            pass
+
+    # Test that optional params can be omitted
+    _check_model_params(ModelWithOptionalParams.__init__, {"required_param": 1})
+
+    # Test that optional params can be provided
+    _check_model_params(
+        ModelWithOptionalParams.__init__, {"required_param": 1, "optional_param": 5}
+    )
+
+    # Test invalid parameter name raises ValueError
+    with pytest.raises(ValueError, match="Invalid model parameter: invalid_param"):
+        _check_model_params(
+            ModelWithOptionalParams.__init__, {"required_param": 1, "invalid_param": 2}
+        )
+
+    # Test missing required parameter raises ValueError
+    with pytest.raises(ValueError, match="Missing required model parameter: param2"):
+        _check_model_params(ModelWithOnlyRequired.__init__, {"param1": 1})
+
+    # Test passing extra parameters raises ValueError
+    with pytest.raises(ValueError, match="Invalid model parameter: extra"):
+        _check_model_params(
+            ModelWithOnlyRequired.__init__, {"param1": 1, "param2": 2, "extra": 3}
+        )
+
+    # Test empty params dict raises ValueError if required params
+    with pytest.raises(ValueError, match="Missing required model parameter"):
+        _check_model_params(ModelWithOnlyRequired.__init__, {})