Mesa 3.0.0a3__py3-none-any.whl → 3.0.0a5__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 +2 -3
- mesa/agent.py +193 -75
- mesa/batchrunner.py +18 -23
- mesa/cookiecutter-mesa/hooks/post_gen_project.py +2 -0
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py +1 -0
- mesa/datacollection.py +138 -27
- mesa/experimental/UserParam.py +67 -0
- mesa/experimental/__init__.py +5 -1
- mesa/experimental/cell_space/__init__.py +7 -0
- mesa/experimental/cell_space/cell.py +61 -20
- mesa/experimental/cell_space/cell_agent.py +12 -7
- mesa/experimental/cell_space/cell_collection.py +54 -17
- mesa/experimental/cell_space/discrete_space.py +16 -5
- mesa/experimental/cell_space/grid.py +19 -8
- mesa/experimental/cell_space/network.py +9 -7
- mesa/experimental/cell_space/voronoi.py +26 -33
- mesa/experimental/components/altair.py +81 -0
- mesa/experimental/components/matplotlib.py +242 -0
- mesa/experimental/devs/__init__.py +2 -0
- mesa/experimental/devs/eventlist.py +36 -15
- mesa/experimental/devs/examples/epstein_civil_violence.py +71 -39
- mesa/experimental/devs/examples/wolf_sheep.py +43 -44
- mesa/experimental/devs/simulator.py +55 -15
- mesa/experimental/solara_viz.py +453 -0
- mesa/main.py +6 -4
- mesa/model.py +64 -61
- mesa/space.py +154 -123
- mesa/time.py +57 -67
- mesa/visualization/UserParam.py +19 -6
- mesa/visualization/__init__.py +14 -2
- mesa/visualization/components/altair.py +18 -1
- mesa/visualization/components/matplotlib.py +26 -2
- mesa/visualization/solara_viz.py +231 -225
- mesa/visualization/utils.py +9 -0
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/METADATA +2 -1
- mesa-3.0.0a5.dist-info/RECORD +44 -0
- mesa-3.0.0a3.dist-info/RECORD +0 -39
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/WHEEL +0 -0
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""Provides several simulator classes.
|
|
2
|
+
|
|
3
|
+
A Simulator is responsible for executing a simulation model. It controls time advancement and enables event scheduling.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from __future__ import annotations
|
|
2
9
|
|
|
3
10
|
import numbers
|
|
@@ -27,6 +34,12 @@ class Simulator:
|
|
|
27
34
|
# TODO: add experimentation support
|
|
28
35
|
|
|
29
36
|
def __init__(self, time_unit: type, start_time: int | float):
|
|
37
|
+
"""Initialize a Simulator instance.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
time_unit: type of the smulaiton time
|
|
41
|
+
start_time: the starttime of the simulator
|
|
42
|
+
"""
|
|
30
43
|
# should model run in a separate thread,
|
|
31
44
|
# and we can then interact with start, stop, run_until, and step?
|
|
32
45
|
self.event_list = EventList()
|
|
@@ -36,10 +49,10 @@ class Simulator:
|
|
|
36
49
|
self.time = self.start_time
|
|
37
50
|
self.model = None
|
|
38
51
|
|
|
39
|
-
def check_time_unit(self, time: int | float) -> bool: ...
|
|
52
|
+
def check_time_unit(self, time: int | float) -> bool: ... # noqa: D102
|
|
40
53
|
|
|
41
54
|
def setup(self, model: Model) -> None:
|
|
42
|
-
"""Set up the simulator with the model to simulate
|
|
55
|
+
"""Set up the simulator with the model to simulate.
|
|
43
56
|
|
|
44
57
|
Args:
|
|
45
58
|
model (Model): The model to simulate
|
|
@@ -49,12 +62,13 @@ class Simulator:
|
|
|
49
62
|
self.model = model
|
|
50
63
|
|
|
51
64
|
def reset(self):
|
|
52
|
-
"""Reset the simulator by clearing the event list and removing the model to simulate"""
|
|
65
|
+
"""Reset the simulator by clearing the event list and removing the model to simulate."""
|
|
53
66
|
self.event_list.clear()
|
|
54
67
|
self.model = None
|
|
55
68
|
self.time = self.start_time
|
|
56
69
|
|
|
57
70
|
def run_until(self, end_time: int | float) -> None:
|
|
71
|
+
"""Run the simulator until the end time."""
|
|
58
72
|
while True:
|
|
59
73
|
try:
|
|
60
74
|
event = self.event_list.pop_event()
|
|
@@ -71,7 +85,7 @@ class Simulator:
|
|
|
71
85
|
break
|
|
72
86
|
|
|
73
87
|
def run_for(self, time_delta: int | float):
|
|
74
|
-
"""
|
|
88
|
+
"""Run the simulator for the specified time delta.
|
|
75
89
|
|
|
76
90
|
Args:
|
|
77
91
|
time_delta (float| int): The time delta. The simulator is run from the current time to the current time
|
|
@@ -88,7 +102,7 @@ class Simulator:
|
|
|
88
102
|
function_args: list[Any] | None = None,
|
|
89
103
|
function_kwargs: dict[str, Any] | None = None,
|
|
90
104
|
) -> SimulationEvent:
|
|
91
|
-
"""Schedule event for the current time instant
|
|
105
|
+
"""Schedule event for the current time instant.
|
|
92
106
|
|
|
93
107
|
Args:
|
|
94
108
|
function (Callable): The callable to execute for this event
|
|
@@ -116,7 +130,7 @@ class Simulator:
|
|
|
116
130
|
function_args: list[Any] | None = None,
|
|
117
131
|
function_kwargs: dict[str, Any] | None = None,
|
|
118
132
|
) -> SimulationEvent:
|
|
119
|
-
"""Schedule event for the specified time instant
|
|
133
|
+
"""Schedule event for the specified time instant.
|
|
120
134
|
|
|
121
135
|
Args:
|
|
122
136
|
function (Callable): The callable to execute for this event
|
|
@@ -150,7 +164,7 @@ class Simulator:
|
|
|
150
164
|
function_args: list[Any] | None = None,
|
|
151
165
|
function_kwargs: dict[str, Any] | None = None,
|
|
152
166
|
) -> SimulationEvent:
|
|
153
|
-
"""Schedule event for the current time plus the time delta
|
|
167
|
+
"""Schedule event for the current time plus the time delta.
|
|
154
168
|
|
|
155
169
|
Args:
|
|
156
170
|
function (Callable): The callable to execute for this event
|
|
@@ -174,13 +188,12 @@ class Simulator:
|
|
|
174
188
|
return event
|
|
175
189
|
|
|
176
190
|
def cancel_event(self, event: SimulationEvent) -> None:
|
|
177
|
-
"""
|
|
191
|
+
"""Remove the event from the event list.
|
|
178
192
|
|
|
179
193
|
Args:
|
|
180
194
|
event (SimulationEvent): The simulation event to remove
|
|
181
195
|
|
|
182
196
|
"""
|
|
183
|
-
|
|
184
197
|
self.event_list.remove(event)
|
|
185
198
|
|
|
186
199
|
def _schedule_event(self, event: SimulationEvent):
|
|
@@ -204,13 +217,29 @@ class ABMSimulator(Simulator):
|
|
|
204
217
|
"""
|
|
205
218
|
|
|
206
219
|
def __init__(self):
|
|
220
|
+
"""Initialize a ABM simulator."""
|
|
207
221
|
super().__init__(int, 0)
|
|
208
222
|
|
|
209
223
|
def setup(self, model):
|
|
224
|
+
"""Set up the simulator with the model to simulate.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
model (Model): The model to simulate
|
|
228
|
+
|
|
229
|
+
"""
|
|
210
230
|
super().setup(model)
|
|
211
231
|
self.schedule_event_now(self.model.step, priority=Priority.HIGH)
|
|
212
232
|
|
|
213
233
|
def check_time_unit(self, time) -> bool:
|
|
234
|
+
"""Check whether the time is of the correct unit.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
time (int | float): the time
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
bool: whether the time is of the correct unit
|
|
241
|
+
|
|
242
|
+
"""
|
|
214
243
|
if isinstance(time, int):
|
|
215
244
|
return True
|
|
216
245
|
if isinstance(time, float):
|
|
@@ -225,9 +254,9 @@ class ABMSimulator(Simulator):
|
|
|
225
254
|
function_args: list[Any] | None = None,
|
|
226
255
|
function_kwargs: dict[str, Any] | None = None,
|
|
227
256
|
) -> SimulationEvent:
|
|
228
|
-
"""Schedule a SimulationEvent for the next tick
|
|
257
|
+
"""Schedule a SimulationEvent for the next tick.
|
|
229
258
|
|
|
230
|
-
Args
|
|
259
|
+
Args:
|
|
231
260
|
function (Callable): the callable to execute
|
|
232
261
|
priority (Priority): the priority of the event
|
|
233
262
|
function_args (List[Any]): List of arguments to pass to the callable
|
|
@@ -243,7 +272,7 @@ class ABMSimulator(Simulator):
|
|
|
243
272
|
)
|
|
244
273
|
|
|
245
274
|
def run_until(self, end_time: int) -> None:
|
|
246
|
-
"""
|
|
275
|
+
"""Run the simulator up to and included the specified end time.
|
|
247
276
|
|
|
248
277
|
Args:
|
|
249
278
|
end_time (float| int): The end_time delta. The simulator is until the specified end time
|
|
@@ -270,7 +299,7 @@ class ABMSimulator(Simulator):
|
|
|
270
299
|
break
|
|
271
300
|
|
|
272
301
|
def run_for(self, time_delta: int):
|
|
273
|
-
"""
|
|
302
|
+
"""Run the simulator for the specified time delta.
|
|
274
303
|
|
|
275
304
|
Args:
|
|
276
305
|
time_delta (float| int): The time delta. The simulator is run from the current time to the current time
|
|
@@ -282,13 +311,24 @@ class ABMSimulator(Simulator):
|
|
|
282
311
|
|
|
283
312
|
|
|
284
313
|
class DEVSimulator(Simulator):
|
|
285
|
-
"""A simulator where the unit of time is a float.
|
|
286
|
-
|
|
314
|
+
"""A simulator where the unit of time is a float.
|
|
315
|
+
|
|
316
|
+
Can be used for full-blown discrete event simulating using event scheduling.
|
|
287
317
|
|
|
288
318
|
"""
|
|
289
319
|
|
|
290
320
|
def __init__(self):
|
|
321
|
+
"""Initialize a DEVS simulator."""
|
|
291
322
|
super().__init__(float, 0.0)
|
|
292
323
|
|
|
293
324
|
def check_time_unit(self, time) -> bool:
|
|
325
|
+
"""Check whether the time is of the correct unit.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
time (float): the time
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
bool: whether the time is of the correct unit
|
|
332
|
+
|
|
333
|
+
"""
|
|
294
334
|
return isinstance(time, numbers.Number)
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""Mesa visualization module for creating interactive model visualizations.
|
|
2
|
+
|
|
3
|
+
This module provides components to create browser- and Jupyter notebook-based visualizations of
|
|
4
|
+
Mesa models, allowing users to watch models run step-by-step and interact with model parameters.
|
|
5
|
+
|
|
6
|
+
Key features:
|
|
7
|
+
- SolaraViz: Main component for creating visualizations, supporting grid displays and plots
|
|
8
|
+
- ModelController: Handles model execution controls (step, play, pause, reset)
|
|
9
|
+
- UserInputs: Generates UI elements for adjusting model parameters
|
|
10
|
+
- Card: Renders individual visualization elements (space, measures)
|
|
11
|
+
|
|
12
|
+
The module uses Solara for rendering in Jupyter notebooks or as standalone web applications.
|
|
13
|
+
It supports various types of visualizations including matplotlib plots, agent grids, and
|
|
14
|
+
custom visualization components.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
1. Define an agent_portrayal function to specify how agents should be displayed
|
|
18
|
+
2. Set up model_params to define adjustable parameters
|
|
19
|
+
3. Create a SolaraViz instance with your model, parameters, and desired measures
|
|
20
|
+
4. Display the visualization in a Jupyter notebook or run as a Solara app
|
|
21
|
+
|
|
22
|
+
See the Visualization Tutorial and example models for more details.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import threading
|
|
26
|
+
|
|
27
|
+
import reacton.ipywidgets as widgets
|
|
28
|
+
import solara
|
|
29
|
+
from solara.alias import rv
|
|
30
|
+
|
|
31
|
+
import mesa.experimental.components.altair as components_altair
|
|
32
|
+
import mesa.experimental.components.matplotlib as components_matplotlib
|
|
33
|
+
from mesa.experimental.UserParam import Slider
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# TODO: Turn this function into a Solara component once the current_step.value
|
|
37
|
+
# dependency is passed to measure()
|
|
38
|
+
def Card(
|
|
39
|
+
model, measures, agent_portrayal, space_drawer, dependencies, color, layout_type
|
|
40
|
+
):
|
|
41
|
+
"""Create a card component for visualizing model space or measures.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
model: The Mesa model instance
|
|
45
|
+
measures: List of measures to be plotted
|
|
46
|
+
agent_portrayal: Function to define agent appearance
|
|
47
|
+
space_drawer: Method to render agent space
|
|
48
|
+
dependencies: List of dependencies for updating the visualization
|
|
49
|
+
color: Background color of the card
|
|
50
|
+
layout_type: Type of layout (Space or Measure)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
rv.Card: A card component containing the visualization
|
|
54
|
+
"""
|
|
55
|
+
with rv.Card(
|
|
56
|
+
style_=f"background-color: {color}; width: 100%; height: 100%"
|
|
57
|
+
) as main:
|
|
58
|
+
if "Space" in layout_type:
|
|
59
|
+
rv.CardTitle(children=["Space"])
|
|
60
|
+
if space_drawer == "default":
|
|
61
|
+
# draw with the default implementation
|
|
62
|
+
components_matplotlib.SpaceMatplotlib(
|
|
63
|
+
model, agent_portrayal, dependencies=dependencies
|
|
64
|
+
)
|
|
65
|
+
elif space_drawer == "altair":
|
|
66
|
+
components_altair.SpaceAltair(
|
|
67
|
+
model, agent_portrayal, dependencies=dependencies
|
|
68
|
+
)
|
|
69
|
+
elif space_drawer:
|
|
70
|
+
# if specified, draw agent space with an alternate renderer
|
|
71
|
+
space_drawer(model, agent_portrayal, dependencies=dependencies)
|
|
72
|
+
elif "Measure" in layout_type:
|
|
73
|
+
rv.CardTitle(children=["Measure"])
|
|
74
|
+
measure = measures[layout_type["Measure"]]
|
|
75
|
+
if callable(measure):
|
|
76
|
+
# Is a custom object
|
|
77
|
+
measure(model)
|
|
78
|
+
else:
|
|
79
|
+
components_matplotlib.PlotMatplotlib(
|
|
80
|
+
model, measure, dependencies=dependencies
|
|
81
|
+
)
|
|
82
|
+
return main
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@solara.component
|
|
86
|
+
def SolaraViz(
|
|
87
|
+
model_class,
|
|
88
|
+
model_params,
|
|
89
|
+
measures=None,
|
|
90
|
+
name=None,
|
|
91
|
+
agent_portrayal=None,
|
|
92
|
+
space_drawer="default",
|
|
93
|
+
play_interval=150,
|
|
94
|
+
seed=None,
|
|
95
|
+
):
|
|
96
|
+
"""Initialize a component to visualize a model.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
model_class: Class of the model to instantiate
|
|
100
|
+
model_params: Parameters for initializing the model
|
|
101
|
+
measures: List of callables or data attributes to plot
|
|
102
|
+
name: Name for display
|
|
103
|
+
agent_portrayal: Options for rendering agents (dictionary);
|
|
104
|
+
Default drawer supports custom `"size"`, `"color"`, and `"shape"`.
|
|
105
|
+
space_drawer: Method to render the agent space for
|
|
106
|
+
the model; default implementation is the `SpaceMatplotlib` component;
|
|
107
|
+
simulations with no space to visualize should
|
|
108
|
+
specify `space_drawer=False`
|
|
109
|
+
play_interval: Play interval (default: 150)
|
|
110
|
+
seed: The random seed used to initialize the model
|
|
111
|
+
"""
|
|
112
|
+
if name is None:
|
|
113
|
+
name = model_class.__name__
|
|
114
|
+
|
|
115
|
+
current_step = solara.use_reactive(0)
|
|
116
|
+
|
|
117
|
+
# 1. Set up model parameters
|
|
118
|
+
reactive_seed = solara.use_reactive(0)
|
|
119
|
+
user_params, fixed_params = split_model_params(model_params)
|
|
120
|
+
model_parameters, set_model_parameters = solara.use_state(
|
|
121
|
+
{**fixed_params, **{k: v.get("value") for k, v in user_params.items()}}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# 2. Set up Model
|
|
125
|
+
def make_model():
|
|
126
|
+
"""Create a new model instance with current parameters and seed."""
|
|
127
|
+
model = model_class.__new__(
|
|
128
|
+
model_class, **model_parameters, seed=reactive_seed.value
|
|
129
|
+
)
|
|
130
|
+
model.__init__(**model_parameters)
|
|
131
|
+
current_step.value = 0
|
|
132
|
+
return model
|
|
133
|
+
|
|
134
|
+
reset_counter = solara.use_reactive(0)
|
|
135
|
+
model = solara.use_memo(
|
|
136
|
+
make_model,
|
|
137
|
+
dependencies=[
|
|
138
|
+
*list(model_parameters.values()),
|
|
139
|
+
reset_counter.value,
|
|
140
|
+
reactive_seed.value,
|
|
141
|
+
],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def handle_change_model_params(name: str, value: any):
|
|
145
|
+
"""Update model parameters when user input changes."""
|
|
146
|
+
set_model_parameters({**model_parameters, name: value})
|
|
147
|
+
|
|
148
|
+
# 3. Set up UI
|
|
149
|
+
|
|
150
|
+
with solara.AppBar():
|
|
151
|
+
solara.AppBarTitle(name)
|
|
152
|
+
|
|
153
|
+
# render layout and plot
|
|
154
|
+
def do_reseed():
|
|
155
|
+
"""Update the random seed for the model."""
|
|
156
|
+
reactive_seed.value = model.random.random()
|
|
157
|
+
|
|
158
|
+
dependencies = [
|
|
159
|
+
*list(model_parameters.values()),
|
|
160
|
+
current_step.value,
|
|
161
|
+
reactive_seed.value,
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
# if space drawer is disabled, do not include it
|
|
165
|
+
layout_types = [{"Space": "default"}] if space_drawer else []
|
|
166
|
+
|
|
167
|
+
if measures:
|
|
168
|
+
layout_types += [{"Measure": elem} for elem in range(len(measures))]
|
|
169
|
+
|
|
170
|
+
grid_layout_initial = make_initial_grid_layout(layout_types=layout_types)
|
|
171
|
+
grid_layout, set_grid_layout = solara.use_state(grid_layout_initial)
|
|
172
|
+
|
|
173
|
+
with solara.Sidebar():
|
|
174
|
+
with solara.Card("Controls", margin=1, elevation=2):
|
|
175
|
+
solara.InputText(
|
|
176
|
+
label="Seed",
|
|
177
|
+
value=reactive_seed,
|
|
178
|
+
continuous_update=True,
|
|
179
|
+
)
|
|
180
|
+
UserInputs(user_params, on_change=handle_change_model_params)
|
|
181
|
+
ModelController(model, play_interval, current_step, reset_counter)
|
|
182
|
+
solara.Button(label="Reseed", color="primary", on_click=do_reseed)
|
|
183
|
+
with solara.Card("Information", margin=1, elevation=2):
|
|
184
|
+
solara.Markdown(md_text=f"Step - {current_step}")
|
|
185
|
+
|
|
186
|
+
items = [
|
|
187
|
+
Card(
|
|
188
|
+
model,
|
|
189
|
+
measures,
|
|
190
|
+
agent_portrayal,
|
|
191
|
+
space_drawer,
|
|
192
|
+
dependencies,
|
|
193
|
+
color="white",
|
|
194
|
+
layout_type=layout_types[i],
|
|
195
|
+
)
|
|
196
|
+
for i in range(len(layout_types))
|
|
197
|
+
]
|
|
198
|
+
solara.GridDraggable(
|
|
199
|
+
items=items,
|
|
200
|
+
grid_layout=grid_layout,
|
|
201
|
+
resizable=True,
|
|
202
|
+
draggable=True,
|
|
203
|
+
on_grid_layout=set_grid_layout,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
JupyterViz = SolaraViz
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@solara.component
|
|
211
|
+
def ModelController(model, play_interval, current_step, reset_counter):
|
|
212
|
+
"""Create controls for model execution (step, play, pause, reset).
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
model: The model being visualized
|
|
216
|
+
play_interval: Interval between steps during play
|
|
217
|
+
current_step: Reactive value for the current step
|
|
218
|
+
reset_counter: Counter to trigger model reset
|
|
219
|
+
"""
|
|
220
|
+
playing = solara.use_reactive(False)
|
|
221
|
+
thread = solara.use_reactive(None)
|
|
222
|
+
# We track the previous step to detect if user resets the model via
|
|
223
|
+
# clicking the reset button or changing the parameters. If previous_step >
|
|
224
|
+
# current_step, it means a model reset happens while the simulation is
|
|
225
|
+
# still playing.
|
|
226
|
+
previous_step = solara.use_reactive(0)
|
|
227
|
+
|
|
228
|
+
def on_value_play(change):
|
|
229
|
+
"""Handle play/pause state changes."""
|
|
230
|
+
if previous_step.value > current_step.value and current_step.value == 0:
|
|
231
|
+
# We add extra checks for current_step.value == 0, just to be sure.
|
|
232
|
+
# We automatically stop the playing if a model is reset.
|
|
233
|
+
playing.value = False
|
|
234
|
+
elif model.running:
|
|
235
|
+
do_step()
|
|
236
|
+
else:
|
|
237
|
+
playing.value = False
|
|
238
|
+
|
|
239
|
+
def do_step():
|
|
240
|
+
"""Advance the model by one step."""
|
|
241
|
+
model.step()
|
|
242
|
+
previous_step.value = current_step.value
|
|
243
|
+
current_step.value = model.steps
|
|
244
|
+
|
|
245
|
+
def do_play():
|
|
246
|
+
"""Run the model continuously."""
|
|
247
|
+
model.running = True
|
|
248
|
+
while model.running:
|
|
249
|
+
do_step()
|
|
250
|
+
|
|
251
|
+
def threaded_do_play():
|
|
252
|
+
"""Start a new thread for continuous model execution."""
|
|
253
|
+
if thread is not None and thread.is_alive():
|
|
254
|
+
return
|
|
255
|
+
thread.value = threading.Thread(target=do_play)
|
|
256
|
+
thread.start()
|
|
257
|
+
|
|
258
|
+
def do_pause():
|
|
259
|
+
"""Pause the model execution."""
|
|
260
|
+
if (thread is None) or (not thread.is_alive()):
|
|
261
|
+
return
|
|
262
|
+
model.running = False
|
|
263
|
+
thread.join()
|
|
264
|
+
|
|
265
|
+
def do_reset():
|
|
266
|
+
"""Reset the model."""
|
|
267
|
+
reset_counter.value += 1
|
|
268
|
+
|
|
269
|
+
def do_set_playing(value):
|
|
270
|
+
"""Set the playing state."""
|
|
271
|
+
if current_step.value == 0:
|
|
272
|
+
# This means the model has been recreated, and the step resets to
|
|
273
|
+
# 0. We want to avoid triggering the playing.value = False in the
|
|
274
|
+
# on_value_play function.
|
|
275
|
+
previous_step.value = current_step.value
|
|
276
|
+
playing.set(value)
|
|
277
|
+
|
|
278
|
+
with solara.Row():
|
|
279
|
+
solara.Button(label="Step", color="primary", on_click=do_step)
|
|
280
|
+
# This style is necessary so that the play widget has almost the same
|
|
281
|
+
# height as typical Solara buttons.
|
|
282
|
+
solara.Style(
|
|
283
|
+
"""
|
|
284
|
+
.widget-play {
|
|
285
|
+
height: 35px;
|
|
286
|
+
}
|
|
287
|
+
.widget-play button {
|
|
288
|
+
color: white;
|
|
289
|
+
background-color: #1976D2; // Solara blue color
|
|
290
|
+
}
|
|
291
|
+
"""
|
|
292
|
+
)
|
|
293
|
+
widgets.Play(
|
|
294
|
+
value=0,
|
|
295
|
+
interval=play_interval,
|
|
296
|
+
repeat=True,
|
|
297
|
+
show_repeat=False,
|
|
298
|
+
on_value=on_value_play,
|
|
299
|
+
playing=playing.value,
|
|
300
|
+
on_playing=do_set_playing,
|
|
301
|
+
)
|
|
302
|
+
solara.Button(label="Reset", color="primary", on_click=do_reset)
|
|
303
|
+
# threaded_do_play is not used for now because it
|
|
304
|
+
# doesn't work in Google colab. We use
|
|
305
|
+
# ipywidgets.Play until it is fixed. The threading
|
|
306
|
+
# version is definite a much better implementation,
|
|
307
|
+
# if it works.
|
|
308
|
+
# solara.Button(label="▶", color="primary", on_click=viz.threaded_do_play)
|
|
309
|
+
# solara.Button(label="⏸︎", color="primary", on_click=viz.do_pause)
|
|
310
|
+
# solara.Button(label="Reset", color="primary", on_click=do_reset)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def split_model_params(model_params):
|
|
314
|
+
"""Split model parameters into user-adjustable and fixed parameters.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
model_params: Dictionary of all model parameters
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
tuple: (user_adjustable_params, fixed_params)
|
|
321
|
+
"""
|
|
322
|
+
model_params_input = {}
|
|
323
|
+
model_params_fixed = {}
|
|
324
|
+
for k, v in model_params.items():
|
|
325
|
+
if check_param_is_fixed(v):
|
|
326
|
+
model_params_fixed[k] = v
|
|
327
|
+
else:
|
|
328
|
+
model_params_input[k] = v
|
|
329
|
+
return model_params_input, model_params_fixed
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def check_param_is_fixed(param):
|
|
333
|
+
"""Check if a parameter is fixed (not user-adjustable).
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
param: Parameter to check
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
bool: True if parameter is fixed, False otherwise
|
|
340
|
+
"""
|
|
341
|
+
if isinstance(param, Slider):
|
|
342
|
+
return False
|
|
343
|
+
if not isinstance(param, dict):
|
|
344
|
+
return True
|
|
345
|
+
if "type" not in param:
|
|
346
|
+
return True
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@solara.component
|
|
350
|
+
def UserInputs(user_params, on_change=None):
|
|
351
|
+
"""Initialize user inputs for configurable model parameters.
|
|
352
|
+
|
|
353
|
+
Currently supports :class:`solara.SliderInt`, :class:`solara.SliderFloat`,
|
|
354
|
+
:class:`solara.Select`, and :class:`solara.Checkbox`.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
user_params: Dictionary with options for the input, including label,
|
|
358
|
+
min and max values, and other fields specific to the input type.
|
|
359
|
+
on_change: Function to be called with (name, value) when the value of an input changes.
|
|
360
|
+
"""
|
|
361
|
+
for name, options in user_params.items():
|
|
362
|
+
|
|
363
|
+
def change_handler(value, name=name):
|
|
364
|
+
on_change(name, value)
|
|
365
|
+
|
|
366
|
+
if isinstance(options, Slider):
|
|
367
|
+
slider_class = (
|
|
368
|
+
solara.SliderFloat if options.is_float_slider else solara.SliderInt
|
|
369
|
+
)
|
|
370
|
+
slider_class(
|
|
371
|
+
options.label,
|
|
372
|
+
value=options.value,
|
|
373
|
+
on_value=change_handler,
|
|
374
|
+
min=options.min,
|
|
375
|
+
max=options.max,
|
|
376
|
+
step=options.step,
|
|
377
|
+
)
|
|
378
|
+
continue
|
|
379
|
+
|
|
380
|
+
# label for the input is "label" from options or name
|
|
381
|
+
label = options.get("label", name)
|
|
382
|
+
input_type = options.get("type")
|
|
383
|
+
if input_type == "SliderInt":
|
|
384
|
+
solara.SliderInt(
|
|
385
|
+
label,
|
|
386
|
+
value=options.get("value"),
|
|
387
|
+
on_value=change_handler,
|
|
388
|
+
min=options.get("min"),
|
|
389
|
+
max=options.get("max"),
|
|
390
|
+
step=options.get("step"),
|
|
391
|
+
)
|
|
392
|
+
elif input_type == "SliderFloat":
|
|
393
|
+
solara.SliderFloat(
|
|
394
|
+
label,
|
|
395
|
+
value=options.get("value"),
|
|
396
|
+
on_value=change_handler,
|
|
397
|
+
min=options.get("min"),
|
|
398
|
+
max=options.get("max"),
|
|
399
|
+
step=options.get("step"),
|
|
400
|
+
)
|
|
401
|
+
elif input_type == "Select":
|
|
402
|
+
solara.Select(
|
|
403
|
+
label,
|
|
404
|
+
value=options.get("value"),
|
|
405
|
+
on_value=change_handler,
|
|
406
|
+
values=options.get("values"),
|
|
407
|
+
)
|
|
408
|
+
elif input_type == "Checkbox":
|
|
409
|
+
solara.Checkbox(
|
|
410
|
+
label=label,
|
|
411
|
+
on_value=change_handler,
|
|
412
|
+
value=options.get("value"),
|
|
413
|
+
)
|
|
414
|
+
else:
|
|
415
|
+
raise ValueError(f"{input_type} is not a supported input type")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def make_text(renderer):
|
|
419
|
+
"""Create a function that renders text using Markdown.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
renderer: Function that takes a model and returns a string
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
function: A function that renders the text as Markdown
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
def function(model):
|
|
429
|
+
solara.Markdown(renderer(model))
|
|
430
|
+
|
|
431
|
+
return function
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def make_initial_grid_layout(layout_types):
|
|
435
|
+
"""Create an initial grid layout for visualization components.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
layout_types: List of layout types (Space or Measure)
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
list: Initial grid layout configuration
|
|
442
|
+
"""
|
|
443
|
+
return [
|
|
444
|
+
{
|
|
445
|
+
"i": i,
|
|
446
|
+
"w": 6,
|
|
447
|
+
"h": 10,
|
|
448
|
+
"moved": False,
|
|
449
|
+
"x": 6 * (i % 2),
|
|
450
|
+
"y": 16 * (i - i % 2),
|
|
451
|
+
}
|
|
452
|
+
for i in range(len(layout_types))
|
|
453
|
+
]
|
mesa/main.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""main module for running mesa models with a server."""
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
import sys
|
|
3
5
|
from pathlib import Path
|
|
@@ -19,13 +21,13 @@ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
|
19
21
|
|
|
20
22
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
21
23
|
def cli():
|
|
22
|
-
"Manage Mesa projects"
|
|
24
|
+
"""Manage Mesa projects."""
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
@cli.command()
|
|
26
28
|
@click.argument("project", type=PROJECT_PATH, default=".")
|
|
27
29
|
def runserver(project):
|
|
28
|
-
"""Run mesa project PROJECT
|
|
30
|
+
"""Run mesa project PROJECT.
|
|
29
31
|
|
|
30
32
|
PROJECT is the path to the directory containing `run.py`, or the current
|
|
31
33
|
directory if not specified.
|
|
@@ -45,7 +47,7 @@ def runserver(project):
|
|
|
45
47
|
"--no-input", is_flag=True, help="Do not prompt user for custom mesa model input."
|
|
46
48
|
)
|
|
47
49
|
def startproject(no_input):
|
|
48
|
-
"""Create a new mesa project"""
|
|
50
|
+
"""Create a new mesa project."""
|
|
49
51
|
args = ["cookiecutter", COOKIECUTTER_PATH]
|
|
50
52
|
if no_input:
|
|
51
53
|
args.append("--no-input")
|
|
@@ -54,7 +56,7 @@ def startproject(no_input):
|
|
|
54
56
|
|
|
55
57
|
@click.command()
|
|
56
58
|
def version():
|
|
57
|
-
"""Show the version of mesa"""
|
|
59
|
+
"""Show the version of mesa."""
|
|
58
60
|
print(f"mesa {__version__}")
|
|
59
61
|
|
|
60
62
|
|