Mesa 3.0.0rc0__py3-none-any.whl → 3.0.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 (41) hide show
  1. mesa/__init__.py +1 -1
  2. mesa/agent.py +15 -3
  3. mesa/examples/__init__.py +2 -2
  4. mesa/examples/advanced/epstein_civil_violence/Readme.md +2 -4
  5. mesa/examples/advanced/pd_grid/app.py +1 -1
  6. mesa/examples/advanced/pd_grid/model.py +1 -1
  7. mesa/examples/advanced/sugarscape_g1mt/app.py +18 -2
  8. mesa/examples/advanced/sugarscape_g1mt/model.py +3 -1
  9. mesa/examples/advanced/wolf_sheep/agents.py +53 -39
  10. mesa/examples/advanced/wolf_sheep/app.py +17 -6
  11. mesa/examples/advanced/wolf_sheep/model.py +68 -74
  12. mesa/examples/basic/boid_flockers/agents.py +48 -16
  13. mesa/examples/basic/boid_flockers/app.py +2 -5
  14. mesa/examples/basic/boid_flockers/model.py +55 -19
  15. mesa/examples/basic/boltzmann_wealth_model/agents.py +23 -5
  16. mesa/examples/basic/boltzmann_wealth_model/app.py +8 -3
  17. mesa/examples/basic/boltzmann_wealth_model/model.py +48 -13
  18. mesa/examples/basic/boltzmann_wealth_model/st_app.py +2 -2
  19. mesa/examples/basic/conways_game_of_life/app.py +25 -3
  20. mesa/examples/basic/conways_game_of_life/model.py +2 -2
  21. mesa/examples/basic/schelling/agents.py +9 -5
  22. mesa/examples/basic/schelling/app.py +1 -1
  23. mesa/examples/basic/schelling/model.py +48 -26
  24. mesa/examples/basic/virus_on_network/Readme.md +2 -13
  25. mesa/examples/basic/virus_on_network/app.py +1 -1
  26. mesa/experimental/cell_space/cell_collection.py +14 -2
  27. mesa/experimental/cell_space/discrete_space.py +16 -2
  28. mesa/experimental/devs/simulator.py +59 -14
  29. mesa/model.py +4 -4
  30. mesa/time.py +4 -4
  31. mesa/visualization/__init__.py +1 -1
  32. mesa/visualization/components/matplotlib_components.py +1 -2
  33. mesa/visualization/mpl_space_drawing.py +42 -7
  34. mesa/visualization/solara_viz.py +133 -54
  35. {mesa-3.0.0rc0.dist-info → mesa-3.0.2.dist-info}/METADATA +6 -8
  36. {mesa-3.0.0rc0.dist-info → mesa-3.0.2.dist-info}/RECORD +41 -41
  37. {mesa-3.0.0rc0.dist-info → mesa-3.0.2.dist-info}/WHEEL +1 -1
  38. /mesa/visualization/{UserParam.py → user_param.py} +0 -0
  39. {mesa-3.0.0rc0.dist-info → mesa-3.0.2.dist-info}/entry_points.txt +0 -0
  40. {mesa-3.0.0rc0.dist-info → mesa-3.0.2.dist-info}/licenses/LICENSE +0 -0
  41. {mesa-3.0.0rc0.dist-info → mesa-3.0.2.dist-info}/licenses/NOTICE +0 -0
mesa/__init__.py CHANGED
@@ -24,7 +24,7 @@ __all__ = [
24
24
  ]
25
25
 
26
26
  __title__ = "mesa"
27
- __version__ = "3.0.0rc0"
27
+ __version__ = "3.0.2"
28
28
  __license__ = "Apache 2.0"
29
29
  _this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
30
30
  __copyright__ = f"Copyright {_this_year} Project Mesa Team"
mesa/agent.py CHANGED
@@ -110,6 +110,12 @@ class AgentSet(MutableSet, Sequence):
110
110
  without preventing garbage collection. It is associated with a specific model instance, enabling
111
111
  interactions with the model's environment and other agents.The implementation uses a WeakKeyDictionary to store agents,
112
112
  which means that agents not referenced elsewhere in the program may be automatically removed from the AgentSet.
