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.

@@ -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)
@@ -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="Mesa Model",
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["value"] for k, v in user_params.items()}}
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(**model_parameters)
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, dependencies=[*list(model_parameters.values()), reset_counter.value]
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=[current_step.value]
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.make_plot(model, measure)
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 = get_initial_grid_layout(layout_types=layout_types)
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
- with solara.Card("Progress", margin=1, elevation=2):
137
- solara.Markdown(md_text=f"####Step - {current_step}")
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
- ColorCard(color="white", layout_type=layout_types[i])
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 += 1
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: 30px;
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 get_initial_grid_layout(layout_types):
323
- grid_lay = []
324
- y_coord = 0
325
- for ii in range(len(layout_types)):
326
- template_layout = {"h": 10, "i": 0, "moved": False, "w": 6, "y": 0, "x": 0}
327
- if ii == 0:
328
- grid_lay.append(template_layout)
329
- else:
330
- template_layout.update({"i": ii})
331
- if ii % 2 == 0:
332
- template_layout.update({"x": 0})
333
- y_coord += 16
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
- run_path = Path(project) / "run.py"
34
- if not run_path.exists():
35
- sys.exit(f"ERROR: file {run_path} does not exist")
36
- args = [sys.executable, str(run_path)]
37
- call(args)
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() # noqa: S311
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
- # Warning flags for current experimental features. These make sure a warning is only printed once per model.
72
- self.agentset_experimental_warning_given = False
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