Repository: projectmesa/mesa
Pull request #2314 (2024-09-21 15:36:55)

Migration guide: Intro, upgrade strategy, model.agents, headers
For the migration guide:
- Write a short introduction
- Add an recommended upgrade strategy
- Add some notes on model.agents
- Make whitespaces and headers more consistent


Patch:
diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py
index 73ddeaba..52519efc 100644
--- a/benchmarks/Schelling/schelling.py
+++ b/benchmarks/Schelling/schelling.py
@@ -24,7 +24,7 @@ class SchellingAgent(CellAgent):
     def step(self):
         """Run one step of the agent."""
         similar = 0
-        neighborhood = self.cell.neighborhood(radius=self.radius)
+        neighborhood = self.cell.get_neighborhood(radius=self.radius)
         for neighbor in neighborhood.agents:
             if neighbor.type == self.type:
                 similar += 1
diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py
index 16c01e00..aaeed135 100644
--- a/benchmarks/WolfSheep/wolf_sheep.py
+++ b/benchmarks/WolfSheep/wolf_sheep.py
@@ -33,7 +33,7 @@ class Animal(CellAgent):
 
     def random_move(self):
         """Move to a random neighboring cell."""
-        self.move_to(self.cell.neighborhood().select_random_cell())
+        self.move_to(self.cell.neighborhood.select_random_cell())
 
     def spawn_offspring(self):
         """Create offspring."""
diff --git a/docs/migration_guide.md b/docs/migration_guide.md
index 8eca2e39..5b24b72f 100644
--- a/docs/migration_guide.md
+++ b/docs/migration_guide.md
@@ -3,11 +3,22 @@ This guide contains breaking changes between major Mesa versions and how to reso
 
 Non-breaking changes aren't included, for those see our [Release history](https://github.com/projectmesa/mesa/releases).
 
+
 ## Mesa 3.0