113
+
114
+ Notes:
115
+ A `UserWarning` is issued if `random=None`. You can resolve this warning by explicitly
116
+ passing a random number generator. In most cases, this will be the seeded random number
117
+ generator in the model. So, you would do `random=self.random` in a `Model` or `Agent` instance.
118
+
113
119
  """
114
120
 
115
121
  def __init__(self, agents: Iterable[Agent], random: Random | None = None):
@@ -120,6 +126,11 @@ class AgentSet(MutableSet, Sequence):
120
126
  random (Random): the random number generator
121
127
  """
122
128
  if random is None:
129
+ warnings.warn(
130
+ "Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly",
131
+ UserWarning,
132
+ stacklevel=2,
133
+ )
123
134
  random = (
124
135
  Random()
125
136
  ) # FIXME see issue 1981, how to get the central rng from model
@@ -167,7 +178,7 @@ class AgentSet(MutableSet, Sequence):
167
178
  """
168
179
  if n is not None:
169
180
  warnings.warn(
170
- "The parameter 'n' is deprecated. Use 'at_most' instead.",
181
+ "The parameter 'n' is deprecated and will be removed in Mesa 3.1. Use 'at_most' instead.",
171
182
  DeprecationWarning,
172
183
  stacklevel=2,
173
184
  )
@@ -276,8 +287,9 @@ class AgentSet(MutableSet, Sequence):
276
287
  return_results = False
277
288
  else:
278
289
  warnings.warn(
279
- "Using return_results is deprecated. Use AgenSet.do in case of return_results=False, and "
280
- "AgentSet.map in case of return_results=True",
290
+ "Using return_results is deprecated and will be removed in Mesa 3.1."
291
+ "Use AgenSet.do in case of return_results=False, and AgentSet.map in case of return_results=True",
292
+ DeprecationWarning,
281
293
  stacklevel=2,
282
294
  )
283
295
 
mesa/examples/__init__.py CHANGED
@@ -3,14 +3,14 @@ from mesa.examples.advanced.pd_grid.model import PdGrid
3
3
  from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt
4
4
  from mesa.examples.advanced.wolf_sheep.model import WolfSheep
5
5
  from mesa.examples.basic.boid_flockers.model import BoidFlockers
6
- from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealthModel
6
+ from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealth
7
7
  from mesa.examples.basic.conways_game_of_life.model import ConwaysGameOfLife
8
8
  from mesa.examples.basic.schelling.model import Schelling
9
9
  from mesa.examples.basic.virus_on_network.model import VirusOnNetwork
10
10
 
11
11
  __all__ = [
12
12
  "BoidFlockers",
13
- "BoltzmannWealthModel",
13
+ "BoltzmannWealth",
14
14
  "ConwaysGameOfLife",
15
15
  "Schelling",
16
16
  "VirusOnNetwork",
@@ -8,14 +8,12 @@ The model generates mass uprising as self-reinforcing processes: if enough agent
8
8
 
9
9
  ## How to Run
10
10
 
11
- To run the model interactively, run ``EpsteinCivilViolenceServer.py`` in this directory. e.g.
11
+ To run the model interactively, in this directory, run the following command
12
12
 
13
13
  ```
14
- $ python EpsteinCivilViolenceServer.py
14
+ $ solara run app.py
15
15
  ```
16
16
 
17
- Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run.
18
-
19
17
  ## Files
20
18
 
21
19
  * ``model.py``: Core model code.
@@ -4,11 +4,11 @@ Solara-based visualization for the Spatial Prisoner's Dilemma Model.
4
4
 
5
5
  from mesa.examples.advanced.pd_grid.model import PdGrid
6
6
  from mesa.visualization import (
7
+ Slider,
7
8
  SolaraViz,
8
9
  make_plot_component,
9
10
  make_space_component,
10
11
  )
11
- from mesa.visualization.UserParam import Slider
12
12
 
13
13
 
14
14
  def pd_agent_portrayal(agent):
@@ -27,7 +27,7 @@ class PdGrid(mesa.Model):
27
27
  """
28
28
  super().__init__(seed=seed)
29
29
  self.activation_order = activation_order
30
- self.grid = OrthogonalMooreGrid((width, height), torus=True)
30
+ self.grid = OrthogonalMooreGrid((width, height), torus=True, random=self.random)
31
31
 
32
32
  if payoffs is not None:
33
33
  self.payoff = payoffs
@@ -4,7 +4,7 @@ from matplotlib.figure import Figure
4
4
 
5
5
  from mesa.examples.advanced.sugarscape_g1mt.agents import Trader
6
6
  from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt
