Repository: projectmesa/mesa
Pull request #2505 (2024-11-13 20:00:12)

Only set model_parameters once
## Summary
While working on a seed widget I saw a major bug in solara viz. Model_parameters get reset on every model reset. This is pretty serious and I think we need another bug release soon. 

## Bug / Issue
**Describe the bug**
Hitting "reset" in the ui resets the model, but also resets the model_parameters to their initial value. 

**To Reproduce**
Run Schelling example. Reduce agent density. Hit reset. Works as expected. Hit reset again. Density is now restored. 


## Implementation
Use solaras `use_effect` to initialize model_parameters only once.


[screen-capture.webm](https://github.com/user-attachments/assets/2bb82a8e-5552-40ca-9840-e4580a2c48aa)


quaquel: That is a serious bug and something that had been nagging me but did not investigate yet. Great you spotted it and fixed it.

I will put some effort on the logging stuff because that would have helped in spotting this earlier.
github-actions[bot]: Performance benchmarks:

| Model           | Size   | Init time [95% CI]       | Run time [95% CI]       |
|-----------------|--------|--------------------------|-------------------------|
| BoltzmannWealth | small  | 🟢 -9.4% [-10.9%, -7.9%] | 🔵 -0.1% [-0.2%, +0.1%] |
| BoltzmannWealth | large  | 🔵 +1.1% [+0.8%, +1.4%]  | 🔵 +0.9% [+0.3%, +1.6%] |
| Schelling       | small  | 🔵 +0.6% [+0.1%, +1.0%]  | 🔵 -0.9% [-1.2%, -0.7%] |
| Schelling       | large  | 🔵 +1.1% [+0.6%, +1.5%]  | 🔵 -0.2% [-1.8%, +2.0%] |
| WolfSheep       | small  | 🔵 +2.5% [+2.1%, +2.9%]  | 🔵 +0.6% [+0.3%, +0.8%] |
| WolfSheep       | large  | 🔵 +1.7% [+0.6%, +3.1%]  | 🔵 +0.9% [-2.1%, +5.0%] |
| BoidFlockers    | small  | 🔵 +0.3% [-0.2%, +0.9%]  | 🔵 +0.3% [-0.4%, +1.0%] |
| BoidFlockers    | large  | 🔵 +0.4% [-0.2%, +1.1%]  | 🔵 -0.1% [-0.7%, +0.5%] |


Patch:
diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py
index ead67021..ca49a472 100644
--- a/mesa/experimental/cell_space/cell_collection.py
+++ b/mesa/experimental/cell_space/cell_collection.py
@@ -48,8 +48,10 @@ class CellCollection(Generic[T]):
         else:
             self._cells = {cell: cell.agents for cell in cells}
 
-        #
-        self._capacity: int = next(iter(self._cells.keys())).capacity
+        # Get capacity from first cell if collection is not empty
+        self._capacity: int | None = (
+            next(iter(self._cells.keys())).capacity if self._cells else None
+        )
 
         if random is None:
             warnings.warn(
diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py
index 10718e8a..1f3c8036 100644
--- a/mesa/visualization/solara_viz.py
+++ b/mesa/visualization/solara_viz.py
@@ -382,11 +382,17 @@ def ModelCreator(
     )
     user_params, fixed_params = split_model_params(user_params)
 
-    # set model_parameters to the default values for all parameters
-    model_parameters.value = {
-        **fixed_params,
-        **{k: v.get("value") for k, v in user_params.items()},
-    }
+    # Use solara.use_effect to run the initialization code only once
+    solara.use_effect(
+        # set model_parameters to the default values for all parameters
+        lambda: model_parameters.set(
+            {
+                **fixed_params,
+                **{k: v.get("value") for k, v in user_params.items()},
+            }
+        ),
+        [],
+    )
 
     def on_change(name, value):
         model_parameters.value = {**model_parameters.value, name: value}


Test patch:
diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py
index add5f6f8..b1170174 100644
--- a/tests/test_cell_space.py
+++ b/tests/test_cell_space.py
@@ -583,6 +583,40 @@ def test_cell_collection():
     assert len(cells) == len(collection)
 
 
+def test_empty_cell_collection():
+    """Test that CellCollection properly handles empty collections."""
+    rng = random.Random(42)
+
+    # Test initializing with empty collection
+    collection = CellCollection([], random=rng)
+    assert len(collection) == 0
+    assert collection._capacity is None
+    assert list(collection.cells) == []
+    assert list(collection.agents) == []
+
+    # Test selecting from empty collection
+    selected = collection.select(lambda cell: True)
+    assert len(selected) == 0
+    assert selected._capacity is None
+
+    # Test filtering to empty collection
+    n = 10
+    full_collection = CellCollection(
+        [Cell((i,), random=rng) for i in range(n)], random=rng
+    )
+    assert len(full_collection) == n
+
+    # Filter to empty collection
+    empty_result = full_collection.select(lambda cell: False)
+    assert len(empty_result) == 0
+    assert empty_result._capacity is None
+
+    # Test at_most with empty collection
+    at_most_result = full_collection.select(lambda cell: False, at_most=5)
+    assert len(at_most_result) == 0
+    assert at_most_result._capacity is None
+
+
 ### PropertyLayer tests
 def test_property_layer_integration():
     """Test integration of PropertyLayer with DiscrateSpace and Cell."""