Mesa 3.0.0a5__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 (91) 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 +24 -43
  71. mesa/batchrunner.py +7 -0
  72. mesa/examples.py +3 -0
  73. mesa/experimental/__init__.py +8 -2
  74. mesa/experimental/cell_space/__init__.py +7 -1
  75. mesa/experimental/cell_space/cell.py +35 -6
  76. mesa/experimental/cell_space/cell_agent.py +114 -23
  77. mesa/experimental/cell_space/discrete_space.py +70 -3
  78. mesa/experimental/cell_space/grid.py +13 -0
  79. mesa/experimental/cell_space/network.py +3 -0
  80. mesa/experimental/devs/examples/wolf_sheep.py +2 -1
  81. mesa/model.py +71 -21
  82. mesa/time.py +7 -5
  83. mesa/visualization/components/matplotlib.py +184 -90
  84. mesa/visualization/solara_viz.py +25 -61
  85. {mesa-3.0.0a5.dist-info → mesa-3.0.0b1.dist-info}/METADATA +55 -13
  86. mesa-3.0.0b1.dist-info/RECORD +114 -0
  87. mesa-3.0.0b1.dist-info/licenses/LICENSE +202 -0
  88. mesa-3.0.0a5.dist-info/licenses/LICENSE → mesa-3.0.0b1.dist-info/licenses/NOTICE +2 -2
  89. mesa-3.0.0a5.dist-info/RECORD +0 -44
  90. {mesa-3.0.0a5.dist-info → mesa-3.0.0b1.dist-info}/WHEEL +0 -0
  91. {mesa-3.0.0a5.dist-info → mesa-3.0.0b1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,321 @@
1
+ import math
2
+
3
+ from mesa.experimental.cell_space import CellAgent
4
+
5
+ from .resource_agents import Resource
6
+
7
+
8
+ # Helper function
9
+ def get_distance(cell_1, cell_2):
10
+ """
11
+ Calculate the Euclidean distance between two positions
12
+
13
+ used in trade.move()
14
+ """
15
+
16
+ x1, y1 = cell_1.coordinate
17
+ x2, y2 = cell_2.coordinate
18
+ dx = x1 - x2
19
+ dy = y1 - y2
20
+ return math.sqrt(dx**2 + dy**2)
21
+
22
+
23
+ class Trader(CellAgent):
24
+ """
25
+ Trader:
26
+ - has a metabolism of sugar and spice
27
+ - harvest and trade sugar and spice to survive
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ model,
33
+ cell,
34
+ sugar=0,
35
+ spice=0,
36
+ metabolism_sugar=0,
37
+ metabolism_spice=0,
38
+ vision=0,
39
+ ):
40
+ super().__init__(model)
41
+ self.cell = cell
42
+ self.sugar = sugar
43
+ self.spice = spice
44
+ self.metabolism_sugar = metabolism_sugar
45
+ self.metabolism_spice = metabolism_spice
46
+ self.vision = vision
47
+ self.prices = []
48
+ self.trade_partners = []
49
+
50
+ def get_resource(self, cell):
51
+ for agent in cell.agents:
52
+ if isinstance(agent, Resource):
53
+ return agent
54
+ raise Exception(f"Resource agent not found in the position {cell.coordinate}")
55
+
56
+ def get_trader(self, cell):
57
+ """
58
+ helper function used in self.trade_with_neighbors()
59
+ """
60
+
61
+ for agent in cell.agents:
62
+ if isinstance(agent, Trader):
63
+ return agent
64
+
65
+ def is_occupied_by_other(self, cell):
66
+ """
67
+ helper function part 1 of self.move()
68
+ """
69
+
70
+ if cell is self.cell:
71
+ # agent's position is considered unoccupied as agent can stay there
72
+ return False
73
+ # get contents of each cell in neighborhood
74
+ return any(isinstance(a, Trader) for a in cell.agents)
75
+
76
+ def calculate_welfare(self, sugar, spice):
77
+ """
78
+ helper function
79
+
80
+ part 2 self.move()
81
+ self.trade()
82
+ """
83
+
84
+ # calculate total resources
85
+ m_total = self.metabolism_sugar + self.metabolism_spice
86
+ # Cobb-Douglas functional form; starting on p. 97
87
+ # on Growing Artificial Societies
88
+ return sugar ** (self.metabolism_sugar / m_total) * spice ** (
89
+ self.metabolism_spice / m_total
90
+ )
91
+
92
+ def is_starved(self):
93
+ """
94
+ Helper function for self.maybe_die()
95
+ """
96
+
97
+ return (self.sugar <= 0) or (self.spice <= 0)
98
+
99
+ def calculate_MRS(self, sugar, spice):
100
+ """
101
+ Helper function for
102
+ - self.trade()
103
+ - self.maybe_self_spice()
104
+
105
+ Determines what trader agent needs and can give up
106
+ """
107
+
108
+ return (spice / self.metabolism_spice) / (sugar / self.metabolism_sugar)
109
+
110
+ def calculate_sell_spice_amount(self, price):
111
+ """
112
+ helper function for self.maybe_sell_spice() which is called from
113
+ self.trade()
114
+ """
115
+
116
+ if price >= 1:
117
+ sugar = 1
118
+ spice = int(price)
119
+ else:
120
+ sugar = int(1 / price)
121
+ spice = 1
122
+ return sugar, spice
123
+
124
+ def sell_spice(self, other, sugar, spice):
125
+ """
126
+ used in self.maybe_sell_spice()
127
+
128
+ exchanges sugar and spice between traders
129
+ """
130
+
131
+ self.sugar += sugar
132
+ other.sugar -= sugar
133
+ self.spice -= spice
134
+ other.spice += spice
135
+
136
+ def maybe_sell_spice(self, other, price, welfare_self, welfare_other):
137
+ """
138
+ helper function for self.trade()
139
+ """
140
+
141
+ sugar_exchanged, spice_exchanged = self.calculate_sell_spice_amount(price)
142
+
143
+ # Assess new sugar and spice amount - what if change did occur
144
+ self_sugar = self.sugar + sugar_exchanged
145
+ other_sugar = other.sugar - sugar_exchanged
146
+ self_spice = self.spice - spice_exchanged
147
+ other_spice = other.spice + spice_exchanged
148
+
149
+ # double check to ensure agents have resources
150
+
151
+ if (
152
+ (self_sugar <= 0)
153
+ or (other_sugar <= 0)
154
+ or (self_spice <= 0)
155
+ or (other_spice <= 0)
156
+ ):
157
+ return False
158
+
159
+ # trade criteria #1 - are both agents better off?
160
+ both_agents_better_off = (
161
+ welfare_self < self.calculate_welfare(self_sugar, self_spice)
162
+ ) and (welfare_other < other.calculate_welfare(other_sugar, other_spice))
163
+
164
+ # trade criteria #2 is their mrs crossing with potential trade
165
+ mrs_not_crossing = self.calculate_MRS(
166
+ self_sugar, self_spice
167
+ ) > other.calculate_MRS(other_sugar, other_spice)
168
+
169
+ if not (both_agents_better_off and mrs_not_crossing):
170
+ return False
171
+
172
+ # criteria met, execute trade
173
+ self.sell_spice(other, sugar_exchanged, spice_exchanged)
174
+
175
+ return True
176
+
177
+ def trade(self, other):
178
+ """
179
+ helper function used in trade_with_neighbors()
180
+
181
+ other is a trader agent object
182
+ """
183
+
184
+ # sanity check to verify code is working as expected
185
+ assert self.sugar > 0
186
+ assert self.spice > 0
187
+ assert other.sugar > 0
188
+ assert other.spice > 0
189
+
190
+ # calculate marginal rate of substitution in Growing Artificial Societies p. 101
191
+ mrs_self = self.calculate_MRS(self.sugar, self.spice)
192
+ mrs_other = other.calculate_MRS(other.sugar, other.spice)
193
+
194
+ # calculate each agents welfare
195
+ welfare_self = self.calculate_welfare(self.sugar, self.spice)
196
+ welfare_other = other.calculate_welfare(other.sugar, other.spice)
197
+
198
+ if math.isclose(mrs_self, mrs_other):
199
+ return
200
+
201
+ # calculate price
202
+ price = math.sqrt(mrs_self * mrs_other)
203
+
204
+ if mrs_self > mrs_other:
205
+ # self is a sugar buyer, spice seller
206
+ sold = self.maybe_sell_spice(other, price, welfare_self, welfare_other)
207
+ # no trade - criteria not met
208
+ if not sold:
209
+ return
210
+ else:
211
+ # self is a spice buyer, sugar seller
212
+ sold = other.maybe_sell_spice(self, price, welfare_other, welfare_self)
213
+ # no trade - criteria not met
214
+ if not sold:
215
+ return
216
+
217
+ # Capture data
218
+ self.prices.append(price)
219
+ self.trade_partners.append(other.unique_id)
220
+
221
+ # continue trading
222
+ self.trade(other)
223
+
224
+ ######################################################################
225
+ # #
226
+ # MAIN TRADE FUNCTIONS #
227
+ # #
228
+ ######################################################################
229
+
230
+ def move(self):
231
+ """
232
+ Function for trader agent to identify optimal move for each step in 4 parts
233
+ 1 - identify all possible moves
234
+ 2 - determine which move maximizes welfare
235
+ 3 - find closest best option
236
+ 4 - move
237
+ """
238
+
239
+ # 1. identify all possible moves
240
+
241
+ neighboring_cells = [
242
+ cell
243
+ for cell in self.cell.get_neighborhood(self.vision, include_center=True)
244
+ if not self.is_occupied_by_other(cell)
245
+ ]
246
+
247
+ # 2. determine which move maximizes welfare
248
+
249
+ welfares = [
250
+ self.calculate_welfare(
251
+ self.sugar + self.get_resource(cell).sugar_amount,
252
+ self.spice + self.get_resource(cell).spice_amount,
253
+ )
254
+ for cell in neighboring_cells
255
+ ]
256
+
257
+ # 3. Find closest best option
258
+
259
+ # find the highest welfare in welfares
260
+ max_welfare = max(welfares)
261
+ # get the index of max welfare cells
262
+ candidate_indices = [
263
+ i for i in range(len(welfares)) if math.isclose(welfares[i], max_welfare)
264
+ ]
265
+
266
+ # convert index to positions of those cells
267
+ candidates = [neighboring_cells[i] for i in candidate_indices]
268
+
269
+ min_dist = min(get_distance(self.cell, cell) for cell in candidates)
270
+
271
+ final_candidates = [
272
+ cell
273
+ for cell in candidates
274
+ if math.isclose(get_distance(self.cell, cell), min_dist, rel_tol=1e-02)
275
+ ]
276
+ # 4. Move Agent
277
+ self.cell = self.random.choice(final_candidates)
278
+
279
+ def eat(self):
280
+ patch = self.get_resource(self.cell)
281
+ if patch.sugar_amount > 0:
282
+ self.sugar += patch.sugar_amount
283
+ patch.sugar_amount = 0
284
+ self.sugar -= self.metabolism_sugar
285
+
286
+ if patch.spice_amount > 0:
287
+ self.spice += patch.spice_amount
288
+ patch.spice_amount = 0
289
+ self.spice -= self.metabolism_spice
290
+
291
+ def maybe_die(self):
292
+ """
293
+ Function to remove Traders who have consumed all their sugar or spice
294
+ """
295
+
296
+ if self.is_starved():
297
+ self.remove()
298
+
299
+ def trade_with_neighbors(self):
300
+ """
301
+ Function for trader agents to decide who to trade with in three parts
302
+
303
+ 1- identify neighbors who can trade
304
+ 2- trade (2 sessions)
305
+ 3- collect data
306
+ """
307
+
308
+ neighbor_agents = [
309
+ self.get_trader(cell)
310
+ for cell in self.cell.get_neighborhood(radius=self.vision)
311
+ if self.is_occupied_by_other(cell)
312
+ ]
313
+
314
+ if len(neighbor_agents) == 0:
315
+ return
316
+
317
+ # iterate through traders in neighboring cells and trade
318
+ for a in neighbor_agents:
319
+ self.trade(a)
320
+
321
+ return
@@ -0,0 +1,72 @@
1
+ import random
2
+
3
+ import numpy as np
4
+ from scipy import stats
5
+ from sugarscape_g1mt.model import SugarscapeG1mt, flatten
6
+ from sugarscape_g1mt.trader_agents import Trader
7
+
8
+ random.seed(1)
9
+
10
+
11
+ def check_slope(y, increasing):
12
+ x = range(len(y))
13
+ slope, intercept, _, p_value, _ = stats.linregress(x, y)
14
+ result = (slope > 0) if increasing else (slope < 0)
15
+ # p_value for significance.
16
+ assert result and p_value < 0.05, (slope, p_value)
17
+
18
+
19
+ def test_decreasing_price_variance():
20
+ # The variance of the average trade price should decrease over time (figure IV-3)
21
+ # See Growing Artificial Societies p. 109.
22
+ model = SugarscapeG1mt()
23
+ model.datacollector._new_model_reporter(
24
+ "price_variance",
25
+ lambda m: np.var(
26
+ flatten([a.prices for a in m.agents_by_type[Trader].values()])
27
+ ),
28
+ )
29
+ model.run_model(step_count=50)
30
+
31
+ df_model = model.datacollector.get_model_vars_dataframe()
32
+
33
+ check_slope(df_model.price_variance, increasing=False)
34
+
35
+
36
+ def test_carrying_capacity():
37
+ def calculate_carrying_capacities(enable_trade):
38
+ carrying_capacities = []
39
+ visions = range(1, 10)
40
+ for vision_max in visions:
41
+ model = SugarscapeG1mt(vision_max=vision_max, enable_trade=enable_trade)
42
+ model.run_model(step_count=50)
43
+ carrying_capacities.append(len(model.agents_by_type[Trader]))
44
+ return carrying_capacities
45
+
46
+ # Carrying capacity should increase over mean vision (figure IV-6).
47
+ # See Growing Artificial Societies p. 112.
48
+ carrying_capacities_with_trade = calculate_carrying_capacities(True)
49
+ check_slope(
50
+ carrying_capacities_with_trade,
51
+ increasing=True,
52
+ )
53
+ # Carrying capacity should be higher when trade is enabled (figure IV-6).
54
+ carrying_capacities_no_trade = calculate_carrying_capacities(False)
55
+ check_slope(
56
+ carrying_capacities_no_trade,
57
+ increasing=True,
58
+ )
59
+
60
+ t_statistic, p_value = stats.ttest_rel(
61
+ carrying_capacities_with_trade, carrying_capacities_no_trade
62
+ )
63
+ # t_statistic > 0 means carrying_capacities_with_trade has larger values
64
+ # than carrying_capacities_no_trade.
65
+ # p_value for significance.
66
+ assert t_statistic > 0 and p_value < 0.05
67
+
68
+
69
+ # TODO:
70
+ # 1. Reproduce figure IV-12 that the log of average price should decrease over average agent age
71
+ # 2. Reproduce figure IV-13 that the gini coefficient on trade should decrease over mean vision, and should be higher with trade
72
+ # 3. a stricter test would be to ensure the amount of variance of the trade price matches figure IV-3
@@ -0,0 +1,57 @@
1
+ # Wolf-Sheep Predation Model
2
+
3
+ ## Summary
4
+
5
+ A simple ecological model, consisting of three agent types: wolves, sheep, and grass. The wolves and the sheep wander around the grid at random. Wolves and sheep both expend energy moving around, and replenish it by eating. Sheep eat grass, and wolves eat sheep if they end up on the same grid cell.
6
+
7
+ If wolves and sheep have enough energy, they reproduce, creating a new wolf or sheep (in this simplified model, only one parent is needed for reproduction). The grass on each cell regrows at a constant rate. If any wolves and sheep run out of energy, they die.
8
+
9
+ The model is tests and demonstrates several Mesa concepts and features:
10
+ - MultiGrid
11
+ - Multiple agent types (wolves, sheep, grass)
12
+ - Overlay arbitrary text (wolf's energy) on agent's shapes while drawing on CanvasGrid
13
+ - Agents inheriting a behavior (random movement) from an abstract parent
14
+ - Writing a model composed of multiple files.
15
+ - Dynamically adding and removing agents from the schedule
16
+
17
+ ## Installation
18
+
19
+ To install the dependencies use pip and the requirements.txt in this directory. e.g.
20
+
21
+ ```
22
+ # First, we clone the Mesa repo
23
+ $ git clone https://github.com/projectmesa/mesa.git
24
+ $ cd mesa
25
+ # Then we cd to the example directory
26
+ $ cd examples/wolf_sheep
27
+ $ pip install -r requirements.txt
28
+ ```
29
+
30
+ ## How to Run
31
+
32
+ To run the model interactively, run ``mesa runserver`` in this directory. e.g.
33
+
34
+ ```
35
+ $ mesa runserver
36
+ ```
37
+
38
+ Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run.
39
+
40
+ ## Files
41
+
42
+ * ``wolf_sheep/random_walk.py``: This defines the ``RandomWalker`` agent, which implements the behavior of moving randomly across a grid, one cell at a time. Both the Wolf and Sheep agents will inherit from it.
43
+ * ``wolf_sheep/test_random_walk.py``: Defines a simple model and a text-only visualization intended to make sure the RandomWalk class was working as expected. This doesn't actually model anything, but serves as an ad-hoc unit test. To run it, ``cd`` into the ``wolf_sheep`` directory and run ``python test_random_walk.py``. You'll see a series of ASCII grids, one per model step, with each cell showing a count of the number of agents in it.
44
+ * ``wolf_sheep/agents.py``: Defines the Wolf, Sheep, and GrassPatch agent classes.
45
+ * ``wolf_sheep/scheduler.py``: Defines a custom variant on the RandomActivationByType scheduler, where we can define filters for the `get_type_count` function.
46
+ * ``wolf_sheep/model.py``: Defines the Wolf-Sheep Predation model itself
47
+ * ``wolf_sheep/server.py``: Sets up the interactive visualization server
48
+ * ``run.py``: Launches a model visualization server.
49
+
50
+ ## Further Reading
51
+
52
+ This model is closely based on the NetLogo Wolf-Sheep Predation Model:
53
+
54
+ Wilensky, U. (1997). NetLogo Wolf Sheep Predation model. http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation. Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL.
55
+
56
+ See also the [Lotka–Volterra equations
57
+ ](https://en.wikipedia.org/wiki/Lotka%E2%80%93Volterra_equations) for an example of a classic differential-equation model with similar dynamics.
File without changes
@@ -0,0 +1 @@
1
+ mesa~=2.0
@@ -0,0 +1,3 @@
1
+ from wolf_sheep.server import server
2
+
3
+ server.launch(open_browser=True)
File without changes
@@ -0,0 +1,102 @@
1
+ from mesa.experimental.cell_space import CellAgent, FixedAgent
2
+
3
+
4
+ class Animal(CellAgent):
5
+ """The base animal class."""
6
+
7
+ def __init__(self, model, energy, p_reproduce, energy_from_food, cell):
8
+ """Initializes an animal.
9
+
10
+ Args:
11
+ model: a model instance
12
+ energy: starting amount of energy
13
+ p_reproduce: probability of sexless reproduction
14
+ energy_from_food: energy obtained from 1 unit of food
15
+ cell: the cell in which the animal starts
16
+ """
17
+ super().__init__(model)
18
+ self.energy = energy
19
+ self.p_reproduce = p_reproduce
20
+ self.energy_from_food = energy_from_food
21
+ self.cell = cell
22
+
23
+ def spawn_offspring(self):
24
+ """Create offspring."""
25
+ self.energy /= 2
26
+ self.__class__(
27
+ self.model,
28
+ self.energy,
29
+ self.p_reproduce,
30
+ self.energy_from_food,
31
+ self.cell,
32
+ )
33
+
34
+ def feed(self): ...
35
+
36
+ def step(self):
37
+ """One step of the agent."""
38
+ self.cell = self.cell.neighborhood.select_random_cell()
39
+ self.energy -= 1
40
+
41
+ self.feed()
42
+
43
+ if self.energy < 0:
44
+ self.remove()
45
+ elif self.random.random() < self.p_reproduce:
46
+ self.spawn_offspring()
47
+
48
+
49
+ class Sheep(Animal):
50
+ """A sheep that walks around, reproduces (asexually) and gets eaten."""
51
+
52
+ def feed(self):
53
+ """If possible eat the food in the current location."""
54
+ # If there is grass available, eat it
55
+ if self.model.grass:
56
+ grass_patch = next(
57
+ obj for obj in self.cell.agents if isinstance(obj, GrassPatch)
58
+ )
59
+ if grass_patch.fully_grown:
60
+ self.energy += self.energy_from_food
61
+ grass_patch.fully_grown = False
62
+
63
+
64
+ class Wolf(Animal):
65
+ """A wolf that walks around, reproduces (asexually) and eats sheep."""
66
+
67
+ def feed(self):
68
+ """If possible eat the food in the current location."""
69
+ sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)]
70
+ if len(sheep) > 0:
71
+ sheep_to_eat = self.random.choice(sheep)
72
+ self.energy += self.energy_from_food
73
+
74
+ # Kill the sheep
75
+ sheep_to_eat.remove()
76
+
77
+
78
+ class GrassPatch(FixedAgent):
79
+ """
80
+ A patch of grass that grows at a fixed rate and it is eaten by sheep
81
+ """
82
+
83
+ def __init__(self, model, fully_grown, countdown):
84
+ """
85
+ Creates a new patch of grass
86
+
87
+ Args:
88
+ grown: (boolean) Whether the patch of grass is fully grown or not
89
+ countdown: Time for the patch of grass to be fully grown again
90
+ """
91
+ super().__init__(model)
92
+ self.fully_grown = fully_grown
93
+ self.countdown = countdown
94
+
95
+ def step(self):
96
+ if not self.fully_grown:
97
+ if self.countdown <= 0:
98
+ # Set as fully grown
99
+ self.fully_grown = True
100
+ self.countdown = self.model.grass_regrowth_time
101
+ else:
102
+ self.countdown -= 1
@@ -0,0 +1,136 @@
1
+ """
2
+ Wolf-Sheep Predation Model
3
+ ================================
4
+
5
+ Replication of the model found in NetLogo:
6
+ Wilensky, U. (1997). NetLogo Wolf Sheep Predation model.
7
+ http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation.
8
+ Center for Connected Learning and Computer-Based Modeling,
9
+ Northwestern University, Evanston, IL.
10
+ """
11
+
12
+ import mesa
13
+ from mesa.experimental.cell_space import OrthogonalMooreGrid
14
+
15
+ from .agents import GrassPatch, Sheep, Wolf
16
+
17
+
18
+ class WolfSheep(mesa.Model):
19
+ """
20
+ Wolf-Sheep Predation Model
21
+ """
22
+
23
+ height = 20
24
+ width = 20
25
+
26
+ initial_sheep = 100
27
+ initial_wolves = 50
28
+
29
+ sheep_reproduce = 0.04
30
+ wolf_reproduce = 0.05
31
+
32
+ wolf_gain_from_food = 20
33
+
34
+ grass = False
35
+ grass_regrowth_time = 30
36
+ sheep_gain_from_food = 4
37
+
38
+ description = (
39
+ "A model for simulating wolf and sheep (predator-prey) ecosystem modelling."
40
+ )
41
+
42
+ def __init__(
43
+ self,
44
+ width=20,
45
+ height=20,
46
+ initial_sheep=100,
47
+ initial_wolves=50,
48
+ sheep_reproduce=0.04,
49
+ wolf_reproduce=0.05,
50
+ wolf_gain_from_food=20,
51
+ grass=False,
52
+ grass_regrowth_time=30,
53
+ sheep_gain_from_food=4,
54
+ seed=None,
55
+ ):
56
+ """
57
+ Create a new Wolf-Sheep model with the given parameters.
58
+
59
+ Args:
60
+ initial_sheep: Number of sheep to start with
61
+ initial_wolves: Number of wolves to start with
62
+ sheep_reproduce: Probability of each sheep reproducing each step
63
+ wolf_reproduce: Probability of each wolf reproducing each step
64
+ wolf_gain_from_food: Energy a wolf gains from eating a sheep
65
+ grass: Whether to have the sheep eat grass for energy
66
+ grass_regrowth_time: How long it takes for a grass patch to regrow
67
+ once it is eaten
68
+ sheep_gain_from_food: Energy sheep gain from grass, if enabled.
69
+ """
70
+ super().__init__(seed=None)
71
+ # Set parameters
72
+ self.width = width
73
+ self.height = height
74
+ self.initial_sheep = initial_sheep
75
+ self.initial_wolves = initial_wolves
76
+ self.grass = grass
77
+ self.grass_regrowth_time = grass_regrowth_time
78
+
79
+ self.grid = OrthogonalMooreGrid((self.width, self.height), torus=True)
80
+
81
+ collectors = {
82
+ "Wolves": lambda m: len(m.agents_by_type[Wolf]),
83
+ "Sheep": lambda m: len(m.agents_by_type[Sheep]),
84
+ }
85
+
86
+ if grass:
87
+ collectors["Grass"] = lambda m: len(m.agents_by_type[GrassPatch])
88
+
89
+ self.datacollector = mesa.DataCollector(collectors)
90
+
91
+ # Create sheep:
92
+ for i in range(self.initial_sheep):
93
+ x = self.random.randrange(self.width)
94
+ y = self.random.randrange(self.height)
95
+ energy = self.random.randrange(2 * self.sheep_gain_from_food)
96
+ Sheep(
97
+ self, energy, sheep_reproduce, sheep_gain_from_food, self.grid[(x, y)]
98
+ )
99
+
100
+ # Create wolves
101
+ for _ in range(self.initial_wolves):
102
+ x = self.random.randrange(self.width)
103
+ y = self.random.randrange(self.height)
104
+ energy = self.random.randrange(2 * self.wolf_gain_from_food)
105
+ Wolf(self, energy, wolf_reproduce, wolf_gain_from_food, self.grid[(x, y)])
106
+
107
+ # Create grass patches
108
+ if self.grass:
109
+ for cell in self.grid.all_cells:
110
+ fully_grown = self.random.choice([True, False])
111
+
112
+ if fully_grown:
113
+ countdown = self.grass_regrowth_time
114
+ else:
115
+ countdown = self.random.randrange(self.grass_regrowth_time)
116
+
117
+ patch = GrassPatch(self, fully_grown, countdown)
118
+ patch.cell = cell
119
+
120
+ self.running = True
121
+ self.datacollector.collect(self)
122
+
123
+ def step(self):
124
+ # This replicated the behavior of the old RandomActivationByType scheduler
125
+ # when using step(shuffle_types=True, shuffle_agents=True).
126
+ # Conceptually, it can be argued that this should be modelled differently.
127
+ self.random.shuffle(self.agent_types)
128
+ for agent_type in self.agent_types:
129
+ self.agents_by_type[agent_type].shuffle_do("step")
130
+
131
+ # collect data
132
+ self.datacollector.collect(self)
133
+
134
+ def run_model(self, step_count=200):
135
+ for i in range(step_count):
136
+ self.step()