7
- from mesa.visualization import SolaraViz, make_plot_component
7
+ from mesa.visualization import Slider, SolaraViz, make_plot_component
8
8
 
9
9
 
10
10
  def SpaceDrawer(model):
@@ -49,13 +49,29 @@ def SpaceDrawer(model):
49
49
  model_params = {
50
50
  "width": 50,
51
51
  "height": 50,
52
+ # Population parameters
53
+ "initial_population": Slider(
54
+ "Initial Population", value=200, min=50, max=500, step=10
55
+ ),
56
+ # Agent endowment parameters
57
+ "endowment_min": Slider("Min Initial Endowment", value=25, min=5, max=30, step=1),
58
+ "endowment_max": Slider("Max Initial Endowment", value=50, min=30, max=100, step=1),
59
+ # Metabolism parameters
60
+ "metabolism_min": Slider("Min Metabolism", value=1, min=1, max=3, step=1),
61
+ "metabolism_max": Slider("Max Metabolism", value=5, min=3, max=8, step=1),
62
+ # Vision parameters
63
+ "vision_min": Slider("Min Vision", value=1, min=1, max=3, step=1),
64
+ "vision_max": Slider("Max Vision", value=5, min=3, max=8, step=1),
65
+ # Trade parameter
66
+ "enable_trade": {"type": "Checkbox", "value": True, "label": "Enable Trading"},
52
67
  }
53
68
 
54
- model1 = SugarscapeG1mt(50, 50)
69
+ model1 = SugarscapeG1mt()
55
70
 
56
71
  page = SolaraViz(
57
72
  model1,
58
73
  components=[SpaceDrawer, make_plot_component(["Trader", "Price"])],
74
+ model_params=model_params,
59
75
  name="Sugarscape {G1, M, T}",
60
76
  play_interval=150,
61
77
  )
@@ -70,7 +70,9 @@ class SugarscapeG1mt(mesa.Model):
70
70
  self.running = True
71
71
 
72
72
  # initiate mesa grid class
73
- self.grid = OrthogonalVonNeumannGrid((self.width, self.height), torus=False)
73
+ self.grid = OrthogonalVonNeumannGrid(
74
+ (self.width, self.height), torus=False, random=self.random
75
+ )
74
76
  # initiate datacollector
75
77
  self.datacollector = mesa.DataCollector(
76
78
  model_reporters={
@@ -5,14 +5,14 @@ class Animal(CellAgent):
5
5
  """The base animal class."""
6
6
 
7
7
  def __init__(self, model, energy, p_reproduce, energy_from_food, cell):
8
- """Initializes an animal.
8
+ """Initialize an animal.
9
9
 
10
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
11
+ model: Model instance
12
+ energy: Starting amount of energy
13
+ p_reproduce: Probability of reproduction (asexual)
14
+ energy_from_food: Energy obtained from 1 unit of food
15
+ cell: Cell in which the animal starts
16
16
  """
17
17
  super().__init__(model)
18
18
  self.energy = energy
@@ -21,7 +21,7 @@ class Animal(CellAgent):
21
21
  self.cell = cell
22
22
 
23
23
  def spawn_offspring(self):
24
- """Create offspring."""
24
+ """Create offspring by splitting energy and creating new instance."""
25
25
  self.energy /= 2
26
26
  self.__class__(
27
27
  self.model,
@@ -31,15 +31,19 @@ class Animal(CellAgent):
31
31
  self.cell,
32
32
  )
33
33
 
34
- def feed(self): ...
34
+ def feed(self):
35
+ """Abstract method to be implemented by subclasses."""
35
36
 
36
37
  def step(self):
37
- """One step of the agent."""
38
+ """Execute one step of the animal's behavior."""
39
+ # Move to random neighboring cell
38
40
  self.cell = self.cell.neighborhood.select_random_cell()
39
41
  self.energy -= 1
40
42
 
43
+ # Try to feed
41
44
  self.feed()
42
45
 
46
+ # Handle death and reproduction
43
47
  if self.energy < 0:
44
48
  self.remove()
45
49
  elif self.random.random() < self.p_reproduce:
@@ -50,53 +54,63 @@ class Sheep(Animal):
50
54
  """A sheep that walks around, reproduces (asexually) and gets eaten."""
51
55
 
52
56
  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
57
+ """If possible, eat grass at current location."""
58
+ grass_patch = next(
59
+ obj for obj in self.cell.agents if isinstance(obj, GrassPatch)
60
+ )
61
+ if grass_patch.fully_grown:
62
+ self.energy += self.energy_from_food
63
+ grass_patch.fully_grown = False
62
64
 
63
65
 
64
66
  class Wolf(Animal):
65
67
  """A wolf that walks around, reproduces (asexually) and eats sheep."""
66
68
 
67
69
  def feed(self):
68
- """If possible eat the food in the current location."""
70
+ """If possible, eat a sheep at current location."""
69
71
  sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)]
