Mesa 3.0.0b0__py3-none-any.whl → 3.0.0b1__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 (87) hide show
  1. examples/README.md +37 -0
  2. examples/__init__.py +0 -0
  3. examples/advanced/__init__.py +0 -0
  4. examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
  5. examples/advanced/epstein_civil_violence/Readme.md +33 -0
  6. examples/advanced/epstein_civil_violence/epstein_civil_violence/__init__.py +0 -0
  7. examples/advanced/epstein_civil_violence/epstein_civil_violence/agent.py +158 -0
  8. examples/advanced/epstein_civil_violence/epstein_civil_violence/model.py +146 -0
  9. examples/advanced/epstein_civil_violence/epstein_civil_violence/portrayal.py +33 -0
  10. examples/advanced/epstein_civil_violence/epstein_civil_violence/server.py +81 -0
  11. examples/advanced/epstein_civil_violence/requirements.txt +3 -0
  12. examples/advanced/epstein_civil_violence/run.py +3 -0
  13. examples/advanced/pd_grid/analysis.ipynb +228 -0
  14. examples/advanced/pd_grid/pd_grid/__init__.py +0 -0
  15. examples/advanced/pd_grid/pd_grid/agent.py +50 -0
  16. examples/advanced/pd_grid/pd_grid/model.py +72 -0
  17. examples/advanced/pd_grid/pd_grid/portrayal.py +19 -0
  18. examples/advanced/pd_grid/pd_grid/server.py +21 -0
  19. examples/advanced/pd_grid/readme.md +42 -0
  20. examples/advanced/pd_grid/requirements.txt +3 -0
  21. examples/advanced/pd_grid/run.py +3 -0
  22. examples/advanced/sugarscape_g1mt/Readme.md +87 -0
  23. examples/advanced/sugarscape_g1mt/app.py +61 -0
  24. examples/advanced/sugarscape_g1mt/requirements.txt +6 -0
  25. examples/advanced/sugarscape_g1mt/run.py +105 -0
  26. examples/advanced/sugarscape_g1mt/sugarscape_g1mt/__init__.py +0 -0
  27. examples/advanced/sugarscape_g1mt/sugarscape_g1mt/model.py +180 -0
  28. examples/advanced/sugarscape_g1mt/sugarscape_g1mt/resource_agents.py +26 -0
  29. examples/advanced/sugarscape_g1mt/sugarscape_g1mt/server.py +61 -0
  30. examples/advanced/sugarscape_g1mt/sugarscape_g1mt/sugar-map.txt +50 -0
  31. examples/advanced/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py +321 -0
  32. examples/advanced/sugarscape_g1mt/tests.py +72 -0
  33. examples/advanced/wolf_sheep/Readme.md +57 -0
  34. examples/advanced/wolf_sheep/__init__.py +0 -0
  35. examples/advanced/wolf_sheep/requirements.txt +1 -0
  36. examples/advanced/wolf_sheep/run.py +3 -0
  37. examples/advanced/wolf_sheep/wolf_sheep/__init__.py +0 -0
  38. examples/advanced/wolf_sheep/wolf_sheep/agents.py +102 -0
  39. examples/advanced/wolf_sheep/wolf_sheep/model.py +136 -0
  40. examples/advanced/wolf_sheep/wolf_sheep/resources/sheep.png +0 -0
  41. examples/advanced/wolf_sheep/wolf_sheep/resources/wolf.png +0 -0
  42. examples/advanced/wolf_sheep/wolf_sheep/server.py +78 -0
  43. examples/basic/__init__.py +13 -0
  44. examples/basic/boid_flockers/Readme.md +43 -0
  45. examples/basic/boid_flockers/agents.py +71 -0
  46. examples/basic/boid_flockers/app.py +59 -0
  47. examples/basic/boid_flockers/model.py +70 -0
  48. examples/basic/boltzmann_wealth_model/Readme.md +60 -0
  49. examples/basic/boltzmann_wealth_model/agents.py +31 -0
  50. examples/basic/boltzmann_wealth_model/app.py +66 -0
  51. examples/basic/boltzmann_wealth_model/model.py +44 -0
  52. examples/basic/boltzmann_wealth_model/st_app.py +115 -0
  53. examples/basic/conways_game_of_life/Readme.md +35 -0
  54. examples/basic/conways_game_of_life/agents.py +47 -0
  55. examples/basic/conways_game_of_life/model.py +32 -0
  56. examples/basic/conways_game_of_life/portrayal.py +18 -0
  57. examples/basic/conways_game_of_life/requirements.txt +1 -0
  58. examples/basic/conways_game_of_life/server.py +11 -0
  59. examples/basic/conways_game_of_life/st_app.py +71 -0
  60. examples/basic/schelling/README.md +47 -0
  61. examples/basic/schelling/agents.py +26 -0
  62. examples/basic/schelling/analysis.ipynb +205 -0
  63. examples/basic/schelling/app.py +43 -0
  64. examples/basic/schelling/model.py +60 -0
  65. examples/basic/virus_on_network/README.md +61 -0
  66. examples/basic/virus_on_network/agents.py +69 -0
  67. examples/basic/virus_on_network/app.py +133 -0
  68. examples/basic/virus_on_network/model.py +99 -0
  69. mesa/__init__.py +4 -1
  70. mesa/agent.py +14 -19
  71. mesa/examples.py +3 -0
  72. mesa/experimental/__init__.py +8 -2
  73. mesa/experimental/cell_space/cell.py +9 -0
  74. mesa/experimental/cell_space/discrete_space.py +7 -1
  75. mesa/experimental/cell_space/grid.py +13 -0
  76. mesa/experimental/cell_space/network.py +3 -0
  77. mesa/model.py +63 -12
  78. mesa/time.py +5 -3
  79. mesa/visualization/components/matplotlib.py +9 -4
  80. mesa/visualization/solara_viz.py +13 -58
  81. {mesa-3.0.0b0.dist-info → mesa-3.0.0b1.dist-info}/METADATA +1 -1
  82. mesa-3.0.0b1.dist-info/RECORD +114 -0
  83. mesa-3.0.0b0.dist-info/RECORD +0 -45
  84. {mesa-3.0.0b0.dist-info → mesa-3.0.0b1.dist-info}/WHEEL +0 -0
  85. {mesa-3.0.0b0.dist-info → mesa-3.0.0b1.dist-info}/entry_points.txt +0 -0
  86. {mesa-3.0.0b0.dist-info → mesa-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
  87. {mesa-3.0.0b0.dist-info → mesa-3.0.0b1.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,69 @@
1
+ from enum import Enum
2
+
3
+ from mesa import Agent
4
+
5
+
6
+ class State(Enum):
7
+ SUSCEPTIBLE = 0
8
+ INFECTED = 1
9
+ RESISTANT = 2
10
+
11
+
12
+ class VirusAgent(Agent):
13
+ """Individual Agent definition and its properties/interaction methods."""
14
+
15
+ def __init__(
16
+ self,
17
+ model,
18
+ initial_state,
19
+ virus_spread_chance,
20
+ virus_check_frequency,
21
+ recovery_chance,
22
+ gain_resistance_chance,
23
+ ):
24
+ super().__init__(model)
25
+
26
+ self.state = initial_state
27
+
28
+ self.virus_spread_chance = virus_spread_chance
29
+ self.virus_check_frequency = virus_check_frequency
30
+ self.recovery_chance = recovery_chance
31
+ self.gain_resistance_chance = gain_resistance_chance
32
+
33
+ def try_to_infect_neighbors(self):
34
+ neighbors_nodes = self.model.grid.get_neighborhood(
35
+ self.pos, include_center=False
36
+ )
37
+ susceptible_neighbors = [
38
+ agent
39
+ for agent in self.model.grid.get_cell_list_contents(neighbors_nodes)
40
+ if agent.state is State.SUSCEPTIBLE
41
+ ]
42
+ for a in susceptible_neighbors:
43
+ if self.random.random() < self.virus_spread_chance:
44
+ a.state = State.INFECTED
45
+
46
+ def try_gain_resistance(self):
47
+ if self.random.random() < self.gain_resistance_chance:
48
+ self.state = State.RESISTANT
49
+
50
+ def try_remove_infection(self):
51
+ # Try to remove
52
+ if self.random.random() < self.recovery_chance:
53
+ # Success
54
+ self.state = State.SUSCEPTIBLE
55
+ self.try_gain_resistance()
56
+ else:
57
+ # Failed
58
+ self.state = State.INFECTED
59
+
60
+ def try_check_situation(self):
61
+ if (self.random.random() < self.virus_check_frequency) and (
62
+ self.state is State.INFECTED
63
+ ):
64
+ self.try_remove_infection()
65
+
66
+ def step(self):
67
+ if self.state is State.INFECTED:
68
+ self.try_to_infect_neighbors()
69
+ self.try_check_situation()
@@ -0,0 +1,133 @@
1
+ import math
2
+
3
+ import solara
4
+ from matplotlib.figure import Figure
5
+ from matplotlib.ticker import MaxNLocator
6
+
7
+ from mesa.visualization import Slider, SolaraViz, make_space_matplotlib
8
+
9
+ from .model import State, VirusOnNetwork, number_infected
10
+
11
+
12
+ def agent_portrayal(graph):
13
+ def get_agent(node):
14
+ return graph.nodes[node]["agent"][0]
15
+
16
+ edge_width = []
17
+ edge_color = []
18
+ for u, v in graph.edges():
19
+ agent1 = get_agent(u)
20
+ agent2 = get_agent(v)
21
+ w = 2
22
+ ec = "#e8e8e8"
23
+ if State.RESISTANT in (agent1.state, agent2.state):
24
+ w = 3
25
+ ec = "black"
26
+ edge_width.append(w)
27
+ edge_color.append(ec)
28
+ node_color_dict = {
29
+ State.INFECTED: "tab:red",
30
+ State.SUSCEPTIBLE: "tab:green",
31
+ State.RESISTANT: "tab:gray",
32
+ }
33
+ node_color = [node_color_dict[get_agent(node).state] for node in graph.nodes()]
34
+ return {
35
+ "width": edge_width,
36
+ "edge_color": edge_color,
37
+ "node_color": node_color,
38
+ }
39
+
40
+
41
+ def get_resistant_susceptible_ratio(model):
42
+ ratio = model.resistant_susceptible_ratio()
43
+ ratio_text = r"$\infty$" if ratio is math.inf else f"{ratio:.2f}"
44
+ infected_text = str(number_infected(model))
45
+
46
+ return f"Resistant/Susceptible Ratio: {ratio_text}<br>Infected Remaining: {infected_text}"
47
+
48
+
49
+ def make_plot(model):
50
+ # This is for the case when we want to plot multiple measures in 1 figure.
51
+ fig = Figure()
52
+ ax = fig.subplots()
53
+ measures = ["Infected", "Susceptible", "Resistant"]
54
+ colors = ["tab:red", "tab:green", "tab:gray"]
55
+ for i, m in enumerate(measures):
56
+ color = colors[i]
57
+ df = model.datacollector.get_model_vars_dataframe()
58
+ ax.plot(df.loc[:, m], label=m, color=color)
59
+ fig.legend()
60
+ # Set integer x axis
61
+ ax.xaxis.set_major_locator(MaxNLocator(integer=True))
62
+ ax.set_xlabel("Step")
63
+ ax.set_ylabel("Number of Agents")
64
+ return solara.FigureMatplotlib(fig)
65
+
66
+
67
+ model_params = {
68
+ "num_nodes": Slider(
69
+ label="Number of agents",
70
+ value=10,
71
+ min=10,
72
+ max=100,
73
+ step=1,
74
+ ),
75
+ "avg_node_degree": Slider(
76
+ label="Avg Node Degree",
77
+ value=3,
78
+ min=3,
79
+ max=8,
80
+ step=1,
81
+ ),
82
+ "initial_outbreak_size": Slider(
83
+ label="Initial Outbreak Size",
84
+ value=1,
85
+ min=1,
86
+ max=10,
87
+ step=1,
88
+ ),
89
+ "virus_spread_chance": Slider(
90
+ label="Virus Spread Chance",
91
+ value=0.4,
92
+ min=0.0,
93
+ max=1.0,
94
+ step=0.1,
95
+ ),
96
+ "virus_check_frequency": Slider(
97
+ label="Virus Check Frequency",
98
+ value=0.4,
99
+ min=0.0,
100
+ max=1.0,
101
+ step=0.1,
102
+ ),
103
+ "recovery_chance": Slider(
104
+ label="Recovery Chance",
105
+ value=0.3,
106
+ min=0.0,
107
+ max=1.0,
108
+ step=0.1,
109
+ ),
110
+ "gain_resistance_chance": Slider(
111
+ label="Gain Resistance Chance",
112
+ value=0.5,
113
+ min=0.0,
114
+ max=1.0,
115
+ step=0.1,
116
+ ),
117
+ }
118
+
119
+ SpacePlot = make_space_matplotlib(agent_portrayal)
120
+
121
+ model1 = VirusOnNetwork()
122
+
123
+ page = SolaraViz(
124
+ model1,
125
+ [
126
+ SpacePlot,
127
+ make_plot,
128
+ # get_resistant_susceptible_ratio, # TODO: Fix and uncomment
129
+ ],
130
+ model_params=model_params,
131
+ name="Virus Model",
132
+ )
133
+ page # noqa
@@ -0,0 +1,99 @@
1
+ import math
2
+
3
+ import networkx as nx
4
+
5
+ import mesa
6
+ from mesa import Model
7
+
8
+ from .agents import State, VirusAgent
9
+
10
+
11
+ def number_state(model, state):
12
+ return sum(1 for a in model.grid.get_all_cell_contents() if a.state is state)
13
+
14
+
15
+ def number_infected(model):
16
+ return number_state(model, State.INFECTED)
17
+
18
+
19
+ def number_susceptible(model):
20
+ return number_state(model, State.SUSCEPTIBLE)
21
+
22
+
23
+ def number_resistant(model):
24
+ return number_state(model, State.RESISTANT)
25
+
26
+
27
+ class VirusOnNetwork(Model):
28
+ """A virus model with some number of agents."""
29
+
30
+ def __init__(
31
+ self,
32
+ num_nodes=10,
33
+ avg_node_degree=3,
34
+ initial_outbreak_size=1,
35
+ virus_spread_chance=0.4,
36
+ virus_check_frequency=0.4,
37
+ recovery_chance=0.3,
38
+ gain_resistance_chance=0.5,
39
+ ):
40
+ super().__init__()
41
+ self.num_nodes = num_nodes
42
+ prob = avg_node_degree / self.num_nodes
43
+ self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=prob)
44
+ self.grid = mesa.space.NetworkGrid(self.G)
45
+
46
+ self.initial_outbreak_size = (
47
+ initial_outbreak_size if initial_outbreak_size <= num_nodes else num_nodes
48
+ )
49
+ self.virus_spread_chance = virus_spread_chance
50
+ self.virus_check_frequency = virus_check_frequency
51
+ self.recovery_chance = recovery_chance
52
+ self.gain_resistance_chance = gain_resistance_chance
53
+
54
+ self.datacollector = mesa.DataCollector(
55
+ {
56
+ "Infected": number_infected,
57
+ "Susceptible": number_susceptible,
58
+ "Resistant": number_resistant,
59
+ }
60
+ )
61
+
62
+ # Create agents
63
+ for node in self.G.nodes():
64
+ a = VirusAgent(
65
+ self,
66
+ State.SUSCEPTIBLE,
67
+ self.virus_spread_chance,
68
+ self.virus_check_frequency,
69
+ self.recovery_chance,
70
+ self.gain_resistance_chance,
71
+ )
72
+
73
+ # Add the agent to the node
74
+ self.grid.place_agent(a, node)
75
+
76
+ # Infect some nodes
77
+ infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
78
+ for a in self.grid.get_cell_list_contents(infected_nodes):
79
+ a.state = State.INFECTED
80
+
81
+ self.running = True
82
+ self.datacollector.collect(self)
83
+
84
+ def resistant_susceptible_ratio(self):
85
+ try:
86
+ return number_state(self, State.RESISTANT) / number_state(
87
+ self, State.SUSCEPTIBLE
88
+ )
89
+ except ZeroDivisionError:
90
+ return math.inf
91
+
92
+ def step(self):
93
+ self.agents.shuffle_do("step")
94
+ # collect data
95
+ self.datacollector.collect(self)
96
+
97
+ def run_model(self, n):
98
+ for _ in range(n):
99
+ self.step()
mesa/__init__.py CHANGED
@@ -5,6 +5,8 @@ Core Objects: Model, and Agent.
5
5
 
6
6
  import datetime
7
7
 
8
+ import mesa.examples as examples
9
+ import mesa.experimental as experimental
8
10
  import mesa.space as space
9
11
  import mesa.time as time
10
12
  from mesa.agent import Agent
@@ -20,10 +22,11 @@ __all__ = [
20
22
  "DataCollector",
21
23
  "batch_run",
22
24
  "experimental",
25
+ "examples",
23
26
  ]
24
27
 
25
28
  __title__ = "mesa"
26
- __version__ = "3.0.0b0"
29
+ __version__ = "3.0.0b1"
27
30
  __license__ = "Apache 2.0"
28
31
  _this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
29
32
  __copyright__ = f"Copyright {_this_year} Project Mesa Team"
mesa/agent.py CHANGED
@@ -62,9 +62,9 @@ class Agent:
62
62
  super().__init__(*args, **kwargs)
63
63
 
64
64
  self.model: Model = model
65
- self.model.register_agent(self)
66
65
  self.unique_id: int = next(self._ids[model])
67
66
  self.pos: Position | None = None
67
+ self.model.register_agent(self)
68
68
 
69
69
  def remove(self) -> None:
70
70
  """Remove and delete the agent from the model."""
@@ -99,14 +99,18 @@ class AgentSet(MutableSet, Sequence):
99
99
  which means that agents not referenced elsewhere in the program may be automatically removed from the AgentSet.
100
100
  """
101
101
 
102
- def __init__(self, agents: Iterable[Agent], model: Model):
102
+ def __init__(self, agents: Iterable[Agent], random: Random | None = None):
103
103
  """Initializes the AgentSet with a collection of agents and a reference to the model.
104
104
 
105
105
  Args:
106
106
  agents (Iterable[Agent]): An iterable of Agent objects to be included in the set.
107
- model (Model): The ABM model instance to which this AgentSet belongs.
107
+ random (Random): the random number generator
108
108
  """
109
- self.model = model
109
+ if random is None:
110
+ random = (
111
+ Random()
112
+ ) # FIXME see issue 1981, how to get the central rng from model
113
+ self.random = random
110
114
  self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
111
115
 
112
116
  def __len__(self) -> int:
@@ -177,7 +181,7 @@ class AgentSet(MutableSet, Sequence):
177
181
 
178
182
  agents = agent_generator(filter_func, agent_type, at_most)
179
183
 
180
- return AgentSet(agents, self.model) if not inplace else self._update(agents)
184
+ return AgentSet(agents, self.random) if not inplace else self._update(agents)
181
185
 
182
186
  def shuffle(self, inplace: bool = False) -> AgentSet:
183
187
  """Randomly shuffle the order of agents in the AgentSet.
@@ -200,7 +204,7 @@ class AgentSet(MutableSet, Sequence):
200
204
  return self
201
205
  else:
202
206
  return AgentSet(
203
- (agent for ref in weakrefs if (agent := ref()) is not None), self.model
207
+ (agent for ref in weakrefs if (agent := ref()) is not None), self.random
204
208
  )
205
209
 
206
210
  def sort(
@@ -225,7 +229,7 @@ class AgentSet(MutableSet, Sequence):
225
229
  sorted_agents = sorted(self._agents.keys(), key=key, reverse=not ascending)
226
230
 
227
231
  return (
228
- AgentSet(sorted_agents, self.model)
232
+ AgentSet(sorted_agents, self.random)
229
233
  if not inplace
230
234
  else self._update(sorted_agents)
231
235
  )
@@ -477,7 +481,7 @@ class AgentSet(MutableSet, Sequence):
477
481
  Returns:
478
482
  dict: A dictionary representing the state of the AgentSet.
479
483
  """
480
- return {"agents": list(self._agents.keys()), "model": self.model}
484
+ return {"agents": list(self._agents.keys()), "random": self.random}
481
485
 
482
486
  def __setstate__(self, state):
483
487
  """Set the state of the AgentSet during deserialization.
@@ -485,18 +489,9 @@ class AgentSet(MutableSet, Sequence):
485
489
  Args:
486
490
  state (dict): A dictionary representing the state to restore.
487
491
  """
488
- self.model = state["model"]
492
+ self.random = state["random"]
489
493
  self._update(state["agents"])
490
494
 
491
- @property
492
- def random(self) -> Random:
493
- """Provide access to the model's random number generator.
494
-
495
- Returns:
496
- Random: The random number generator associated with the model.
497
- """
498
- return self.model.random
499
-
500
495
  def groupby(self, by: Callable | str, result_type: str = "agentset") -> GroupBy:
501
496
  """Group agents by the specified attribute or return from the callable.
502
497
 
@@ -529,7 +524,7 @@ class AgentSet(MutableSet, Sequence):
529
524
 
530
525
  if result_type == "agentset":
531
526
  return GroupBy(
532
- {k: AgentSet(v, model=self.model) for k, v in groups.items()}
527
+ {k: AgentSet(v, random=self.random) for k, v in groups.items()}
533
528
  )
534
529
  else:
535
530
  return GroupBy(groups)
mesa/examples.py ADDED
@@ -0,0 +1,3 @@
1
+ """This module is a collection of example models built using the Mesa framework."""
2
+
3
+ __path__ = ["examples"]
@@ -2,6 +2,12 @@
2
2
 
3
3
  from mesa.experimental import cell_space
4
4
 
5
- from .solara_viz import JupyterViz, Slider, SolaraViz, make_text
5
+ try:
6
+ from .solara_viz import JupyterViz, Slider, SolaraViz, make_text
6
7
 
7
- __all__ = ["cell_space", "JupyterViz", "SolaraViz", "make_text", "Slider"]
8
+ __all__ = ["cell_space", "JupyterViz", "Slider", "SolaraViz", "make_text"]
9
+ except ImportError:
10
+ print(
11
+ "Could not import SolaraViz. If you need it, install with 'pip install --pre mesa[viz]'"
12
+ )
13
+ __all__ = ["cell_space"]
@@ -211,3 +211,12 @@ class Cell:
211
211
  self._mesa_property_layers[property_name].modify_cell(
212
212
  self.coordinate, operation, value
213
213
  )
214
+
215
+ def __getstate__(self):
216
+ """Return state of the Cell with connections set to empty."""
217
+ # fixme, once we shift to 3.11, replace this with super. __getstate__
218
+ state = (self.__dict__, {k: getattr(self, k) for k in self.__slots__})
219
+ state[1][
220
+ "connections"
221
+ ] = {} # replace this with empty connections to avoid infinite recursion error in pickle/deepcopy
222
+ return state
@@ -22,7 +22,7 @@ class DiscreteSpace(Generic[T]):
22
22
  all_cells (CellCollection): The cells composing the discrete space
23
23
  random (Random): The random number generator
24
24
  cell_klass (Type) : the type of cell class
25
- empties (CellCollection) : collecction of all cells that are empty
25
+ empties (CellCollection) : collection of all cells that are empty
26
26
  property_layers (dict[str, PropertyLayer]): the property layers of the discrete space
27
27
  """
28
28
 
@@ -55,6 +55,7 @@ class DiscreteSpace(Generic[T]):
55
55
  def cutoff_empties(self): # noqa
56
56
  return 7.953 * len(self._cells) ** 0.384
57
57
 
58
+ def _connect_cells(self): ...
58
59
  def _connect_single_cell(self, cell: T): ...
59
60
 
60
61
  @cached_property
@@ -134,3 +135,8 @@ class DiscreteSpace(Generic[T]):
134
135
  condition: a function that takes a cell and returns a boolean (used to filter cells)
135
136
  """
136
137
  self.property_layers[property_name].modify_cells(operation, value, condition)
138
+
139
+ def __setstate__(self, state):
140
+ """Set the state of the discrete space and rebuild the connections."""
141
+ self.__dict__ = state
142
+ self._connect_cells()
@@ -22,8 +22,21 @@ class Grid(DiscreteSpace[T], Generic[T]):
22
22
  random (Random): the random number generator
23
23
  _try_random (bool): whether to get empty cell be repeatedly trying random cell
24
24
 
25
+ Notes:
26
+ width and height are accessible via properties, higher dimensions can be retrieved via dimensions
27
+
25
28
  """
26
29
 
30
+ @property
31
+ def width(self) -> int:
32
+ """Convenience access to the width of the grid."""
33
+ return self.dimensions[0]
34
+
35
+ @property
36
+ def height(self) -> int:
37
+ """Convenience access to the height of the grid."""
38
+ return self.dimensions[1]
39
+
27
40
  def __init__(
28
41
  self,
29
42
  dimensions: Sequence[int],
@@ -34,6 +34,9 @@ class Network(DiscreteSpace[Cell]):
34
34
  node_id, capacity, random=self.random
35
35
  )
36
36
 
37
+ self._connect_cells()
38
+
39
+ def _connect_cells(self) -> None:
37
40
  for cell in self.all_cells:
38
41
  self._connect_single_cell(cell)
39
42
 
mesa/model.py CHANGED
@@ -8,14 +8,21 @@ Core Objects: Model
8
8
  from __future__ import annotations
9
9
 
10
10
  import random
11
+ import sys
11
12
  import warnings
13
+ from collections.abc import Sequence
12
14
 
13
15
  # mypy
14
16
  from typing import Any
15
17
 
18
+ import numpy as np
19
+
16
20
  from mesa.agent import Agent, AgentSet
17
21
  from mesa.datacollection import DataCollector
18
22
 
23
+ SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence
24
+ RNGLike = np.random.Generator | np.random.BitGenerator
25
+
19
26
 
20
27
  class Model:
21
28
  """Base class for models in the Mesa ABM library.
@@ -28,7 +35,8 @@ class Model:
28
35
  running: A boolean indicating if the model should continue running.
29
36
  schedule: An object to manage the order and execution of agent steps.
30
37
  steps: the number of times `model.step()` has been called.
31
- random: a seeded random number generator.
38
+ random: a seeded python.random number generator.
39
+ rng : a seeded numpy.random.Generator
32
40
 
33
41
  Notes:
34
42
  Model.agents returns the AgentSet containing all agents registered with the model. Changing
@@ -37,7 +45,13 @@ class Model:
37
45
 
38
46
  """
39
47
 
40
- def __init__(self, *args: Any, seed: float | None = None, **kwargs: Any) -> None:
48
+ def __init__(
49
+ self,
50
+ *args: Any,
51
+ seed: float | None = None,
52
+ rng: RNGLike | SeedLike | None = None,
53
+ **kwargs: Any,
54
+ ) -> None:
41
55
  """Create a new model.
42
56
 
43
57
  Overload this method with the actual code to initialize the model. Always start with super().__init__()
@@ -46,25 +60,51 @@ class Model:
46
60
  Args:
47
61
  args: arguments to pass onto super
48
62
  seed: the seed for the random number generator
63
+ rng : Pseudorandom number generator state. When `rng` is None, a new `numpy.random.Generator` is created
64
+ using entropy from the operating system. Types other than `numpy.random.Generator` are passed to
65
+ `numpy.random.default_rng` to instantiate a `Generator`.
49
66
  kwargs: keyword arguments to pass onto super
67
+
68
+ Notes:
69
+ you have to pass either seed or rng, but not both.
70
+
50
71
  """
51
72
  super().__init__(*args, **kwargs)
52
73
  self.running = True
53
74
  self.steps: int = 0
54
75
 
55
- self._setup_agent_registration()
56
-
57
- self._seed = seed
58
- if self._seed is None:
59
- # We explicitly specify the seed here so that we know its value in
60
- # advance.
61
- self._seed = random.random()
62
- self.random = random.Random(self._seed)
76
+ if (seed is not None) and (rng is not None):
77
+ raise ValueError("you have to pass either rng or seed, not both")
78
+ elif seed is None:
79
+ self.rng: np.random.Generator = np.random.default_rng(rng)
80
+ self._rng = (
81
+ self.rng.bit_generator.state
82
+ ) # this allows for reproducing the rng
83
+
84
+ try:
85
+ self.random = random.Random(rng)
86
+ except TypeError:
87
+ seed = int(self.rng.integers(np.iinfo(np.int32).max))
88
+ self.random = random.Random(seed)
89
+ self._seed = seed # this allows for reproducing stdlib.random
90
+ elif rng is None:
91
+ self.random = random.Random(seed)
92
+ self._seed = seed # this allows for reproducing stdlib.random
93
+
94
+ try:
95
+ self.rng: np.random.Generator = np.random.default_rng(rng)
96
+ except TypeError:
97
+ rng = self.random.randint(0, sys.maxsize)
98
+ self.rng: np.random.Generator = np.random.default_rng(rng)
99
+ self._rng = self.rng.bit_generator.state
63
100
 
64
101
  # Wrap the user-defined step method
65
102
  self._user_step = self.step
66
103
  self.step = self._wrapped_step
67
104
 
105
+ # setup agent registration data structures
106
+ self._setup_agent_registration()
107
+
68
108
  def _wrapped_step(self, *args: Any, **kwargs: Any) -> None:
69
109
  """Automatically increments time and steps after calling the user's step method."""
70
110
  # Automatically increment time and step counters
@@ -119,7 +159,9 @@ class Model:
119
159
  self._agents_by_type: dict[
120
160
  type[Agent], AgentSet
121
161
  ] = {} # a dict with an agentset for each class of agents
122
- self._all_agents = AgentSet([], self) # an agenset with all agents
162
+ self._all_agents = AgentSet(
163
+ [], random=self.random
164
+ ) # an agenset with all agents
123
165
 
124
166
  def register_agent(self, agent):
125
167
  """Register the agent with the model.
@@ -153,7 +195,7 @@ class Model:
153
195
  [
154
196
  agent,
155
197
  ],
156
- self,
198
+ random=self.random,
157
199
  )
158
200
 
159
201
  self._all_agents.add(agent)
@@ -194,6 +236,15 @@ class Model:
194
236
  self.random.seed(seed)
195
237
  self._seed = seed
196
238
 
239
+ def reset_rng(self, rng: RNGLike | SeedLike | None = None) -> None:
240
+ """Reset the model random number generator.
241
+
242
+ Args:
243
+ rng: A new seed for the RNG; if None, reset using the current seed
244
+ """
245
+ self.rng = np.random.default_rng(rng)
246
+ self._rng = self.rng.bit_generator.state
247
+
197
248
  def initialize_data_collector(
198
249
  self,
199
250
  model_reporters=None,
mesa/time.py CHANGED
@@ -77,7 +77,7 @@ class BaseScheduler:
77
77
  if agents is None:
78
78
  agents = []
79
79
 
80
- self._agents: AgentSet = AgentSet(agents, model)
80
+ self._agents: AgentSet = AgentSet(agents, model.random)
81
81
 
82
82
  self._remove_warning_given = False
83
83
  self._agents_key_warning_given = False
@@ -312,7 +312,9 @@ class RandomActivationByType(BaseScheduler):
312
312
  try:
313
313
  self._agents_by_type[type(agent)].add(agent)
314
314
  except KeyError:
315
- self._agents_by_type[type(agent)] = AgentSet([agent], self.model)
315
+ self._agents_by_type[type(agent)] = AgentSet(
316
+ [agent], self.model.random
317
+ )
316
318
 
317
319
  def add(self, agent: Agent) -> None:
318
320
  """Add an Agent object to the schedule.
@@ -325,7 +327,7 @@ class RandomActivationByType(BaseScheduler):
325
327
  try:
326
328
  self._agents_by_type[type(agent)].add(agent)
327
329
  except KeyError:
328
- self._agents_by_type[type(agent)] = AgentSet([agent], self.model)
330
+ self._agents_by_type[type(agent)] = AgentSet([agent], self.model.random)
329
331
 
330
332
  def remove(self, agent: Agent) -> None:
331
333
  """Remove all instances of a given agent from the schedule.