Mesa 2.2.3__py3-none-any.whl → 2.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 +2 -1
- mesa/agent.py +36 -31
- mesa/datacollection.py +12 -8
- mesa/experimental/UserParam.py +56 -0
- mesa/experimental/__init__.py +5 -1
- mesa/experimental/cell_space/__init__.py +23 -0
- mesa/experimental/cell_space/cell.py +152 -0
- mesa/experimental/cell_space/cell_agent.py +37 -0
- mesa/experimental/cell_space/cell_collection.py +81 -0
- mesa/experimental/cell_space/discrete_space.py +64 -0
- mesa/experimental/cell_space/grid.py +204 -0
- mesa/experimental/cell_space/network.py +40 -0
- mesa/experimental/components/altair.py +72 -0
- mesa/experimental/components/matplotlib.py +6 -2
- mesa/experimental/devs/__init__.py +4 -0
- mesa/experimental/devs/eventlist.py +166 -0
- mesa/experimental/devs/examples/epstein_civil_violence.py +273 -0
- mesa/experimental/devs/examples/wolf_sheep.py +250 -0
- mesa/experimental/devs/simulator.py +293 -0
- mesa/experimental/jupyter_viz.py +122 -60
- mesa/main.py +8 -5
- mesa/model.py +15 -4
- mesa/space.py +32 -16
- mesa/time.py +13 -113
- {mesa-2.2.3.dist-info → mesa-2.3.0.dist-info}/METADATA +12 -9
- mesa-2.3.0.dist-info/RECORD +45 -0
- {mesa-2.2.3.dist-info → mesa-2.3.0.dist-info}/WHEEL +1 -1
- mesa-2.2.3.dist-info/RECORD +0 -31
- {mesa-2.2.3.dist-info → mesa-2.3.0.dist-info}/entry_points.txt +0 -0
- {mesa-2.2.3.dist-info → mesa-2.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numbers
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from mesa import Model
|
|
7
|
+
|
|
8
|
+
from .eventlist import EventList, Priority, SimulationEvent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Simulator:
|
|
12
|
+
"""The Simulator controls the time advancement of the model.
|
|
13
|
+
|
|
14
|
+
The simulator uses next event time progression to advance the simulation time, and execute the next event
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
event_list (EventList): The list of events to execute
|
|
18
|
+
time (float | int): The current simulation time
|
|
19
|
+
time_unit (type) : The unit of the simulation time
|
|
20
|
+
model (Model): The model to simulate
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# TODO: add replication support
|
|
26
|
+
# TODO: add experimentation support
|
|
27
|
+
|
|
28
|
+
def __init__(self, time_unit: type, start_time: int | float):
|
|
29
|
+
# should model run in a separate thread,
|
|
30
|
+
# and we can then interact with start, stop, run_until, and step?
|
|
31
|
+
self.event_list = EventList()
|
|
32
|
+
self.start_time = start_time
|
|
33
|
+
self.time_unit = time_unit
|
|
34
|
+
|
|
35
|
+
self.time = self.start_time
|
|
36
|
+
self.model = None
|
|
37
|
+
|
|
38
|
+
def check_time_unit(self, time: int | float) -> bool: ...
|
|
39
|
+
|
|
40
|
+
def setup(self, model: Model) -> None:
|
|
41
|
+
"""Set up the simulator with the model to simulate
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
model (Model): The model to simulate
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
self.event_list.clear()
|
|
48
|
+
self.model = model
|
|
49
|
+
|
|
50
|
+
def reset(self):
|
|
51
|
+
"""Reset the simulator by clearing the event list and removing the model to simulate"""
|
|
52
|
+
self.event_list.clear()
|
|
53
|
+
self.model = None
|
|
54
|
+
self.time = self.start_time
|
|
55
|
+
|
|
56
|
+
def run_until(self, end_time: int | float) -> None:
|
|
57
|
+
while True:
|
|
58
|
+
try:
|
|
59
|
+
event = self.event_list.pop_event()
|
|
60
|
+
except IndexError: # event list is empty
|
|
61
|
+
self.time = end_time
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
if event.time <= end_time:
|
|
65
|
+
self.time = event.time
|
|
66
|
+
event.execute()
|
|
67
|
+
else:
|
|
68
|
+
self.time = end_time
|
|
69
|
+
self._schedule_event(event) # reschedule event
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
def run_for(self, time_delta: int | float):
|
|
73
|
+
"""run the simulator for the specified time delta
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
time_delta (float| int): The time delta. The simulator is run from the current time to the current time
|
|
77
|
+
plus the time delta
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
end_time = self.time + time_delta
|
|
81
|
+
self.run_until(end_time)
|
|
82
|
+
|
|
83
|
+
def schedule_event_now(
|
|
84
|
+
self,
|
|
85
|
+
function: Callable,
|
|
86
|
+
priority: Priority = Priority.DEFAULT,
|
|
87
|
+
function_args: list[Any] | None = None,
|
|
88
|
+
function_kwargs: dict[str, Any] | None = None,
|
|
89
|
+
) -> SimulationEvent:
|
|
90
|
+
"""Schedule event for the current time instant
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
function (Callable): The callable to execute for this event
|
|
94
|
+
priority (Priority): the priority of the event, optional
|
|
95
|
+
function_args (List[Any]): list of arguments for function
|
|
96
|
+
function_kwargs (Dict[str, Any]): dict of keyword arguments for function
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
SimulationEvent: the simulation event that is scheduled
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
return self.schedule_event_relative(
|
|
103
|
+
function,
|
|
104
|
+
0.0,
|
|
105
|
+
priority=priority,
|
|
106
|
+
function_args=function_args,
|
|
107
|
+
function_kwargs=function_kwargs,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def schedule_event_absolute(
|
|
111
|
+
self,
|
|
112
|
+
function: Callable,
|
|
113
|
+
time: int | float,
|
|
114
|
+
priority: Priority = Priority.DEFAULT,
|
|
115
|
+
function_args: list[Any] | None = None,
|
|
116
|
+
function_kwargs: dict[str, Any] | None = None,
|
|
117
|
+
) -> SimulationEvent:
|
|
118
|
+
"""Schedule event for the specified time instant
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
function (Callable): The callable to execute for this event
|
|
122
|
+
time (int | float): the time for which to schedule the event
|
|
123
|
+
priority (Priority): the priority of the event, optional
|
|
124
|
+
function_args (List[Any]): list of arguments for function
|
|
125
|
+
function_kwargs (Dict[str, Any]): dict of keyword arguments for function
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
SimulationEvent: the simulation event that is scheduled
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
if self.time > time:
|
|
132
|
+
raise ValueError("trying to schedule an event in the past")
|
|
133
|
+
|
|
134
|
+
event = SimulationEvent(
|
|
135
|
+
time,
|
|
136
|
+
function,
|
|
137
|
+
priority=priority,
|
|
138
|
+
function_args=function_args,
|
|
139
|
+
function_kwargs=function_kwargs,
|
|
140
|
+
)
|
|
141
|
+
self._schedule_event(event)
|
|
142
|
+
return event
|
|
143
|
+
|
|
144
|
+
def schedule_event_relative(
|
|
145
|
+
self,
|
|
146
|
+
function: Callable,
|
|
147
|
+
time_delta: int | float,
|
|
148
|
+
priority: Priority = Priority.DEFAULT,
|
|
149
|
+
function_args: list[Any] | None = None,
|
|
150
|
+
function_kwargs: dict[str, Any] | None = None,
|
|
151
|
+
) -> SimulationEvent:
|
|
152
|
+
"""Schedule event for the current time plus the time delta
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
function (Callable): The callable to execute for this event
|
|
156
|
+
time_delta (int | float): the time delta
|
|
157
|
+
priority (Priority): the priority of the event, optional
|
|
158
|
+
function_args (List[Any]): list of arguments for function
|
|
159
|
+
function_kwargs (Dict[str, Any]): dict of keyword arguments for function
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
SimulationEvent: the simulation event that is scheduled
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
event = SimulationEvent(
|
|
166
|
+
self.time + time_delta,
|
|
167
|
+
function,
|
|
168
|
+
priority=priority,
|
|
169
|
+
function_args=function_args,
|
|
170
|
+
function_kwargs=function_kwargs,
|
|
171
|
+
)
|
|
172
|
+
self._schedule_event(event)
|
|
173
|
+
return event
|
|
174
|
+
|
|
175
|
+
def cancel_event(self, event: SimulationEvent) -> None:
|
|
176
|
+
"""remove the event from the event list
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
event (SimulationEvent): The simulation event to remove
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
self.event_list.remove(event)
|
|
184
|
+
|
|
185
|
+
def _schedule_event(self, event: SimulationEvent):
|
|
186
|
+
if not self.check_time_unit(event.time):
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"time unit mismatch {event.time} is not of time unit {self.time_unit}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# check timeunit of events
|
|
192
|
+
self.event_list.add_event(event)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ABMSimulator(Simulator):
|
|
196
|
+
"""This simulator uses incremental time progression, while allowing for additional event scheduling.
|
|
197
|
+
|
|
198
|
+
The basic time unit of this simulator is an integer. It schedules `model.step` for each tick with the
|
|
199
|
+
highest priority. This implies that by default, `model.step` is the first event executed at a specific tick.
|
|
200
|
+
In addition, discrete event scheduling, using integer as the time unit is fully supported, paving the way
|
|
201
|
+
for hybrid ABM-DEVS simulations.
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def __init__(self):
|
|
206
|
+
super().__init__(int, 0)
|
|
207
|
+
|
|
208
|
+
def setup(self, model):
|
|
209
|
+
super().setup(model)
|
|
210
|
+
self.schedule_event_now(self.model.step, priority=Priority.HIGH)
|
|
211
|
+
|
|
212
|
+
def check_time_unit(self, time) -> bool:
|
|
213
|
+
if isinstance(time, int):
|
|
214
|
+
return True
|
|
215
|
+
if isinstance(time, float):
|
|
216
|
+
return time.is_integer()
|
|
217
|
+
else:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
def schedule_event_next_tick(
|
|
221
|
+
self,
|
|
222
|
+
function: Callable,
|
|
223
|
+
priority: Priority = Priority.DEFAULT,
|
|
224
|
+
function_args: list[Any] | None = None,
|
|
225
|
+
function_kwargs: dict[str, Any] | None = None,
|
|
226
|
+
) -> SimulationEvent:
|
|
227
|
+
"""Schedule a SimulationEvent for the next tick
|
|
228
|
+
|
|
229
|
+
Args
|
|
230
|
+
function (Callable): the callable to execute
|
|
231
|
+
priority (Priority): the priority of the event
|
|
232
|
+
function_args (List[Any]): List of arguments to pass to the callable
|
|
233
|
+
function_kwargs (Dict[str, Any]): List of keyword arguments to pass to the callable
|
|
234
|
+
|
|
235
|
+
"""
|
|
236
|
+
return self.schedule_event_relative(
|
|
237
|
+
function,
|
|
238
|
+
1,
|
|
239
|
+
priority=priority,
|
|
240
|
+
function_args=function_args,
|
|
241
|
+
function_kwargs=function_kwargs,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def run_until(self, end_time: int) -> None:
|
|
245
|
+
"""run the simulator up to and included the specified end time
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
end_time (float| int): The end_time delta. The simulator is until the specified end time
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
while True:
|
|
252
|
+
try:
|
|
253
|
+
event = self.event_list.pop_event()
|
|
254
|
+
except IndexError:
|
|
255
|
+
self.time = end_time
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
if event.time <= end_time:
|
|
259
|
+
self.time = event.time
|
|
260
|
+
if event.fn() == self.model.step:
|
|
261
|
+
self.schedule_event_next_tick(
|
|
262
|
+
self.model.step, priority=Priority.HIGH
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
event.execute()
|
|
266
|
+
else:
|
|
267
|
+
self.time = end_time
|
|
268
|
+
self._schedule_event(event)
|
|
269
|
+
break
|
|
270
|
+
|
|
271
|
+
def run_for(self, time_delta: int):
|
|
272
|
+
"""run the simulator for the specified time delta
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
time_delta (float| int): The time delta. The simulator is run from the current time to the current time
|
|
276
|
+
plus the time delta
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
end_time = self.time + time_delta - 1
|
|
280
|
+
self.run_until(end_time)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class DEVSimulator(Simulator):
|
|
284
|
+
"""A simulator where the unit of time is a float. Can be used for full-blown discrete event simulating using
|
|
285
|
+
event scheduling.
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
def __init__(self):
|
|
290
|
+
super().__init__(float, 0.0)
|
|
291
|
+
|
|
292
|
+
def check_time_unit(self, time) -> bool:
|
|
293
|
+
return isinstance(time, numbers.Number)
|
mesa/experimental/jupyter_viz.py
CHANGED
|
@@ -6,21 +6,59 @@ import reacton.ipywidgets as widgets
|
|
|
6
6
|
import solara
|
|
7
7
|
from solara.alias import rv
|
|
8
8
|
|
|
9
|
+
import mesa.experimental.components.altair as components_altair
|
|
9
10
|
import mesa.experimental.components.matplotlib as components_matplotlib
|
|
11
|
+
from mesa.experimental.UserParam import Slider
|
|
10
12
|
|
|
11
13
|
# Avoid interactive backend
|
|
12
14
|
plt.switch_backend("agg")
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
# TODO: Turn this function into a Solara component once the current_step.value
|
|
18
|
+
# dependency is passed to measure()
|
|
19
|
+
def Card(
|
|
20
|
+
model, measures, agent_portrayal, space_drawer, dependencies, color, layout_type
|
|
21
|
+
):
|
|
22
|
+
with rv.Card(
|
|
23
|
+
style_=f"background-color: {color}; width: 100%; height: 100%"
|
|
24
|
+
) as main:
|
|
25
|
+
if "Space" in layout_type:
|
|
26
|
+
rv.CardTitle(children=["Space"])
|
|
27
|
+
if space_drawer == "default":
|
|
28
|
+
# draw with the default implementation
|
|
29
|
+
components_matplotlib.SpaceMatplotlib(
|
|
30
|
+
model, agent_portrayal, dependencies=dependencies
|
|
31
|
+
)
|
|
32
|
+
elif space_drawer == "altair":
|
|
33
|
+
components_altair.SpaceAltair(
|
|
34
|
+
model, agent_portrayal, dependencies=dependencies
|
|
35
|
+
)
|
|
36
|
+
elif space_drawer:
|
|
37
|
+
# if specified, draw agent space with an alternate renderer
|
|
38
|
+
space_drawer(model, agent_portrayal)
|
|
39
|
+
elif "Measure" in layout_type:
|
|
40
|
+
rv.CardTitle(children=["Measure"])
|
|
41
|
+
measure = measures[layout_type["Measure"]]
|
|
42
|
+
if callable(measure):
|
|
43
|
+
# Is a custom object
|
|
44
|
+
measure(model)
|
|
45
|
+
else:
|
|
46
|
+
components_matplotlib.PlotMatplotlib(
|
|
47
|
+
model, measure, dependencies=dependencies
|
|
48
|
+
)
|
|
49
|
+
return main
|
|
50
|
+
|
|
51
|
+
|
|
15
52
|
@solara.component
|
|
16
53
|
def JupyterViz(
|
|
17
54
|
model_class,
|
|
18
55
|
model_params,
|
|
19
56
|
measures=None,
|
|
20
|
-
name=
|
|
57
|
+
name=None,
|
|
21
58
|
agent_portrayal=None,
|
|
22
59
|
space_drawer="default",
|
|
23
60
|
play_interval=150,
|
|
61
|
+
seed=None,
|
|
24
62
|
):
|
|
25
63
|
"""Initialize a component to visualize a model.
|
|
26
64
|
Args:
|
|
@@ -34,64 +72,54 @@ def JupyterViz(
|
|
|
34
72
|
simulations with no space to visualize should
|
|
35
73
|
specify `space_drawer=False`
|
|
36
74
|
play_interval: play interval (default: 150)
|
|
75
|
+
seed: the random seed used to initialize the model
|
|
37
76
|
"""
|
|
77
|
+
if name is None:
|
|
78
|
+
name = model_class.__name__
|
|
79
|
+
|
|
38
80
|
current_step = solara.use_reactive(0)
|
|
39
81
|
|
|
40
82
|
# 1. Set up model parameters
|
|
83
|
+
reactive_seed = solara.use_reactive(0)
|
|
41
84
|
user_params, fixed_params = split_model_params(model_params)
|
|
42
85
|
model_parameters, set_model_parameters = solara.use_state(
|
|
43
|
-
{**fixed_params, **{k: v
|
|
86
|
+
{**fixed_params, **{k: v.get("value") for k, v in user_params.items()}}
|
|
44
87
|
)
|
|
45
88
|
|
|
46
89
|
# 2. Set up Model
|
|
47
90
|
def make_model():
|
|
48
|
-
model = model_class(
|
|
91
|
+
model = model_class.__new__(
|
|
92
|
+
model_class, **model_parameters, seed=reactive_seed.value
|
|
93
|
+
)
|
|
94
|
+
model.__init__(**model_parameters)
|
|
49
95
|
current_step.value = 0
|
|
50
96
|
return model
|
|
51
97
|
|
|
52
98
|
reset_counter = solara.use_reactive(0)
|
|
53
99
|
model = solara.use_memo(
|
|
54
|
-
make_model,
|
|
100
|
+
make_model,
|
|
101
|
+
dependencies=[
|
|
102
|
+
*list(model_parameters.values()),
|
|
103
|
+
reset_counter.value,
|
|
104
|
+
reactive_seed.value,
|
|
105
|
+
],
|
|
55
106
|
)
|
|
56
107
|
|
|
57
108
|
def handle_change_model_params(name: str, value: any):
|
|
58
109
|
set_model_parameters({**model_parameters, name: value})
|
|
59
110
|
|
|
60
|
-
def ColorCard(color, layout_type):
|
|
61
|
-
# TODO: turn this into a Solara component, but must pass in current
|
|
62
|
-
# step as a dependency for the plots, so that there is no flickering
|
|
63
|
-
# due to rerender.
|
|
64
|
-
with rv.Card(
|
|
65
|
-
style_=f"background-color: {color}; width: 100%; height: 100%"
|
|
66
|
-
) as main:
|
|
67
|
-
if "Space" in layout_type:
|
|
68
|
-
rv.CardTitle(children=["Space"])
|
|
69
|
-
if space_drawer == "default":
|
|
70
|
-
# draw with the default implementation
|
|
71
|
-
components_matplotlib.SpaceMatplotlib(
|
|
72
|
-
model, agent_portrayal, dependencies=[current_step.value]
|
|
73
|
-
)
|
|
74
|
-
elif space_drawer:
|
|
75
|
-
# if specified, draw agent space with an alternate renderer
|
|
76
|
-
space_drawer(model, agent_portrayal)
|
|
77
|
-
elif "Measure" in layout_type:
|
|
78
|
-
rv.CardTitle(children=["Measure"])
|
|
79
|
-
measure = measures[layout_type["Measure"]]
|
|
80
|
-
if callable(measure):
|
|
81
|
-
# Is a custom object
|
|
82
|
-
measure(model)
|
|
83
|
-
else:
|
|
84
|
-
components_matplotlib.make_plot(model, measure)
|
|
85
|
-
return main
|
|
86
|
-
|
|
87
111
|
# 3. Set up UI
|
|
88
112
|
|
|
89
113
|
with solara.AppBar():
|
|
90
114
|
solara.AppBarTitle(name)
|
|
91
115
|
|
|
92
116
|
# render layout and plot
|
|
117
|
+
def do_reseed():
|
|
118
|
+
reactive_seed.value = model.random.random()
|
|
93
119
|
|
|
94
120
|
# jupyter
|
|
121
|
+
dependencies = [current_step.value, reactive_seed.value]
|
|
122
|
+
|
|
95
123
|
def render_in_jupyter():
|
|
96
124
|
with solara.GridFixed(columns=2):
|
|
97
125
|
UserInputs(user_params, on_change=handle_change_model_params)
|
|
@@ -103,7 +131,11 @@ def JupyterViz(
|
|
|
103
131
|
if space_drawer == "default":
|
|
104
132
|
# draw with the default implementation
|
|
105
133
|
components_matplotlib.SpaceMatplotlib(
|
|
106
|
-
model, agent_portrayal, dependencies=
|
|
134
|
+
model, agent_portrayal, dependencies=dependencies
|
|
135
|
+
)
|
|
136
|
+
elif space_drawer == "altair":
|
|
137
|
+
components_altair.SpaceAltair(
|
|
138
|
+
model, agent_portrayal, dependencies=dependencies
|
|
107
139
|
)
|
|
108
140
|
elif space_drawer:
|
|
109
141
|
# if specified, draw agent space with an alternate renderer
|
|
@@ -111,13 +143,14 @@ def JupyterViz(
|
|
|
111
143
|
# otherwise, do nothing (do not draw space)
|
|
112
144
|
|
|
113
145
|
# 5. Plots
|
|
114
|
-
|
|
115
146
|
for measure in measures:
|
|
116
147
|
if callable(measure):
|
|
117
148
|
# Is a custom object
|
|
118
149
|
measure(model)
|
|
119
150
|
else:
|
|
120
|
-
components_matplotlib.
|
|
151
|
+
components_matplotlib.PlotMatplotlib(
|
|
152
|
+
model, measure, dependencies=dependencies
|
|
153
|
+
)
|
|
121
154
|
|
|
122
155
|
def render_in_browser():
|
|
123
156
|
# if space drawer is disabled, do not include it
|
|
@@ -126,18 +159,32 @@ def JupyterViz(
|
|
|
126
159
|
if measures:
|
|
127
160
|
layout_types += [{"Measure": elem} for elem in range(len(measures))]
|
|
128
161
|
|
|
129
|
-
grid_layout_initial =
|
|
162
|
+
grid_layout_initial = make_initial_grid_layout(layout_types=layout_types)
|
|
130
163
|
grid_layout, set_grid_layout = solara.use_state(grid_layout_initial)
|
|
131
164
|
|
|
132
165
|
with solara.Sidebar():
|
|
133
166
|
with solara.Card("Controls", margin=1, elevation=2):
|
|
167
|
+
solara.InputText(
|
|
168
|
+
label="Seed",
|
|
169
|
+
value=reactive_seed,
|
|
170
|
+
continuous_update=True,
|
|
171
|
+
)
|
|
134
172
|
UserInputs(user_params, on_change=handle_change_model_params)
|
|
135
173
|
ModelController(model, play_interval, current_step, reset_counter)
|
|
136
|
-
|
|
137
|
-
|
|
174
|
+
solara.Button(label="Reseed", color="primary", on_click=do_reseed)
|
|
175
|
+
with solara.Card("Information", margin=1, elevation=2):
|
|
176
|
+
solara.Markdown(md_text=f"Step - {current_step}")
|
|
138
177
|
|
|
139
178
|
items = [
|
|
140
|
-
|
|
179
|
+
Card(
|
|
180
|
+
model,
|
|
181
|
+
measures,
|
|
182
|
+
agent_portrayal,
|
|
183
|
+
space_drawer,
|
|
184
|
+
dependencies,
|
|
185
|
+
color="white",
|
|
186
|
+
layout_type=layout_types[i],
|
|
187
|
+
)
|
|
141
188
|
for i in range(len(layout_types))
|
|
142
189
|
]
|
|
143
190
|
solara.GridDraggable(
|
|
@@ -178,7 +225,7 @@ def ModelController(model, play_interval, current_step, reset_counter):
|
|
|
178
225
|
def do_step():
|
|
179
226
|
model.step()
|
|
180
227
|
previous_step.value = current_step.value
|
|
181
|
-
current_step.value
|
|
228
|
+
current_step.value = model._steps
|
|
182
229
|
|
|
183
230
|
def do_play():
|
|
184
231
|
model.running = True
|
|
@@ -215,7 +262,11 @@ def ModelController(model, play_interval, current_step, reset_counter):
|
|
|
215
262
|
solara.Style(
|
|
216
263
|
"""
|
|
217
264
|
.widget-play {
|
|
218
|
-
height:
|
|
265
|
+
height: 35px;
|
|
266
|
+
}
|
|
267
|
+
.widget-play button {
|
|
268
|
+
color: white;
|
|
269
|
+
background-color: #1976D2; // Solara blue color
|
|
219
270
|
}
|
|
220
271
|
"""
|
|
221
272
|
)
|
|
@@ -251,6 +302,8 @@ def split_model_params(model_params):
|
|
|
251
302
|
|
|
252
303
|
|
|
253
304
|
def check_param_is_fixed(param):
|
|
305
|
+
if isinstance(param, Slider):
|
|
306
|
+
return False
|
|
254
307
|
if not isinstance(param, dict):
|
|
255
308
|
return True
|
|
256
309
|
if "type" not in param:
|
|
@@ -270,13 +323,27 @@ def UserInputs(user_params, on_change=None):
|
|
|
270
323
|
"""
|
|
271
324
|
|
|
272
325
|
for name, options in user_params.items():
|
|
273
|
-
# label for the input is "label" from options or name
|
|
274
|
-
label = options.get("label", name)
|
|
275
|
-
input_type = options.get("type")
|
|
276
326
|
|
|
277
327
|
def change_handler(value, name=name):
|
|
278
328
|
on_change(name, value)
|
|
279
329
|
|
|
330
|
+
if isinstance(options, Slider):
|
|
331
|
+
slider_class = (
|
|
332
|
+
solara.SliderFloat if options.is_float_slider else solara.SliderInt
|
|
333
|
+
)
|
|
334
|
+
slider_class(
|
|
335
|
+
options.label,
|
|
336
|
+
value=options.value,
|
|
337
|
+
on_value=change_handler,
|
|
338
|
+
min=options.min,
|
|
339
|
+
max=options.max,
|
|
340
|
+
step=options.step,
|
|
341
|
+
)
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
# label for the input is "label" from options or name
|
|
345
|
+
label = options.get("label", name)
|
|
346
|
+
input_type = options.get("type")
|
|
280
347
|
if input_type == "SliderInt":
|
|
281
348
|
solara.SliderInt(
|
|
282
349
|
label,
|
|
@@ -319,20 +386,15 @@ def make_text(renderer):
|
|
|
319
386
|
return function
|
|
320
387
|
|
|
321
388
|
|
|
322
|
-
def
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
else:
|
|
335
|
-
template_layout.update({"x": 6})
|
|
336
|
-
template_layout.update({"y": y_coord})
|
|
337
|
-
grid_lay.append(template_layout)
|
|
338
|
-
return grid_lay
|
|
389
|
+
def make_initial_grid_layout(layout_types):
|
|
390
|
+
return [
|
|
391
|
+
{
|
|
392
|
+
"i": i,
|
|
393
|
+
"w": 6,
|
|
394
|
+
"h": 10,
|
|
395
|
+
"moved": False,
|
|
396
|
+
"x": 6 * (i % 2),
|
|
397
|
+
"y": 16 * (i - i % 2),
|
|
398
|
+
}
|
|
399
|
+
for i in range(len(layout_types))
|
|
400
|
+
]
|
mesa/main.py
CHANGED
|
@@ -30,11 +30,14 @@ def runserver(project):
|
|
|
30
30
|
PROJECT is the path to the directory containing `run.py`, or the current
|
|
31
31
|
directory if not specified.
|
|
32
32
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
run_files = ["run.py", "server.py"]
|
|
34
|
+
for run_file in run_files:
|
|
35
|
+
run_path = Path(project) / run_file
|
|
36
|
+
if not run_path.exists():
|
|
37
|
+
continue
|
|
38
|
+
args = [sys.executable, str(run_path)]
|
|
39
|
+
call(args)
|
|
40
|
+
sys.exit(f"ERROR: file run.py or server.py (in {Path(project)}) does not exist")
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
@click.command()
|
mesa/model.py
CHANGED
|
@@ -3,6 +3,7 @@ The model class for Mesa framework.
|
|
|
3
3
|
|
|
4
4
|
Core Objects: Model
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
# Mypy; for the `|` operator purpose
|
|
7
8
|
# Remove this __future__ import once the oldest supported Python is 3.10
|
|
8
9
|
from __future__ import annotations
|
|
@@ -13,11 +14,13 @@ import warnings
|
|
|
13
14
|
from collections import defaultdict
|
|
14
15
|
|
|
15
16
|
# mypy
|
|
16
|
-
from typing import Any
|
|
17
|
+
from typing import Any, Union
|
|
17
18
|
|
|
18
19
|
from mesa.agent import Agent, AgentSet
|
|
19
20
|
from mesa.datacollection import DataCollector
|
|
20
21
|
|
|
22
|
+
TimeT = Union[float, int]
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
class Model:
|
|
23
26
|
"""Base class for models in the Mesa ABM library.
|
|
@@ -53,8 +56,11 @@ class Model:
|
|
|
53
56
|
if obj._seed is None:
|
|
54
57
|
# We explicitly specify the seed here so that we know its value in
|
|
55
58
|
# advance.
|
|
56
|
-
obj._seed = random.random()
|
|
59
|
+
obj._seed = random.random()
|
|
57
60
|
obj.random = random.Random(obj._seed)
|
|
61
|
+
# TODO: Remove these 2 lines just before Mesa 3.0
|
|
62
|
+
obj._steps = 0
|
|
63
|
+
obj._time = 0
|
|
58
64
|
return obj
|
|
59
65
|
|
|
60
66
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
@@ -68,8 +74,8 @@ class Model:
|
|
|
68
74
|
self.current_id = 0
|
|
69
75
|
self.agents_: defaultdict[type, dict] = defaultdict(dict)
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
self.
|
|
77
|
+
self._steps: int = 0
|
|
78
|
+
self._time: TimeT = 0 # the model's clock
|
|
73
79
|
|
|
74
80
|
@property
|
|
75
81
|
def agents(self) -> AgentSet:
|
|
@@ -112,6 +118,11 @@ class Model:
|
|
|
112
118
|
def step(self) -> None:
|
|
113
119
|
"""A single step. Fill in here."""
|
|
114
120
|
|
|
121
|
+
def _advance_time(self, deltat: TimeT = 1):
|
|
122
|
+
"""Increment the model's steps counter and clock."""
|
|
123
|
+
self._steps += 1
|
|
124
|
+
self._time += deltat
|
|
125
|
+
|
|
115
126
|
def next_id(self) -> int:
|
|
116
127
|
"""Return the next unique ID for agents, increment current_id"""
|
|
117
128
|
self.current_id += 1
|