70
- if len(sheep) > 0:
72
+ if sheep: # If there are any sheep present
71
73
  sheep_to_eat = self.random.choice(sheep)
72
74
  self.energy += self.energy_from_food
73
-
74
- # Kill the sheep
75
75
  sheep_to_eat.remove()
76
76
 
77
77
 
78
78
  class GrassPatch(FixedAgent):
79
- """
80
- A patch of grass that grows at a fixed rate and it is eaten by sheep
81
- """
79
+ """A patch of grass that grows at a fixed rate and can be eaten by sheep."""
80
+
81
+ @property
82
+ def fully_grown(self):
83
+ """Whether the grass patch is fully grown."""
84
+ return self._fully_grown
85
+
86
+ @fully_grown.setter
87
+ def fully_grown(self, value: bool) -> None:
88
+ """Set grass growth state and schedule regrowth if eaten."""
89
+ self._fully_grown = value
90
+
91
+ if not value: # If grass was just eaten
92
+ self.model.simulator.schedule_event_relative(
93
+ setattr,
94
+ self.grass_regrowth_time,
95
+ function_args=[self, "fully_grown", True],
96
+ )
82
97
 
83
- def __init__(self, model, fully_grown, countdown):
84
- """
85
- Creates a new patch of grass
98
+ def __init__(self, model, countdown, grass_regrowth_time, cell):
99
+ """Create a new patch of grass.
86
100
 
87
101
  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
102
+ model: Model instance
103
+ countdown: Time until grass is fully grown again
104
+ grass_regrowth_time: Time needed to regrow after being eaten
105
+ cell: Cell to which this grass patch belongs
90
106
  """
91
107
  super().__init__(model)
92
- self.fully_grown = fully_grown
93
- self.countdown = countdown
108
+ self._fully_grown = countdown == 0
109
+ self.grass_regrowth_time = grass_regrowth_time
110
+ self.cell = cell
94
111
 
95
- def step(self):
112
+ # Schedule initial growth if not fully grown
96
113
  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
114
+ self.model.simulator.schedule_event_relative(
115
+ setattr, countdown, function_args=[self, "fully_grown", True]
116
+ )
@@ -1,5 +1,6 @@
1
1
  from mesa.examples.advanced.wolf_sheep.agents import GrassPatch, Sheep, Wolf
2
2
  from mesa.examples.advanced.wolf_sheep.model import WolfSheep
3
+ from mesa.experimental.devs import ABMSimulator
3
4
  from mesa.visualization import (
4
5
  Slider,
5
6
  SolaraViz,
@@ -36,7 +37,11 @@ def wolf_sheep_portrayal(agent):
36
37
 
37
38
 
38
39
  model_params = {
39
- # The following line is an example to showcase StaticText.
40
+ "seed": {
41
+ "type": "InputText",
42
+ "value": 42,
43
+ "label": "Random Seed",
44
+ },
40
45
  "grass": {
41
46
  "type": "Select",
42
47
  "value": True,
@@ -59,26 +64,32 @@ model_params = {
59
64
  }
60
65
 
61
66
 
62
- def post_process(ax):
67
+ def post_process_space(ax):
63
68
  ax.set_aspect("equal")
64
69
  ax.set_xticks([])
65
70
  ax.set_yticks([])
66
71
 
67
72
 
73
+ def post_process_lines(ax):
74
+ ax.legend(loc="center left", bbox_to_anchor=(1, 0.9))
75
+
76
+
68
77
  space_component = make_space_component(
69
- wolf_sheep_portrayal, draw_grid=False, post_process=post_process
78
+ wolf_sheep_portrayal, draw_grid=False, post_process=post_process_space
70
79
  )
71
80
  lineplot_component = make_plot_component(
72
- {"Wolves": "tab:orange", "Sheep": "tab:cyan", "Grass": "tab:green"}
81
+ {"Wolves": "tab:orange", "Sheep": "tab:cyan", "Grass": "tab:green"},
82
+ post_process=post_process_lines,
73
83
  )
74
84
 
75
- model = WolfSheep(grass=True)
76
-
85
+ simulator = ABMSimulator()
86
+ model = WolfSheep(simulator=simulator, grass=True)
77
87
 
78
88
  page = SolaraViz(
79
89
  model,
80
90
  components=[space_component, lineplot_component],
81
91
  model_params=model_params,
82
92
  name="Wolf Sheep",
93
+ simulator=simulator,
83
94
  )
84
95
  page # noqa
@@ -9,30 +9,20 @@ Replication of the model found in NetLogo:
9
9
  Northwestern University, Evanston, IL.
10
10
  """
