Mesa 3.1.4__py3-none-any.whl → 3.2.0.dev0__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 (47) hide show
  1. mesa/__init__.py +3 -1
  2. mesa/agent.py +20 -5
  3. mesa/batchrunner.py +6 -3
  4. mesa/discrete_space/__init__.py +50 -0
  5. mesa/{experimental/cell_space → discrete_space}/cell.py +29 -10
  6. mesa/{experimental/cell_space → discrete_space}/cell_agent.py +1 -1
  7. mesa/{experimental/cell_space → discrete_space}/cell_collection.py +3 -3
  8. mesa/{experimental/cell_space → discrete_space}/discrete_space.py +65 -3
  9. mesa/{experimental/cell_space → discrete_space}/grid.py +2 -2
  10. mesa/{experimental/cell_space → discrete_space}/network.py +22 -2
  11. mesa/{experimental/cell_space → discrete_space}/property_layer.py +1 -10
  12. mesa/{experimental/cell_space → discrete_space}/voronoi.py +2 -2
  13. mesa/examples/README.md +9 -4
  14. mesa/examples/advanced/epstein_civil_violence/agents.py +1 -1
  15. mesa/examples/advanced/epstein_civil_violence/model.py +1 -1
  16. mesa/examples/advanced/pd_grid/agents.py +1 -1
  17. mesa/examples/advanced/pd_grid/model.py +1 -1
  18. mesa/examples/advanced/sugarscape_g1mt/agents.py +1 -1
  19. mesa/examples/advanced/sugarscape_g1mt/model.py +2 -2
  20. mesa/examples/advanced/wolf_sheep/agents.py +1 -1
  21. mesa/examples/advanced/wolf_sheep/app.py +2 -1
  22. mesa/examples/advanced/wolf_sheep/model.py +1 -1
  23. mesa/examples/basic/boid_flockers/agents.py +1 -0
  24. mesa/examples/basic/boid_flockers/app.py +17 -2
  25. mesa/examples/basic/boid_flockers/model.py +12 -0
  26. mesa/examples/basic/boltzmann_wealth_model/agents.py +6 -11
  27. mesa/examples/basic/boltzmann_wealth_model/app.py +2 -2
  28. mesa/examples/basic/boltzmann_wealth_model/model.py +7 -11
  29. mesa/examples/basic/conways_game_of_life/agents.py +13 -5
  30. mesa/examples/basic/conways_game_of_life/model.py +10 -7
  31. mesa/examples/basic/schelling/agents.py +13 -8
  32. mesa/examples/basic/schelling/model.py +6 -9
  33. mesa/examples/basic/virus_on_network/agents.py +13 -17
  34. mesa/examples/basic/virus_on_network/model.py +20 -24
  35. mesa/experimental/__init__.py +2 -2
  36. mesa/experimental/cell_space/__init__.py +18 -8
  37. mesa/space.py +1 -12
  38. mesa/visualization/__init__.py +2 -0
  39. mesa/visualization/command_console.py +482 -0
  40. mesa/visualization/components/altair_components.py +276 -16
  41. mesa/visualization/mpl_space_drawing.py +17 -9
  42. mesa/visualization/solara_viz.py +150 -21
  43. {mesa-3.1.4.dist-info → mesa-3.2.0.dev0.dist-info}/METADATA +12 -8
  44. {mesa-3.1.4.dist-info → mesa-3.2.0.dev0.dist-info}/RECORD +47 -45
  45. {mesa-3.1.4.dist-info → mesa-3.2.0.dev0.dist-info}/WHEEL +0 -0
  46. {mesa-3.1.4.dist-info → mesa-3.2.0.dev0.dist-info}/licenses/LICENSE +0 -0
  47. {mesa-3.1.4.dist-info → mesa-3.2.0.dev0.dist-info}/licenses/NOTICE +0 -0
mesa/__init__.py CHANGED
@@ -5,6 +5,7 @@ Core Objects: Model, and Agent.
5
5
 
6
6
  import datetime
7
7
 
8
+ import mesa.discrete_space as discrete_space
8
9
  import mesa.experimental as experimental
