Skip to content

Commit cd852c8

Browse files
committed
Simplify ModelController
1 parent 113680d commit cd852c8

File tree

1 file changed

+22
-81
lines changed

1 file changed

+22
-81
lines changed

mesa/visualization/solara_viz.py

Lines changed: 22 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@
2424
"""
2525

2626
import copy
27-
import threading
27+
import time
2828
from typing import TYPE_CHECKING, Literal
2929

30-
import reacton.ipywidgets as widgets
3130
import solara
3231
from solara.alias import rv
3332

@@ -95,7 +94,7 @@ def SolaraViz(
9594
model: "Model" | solara.Reactive["Model"],
9695
components: list[solara.component] | Literal["default"] = "default",
9796
*args,
98-
play_interval=150,
97+
play_interval=100,
9998
model_params=None,
10099
seed=0,
101100
name: str | None = None,
@@ -149,115 +148,57 @@ def step():
149148

150149

151150
@solara.component
152-
def ModelController(model: solara.Reactive["Model"], play_interval):
151+
def ModelController(model: solara.Reactive["Model"], play_interval=100):
153152
"""
154153
Create controls for model execution (step, play, pause, reset).
155154
156155
Args:
157-
model: The model being visualized
156+
model: The reactive model being visualized
158157
play_interval: Interval between steps during play
159-
current_step: Reactive value for the current step
160-
reset_counter: Counter to trigger model reset
161158
"""
159+
if not isinstance(model, solara.Reactive):
160+
model = solara.use_reactive(model)
161+
162162
playing = solara.use_reactive(False)
163-
thread = solara.use_reactive(None)
164-
# We track the previous step to detect if user resets the model via
165-
# clicking the reset button or changing the parameters. If previous_step >
166-
# current_step, it means a model reset happens while the simulation is
167-
# still playing.
168-
previous_step = solara.use_reactive(0)
169163
original_model = solara.use_reactive(None)
170164

171165
def save_initial_model():
172166
"""Save the initial model for comparison."""
173167
original_model.set(copy.deepcopy(model.value))
168+
playing.value = False
169+
force_update()
174170

175171
solara.use_effect(save_initial_model, [model.value])
176172

177-
def on_value_play(change):
178-
"""Handle play/pause state changes."""
179-
if previous_step.value > model.value.steps and model.value.steps == 0:
180-
# We add extra checks for model.value.steps == 0, just to be sure.
181-
# We automatically stop the playing if a model is reset.
182-
playing.value = False
183-
elif model.value.running:
173+
def step():
174+
while playing.value:
175+
time.sleep(play_interval / 1000)
184176
do_step()
185-
else:
186-
playing.value = False
177+
178+
solara.use_thread(step, [playing.value])
187179

188180
def do_step():
189181
"""Advance the model by one step."""
190-
previous_step.value = model.value.steps
191182
model.value.step()
192183

193184
def do_play():
194185
"""Run the model continuously."""
195-
model.value.running = True
196-
while model.value.running:
197-
do_step()
198-
199-
def threaded_do_play():
200-
"""Start a new thread for continuous model execution."""
201-
if thread is not None and thread.is_alive():
202-
return
203-
thread.value = threading.Thread(target=do_play)
204-
thread.start()
186+
playing.value = True
205187

206188
def do_pause():
207189
"""Pause the model execution."""
208-
if (thread is None) or (not thread.is_alive()):
209-
return
210-
model.value.running = False
211-
thread.join()
190+
playing.value = False
212191

213192
def do_reset():
214-
"""Reset the model"""
193+
"""Reset the model to its initial state."""
194+
playing.value = False
215195
model.value = copy.deepcopy(original_model.value)
216-
previous_step.value = 0
217-
force_update()
218-
219-
def do_set_playing(value):
220-
"""Set the playing state."""
221-
if model.value.steps == 0:
222-
# This means the model has been recreated, and the step resets to
223-
# 0. We want to avoid triggering the playing.value = False in the
224-
# on_value_play function.
225-
previous_step.value = model.value.steps
226-
playing.set(value)
227196

228-
with solara.Row():
229-
solara.Button(label="Step", color="primary", on_click=do_step)
230-
# This style is necessary so that the play widget has almost the same
231-
# height as typical Solara buttons.
232-
solara.Style(
233-
"""
234-
.widget-play {
235-
height: 35px;
236-
}
237-
.widget-play button {
238-
color: white;
239-
background-color: #1976D2; // Solara blue color
240-
}
241-
"""
242-
)
243-
widgets.Play(
244-
value=0,
245-
interval=play_interval,
246-
repeat=True,
247-
show_repeat=False,
248-
on_value=on_value_play,
249-
playing=playing.value,
250-
on_playing=do_set_playing,
251-
)
197+
with solara.Row(justify="space-between"):
252198
solara.Button(label="Reset", color="primary", on_click=do_reset)
253-
# threaded_do_play is not used for now because it
254-
# doesn't work in Google colab. We use
255-
# ipywidgets.Play until it is fixed. The threading
256-
# version is definite a much better implementation,
257-
# if it works.
258-
# solara.Button(label="▶", color="primary", on_click=viz.threaded_do_play)
259-
# solara.Button(label="⏸︎", color="primary", on_click=viz.do_pause)
260-
# solara.Button(label="Reset", color="primary", on_click=do_reset)
199+
solara.Button(label="Step", color="primary", on_click=do_step)
200+
solara.Button(label="▶", color="primary", on_click=do_play)
201+
solara.Button(label="⏸︎", color="primary", on_click=do_pause)
261202

262203

263204
def split_model_params(model_params):

0 commit comments

Comments
 (0)