11
11
 
12
- import mesa
13
- from mesa.examples.advanced.wolf_sheep.agents import GrassPatch, Sheep, Wolf
14
- from mesa.experimental.cell_space import OrthogonalMooreGrid
15
-
16
-
17
- class WolfSheep(mesa.Model):
18
- """
19
- Wolf-Sheep Predation Model
20
- """
21
-
22
- height = 20
23
- width = 20
12
+ import math
24
13
 
25
- initial_sheep = 100
26
- initial_wolves = 50
14
+ from mesa import Model
15
+ from mesa.datacollection import DataCollector
16
+ from mesa.examples.advanced.wolf_sheep.agents import GrassPatch, Sheep, Wolf
17
+ from mesa.experimental.cell_space import OrthogonalVonNeumannGrid
18
+ from mesa.experimental.devs import ABMSimulator
27
19
 
28
- sheep_reproduce = 0.04
29
- wolf_reproduce = 0.05
30
20
 
31
- wolf_gain_from_food = 20
21
+ class WolfSheep(Model):
22
+ """Wolf-Sheep Predation Model.
32
23
 
33
- grass = False
34
- grass_regrowth_time = 30
35
- sheep_gain_from_food = 4
24
+ A model for simulating wolf and sheep (predator-prey) ecosystem modelling.
25
+ """
36
26
 
37
27
  description = (
38
28
  "A model for simulating wolf and sheep (predator-prey) ecosystem modelling."
@@ -47,15 +37,17 @@ class WolfSheep(mesa.Model):
47
37
  sheep_reproduce=0.04,
48
38
  wolf_reproduce=0.05,
49
39
  wolf_gain_from_food=20,
50
- grass=False,
40
+ grass=True,
51
41
  grass_regrowth_time=30,
52
42
  sheep_gain_from_food=4,
53
43
  seed=None,
44
+ simulator: ABMSimulator = None,
54
45
  ):
55
- """
56
- Create a new Wolf-Sheep model with the given parameters.
46
+ """Create a new Wolf-Sheep model with the given parameters.
57
47
 
58
48
  Args:
49
+ height: Height of the grid
50
+ width: Width of the grid
59
51
  initial_sheep: Number of sheep to start with
60
52
  initial_wolves: Number of wolves to start with
61
53
  sheep_reproduce: Probability of each sheep reproducing each step
@@ -63,75 +55,77 @@ class WolfSheep(mesa.Model):
63
55
  wolf_gain_from_food: Energy a wolf gains from eating a sheep
64
56
  grass: Whether to have the sheep eat grass for energy
65
57
  grass_regrowth_time: How long it takes for a grass patch to regrow