-<!-- TODO small introduction-->
+Mesa 3.0 introduces significant changes to core functionalities, including agent and model initialization, scheduling, and visualization. The guide below outlines these changes and provides instructions for migrating your existing Mesa projects to version 3.0.
 
 _This guide is a work in progress. The development of it is tracked in [Issue #2233](https://github.com/projectmesa/mesa/issues/2233)._
 
+
+### Upgrade strategy
+We recommend the following upgrade strategy:
+- Update the the latest Mesa 2.x release (`mesa<3`).
+- Update the the latest Mesa 3.0.x release (`mesa<3.1`).
+- Update to the latest Mesa 3.x release (`mesa<4`).
+
+With each update, resolve all errors and warnings, before updating to the next one.
+
+
 ### Reserved and private variables
 <!-- TODO: Update this section based on https://github.com/projectmesa/mesa/discussions/2230 -->
 
@@ -17,6 +28,7 @@ Currently, we have reserved the following variables:
   - Agent: `unique_id`, `model`.
 
 You can use (read) any reserved variable, but Mesa may update them automatically and rely on them, so modify/update at your own risk.
+
 #### Private variables
 Any variables starting with an underscore (`_`) are considered private and for Mesa's internal use. We might use any of those. Modifying or overwriting any private variable is at your own risk.
 
@@ -75,11 +87,27 @@ In Mesa 3.0, `unique_id` for agents is now automatically assigned, simplifying a
 
 
 ### AgentSet and `Model.agents`
-#### AgentSet
-<!-- TODO  -->
+In Mesa 3.0, the Model class internally manages agents using several data structures:
+
+- `self._agents`: A dictionary containing hard references to all agents, indexed by their `unique_id`.
+- `self._agents_by_type`: A dictionary of AgentSets, organizing agents by their type.
+- `self._all_agents`: An AgentSet containing all agents in the model.
+
+These internal structures are used to efficiently manage and access agents. Users should interact with agents through the public `model.agents` property, which returns the `self._all_agents` AgentSet.
 
 #### `Model.agents`
-<!-- TODO  -->
+- Attempting to set `model.agents` now raises an `AttributeError` instead of a warning. This attribute is reserved for internal use by Mesa.
+- If you were previously setting `model.agents` in your code, you must update it to use a different attribute name for custom agent storage.
+
+For example, replace:
+```python
+model.agents = my_custom_agents
+```
+
+With:
+```python
+model.custom_agents = my_custom_agents
+```
 
 
 ### Time and schedulers
@@ -99,12 +127,12 @@ You can access it by `Model.steps`, and it's internally in the datacollector, ba
 #### Removal of `Model._advance_time()`
 - The `Model._advance_time()` method is removed. This now happens automatically.
 
-### Replacing Schedulers with AgentSet functionality
+#### Replacing Schedulers with AgentSet functionality
 The whole Time module in Mesa is deprecated, and all schedulers are being replaced with AgentSet functionality and the internal `Model.steps` counter. This allows much more flexibility in how to activate Agents and makes it explicit what's done exactly.
 
 Here's how to replace each scheduler:
 
-#### BaseScheduler
+##### BaseScheduler
 Replace:
 ```python
 self.schedule = BaseScheduler(self)
@@ -115,7 +143,7 @@ With:
 self.agents.do("step")
 ```
 
-#### RandomActivation
+##### RandomActivation
 Replace:
 ```python
 self.schedule = RandomActivation(self)
@@ -126,7 +154,7 @@ With:
 self.agents.shuffle_do("step")
 ```
 
-#### SimultaneousActivation
+##### SimultaneousActivation
 Replace:
 ```python
 self.schedule = SimultaneousActivation(self)
@@ -138,7 +166,7 @@ self.agents.do("step")
 self.agents.do("advance")
 ```
 
-#### StagedActivation
+##### StagedActivation
 Replace:
 ```python
 self.schedule = StagedActivation(self, ["stage1", "stage2", "stage3"])
@@ -162,7 +190,7 @@ for stage in stages:
         self.agents.do(stage)
 ```
 
-#### RandomActivationByType
+##### RandomActivationByType
 Replace:
 ```python
 self.schedule = RandomActivationByType(self)
@@ -174,7 +202,7 @@ for agent_class in self.agent_types:
     self.agents_by_type[agent_class].shuffle_do("step")
 ```
 
-##### Replacing `step_type`
+###### Replacing `step_type`
 The `RandomActivationByType` scheduler had a `step_type` method that allowed stepping only agents of a specific type. To replicate this functionality using AgentSet:
 
 Replace:
@@ -187,7 +215,7 @@ With:
 self.agents_by_type[AgentType].shuffle_do("step")
 ```
 
-#### General Notes
+##### General Notes
 
 1. The `Model.steps` counter is now automatically incremented. You don't need to manage it manually.
 2. If you were using `self.schedule.agents`, replace it with `self.agents`.
diff --git a/docs/tutorials/MoneyModel.py b/docs/tutorials/MoneyModel.py
index d723a52b..08b89726 100644
--- a/docs/tutorials/MoneyModel.py
+++ b/docs/tutorials/MoneyModel.py
@@ -63,8 +63,8 @@ class MoneyModel(mesa.Model):
         self.schedule = mesa.time.RandomActivation(self)
 
         # Create agents
-        for i in range(self.num_agents):
-            a = MoneyAgent(i, self)
+        for _ in range(self.num_agents):
+            a = MoneyAgent(self)
             self.schedule.add(a)
             # Add the agent to a random grid cell
             x = self.random.randrange(self.grid.width)
diff --git a/docs/tutorials/intro_tutorial.ipynb b/docs/tutorials/intro_tutorial.ipynb
index 28b855b3..d791f7d1 100644
--- a/docs/tutorials/intro_tutorial.ipynb
+++ b/docs/tutorials/intro_tutorial.ipynb
@@ -194,9 +194,9 @@
     "class MoneyAgent(mesa.Agent):\n",
     "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
     "\n",
-    "    def __init__(self, unique_id, model):\n",
+    "    def __init__(self, model):\n",
     "        # Pass the parameters to the parent class.\n",
-    "        super().__init__(unique_id, model)\n",
+    "        super().__init__(model)\n",
     "\n",
     "        # Create the agent's variable and set the initial values.\n",
     "        self.wealth = 1"
@@ -232,8 +232,8 @@
     "        super().__init__()\n",
     "        self.num_agents = N\n",
     "        # Create agents\n",
-    "        for i in range(self.num_agents):\n",
-    "            a = MoneyAgent(i, self)"
+    "        for _ in range(self.num_agents):\n",
+    "            a = MoneyAgent(self)"
    ]
   },
   {
@@ -261,9 +261,9 @@
     "class MoneyAgent(mesa.Agent):\n",
     "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
     "\n",
-    "    def __init__(self, unique_id, model):\n",
+    "    def __init__(self, model):\n",
     "        # Pass the parameters to the parent class.\n",
-    "        super().__init__(unique_id, model)\n",
+    "        super().__init__(model)\n",
     "\n",
     "        # Create the agent's attribute and set the initial values.\n",
     "        self.wealth = 1\n",
@@ -284,8 +284,8 @@
     "        self.schedule = mesa.time.RandomActivation(self)\n",
     "\n",
     "        # Create agents\n",
-    "        for i in range(self.num_agents):\n",
-    "            a = MoneyAgent(i, self)\n",
+    "        for _ in range(self.num_agents):\n",
+    "            a = MoneyAgent(self)\n",
     "            # Add the agent to the scheduler\n",
     "            self.schedule.add(a)\n",
     "\n",
@@ -357,9 +357,9 @@
     "class MoneyAgent(mesa.Agent):\n",
     "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
     "\n",
-    "    def __init__(self, unique_id, model):\n",
+    "    def __init__(self, model):\n",
     "        # Pass the parameters to the parent class.\n",
-    "        super().__init__(unique_id, model)\n",
+    "        super().__init__(model)\n",
     "\n",
     "        # Create the agent's variable and set the initial values.\n",
     "        self.wealth = 1\n",
@@ -424,9 +424,9 @@
     "class MoneyAgent(mesa.Agent):\n",
     "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
     "\n",
-    "    def __init__(self, unique_id, model):\n",
+    "    def __init__(self, model):\n",
     "        # Pass the parameters to the parent class.\n",
-    "        super().__init__(unique_id, model)\n",
+    "        super().__init__(model)\n",
     "\n",
     "        # Create the agent's variable and set the initial values.\n",
     "        self.wealth = 1\n",
@@ -464,7 +464,7 @@
    "outputs": [],
    "source": [
     "model = MoneyModel(10)\n",
-    "for i in range(10):\n",
+    "for _ in range(10):\n",
     "    model.step()"
    ]
   },
@@ -511,10 +511,10 @@
    "source": [
     "all_wealth = []\n",
     "# This runs the model 100 times, each model executing 10 steps.\n",
-    "for j in range(100):\n",
+    "for _ in range(100):\n",
     "    # Run the model\n",
     "    model = MoneyModel(10)\n",
-    "    for i in range(10):\n",
+    "    for _ in range(10):\n",
     "        model.step()\n",
     "\n",
     "    # Store the results\n",
@@ -573,8 +573,8 @@
     "        self.schedule = mesa.time.RandomActivation(self)\n",
     "\n",
     "        # Create agents\n",
-    "        for i in range(self.num_agents):\n",
-    "            a = MoneyAgent(i, self)\n",
+    "        for _ in range(self.num_agents):\n",
+    "            a = MoneyAgent(self)\n",
     "            self.schedule.add(a)\n",
     "\n",
     "            # Add the agent to a random grid cell\n",
@@ -653,8 +653,8 @@
     "class MoneyAgent(mesa.Agent):\n",
     "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
     "\n",
-    "    def __init__(self, unique_id, model):\n",
-    "        super().__init__(unique_id, model)\n",
+    "    def __init__(self, model):\n",
+    "        super().__init__(model)\n",
     "        self.wealth = 1\n",
     "\n",
     "    def move(self):\n",
@@ -686,8 +686,8 @@
     "        self.grid = mesa.space.MultiGrid(width, height, True)\n",
     "        self.schedule = mesa.time.RandomActivation(self)\n",
     "        # Create agents\n",
-    "        for i in range(self.num_agents):\n",
-    "            a = MoneyAgent(i, self)\n",
+    "        for _ in range(self.num_agents):\n",
+    "            a = MoneyAgent(self)\n",
     "            self.schedule.add(a)\n",
     "            # Add the agent to a random grid cell\n",
     "            x = self.random.randrange(self.grid.width)\n",
@@ -712,7 +712,7 @@
    "outputs": [],
    "source": [
     "model = MoneyModel(100, 10, 10)\n",
-    "for i in range(20):\n",
+    "for _ in range(20):\n",
     "    model.step()"
    ]
   },
@@ -771,8 +771,8 @@
     "class MoneyAgent(mesa.Agent):\n",
     "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
     "\n",
-    "    def __init__(self, unique_id, model):\n",
-    "        super().__init__(unique_id, model)\n",
+    "    def __init__(self, model):\n",
+    "        super().__init__(model)\n",
     "        self.wealth = 1\n",
     "\n",
     "    def move(self):\n",
@@ -810,8 +810,8 @@
     "        self.schedule = mesa.time.RandomActivation(self)\n",
     "\n",
     "        # Create agents\n",
-    "        for i in range(self.num_agents):\n",
-    "            a = MoneyAgent(i, self)\n",
+    "        for _ in range(self.num_agents):\n",
+    "            a = MoneyAgent(self)\n",
     "            self.schedule.add(a)\n",
     "            # Add the agent to a random grid cell\n",
     "            x = self.random.randrange(self.grid.width)\n",
@@ -845,7 +845,7 @@
    "outputs": [],
    "source": [
     "model = MoneyModel(100, 10, 10)\n",
-    "for i in range(100):\n",
+    "for _ in range(100):\n",
     "    model.step()"
    ]
   },
@@ -1052,8 +1052,8 @@
     "        self.running = True\n",
     "\n",
     "        # Create agents\n",
-    "        for i in range(self.num_agents):\n",
-    "            a = MoneyAgent(i, self)\n",
+    "        for _ in range(self.num_agents):\n",
+    "            a = MoneyAgent(self)\n",
     "            self.schedule.add(a)\n",
     "            # Add the agent to a random grid cell\n",
     "            x = self.random.randrange(self.grid.width)\n",
@@ -1073,8 +1073,8 @@
     "class MoneyAgent(mesa.Agent):\n",
     "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
     "\n",
-    "    def __init__(self, unique_id, model):\n",
-    "        super().__init__(unique_id, model)\n",
+    "    def __init__(self, model):\n",
+    "        super().__init__(model)\n",
     "        self.wealth = 1\n",
     "        self.steps_not_given = 0\n",
     "\n",
diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py
index 08a37102..51b2ad75 100644
--- a/mesa/experimental/cell_space/cell.py
+++ b/mesa/experimental/cell_space/cell.py
@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from functools import cache
+from functools import cache, cached_property
 from random import Random
 from typing import TYPE_CHECKING
 
@@ -34,6 +34,7 @@ class Cell:
         "capacity",
         "properties",
         "random",
+        "__dict__",
     ]
 
     # def __new__(cls,
@@ -131,10 +132,33 @@ class Cell:
     def __repr__(self):  # noqa
         return f"Cell({self.coordinate}, {self.agents})"
 
+    @cached_property
+    def neighborhood(self) -> CellCollection:
+        """Returns the direct neighborhood of the cell.
+
+        This is equivalent to cell.get_neighborhood(radius=1)
+
+        """
+        return self.get_neighborhood()
+
     # FIXME: Revisit caching strategy on methods
     @cache  # noqa: B019
-    def neighborhood(self, radius: int = 1, include_center: bool = False):
-        """Returns a list of all neighboring cells."""
+    def get_neighborhood(
+        self, radius: int = 1, include_center: bool = False
+    ) -> CellCollection:
+        """Returns a list of all neighboring cells for the given radius.
+
+        For getting the direct neighborhood (i.e., radius=1) you can also use
+        the `neighborhood` property.
+
+        Args:
+            radius (int): the radius of the neighborhood
+            include_center (bool): include the center of the neighborhood
+
+        Returns:
+            a list of all neighboring cells
+
+        """
         return CellCollection[Cell](
             self._neighborhood(radius=radius, include_center=include_center),
             random=self.random,


Test patch:
diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py
index 64d27566..01420043 100644
--- a/tests/test_cell_space.py
+++ b/tests/test_cell_space.py
@@ -280,7 +280,10 @@ def test_cell_neighborhood():
     height = 10
     grid = OrthogonalVonNeumannGrid((width, height), torus=False, capacity=None)
     for radius, n in zip(range(1, 4), [2, 5, 9]):
-        neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius)
+        if radius == 1:
+            neighborhood = grid._cells[(0, 0)].neighborhood
+        else:
+            neighborhood = grid._cells[(0, 0)].get_neighborhood(radius=radius)
         assert len(neighborhood) == n
 
     ## Moore
@@ -288,25 +291,34 @@ def test_cell_neighborhood():
     height = 10
     grid = OrthogonalMooreGrid((width, height), torus=False, capacity=None)
     for radius, n in zip(range(1, 4), [3, 8, 15]):
-        neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius)
+        if radius == 1:
+            neighborhood = grid._cells[(0, 0)].neighborhood
+        else:
+            neighborhood = grid._cells[(0, 0)].get_neighborhood(radius=radius)
         assert len(neighborhood) == n
 
     with pytest.raises(ValueError):
-        grid._cells[(0, 0)].neighborhood(radius=0)
+        grid._cells[(0, 0)].get_neighborhood(radius=0)
 
     # hexgrid
     width = 10
     height = 10
     grid = HexGrid((width, height), torus=False, capacity=None)
     for radius, n in zip(range(1, 4), [2, 6, 11]):
-        neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius)
+        if radius == 1:
+            neighborhood = grid._cells[(0, 0)].neighborhood
+        else:
+            neighborhood = grid._cells[(0, 0)].get_neighborhood(radius=radius)
         assert len(neighborhood) == n
 
     width = 10
     height = 10
     grid = HexGrid((width, height), torus=False, capacity=None)
     for radius, n in zip(range(1, 4), [5, 10, 17]):
-        neighborhood = grid._cells[(1, 0)].neighborhood(radius=radius)
+        if radius == 1:
+            neighborhood = grid._cells[(1, 0)].neighborhood
+        else:
+            neighborhood = grid._cells[(1, 0)].get_neighborhood(radius=radius)
         assert len(neighborhood) == n
 
     # networkgrid