Mesa 3.2.0__py3-none-any.whl → 3.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Mesa might be problematic. Click here for more details.

Files changed (40) hide show
  1. mesa/__init__.py +1 -1
  2. mesa/agent.py +3 -3
  3. mesa/datacollection.py +1 -1
  4. mesa/examples/advanced/epstein_civil_violence/app.py +11 -11
  5. mesa/examples/advanced/pd_grid/app.py +10 -11
  6. mesa/examples/advanced/sugarscape_g1mt/app.py +34 -16
  7. mesa/examples/advanced/wolf_sheep/app.py +21 -18
  8. mesa/examples/basic/boid_flockers/app.py +15 -11
  9. mesa/examples/basic/boltzmann_wealth_model/app.py +39 -32
  10. mesa/examples/basic/conways_game_of_life/app.py +13 -16
  11. mesa/examples/basic/schelling/Readme.md +2 -2
  12. mesa/examples/basic/schelling/agents.py +9 -3
  13. mesa/examples/basic/schelling/app.py +50 -3
  14. mesa/examples/basic/schelling/model.py +2 -0
  15. mesa/examples/basic/schelling/resources/blue_happy.png +0 -0
  16. mesa/examples/basic/schelling/resources/blue_unhappy.png +0 -0
  17. mesa/examples/basic/schelling/resources/orange_happy.png +0 -0
  18. mesa/examples/basic/schelling/resources/orange_unhappy.png +0 -0
  19. mesa/examples/basic/virus_on_network/app.py +31 -14
  20. mesa/experimental/continuous_space/continuous_space.py +1 -1
  21. mesa/space.py +4 -1
  22. mesa/visualization/__init__.py +2 -0
  23. mesa/visualization/backends/__init__.py +23 -0
  24. mesa/visualization/backends/abstract_renderer.py +97 -0
  25. mesa/visualization/backends/altair_backend.py +440 -0
  26. mesa/visualization/backends/matplotlib_backend.py +419 -0
  27. mesa/visualization/components/__init__.py +28 -8
  28. mesa/visualization/components/altair_components.py +86 -0
  29. mesa/visualization/components/matplotlib_components.py +4 -2
  30. mesa/visualization/components/portrayal_components.py +120 -0
  31. mesa/visualization/mpl_space_drawing.py +292 -129
  32. mesa/visualization/solara_viz.py +274 -32
  33. mesa/visualization/space_drawers.py +797 -0
  34. mesa/visualization/space_renderer.py +399 -0
  35. {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/METADATA +13 -4
  36. {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/RECORD +39 -29
  37. mesa/examples/advanced/sugarscape_g1mt/tests.py +0 -69
  38. {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/WHEEL +0 -0
  39. {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/licenses/LICENSE +0 -0
  40. {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/licenses/NOTICE +0 -0
@@ -24,13 +24,17 @@ See the Visualization Tutorial and example models for more details.
24
24
  from __future__ import annotations
25
25
 
26
26
  import asyncio
27
+ import collections
27
28
  import inspect
29
+ import itertools
28
30
  import threading
29
31
  import time
30
32
  import traceback
31
33
  from collections.abc import Callable
32
- from typing import TYPE_CHECKING, Literal
34
+ from typing import TYPE_CHECKING, Any, Literal
33
35
 
36
+ import altair as alt
37
+ import pandas as pd
34
38
  import reacton.core
35
39
  import solara
36
40
  import solara.lab
@@ -39,6 +43,7 @@ import mesa.visualization.components.altair_components as components_altair
39
43
  from mesa.experimental.devs.simulator import Simulator
40
44
  from mesa.mesa_logging import create_module_logger, function_logger
41
45
  from mesa.visualization.command_console import CommandConsole
46
+ from mesa.visualization.space_renderer import SpaceRenderer
42
47
  from mesa.visualization.user_param import Slider
43
48
  from mesa.visualization.utils import force_update, update_counter
44
49
 
@@ -52,9 +57,10 @@ _mesa_logger = create_module_logger()
52
57
  @function_logger(__name__)
53
58
  def SolaraViz(
54
59
  model: Model | solara.Reactive[Model],
55
- components: list[reacton.core.Component]
56
- | list[Callable[[Model], reacton.core.Component]]
57
- | Literal["default"] = "default",
60
+ renderer: SpaceRenderer | None = None,
61
+ components: list[tuple[reacton.core.Component], int]
62
+ | list[tuple[Callable[[Model], reacton.core.Component], 0]]
63
+ | Literal["default"] = [], # noqa: B006
58
64
  *,
59
65
  play_interval: int = 100,
60
66
  render_interval: int = 1,
@@ -74,10 +80,11 @@ def SolaraViz(
74
80
  model (Model | solara.Reactive[Model]): A Model instance or a reactive Model.
75
81
  This is the main model to be visualized. If a non-reactive model is provided,
76
82
  it will be converted to a reactive model.
77
- components (list[solara.component] | Literal["default"], optional): List of solara
78
- components or functions that return a solara component.
83
+ renderer (SpaceRenderer): A SpaceRenderer instance to render the model's space.
84
+ components (list[tuple[solara.component], int] | Literal["default"], optional): List of solara
85
+ (components, page) or functions that return a solara (component, page).
79
86
  These components are used to render different parts of the model visualization.
80
- Defaults to "default", which uses the default Altair space visualization.
87
+ Defaults to "default", which uses the default Altair space visualization on page 0.
81
88
  play_interval (int, optional): Interval for playing the model steps in milliseconds.
82
89
  This controls the speed of the model's automatic stepping. Defaults to 100 ms.
83
90
  render_interval (int, optional): Controls how often plots are updated during a simulation,
@@ -113,8 +120,13 @@ def SolaraViz(
113
120
  """
114
121
  if components == "default":
115
122
  components = [
116
- components_altair.make_altair_space(
117
- agent_portrayal=None, propertylayer_portrayal=None, post_process=None
123
+ (
124
+ components_altair.make_altair_space(
125
+ agent_portrayal=None,
126
+ propertylayer_portrayal=None,
127
+ post_process=None,
128
+ ),
129
+ 0,
118
130
  )
119
131
  ]
120
132
  if model_params is None:
@@ -122,13 +134,22 @@ def SolaraViz(
122
134
 
123
135
  # Convert model to reactive
124
136
  if not isinstance(model, solara.Reactive):
125
- model = solara.use_reactive(model) # noqa: SH102, RUF100
137
+ model = solara.use_reactive(model) # noqa: RUF100 # noqa: SH102
126
138
 
127
- # set up reactive model_parameters shared by ModelCreator and ModelController
139
+ # Set up reactive model_parameters shared by ModelCreator and ModelController
128
140
  reactive_model_parameters = solara.use_reactive({})
129
141
  reactive_play_interval = solara.use_reactive(play_interval)
130
142
  reactive_render_interval = solara.use_reactive(render_interval)
131
143
  reactive_use_threads = solara.use_reactive(use_threads)
144
+
145
+ # Make a copy of the components to avoid modifying the original list
146
+ display_components = list(components)
147
+ # Create space component based on the renderer
148
+ if renderer is not None:
149
+ if isinstance(renderer, SpaceRenderer):
150
+ renderer = solara.use_reactive(renderer) # noqa: RUF100 # noqa: SH102
151
+ display_components.insert(0, (create_space_component(renderer.value), 0))
152
+
132
153
  with solara.AppBar():
133
154
  solara.AppBarTitle(name if name else model.value.__class__.__name__)
134
155
  solara.lab.ThemeToggle()
@@ -166,6 +187,7 @@ def SolaraViz(
166
187
  if not isinstance(simulator, Simulator):
167
188
  ModelController(
168
189
  model,
190
+ renderer=renderer,
169
191
  model_parameters=reactive_model_parameters,
170
192
  play_interval=reactive_play_interval,
171
193
  render_interval=reactive_render_interval,
@@ -175,6 +197,7 @@ def SolaraViz(
175
197
  SimulatorController(
176
198
  model,
177
199
  simulator,
200
+ renderer=renderer,
178
201
  model_parameters=reactive_model_parameters,
179
202
  play_interval=reactive_play_interval,
180
203
  render_interval=reactive_render_interval,
@@ -187,14 +210,139 @@ def SolaraViz(
187
210
  with solara.Card("Information"):
188
211
  ShowSteps(model.value)
189
212
  if (
190
- CommandConsole in components
213
+ CommandConsole in display_components
191
214
  ): # If command console in components show it in sidebar
192
- components.remove(CommandConsole)
215
+ display_components.remove(CommandConsole)
193
216
  additional_imports = console_kwargs.get("additional_imports", {})
194
217
  with solara.Card("Command Console"):
195
218
  CommandConsole(model.value, additional_imports=additional_imports)
196
219
 
197
- ComponentsView(components, model.value)
220
+ # Render the main components view
221
+ ComponentsView(display_components, model.value)
222
+
223
+
224
+ def create_space_component(renderer: SpaceRenderer):
225
+ """Create a space visualization component for the given renderer."""
226
+
227
+ def SpaceVisualizationComponent(model: Model):
228
+ """Component that renders the model's space using the provided renderer."""
229
+ return SpaceRendererComponent(model, renderer)
230
+
231
+ return SpaceVisualizationComponent
232
+
233
+
234
+ @solara.component
235
+ def SpaceRendererComponent(
236
+ model: Model,
237
+ renderer: SpaceRenderer,
238
+ # FIXME: Manage dependencies properly
239
+ dependencies: list[Any] | None = None,
240
+ ):
241
+ """Render the space of a model using a SpaceRenderer.
242
+
243
+ Args:
244
+ model (Model): The model whose space is to be rendered.
245
+ renderer: A SpaceRenderer instance to render the model's space.
246
+ dependencies (list[any], optional): List of dependencies for the component.
247
+ """
248
+ update_counter.get()
249
+
250
+ # update renderer's space according to the model's space/grid
251
+ renderer.space = getattr(model, "grid", getattr(model, "space", None))
252
+
253
+ if renderer.backend == "matplotlib":
254
+ # Clear the previous plotted data and agents
255
+ all_artists = [
256
+ renderer.canvas.lines[:],
257
+ renderer.canvas.collections[:],
258
+ renderer.canvas.patches[:],
259
+ renderer.canvas.images[:],
260
+ renderer.canvas.artists[:],
261
+ ]
262
+
263
+ # Remove duplicate colorbars from the canvas
264
+ for cbar in renderer.backend_renderer._active_colorbars:
265
+ cbar.remove()
266
+ renderer.backend_renderer._active_colorbars.clear()
267
+
268
+ # Chain them together into a single iterable
269
+ for artist in itertools.chain.from_iterable(all_artists):
270
+ artist.remove()
271
+
272
+ # Draw the space structure if specified
273
+ if renderer.space_mesh:
274
+ renderer.draw_structure(**renderer.space_kwargs)
275
+
276
+ # Draw agents if specified
277
+ if renderer.agent_mesh:
278
+ renderer.draw_agents(
279
+ agent_portrayal=renderer.agent_portrayal, **renderer.agent_kwargs
280
+ )
281
+
282
+ # Draw property layers if specified
283
+ if renderer.propertylayer_mesh:
284
+ renderer.draw_propertylayer(renderer.propertylayer_portrayal)
285
+
286
+ # Update the fig every time frame
287
+ if dependencies:
288
+ dependencies.append(update_counter.value)
289
+ else:
290
+ dependencies = [update_counter.value]
291
+
292
+ if renderer.post_process and not renderer._post_process_applied:
293
+ renderer.post_process(renderer.canvas)
294
+ renderer._post_process_applied = True
295
+
296
+ solara.FigureMatplotlib(
297
+ renderer.canvas.get_figure(),
298
+ format="png",
299
+ bbox_inches="tight",
300
+ dependencies=dependencies,
301
+ )
302
+ return None
303
+ else:
304
+ structure = renderer.space_mesh if renderer.space_mesh else None
305
+ agents = renderer.agent_mesh if renderer.agent_mesh else None
306
+ propertylayer = renderer.propertylayer_mesh or None
307
+
308
+ if renderer.space_mesh:
309
+ structure = renderer.draw_structure(**renderer.space_kwargs)
310
+ if renderer.agent_mesh:
311
+ agents = renderer.draw_agents(
312
+ renderer.agent_portrayal, **renderer.agent_kwargs
313
+ )
314
+ if renderer.propertylayer_mesh:
315
+ propertylayer = renderer.draw_propertylayer(
316
+ renderer.propertylayer_portrayal
317
+ )
318
+
319
+ spatial_charts_list = [
320
+ chart for chart in [structure, propertylayer, agents] if chart
321
+ ]
322
+
323
+ final_chart = None
324
+ if spatial_charts_list:
325
+ final_chart = (
326
+ spatial_charts_list[0]
327
+ if len(spatial_charts_list) == 1
328
+ else alt.layer(*spatial_charts_list).resolve_axis(
329
+ x="independent", y="independent"
330
+ )
331
+ )
332
+
333
+ if final_chart is None:
334
+ # If no charts are available, return an empty chart
335
+ final_chart = (
336
+ alt.Chart(pd.DataFrame()).mark_point().properties(width=450, height=350)
337
+ )
338
+
339
+ if renderer.post_process:
340
+ final_chart = renderer.post_process(final_chart)
341
+
342
+ final_chart = final_chart.configure_view(stroke="black", strokeWidth=1.5)
343
+
344
+ solara.FigureAltair(final_chart, on_click=None, on_hover=None)
345
+ return None
198
346
 
199
347
 
200
348
  def _wrap_component(
@@ -214,27 +362,88 @@ def _wrap_component(
214
362
 
215
363
  @solara.component
216
364
  def ComponentsView(
217
- components: list[reacton.core.Component]
218
- | list[Callable[[Model], reacton.core.Component]],
365
+ components: list[tuple[reacton.core.Component], int]
366
+ | list[tuple[Callable[[Model], reacton.core.Component], int]],
219
367
  model: Model,
220
368
  ):
221
369
  """Display a list of components.
222
370
 
223
371
  Args:
224
- components: List of components to display
372
+ components: List of (components, page) to display
225
373
  model: Model instance to pass to each component
226
374
  """
227
- wrapped_components = [_wrap_component(component) for component in components]
228
- items = [component(model) for component in wrapped_components]
229
- grid_layout_initial = make_initial_grid_layout(num_components=len(items))
230
- grid_layout, set_grid_layout = solara.use_state(grid_layout_initial)
231
- solara.GridDraggable(
232
- items=items,
233
- grid_layout=grid_layout,
234
- resizable=True,
235
- draggable=True,
236
- on_grid_layout=set_grid_layout,
237
- )
375
+ if not components:
376
+ return
377
+
378
+ # Backward's compatibility, page = 0 if not passed.
379
+ for i, comp in enumerate(components):
380
+ if not isinstance(comp, tuple):
381
+ components[i] = (comp, 0)
382
+
383
+ # Build pages mapping
384
+ pages = collections.defaultdict(list)
385
+ for component, page_index in components:
386
+ pages[page_index].append(_wrap_component(component))
387
+
388
+ # Fill in missing page indices for sequential tab order
389
+ all_indices = sorted(pages.keys())
390
+ if len(all_indices) > 1:
391
+ min_page, max_page = all_indices[0], all_indices[-1]
392
+ all_indices = list(range(min_page, max_page + 1))
393
+ for idx in all_indices:
394
+ pages.setdefault(idx, [])
395
+
396
+ sorted_page_indices = all_indices
397
+
398
+ # State for current tab and layouts
399
+ current_tab_index, set_current_tab_index = solara.use_state(0)
400
+ layouts, set_layouts = solara.use_state({})
401
+
402
+ # Keep layouts in sync with pages
403
+ def sync_layouts():
404
+ current_keys = set(pages.keys())
405
+ layout_keys = set(layouts.keys())
406
+
407
+ # Add layouts for new pages
408
+ new_layouts = {
409
+ index: make_initial_grid_layout(len(pages[index]))
410
+ for index in current_keys - layout_keys
411
+ }
412
+
413
+ # Remove layouts for deleted pages
414
+ cleaned_layouts = {k: v for k, v in layouts.items() if k in current_keys}
415
+
416
+ if new_layouts or len(cleaned_layouts) != len(layouts):
417
+ set_layouts({**cleaned_layouts, **new_layouts})
418
+
419
+ solara.use_effect(sync_layouts, list(pages.keys()))
420
+
421
+ # Tab Navigation
422
+ with solara.v.Tabs(v_model=current_tab_index, on_v_model=set_current_tab_index):
423
+ for index in sorted_page_indices:
424
+ solara.v.Tab(children=[f"Page {index}"])
425
+
426
+ with solara.v.TabsItems(v_model=current_tab_index):
427
+ for _, page_id in enumerate(sorted_page_indices):
428
+ with solara.v.TabItem():
429
+ if page_id == current_tab_index:
430
+ page_components = pages[page_id]
431
+ page_layout = layouts.get(page_id)
432
+
433
+ if page_layout:
434
+
435
+ def on_layout_change(new_layout, current_page_id=page_id):
436
+ set_layouts(
437
+ lambda old: {**old, current_page_id: new_layout}
438
+ )
439
+
440
+ solara.GridDraggable(
441
+ items=[c(model) for c in page_components],
442
+ grid_layout=page_layout,
443
+ resizable=True,
444
+ draggable=True,
445
+ on_grid_layout=on_layout_change,
446
+ )
238
447
 
239
448
 
240
449
  JupyterViz = SolaraViz
@@ -244,6 +453,7 @@ JupyterViz = SolaraViz
244
453
  def ModelController(
245
454
  model: solara.Reactive[Model],
246
455
  *,
456
+ renderer: solara.Reactive[SpaceRenderer] | None = None,
247
457
  model_parameters: dict | solara.Reactive[dict] = None,
248
458
  play_interval: int | solara.Reactive[int] = 100,
249
459
  render_interval: int | solara.Reactive[int] = 1,
@@ -253,6 +463,7 @@ def ModelController(
253
463
 
254
464
  Args:
255
465
  model: Reactive model instance
466
+ renderer: SpaceRenderer instance to render the model's space.
256
467
  model_parameters: Reactive parameters for (re-)instantiating a model.
257
468
  play_interval: Interval for playing the model steps in milliseconds.
258
469
  render_interval: Controls how often the plots are updated during simulation steps.Higher value reduce update frequency.
@@ -331,6 +542,9 @@ def ModelController(
331
542
  f"creating new {model.value.__class__} instance with {model_parameters.value}",
332
543
  )
333
544
  model.value = model.value = model.value.__class__(**model_parameters.value)
545
+ if renderer is not None:
546
+ renderer.value = copy_renderer(renderer.value, model.value)
547
+ force_update()
334
548
 
335
549
  @function_logger(__name__)
336
550
  def do_play_pause():
@@ -360,6 +574,7 @@ def ModelController(
360
574
  def SimulatorController(
361
575
  model: solara.Reactive[Model],
362
576
  simulator,
577
+ renderer: solara.Reactive[SpaceRenderer] | None = None,
363
578
  *,
364
579
  model_parameters: dict | solara.Reactive[dict] = None,
365
580
  play_interval: int | solara.Reactive[int] = 100,
@@ -371,6 +586,7 @@ def SimulatorController(
371
586
  Args:
372
587
  model: Reactive model instance
373
588
  simulator: Simulator instance
589
+ renderer: SpaceRenderer instance to render the model's space.
374
590
  model_parameters: Reactive parameters for (re-)instantiating a model.
375
591
  play_interval: Interval for playing the model steps in milliseconds.
376
592
  render_interval: Controls how often the plots are updated during simulation steps.Higher values reduce update frequency.
@@ -453,6 +669,9 @@ def SimulatorController(
453
669
  model.value = model.value = model.value.__class__(
454
670
  simulator=simulator, **model_parameters.value
455
671
  )
672
+ if renderer is not None:
673
+ renderer.value = copy_renderer(renderer.value, model.value)
674
+ force_update()
456
675
 
457
676
  def do_play_pause():
458
677
  """Toggle play/pause."""
@@ -553,10 +772,10 @@ def ModelCreator(
553
772
  model_parameters = solara.use_reactive(model_parameters)
554
773
 
555
774
  solara.use_effect(
556
- lambda: _check_model_params(model.value.__class__.__init__, fixed_params),
775
+ lambda: _check_model_params(model.value.__class__.__init__, user_params),
557
776
  [model.value],
558
777
  )
559
- user_params, fixed_params = split_model_params(user_params)
778
+ user_adjust_params, fixed_params = split_model_params(user_params)
560
779
 
561
780
  # Use solara.use_effect to run the initialization code only once
562
781
  solara.use_effect(
@@ -564,7 +783,7 @@ def ModelCreator(
564
783
  lambda: model_parameters.set(
565
784
  {
566
785
  **fixed_params,
567
- **{k: v.get("value") for k, v in user_params.items()},
786
+ **{k: v.get("value") for k, v in user_adjust_params.items()},
568
787
  }
569
788
  ),
570
789
  [],
@@ -574,7 +793,7 @@ def ModelCreator(
574
793
  def on_change(name, value):
575
794
  model_parameters.value = {**model_parameters.value, name: value}
576
795
 
577
- UserInputs(user_params, on_change=on_change)
796
+ UserInputs(user_adjust_params, on_change=on_change)
578
797
 
579
798
 
580
799
  def _check_model_params(init_func, model_params):
@@ -708,6 +927,29 @@ def make_initial_grid_layout(num_components):
708
927
  ]
709
928
 
710
929
 
930
+ def copy_renderer(renderer: SpaceRenderer, model: Model):
931
+ """Create a new renderer instance with the same configuration as the original."""
932
+ new_renderer = renderer.__class__(model=model, backend=renderer.backend)
933
+
934
+ attributes_to_copy = [
935
+ "agent_portrayal",
936
+ "propertylayer_portrayal",
937
+ "space_kwargs",
938
+ "agent_kwargs",
939
+ "space_mesh",
940
+ "agent_mesh",
941
+ "propertylayer_mesh",
942
+ "post_process_func",
943
+ ]
944
+
945
+ for attr in attributes_to_copy:
946
+ if hasattr(renderer, attr):
947
+ value_to_copy = getattr(renderer, attr)
948
+ setattr(new_renderer, attr, value_to_copy)
949
+
950
+ return new_renderer
951
+
952
+
711
953
  @solara.component
712
954
  def ShowSteps(model):
713
955
  """Display the current step of the model."""