66
- once it is eaten
67
- sheep_gain_from_food: Energy sheep gain from grass, if enabled.
58
+ once it is eaten
59
+ sheep_gain_from_food: Energy sheep gain from grass, if enabled
60
+ seed: Random seed
61
+ simulator: ABMSimulator instance for event scheduling
68
62
  """
69
63
  super().__init__(seed=seed)
70
- # Set parameters
71
- self.width = width
64
+ self.simulator = simulator
65
+ self.simulator.setup(self)
66
+
67
+ # Initialize model parameters
72
68
  self.height = height
73
- self.initial_sheep = initial_sheep
74
- self.initial_wolves = initial_wolves
69
+ self.width = width
75
70
  self.grass = grass
76
- self.grass_regrowth_time = grass_regrowth_time
77
71
 
78
- self.grid = OrthogonalMooreGrid((self.width, self.height), torus=True)
72
+ # Create grid using experimental cell space
73
+ self.grid = OrthogonalVonNeumannGrid(
74
+ [self.height, self.width],
75
+ torus=True,
76
+ capacity=math.inf,
77
+ random=self.random,
78
+ )
79
79
 
80
- collectors = {
80
+ # Set up data collection
81
+ model_reporters = {
81
82
  "Wolves": lambda m: len(m.agents_by_type[Wolf]),
82
83
  "Sheep": lambda m: len(m.agents_by_type[Sheep]),
83
- "Grass": lambda m: len(
84
+ }
85
+ if grass:
86
+ model_reporters["Grass"] = lambda m: len(
84
87
  m.agents_by_type[GrassPatch].select(lambda a: a.fully_grown)
85
88
  )
86
- if m.grass
87
- else -1,
88
- }
89
89
 
90
- self.datacollector = mesa.DataCollector(collectors)
90
+ self.datacollector = DataCollector(model_reporters)
91
91
 
92
92
  # Create sheep:
93
- for _ in range(self.initial_sheep):
94
- x = self.random.randrange(self.width)
95
- y = self.random.randrange(self.height)
96
- energy = self.random.randrange(2 * self.sheep_gain_from_food)
97
- Sheep(
98
- self, energy, sheep_reproduce, sheep_gain_from_food, self.grid[(x, y)]
93
+ for _ in range(initial_sheep):
94
+ pos = (
95
+ self.random.randrange(width),
96
+ self.random.randrange(height),
99
97
  )
98
+ energy = self.random.randrange(2 * sheep_gain_from_food)
99
+ Sheep(self, energy, sheep_reproduce, sheep_gain_from_food, self.grid[pos])
100
100
 
101
101
  # Create wolves
102
- for _ in range(self.initial_wolves):
103
- x = self.random.randrange(self.width)
104
- y = self.random.randrange(self.height)
105
- energy = self.random.randrange(2 * self.wolf_gain_from_food)
106
- Wolf(self, energy, wolf_reproduce, wolf_gain_from_food, self.grid[(x, y)])
107
-
108
- # Create grass patches
109
- if self.grass:
110
- for cell in self.grid.all_cells:
111
- fully_grown = self.random.choice([True, False])
112
-
113
- if fully_grown:
114
- countdown = self.grass_regrowth_time
115
- else:
116
- countdown = self.random.randrange(self.grass_regrowth_time)
117
-
118
- patch = GrassPatch(self, fully_grown, countdown)
119
- patch.cell = cell
120
-
102
+ for _ in range(initial_wolves):
103
+ pos = (
104
+ self.random.randrange(width),
105
+ self.random.randrange(height),
106
+ )
107
+ energy = self.random.randrange(2 * wolf_gain_from_food)
108
+ Wolf(self, energy, wolf_reproduce, wolf_gain_from_food, self.grid[pos])
109
+
110
+ # Create grass patches if enabled
111
+ if grass:
112
+ possibly_fully_grown = [True, False]
113
+ for cell in self.grid:
114
+ fully_grown = self.random.choice(possibly_fully_grown)
115
+ countdown = (
116
+ 0 if fully_grown else self.random.randrange(0, grass_regrowth_time)
117
+ )
118
+ GrassPatch(self, countdown, grass_regrowth_time, cell)
119
+
120
+ # Collect initial data
121
121
  self.running = True
122
122
  self.datacollector.collect(self)
123
123
 
124
124
  def step(self):
125
- # This replicated the behavior of the old RandomActivationByType scheduler
126
- # when using step(shuffle_types=True, shuffle_agents=True).
127
- # Conceptually, it can be argued that this should be modelled differently.
128
- self.random.shuffle(self.agent_types)
129
- for agent_type in self.agent_types:
130
- self.agents_by_type[agent_type].shuffle_do("step")
131
-
132
- # collect data
133
- self.datacollector.collect(self)
125
+ """Execute one step of the model."""
126
+ # First activate all sheep, then all wolves, both in random order
127
+ self.agents_by_type[Sheep].shuffle_do("step")
128
+ self.agents_by_type[Wolf].shuffle_do("step")
134
129
 
135
- def run_model(self, step_count=200):
136
- for _ in range(step_count):
137
- self.step()
130
+ # Collect data
131
+ self.datacollector.collect(self)
@@ -1,3 +1,9 @@
1
+ """A Boid (bird-oid) agent for implementing Craig Reynolds's Boids flocking model.
2
+
3
+ This implementation uses numpy arrays to represent vectors for efficient computation
4
+ of flocking behavior.
5
+ """
6
+
1
7
  import numpy as np
2
8
 
3
9
  from mesa import Agent
@@ -7,9 +13,9 @@ class Boid(Agent):
7
13
  """A Boid-style flocker agent.