9
10
  import mesa.space as space
10
11
  from mesa.agent import Agent
@@ -17,12 +18,13 @@ __all__ = [
17
18
  "DataCollector",
18
19
  "Model",
19
20
  "batch_run",
21
+ "discrete_space",
20
22
  "experimental",
21
23
  "space",
22
24
  ]
23
25
 
24
26
  __title__ = "mesa"
25
- __version__ = "3.1.4"
27
+ __version__ = "3.2.0.dev"
26
28
  __license__ = "Apache 2.0"
27
29
  _this_year = datetime.datetime.now(tz=datetime.UTC).date().year
28
30
  __copyright__ = f"Copyright {_this_year} Project Mesa Team"
mesa/agent.py CHANGED
@@ -381,18 +381,33 @@ class AgentSet(MutableSet, Sequence):
381
381
 
382
382
  return res
383
383
 
384
- def agg(self, attribute: str, func: Callable) -> Any:
385
- """Aggregate an attribute of all agents in the AgentSet using a specified function.
384
+ def agg(
385
+ self, attribute: str, func: Callable | Iterable[Callable]
386
+ ) -> Any | list[Any]:
387
+ """Aggregate an attribute of all agents in the AgentSet using one or more functions.
386
388
 
387
389
  Args:
388
390
  attribute (str): The name of the attribute to aggregate.
389
- func (Callable): The function to apply to the attribute values (e.g., min, max, sum, np.mean).
391
+ func (Callable | Iterable[Callable]):
392
+ - If Callable: A single function to apply to the attribute values (e.g., min, max, sum, np.mean)
393
+ - If Iterable: Multiple functions to apply to the attribute values
390
394
 
391
395
  Returns:
392
- Any: The result of applying the function to the attribute values. Often a single value.
396
+ Any | [Any, ...]: Result of applying the function(s) to the attribute values.
397
+
398
+ Examples:
399
+ # Single function
400
+ avg_energy = model.agents.agg("energy", np.mean)
401
+
402
+ # Multiple functions
403
+ min_wealth, max_wealth, total_wealth = model.agents.agg("wealth", [min, max, sum])
393
404
  """
394
405
  values = self.get(attribute)
395
- return func(values)
406
+
407
+ if isinstance(func, Callable):
408
+ return func(values)
409
+ else:
410
+ return [f(values) for f in func]
396
411
 
397
412
  @overload
398
413
  def get(
mesa/batchrunner.py CHANGED
@@ -3,7 +3,12 @@
3
3
  To take advantage of parallel execution of experiments, `batch_run` uses
4
4
  multiprocessing if ``number_processes`` is larger than 1. It is strongly advised
5
5
  to only run in parallel using a normal python file (so don't try to do it in a
6
- jupyter notebook). Moreover, best practice when using multiprocessing is to
6
+ jupyter notebook). This is because Jupyter notebooks have a different execution
7
+ model that can cause issues with Python's multiprocessing module, especially on
8
+ Windows. The main problems include the lack of a traditional __main__ entry
9
+ point, serialization issues, and potential deadlocks.
10
+
11
+ Moreover, best practice when using multiprocessing is to
7
12
  put the code inside an ``if __name__ == '__main__':`` code black as shown below::
8
13
 
9
14
  from mesa.batchrunner import batch_run
@@ -21,8 +26,6 @@ put the code inside an ``if __name__ == '__main__':`` code black as shown below:
21
26
  display_progress=True,
22
27
  )
23
28
 
24
-
25
-
26
29
  """
27
30
 
28
31
  import itertools
