Repository: projectmesa/mesa
Pull request #2308 (2024-09-21 08:34:29)

Remove schedulers from benchmark models.
this removes the schedulers from the benchmark models (in one case it was in the init but not used in step anyway).

github-actions[bot]: Performance benchmarks:

| Model           | Size   | Init time [95% CI]         | Run time [95% CI]          |
|-----------------|--------|----------------------------|----------------------------|
| BoltzmannWealth | small  | 🔴 +20.3% [+18.9%, +21.7%] | 🔴 +23.7% [+23.4%, +23.9%] |
| BoltzmannWealth | large  | 🔴 +28.6% [+28.1%, +29.1%] | 🔴 +22.0% [+21.1%, +22.8%] |
| Schelling       | small  | 🟢 -8.0% [-8.4%, -7.6%]    | 🔵 -1.0% [-1.2%, -0.8%]    |
| Schelling       | large  | 🟢 -9.0% [-9.4%, -8.5%]    | 🔵 -1.7% [-2.2%, -1.2%]    |
| WolfSheep       | small  | 🔵 -0.7% [-0.9%, -0.5%]    | 🔵 -0.0% [-0.2%, +0.1%]    |
| WolfSheep       | large  | 🔵 -2.9% [-3.8%, -2.0%]    | 🔵 -2.2% [-4.8%, -0.2%]    |
| BoidFlockers    | small  | 🟢 -8.0% [-8.8%, -7.4%]    | 🔵 +0.9% [+0.2%, +1.6%]    |
| BoidFlockers    | large  | 🟢 -8.4% [-8.9%, -8.0%]    | 🔵 -0.3% [-1.2%, +0.7%]    |

github-actions[bot]: Performance benchmarks:

| Model           | Size   | Init time [95% CI]         | Run time [95% CI]          |
|-----------------|--------|----------------------------|----------------------------|
| BoltzmannWealth | small  | 🔴 +23.1% [+21.9%, +24.3%] | 🔴 +24.5% [+24.3%, +24.7%] |
| BoltzmannWealth | large  | 🔴 +29.4% [+29.0%, +29.7%] | 🔴 +23.9% [+23.3%, +24.7%] |
| Schelling       | small  | 🟢 -8.0% [-8.4%, -7.6%]    | 🔵 -1.2% [-1.4%, -1.0%]    |
| Schelling       | large  | 🟢 -8.7% [-8.9%, -8.5%]    | 🔵 -1.1% [-2.1%, -0.2%]    |
| WolfSheep       | small  | 🔵 +0.1% [-0.1%, +0.2%]    | 🔵 +0.0% [-0.2%, +0.2%]    |
| WolfSheep       | large  | 🔵 -1.1% [-1.4%, -0.7%]    | 🔵 +0.3% [-0.2%, +0.7%]    |
| BoidFlockers    | small  | 🟢 -8.0% [-8.5%, -7.4%]    | 🔵 -0.4% [-1.0%, +0.2%]    |
| BoidFlockers    | large  | 🟢 -8.3% [-8.7%, -7.9%]    | 🔵 -1.7% [-2.1%, -1.1%]    |

EwoutH: I don’t understand how BoltzmannWealth gets slower while doing less (being already on `shuffle_ do()`).

Probably just a fluke.

Can you rebase?
github-actions[bot]: Performance benchmarks:

| Model           | Size   | Init time [95% CI]         | Run time [95% CI]          |
|-----------------|--------|----------------------------|----------------------------|
| BoltzmannWealth | small  | 🔴 +28.7% [+27.0%, +30.2%] | 🔴 +25.4% [+25.2%, +25.6%] |
| BoltzmannWealth | large  | 🔴 +30.0% [+29.6%, +30.3%] | 🔴 +33.5% [+32.7%, +34.3%] |
| Schelling       | small  | 🟢 -8.0% [-8.3%, -7.6%]    | 🔵 -0.1% [-0.3%, +0.1%]    |
| Schelling       | large  | 🟢 -7.6% [-8.3%, -6.9%]    | 🔴 +9.7% [+7.8%, +11.8%]   |
| WolfSheep       | small  | 🔵 +0.5% [+0.2%, +0.9%]    | 🔵 +1.8% [+1.6%, +2.0%]    |
| WolfSheep       | large  | 🔵 +2.1% [+0.1%, +4.1%]    | 🔵 +7.4% [+2.6%, +12.3%]   |
| BoidFlockers    | small  | 🟢 -7.8% [-8.4%, -7.1%]    | 🔵 +1.9% [+1.0%, +2.7%]    |
| BoidFlockers    | large  | 🟢 -9.0% [-9.2%, -8.7%]    | 🔵 +0.8% [+0.4%, +1.3%]    |

quaquel: I think it has to do with the data collection in the Boltzman wealth model. I need to dig deeper to confirm.

Patch:
diff --git a/benchmarks/BoltzmannWealth/boltzmann_wealth.py b/benchmarks/BoltzmannWealth/boltzmann_wealth.py
index 93d4da14..5423eb82 100644
--- a/benchmarks/BoltzmannWealth/boltzmann_wealth.py
+++ b/benchmarks/BoltzmannWealth/boltzmann_wealth.py
@@ -43,7 +43,6 @@ class BoltzmannWealth(mesa.Model):
         super().__init__(seed)
         self.num_agents = n
         self.grid = mesa.space.MultiGrid(width, height, True)
