Repository: projectmesa/mesa (python)

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. 

Pull request:
Fix #2452 - handle solara viz model params better
## Summary
Bug description is in #2452, here is a copy
> 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 [model: Move random seed and random to __init__ #1940](https://github.com/projectmesa/mesa/pull/1940) merged, this is no longer needed, and seed can just be integrated into the user-specifiable parameters.

## Implementation
1. On startup solara viz will check the user_params and compare to `model.__class__.__init__`. If there are are parameters that don't have a default value but are not in user_params solara viz will raise a ValueError. Also raises ValueError if user provided model_params include parameters not used by the model.
2. Currently the ModelCreator was creating a new instance whenever model parameters changed. However, the initial call to ModelCreator "changed" the parameters, triggering an update and overwriting the provided model. With this PR, only actual parameter changes trigger a model re-creation
3. "Reactive" Seed handling was removed completely. No its treated just like any other model parameter. 


Patch:
diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py
index d5d83f43..fefbfb32 100644
--- a/mesa/visualization/solara_viz.py
+++ b/mesa/visualization/solara_viz.py
@@ -25,6 +25,7 @@ from __future__ import annotations
 
 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 92766966..7dfeae72 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__, {})