@@ -0,0 +1,50 @@
1
+ """Cell spaces for active, property-rich spatial modeling in Mesa.
2
+
3
+ Cell spaces extend Mesa's spatial modeling capabilities by making the space itself active -
4
+ each position (cell) can have properties and behaviors rather than just containing agents.
5
+ This enables more sophisticated environmental modeling and agent-environment interactions.
6
+
7
+ Key components:
8
+ - Cells: Active positions that can have properties and contain agents
9
+ - CellAgents: Agents that understand how to interact with cells
10
+ - Spaces: Different cell organization patterns (grids, networks, etc.)
11
+ - PropertyLayers: Efficient property storage and manipulation
12
+
13
+ This is particularly useful for models where the environment plays an active role,
14
+ like resource growth, pollution diffusion, or infrastructure networks. The cell
15
+ space system is experimental and under active development.
16
+ """
17
+
18
+ from mesa.discrete_space.cell import Cell
19
+ from mesa.discrete_space.cell_agent import (
20
+ CellAgent,
21
+ FixedAgent,
22
+ Grid2DMovingAgent,
23
+ )
24
+ from mesa.discrete_space.cell_collection import CellCollection
25
+ from mesa.discrete_space.discrete_space import DiscreteSpace
26
+ from mesa.discrete_space.grid import (
27
+ Grid,
28
+ HexGrid,
29
+ OrthogonalMooreGrid,
30
+ OrthogonalVonNeumannGrid,
31
+ )
32
+ from mesa.discrete_space.network import Network
33
+ from mesa.discrete_space.property_layer import PropertyLayer
34
+ from mesa.discrete_space.voronoi import VoronoiGrid
35
+
36
+ __all__ = [
37
+ "Cell",
38
+ "CellAgent",
39
+ "CellCollection",
40
+ "DiscreteSpace",
41
+ "FixedAgent",
42
+ "Grid",
43
+ "Grid2DMovingAgent",
44
+ "HexGrid",
45
+ "Network",
46
+ "OrthogonalMooreGrid",
47
+ "OrthogonalVonNeumannGrid",
48
+ "PropertyLayer",
49
+ "VoronoiGrid",
50
+ ]
@@ -18,8 +18,8 @@ from functools import cache, cached_property
18
18
  from random import Random
19
19
  from typing import TYPE_CHECKING
20
20
 
21
- from mesa.experimental.cell_space.cell_agent import CellAgent
22
- from mesa.experimental.cell_space.cell_collection import CellCollection
21
+ from mesa.discrete_space.cell_agent import CellAgent
22
+ from mesa.discrete_space.cell_collection import CellCollection
23
23
 
24
24
  if TYPE_CHECKING:
25
25
  from mesa.agent import Agent
@@ -40,7 +40,7 @@ class Cell:
40
40
 
