|
24 | 24 | """ |
25 | 25 |
|
26 | 26 | import copy |
27 | | -import threading |
| 27 | +import time |
28 | 28 | from typing import TYPE_CHECKING, Literal |
29 | 29 |
|
30 | | -import reacton.ipywidgets as widgets |
31 | 30 | import solara |
32 | 31 | from solara.alias import rv |
33 | 32 |
|
@@ -95,7 +94,7 @@ def SolaraViz( |
95 | 94 | model: "Model" | solara.Reactive["Model"], |
96 | 95 | components: list[solara.component] | Literal["default"] = "default", |
97 | 96 | *args, |
98 | | - play_interval=150, |
| 97 | + play_interval=100, |
99 | 98 | model_params=None, |
100 | 99 | seed=0, |
101 | 100 | name: str | None = None, |
@@ -149,115 +148,57 @@ def step(): |
149 | 148 |
|
150 | 149 |
|
151 | 150 | @solara.component |
152 | | -def ModelController(model: solara.Reactive["Model"], play_interval): |
| 151 | +def ModelController(model: solara.Reactive["Model"], play_interval=100): |
153 | 152 | """ |
154 | 153 | Create controls for model execution (step, play, pause, reset). |
155 | 154 |
|
156 | 155 | Args: |
157 | | - model: The model being visualized |
| 156 | + model: The reactive model being visualized |
158 | 157 | play_interval: Interval between steps during play |
159 | | - current_step: Reactive value for the current step |
160 | | - reset_counter: Counter to trigger model reset |
161 | 158 | """ |
| 159 | + if not isinstance(model, solara.Reactive): |
| 160 | + model = solara.use_reactive(model) |
| 161 | + |
162 | 162 | 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) |
169 | 163 | original_model = solara.use_reactive(None) |
170 | 164 |
|
171 | 165 | def save_initial_model(): |
172 | 166 | """Save the initial model for comparison.""" |
173 | 167 | original_model.set(copy.deepcopy(model.value)) |
| 168 | + playing.value = False |
| 169 | + force_update() |
174 | 170 |
|
175 | 171 | solara.use_effect(save_initial_model, [model.value]) |
176 | 172 |
|
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) |
184 | 176 | do_step() |
185 | | - else: |
186 | | - playing.value = False |
| 177 | + |
| 178 | + solara.use_thread(step, [playing.value]) |
187 | 179 |
|
188 | 180 | def do_step(): |
189 | 181 | """Advance the model by one step.""" |
190 | | - previous_step.value = model.value.steps |
191 | 182 | model.value.step() |
192 | 183 |
|
193 | 184 | def do_play(): |
194 | 185 | """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 |
205 | 187 |
|
206 | 188 | def do_pause(): |
207 | 189 | """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 |
212 | 191 |
|
213 | 192 | def do_reset(): |
214 | | - """Reset the model""" |
| 193 | + """Reset the model to its initial state.""" |
| 194 | + playing.value = False |
215 | 195 | 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) |
227 | 196 |
|
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"): |
252 | 198 | 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) |
261 | 202 |
|
262 | 203 |
|
263 | 204 | def split_model_params(model_params): |
|
0 commit comments