Mesa 3.1.0.dev0__py3-none-any.whl → 3.1.2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (60) hide show
  1. mesa/__init__.py +3 -3
  2. mesa/agent.py +48 -0
  3. mesa/batchrunner.py +14 -1
  4. mesa/datacollection.py +1 -6
  5. mesa/examples/__init__.py +2 -2
  6. mesa/examples/advanced/epstein_civil_violence/app.py +5 -0
  7. mesa/examples/advanced/pd_grid/agents.py +2 -1
  8. mesa/examples/advanced/pd_grid/analysis.ipynb +44 -89
  9. mesa/examples/advanced/pd_grid/app.py +5 -0
  10. mesa/examples/advanced/pd_grid/model.py +3 -5
  11. mesa/examples/advanced/sugarscape_g1mt/agents.py +12 -65
  12. mesa/examples/advanced/sugarscape_g1mt/app.py +24 -19
  13. mesa/examples/advanced/sugarscape_g1mt/model.py +45 -52
  14. mesa/examples/advanced/wolf_sheep/agents.py +36 -2
  15. mesa/examples/advanced/wolf_sheep/model.py +17 -16
  16. mesa/examples/basic/boid_flockers/app.py +5 -0
  17. mesa/examples/basic/boltzmann_wealth_model/app.py +8 -5
  18. mesa/examples/basic/boltzmann_wealth_model/st_app.py +1 -1
  19. mesa/examples/basic/conways_game_of_life/app.py +5 -0
  20. mesa/examples/basic/conways_game_of_life/st_app.py +2 -2
  21. mesa/examples/basic/schelling/agents.py +11 -5
  22. mesa/examples/basic/schelling/analysis.ipynb +42 -36
  23. mesa/examples/basic/schelling/app.py +6 -1
  24. mesa/examples/basic/schelling/model.py +3 -3
  25. mesa/examples/basic/virus_on_network/app.py +5 -0
  26. mesa/experimental/__init__.py +17 -10
  27. mesa/experimental/cell_space/__init__.py +19 -7
  28. mesa/experimental/cell_space/cell.py +22 -37
  29. mesa/experimental/cell_space/cell_agent.py +12 -1
  30. mesa/experimental/cell_space/cell_collection.py +18 -3
  31. mesa/experimental/cell_space/discrete_space.py +15 -64
  32. mesa/experimental/cell_space/grid.py +74 -4
  33. mesa/experimental/cell_space/network.py +13 -1
  34. mesa/experimental/cell_space/property_layer.py +444 -0
  35. mesa/experimental/cell_space/voronoi.py +13 -1
  36. mesa/experimental/devs/__init__.py +20 -2
  37. mesa/experimental/devs/eventlist.py +19 -1
  38. mesa/experimental/devs/simulator.py +24 -8
  39. mesa/experimental/mesa_signals/__init__.py +23 -0
  40. mesa/experimental/mesa_signals/mesa_signal.py +485 -0
  41. mesa/experimental/mesa_signals/observable_collections.py +133 -0
  42. mesa/experimental/mesa_signals/signals_util.py +52 -0
  43. mesa/mesa_logging.py +190 -0
  44. mesa/model.py +17 -23
  45. mesa/visualization/__init__.py +2 -2
  46. mesa/visualization/mpl_space_drawing.py +8 -6
  47. mesa/visualization/solara_viz.py +49 -11
  48. {mesa-3.1.0.dev0.dist-info → mesa-3.1.2.dist-info}/METADATA +4 -2
  49. mesa-3.1.2.dist-info/RECORD +94 -0
  50. {mesa-3.1.0.dev0.dist-info → mesa-3.1.2.dist-info}/WHEEL +1 -1
  51. mesa/experimental/UserParam.py +0 -67
  52. mesa/experimental/components/altair.py +0 -81
  53. mesa/experimental/components/matplotlib.py +0 -242
  54. mesa/experimental/devs/examples/epstein_civil_violence.py +0 -305
  55. mesa/experimental/devs/examples/wolf_sheep.py +0 -250
  56. mesa/experimental/solara_viz.py +0 -453
  57. mesa-3.1.0.dev0.dist-info/RECORD +0 -94
  58. {mesa-3.1.0.dev0.dist-info → mesa-3.1.2.dist-info}/entry_points.txt +0 -0
  59. {mesa-3.1.0.dev0.dist-info → mesa-3.1.2.dist-info}/licenses/LICENSE +0 -0
  60. {mesa-3.1.0.dev0.dist-info → mesa-3.1.2.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,52 @@
1
+ """Utility functions and classes for Mesa's signals implementation.
2
+
3
+ This module provides helper functionality used by Mesa's reactive programming system:
4
+
5
+ - AttributeDict: A dictionary subclass that allows attribute-style access to its contents
6
+ - create_weakref: Helper function to properly create weak references to different types
7
+
8
+ These utilities support the core signals implementation by providing reference
9
+ management and convenient data structures used throughout the reactive system.
10
+ """
11
+
12
+ import weakref
13
+
14
+ __all__ = [
15
+ "AttributeDict",
16
+ "create_weakref",
17
+ ]
18
+
19
+
20
+ class AttributeDict(dict):
21
+ """A dict with attribute like access.
22
+
23
+ Each value can be accessed as if it were an attribute with its key as attribute name
24
+
25
+ """
26
+
27
+ # I want our signals to act like traitlet signals, so this is inspired by trailets Bunch
28
+ # and some stack overflow posts.
29
+ __setattr__ = dict.__setitem__
30
+ __delattr__ = dict.__delitem__
31
+
32
+ def __getattr__(self, key): # noqa: D105
33
+ try:
34
+ return self.__getitem__(key)
35
+ except KeyError as e:
36
+ # we need to go from key error to attribute error
37
+ raise AttributeError(key) from e
38
+
39
+ def __dir__(self): # noqa: D105
40
+ # allows us to easily access all defined attributes
41
+ names = dir({})
42
+ names.extend(self.keys())
43
+ return names
44
+
45
+
46
+ def create_weakref(item, callback=None):
47
+ """Helper function to create a correct weakref for any item."""
48
+ if hasattr(item, "__self__"):
49
+ ref = weakref.WeakMethod(item, callback)
50
+ else:
51
+ ref = weakref.ref(item, callback)
52
+ return ref
mesa/mesa_logging.py ADDED
@@ -0,0 +1,190 @@
1
+ """This provides logging functionality for MESA.
2
+
3
+ It is modeled on the default `logging approach that comes with Python <https://docs.python.org/library/logging.html>`_.
4
+
5
+
6
+ """
7
+
8
+ import inspect
9
+ import logging
10
+ from functools import wraps
11
+ from logging import DEBUG, INFO
12
+
13
+ __all__ = [
14
+ "DEBUG",
15
+ "DEFAULT_LEVEL",
16
+ "INFO",
17
+ "LOGGER_NAME",
18
+ "function_logger",
19
+ "get_module_logger",
20
+ "get_rootlogger",
21
+ "log_to_stderr",
22
+ "method_logger",
23
+ ]
24
+ LOGGER_NAME = "MESA"
25
+ DEFAULT_LEVEL = DEBUG
26
+
27
+
28
+ def create_module_logger(name: str | None = None):
29
+ """Helper function for creating a module logger.
30
+
31
+ Args:
32
+ name (str): The name to be given to the logger. If the name is None, the name defaults to the name of the module.
33
+
34
+ """
35
+ if name is None:
36
+ frm = inspect.stack()[1]
37
+ mod = inspect.getmodule(frm[0])
38
+ name = mod.__name__
39
+ logger = logging.getLogger(f"{LOGGER_NAME}.{name}")
40
+
41
+ _module_loggers[name] = logger
42
+ return logger
43
+
44
+
45
+ def get_module_logger(name: str):
46
+ """Helper function for getting the module logger.
47
+
48
+ Args:
49
+ name (str): The name of the module in which the method being decorated is located
50
+
51
+ """
52
+ try:
53
+ logger = _module_loggers[name]
54
+ except KeyError:
55
+ logger = create_module_logger(name)
56
+
57
+ return logger
58
+
59
+
60
+ _rootlogger = None
61
+ _module_loggers = {}
62
+ _logger = get_module_logger(__name__)
63
+
64
+
65
+ class MESAColorFormatter(logging.Formatter):
66
+ """Custom formatter for color based formatting."""
67
+
68
+ grey = "\x1b[38;20m"
69
+ green = "\x1b[32m"
70
+ yellow = "\x1b[33;20m"
71
+ red = "\x1b[31;20m"
72
+ bold_red = "\x1b[31;1m"
73
+ reset = "\x1b[0m"
74
+ format = (
75
+ "[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]"
76
+ )
77
+
78
+ FORMATS = {
79
+ logging.DEBUG: grey + format + reset,
80
+ logging.INFO: green + format + reset,
81
+ logging.WARNING: yellow + format + reset,
82
+ logging.ERROR: red + format + reset,
83
+ logging.CRITICAL: bold_red + format + reset,
84
+ }
85
+
86
+ def format(self, record):
87
+ """Format record."""
88
+ log_fmt = self.FORMATS.get(record.levelno)
89
+ formatter = logging.Formatter(log_fmt)
90
+ return formatter.format(record)
91
+
92
+
93
+ def method_logger(name: str):
94
+ """Decorator for adding logging to a method.
95
+
96
+ Args:
97
+ name (str): The name of the module in which the method being decorated is located
98
+
99
+ """
100
+ logger = get_module_logger(name)
101
+ classname = inspect.getouterframes(inspect.currentframe())[1][3]
102
+
103
+ def real_decorator(func):
104
+ @wraps(func)
105
+ def wrapper(*args, **kwargs):
106
+ # hack, because log is applied to methods, we can get
107
+ # object instance as first arguments in args
108
+ logger.debug(
109
+ f"calling {classname}.{func.__name__} with {args[1::]} and {kwargs}"
110
+ )
111
+ res = func(*args, **kwargs)
112
+ return res
113
+
114
+ return wrapper
115
+
116
+ return real_decorator
117
+
118
+
119
+ def function_logger(name):
120
+ """Decorator for adding logging to a Function.
121
+
122
+ Args:
123
+ name (str): The name of the module in which the function being decorated is located
124
+
125
+ """
126
+ logger = get_module_logger(name)
127
+
128
+ def real_decorator(func):
129
+ @wraps(func)
130
+ def wrapper(*args, **kwargs):
131
+ logger.debug(f"calling {func.__name__} with {args} and {kwargs}")
132
+ res = func(*args, **kwargs)
133
+ return res
134
+
135
+ return wrapper
136
+
137
+ return real_decorator
138
+
139
+
140
+ def get_rootlogger():
141
+ """Returns root logger used by MESA.
142
+
143
+ Returns:
144
+ the root logger of MESA
145
+
146
+ """
147
+ global _rootlogger # noqa: PLW0603
148
+
149
+ if not _rootlogger:
150
+ _rootlogger = logging.getLogger(LOGGER_NAME)
151
+ _rootlogger.handlers = []
152
+ _rootlogger.addHandler(logging.NullHandler())
153
+ _rootlogger.setLevel(DEBUG)
154
+
155
+ return _rootlogger
156
+
157
+
158
+ def log_to_stderr(level: int | None = None, pass_root_logger_level: bool = False):
159
+ """Turn on logging and add a handler which prints to stderr.
160
+
161
+ Args:
162
+ level: minimum level of the messages that will be logged
163
+ pass_root_logger_level: bool, optional. Default False
164
+ if True, all module loggers will be set to the same logging level as the root logger.
165
+
166
+ """
167
+ if not level:
168
+ level = DEFAULT_LEVEL
169
+
170
+ logger = get_rootlogger()
171
+
172
+ # avoid creation of multiple stream handlers for logging to console
173
+ for entry in logger.handlers:
174
+ if (isinstance(entry, logging.StreamHandler)) and (
175
+ isinstance(entry.formatter, MESAColorFormatter)
176
+ ):
177
+ return logger
178
+
179
+ formatter = MESAColorFormatter()
180
+ handler = logging.StreamHandler()
181
+ handler.setLevel(level)
182
+ handler.setFormatter(formatter)
183
+ logger.addHandler(handler)
184
+ logger.propagate = False
185
+
186
+ if pass_root_logger_level:
187
+ for _, mod_logger in _module_loggers.items():
188
+ mod_logger.setLevel(level)
189
+
190
+ return logger
mesa/model.py CHANGED
@@ -9,7 +9,6 @@ from __future__ import annotations
9
9
 
10
10
  import random
11
11
  import sys
12
- import warnings
13
12
  from collections.abc import Sequence
14
13
 
15
14
  # mypy
@@ -18,11 +17,15 @@ from typing import Any
18
17
  import numpy as np
19
18
 
20
19
  from mesa.agent import Agent, AgentSet
20
+ from mesa.mesa_logging import create_module_logger, method_logger
21
21
 
22
22
  SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence
23
23
  RNGLike = np.random.Generator | np.random.BitGenerator
24
24
 
25
25
 
26
+ _mesa_logger = create_module_logger()
27
+
28
+
26
29
  class Model:
27
30
  """Base class for models in the Mesa ABM library.
28
31
 
@@ -32,7 +35,6 @@ class Model:
32
35
 
33
36
  Attributes:
34
37
  running: A boolean indicating if the model should continue running.
35
- schedule: An object to manage the order and execution of agent steps.
36
38
  steps: the number of times `model.step()` has been called.
37
39
  random: a seeded python.random number generator.
38
40
  rng : a seeded numpy.random.Generator
@@ -44,6 +46,7 @@ class Model:
44
46
 
45
47
  """
46
48
 
49
+ @method_logger(__name__)
47
50
  def __init__(
48
51
  self,
49
52
  *args: Any,
@@ -102,12 +105,19 @@ class Model:
102
105
  self.step = self._wrapped_step
103
106
 
104
107
  # setup agent registration data structures
105
- self._setup_agent_registration()
108
+ self._agents = {} # the hard references to all agents in the model
109
+ self._agents_by_type: dict[
110
+ type[Agent], AgentSet
111
+ ] = {} # a dict with an agentset for each class of agents
112
+ self._all_agents = AgentSet(
113
+ [], random=self.random
114
+ ) # an agenset with all agents
106
115
 
107
116
  def _wrapped_step(self, *args: Any, **kwargs: Any) -> None:
108
117
  """Automatically increments time and steps after calling the user's step method."""
109
118
  # Automatically increment time and step counters
110
119
  self.steps += 1
120
+ _mesa_logger.info(f"calling model.step for timestep {self.steps} ")
111
121
  # Call the original user-defined step method
112
122
  self._user_step(*args, **kwargs)
113
123
 
@@ -134,16 +144,6 @@ class Model:
134
144
  """A dictionary where the keys are agent types and the values are the corresponding AgentSets."""
135
145
  return self._agents_by_type
136
146
 
137
- def _setup_agent_registration(self):
138
- """Helper method to initialize the agent registration datastructures."""
139
- self._agents = {} # the hard references to all agents in the model
140
- self._agents_by_type: dict[
141
- type[Agent], AgentSet
142
- ] = {} # a dict with an agentset for each class of agents
143
- self._all_agents = AgentSet(
144
- [], random=self.random
145
- ) # an agenset with all agents
146
-
147
147
  def register_agent(self, agent):
148
148
  """Register the agent with the model.
149
149
 
@@ -155,16 +155,6 @@ class Model:
155
155
  if you are subclassing Agent and calling its super in the ``__init__`` method.
156
156
 
157
157
  """
158
- if not hasattr(self, "_agents"):
159
- self._setup_agent_registration()
160
-
161
- warnings.warn(
162
- "The Mesa Model class was not initialized. In the future, you need to explicitly initialize "
163
- "the Model by calling super().__init__() on initialization.",
164
- FutureWarning,
165
- stacklevel=2,
166
- )
167
-
168
158
  self._agents[agent] = None
169
159
 
170
160
  # because AgentSet requires model, we cannot use defaultdict
@@ -180,6 +170,9 @@ class Model:
180
170
  )
181
171
 
182
172
  self._all_agents.add(agent)
173
+ _mesa_logger.debug(
174
+ f"registered {agent.__class__.__name__} with agent_id {agent.unique_id}"
175
+ )
183
176
 
184
177
  def deregister_agent(self, agent):
185
178
  """Deregister the agent with the model.
@@ -194,6 +187,7 @@ class Model:
194
187
  del self._agents[agent]
195
188
  self._agents_by_type[type(agent)].remove(agent)
196
189
  self._all_agents.remove(agent)
190
+ _mesa_logger.debug(f"deregistered agent with agent_id {agent.unique_id}")
197
191
 
198
192
  def run_model(self) -> None:
199
193
  """Run the model until the end condition is reached.
@@ -17,10 +17,10 @@ from .user_param import Slider
17
17
 
18
18
  __all__ = [
19
19
  "JupyterViz",
20
- "SolaraViz",
21
20
  "Slider",
22
- "make_space_altair",
21
+ "SolaraViz",
23
22
  "draw_space",
24
23
  "make_plot_component",
24
+ "make_space_altair",
25
25
  "make_space_component",
26
26
  ]
@@ -117,7 +117,6 @@ def draw_space(
117
117
  agent_portrayal: A callable that returns a dict specifying how to show the agent
118
118
  propertylayer_portrayal: a dict specifying how to show propertylayer(s)
119
119
  ax: the axes upon which to draw the plot
120
- post_process: a callable called with the Axes instance
121
120
  space_drawing_kwargs: any additional keyword arguments to be passed on to the underlying function for drawing the space.
122
121
 
123
122
  Returns:
@@ -178,15 +177,18 @@ def draw_property_layers(
178
177
  property_layers = space.properties
179
178
  except AttributeError:
180
179
  # new style spaces
181
- property_layers = space.property_layers
180
+ property_layers = space._mesa_property_layers
182
181
 
183
182
  for layer_name, portrayal in propertylayer_portrayal.items():
184
183
  layer = property_layers.get(layer_name, None)
185
- if not isinstance(layer, PropertyLayer):
184
+ if not isinstance(
185
+ layer,
186
+ PropertyLayer | mesa.experimental.cell_space.property_layer.PropertyLayer,
187
+ ):
186
188
  continue
187
189
 
188
190
  data = layer.data.astype(float) if layer.data.dtype == bool else layer.data
189
- width, height = data.shape if space is None else (space.width, space.height)
191
+ width, height = data.shape # if space is None else (space.width, space.height)
190
192
 
191
193
  if space and data.shape != (width, height):
192
194
  warnings.warn(
@@ -212,7 +214,7 @@ def draw_property_layers(
212
214
  layer_name, [(0, 0, 0, 0), (*rgba_color[:3], alpha)]
213
215
  )
214
216
  im = ax.imshow(
215
- rgba_data.transpose(1, 0, 2),
217
+ rgba_data,
216
218
  origin="lower",
217
219
  )
218
220
  if colorbar:
@@ -226,7 +228,7 @@ def draw_property_layers(
226
228
  if isinstance(cmap, list):
227
229
  cmap = LinearSegmentedColormap.from_list(layer_name, cmap)
228
230
  im = ax.imshow(
229
- data.T,
231
+ data,
230
232
  cmap=cmap,
231
233
  alpha=alpha,
232
234
  vmin=vmin,
@@ -33,14 +33,18 @@ import solara
33
33
 
34
34
  import mesa.visualization.components.altair_components as components_altair
35
35
  from mesa.experimental.devs.simulator import Simulator
36
+ from mesa.mesa_logging import create_module_logger, function_logger
36
37
  from mesa.visualization.user_param import Slider
37
38
  from mesa.visualization.utils import force_update, update_counter
38
39
 
39
40
  if TYPE_CHECKING:
40
41
  from mesa.model import Model
41
42
 
43
+ _mesa_logger = create_module_logger()
44
+
42
45
 
43
46
  @solara.component
47
+ @function_logger(__name__)
44
48
  def SolaraViz(
45
49
  model: Model | solara.Reactive[Model],
46
50
  components: list[reacton.core.Component]
@@ -98,24 +102,33 @@ def SolaraViz(
98
102
 
99
103
  # set up reactive model_parameters shared by ModelCreator and ModelController
100
104
  reactive_model_parameters = solara.use_reactive({})
105
+ reactive_play_interval = solara.use_reactive(play_interval)
101
106
 
102
107
  with solara.AppBar():
103
108
  solara.AppBarTitle(name if name else model.value.__class__.__name__)
104
109
 
105
110
  with solara.Sidebar(), solara.Column():
106
111
  with solara.Card("Controls"):
112
+ solara.SliderInt(
113
+ label="Play Interval (ms)",
114
+ value=reactive_play_interval,
115
+ on_value=lambda v: reactive_play_interval.set(v),
116
+ min=1,
117
+ max=500,
118
+ step=10,
119
+ )
107
120
  if not isinstance(simulator, Simulator):
108
121
  ModelController(
109
122
  model,
110
123
  model_parameters=reactive_model_parameters,
111
- play_interval=play_interval,
124
+ play_interval=reactive_play_interval,
112
125
  )
113
126
  else:
114
127
  SimulatorController(
115
128
  model,
116
129
  simulator,
117
130
  model_parameters=reactive_model_parameters,
118
- play_interval=play_interval,
131
+ play_interval=reactive_play_interval,
119
132
  )
120
133
  with solara.Card("Model Parameters"):
121
134
  ModelCreator(
@@ -175,7 +188,7 @@ def ModelController(
175
188
  model: solara.Reactive[Model],
176
189
  *,
177
190
  model_parameters: dict | solara.Reactive[dict] = None,
178
- play_interval: int = 100,
191
+ play_interval: int | solara.Reactive[int] = 100,
179
192
  ):
180
193
  """Create controls for model execution (step, play, pause, reset).
181
194
 
@@ -193,25 +206,32 @@ def ModelController(
193
206
 
194
207
  async def step():
195
208
  while playing.value and running.value:
196
- await asyncio.sleep(play_interval / 1000)
209
+ await asyncio.sleep(play_interval.value / 1000)
197
210
  do_step()
198
211
 
199
212
  solara.lab.use_task(
200
213
  step, dependencies=[playing.value, running.value], prefer_threaded=False
201
214
  )
202
215
 
216
+ @function_logger(__name__)
203
217
  def do_step():
204
218
  """Advance the model by one step."""
205
219
  model.value.step()
206
220
  running.value = model.value.running
207
221
  force_update()
208
222
 
223
+ @function_logger(__name__)
209
224
  def do_reset():
210
225
  """Reset the model to its initial state."""
211
226
  playing.value = False
212
227
  running.value = True
228
+ _mesa_logger.log(
229
+ 10,
230
+ f"creating new {model.value.__class__} instance with {model_parameters.value}",
231
+ )
213
232
  model.value = model.value = model.value.__class__(**model_parameters.value)
214
233
 
234
+ @function_logger(__name__)
215
235
  def do_play_pause():
216
236
  """Toggle play/pause."""
217
237
  playing.value = not playing.value
@@ -238,7 +258,7 @@ def SimulatorController(
238
258
  simulator,
239
259
  *,
240
260
  model_parameters: dict | solara.Reactive[dict] = None,
241
- play_interval: int = 100,
261
+ play_interval: int | solara.Reactive[int] = 100,
242
262
  ):
243
263
  """Create controls for model execution (step, play, pause, reset).
244
264
 
@@ -257,7 +277,7 @@ def SimulatorController(
257
277
 
258
278
  async def step():
259
279
  while playing.value and running.value:
260
- await asyncio.sleep(play_interval / 1000)
280
+ await asyncio.sleep(play_interval.value / 1000)
261
281
  do_step()
262
282
 
263
283
  solara.lab.use_task(
@@ -382,12 +402,19 @@ def ModelCreator(
382
402
  )
383
403
  user_params, fixed_params = split_model_params(user_params)
384
404
 
385
- # set model_parameters to the default values for all parameters
386
- model_parameters.value = {
387
- **fixed_params,
388
- **{k: v.get("value") for k, v in user_params.items()},
389
- }
405
+ # Use solara.use_effect to run the initialization code only once
406
+ solara.use_effect(
407
+ # set model_parameters to the default values for all parameters
408
+ lambda: model_parameters.set(
409
+ {
410
+ **fixed_params,
411
+ **{k: v.get("value") for k, v in user_params.items()},
412
+ }
413
+ ),
414
+ [],
415
+ )
390
416
 
417
+ @function_logger(__name__)
391
418
  def on_change(name, value):
392
419
  model_parameters.value = {**model_parameters.value, name: value}
393
420
 
@@ -405,6 +432,17 @@ def _check_model_params(init_func, model_params):
405
432
  ValueError: If a parameter is not valid for the model's initialization function
406
433
  """
407
434
  model_parameters = inspect.signature(init_func).parameters
435
+
436
+ has_var_positional = any(
437
+ param.kind == inspect.Parameter.VAR_POSITIONAL
438
+ for param in model_parameters.values()
439
+ )
440
+
441
+ if has_var_positional:
442
+ raise ValueError(
443
+ "Mesa's visualization requires the use of keyword arguments to ensure the parameters are passed to Solara correctly. Please ensure all model parameters are of form param=value"
444
+ )
445
+
408
446
  for name in model_parameters:
409
447
  if (
410
448
  model_parameters[name].default == inspect.Parameter.empty
@@ -1,11 +1,13 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: Mesa
3
- Version: 3.1.0.dev0
3
+ Version: 3.1.2
4
4
  Summary: Agent-based modeling (ABM) in Python
5
5
  Project-URL: homepage, https://github.com/projectmesa/mesa
6
6
  Project-URL: repository, https://github.com/projectmesa/mesa
7
7
  Author-email: Project Mesa Team <projectmesa@googlegroups.com>
8
8
  License: Apache 2.0
9
+ License-File: LICENSE
10
+ License-File: NOTICE
9
11
  Keywords: ABM,agent,based,model,modeling,multi-agent,simulation
10
12
  Classifier: Development Status :: 3 - Alpha
11
13
  Classifier: Intended Audience :: Science/Research