projectmesa / mesa

Mesa is an open-source Python library for agent-based modeling, ideal for simulating complex systems and exploring emergent behaviors.
https://mesa.readthedocs.io
Apache License 2.0
2.45k stars 874 forks source link

unable to start solara app when model cannot be pickled #2427

Closed wang-boyu closed 2 days ago

wang-boyu commented 1 week ago

Describe the bug

When a model or agents have attributes that cannot be pickled, Solara app won't start due to

TypeError: cannot pickle '_thread.lock' object

Expected behavior

Should be able to run model with Solara regardless it can be deep copied or not

To Reproduce

Take the Schelling model as an example, add the following into the __init__() method in model.py:

import threading
self.t = threading.Thread(target=print, args=("Hello"))
self.t.start()

Then run solara run app.py in the command line.

Additional context

It appears to be related to https://github.com/projectmesa/mesa/blob/1d985608db1abe346a6e817bf5bb12c18cad556d/mesa/visualization/solara_viz.py#L188 and https://github.com/projectmesa/mesa/blob/1d985608db1abe346a6e817bf5bb12c18cad556d/mesa/visualization/solara_viz.py#L212

where we try to create deep copies of the model object.

If we remove all usages related to deepcopy:

diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py
index 4bca98d..c2d4960 100644
--- a/mesa/visualization/solara_viz.py
+++ b/mesa/visualization/solara_viz.py
@@ -183,14 +183,6 @@ def ModelController(model: solara.Reactive[Model], play_interval=100):
     running = solara.use_reactive(True)
     original_model = solara.use_reactive(None)

-    def save_initial_model():
-        """Save the initial model for comparison."""
-        original_model.set(copy.deepcopy(model.value))
-        playing.value = False
-        force_update()
-
-    solara.use_effect(save_initial_model, [model.value])
-
     async def step():
         while playing.value and running.value:
             await asyncio.sleep(play_interval / 1000)
@@ -205,18 +197,11 @@ def ModelController(model: solara.Reactive[Model], play_interval=100):
         model.value.step()
         running.value = model.value.running

-    def do_reset():
-        """Reset the model to its initial state."""
-        playing.value = False
-        running.value = True
-        model.value = copy.deepcopy(original_model.value)
-
     def do_play_pause():
         """Toggle play/pause."""
         playing.value = not playing.value

     with solara.Row(justify="space-between"):
-        solara.Button(label="Reset", color="primary", on_click=do_reset)
         solara.Button(
             label="▶" if not playing.value else "❚❚",
             color="primary",

Then the Solara app runs but without the Reset button.

Probably need a better way to create or save the initial model object?

EwoutH commented 1 week ago

@quaquel you looked into deepcopy stuff recently, is this related?

quaquel commented 1 week ago

The only relation is that it's triggered by deepcopy. This one is not fixable within mesa itself, unless there is a way of getting rid of deepcopy in solara_viz and no further deepcopy operations inside solara. @Corvince, might you be able to explain why deepcopy is used in solara_viz?

Corvince commented 1 week ago

Previously we called SolaraViz with the model class and init parameters. Now we pass in a model instance. So I thought the easiest way to reset a model was to restore the initial state through deepcopy.

But this also causes a problem if you call SolaraViz with a model step where already some steps have passed (say 5). Because now you can only reset to step number 5. So we should really reconsider. I think we can derive the model class through from the instance, but we don't necessarily have access to all init parameters, which makes this a bit challenging

quaquel commented 1 week ago

Thanks for the clarification. I'll try to take a closer look over the weekend and see what is possible.

EwoutH commented 1 week ago

Now that we have tried both, it might be useful to clearly list and weigh the benefits and disadvantages of calling with a model class and calling with an model instance.

quaquel commented 1 week ago

I am quite sure that the current code is not even working as intended. While cleaning up the examples, I instantiated wolf-sheep with grass=True, but solara still errored because no grass agents existed in the model.

The cleanest long-term solution would be to abstract all this away in a dedicated run_control functionality and have a dedicated Experiment class. This, however, is not something that can be implemented overnight and requires a longer discussion at some later point.

So, my current idea is to just make it work with both object and class. If instantiated with a class, the cleanest solution would be to take values from the specified input controls.