-        self.schedule = mesa.time.RandomActivation(self)
         self.datacollector = mesa.DataCollector(
             model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"}
         )
diff --git a/benchmarks/Flocking/flocking.py b/benchmarks/Flocking/flocking.py
index 9678f34b..d2d23b1a 100644
--- a/benchmarks/Flocking/flocking.py
+++ b/benchmarks/Flocking/flocking.py
@@ -116,7 +116,6 @@ class BoidFlockers(mesa.Model):
         self.height = height
         self.simulator = simulator
 
-        self.schedule = mesa.time.RandomActivation(self)
         self.space = mesa.space.ContinuousSpace(self.width, self.height, True)
         self.factors = {
             "cohere": cohere,
@@ -138,11 +137,10 @@ class BoidFlockers(mesa.Model):
                 **self.factors,
             )
             self.space.place_agent(boid, pos)
-            self.schedule.add(boid)
 
     def step(self):
         """Run the model for one step."""
-        self.schedule.step()
+        self.agents.shuffle_do("step")
 
 
 if __name__ == "__main__":
diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py
index 47bf521e..73ddeaba 100644
--- a/benchmarks/Schelling/schelling.py
+++ b/benchmarks/Schelling/schelling.py
@@ -2,7 +2,6 @@
 
 from mesa import Model
 from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid
-from mesa.time import RandomActivation
 
 
 class SchellingAgent(CellAgent):
@@ -67,7 +66,6 @@ class Schelling(Model):
         self.minority_pc = minority_pc
         self.simulator = simulator
 
-        self.schedule = RandomActivation(self)
         self.grid = OrthogonalMooreGrid(
             [height, width],
             torus=True,
@@ -84,12 +82,11 @@ class Schelling(Model):
                 agent_type = 1 if self.random.random() < self.minority_pc else 0
                 agent = SchellingAgent(self, agent_type, radius, homophily)
                 agent.move_to(cell)
-                self.schedule.add(agent)
 
     def step(self):
         """Run one step of the model."""
         self.happy = 0  # Reset counter of happy agents
-        self.schedule.step()
+        self.agents.shuffle_do("step")
 
 
 if __name__ == "__main__":
diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py
index 381d0f6c..08a37102 100644
--- a/mesa/experimental/cell_space/cell.py
+++ b/mesa/experimental/cell_space/cell.py
@@ -48,7 +48,7 @@ class Cell:
     def __init__(
         self,
         coordinate: Coordinate,
-        capacity: float | None = None,
+        capacity: int | None = None,
         random: Random | None = None,
     ) -> None:
         """Initialise the cell.
@@ -65,7 +65,7 @@ class Cell:
         self.agents: list[
             Agent
         ] = []  # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
-        self.capacity = capacity
+        self.capacity: int = capacity
         self.properties: dict[Coordinate, object] = {}
         self.random = random
 
diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py
index 14832d51..a8c36f51 100644
--- a/mesa/experimental/cell_space/cell_collection.py
+++ b/mesa/experimental/cell_space/cell_collection.py
@@ -83,25 +83,36 @@ class CellCollection(Generic[T]):
         """
         return self.random.choice(list(self.agents))
 
-    def select(self, filter_func: Callable[[T], bool] | None = None, n=0):
+    def select(
+        self,
+        filter_func: Callable[[T], bool] | None = None,
+        at_most: int | float = float("inf"),
+    ):
         """Select cells based on filter function.
 
         Args:
             filter_func: filter function
-            n: number of cells to select
+            at_most: The maximum amount of cells to select. Defaults to infinity.
+              - If an integer, at most the first number of matching cells is selected.
+              - If a float between 0 and 1, at most that fraction of original number of cells
 
         Returns:
             CellCollection
 
         """
-        # FIXME: n is not considered
-        if filter_func is None and n == 0:
+        if filter_func is None and at_most == float("inf"):
             return self
 
-        return CellCollection(
-            {
-                cell: agents
-                for cell, agents in self._cells.items()
-                if filter_func is None or filter_func(cell)
-            }
-        )
+        if at_most <= 1.0 and isinstance(at_most, float):
+            at_most = int(len(self) * at_most)  # Note that it rounds down (floor)
+
+        def cell_generator(filter_func, at_most):
+            count = 0
+            for cell in self:
+                if count >= at_most:
+                    break
+                if not filter_func or filter_func(cell):
+                    yield cell
+                    count += 1
+
+        return CellCollection(cell_generator(filter_func, at_most))


Test patch:
diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py
index 5ef4c44e..64d27566 100644
--- a/tests/test_cell_space.py
+++ b/tests/test_cell_space.py
@@ -500,3 +500,15 @@ def test_cell_collection():
 
     agents = collection[cells[0]]
     assert agents == cells[0].agents
+
+    cell = collection.select(at_most=1)
+    assert len(cell) == 1
+
+    cells = collection.select(at_most=2)
+    assert len(cells) == 2
+
+    cells = collection.select(at_most=0.5)
+    assert len(cells) == 5
+
+    cells = collection.select()
+    assert len(cells) == len(collection)