Mesa 2.2.3__py3-none-any.whl → 2.3.0__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.

mesa/__init__.py CHANGED
@@ -3,6 +3,7 @@ Mesa Agent-Based Modeling Framework
3
3
 
4
4
  Core Objects: Model, and Agent.
5
5
  """
6
+
6
7
  import datetime
7
8
 
8
9
  import mesa.space as space
@@ -25,7 +26,7 @@ __all__ = [
25
26
  ]
26
27
 
27
28
  __title__ = "mesa"
28
- __version__ = "2.2.3"
29
+ __version__ = "2.3.0"
29
30
  __license__ = "Apache 2.0"
30
31
  _this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
31
32
  __copyright__ = f"Copyright {_this_year} Project Mesa Team"
mesa/agent.py CHANGED
@@ -3,6 +3,7 @@ The agent class for Mesa framework.
3
3
 
4
4
  Core Objects: Agent
5
5
  """
6
+
6
7
  # Mypy; for the `|` operator purpose
7
8
  # Remove this __future__ import once the oldest supported Python is 3.10
8
9
  from __future__ import annotations
@@ -54,6 +55,7 @@ class Agent:
54
55
  except AttributeError:
55
56
  # model super has not been called
56
57
  self.model.agents_ = defaultdict(dict)
58
+ self.model.agents_[type(self)][self] = None
57
59
  self.model.agentset_experimental_warning_given = False
58
60
 
