25
25
26
26
import asyncio
27
27
import inspect
28
+ import threading
29
+ import time
28
30
from collections .abc import Callable
29
31
from typing import TYPE_CHECKING , Literal
30
32
@@ -57,6 +59,7 @@ def SolaraViz(
57
59
simulator : Simulator | None = None ,
58
60
model_params = None ,
59
61
name : str | None = None ,
62
+ use_threads : bool = False ,
60
63
):
61
64
"""Solara visualization component.
62
65
@@ -76,6 +79,8 @@ def SolaraViz(
76
79
This controls the speed of the model's automatic stepping. Defaults to 100 ms.
77
80
render_interval (int, optional): Controls how often plots are updated during a simulation,
78
81
allowing users to skip intermediate steps and update graphs less frequently.
82
+ use_threads: Flag for indicating whether to utilize multi-threading for model execution.
83
+ When checked, the model will utilize multiple threads,adjust based on system capabilities.
79
84
simulator: A simulator that controls the model (optional)
80
85
model_params (dict, optional): Parameters for (re-)instantiating a model.
81
86
Can include user-adjustable parameters and fixed parameters. Defaults to None.
@@ -114,6 +119,7 @@ def SolaraViz(
114
119
reactive_model_parameters = solara .use_reactive ({})
115
120
reactive_play_interval = solara .use_reactive (play_interval )
116
121
reactive_render_interval = solara .use_reactive (render_interval )
122
+ reactive_use_threads = solara .use_reactive (use_threads )
117
123
with solara .AppBar ():
118
124
solara .AppBarTitle (name if name else model .value .__class__ .__name__ )
119
125
solara .lab .ThemeToggle ()
@@ -136,12 +142,25 @@ def SolaraViz(
136
142
max = 100 ,
137
143
step = 2 ,
138
144
)
145
+ if reactive_use_threads .value :
146
+ solara .Text ("Increase play interval to avoid skipping plots" )
147
+
148
+ def set_reactive_use_threads (value ):
149
+ reactive_use_threads .set (value )
150
+
151
+ solara .Checkbox (
152
+ label = "Use Threads" ,
153
+ value = reactive_use_threads ,
154
+ on_value = set_reactive_use_threads ,
155
+ )
156
+
139
157
if not isinstance (simulator , Simulator ):
140
158
ModelController (
141
159
model ,
142
160
model_parameters = reactive_model_parameters ,
143
161
play_interval = reactive_play_interval ,
144
162
render_interval = reactive_render_interval ,
163
+ use_threads = reactive_use_threads ,
145
164
)
146
165
else :
147
166
SimulatorController (
@@ -150,6 +169,7 @@ def SolaraViz(
150
169
model_parameters = reactive_model_parameters ,
151
170
play_interval = reactive_play_interval ,
152
171
render_interval = reactive_render_interval ,
172
+ use_threads = reactive_use_threads ,
153
173
)
154
174
with solara .Card ("Model Parameters" ):
155
175
ModelCreator (
@@ -211,6 +231,7 @@ def ModelController(
211
231
model_parameters : dict | solara .Reactive [dict ] = None ,
212
232
play_interval : int | solara .Reactive [int ] = 100 ,
213
233
render_interval : int | solara .Reactive [int ] = 1 ,
234
+ use_threads : bool | solara .Reactive [bool ] = False ,
214
235
):
215
236
"""Create controls for model execution (step, play, pause, reset).
216
237
@@ -219,37 +240,70 @@ def ModelController(
219
240
model_parameters: Reactive parameters for (re-)instantiating a model.
220
241
play_interval: Interval for playing the model steps in milliseconds.
221
242
render_interval: Controls how often the plots are updated during simulation steps.Higher value reduce update frequency.
243
+ use_threads: Flag for indicating whether to utilize multi-threading for model execution.
222
244
"""
223
245
playing = solara .use_reactive (False )
224
246
running = solara .use_reactive (True )
247
+
225
248
if model_parameters is None :
226
249
model_parameters = {}
227
250
model_parameters = solara .use_reactive (model_parameters )
228
-
229
- async def step ():
230
- while playing .value and running .value :
231
- await asyncio .sleep (play_interval .value / 1000 )
232
- do_step ()
251
+ visualization_pause_event = solara .use_memo (lambda : threading .Event (), [])
252
+
253
+ def step ():
254
+ try :
255
+ while running .value and playing .value :
256
+ time .sleep (play_interval .value / 1000 )
257
+ do_step ()
258
+ if use_threads .value :
259
+ visualization_pause_event .set ()
260
+ except Exception as e :
261
+ print (f"Error in step: { e } " )
262
+ return
263
+
264
+ def visualization_task ():
265
+ if use_threads .value :
266
+ try :
267
+ while playing .value and running .value :
268
+ visualization_pause_event .wait ()
269
+ visualization_pause_event .clear ()
270
+ force_update ()
271
+ except Exception as e :
272
+ print (f"Error in visualization_task: { e } " )
233
273
234
274
solara .lab .use_task (
235
- step , dependencies = [playing .value , running .value ], prefer_threaded = False
275
+ step , dependencies = [playing .value , running .value ], prefer_threaded = True
276
+ )
277
+
278
+ solara .use_thread (
279
+ visualization_task ,
280
+ dependencies = [playing .value , running .value ],
236
281
)
237
282
238
283
@function_logger (__name__ )
239
284
def do_step ():
240
285
"""Advance the model by the number of steps specified by the render_interval slider."""
241
- for _ in range (render_interval .value ):
242
- model .value .step ()
286
+ if playing .value :
287
+ for _ in range (render_interval .value ):
288
+ model .value .step ()
289
+ running .value = model .value .running
290
+ if not playing .value :
291
+ break
292
+ if not use_threads .value :
293
+ force_update ()
243
294
244
- running .value = model .value .running
245
-
246
- force_update ()
295
+ else :
296
+ for _ in range (render_interval .value ):
297
+ model .value .step ()
298
+ running .value = model .value .running
299
+ force_update ()
247
300
248
301
@function_logger (__name__ )
249
302
def do_reset ():
250
303
"""Reset the model to its initial state."""
251
304
playing .value = False
252
305
running .value = True
306
+ visualization_pause_event .clear ()
253
307
_mesa_logger .log (
254
308
10 ,
255
309
f"creating new { model .value .__class__ } instance with { model_parameters .value } " ,
@@ -285,6 +339,7 @@ def SimulatorController(
285
339
model_parameters : dict | solara .Reactive [dict ] = None ,
286
340
play_interval : int | solara .Reactive [int ] = 100 ,
287
341
render_interval : int | solara .Reactive [int ] = 1 ,
342
+ use_threads : bool | solara .Reactive [bool ] = False ,
288
343
):
289
344
"""Create controls for model execution (step, play, pause, reset).
290
345
@@ -294,6 +349,7 @@ def SimulatorController(
294
349
model_parameters: Reactive parameters for (re-)instantiating a model.
295
350
play_interval: Interval for playing the model steps in milliseconds.
296
351
render_interval: Controls how often the plots are updated during simulation steps.Higher values reduce update frequency.
352
+ use_threads: Flag for indicating whether to utilize multi-threading for model execution.
297
353
298
354
Notes:
299
355
The `step button` increments the step by the value specified in the `render_interval` slider.
@@ -304,27 +360,66 @@ def SimulatorController(
304
360
if model_parameters is None :
305
361
model_parameters = {}
306
362
model_parameters = solara .use_reactive (model_parameters )
307
-
308
- async def step ():
309
- while playing .value and running .value :
310
- await asyncio .sleep (play_interval .value / 1000 )
311
- do_step ()
363
+ visualization_pause_event = solara .use_memo (lambda : threading .Event (), [])
364
+ pause_step_event = solara .use_memo (lambda : threading .Event (), [])
365
+
366
+ def step ():
367
+ try :
368
+ while running .value and playing .value :
369
+ time .sleep (play_interval .value / 1000 )
370
+ if use_threads .value :
371
+ pause_step_event .wait ()
372
+ pause_step_event .clear ()
373
+ do_step ()
374
+ if use_threads .value :
375
+ visualization_pause_event .set ()
376
+ except Exception as e :
377
+ print (f"Error in step: { e } " )
378
+
379
+ def visualization_task ():
380
+ if use_threads .value :
381
+ try :
382
+ loop = asyncio .new_event_loop ()
383
+ asyncio .set_event_loop (loop )
384
+ pause_step_event .set ()
385
+ while playing .value and running .value :
386
+ visualization_pause_event .wait ()
387
+ visualization_pause_event .clear ()
388
+ force_update ()
389
+ pause_step_event .set ()
390
+ except Exception as e :
391
+ print (f"Error in visualization_task: { e } " )
392
+ return
312
393
313
394
solara .lab .use_task (
314
395
step , dependencies = [playing .value , running .value ], prefer_threaded = False
315
396
)
397
+ solara .lab .use_task (visualization_task , dependencies = [playing .value ])
316
398
317
399
def do_step ():
318
400
"""Advance the model by the number of steps specified by the render_interval slider."""
319
- simulator .run_for (render_interval .value )
320
- running .value = model .value .running
321
- force_update ()
401
+ if playing .value :
402
+ for _ in range (render_interval .value ):
403
+ simulator .run_for (1 )
404
+ running .value = model .value .running
405
+ if not playing .value :
406
+ break
407
+ if not use_threads .value :
408
+ force_update ()
409
+
410
+ else :
411
+ for _ in range (render_interval .value ):
412
+ simulator .run_for (1 )
413
+ running .value = model .value .running
414
+ force_update ()
322
415
323
416
def do_reset ():
324
417
"""Reset the model to its initial state."""
325
418
playing .value = False
326
419
running .value = True
327
420
simulator .reset ()
421
+ visualization_pause_event .clear ()
422
+ pause_step_event .clear ()
328
423
model .value = model .value = model .value .__class__ (
329
424
simulator = simulator , ** model_parameters .value
330
425
)
0 commit comments