From 47224470f87da046db3384ce2da4b1d655e3849d Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sun, 19 Jan 2025 21:46:57 +0530 Subject: [PATCH 01/22] Optimize UI responsiveness by Offloading model execution to separate thread --- mesa/visualization/solara_viz.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index f5fde84b1a3..737082a2da5 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -219,14 +219,23 @@ def ModelController( if model_parameters is None: model_parameters = {} model_parameters = solara.use_reactive(model_parameters) + pause_event = asyncio.Event() async def step(): - while playing.value and running.value: - await asyncio.sleep(play_interval.value / 1000) - do_step() - + try: + while running.value: + if not playing.value: + pause_event.clear() + await pause_event.wait() + await asyncio.sleep(play_interval.value / 1000) + do_step() + except asyncio.CancelledError: + # Handle the cancellation explicitly to avoid the exception from being unhandled + print("Step task was cancelled.") + return + solara.lab.use_task( - step, dependencies=[playing.value, running.value], prefer_threaded=False + step, dependencies=[playing.value, running.value], prefer_threaded=True ) @function_logger(__name__) @@ -234,7 +243,8 @@ def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" for _ in range(render_interval.value): model.value.step() - + if not playing.value: + break running.value = model.value.running force_update() @@ -305,7 +315,7 @@ async def step(): do_step() solara.lab.use_task( - step, dependencies=[playing.value, running.value], prefer_threaded=False + step, dependencies=[playing.value, running.value], prefer_threaded=True ) def do_step(): From ac55736389c92e6852df5902b8e2994e6440f5e8 Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Wed, 22 Jan 2025 22:44:12 +0530 Subject: [PATCH 02/22] adding thread without implementing render_interval --- mesa/visualization/solara_viz.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 737082a2da5..79aa82cab20 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -30,7 +30,7 @@ import reacton.core import solara - +import threading import mesa.visualization.components.altair_components as components_altair from mesa.experimental.devs.simulator import Simulator from mesa.mesa_logging import create_module_logger, function_logger @@ -219,21 +219,33 @@ def ModelController( if model_parameters is None: model_parameters = {} model_parameters = solara.use_reactive(model_parameters) - pause_event = asyncio.Event() + pause_event = asyncio.Event() async def step(): try: + current_thread = threading.Thread(target=vis, daemon=True) + current_thread.start() + print("thread started") while running.value: if not playing.value: pause_event.clear() await pause_event.wait() await asyncio.sleep(play_interval.value / 1000) do_step() + + current_thread.join() except asyncio.CancelledError: # Handle the cancellation explicitly to avoid the exception from being unhandled print("Step task was cancelled.") return - + + def vis(): + print("entered") + while playing.value: + print("Rendering") + force_update() + print("leaving") + solara.lab.use_task( step, dependencies=[playing.value, running.value], prefer_threaded=True ) @@ -241,13 +253,10 @@ async def step(): @function_logger(__name__) def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" - for _ in range(render_interval.value): - model.value.step() - if not playing.value: - break + model.value.step() running.value = model.value.running - force_update() + @function_logger(__name__) def do_reset(): From 4182285daa3abe76bcebad76d0b9cc3341e381c1 Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sat, 25 Jan 2025 03:45:08 +0530 Subject: [PATCH 03/22] visualisation thread --- mesa/visualization/solara_viz.py | 51 +++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 79aa82cab20..607214c84bb 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -25,12 +25,13 @@ import asyncio import inspect +import threading from collections.abc import Callable from typing import TYPE_CHECKING, Literal import reacton.core import solara -import threading + import mesa.visualization.components.altair_components as components_altair from mesa.experimental.devs.simulator import Simulator from mesa.mesa_logging import create_module_logger, function_logger @@ -223,28 +224,43 @@ def ModelController( async def step(): try: + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) current_thread = threading.Thread(target=vis, daemon=True) - current_thread.start() - print("thread started") - while running.value: - if not playing.value: - pause_event.clear() - await pause_event.wait() + if playing.value: + + current_thread.start() + print("thread started") + + while running.value and playing.value: + + await asyncio.sleep(play_interval.value / 1000) - do_step() - - current_thread.join() + do_step() + else: + if current_thread.is_alive(): + current_thread.join() + print("thread stopped") + pause_event.clear() + await pause_event.wait() except asyncio.CancelledError: - # Handle the cancellation explicitly to avoid the exception from being unhandled + print("Step task was cancelled.") return def vis(): - print("entered") - while playing.value: - print("Rendering") - force_update() - print("leaving") + try: + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + print("entered") + while playing.value: + print("Rendering") + force_update() + print("leaving") + except Exception as e: + print(f"Error in vis thread: {e}") solara.lab.use_task( step, dependencies=[playing.value, running.value], prefer_threaded=True @@ -253,11 +269,10 @@ def vis(): @function_logger(__name__) def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" + print("Called") model.value.step() running.value = model.value.running - - @function_logger(__name__) def do_reset(): """Reset the model to its initial state.""" From 2561df88f82ee16b5079dcb87cd53060a7604a2c Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sat, 25 Jan 2025 06:25:51 +0530 Subject: [PATCH 04/22] render _interval functioning execution is slower,thread not stopping gracefully --- mesa/visualization/solara_viz.py | 41 +++++++++++++++++--------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 607214c84bb..288e37a236a 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -224,40 +224,36 @@ def ModelController( async def step(): try: - loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) current_thread = threading.Thread(target=vis, daemon=True) - if playing.value: - + if playing.value: current_thread.start() print("thread started") - + while running.value and playing.value: - - await asyncio.sleep(play_interval.value / 1000) - do_step() - else: - if current_thread.is_alive(): - current_thread.join() - print("thread stopped") - pause_event.clear() - await pause_event.wait() + do_step() + if current_thread.is_alive(): + current_thread.join() + print("thread stopped") + pause_event.clear() + await pause_event.wait() except asyncio.CancelledError: - print("Step task was cancelled.") return def vis(): try: - loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) print("entered") while playing.value: print("Rendering") - force_update() + if model.value.steps % render_interval.value == 0: + print("Rendering") + force_update() + print("leaving") except Exception as e: print(f"Error in vis thread: {e}") @@ -269,9 +265,16 @@ def vis(): @function_logger(__name__) def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" - print("Called") - model.value.step() - running.value = model.value.running + if playing.value: + print("Called") + model.value.step() + running.value = model.value.running + else: + print("Called") + for _ in range(render_interval.value): + model.value.step() + running.value = model.value.running + force_update() @function_logger(__name__) def do_reset(): From 00c3c936bcb800e549001dfdccde7ca1da6d88e4 Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sat, 25 Jan 2025 10:48:15 +0530 Subject: [PATCH 05/22] Update solara_viz.py pending- eipstein not rendering prply(check other models) stopping thread(use_thread) graph inaccuracy --- mesa/visualization/solara_viz.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 288e37a236a..3aa5954b733 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -249,7 +249,7 @@ def vis(): asyncio.set_event_loop(loop) print("entered") while playing.value: - print("Rendering") + print("model.value.steps:",model.value.steps) if model.value.steps % render_interval.value == 0: print("Rendering") force_update() @@ -266,11 +266,11 @@ def vis(): def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" if playing.value: - print("Called") - model.value.step() - running.value = model.value.running + for _ in range(render_interval.value): + model.value.step() + running.value = model.value.running else: - print("Called") + print("Called2") for _ in range(render_interval.value): model.value.step() running.value = model.value.running From a68b4fdefc201cfe96053e82623765eb80407d9e Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sat, 25 Jan 2025 17:57:15 +0530 Subject: [PATCH 06/22] Update solara_viz.py --- mesa/visualization/solara_viz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 3aa5954b733..f7aeeb8964e 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -249,7 +249,7 @@ def vis(): asyncio.set_event_loop(loop) print("entered") while playing.value: - print("model.value.steps:",model.value.steps) + print("model.value.steps:", model.value.steps) if model.value.steps % render_interval.value == 0: print("Rendering") force_update() From 8e72a05f8cbc88ea3165ffb01c81e181b002a82c Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sat, 25 Jan 2025 20:15:05 +0530 Subject: [PATCH 07/22] terminating threads --- mesa/visualization/solara_viz.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index f7aeeb8964e..e90fe75a22c 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -220,25 +220,22 @@ def ModelController( if model_parameters is None: model_parameters = {} model_parameters = solara.use_reactive(model_parameters) - pause_event = asyncio.Event() async def step(): try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - current_thread = threading.Thread(target=vis, daemon=True) if playing.value: - current_thread.start() - print("thread started") - - while running.value and playing.value: - await asyncio.sleep(play_interval.value / 1000) - do_step() - if current_thread.is_alive(): - current_thread.join() - print("thread stopped") - pause_event.clear() - await pause_event.wait() + current_thread = threading.Thread(target=vis, daemon=True) + if playing.value: + current_thread.start() + print("thread started") + + while running.value and playing.value: + await asyncio.sleep(play_interval.value / 1000) + do_step() + if current_thread.is_alive(): + current_thread.join() + print("thread stopped") + except asyncio.CancelledError: print("Step task was cancelled.") return From a08bdcbd9ed3041e5c2330d67f745ed7534c56f9 Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sat, 25 Jan 2025 20:22:29 +0530 Subject: [PATCH 08/22] removing asynchornous task to avoid cancelledError exception --- mesa/visualization/solara_viz.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index e90fe75a22c..ecc04426799 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -37,7 +37,7 @@ from mesa.mesa_logging import create_module_logger, function_logger from mesa.visualization.user_param import Slider from mesa.visualization.utils import force_update, update_counter - +import time if TYPE_CHECKING: from mesa.model import Model @@ -221,7 +221,7 @@ def ModelController( model_parameters = {} model_parameters = solara.use_reactive(model_parameters) - async def step(): + def step(): try: if playing.value: current_thread = threading.Thread(target=vis, daemon=True) @@ -230,7 +230,7 @@ async def step(): print("thread started") while running.value and playing.value: - await asyncio.sleep(play_interval.value / 1000) + time.sleep(play_interval.value / 1000) do_step() if current_thread.is_alive(): current_thread.join() From fd1758488d61093882faf3713e3115000b9dcc4b Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sun, 26 Jan 2025 03:30:17 +0530 Subject: [PATCH 09/22] bidirectional synchronization implementing bi-directional threads is ensuring synchronization between model execution and visualization, though this is resulting in reduced performance. --- mesa/visualization/solara_viz.py | 75 ++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index ecc04426799..2dbc871642d 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -57,6 +57,7 @@ def SolaraViz( simulator: Simulator | None = None, model_params=None, name: str | None = None, + use_threads: bool = False, ): """Solara visualization component. @@ -76,6 +77,8 @@ def SolaraViz( This controls the speed of the model's automatic stepping. Defaults to 100 ms. render_interval (int, optional): Controls how often plots are updated during a simulation, allowing users to skip intermediate steps and update graphs less frequently. + use_threads: Flag for indicating whether to utilize multi-threading for model execution. + When checked, the model will utilize multiple threads,adjust based on system capabilities. simulator: A simulator that controls the model (optional) model_params (dict, optional): Parameters for (re-)instantiating a model. Can include user-adjustable parameters and fixed parameters. Defaults to None. @@ -110,6 +113,7 @@ def SolaraViz( reactive_model_parameters = solara.use_reactive({}) reactive_play_interval = solara.use_reactive(play_interval) reactive_render_interval = solara.use_reactive(render_interval) + reactive_use_threads = solara.use_reactive(use_threads) with solara.AppBar(): solara.AppBarTitle(name if name else model.value.__class__.__name__) @@ -131,12 +135,19 @@ def SolaraViz( max=100, step=2, ) + solara.Checkbox( + label="Use Threads", + value=reactive_use_threads, + on_value=lambda v: reactive_use_threads.set(v), + ) + if not isinstance(simulator, Simulator): ModelController( model, model_parameters=reactive_model_parameters, play_interval=reactive_play_interval, render_interval=reactive_render_interval, + use_threads=reactive_use_threads, ) else: SimulatorController( @@ -206,6 +217,7 @@ def ModelController( model_parameters: dict | solara.Reactive[dict] = None, play_interval: int | solara.Reactive[int] = 100, render_interval: int | solara.Reactive[int] = 1, + use_threads: bool | solara.Reactive[bool] = False, ): """Create controls for model execution (step, play, pause, reset). @@ -214,50 +226,52 @@ def ModelController( model_parameters: Reactive parameters for (re-)instantiating a model. play_interval: Interval for playing the model steps in milliseconds. render_interval: Controls how often the plots are updated during simulation steps.Higher value reduce update frequency. + use_threads: Flag for indicating whether to utilize multi-threading for model execution. """ playing = solara.use_reactive(False) running = solara.use_reactive(True) if model_parameters is None: model_parameters = {} model_parameters = solara.use_reactive(model_parameters) + pause_vis_event = threading.Event() + pause_step_event = threading.Event() def step(): try: - if playing.value: - current_thread = threading.Thread(target=vis, daemon=True) - if playing.value: - current_thread.start() - print("thread started") - - while running.value and playing.value: - time.sleep(play_interval.value / 1000) - do_step() - if current_thread.is_alive(): - current_thread.join() - print("thread stopped") - + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + while running.value and playing.value: + await asyncio.sleep(play_interval.value / 1000) + if use_threads.value: + pause_step_event.wait() + pause_step_event.clear() + do_step() + if use_threads.value: + pause_vis_event.set() except asyncio.CancelledError: print("Step task was cancelled.") return def vis(): - try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - print("entered") - while playing.value: - print("model.value.steps:", model.value.steps) - if model.value.steps % render_interval.value == 0: - print("Rendering") + if use_threads.value: + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + pause_step_event.set() + while playing.value and running.value: + pause_vis_event.wait() + pause_vis_event.clear() force_update() + pause_step_event.set() + except Exception as e: + print(f"Error in vis task: {e}") + finally: + loop.close() - print("leaving") - except Exception as e: - print(f"Error in vis thread: {e}") + # h216 result error? + solara.lab.use_task(step, dependencies=[playing.value, running.value]) - solara.lab.use_task( - step, dependencies=[playing.value, running.value], prefer_threaded=True - ) + solara.use_thread(vis, dependencies=[playing.value]) @function_logger(__name__) def do_step(): @@ -266,8 +280,13 @@ def do_step(): for _ in range(render_interval.value): model.value.step() running.value = model.value.running + if not playing.value: + break + + if not use_threads.value: + force_update() + else: - print("Called2") for _ in range(render_interval.value): model.value.step() running.value = model.value.running From d1c79655138ced1dda99fc75871990a5717f6eab Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sun, 26 Jan 2025 15:50:12 +0530 Subject: [PATCH 10/22] takes care of valueError and CancelledError takes care of valueError and CancelledError. pending changes:clean code --- mesa/visualization/solara_viz.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 2dbc871642d..c70130d9576 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -234,35 +234,43 @@ def ModelController( model_parameters = {} model_parameters = solara.use_reactive(model_parameters) pause_vis_event = threading.Event() - pause_step_event = threading.Event() + #pause_step_event = threading.Event() def step(): try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) while running.value and playing.value: - await asyncio.sleep(play_interval.value / 1000) - if use_threads.value: - pause_step_event.wait() - pause_step_event.clear() + time.sleep(play_interval.value / 1000) + #if use_threads.value: + #pause_step_event.wait() + #pause_step_event.clear() do_step() if use_threads.value: pause_vis_event.set() except asyncio.CancelledError: print("Step task was cancelled.") return + finally: + loop.close() def vis(): if use_threads.value: try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - pause_step_event.set() + #pause_step_event.set() + #render_step=-1 while playing.value and running.value: + #if render_step!=model.value.steps: + print("before waiting",model.value.steps) pause_vis_event.wait() pause_vis_event.clear() + print("after waiting",model.value.steps) force_update() - pause_step_event.set() + print("after rendering",model.value.steps) + # render_step=model.value.steps + #pause_step_event.set() except Exception as e: print(f"Error in vis task: {e}") finally: @@ -278,17 +286,21 @@ def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" if playing.value: for _ in range(render_interval.value): + print("model step:",model.value.steps) model.value.step() running.value = model.value.running if not playing.value: break if not use_threads.value: + print("rendering") force_update() + print("renderedd") else: for _ in range(render_interval.value): model.value.step() + print("model step:",model.value.steps) running.value = model.value.running force_update() From 8b8cd19409346d83dbcbfe3259a2175a6dc4ccfe Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sun, 26 Jan 2025 16:10:48 +0530 Subject: [PATCH 11/22] final changes optimal performance in browser and jupyter --- mesa/visualization/solara_viz.py | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index c70130d9576..b6b65b037ff 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -26,6 +26,7 @@ import asyncio import inspect import threading +import time from collections.abc import Callable from typing import TYPE_CHECKING, Literal @@ -37,7 +38,7 @@ from mesa.mesa_logging import create_module_logger, function_logger from mesa.visualization.user_param import Slider from mesa.visualization.utils import force_update, update_counter -import time + if TYPE_CHECKING: from mesa.model import Model @@ -234,17 +235,13 @@ def ModelController( model_parameters = {} model_parameters = solara.use_reactive(model_parameters) pause_vis_event = threading.Event() - #pause_step_event = threading.Event() - + def step(): try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) while running.value and playing.value: time.sleep(play_interval.value / 1000) - #if use_threads.value: - #pause_step_event.wait() - #pause_step_event.clear() do_step() if use_threads.value: pause_vis_event.set() @@ -259,24 +256,15 @@ def vis(): try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - #pause_step_event.set() - #render_step=-1 while playing.value and running.value: - #if render_step!=model.value.steps: - print("before waiting",model.value.steps) pause_vis_event.wait() pause_vis_event.clear() - print("after waiting",model.value.steps) force_update() - print("after rendering",model.value.steps) - # render_step=model.value.steps - #pause_step_event.set() except Exception as e: print(f"Error in vis task: {e}") finally: loop.close() - # h216 result error? solara.lab.use_task(step, dependencies=[playing.value, running.value]) solara.use_thread(vis, dependencies=[playing.value]) @@ -286,21 +274,16 @@ def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" if playing.value: for _ in range(render_interval.value): - print("model step:",model.value.steps) model.value.step() running.value = model.value.running if not playing.value: break - if not use_threads.value: - print("rendering") force_update() - print("renderedd") - + else: for _ in range(render_interval.value): model.value.step() - print("model step:",model.value.steps) running.value = model.value.running force_update() From 1c276aa5f745613608734644eb544f880e84b80a Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sun, 26 Jan 2025 16:43:22 +0530 Subject: [PATCH 12/22] Display message for adjusting play interval when using threads --- mesa/visualization/solara_viz.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index b6b65b037ff..fd9c3196c67 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -136,12 +136,14 @@ def SolaraViz( max=100, step=2, ) + if reactive_use_threads.value: + solara.Text("Adjust play interval to avoid skipping plots") + solara.Checkbox( label="Use Threads", value=reactive_use_threads, on_value=lambda v: reactive_use_threads.set(v), ) - if not isinstance(simulator, Simulator): ModelController( model, @@ -235,7 +237,7 @@ def ModelController( model_parameters = {} model_parameters = solara.use_reactive(model_parameters) pause_vis_event = threading.Event() - + def step(): try: loop = asyncio.new_event_loop() @@ -280,7 +282,7 @@ def do_step(): break if not use_threads.value: force_update() - + else: for _ in range(render_interval.value): model.value.step() From fec0861a308bbad03b8e4b447401f5796591e910 Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Mon, 27 Jan 2025 12:12:23 +0530 Subject: [PATCH 13/22] updating SimulatorConrtoller --- mesa/visualization/solara_viz.py | 45 ++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index fd9c3196c67..961ecd79b81 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -137,7 +137,7 @@ def SolaraViz( step=2, ) if reactive_use_threads.value: - solara.Text("Adjust play interval to avoid skipping plots") + solara.Text("Increase play interval to avoid skipping plots") solara.Checkbox( label="Use Threads", @@ -159,6 +159,7 @@ def SolaraViz( model_parameters=reactive_model_parameters, play_interval=reactive_play_interval, render_interval=reactive_render_interval, + use_threads=reactive_use_threads, ) with solara.Card("Model Parameters"): ModelCreator( @@ -269,7 +270,7 @@ def vis(): solara.lab.use_task(step, dependencies=[playing.value, running.value]) - solara.use_thread(vis, dependencies=[playing.value]) + solara.use_thread(vis, dependencies=[playing.value,running.value]) @function_logger(__name__) def do_step(): @@ -280,14 +281,12 @@ def do_step(): running.value = model.value.running if not playing.value: break - if not use_threads.value: - force_update() - else: for _ in range(render_interval.value): model.value.step() running.value = model.value.running - force_update() + + force_update() @function_logger(__name__) def do_reset(): @@ -329,6 +328,7 @@ def SimulatorController( model_parameters: dict | solara.Reactive[dict] = None, play_interval: int | solara.Reactive[int] = 100, render_interval: int | solara.Reactive[int] = 1, + use_threads: bool | solara.Reactive[bool] = False, ): """Create controls for model execution (step, play, pause, reset). @@ -338,7 +338,7 @@ def SimulatorController( model_parameters: Reactive parameters for (re-)instantiating a model. play_interval: Interval for playing the model steps in milliseconds. render_interval: Controls how often the plots are updated during simulation steps.Higher values reduce update frequency. - + use_threads: Flag for indicating whether to utilize multi-threading for model execution. Notes: The `step button` increments the step by the value specified in the `render_interval` slider. This behavior ensures synchronization between simulation steps and plot updates. @@ -348,16 +348,33 @@ def SimulatorController( if model_parameters is None: model_parameters = {} model_parameters = solara.use_reactive(model_parameters) + pause_vis_event = threading.Event() + + def step(): + try: + while running.value and playing.value: + time.sleep(play_interval.value / 1000) + do_step() + if use_threads.value: + pause_vis_event.set() + except asyncio.CancelledError: + print("Step task was cancelled.") + return - async def step(): - while playing.value and running.value: - await asyncio.sleep(play_interval.value / 1000) - do_step() + def vis(): + if use_threads.value: + try: + while playing.value and running.value: + pause_vis_event.wait() + pause_vis_event.clear() + force_update() + except Exception as e: + print(f"Error in vis task: {e}") - solara.lab.use_task( - step, dependencies=[playing.value, running.value], prefer_threaded=True - ) + solara.lab.use_task(step, dependencies=[playing.value, running.value]) + solara.use_thread(vis, dependencies=[playing.value,running.value]) + def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" simulator.run_for(render_interval.value) From 775eaa9e28844054d77938d1906d52a9bbdc9221 Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Mon, 27 Jan 2025 12:12:36 +0530 Subject: [PATCH 14/22] Update solara_viz.py --- mesa/visualization/solara_viz.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 961ecd79b81..0e83ada09d6 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -270,7 +270,7 @@ def vis(): solara.lab.use_task(step, dependencies=[playing.value, running.value]) - solara.use_thread(vis, dependencies=[playing.value,running.value]) + solara.use_thread(vis, dependencies=[playing.value, running.value]) @function_logger(__name__) def do_step(): @@ -339,6 +339,7 @@ def SimulatorController( play_interval: Interval for playing the model steps in milliseconds. render_interval: Controls how often the plots are updated during simulation steps.Higher values reduce update frequency. use_threads: Flag for indicating whether to utilize multi-threading for model execution. + Notes: The `step button` increments the step by the value specified in the `render_interval` slider. This behavior ensures synchronization between simulation steps and plot updates. @@ -373,8 +374,8 @@ def vis(): solara.lab.use_task(step, dependencies=[playing.value, running.value]) - solara.use_thread(vis, dependencies=[playing.value,running.value]) - + solara.use_thread(vis, dependencies=[playing.value, running.value]) + def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" simulator.run_for(render_interval.value) From 5e5b2cf67d361c9a1cad78d00803c1ce3660eb3e Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Wed, 29 Jan 2025 03:52:26 +0530 Subject: [PATCH 15/22] solving the 'non rendering of plots while using threads' bug In use_threads, the threads for step and vis may compete for GI leading to slower execution, especially when sharing resources. In contrast, use_task, which is asyncio-based, avoids GIL contention. --- mesa/visualization/solara_viz.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 0e83ada09d6..d519c7fb553 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -268,9 +268,9 @@ def vis(): finally: loop.close() - solara.lab.use_task(step, dependencies=[playing.value, running.value]) + solara.lab.use_task(step, dependencies=[playing.value, running.value],prefer_threaded=True) - solara.use_thread(vis, dependencies=[playing.value, running.value]) + solara.lab.use_task(vis, dependencies=[playing.value,running.value],prefer_threaded=True) @function_logger(__name__) def do_step(): @@ -281,12 +281,14 @@ def do_step(): running.value = model.value.running if not playing.value: break + if not use_threads.value: + force_update() + else: for _ in range(render_interval.value): model.value.step() running.value = model.value.running - - force_update() + force_update() @function_logger(__name__) def do_reset(): @@ -339,7 +341,6 @@ def SimulatorController( play_interval: Interval for playing the model steps in milliseconds. render_interval: Controls how often the plots are updated during simulation steps.Higher values reduce update frequency. use_threads: Flag for indicating whether to utilize multi-threading for model execution. - Notes: The `step button` increments the step by the value specified in the `render_interval` slider. This behavior ensures synchronization between simulation steps and plot updates. @@ -374,8 +375,8 @@ def vis(): solara.lab.use_task(step, dependencies=[playing.value, running.value]) - solara.use_thread(vis, dependencies=[playing.value, running.value]) - + solara.use_thread(vis, dependencies=[playing.value,running.value]) + def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" simulator.run_for(render_interval.value) From b1ffeff0924a2d5d089c336a9f7443b891bf0d37 Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Wed, 29 Jan 2025 04:39:50 +0530 Subject: [PATCH 16/22] Fix code indentation --- mesa/visualization/solara_viz.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index d519c7fb553..8827b302591 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -268,9 +268,13 @@ def vis(): finally: loop.close() - solara.lab.use_task(step, dependencies=[playing.value, running.value],prefer_threaded=True) + solara.lab.use_task( + step, dependencies=[playing.value, running.value], prefer_threaded=True + ) - solara.lab.use_task(vis, dependencies=[playing.value,running.value],prefer_threaded=True) + solara.use_thread( + vis, dependencies=[playing.value, running.value], prefer_threaded=True + ) @function_logger(__name__) def do_step(): @@ -341,6 +345,7 @@ def SimulatorController( play_interval: Interval for playing the model steps in milliseconds. render_interval: Controls how often the plots are updated during simulation steps.Higher values reduce update frequency. use_threads: Flag for indicating whether to utilize multi-threading for model execution. + Notes: The `step button` increments the step by the value specified in the `render_interval` slider. This behavior ensures synchronization between simulation steps and plot updates. @@ -375,8 +380,8 @@ def vis(): solara.lab.use_task(step, dependencies=[playing.value, running.value]) - solara.use_thread(vis, dependencies=[playing.value,running.value]) - + solara.use_thread(vis, dependencies=[playing.value, running.value]) + def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" simulator.run_for(render_interval.value) From 0fcf2b2cdd53a40c66de1c3b5035adfeec4cff29 Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Wed, 29 Jan 2025 04:53:07 +0530 Subject: [PATCH 17/22] Fix code indentation --- mesa/visualization/solara_viz.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 8827b302591..5d1b4de07a3 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -237,7 +237,7 @@ def ModelController( if model_parameters is None: model_parameters = {} model_parameters = solara.use_reactive(model_parameters) - pause_vis_event = threading.Event() + visualization_pause_event = threading.Event() def step(): try: @@ -247,24 +247,24 @@ def step(): time.sleep(play_interval.value / 1000) do_step() if use_threads.value: - pause_vis_event.set() + visualization_pause_event.set() except asyncio.CancelledError: print("Step task was cancelled.") return finally: loop.close() - def vis(): + def visualization_task(): if use_threads.value: try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) while playing.value and running.value: - pause_vis_event.wait() - pause_vis_event.clear() + visualization_pause_event.wait() + visualization_pause_event.clear() force_update() except Exception as e: - print(f"Error in vis task: {e}") + print(f"Error in visualization_task task: {e}") finally: loop.close() @@ -273,7 +273,9 @@ def vis(): ) solara.use_thread( - vis, dependencies=[playing.value, running.value], prefer_threaded=True + visualization_task, + dependencies=[playing.value, running.value], + prefer_threaded=True, ) @function_logger(__name__) @@ -355,7 +357,7 @@ def SimulatorController( if model_parameters is None: model_parameters = {} model_parameters = solara.use_reactive(model_parameters) - pause_vis_event = threading.Event() + visualization_pause_event = threading.Event() def step(): try: @@ -363,24 +365,24 @@ def step(): time.sleep(play_interval.value / 1000) do_step() if use_threads.value: - pause_vis_event.set() + visualization_pause_event.set() except asyncio.CancelledError: print("Step task was cancelled.") return - def vis(): + def visualization_task(): if use_threads.value: try: while playing.value and running.value: - pause_vis_event.wait() - pause_vis_event.clear() + visualization_pause_event.wait() + visualization_pause_event.clear() force_update() except Exception as e: - print(f"Error in vis task: {e}") + print(f"Error in visualization_task: {e}") solara.lab.use_task(step, dependencies=[playing.value, running.value]) - solara.use_thread(vis, dependencies=[playing.value, running.value]) + solara.use_thread(visualization_task, dependencies=[playing.value, running.value]) def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" From 791d70b034fe2da2ef6b4b7a56a51bdca380be89 Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Fri, 31 Jan 2025 12:16:57 +0530 Subject: [PATCH 18/22] threading for simulator --- mesa/visualization/solara_viz.py | 43 +++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 5d1b4de07a3..9c71b4c3671 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -264,7 +264,7 @@ def visualization_task(): visualization_pause_event.clear() force_update() except Exception as e: - print(f"Error in visualization_task task: {e}") + print(f"Error in visualization_task: {e}") finally: loop.close() @@ -275,7 +275,6 @@ def visualization_task(): solara.use_thread( visualization_task, dependencies=[playing.value, running.value], - prefer_threaded=True, ) @function_logger(__name__) @@ -301,6 +300,7 @@ def do_reset(): """Reset the model to its initial state.""" playing.value = False running.value = True + visualization_pause_event.clear() _mesa_logger.log( 10, f"creating new {model.value.__class__} instance with {model_parameters.value}", @@ -358,14 +358,23 @@ def SimulatorController( model_parameters = {} model_parameters = solara.use_reactive(model_parameters) visualization_pause_event = threading.Event() + pause_step_event = threading.Event() def step(): try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) while running.value and playing.value: time.sleep(play_interval.value / 1000) + if use_threads.value: + pause_step_event.wait() + pause_step_event.clear() do_step() if use_threads.value: visualization_pause_event.set() + + + except asyncio.CancelledError: print("Step task was cancelled.") return @@ -373,22 +382,44 @@ def step(): def visualization_task(): if use_threads.value: try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + pause_step_event.set() while playing.value and running.value: visualization_pause_event.wait() visualization_pause_event.clear() force_update() + pause_step_event.set() except Exception as e: print(f"Error in visualization_task: {e}") + finally: + loop.close() + # h216 result error? solara.lab.use_task(step, dependencies=[playing.value, running.value]) - solara.use_thread(visualization_task, dependencies=[playing.value, running.value]) + + solara.use_thread(visualization_task, dependencies=[playing.value]) + + + def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" - simulator.run_for(render_interval.value) - running.value = model.value.running - force_update() + if playing.value: + for _ in range(render_interval.value): + simulator.run_for(1) + running.value = model.value.running + if not playing.value: + break + if not use_threads.value: + force_update() + + else: + for _ in range(render_interval.value): + simulator.run_for(1) + running.value = model.value.running + force_update() def do_reset(): """Reset the model to its initial state.""" From 6cd0f22e2d895e4218da1f1b24a4a7626002d0c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 06:51:24 +0000 Subject: [PATCH 19/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/visualization/solara_viz.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 9c71b4c3671..1d86405acfd 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -373,8 +373,6 @@ def step(): if use_threads.value: visualization_pause_event.set() - - except asyncio.CancelledError: print("Step task was cancelled.") return @@ -398,12 +396,8 @@ def visualization_task(): # h216 result error? solara.lab.use_task(step, dependencies=[playing.value, running.value]) - solara.use_thread(visualization_task, dependencies=[playing.value]) - - - def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" if playing.value: From 3ba35939eebb6630ce4113fa69abff5672a71c4f Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sat, 1 Feb 2025 18:07:34 +0530 Subject: [PATCH 20/22] Change from use_thread to use_task in SimulatorController for better control This should improve the overall functionality and reliability of the SimulatorController, particularly in scenarios where stopping the execution is necessary. --- mesa/visualization/solara_viz.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 1d86405acfd..d3c9c58844f 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -248,8 +248,8 @@ def step(): do_step() if use_threads.value: visualization_pause_event.set() - except asyncio.CancelledError: - print("Step task was cancelled.") + except Exception as e: + print(f"Error in step: {e}") return finally: loop.close() @@ -372,10 +372,10 @@ def step(): do_step() if use_threads.value: visualization_pause_event.set() - - except asyncio.CancelledError: - print("Step task was cancelled.") - return + except Exception as e: + print(f"Error in step: {e}") + finally: + loop.close() def visualization_task(): if use_threads.value: @@ -390,13 +390,14 @@ def visualization_task(): pause_step_event.set() except Exception as e: print(f"Error in visualization_task: {e}") + return finally: loop.close() - # h216 result error? - solara.lab.use_task(step, dependencies=[playing.value, running.value]) - - solara.use_thread(visualization_task, dependencies=[playing.value]) + solara.lab.use_task( + step, dependencies=[playing.value, running.value], prefer_threaded=False + ) + solara.lab.use_task(visualization_task, dependencies=[playing.value]) def do_step(): """Advance the model by the number of steps specified by the render_interval slider.""" @@ -420,6 +421,8 @@ def do_reset(): playing.value = False running.value = True simulator.reset() + visualization_pause_event.clear() + pause_step_event.clear() model.value = model.value = model.value.__class__( simulator=simulator, **model_parameters.value ) From e5579ad949d9e7f22cff7c1a3422b715a7b100de Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Sun, 16 Feb 2025 03:34:33 +0530 Subject: [PATCH 21/22] ensuring event objects dont reset during re-renders and removing async tasks --- mesa/visualization/solara_viz.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index fe4d79f00f8..a9d8e2872c7 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -238,15 +238,14 @@ def ModelController( """ playing = solara.use_reactive(False) running = solara.use_reactive(True) + if model_parameters is None: model_parameters = {} model_parameters = solara.use_reactive(model_parameters) - visualization_pause_event = threading.Event() + visualization_pause_event = solara.use_memo(lambda: threading.Event(), []) def step(): try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) while running.value and playing.value: time.sleep(play_interval.value / 1000) do_step() @@ -255,22 +254,16 @@ def step(): except Exception as e: print(f"Error in step: {e}") return - finally: - loop.close() def visualization_task(): if use_threads.value: try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) while playing.value and running.value: visualization_pause_event.wait() visualization_pause_event.clear() force_update() except Exception as e: print(f"Error in visualization_task: {e}") - finally: - loop.close() solara.lab.use_task( step, dependencies=[playing.value, running.value], prefer_threaded=True @@ -361,13 +354,11 @@ def SimulatorController( if model_parameters is None: model_parameters = {} model_parameters = solara.use_reactive(model_parameters) - visualization_pause_event = threading.Event() - pause_step_event = threading.Event() + visualization_pause_event = solara.use_memo(lambda: threading.Event(), []) + pause_step_event = solara.use_memo(lambda: threading.Event(), []) def step(): try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) while running.value and playing.value: time.sleep(play_interval.value / 1000) if use_threads.value: @@ -378,8 +369,6 @@ def step(): visualization_pause_event.set() except Exception as e: print(f"Error in step: {e}") - finally: - loop.close() def visualization_task(): if use_threads.value: @@ -395,8 +384,6 @@ def visualization_task(): except Exception as e: print(f"Error in visualization_task: {e}") return - finally: - loop.close() solara.lab.use_task( step, dependencies=[playing.value, running.value], prefer_threaded=False From d771f7f4785263e0fd0ee8c01da92e7c1d4636a6 Mon Sep 17 00:00:00 2001 From: HMNS19 Date: Wed, 19 Feb 2025 01:38:30 +0530 Subject: [PATCH 22/22] removing lambda --- mesa/visualization/solara_viz.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 07b3abe190e..6209f558f9e 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -145,11 +145,15 @@ def SolaraViz( if reactive_use_threads.value: solara.Text("Increase play interval to avoid skipping plots") + def set_reactive_use_threads(value): + reactive_use_threads.set(value) + solara.Checkbox( label="Use Threads", value=reactive_use_threads, - on_value=lambda v: reactive_use_threads.set(v), + on_value=set_reactive_use_threads, ) + if not isinstance(simulator, Simulator): ModelController( model,