59
61
  warnings.warn(
@@ -80,12 +82,6 @@ class Agent:
80
82
 
81
83
  class AgentSet(MutableSet, Sequence):
82
84
  """
83
- .. warning::
84
- The AgentSet is experimental. It may be changed or removed in any and all future releases, including
85
- patch releases.
86
- We would love to hear what you think about this new feature. If you have any thoughts, share them with
87
- us here: https://github.com/projectmesa/mesa/discussions/1919
88
-
89
85
  A collection class that represents an ordered set of agents within an agent-based model (ABM). This class
90
86
  extends both MutableSet and Sequence, providing set-like functionality with order preservation and
91
87
  sequence operations.
@@ -114,17 +110,8 @@ class AgentSet(MutableSet, Sequence):
114
110
  agents (Iterable[Agent]): An iterable of Agent objects to be included in the set.
115
111
  model (Model): The ABM model instance to which this AgentSet belongs.
116
112
  """
117
- self.model = model
118
-
119
- if not self.__class__.agentset_experimental_warning_given:
120
- self.__class__.agentset_experimental_warning_given = True
121
- warnings.warn(
122
- "The AgentSet is experimental. It may be changed or removed in any and all future releases, including patch releases.\n"
123
- "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/1919",
124
- FutureWarning,
125
- stacklevel=2,
126
- )
127
113
 
114
+ self.model = model
128
115
  self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
129
116
 
130
117
  def __len__(self) -> int:
@@ -187,15 +174,21 @@ class AgentSet(MutableSet, Sequence):
187
174
 
188
175
  Returns:
189
176
  AgentSet: A shuffled AgentSet. Returns the current AgentSet if inplace is True.
190
- """
191
- shuffled_agents = list(self)
192
- self.random.shuffle(shuffled_agents)
193
177
 
194
- return (
195
- AgentSet(shuffled_agents, self.model)
196
- if not inplace
197
- else self._update(shuffled_agents)
198
- )
178
+ Note:
179
+ Using inplace = True is more performant
180
+
181
+ """
182
+ weakrefs = list(self._agents.keyrefs())
183
+ self.random.shuffle(weakrefs)
184
+
185
+ if inplace:
186
+ self._agents.data = {entry: None for entry in weakrefs}
187
+ return self
188
+ else:
189
+ return AgentSet(
190
+ (agent for ref in weakrefs if (agent := ref()) is not None), self.model
191
+ )
199
192
 
200
193
  def sort(
201
194
  self,
@@ -250,24 +243,36 @@ class AgentSet(MutableSet, Sequence):
250
243
  """
251
244
  # we iterate over the actual weakref keys and check if weakref is alive before calling the method
252
245
  res = [
253
- getattr(agentref(), method_name)(*args, **kwargs)
246
+ getattr(agent, method_name)(*args, **kwargs)
254
247
  for agentref in self._agents.keyrefs()
255
- if agentref()
248
+ if (agent := agentref()) is not None
256
249
  ]
257
250
 
258
251
  return res if return_results else self
259
252
 
260
- def get(self, attr_name: str) -> list[Any]:
253
+ def get(self, attr_names: str | list[str]) -> list[Any]:
261
254
  """
262
- Retrieve a specified attribute from each agent in the AgentSet.
255
+ Retrieve the specified attribute(s) from each agent in the AgentSet.
263
256
 
264
257
  Args:
265
- attr_name (str): The name of the attribute to retrieve from each agent.
258
+ attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent.
266
259
 
267
260
  Returns:
268
- list[Any]: A list of attribute values from each agent in the set.
261
+ list[Any]: A list with the attribute value for each agent in the set if attr_names is a str
262
+ list[list[Any]]: A list with a list of attribute values for each agent in the set if attr_names is a list of str
263
+
264
+ Raises:
265
+ AttributeError if an agent does not have the specified attribute(s)
266
+
269
267
  """
270
- return [getattr(agent, attr_name) for agent in self._agents]
268
+
269
+ if isinstance(attr_names, str):
270
+ return [getattr(agent, attr_names) for agent in self._agents]
271
+ else:
272
+ return [
273
+ [getattr(agent, attr_name) for attr_name in attr_names]
274
+ for agent in self._agents
275
+ ]
271
276
 
272
277
  def __getitem__(self, item: int | slice) -> Agent:
273
278
  """
mesa/datacollection.py CHANGED
@@ -14,8 +14,7 @@ name.
14
14
 
15
15
  When the collect() method is called, each model-level function is called, with
16
16
  the model as the argument, and the results associated with the relevant
17
- variable. Then the agent-level functions are called on each agent in the model
18
- scheduler.
17
+ variable. Then the agent-level functions are called on each agent.
19
18
 
20
19
  Additionally, other objects can write directly to tables by passing in an
21
20
  appropriate dictionary object for a table row.
@@ -30,10 +29,10 @@ The DataCollector then stores the data it collects in dictionaries:
30
29
  Finally, DataCollector can create a pandas DataFrame from each collection.
31
30
 
32
31
  The default DataCollector here makes several assumptions:
33
- * The model has a schedule object called 'schedule'
34
- * The schedule has an agent list called agents
32
+ * The model has an agent list called agents
35
33
  * For collecting agent-level variables, agents must have a unique_id
36
34
  """
35
+
37
36
  import contextlib
38
37
  import itertools
39
38
  import types
@@ -67,7 +66,7 @@ class DataCollector:
67
66
 
68
67
  Model reporters can take four types of arguments:
69
68
  1. Lambda function:
70
- {"agent_count": lambda m: m.schedule.get_agent_count()}
69
+ {"agent_count": lambda m: len(m.agents)}
71
70
  2. Method of a class/instance:
72
71
  {"agent_count": self.get_agent_count} # self here is a class instance
73
72
  {"agent_count": Model.get_agent_count} # Model here is a class
@@ -180,11 +179,16 @@ class DataCollector:
180
179
  rep_funcs = self.agent_reporters.values()
181
180
 
182
181
  def get_reports(agent):
183
- _prefix = (agent.model.schedule.steps, agent.unique_id)
182
+ _prefix = (agent.model._steps, agent.unique_id)
184
183
  reports = tuple(rep(agent) for rep in rep_funcs)
185
184
  return _prefix + reports
186
185
 
187
- agent_records = map(get_reports, model.schedule.agents)
186
+ agent_records = map(
187
+ get_reports,
188
+ model.schedule.agents
189
+ if hasattr(model, "schedule") and model.schedule is not None
190
+ else model.agents,
191
+ )
188
192
  return agent_records
189
193
 
190
194
  def collect(self, model):
@@ -207,7 +211,7 @@ class DataCollector:
207
211
 
208
212
  if self.agent_reporters:
209
213
  agent_records = self._record_agents(model)
210
- self._agent_records[model.schedule.steps] = list(agent_records)
214
+ self._agent_records[model._steps] = list(agent_records)
211
215
 
212
216
  def add_table_row(self, table_name, row, ignore_missing=False):
213
217
  """Add a row dictionary to a specific table.
@@ -0,0 +1,56 @@
1
+ class UserParam:
2
+ _ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'"
3
+
4
+ def maybe_raise_error(self, param_type, valid):
5
+ if valid:
6
+ return
7
+ msg = self._ERROR_MESSAGE.format(param_type, self.label)
8
+ raise ValueError(msg)
9
+
10
+
11
+ class Slider(UserParam):
12
+ """
13
+ A number-based slider input with settable increment.
14
+
15
+ Example:
16
+
17
+ slider_option = Slider("My Slider", value=123, min=10, max=200, step=0.1)
18
+
19
+ Args:
20
+ label: The displayed label in the UI
21
+ value: The initial value of the slider
22
+ min: The minimum possible value of the slider
23
+ max: The maximum possible value of the slider
24
+ step: The step between min and max for a range of possible values
25
+ dtype: either int or float
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ label="",
31
+ value=None,
32
+ min=None,
33
+ max=None,
34
+ step=1,
35
+ dtype=None,
36
+ ):
37
+ self.label = label
38
+ self.value = value
39
+ self.min = min
40
+ self.max = max
41
+ self.step = step
42
+
43
+ # Validate option type to make sure values are supplied properly
44
+ valid = not (self.value is None or self.min is None or self.max is None)
45
+ self.maybe_raise_error("slider", valid)
46
+
47
+ if dtype is None:
48
+ self.is_float_slider = self._check_values_are_float(value, min, max, step)
49
+ else:
50
+ self.is_float_slider = dtype == float
51
+
52
+ def _check_values_are_float(self, value, min, max, step):
53
+ return any(isinstance(n, float) for n in (value, min, max, step))
54
+
55
+ def get(self, attr):
56
+ return getattr(self, attr)
@@ -1 +1,5 @@
1
- from .jupyter_viz import JupyterViz, make_text # noqa
1
+ from .jupyter_viz import JupyterViz, make_text, Slider # noqa
2
+ from mesa.experimental import cell_space
3
+
4
+
5
+ __all__ = ["JupyterViz", "make_text", "Slider", "cell_space"]
@@ -0,0 +1,23 @@
1
+ from mesa.experimental.cell_space.cell import Cell
2
+ from mesa.experimental.cell_space.cell_agent import CellAgent
3
+ from mesa.experimental.cell_space.cell_collection import CellCollection
4
+ from mesa.experimental.cell_space.discrete_space import DiscreteSpace
5
+ from mesa.experimental.cell_space.grid import (
6
+ Grid,
7
+ HexGrid,
8
+ OrthogonalMooreGrid,
9
+ OrthogonalVonNeumannGrid,
10
+ )
11
+ from mesa.experimental.cell_space.network import Network
12
+
13
+ __all__ = [
14
+ "CellCollection",
15
+ "Cell",
16
+ "CellAgent",
17
+ "DiscreteSpace",
18
+ "Grid",
19
+ "HexGrid",
20
+ "OrthogonalMooreGrid",
21
+ "OrthogonalVonNeumannGrid",
22
+ "Network",
23
+ ]
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import cache
4
+ from random import Random
5
+ from typing import TYPE_CHECKING
6
+
7
+ from mesa.experimental.cell_space.cell_collection import CellCollection
8
+
9
+ if TYPE_CHECKING:
10
+ from mesa.experimental.cell_space.cell_agent import CellAgent
11
+
12
+
13
+ class Cell:
14
+ """The cell represents a position in a discrete space.
15
+
16
+ Attributes:
17
+ coordinate (Tuple[int, int]) : the position of the cell in the discrete space
18
+ agents (List[Agent]): the agents occupying the cell
19
+ capacity (int): the maximum number of agents that can simultaneously occupy the cell
20
+ properties (dict[str, Any]): the properties of the cell
21
+ random (Random): the random number generator
22
+
23
+ """
24
+
25
+ __slots__ = [
26
+ "coordinate",
27
+ "_connections",
28
+ "agents",
29
+ "capacity",
30
+ "properties",
31
+ "random",
32
+ ]
33
+
34
+ # def __new__(cls,
35
+ # coordinate: tuple[int, ...],
36
+ # capacity: float | None = None,
37
+ # random: Random | None = None,):
38
+ # if capacity != 1:
39
+ # return object.__new__(cls)
40
+ # else:
41
+ # return object.__new__(SingleAgentCell)
42
+
43
+ def __init__(
44
+ self,
45
+ coordinate: tuple[int, ...],
46
+ capacity: float | None = None,
47
+ random: Random | None = None,
48
+ ) -> None:
49
+ """ "
50
+
51
+ Args:
52
+ coordinate:
53
+ capacity (int) : the capacity of the cell. If None, the capacity is infinite
54
+ random (Random) : the random number generator to use
55
+
56
+ """
57
+ super().__init__()
58
+ self.coordinate = coordinate
59
+ self._connections: list[Cell] = [] # TODO: change to CellCollection?
60
+ self.agents = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
61
+ self.capacity = capacity
62
+ self.properties: dict[str, object] = {}
63
+ self.random = random
64
+
65
+ def connect(self, other: Cell) -> None:
66
+ """Connects this cell to another cell.
67
+
68
+ Args:
69
+ other (Cell): other cell to connect to
70
+
71
+ """
72
+ self._connections.append(other)
73
+
74
+ def disconnect(self, other: Cell) -> None:
75
+ """Disconnects this cell from another cell.
76
+
77
+ Args:
78
+ other (Cell): other cell to remove from connections
79
+
80
+ """
81
+ self._connections.remove(other)
82
+
83
+ def add_agent(self, agent: CellAgent) -> None:
84
+ """Adds an agent to the cell.
85
+
86
+ Args:
87
+ agent (CellAgent): agent to add to this Cell
88
+
89
+ """
90
+ n = len(self.agents)
91
+
92
+ if self.capacity and n >= self.capacity:
93
+ raise Exception(
94
+ "ERROR: Cell is full"
95
+ ) # FIXME we need MESA errors or a proper error
96
+
97
+ self.agents.append(agent)
98
+
99
+ def remove_agent(self, agent: CellAgent) -> None:
100
+ """Removes an agent from the cell.
101
+
102
+ Args:
103
+ agent (CellAgent): agent to remove from this cell
104
+
105
+ """
106
+ self.agents.remove(agent)
107
+ agent.cell = None
108
+
109
+ @property
110
+ def is_empty(self) -> bool:
111
+ """Returns a bool of the contents of a cell."""
112
+ return len(self.agents) == 0
113
+
114
+ @property
115
+ def is_full(self) -> bool:
116
+ """Returns a bool of the contents of a cell."""
117
+ return len(self.agents) == self.capacity
118
+
119
+ def __repr__(self):
120
+ return f"Cell({self.coordinate}, {self.agents})"
121
+
122
+ # FIXME: Revisit caching strategy on methods
123
+ @cache # noqa: B019
124
+ def neighborhood(self, radius=1, include_center=False):
125
+ return CellCollection(
126
+ self._neighborhood(radius=radius, include_center=include_center),
127
+ random=self.random,
128
+ )
129
+
130
+ # FIXME: Revisit caching strategy on methods
131
+ @cache # noqa: B019
132
+ def _neighborhood(self, radius=1, include_center=False):
133
+ # if radius == 0:
134
+ # return {self: self.agents}
135
+ if radius < 1:
136
+ raise ValueError("radius must be larger than one")
137
+ if radius == 1:
138
+ neighborhood = {neighbor: neighbor.agents for neighbor in self._connections}
139
+ if not include_center:
140
+ return neighborhood
141
+ else:
142
+ neighborhood[self] = self.agents
143
+ return neighborhood
144
+ else:
145
+ neighborhood = {}
146
+ for neighbor in self._connections:
147
+ neighborhood.update(
148
+ neighbor._neighborhood(radius - 1, include_center=True)
149
+ )
150
+ if not include_center:
151
+ neighborhood.pop(self, None)
152
+ return neighborhood
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from mesa import Agent, Model
6
+
7
+ if TYPE_CHECKING:
8
+ from mesa.experimental.cell_space.cell import Cell
9
+
10
+
11
+ class CellAgent(Agent):
12
+ """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces
13
+
14
+
15
+ Attributes:
16
+ unique_id (int): A unique identifier for this agent.
17
+ model (Model): The model instance to which the agent belongs
18
+ pos: (Position | None): The position of the agent in the space
19
+ cell: (Cell | None): the cell which the agent occupies
20
+ """
21
+
22
+ def __init__(self, unique_id: int, model: Model) -> None:
23
+ """
24
+ Create a new agent.
25
+
26
+ Args:
27
+ unique_id (int): A unique identifier for this agent.
28
+ model (Model): The model instance in which the agent exists.
29
+ """
30
+ super().__init__(unique_id, model)
31
+ self.cell: Cell | None = None
32
+
33
+ def move_to(self, cell) -> None:
34
+ if self.cell is not None:
35
+ self.cell.remove_agent(self)
36
+ self.cell = cell
37
+ cell.add_agent(self)
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ from collections.abc import Iterable, Mapping
5
+ from functools import cached_property
6
+ from random import Random
7
+ from typing import TYPE_CHECKING, Callable, Generic, TypeVar
8
+
9
+ if TYPE_CHECKING:
10
+ from mesa.experimental.cell_space.cell import Cell
11
+ from mesa.experimental.cell_space.cell_agent import CellAgent
12
+
13
+ T = TypeVar("T", bound="Cell")
14
+
15
+
16
+ class CellCollection(Generic[T]):
17
+ """An immutable collection of cells
18
+
19
+ Attributes:
20
+ cells (List[Cell]): The list of cells this collection represents
21
+ agents (List[CellAgent]) : List of agents occupying the cells in this collection
22
+ random (Random) : The random number generator
23
+
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ cells: Mapping[T, list[CellAgent]] | Iterable[T],
29
+ random: Random | None = None,
30
+ ) -> None:
31
+ if isinstance(cells, dict):
32
+ self._cells = cells
33
+ else:
34
+ self._cells = {cell: cell.agents for cell in cells}
35
+
36
+ #
37
+ self._capacity: int = next(iter(self._cells.keys())).capacity
38
+
39
+ if random is None:
40
+ random = Random() # FIXME
41
+ self.random = random
42
+
43
+ def __iter__(self):
44
+ return iter(self._cells)
45
+
46
+ def __getitem__(self, key: T) -> Iterable[CellAgent]:
47
+ return self._cells[key]
48
+
49
+ # @cached_property
50
+ def __len__(self) -> int:
51
+ return len(self._cells)
52
+
53
+ def __repr__(self):
54
+ return f"CellCollection({self._cells})"
55
+
56
+ @cached_property
57
+ def cells(self) -> list[T]:
58
+ return list(self._cells.keys())
59
+
60
+ @property
61
+ def agents(self) -> Iterable[CellAgent]:
62
+ return itertools.chain.from_iterable(self._cells.values())
63
+
64
+ def select_random_cell(self) -> T:
65
+ return self.random.choice(self.cells)
66
+
67
+ def select_random_agent(self) -> CellAgent:
68
+ return self.random.choice(list(self.agents))
69
+
70
+ def select(self, filter_func: Callable[[T], bool] | None = None, n=0):
71
+ # FIXME: n is not considered
72
+ if filter_func is None and n == 0:
73
+ return self
74
+
75
+ return CellCollection(
76
+ {
77
+ cell: agents
78
+ for cell, agents in self._cells.items()
79
+ if filter_func is None or filter_func(cell)
80
+ }
81
+ )
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import cached_property
4
+ from random import Random
5
+ from typing import Generic, TypeVar
6
+
7
+ from mesa.experimental.cell_space.cell import Cell
8
+ from mesa.experimental.cell_space.cell_collection import CellCollection
9
+
10
+ T = TypeVar("T", bound=Cell)
11
+
12
+
13
+ class DiscreteSpace(Generic[T]):
14
+ """Base class for all discrete spaces.
15
+
16
+ Attributes:
17
+ capacity (int): The capacity of the cells in the discrete space
18
+ all_cells (CellCollection): The cells composing the discrete space
19
+ random (Random): The random number generator
20
+ cell_klass (Type) : the type of cell class
21
+ empties (CellCollection) : collecction of all cells that are empty
22
+
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ capacity: int | None = None,
28
+ cell_klass: type[T] = Cell,
29
+ random: Random | None = None,
30
+ ):
31
+ super().__init__()
32
+ self.capacity = capacity
33
+ self._cells: dict[tuple[int, ...], T] = {}
34
+ if random is None:
35
+ random = Random() # FIXME should default to default rng from model
36
+ self.random = random
37
+ self.cell_klass = cell_klass
38
+
39
+ self._empties: dict[tuple[int, ...], None] = {}
40
+ self._empties_initialized = False
41
+
42
+ @property
43
+ def cutoff_empties(self):
44
+ return 7.953 * len(self._cells) ** 0.384
45
+
46
+ def _connect_single_cell(self, cell: T): ...
47
+
48
+ @cached_property
49
+ def all_cells(self):
50
+ return CellCollection({cell: cell.agents for cell in self._cells.values()})
51
+
52
+ def __iter__(self):
53
+ return iter(self._cells.values())
54
+
55
+ def __getitem__(self, key):
56
+ return self._cells[key]
57
+
58
+ @property
59
+ def empties(self) -> CellCollection:
60
+ return self.all_cells.select(lambda cell: cell.is_empty)
61
+
62
+ def select_random_empty_cell(self) -> T:
63
+ """select random empty cell"""
64
+ return self.random.choice(list(self.empties))