Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify ModelController #2282

Merged
merged 1 commit into from
Sep 6, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 22 additions & 81 deletions mesa/visualization/solara_viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@
"""

import copy
import threading
import time
from typing import TYPE_CHECKING, Literal

import reacton.ipywidgets as widgets
import solara
from solara.alias import rv

Expand Down Expand Up @@ -95,7 +94,7 @@ def SolaraViz(
model: "Model" | solara.Reactive["Model"],
components: list[solara.component] | Literal["default"] = "default",
*args,
play_interval=150,
play_interval=100,
model_params=None,
seed=0,
name: str | None = None,
Expand Down Expand Up @@ -149,115 +148,57 @@ def step():


@solara.component
def ModelController(model: solara.Reactive["Model"], play_interval):
def ModelController(model: solara.Reactive["Model"], play_interval=100):
"""
Create controls for model execution (step, play, pause, reset).

Args:
model: The model being visualized
model: The reactive model being visualized
play_interval: Interval between steps during play
current_step: Reactive value for the current step
reset_counter: Counter to trigger model reset
"""
if not isinstance(model, solara.Reactive):
model = solara.use_reactive(model)

playing = solara.use_reactive(False)
thread = solara.use_reactive(None)
# We track the previous step to detect if user resets the model via
# clicking the reset button or changing the parameters. If previous_step >
# current_step, it means a model reset happens while the simulation is
# still playing.
previous_step = solara.use_reactive(0)
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])

def on_value_play(change):
"""Handle play/pause state changes."""
if previous_step.value > model.value.steps and model.value.steps == 0:
# We add extra checks for model.value.steps == 0, just to be sure.
# We automatically stop the playing if a model is reset.
playing.value = False
elif model.value.running:
def step():
while playing.value:
time.sleep(play_interval / 1000)
do_step()
else:
playing.value = False

solara.use_thread(step, [playing.value])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to test if this works on Google Colab.


def do_step():
"""Advance the model by one step."""
previous_step.value = model.value.steps
model.value.step()

def do_play():
"""Run the model continuously."""
model.value.running = True
while model.value.running:
do_step()

def threaded_do_play():
"""Start a new thread for continuous model execution."""
if thread is not None and thread.is_alive():
return
thread.value = threading.Thread(target=do_play)
thread.start()
playing.value = True

def do_pause():
"""Pause the model execution."""
if (thread is None) or (not thread.is_alive()):
return
model.value.running = False
thread.join()
playing.value = False

def do_reset():
"""Reset the model"""
"""Reset the model to its initial state."""
playing.value = False
model.value = copy.deepcopy(original_model.value)
previous_step.value = 0
force_update()

def do_set_playing(value):
"""Set the playing state."""
if model.value.steps == 0:
# This means the model has been recreated, and the step resets to
# 0. We want to avoid triggering the playing.value = False in the
# on_value_play function.
previous_step.value = model.value.steps
playing.set(value)

with solara.Row():
solara.Button(label="Step", color="primary", on_click=do_step)
# This style is necessary so that the play widget has almost the same
# height as typical Solara buttons.
solara.Style(
"""
.widget-play {
height: 35px;
}
.widget-play button {
color: white;
background-color: #1976D2; // Solara blue color
}
"""
)
widgets.Play(
value=0,
interval=play_interval,
repeat=True,
show_repeat=False,
on_value=on_value_play,
playing=playing.value,
on_playing=do_set_playing,
)
with solara.Row(justify="space-between"):
solara.Button(label="Reset", color="primary", on_click=do_reset)
# threaded_do_play is not used for now because it
# doesn't work in Google colab. We use
# ipywidgets.Play until it is fixed. The threading
# version is definite a much better implementation,
# if it works.
# solara.Button(label="▶", color="primary", on_click=viz.threaded_do_play)
# solara.Button(label="⏸︎", color="primary", on_click=viz.do_pause)
# solara.Button(label="Reset", color="primary", on_click=do_reset)
solara.Button(label="Step", color="primary", on_click=do_step)
solara.Button(label="▶", color="primary", on_click=do_play)
solara.Button(label="⏸︎", color="primary", on_click=do_pause)


def split_model_params(model_params):
Expand Down
Loading