Repository: projectmesa/mesa
Pull request #2309 (2024-09-21 08:45:37)

Have a dedicated neighborhood property and a get_neighborhood method on Cell
This picks up on a discussion in #2296. In the current code, we have method named `Cell.neighborhood`. This is an unfortunate name. So we agreed to split this into a property for the direct neighborhood (i.e. radius = 1) and a `get_neighborhood` method for larger radii. We had a bit of discussion on the names, but I decided to keep it simple: `neighborhood` and `get_neighborhood`. I have updated the docstrings to clearly explain when to use which one. 

quaquel: just a quick observation about cellspaces. A random walk is now a one liner: `self.cell.neighborhood.select_random_cell()`.
EwoutH: That’s really nice!

I would appreciate a deprecationwarning for `neighborhood()`
quaquel: > I would appreciate a deprecationwarning for neighborhood()

AFAIK that is not possible because the name clashes with the property. 
EwoutH: It looks like this does introduce some errors.
github-actions[bot]: Performance benchmarks:

| Model           | Size   | Init time [95% CI]         | Run time [95% CI]          |
|-----------------|--------|----------------------------|----------------------------|
| BoltzmannWealth | small  | 🟢 -15.6% [-16.6%, -14.7%] | 🟢 -19.6% [-19.7%, -19.5%] |
| BoltzmannWealth | large  | 🟢 -22.9% [-23.1%, -22.7%] | 🟢 -17.7% [-18.3%, -17.1%] |
| Schelling       | small  | 🔴 +11.3% [+11.0%, +11.7%] | 🔵 +1.7% [+1.5%, +1.8%]    |
| Schelling       | large  | 🔴 +11.6% [+11.1%, +12.1%] | 🔵 +0.4% [-1.0%, +1.7%]    |
| WolfSheep       | small  | 🔵 +1.4% [+1.2%, +1.6%]    | 🔵 +2.2% [+2.0%, +2.5%]    |
| WolfSheep       | large  | 🔵 +1.9% [+1.7%, +2.1%]    | 🔵 +1.4% [+1.0%, +1.8%]    |
| BoidFlockers    | small  | 🔴 +10.6% [+9.9%, +11.2%]  | 🔵 -0.7% [-1.5%, -0.1%]    |
| BoidFlockers    | large  | 🔴 +10.1% [+9.6%, +10.6%]  | 🔵 +0.4% [-0.4%, +1.0%]    |

EwoutH: These benchmark results are exactly opposite of https://github.com/projectmesa/mesa/pull/2308.

So seems to cancel out and are just based on an outdated branch or whatever.
github-actions[bot]: Performance benchmarks:

| Model           | Size   | Init time [95% CI]         | Run time [95% CI]          |
|-----------------|--------|----------------------------|----------------------------|
| BoltzmannWealth | small  | 🟢 -20.5% [-21.6%, -19.4%] | 🟢 -19.2% [-19.3%, -19.1%] |
| BoltzmannWealth | large  | 🟢 -22.8% [-23.0%, -22.5%] | 🟢 -18.1% [-18.9%, -17.4%] |
| Schelling       | small  | 🔴 +10.6% [+10.2%, +11.1%] | 🔵 +1.7% [+1.5%, +1.9%]    |
| Schelling       | large  | 🔴 +12.2% [+11.6%, +12.8%] | 🔴 +8.6% [+6.3%, +10.6%]   |
| WolfSheep       | small  | 🔵 +0.4% [+0.3%, +0.6%]    | 🔵 +1.0% [+0.7%, +1.2%]    |
| WolfSheep       | large  | 🔵 +1.7% [+0.7%, +2.8%]    | 🔴 +5.5% [+3.3%, +7.5%]    |
| BoidFlockers    | small  | 🔴 +12.7% [+12.2%, +13.3%] | 🔵 +0.9% [+0.2%, +1.5%]    |
| BoidFlockers    | large  | 🔴 +12.0% [+11.7%, +12.4%] | 🔵 +0.4% [+0.1%, +0.8%]    |


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/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py
index 08a37102..4ee0c5ea 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 neigborhood 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