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.
- mesa/__init__.py +1 -1
- mesa/agent.py +3 -3
- mesa/datacollection.py +1 -1
- mesa/examples/advanced/epstein_civil_violence/app.py +11 -11
- mesa/examples/advanced/pd_grid/app.py +10 -11
- mesa/examples/advanced/sugarscape_g1mt/app.py +34 -16
- mesa/examples/advanced/wolf_sheep/app.py +21 -18
- mesa/examples/basic/boid_flockers/app.py +15 -11
- mesa/examples/basic/boltzmann_wealth_model/app.py +39 -32
- mesa/examples/basic/conways_game_of_life/app.py +13 -16
- mesa/examples/basic/schelling/Readme.md +2 -2
- mesa/examples/basic/schelling/agents.py +9 -3
- mesa/examples/basic/schelling/app.py +50 -3
- mesa/examples/basic/schelling/model.py +2 -0
- mesa/examples/basic/schelling/resources/blue_happy.png +0 -0
- mesa/examples/basic/schelling/resources/blue_unhappy.png +0 -0
- mesa/examples/basic/schelling/resources/orange_happy.png +0 -0
- mesa/examples/basic/schelling/resources/orange_unhappy.png +0 -0
- mesa/examples/basic/virus_on_network/app.py +31 -14
- mesa/experimental/continuous_space/continuous_space.py +1 -1
- mesa/space.py +4 -1
- mesa/visualization/__init__.py +2 -0
- mesa/visualization/backends/__init__.py +23 -0
- mesa/visualization/backends/abstract_renderer.py +97 -0
- mesa/visualization/backends/altair_backend.py +440 -0
- mesa/visualization/backends/matplotlib_backend.py +419 -0
- mesa/visualization/components/__init__.py +28 -8
- mesa/visualization/components/altair_components.py +86 -0
- mesa/visualization/components/matplotlib_components.py +4 -2
- mesa/visualization/components/portrayal_components.py +120 -0
- mesa/visualization/mpl_space_drawing.py +292 -129
- mesa/visualization/solara_viz.py +274 -32
- mesa/visualization/space_drawers.py +797 -0
- mesa/visualization/space_renderer.py +399 -0
- {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/METADATA +13 -4
- {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/RECORD +39 -29
- mesa/examples/advanced/sugarscape_g1mt/tests.py +0 -69
- {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/WHEEL +0 -0
- {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/licenses/LICENSE +0 -0
- {mesa-3.2.0.dist-info → mesa-3.3.0.dist-info}/licenses/NOTICE +0 -0
mesa/visualization/solara_viz.py
CHANGED
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
|
137
|
+
model = solara.use_reactive(model) # noqa: RUF100 # noqa: SH102
|
|
126
138
|
|
|
127
|
-
#
|
|
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
|
|
213
|
+
CommandConsole in display_components
|
|
191
214
|
): # If command console in components show it in sidebar
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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__,
|
|
775
|
+
lambda: _check_model_params(model.value.__class__.__init__, user_params),
|
|
557
776
|
[model.value],
|
|
558
777
|
)
|
|
559
|
-
|
|
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
|
|
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(
|
|
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."""
|