8
14
 
9
15
  The agent follows three behaviors to flock:
10
- - Cohesion: steering towards neighboring agents.
11
- - Separation: avoiding getting too close to any other agent.
12
- - Alignment: try to fly in the same direction as the neighbors.
16
+ - Cohesion: steering towards neighboring agents
17
+ - Separation: avoiding getting too close to any other agent
18
+ - Alignment: trying to fly in the same direction as neighbors
13
19
 
14
20
  Boids have a vision that defines the radius in which they look for their
15
21
  neighbors to flock with. Their speed (a scalar) and direction (a vector)
@@ -31,13 +37,14 @@ class Boid(Agent):
31
37
  """Create a new Boid flocker agent.
32
38
 
33
39
  Args:
34
- speed: Distance to move per step.
35
- direction: numpy vector for the Boid's direction of movement.
36
- vision: Radius to look around for nearby Boids.
37
- separation: Minimum distance to maintain from other Boids.
38
- cohere: the relative importance of matching neighbors' positions
39
- separate: the relative importance of avoiding close neighbors
40
- match: the relative importance of matching neighbors' headings
40
+ model: Model instance the agent belongs to
41
+ speed: Distance to move per step
42
+ direction: numpy vector for the Boid's direction of movement
43
+ vision: Radius to look around for nearby Boids
44
+ separation: Minimum distance to maintain from other Boids
45
+ cohere: Relative importance of matching neighbors' positions (default: 0.03)
46
+ separate: Relative importance of avoiding close neighbors (default: 0.015)
47
+ match: Relative importance of matching neighbors' directions (default: 0.05)
41
48
  """
42
49
  super().__init__(model)
43
50
  self.speed = speed
@@ -47,25 +54,50 @@ class Boid(Agent):
47
54
  self.cohere_factor = cohere
48
55
  self.separate_factor = separate
49
56
  self.match_factor = match
50
- self.neighbors = None
57
+ self.neighbors = []
51
58
 
52
59
  def step(self):
53
60
  """Get the Boid's neighbors, compute the new vector, and move accordingly."""
54
61
  self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False)
55
- n = 0
56
- match_vector, separation_vector, cohere = np.zeros((3, 2))
62
+
63
+ # If no neighbors, maintain current direction
64
+ if not self.neighbors:
65
+ new_pos = self.pos + self.direction * self.speed
66
+ self.model.space.move_agent(self, new_pos)
67
+ return
68
+
69
+ # Initialize vectors for the three flocking behaviors
70
+ cohere = np.zeros(2) # Cohesion vector
71
+ match_vector = np.zeros(2) # Alignment vector
72
+ separation_vector = np.zeros(2) # Separation vector
73
+
74
+ # Calculate the contribution of each neighbor to the three behaviors
57
75
  for neighbor in self.neighbors:
58
- n += 1
59
76
  heading = self.model.space.get_heading(self.pos, neighbor.pos)
77
+ distance = self.model.space.get_distance(self.pos, neighbor.pos)
78
+
79
+ # Cohesion - steer towards the average position of neighbors
60
80
  cohere += heading
61
- if self.model.space.get_distance(self.pos, neighbor.pos) < self.separation:
81
+
82
+ # Separation - avoid getting too close
83
+ if distance < self.separation:
62
84
  separation_vector -= heading
85
+
86
+ # Alignment - match neighbors' flying direction
63
87
  match_vector += neighbor.direction
64
- n = max(n, 1)
88
+
89
+ # Weight each behavior by its factor and normalize by number of neighbors
90
+ n = len(self.neighbors)
65
91
  cohere = cohere * self.cohere_factor
66
92
  separation_vector = separation_vector * self.separate_factor
67
93
  match_vector = match_vector * self.match_factor
94
+
95
+ # Update direction based on the three behaviors
68
96
  self.direction += (cohere + separation_vector + match_vector) / n
97
+
98
+ # Normalize direction vector
69
99
  self.direction /= np.linalg.norm(self.direction)
100
+
101
+ # Move boid
70
102
  new_pos = self.pos + self.direction * self.speed
71
103
  self.model.space.move_agent(self, new_pos)