1
+ """
2
+ Mesa visualization module for creating interactive model visualizations.
3
+
4
+ This module provides components to create browser- and Jupyter notebook-based visualizations of
5
+ Mesa models, allowing users to watch models run step-by-step and interact with model parameters.
6
+
7
+ Key features:
8
+ - JupyterViz: Main component for creating visualizations, supporting grid displays and plots
9
+ - ModelController: Handles model execution controls (step, play, pause, reset)
10
+ - UserInputs: Generates UI elements for adjusting model parameters
11
+ - Card: Renders individual visualization elements (space, measures)
12
+
13
+ The module uses Solara for rendering in Jupyter notebooks or as standalone web applications.
14
+ It supports various types of visualizations including matplotlib plots, agent grids, and
15
+ custom visualization components.
16
+
17
+ Usage:
18
+ 1. Define an agent_portrayal function to specify how agents should be displayed
19
+ 2. Set up model_params to define adjustable parameters
20
+ 3. Create a JupyterViz instance with your model, parameters, and desired measures
21
+ 4. Display the visualization in a Jupyter notebook or run as a Solara app
22
+
23
+ See the Visualization Tutorial and example models for more details.
24
+ """
25
+
1
26
import sys
2
27
import threading
3
28
19
44
def Card (
20
45
model , measures , agent_portrayal , space_drawer , dependencies , color , layout_type
21
46
):
47
+ """
48
+ Create a card component for visualizing model space or measures.
49
+
50
+ Args:
51
+ model: The Mesa model instance
52
+ measures: List of measures to be plotted
53
+ agent_portrayal: Function to define agent appearance
54
+ space_drawer: Method to render agent space
55
+ dependencies: List of dependencies for updating the visualization
56
+ color: Background color of the card
57
+ layout_type: Type of layout (Space or Measure)
58
+
59
+ Returns:
60
+ rv.Card: A card component containing the visualization
61
+ """
22
62
with rv .Card (
23
63
style_ = f"background-color: { color } ; width: 100%; height: 100%"
24
64
) as main :
@@ -60,19 +100,21 @@ def JupyterViz(
60
100
play_interval = 150 ,
61
101
seed = None ,
62
102
):
63
- """Initialize a component to visualize a model.
103
+ """
104
+ Initialize a component to visualize a model.
105
+
64
106
Args:
65
- model_class: class of the model to instantiate
66
- model_params: parameters for initializing the model
67
- measures: list of callables or data attributes to plot
68
- name: name for display
69
- agent_portrayal: options for rendering agents (dictionary)
70
- space_drawer: method to render the agent space for
107
+ model_class: Class of the model to instantiate
108
+ model_params: Parameters for initializing the model
109
+ measures: List of callables or data attributes to plot
110
+ name: Name for display
111
+ agent_portrayal: Options for rendering agents (dictionary)
112
+ space_drawer: Method to render the agent space for
71
113
the model; default implementation is the `SpaceMatplotlib` component;
72
114
simulations with no space to visualize should
73
115
specify `space_drawer=False`
74
- play_interval: play interval (default: 150)
75
- seed: the random seed used to initialize the model
116
+ play_interval: Play interval (default: 150)
117
+ seed: The random seed used to initialize the model
76
118
"""
77
119
if name is None :
78
120
name = model_class .__name__
@@ -88,6 +130,7 @@ def JupyterViz(
88
130
89
131
# 2. Set up Model
90
132
def make_model ():
133
+ """Create a new model instance with current parameters and seed."""
91
134
model = model_class .__new__ (
92
135
model_class , ** model_parameters , seed = reactive_seed .value
93
136
)
@@ -106,6 +149,7 @@ def make_model():
106
149
)
107
150
108
151
def handle_change_model_params (name : str , value : any ):
152
+ """Update model parameters when user input changes."""
109
153
set_model_parameters ({** model_parameters , name : value })
110
154
111
155
# 3. Set up UI
@@ -115,12 +159,14 @@ def handle_change_model_params(name: str, value: any):
115
159
116
160
# render layout and plot
117
161
def do_reseed ():
162
+ """Update the random seed for the model."""
118
163
reactive_seed .value = model .random .random ()
119
164
120
165
# jupyter
121
166
dependencies = [current_step .value , reactive_seed .value ]
122
167
123
168
def render_in_jupyter ():
169
+ """Render the visualization components in Jupyter notebook."""
124
170
with solara .GridFixed (columns = 2 ):
125
171
UserInputs (user_params , on_change = handle_change_model_params )
126
172
ModelController (model , play_interval , current_step , reset_counter )
@@ -154,6 +200,7 @@ def render_in_jupyter():
154
200
)
155
201
156
202
def render_in_browser ():
203
+ """Render the visualization components in a web browser."""
157
204
# if space drawer is disabled, do not include it
158
205
layout_types = [{"Space" : "default" }] if space_drawer else []
159
206
@@ -205,6 +252,15 @@ def render_in_browser():
205
252
206
253
@solara .component
207
254
def ModelController (model , play_interval , current_step , reset_counter ):
255
+ """
256
+ Create controls for model execution (step, play, pause, reset).
257
+
258
+ Args:
259
+ model: The model being visualized
260
+ play_interval: Interval between steps during play
261
+ current_step: Reactive value for the current step
262
+ reset_counter: Counter to trigger model reset
263
+ """
208
264
playing = solara .use_reactive (False )
209
265
thread = solara .use_reactive (None )
210
266
# We track the previous step to detect if user resets the model via
@@ -214,6 +270,7 @@ def ModelController(model, play_interval, current_step, reset_counter):
214
270
previous_step = solara .use_reactive (0 )
215
271
216
272
def on_value_play (change ):
273
+ """Handle play/pause state changes."""
217
274
if previous_step .value > current_step .value and current_step .value == 0 :
218
275
# We add extra checks for current_step.value == 0, just to be sure.
219
276
# We automatically stop the playing if a model is reset.
@@ -224,31 +281,37 @@ def on_value_play(change):
224
281
playing .value = False
225
282
226
283
def do_step ():
284
+ """Advance the model by one step."""
227
285
model .step ()
228
286
previous_step .value = current_step .value
229
287
current_step .value = model ._steps
230
288
231
289
def do_play ():
290
+ """Run the model continuously."""
232
291
model .running = True
233
292
while model .running :
234
293
do_step ()
235
294
236
295
def threaded_do_play ():
296
+ """Start a new thread for continuous model execution."""
237
297
if thread is not None and thread .is_alive ():
238
298
return
239
299
thread .value = threading .Thread (target = do_play )
240
300
thread .start ()
241
301
242
302
def do_pause ():
303
+ """Pause the model execution."""
243
304
if (thread is None ) or (not thread .is_alive ()):
244
305
return
245
306
model .running = False
246
307
thread .join ()
247
308
248
309
def do_reset ():
310
+ """Reset the model."""
249
311
reset_counter .value += 1
250
312
251
313
def do_set_playing (value ):
314
+ """Set the playing state."""
252
315
if current_step .value == 0 :
253
316
# This means the model has been recreated, and the step resets to
254
317
# 0. We want to avoid triggering the playing.value = False in the
@@ -292,6 +355,15 @@ def do_set_playing(value):
292
355
293
356
294
357
def split_model_params (model_params ):
358
+ """
359
+ Split model parameters into user-adjustable and fixed parameters.
360
+
361
+ Args:
362
+ model_params: Dictionary of all model parameters
363
+
364
+ Returns:
365
+ tuple: (user_adjustable_params, fixed_params)
366
+ """
295
367
model_params_input = {}
296
368
model_params_fixed = {}
297
369
for k , v in model_params .items ():
@@ -303,6 +375,15 @@ def split_model_params(model_params):
303
375
304
376
305
377
def check_param_is_fixed (param ):
378
+ """
379
+ Check if a parameter is fixed (not user-adjustable).
380
+
381
+ Args:
382
+ param: Parameter to check
383
+
384
+ Returns:
385
+ bool: True if parameter is fixed, False otherwise
386
+ """
306
387
if isinstance (param , Slider ):
307
388
return False
308
389
if not isinstance (param , dict ):
@@ -313,14 +394,15 @@ def check_param_is_fixed(param):
313
394
314
395
@solara .component
315
396
def UserInputs (user_params , on_change = None ):
316
- """Initialize user inputs for configurable model parameters.
397
+ """
398
+ Initialize user inputs for configurable model parameters.
317
399
Currently supports :class:`solara.SliderInt`, :class:`solara.SliderFloat`,
318
400
:class:`solara.Select`, and :class:`solara.Checkbox`.
319
401
320
- Props :
321
- user_params: dictionary with options for the input, including label,
402
+ Args :
403
+ user_params: Dictionary with options for the input, including label,
322
404
min and max values, and other fields specific to the input type.
323
- on_change: function to be called with (name, value) when the value of an input changes.
405
+ on_change: Function to be called with (name, value) when the value of an input changes.
324
406
"""
325
407
326
408
for name , options in user_params .items ():
@@ -381,13 +463,32 @@ def change_handler(value, name=name):
381
463
382
464
383
465
def make_text (renderer ):
466
+ """
467
+ Create a function that renders text using Markdown.
468
+
469
+ Args:
470
+ renderer: Function that takes a model and returns a string
471
+
472
+ Returns:
473
+ function: A function that renders the text as Markdown
474
+ """
475
+
384
476
def function (model ):
385
477
solara .Markdown (renderer (model ))
386
478
387
479
return function
388
480
389
481
390
482
def make_initial_grid_layout (layout_types ):
483
+ """
484
+ Create an initial grid layout for visualization components.
485
+
486
+ Args:
487
+ layout_types: List of layout types (Space or Measure)
488
+
489
+ Returns:
490
+ list: Initial grid layout configuration
491
+ """
391
492
return [
392
493
{
393
494
"i" : i ,
0 commit comments