41
41
  __slots__ = [
42
42
  "__dict__",
43
- "agents",
43
+ "_agents",
44
44
  "capacity",
45
45
  "connections",
46
46
  "coordinate",
@@ -65,8 +65,8 @@ class Cell:
65
65
  super().__init__()
66
66
  self.coordinate = coordinate
67
67
  self.connections: dict[Coordinate, Cell] = {}
68
- self.agents: list[
69
- Agent
68
+ self._agents: list[
69
+ CellAgent
70
70
  ] = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
71
71
  self.capacity: int | None = capacity
72
72
  self.properties: dict[
@@ -84,6 +84,7 @@ class Cell:
84
84
  """
85
85
  if key is None:
86
86
  key = other.coordinate
87
+ self._clear_cache()
87
88
  self.connections[key] = other
88
89
 
89
90
  def disconnect(self, other: Cell) -> None:
@@ -96,6 +97,7 @@ class Cell:
96
97
  keys_to_remove = [k for k, v in self.connections.items() if v == other]
97
98
  for key in keys_to_remove:
98
99
  del self.connections[key]
100
+ self._clear_cache()
99
101
 
100
102
  def add_agent(self, agent: CellAgent) -> None:
101
103
  """Adds an agent to the cell.
@@ -104,7 +106,7 @@ class Cell:
104
106
  agent (CellAgent): agent to add to this Cell
105
107
 
106
108
  """
107
- n = len(self.agents)
109
+ n = len(self._agents)
108
110
  self.empty = False
109
111
 
110
112
  if self.capacity and n >= self.capacity:
@@ -112,7 +114,7 @@ class Cell:
112
114
  "ERROR: Cell is full"
113
115
  ) # FIXME we need MESA errors or a proper error
114
116
 
115
- self.agents.append(agent)
117
+ self._agents.append(agent)
116
118
 
117
119
  def remove_agent(self, agent: CellAgent) -> None:
118
120
  """Removes an agent from the cell.
@@ -121,7 +123,7 @@ class Cell:
121
123
  agent (CellAgent): agent to remove from this cell
122
124
 
123
125
  """
124
- self.agents.remove(agent)
126
+ self._agents.remove(agent)
125
127
  self.empty = self.is_empty
126
128
 
127
129
  @property
@@ -134,6 +136,11 @@ class Cell:
134
136
  """Returns a bool of the contents of a cell."""
135
137
  return len(self.agents) == self.capacity
136
138
 
139
+ @property
140
+ def agents(self) -> list[CellAgent]:
141
+ """Returns a list of the agents occupying the cell."""
142
+ return self._agents.copy()
143
+
137
144
  def __repr__(self): # noqa
138
145
  return f"Cell({self.coordinate}, {self.agents})"
139
146
 
@@ -180,12 +187,12 @@ class Cell:
180
187
  raise ValueError("radius must be larger than one")
181
188
  if radius == 1:
182
189
  neighborhood = {
183
- neighbor: neighbor.agents for neighbor in self.connections.values()
190
+ neighbor: neighbor._agents for neighbor in self.connections.values()
184
191
  }
185
192
  if not include_center:
186
193
  return neighborhood
187
194
  else:
188
- neighborhood[self] = self.agents
195
+ neighborhood[self] = self._agents
189
196
  return neighborhood
190
197
  else:
191
198
  neighborhood: dict[Cell, list[Agent]] = {}
@@ -205,3 +212,15 @@ class Cell:
205
212
  "connections"
206
213
  ] = {} # replace this with empty connections to avoid infinite recursion error in pickle/deepcopy
207
214
  return state
215
+
216
+ def _clear_cache(self):
217
+ """Helper function to clear local cache."""
218
+ try:
219
+ self.__dict__.pop(
220
+ "neighborhood"
221
+ ) # cached properties are stored in __dict__, see functools.cached_property docs
222
+ except KeyError:
223
+ pass # cache is not set
224
+ else:
225
+ self.get_neighborhood.cache_clear()
226
+ self._neighborhood.cache_clear()
@@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Protocol
18
18
  from mesa.agent import Agent
19
19
 
20
20
  if TYPE_CHECKING:
21
- from mesa.experimental.cell_space import Cell
21
+ from mesa.discrete_space import Cell
22
22
 
23
23
 
24
24
  class HasCellProtocol(Protocol):
@@ -23,8 +23,8 @@ from random import Random
23
23
  from typing import TYPE_CHECKING, Generic, TypeVar
24
24
 
25
25
  if TYPE_CHECKING:
26
- from mesa.experimental.cell_space.cell import Cell
27
- from mesa.experimental.cell_space.cell_agent import CellAgent
26
+ from mesa.discrete_space.cell import Cell
27
+ from mesa.discrete_space.cell_agent import CellAgent
28
28
 
29
29
  T = TypeVar("T", bound="Cell")
30
30
 
@@ -59,7 +59,7 @@ class CellCollection(Generic[T]):
59
59
  if isinstance(cells, dict):
60
60
  self._cells = cells
61
61
  else:
62
- self._cells = {cell: cell.agents for cell in cells}
62
+ self._cells = {cell: cell._agents for cell in cells}
63
63
 
64
64
  # Get capacity from first cell if collection is not empty
65
65
  self._capacity: int | None = (
@@ -21,8 +21,8 @@ from random import Random
21
21
  from typing import Generic, TypeVar
22
22
 
23
23
  from mesa.agent import AgentSet
24
- from mesa.experimental.cell_space.cell import Cell
25
- from mesa.experimental.cell_space.cell_collection import CellCollection
24
+ from mesa.discrete_space.cell import Cell
25
+ from mesa.discrete_space.cell_collection import CellCollection
26
26
 
27
27
  T = TypeVar("T", bound=Cell)
28
28
 
@@ -85,11 +85,73 @@ class DiscreteSpace(Generic[T]):
85
85
  def _connect_cells(self): ...
86
86
  def _connect_single_cell(self, cell: T): ...
87
87
 
88
+ def add_cell(self, cell: T):
89
+ """Add a cell to the space.
90
+
91
+ Args:
92
+ cell: cell to add
93
+
94
+ Note:
95
+ Discrete spaces rely on caching neighborhood relations for speedups. Adding or removing cells and
96
+ connections at runtime is possible. However, only the caches of cells directly affected will be cleared. So
97
+ if you rely on getting neighborhoods of cells with a radius higher than 1, these might not be cleared
98
+ correctly if you are adding or removing cells and connections at runtime.
99
+
100
+ """
101
+ self.__dict__.pop("all_cells", None)
102
+ self._cells[cell.coordinate] = cell
103
+
104
+ def remove_cell(self, cell: T):
105
+ """Remove a cell from the space.
106
+
107
+ Note:
108
+ Discrete spaces rely on caching neighborhood relations for speedups. Adding or removing cells and
109
+ connections at runtime is possible. However, only the caches of cells directly affected will be cleared. So
110
+ if you rely on getting neighborhoods of cells with a radius higher than 1, these might not be cleared
111
+ correctly if you are adding or removing cells and connections at runtime.
112
+
113
+
114
+ """
115
+ neighbors = cell.neighborhood
116
+ self._cells.pop(cell.coordinate)
117
+ self.__dict__.pop("all_cells", None)
118
+
119
+ # iterate over all neighbors
120
+ for neighbor in neighbors.cells:
121
+ neighbor.disconnect(cell)
122
+ cell.disconnect(neighbor)
123
+
124
+ def add_connection(self, cell1: T, cell2: T):
125
+ """Add a connection between the two cells.
126
+
127
+ Note:
128
+ Discrete spaces rely on caching neighborhood relations for speedups. Adding or removing cells and
129
+ connections at runtime is possible. However, only the caches of cells directly affected will be cleared. So
130
+ if you rely on getting neighborhoods of cells with a radius higher than 1, these might not be cleared
131
+ correctly if you are adding or removing cells and connections at runtime.
132
+
133
+ """
134
+ cell1.connect(cell2)
135
+ cell2.connect(cell1)
136
+
137
+ def remove_connection(self, cell1: T, cell2: T):
138
+ """Remove a connection between the two cells.
139
+
140
+ Note:
141
+ Discrete spaces rely on caching neighborhood relations for speedups. Adding or removing cells and
142
+ connections at runtime is possible. However, only the caches of cells directly affected will be cleared. So
143
+ if you rely on getting neighborhoods of cells with a radius higher than 1, these might not be cleared
144
+ correctly if you are adding or removing cells and connections at runtime.
145
+
146
+ """
147
+ cell1.disconnect(cell2)
148
+ cell2.disconnect(cell1)
149
+
88
150
  @cached_property
89
151
  def all_cells(self):
90
152
  """Return all cells in space."""
91
153
  return CellCollection(
92
- {cell: cell.agents for cell in self._cells.values()}, random=self.random
154
+ {cell: cell._agents for cell in self._cells.values()}, random=self.random
93
155
  )
94
156
 
95
157
  def __iter__(self): # noqa
@@ -19,8 +19,8 @@ from itertools import product
19
19
  from random import Random
20
20
  from typing import Any, Generic, TypeVar
21
21
 
22
- from mesa.experimental.cell_space import Cell, DiscreteSpace
23
- from mesa.experimental.cell_space.property_layer import (
22
+ from mesa.discrete_space import Cell, DiscreteSpace
23
+ from mesa.discrete_space.property_layer import (
24
24
  HasPropertyLayers,
25
25
  PropertyDescriptor,
26
26
  )
@@ -15,8 +15,8 @@ or any environment where connectivity matters more than physical location.
15
15
  from random import Random
16
16
  from typing import Any
17
17
 
18
- from mesa.experimental.cell_space.cell import Cell
19
- from mesa.experimental.cell_space.discrete_space import DiscreteSpace
18
+ from mesa.discrete_space.cell import Cell
19
+ from mesa.discrete_space.discrete_space import DiscreteSpace
20
20
 
21
21
 
22
22
  class Network(DiscreteSpace[Cell]):
@@ -55,3 +55,23 @@ class Network(DiscreteSpace[Cell]):
55
55
  def _connect_single_cell(self, cell: Cell):
56
56
  for node_id in self.G.neighbors(cell.coordinate):
57
57
  cell.connect(self._cells[node_id], node_id)
58
+
59
+ def add_cell(self, cell: Cell):
60
+ """Add a cell to the space."""
61
+ super().add_cell(cell)
62
+ self.G.add_node(cell.coordinate)
63
+
64
+ def remove_cell(self, cell: Cell):
65
+ """Remove a cell from the space."""
66
+ super().remove_cell(cell)
67
+ self.G.remove_node(cell.coordinate)
68
+
69
+ def add_connection(self, cell1: Cell, cell2: Cell):
70
+ """Add a connection between the two cells."""
71
+ super().add_connection(cell1, cell2)
72
+ self.G.add_edge(cell1.coordinate, cell2.coordinate)
73
+
74
+ def remove_connection(self, cell1: Cell, cell2: Cell):
75
+ """Remove a connection between the two cells."""
76
+ super().remove_connection(cell1, cell2)
77
+ self.G.remove_edge(cell1.coordinate, cell2.coordinate)
@@ -21,7 +21,7 @@ from typing import Any, TypeVar
21
21
 
22
22
  import numpy as np
23
23
 
24
- from mesa.experimental.cell_space import Cell
24
+ from mesa.discrete_space import Cell
25
25
 
26
26
  Coordinate = Sequence[int]
27
27
  T = TypeVar("T", bound=Cell)
@@ -85,15 +85,6 @@ class PropertyLayer:
85
85
  # fixme why not initialize with empty?
86
86
  self._mesa_data = np.full(self.dimensions, default_value, dtype=dtype)
87
87
 
88
- if not self.__class__.propertylayer_experimental_warning_given:
89
- warnings.warn(
90
- "The property layer functionality and associated classes are experimental. It may be changed or removed in any and all future releases, including patch releases.\n"
91
- "We would love to hear what you think about this new feature. If you have any thoughts, share them with us here: https://github.com/projectmesa/mesa/discussions/1932",
92
- FutureWarning,
93
- stacklevel=2,
94
- )
95
- self.__class__.propertylayer_experimental_warning_given = True
96
-
97
88
  @classmethod
98
89
  def from_data(cls, name: str, data: np.ndarray):
99
90
  """Create a property layer from a NumPy array.
@@ -18,8 +18,8 @@ from random import Random
18
18
 
19
19
  import numpy as np
20
20
 
21
- from mesa.experimental.cell_space.cell import Cell
22
- from mesa.experimental.cell_space.discrete_space import DiscreteSpace
21
+ from mesa.discrete_space.cell import Cell
22
+ from mesa.discrete_space.discrete_space import DiscreteSpace
23
23
 
24
24
 
25
25
  class Delaunay:
mesa/examples/README.md CHANGED
@@ -1,7 +1,12 @@
1
- # Mesa core examples
2
- These examples are a collection of classic agent based models built using Mesa. These core examples are maintained by the Mesa team and are intended to demonstrate the capabilities of Mesa.
1
+ # Mesa Core Examples
2
+ This repository contains a curated set of classic agent-based models implemented using Mesa. These core examples are maintained by the Mesa development team and serve as both demonstrations of Mesa's capabilities and starting points for your own models.
3
3
 
4
- More user examples and showcases can be found in the [mesa-examples](https://github.com/projectmesa/mesa-examples) repository.
4
+ ## Overview
5
+ The examples are categorized into two groups:
6
+ 1. **Basic Examples** - Simpler models that use only stable Mesa features; ideal for beginners
7
+ 2. **Advanced Examples** - More complex models that demonstrate additional concepts and may use some experimental features
8
+
9
+ > **Note:** Looking for more examples? Visit the [mesa-examples](https://github.com/projectmesa/mesa-examples) repository for user-contributed models and showcases.
5
10
 
6
11
  ## Basic Examples
7
12
  The basic examples are relatively simple and only use stable Mesa features. They are good starting points for learning how to use Mesa.
@@ -34,4 +39,4 @@ Grid-based demographic prisoner's dilemma model, demonstrating how simple rules
34
39
  This is Epstein & Axtell's Sugarscape model with Traders, a detailed description is in Chapter four of *Growing Artificial Societies: Social Science from the Bottom Up (1996)*. The model shows how emergent price equilibrium can happen via decentralized dynamics.
35
40
 
36
41
  ### [Wolf-Sheep Predation Model](examples/advanced/wolf_sheep)
37
- Implementation of an ecological model of predation and reproduction, based on the NetLogo [Wolf Sheep Predation](http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation) model.
42
+ Implementation of an ecological model of predation and reproduction, based on the NetLogo [Wolf Sheep Predation](http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation) model.
@@ -10,7 +10,7 @@ class CitizenState(Enum):
10
10
  ARRESTED = 3
11
11
 
12
12
 
13
- class EpsteinAgent(mesa.experimental.cell_space.CellAgent):
13
+ class EpsteinAgent(mesa.discrete_space.CellAgent):
14
14
  def update_neighbors(self):
15
15
  """
16
16
  Look around and see who my neighbors are
@@ -53,7 +53,7 @@ class EpsteinCivilViolence(mesa.Model):
53
53
  self.movement = movement
54
54
  self.max_iters = max_iters
55
55
 
56
- self.grid = mesa.experimental.cell_space.OrthogonalVonNeumannGrid(
56
+ self.grid = mesa.discrete_space.OrthogonalVonNeumannGrid(
57
57
  (width, height), capacity=1, torus=True, random=self.random
58
58
  )
59
59
 
@@ -1,4 +1,4 @@
1
- from mesa.experimental.cell_space import CellAgent
1
+ from mesa.discrete_space import CellAgent
2
2
 
3
3
 
4
4
  class PDAgent(CellAgent):
@@ -1,6 +1,6 @@
1
1
  import mesa
2
+ from mesa.discrete_space import OrthogonalMooreGrid
2
3
  from mesa.examples.advanced.pd_grid.agents import PDAgent
3
- from mesa.experimental.cell_space import OrthogonalMooreGrid
4
4
 
5
5
 
6
6
  class PdGrid(mesa.Model):
@@ -1,6 +1,6 @@
1
1
  import math
2
2
 
3
- from mesa.experimental.cell_space import CellAgent
3
+ from mesa.discrete_space import CellAgent
4
4
 
5
5
 
6
6
  # Helper function
@@ -3,9 +3,9 @@ from pathlib import Path
3
3
  import numpy as np
4
4
 
5
5
  import mesa
6
+ from mesa.discrete_space import OrthogonalVonNeumannGrid
7
+ from mesa.discrete_space.property_layer import PropertyLayer
6
8
  from mesa.examples.advanced.sugarscape_g1mt.agents import Trader
7
- from mesa.experimental.cell_space import OrthogonalVonNeumannGrid
8
- from mesa.experimental.cell_space.property_layer import PropertyLayer
9
9
 
10
10
 
11
11
  # Helper Functions
@@ -1,4 +1,4 @@
1
- from mesa.experimental.cell_space import CellAgent, FixedAgent
1
+ from mesa.discrete_space import CellAgent, FixedAgent
2
2
 
3
3
 
4
4
  class Animal(CellAgent):
@@ -2,6 +2,7 @@ from mesa.examples.advanced.wolf_sheep.agents import GrassPatch, Sheep, Wolf
2
2
  from mesa.examples.advanced.wolf_sheep.model import WolfSheep
3
3
  from mesa.experimental.devs import ABMSimulator
4
4
  from mesa.visualization import (
5
+ CommandConsole,
5
6
  Slider,
6
7
  SolaraViz,
7
8
  make_plot_component,
@@ -87,7 +88,7 @@ model = WolfSheep(simulator=simulator, grass=True)
87
88
 
88
89
  page = SolaraViz(
89
90
  model,
90
- components=[space_component, lineplot_component],
91
+ components=[space_component, lineplot_component, CommandConsole],
91
92
  model_params=model_params,
92
93
  name="Wolf Sheep",
93
94
  simulator=simulator,
@@ -13,8 +13,8 @@ import math
13
13
 
14
14
  from mesa import Model
15
15
  from mesa.datacollection import DataCollector
16
+ from mesa.discrete_space import OrthogonalVonNeumannGrid
16
17
  from mesa.examples.advanced.wolf_sheep.agents import GrassPatch, Sheep, Wolf
17
- from mesa.experimental.cell_space import OrthogonalVonNeumannGrid
18
18
  from mesa.experimental.devs import ABMSimulator
19
19
 
20
20
 
@@ -58,6 +58,7 @@ class Boid(ContinuousSpaceAgent):
58
58
  self.separate_factor = separate
59
59
  self.match_factor = match
60
60
  self.neighbors = []
61
+ self.angle = 0.0 # represents the angle at which the boid is moving
61
62
 
62
63
  def step(self):
63
64
  """Get the Boid's neighbors, compute the new vector, and move accordingly."""
@@ -1,19 +1,34 @@
1
1
  import os
2
2
  import sys
3
3
 
4
+ from matplotlib.markers import MarkerStyle
5
+
4
6
  sys.path.insert(0, os.path.abspath("../../../.."))
5
7
 
6
8
  from mesa.examples.basic.boid_flockers.model import BoidFlockers
7
9
  from mesa.visualization import Slider, SolaraViz, make_space_component
8
10
 
11
+ # Pre-compute markers for different angles (e.g., every 10 degrees)
12
+ MARKER_CACHE = {}
13
+ for angle in range(0, 360, 10):
14
+ marker = MarkerStyle(10)
15
+ marker._transform = marker.get_transform().rotate_deg(angle)
16
+ MARKER_CACHE[angle] = marker
17
+
9
18
 
10
19
  def boid_draw(agent):
11
20
  neighbors = len(agent.neighbors)
12
21
 
22
+ # Calculate the angle
23
+ deg = agent.angle
24
+ # Round to nearest 10 degrees
25
+ rounded_deg = round(deg / 10) * 10 % 360
26
+
27
+ # using cached markers to speed things up
13
28
  if neighbors <= 1:
14
- return {"color": "red", "size": 20}
29
+ return {"color": "red", "size": 20, "marker": MARKER_CACHE[rounded_deg]}
15
30
  elif neighbors >= 2:
16
- return {"color": "green", "size": 20}
31
+ return {"color": "green", "size": 20, "marker": MARKER_CACHE[rounded_deg]}
17
32
 
18
33
 
19
34
  model_params = {
@@ -49,6 +49,9 @@ class BoidFlockers(Model):
49
49
  seed: Random seed for reproducibility (default: None)
50
50
  """
51
51
  super().__init__(seed=seed)
52
+ self.agent_angles = np.zeros(
53
+ population_size
54
+ ) # holds the angle representing the direction of all agents at a given step
52
55
 
53
56
  # Set up the space
54
57
  self.space = ContinuousSpace(
@@ -79,6 +82,14 @@ class BoidFlockers(Model):
79
82
  self.average_heading = None
80
83
  self.update_average_heading()
81
84
 
85
+ # vectorizing the calculation of angles for all agents
86
+ def calculate_angles(self):
87
+ d1 = np.array([agent.direction[0] for agent in self.agents])
88
+ d2 = np.array([agent.direction[1] for agent in self.agents])
89
+ self.agent_angles = np.degrees(np.arctan2(d1, d2))
90
+ for agent, angle in zip(self.agents, self.agent_angles):
91
+ agent.angle = angle
92
+
82
93
  def update_average_heading(self):
83
94
  """Calculate the average heading (direction) of all Boids."""
84
95
  if not self.agents:
@@ -96,3 +107,4 @@ class BoidFlockers(Model):
96
107
  """
97
108
  self.agents.shuffle_do("step")
98
109
  self.update_average_heading()
110
+ self.calculate_angles()