Repository: projectmesa/mesa
Pull request #2473 (2024-11-09 10:38:55)

Integrate benchmarks and example models
This PR integrates the benchmark models into the example models and runs the benchmarks on the example models.

The first commit moves over the benchmark scripts to run on the example models, the following commits merge the benchmark models into the example models one-by-one.


github-actions[bot]: Performance benchmarks:


EwoutH: Ready for review. Note that wolf_sheep uses experimental features like a ABMSimulator and the Cell space, while the other models are basic models only using stable features.

I added a lot of docstring in the process (maybe too much, let me know). Rendered docs: https://mesa--2473.org.readthedocs.build/2473/examples.html
github-actions[bot]: Performance benchmarks:


quaquel: just a quick clarifying question: this will break app.py for wolf-sheep until #2470 is merged. So, what is your envisioned timeline on both?
github-actions[bot]: Performance benchmarks:


EwoutH: Fair, didn't think about that yet. We can do a few things:

1. Split off wolf_sheep from this PR
2. Wait for #2470
3. Merge after 3.0-branch cutoff
4. Accept one of our advanced examples has temporarily broken visualisation

It depends I think if #2470 will be API breaking of not and when its ready. If it's non-breaking, it can go into 3.0.1.
quaquel: As indicated in #2470, it won't be API breaking. It is also complete accept for the startup problem. I just have to decide on how to address that. I might be able to put a monkey patch in place. Once #2291 is merged, we can then go back in an do it cleaner for 3.1. All of this would still be implementatin changes, no API changes. 
EwoutH: I propose merging this as is, and following up the visualisation with the SolaraViz PR. Then that PR can directly demonstrate how an model or app.py needs to be updated to work with such a visualisation (if at all).
quaquel: i'll try to take a look at the other examples later today.
Corvince: > We will tag 3.0.1 when both are merged, so our stable docs keep working the whole time.

At first I thought we shouldn't be so lenient with semantic versioning, since #2470 actually changes the API and thus should be 3.1. But it think both changes of 2470 can also be considered as bug fixes. Removing the monkey patch is good since we then no longer change the underlying model. And supporting DEVS is something that should have also been possible in 3.0, so I am fine with releasing as 3.0.1. 

Corvince: And It would be great to get it through this weekend. But after that let's be a bit more stringent with semantic versioning. No problem if we already reach something like 3.4 before the end of the year. 
EwoutH: Agree on both points!
quaquel: > At first I thought we shouldn't be so lenient with semantic versioning, since https://github.com/projectmesa/mesa/pull/2470 actually changes the API and thus should be 3.1.

Just a clarifying question: when is something an API change? #2470 adds a keyword argument to `SolaraViz`, but past calls are still valid. Likewise, I don't change anything on the outside of devs, just slight changes in behavior inside the methods. 
EwoutH: [Official spec](https://semver.org/) says:

> Patch version Z (x.y.Z | x > 0) MUST be incremented if only backward compatible bug fixes are introduced. A bug fix is defined as an internal change that fixes incorrect behavior.
> 
> Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backward compatible functionality is introduced to the public API. It MUST be incremented if any public API functionality is marked as deprecated. It MAY be incremented if substantial new functionality or improvements are introduced within the private code. It MAY include patch level changes. Patch version MUST be reset to 0 when minor version is incremented.
> 
> Major version X (X.y.z | X > 0) MUST be incremented if any backward incompatible changes are introduced to the public API. It MAY also include minor and patch level changes. Patch and minor versions MUST be reset to 0 when major version is incremented.

But breaking things is the thing the main thing you don't want to do. Considering:
- We declared SolaraViz experimental
- The Cell Space is experimental
- We break nothing
- 3.0 has been out for an extremely short time
 
We're fine doing it in a patch instead of an minor release.

But yeah, something like a minor release a month, there's nothing wrong with that.
Corvince: > Just a clarifying question: when is something an API change? #2470 adds a keyword argument to SolaraViz, but past calls are still valid. Likewise, I don't change anything on the outside of devs, just slight changes in behavior inside the methods.

To be a bit more specific in this example. Adding an additional keyword is changing the public API. But it's fully backwards compatible, so a x.Y.z. change and not a X.y.z. change (which would be breaking). Patch version changes (x.y.Z.) are when the public API stays the same but the internal mechanism is changed in a way to achieve the actually documented behavior. 

Patch:
diff --git a/benchmarks/BoltzmannWealth/__init__.py b/benchmarks/BoltzmannWealth/__init__.py
deleted file mode 100644
index b70e37fa..00000000
--- a/benchmarks/BoltzmannWealth/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-"""init file for BoltzmannWealth module."""
diff --git a/benchmarks/BoltzmannWealth/boltzmann_wealth.py b/benchmarks/BoltzmannWealth/boltzmann_wealth.py
deleted file mode 100644
index e15aec20..00000000
--- a/benchmarks/BoltzmannWealth/boltzmann_wealth.py
+++ /dev/null
@@ -1,112 +0,0 @@
-"""boltmann wealth model for performance benchmarking.
-
-https://github.com/projectmesa/mesa-examples/blob/main/examples/boltzmann_wealth_model_experimental/model.py
-"""
-
-import mesa
-
-
-def compute_gini(model):
-    """Calculate gini for wealth in model.
-
-    Args:
-        model: a Model instance
-
-    Returns:
-        float: gini score
-
-    """
-    agent_wealths = [agent.wealth for agent in model.agents]
-    x = sorted(agent_wealths)
-    n = model.num_agents
-    b = sum(xi * (n - i) for i, xi in enumerate(x)) / (n * sum(x))
-    return 1 + (1 / n) - 2 * b
-
-
-class BoltzmannWealth(mesa.Model):
-    """A simple model of an economy where agents exchange currency at random.
-
-    All the agents begin with one unit of currency, and each time step can give
-    a unit of currency to another agent. Note how, over time, this produces a
-    highly skewed distribution of wealth.
-    """
-
-    def __init__(self, seed=None, n=100, width=10, height=10):
-        """Initializes the model.
-
-        Args:
-            seed: the seed for random number generator
-            n: the number of agents
-            width: the width of the grid
-            height: the height of the grid
-        """
-        super().__init__(seed=seed)
-        self.num_agents = n
-        self.grid = mesa.space.MultiGrid(width, height, True)
-        self.datacollector = mesa.DataCollector(
-            model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"}
-        )
-        # Create agents
-        for _ in range(self.num_agents):
-            a = MoneyAgent(self)
-            # Add the agent to a random grid cell
-            x = self.random.randrange(self.grid.width)
-            y = self.random.randrange(self.grid.height)
-            self.grid.place_agent(a, (x, y))
-
-        self.running = True
-        self.datacollector.collect(self)
-
-    def step(self):
-        """Run the model for a single step."""
-        self.agents.shuffle_do("step")
-        # collect data
-        self.datacollector.collect(self)
-
-    def run_model(self, n):
-        """Run the model for n steps.
-
-        Args:
-            n: the number of steps for which to run the model
-
-        """
-        for _ in range(n):
-            self.step()
-
-
-class MoneyAgent(mesa.Agent):
-    """An agent with fixed initial wealth."""
-
-    def __init__(self, model):
-        """Instantiate an agent.
-
-        Args:
-            model: a Model instance
-        """
-        super().__init__(model)
-        self.wealth = 1
-
-    def move(self):
-        """Move the agent to a random neighboring cell."""
-        possible_steps = self.model.grid.get_neighborhood(
-            self.pos, moore=True, include_center=False
-        )
-        new_position = self.random.choice(possible_steps)
-        self.model.grid.move_agent(self, new_position)
-
-    def give_money(self):
-        """Give money to a random cell mate."""
-        cellmates = self.model.grid.get_cell_list_contents([self.pos])
-        cellmates.pop(
-            cellmates.index(self)
-        )  # Ensure agent is not giving money to itself
-        if len(cellmates) > 0:
-            other = self.random.choice(cellmates)
-            other.wealth += 1
-            self.wealth -= 1
-
-    def step(self):
-        """Run the agent for 1 step."""
-        self.move()
-        if self.wealth > 0:
-            self.give_money()
diff --git a/benchmarks/Flocking/__init__.py b/benchmarks/Flocking/__init__.py
deleted file mode 100644
index 684c3743..00000000
--- a/benchmarks/Flocking/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-"""initi for flocking benchmark model."""
diff --git a/benchmarks/Flocking/flocking.py b/benchmarks/Flocking/flocking.py
deleted file mode 100644
index d2d23b1a..00000000
--- a/benchmarks/Flocking/flocking.py
+++ /dev/null
@@ -1,156 +0,0 @@
-"""A Mesa implementation of Craig Reynolds's Boids flocker model.
-
-Uses numpy arrays to represent vectors.
-"""
-
-import numpy as np
-
-import mesa
-
-
-class Boid(mesa.Agent):
-    """A Boid-style flocker agent.
-
-    The agent follows three behaviors to flock:
-        - Cohesion: steering towards neighboring agents.
-        - Separation: avoiding getting too close to any other agent.
-        - Alignment: try to fly in the same direction as the neighbors.
-
-    Boids have a vision that defines the radius in which they look for their
-    neighbors to flock with. Their speed (a scalar) and direction (a vector)
-    define their movement. Separation is their desired minimum distance from
-    any other Boid.
-    """
-
-    def __init__(
-        self,
-        model,
-        speed,
-        direction,
-        vision,
-        separation,
-        cohere=0.03,
-        separate=0.015,
-        match=0.05,
-    ):
-        """Create a new Boid flocker agent.
-
-        Args:
-            model: a Model instance
-            speed: Distance to move per step.
-            direction: numpy vector for the Boid's direction of movement.
-            vision: Radius to look around for nearby Boids.
-            separation: Minimum distance to maintain from other Boids.
-            cohere: the relative importance of matching neighbors' positions
-            separate: the relative importance of avoiding close neighbors
-            match: the relative importance of matching neighbors' directions
-
-        """
-        super().__init__(model)
-        self.speed = speed
-        self.direction = direction
-        self.vision = vision
-        self.separation = separation
-        self.cohere_factor = cohere
-        self.separate_factor = separate
-        self.match_factor = match
-
-    def step(self):
-        """Get the Boid's neighbors, compute the new vector, and move accordingly."""
-        neighbors = self.model.space.get_neighbors(self.pos, self.vision, False)
-        n = 0
-        match_vector, separation_vector, cohere = np.zeros((3, 2))
-        for neighbor in neighbors:
-            n += 1
-            heading = self.model.space.get_heading(self.pos, neighbor.pos)
-            cohere += heading
-            if self.model.space.get_distance(self.pos, neighbor.pos) < self.separation:
-                separation_vector -= heading
-            match_vector += neighbor.direction
-        n = max(n, 1)
-        cohere = cohere * self.cohere_factor
-        separation_vector = separation_vector * self.separate_factor
-        match_vector = match_vector * self.match_factor
-        self.direction += (cohere + separation_vector + match_vector) / n
-        self.direction /= np.linalg.norm(self.direction)
-        new_pos = self.pos + self.direction * self.speed
-        self.model.space.move_agent(self, new_pos)
-
-
-class BoidFlockers(mesa.Model):
-    """Flocker model class. Handles agent creation, placement and scheduling."""
-
-    def __init__(
-        self,
-        seed=None,
-        population=100,
-        width=100,
-        height=100,
-        vision=10,
-        speed=1,
-        separation=1,
-        cohere=0.03,
-        separate=0.015,
-        match=0.05,
-        simulator=None,
-    ):
-        """Create a new Flockers model.
-
-        Args:
-            seed: seed for random number generator
-            population: Number of Boids
-            width: the width of the space
-            height: the height of the space
-            speed: How fast should the Boids move.
-            vision: How far around should each Boid look for its neighbors
-            separation: What's the minimum distance each Boid will attempt to keep from any other
-            cohere: the relative importance of matching neighbors' positions'
-            separate: the relative importance of avoiding close neighbors
-            match: factors for the relative importance of
-                    the three drives.
-            simulator: a Simulator Instance
-        """
-        super().__init__(seed=seed)
-        self.population = population
-        self.width = width
-        self.height = height
-        self.simulator = simulator
-
-        self.space = mesa.space.ContinuousSpace(self.width, self.height, True)
-        self.factors = {
-            "cohere": cohere,
-            "separate": separate,
-            "match": match,
-        }
-
-        for _ in range(self.population):
-            x = self.random.random() * self.space.x_max
-            y = self.random.random() * self.space.y_max
-            pos = np.array((x, y))
-            direction = np.random.random(2) * 2 - 1
-            boid = Boid(
-                model=self,
-                speed=speed,
-                direction=direction,
-                vision=vision,
-                separation=separation,
-                **self.factors,
-            )
-            self.space.place_agent(boid, pos)
-
-    def step(self):
-        """Run the model for one step."""
-        self.agents.shuffle_do("step")
-
-
-if __name__ == "__main__":
-    import time
-
-    # model = BoidFlockers(seed=15, population=200, width=100, height=100, vision=5)
-    model = BoidFlockers(seed=15, population=400, width=100, height=100, vision=15)
-
-    start_time = time.perf_counter()
-    for _ in range(100):
-        model.step()
-
-    print(time.perf_counter() - start_time)
diff --git a/benchmarks/Schelling/__init__.py b/benchmarks/Schelling/__init__.py
deleted file mode 100644
index de8d0f1a..00000000
--- a/benchmarks/Schelling/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-"""Schelling separation for performance benchmarking."""
diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py
deleted file mode 100644
index f4543cb4..00000000
--- a/benchmarks/Schelling/schelling.py
+++ /dev/null
@@ -1,113 +0,0 @@
-"""Schelling separation for performance benchmarking."""
-
-from __future__ import annotations
-
-from mesa import Model
-from mesa.experimental.cell_space import Cell, CellAgent, OrthogonalMooreGrid
-
-
-class SchellingAgent(CellAgent):
-    """Schelling segregation agent."""
-
-    def __init__(
-        self,
-        model: Schelling,
-        agent_type: int,
-        radius: int,
-        homophily: float,
-        cell: Cell,
-    ):
-        """Create a new Schelling agent.
-
-        Args:
-            model: model instance
-            agent_type: type of agent (minority=1, majority=0)
-            radius: size of neighborhood of agent
-            homophily: fraction of neighbors of the same type that triggers movement
-            cell: the cell in which the agent is located
-        """
-        super().__init__(model)
-        self.type = agent_type
-        self.radius = radius
-        self.homophily = homophily
-        self.cell = cell
-
-    def step(self):
-        """Run one step of the agent."""
-        neighbors = self.cell.get_neighborhood(radius=self.radius).agents
-        similar = len(
-            [neighbor for neighbor in neighbors if neighbor.type == self.type]
-        )
-
-        # If unhappy, move:
-        if similar < self.homophily:
-            self.cell = self.model.grid.select_random_empty_cell()
-        else:
-            self.model.happy += 1
-
-
-class Schelling(Model):
-    """Model class for the Schelling segregation model."""
-
-    def __init__(
-        self,
-        simulator=None,
-        height=40,
-        width=40,
-        homophily=3,
-        radius=1,
-        density=0.8,
-        minority_pc=0.5,
-        seed=None,
-    ):
-        """Create a new Schelling model.
-
-        Args:
-            simulator: simulator instance
-            height: height of the grid
-            width: width of the grid
-            homophily: Minimum number of agents of same class needed to be happy
-            radius: Search radius for checking similarity
-            density: Initial Chance for a cell to populated
-            minority_pc: Chances for an agent to be in minority class
-            seed: the seed for the random number generator
-            simulator: a simulator instance
-        """
-        super().__init__(seed=seed)
-        self.simulator = simulator
-        self.happy = 0
-
-        self.grid = OrthogonalMooreGrid(
-            [height, width],
-            torus=True,
-            capacity=1,
-            random=self.random,
-        )
-
-        # Set up agents
-        # We use a grid iterator that returns
-        # the coordinates of a cell as well as
-        # its contents. (coord_iter)
-        for cell in self.grid:
-            if self.random.random() < density:
-                agent_type = 1 if self.random.random() < minority_pc else 0
-                SchellingAgent(self, agent_type, radius, homophily, cell)
-
-    def step(self):
-        """Run one step of the model."""
-        self.happy = 0  # Reset counter of happy agents
-        self.agents.shuffle_do("step")
-
-
-if __name__ == "__main__":
-    import time
-
-    # model = Schelling(seed=15, height=40, width=40, homophily=3, radius=1, density=0.625)
-    model = Schelling(
-        seed=15, height=100, width=100, homophily=8, radius=2, density=0.8
-    )
-
-    start_time = time.perf_counter()
-    for _ in range(100):
-        model.step()
-    print(time.perf_counter() - start_time)
diff --git a/benchmarks/WolfSheep/__init__.py b/benchmarks/WolfSheep/__init__.py
deleted file mode 100644
index 89c18853..00000000
--- a/benchmarks/WolfSheep/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-"""Wolf-Sheep Predation Model for performance benchmarking."""
diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py
deleted file mode 100644
index f085ce42..00000000
--- a/benchmarks/WolfSheep/wolf_sheep.py
+++ /dev/null
@@ -1,231 +0,0 @@
-"""Wolf-Sheep Predation Model for performance benchmarking.
-
-Replication of the model found in NetLogo:
-    Wilensky, U. (1997). NetLogo Wolf Sheep Predation model.
-    http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation.
-    Center for Connected Learning and Computer-Based Modeling,
-    Northwestern University, Evanston, IL.
-"""
-
-import math
-
-from mesa import Model
-from mesa.experimental.cell_space import CellAgent, FixedAgent, OrthogonalVonNeumannGrid
-from mesa.experimental.devs import ABMSimulator
-
-
-class Animal(CellAgent):
-    """The base animal class."""
-
-    def __init__(self, model, energy, p_reproduce, energy_from_food, cell):
-        """Initializes an animal.
-
-        Args:
-            model: a model instance
-            energy: starting amount of energy
-            p_reproduce: probability of sexless reproduction
-            energy_from_food: energy obtained from 1 unit of food
-            cell: the cell in which the animal starts
-        """
-        super().__init__(model)
-        self.energy = energy
-        self.p_reproduce = p_reproduce
-        self.energy_from_food = energy_from_food
-        self.cell = cell
-
-    def spawn_offspring(self):
-        """Create offspring."""
-        self.energy /= 2
-        self.__class__(
-            self.model,
-            self.energy,
-            self.p_reproduce,
-            self.energy_from_food,
-            self.cell,
-        )
-
-    def feed(self): ...  # noqa: D102
-
-    def step(self):
-        """One step of the agent."""
-        self.cell = self.cell.neighborhood.select_random_cell()
-        self.energy -= 1
-
-        self.feed()
-
-        if self.energy < 0:
-            self.remove()
-        elif self.random.random() < self.p_reproduce:
-            self.spawn_offspring()
-
-
-class Sheep(Animal):
-    """A sheep that walks around, reproduces (asexually) and gets eaten."""
-
-    def feed(self):
-        """If possible eat the food in the current location."""
-        # If there is grass available, eat it
-        grass_patch = next(
-            obj for obj in self.cell.agents if isinstance(obj, GrassPatch)
-        )
-        if grass_patch.fully_grown:
-            self.energy += self.energy_from_food
-            grass_patch.fully_grown = False
-
-
-class Wolf(Animal):
-    """A wolf that walks around, reproduces (asexually) and eats sheep."""
-
-    def feed(self):
-        """If possible eat the food in the current location."""
-        sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)]
-        if len(sheep) > 0:
-            sheep_to_eat = self.random.choice(sheep)
-            self.energy += self.energy_from_food
-
-            # Kill the sheep
-            sheep_to_eat.remove()
-
-
-class GrassPatch(FixedAgent):
-    """A patch of grass that grows at a fixed rate and it is eaten by sheep."""
-
-    @property
-    def fully_grown(self):  # noqa: D102
-        return self._fully_grown
-
-    @fully_grown.setter
-    def fully_grown(self, value: bool) -> None:
-        self._fully_grown = value
-
-        if not value:
-            self.model.simulator.schedule_event_relative(
-                setattr,
-                self.grass_regrowth_time,
-                function_args=[self, "fully_grown", True],
-            )
-
-    def __init__(self, model, countdown, grass_regrowth_time, cell):
-        """Creates a new patch of grass.
-
-        Args:
-            model: a model instance
-            countdown: Time for the patch of grass to be fully grown again
-            grass_regrowth_time : time to fully regrow grass
-            cell: the cell to which the patch of grass belongs
-        """
-        super().__init__(model)
-        self._fully_grown = True if countdown == 0 else False  # Noqa: SIM210
-        self.grass_regrowth_time = grass_regrowth_time
-        self.cell = cell
-
-        if not self.fully_grown:
-            self.model.simulator.schedule_event_relative(
-                setattr, countdown, function_args=[self, "fully_grown", True]
-            )
-
-
-class WolfSheep(Model):
-    """Wolf-Sheep Predation Model.
-
-    A model for simulating wolf and sheep (predator-prey) ecosystem modelling.
-    """
-
-    def __init__(
-        self,
-        simulator,
-        height,
-        width,
-        initial_sheep,
-        initial_wolves,
-        sheep_reproduce,
-        wolf_reproduce,
-        grass_regrowth_time,
-        wolf_gain_from_food=13,
-        sheep_gain_from_food=5,
-        seed=None,
-    ):
-        """Create a new Wolf-Sheep model with the given parameters.
-
-        Args:
-            simulator: ABMSimulator instance
-            width: width of the grid
-            height: height of the grid
-            initial_sheep: Number of sheep to start with
-            initial_wolves: Number of wolves to start with
-            sheep_reproduce: Probability of each sheep reproducing each step
-            wolf_reproduce: Probability of each wolf reproducing each step
-            grass_regrowth_time: How long it takes for a grass patch to regrow
-                                 once it is eaten
-            wolf_gain_from_food: Energy a wolf gains from eating a sheep
-            sheep_gain_from_food: Energy sheep gain from grass, if enabled.
-            seed : the random seed
-        """
-        super().__init__(seed=seed)
-        # Set parameters
-        self.height = height
-        self.width = width
-        self.simulator = simulator
-
-        self.initial_sheep = initial_sheep
-        self.initial_wolves = initial_wolves
-
-        self.grid = OrthogonalVonNeumannGrid(
-            [self.height, self.width],
-            torus=False,
-            capacity=math.inf,
-            random=self.random,
-        )
-
-        # Create sheep:
-        for _ in range(self.initial_sheep):
-            pos = (
-                self.random.randrange(self.width),
-                self.random.randrange(self.height),
-            )
-            energy = self.random.randrange(2 * sheep_gain_from_food)
-            Sheep(self, energy, sheep_reproduce, sheep_gain_from_food, self.grid[pos])
-
-        # Create wolves
-        for _ in range(self.initial_wolves):
-            pos = (
-                self.random.randrange(self.width),
-                self.random.randrange(self.height),
-            )
-            energy = self.random.randrange(2 * wolf_gain_from_food)
-            Wolf(self, energy, wolf_reproduce, wolf_gain_from_food, self.grid[pos])
-
-        # Create grass patches
-        possibly_fully_grown = [True, False]
-        for cell in self.grid:
-            fully_grown = self.random.choice(possibly_fully_grown)
-            countdown = 0 if fully_grown else self.random.randrange(grass_regrowth_time)
-            GrassPatch(self, countdown, grass_regrowth_time, cell)
-
-    def step(self):
-        """Run one step of the model."""
-        self.agents_by_type[Sheep].shuffle_do("step")
-        self.agents_by_type[Wolf].shuffle_do("step")
-
-
-if __name__ == "__main__":
-    import time
-
-    simulator = ABMSimulator()
-    model = WolfSheep(
-        simulator,
-        25,
-        25,
-        60,
-        40,
-        0.2,
-        0.1,
-        20,
-        seed=15,
-    )
-
-    simulator.setup(model)
-
-    start_time = time.perf_counter()
-    simulator.run(100)
-    print("Time:", time.perf_counter() - start_time)
diff --git a/benchmarks/configurations.py b/benchmarks/configurations.py
index 0f2be541..95bb41c8 100644
--- a/benchmarks/configurations.py
+++ b/benchmarks/configurations.py
@@ -1,9 +1,6 @@
 """configurations for benchmarks."""
 
-from BoltzmannWealth.boltzmann_wealth import BoltzmannWealth
-from Flocking.flocking import BoidFlockers
-from Schelling.schelling import Schelling
-from WolfSheep.wolf_sheep import WolfSheep
+from mesa.examples import BoidFlockers, BoltzmannWealth, Schelling, WolfSheep
 
 configurations = {
     # Schelling Model Configurations
diff --git a/benchmarks/global_benchmark.py b/benchmarks/global_benchmark.py
index 41c2643f..c3f0c071 100644
--- a/benchmarks/global_benchmark.py
+++ b/benchmarks/global_benchmark.py
@@ -28,21 +28,22 @@ def run_model(model_class, seed, parameters):
     Returns:
         startup time and run time
     """
-    no_simulator = ["BoltzmannWealth"]
+    uses_simulator = ["WolfSheep"]
     start_init = timeit.default_timer()
-    if model_class.__name__ in no_simulator:
-        model = model_class(seed=seed, **parameters)
-    else:
+    if model_class.__name__ in uses_simulator:
         simulator = ABMSimulator()
         model = model_class(simulator=simulator, seed=seed, **parameters)
         simulator.setup(model)
+    else:
+        model = model_class(seed=seed, **parameters)
 
     end_init_start_run = timeit.default_timer()
 
-    if model_class.__name__ in no_simulator:
-        model.run_model(config["steps"])
-    else:
+    if model_class.__name__ in uses_simulator:
         simulator.run_for(config["steps"])
+    else:
+        for _ in range(config["steps"]):
+            model.step()
 
     end_run = timeit.default_timer()
 
diff --git a/mesa/examples/__init__.py b/mesa/examples/__init__.py
index 048138b7..d0a736fa 100644
--- a/mesa/examples/__init__.py
+++ b/mesa/examples/__init__.py
@@ -3,14 +3,14 @@ from mesa.examples.advanced.pd_grid.model import PdGrid
 from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt
 from mesa.examples.advanced.wolf_sheep.model import WolfSheep
 from mesa.examples.basic.boid_flockers.model import BoidFlockers
-from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealthModel
+from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealth
 from mesa.examples.basic.conways_game_of_life.model import ConwaysGameOfLife
 from mesa.examples.basic.schelling.model import Schelling
 from mesa.examples.basic.virus_on_network.model import VirusOnNetwork
 
 __all__ = [
     "BoidFlockers",
-    "BoltzmannWealthModel",
+    "BoltzmannWealth",
     "ConwaysGameOfLife",
     "Schelling",
     "VirusOnNetwork",
diff --git a/mesa/examples/advanced/wolf_sheep/agents.py b/mesa/examples/advanced/wolf_sheep/agents.py
index 8e71988b..e3f308aa 100644
--- a/mesa/examples/advanced/wolf_sheep/agents.py
+++ b/mesa/examples/advanced/wolf_sheep/agents.py
@@ -5,14 +5,14 @@ class Animal(CellAgent):
     """The base animal class."""
 
     def __init__(self, model, energy, p_reproduce, energy_from_food, cell):
-        """Initializes an animal.
+        """Initialize an animal.
 
         Args:
-            model: a model instance
-            energy: starting amount of energy
-            p_reproduce: probability of sexless reproduction
-            energy_from_food: energy obtained from 1 unit of food
-            cell: the cell in which the animal starts
+            model: Model instance
+            energy: Starting amount of energy
+            p_reproduce: Probability of reproduction (asexual)
+            energy_from_food: Energy obtained from 1 unit of food
+            cell: Cell in which the animal starts
         """
         super().__init__(model)
         self.energy = energy
@@ -21,7 +21,7 @@ class Animal(CellAgent):
         self.cell = cell
 
     def spawn_offspring(self):
-        """Create offspring."""
+        """Create offspring by splitting energy and creating new instance."""
         self.energy /= 2
         self.__class__(
             self.model,
@@ -31,15 +31,19 @@ class Animal(CellAgent):
             self.cell,
         )
 
-    def feed(self): ...
+    def feed(self):
+        """Abstract method to be implemented by subclasses."""
 
     def step(self):
-        """One step of the agent."""
+        """Execute one step of the animal's behavior."""
+        # Move to random neighboring cell
         self.cell = self.cell.neighborhood.select_random_cell()
         self.energy -= 1
 
+        # Try to feed
         self.feed()
 
+        # Handle death and reproduction
         if self.energy < 0:
             self.remove()
         elif self.random.random() < self.p_reproduce:
@@ -50,53 +54,63 @@ class Sheep(Animal):
     """A sheep that walks around, reproduces (asexually) and gets eaten."""
 
     def feed(self):
-        """If possible eat the food in the current location."""
-        # If there is grass available, eat it
-        if self.model.grass:
-            grass_patch = next(
-                obj for obj in self.cell.agents if isinstance(obj, GrassPatch)
-            )
-            if grass_patch.fully_grown:
-                self.energy += self.energy_from_food
-                grass_patch.fully_grown = False
+        """If possible, eat grass at current location."""
+        grass_patch = next(
+            obj for obj in self.cell.agents if isinstance(obj, GrassPatch)
+        )
+        if grass_patch.fully_grown:
+            self.energy += self.energy_from_food
+            grass_patch.fully_grown = False
 
 
 class Wolf(Animal):
     """A wolf that walks around, reproduces (asexually) and eats sheep."""
 
     def feed(self):
-        """If possible eat the food in the current location."""
+        """If possible, eat a sheep at current location."""
         sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)]
-        if len(sheep) > 0:
+        if sheep:  # If there are any sheep present
             sheep_to_eat = self.random.choice(sheep)
             self.energy += self.energy_from_food
-
-            # Kill the sheep
             sheep_to_eat.remove()
 
 
 class GrassPatch(FixedAgent):
-    """
-    A patch of grass that grows at a fixed rate and it is eaten by sheep
-    """
+    """A patch of grass that grows at a fixed rate and can be eaten by sheep."""
+
+    @property
+    def fully_grown(self):
+        """Whether the grass patch is fully grown."""
+        return self._fully_grown
+
+    @fully_grown.setter
+    def fully_grown(self, value: bool) -> None:
+        """Set grass growth state and schedule regrowth if eaten."""
+        self._fully_grown = value
+
+        if not value:  # If grass was just eaten
+            self.model.simulator.schedule_event_relative(
+                setattr,
+                self.grass_regrowth_time,
+                function_args=[self, "fully_grown", True],
+            )
 
-    def __init__(self, model, fully_grown, countdown):
-        """
-        Creates a new patch of grass
+    def __init__(self, model, countdown, grass_regrowth_time, cell):
+        """Create a new patch of grass.
 
         Args:
-            grown: (boolean) Whether the patch of grass is fully grown or not
-            countdown: Time for the patch of grass to be fully grown again
+            model: Model instance
+            countdown: Time until grass is fully grown again
+            grass_regrowth_time: Time needed to regrow after being eaten
+            cell: Cell to which this grass patch belongs
         """
         super().__init__(model)
-        self.fully_grown = fully_grown
-        self.countdown = countdown
+        self._fully_grown = countdown == 0
+        self.grass_regrowth_time = grass_regrowth_time
+        self.cell = cell
 
-    def step(self):
+        # Schedule initial growth if not fully grown
         if not self.fully_grown:
-            if self.countdown <= 0:
-                # Set as fully grown
-                self.fully_grown = True
-                self.countdown = self.model.grass_regrowth_time
-            else:
-                self.countdown -= 1
+            self.model.simulator.schedule_event_relative(
+                setattr, countdown, function_args=[self, "fully_grown", True]
+            )
diff --git a/mesa/examples/advanced/wolf_sheep/model.py b/mesa/examples/advanced/wolf_sheep/model.py
index 2ee09d3f..982fcc28 100644
--- a/mesa/examples/advanced/wolf_sheep/model.py
+++ b/mesa/examples/advanced/wolf_sheep/model.py
@@ -9,30 +9,20 @@ Replication of the model found in NetLogo:
     Northwestern University, Evanston, IL.
 """
 
-import mesa
-from mesa.examples.advanced.wolf_sheep.agents import GrassPatch, Sheep, Wolf
-from mesa.experimental.cell_space import OrthogonalMooreGrid
-
+import math
 
-class WolfSheep(mesa.Model):
-    """
-    Wolf-Sheep Predation Model
-    """
-
-    height = 20
-    width = 20
-
-    initial_sheep = 100
-    initial_wolves = 50
+from mesa import Model
+from mesa.datacollection import DataCollector
+from mesa.examples.advanced.wolf_sheep.agents import GrassPatch, Sheep, Wolf
+from mesa.experimental.cell_space import OrthogonalVonNeumannGrid
+from mesa.experimental.devs import ABMSimulator
 
-    sheep_reproduce = 0.04
-    wolf_reproduce = 0.05
 
-    wolf_gain_from_food = 20
+class WolfSheep(Model):
+    """Wolf-Sheep Predation Model.
 
-    grass = False
-    grass_regrowth_time = 30
-    sheep_gain_from_food = 4
+    A model for simulating wolf and sheep (predator-prey) ecosystem modelling.
+    """
 
     description = (
         "A model for simulating wolf and sheep (predator-prey) ecosystem modelling."
@@ -40,22 +30,24 @@ class WolfSheep(mesa.Model):
 
     def __init__(
         self,
-        width=20,
         height=20,
+        width=20,
         initial_sheep=100,
         initial_wolves=50,
         sheep_reproduce=0.04,
         wolf_reproduce=0.05,
         wolf_gain_from_food=20,
-        grass=False,
+        grass=True,
         grass_regrowth_time=30,
         sheep_gain_from_food=4,
         seed=None,
+        simulator: ABMSimulator = None,
     ):
-        """
-        Create a new Wolf-Sheep model with the given parameters.
+        """Create a new Wolf-Sheep model with the given parameters.
 
         Args:
+            height: Height of the grid
+            width: Width of the grid
             initial_sheep: Number of sheep to start with
             initial_wolves: Number of wolves to start with
             sheep_reproduce: Probability of each sheep reproducing each step
@@ -63,75 +55,76 @@ class WolfSheep(mesa.Model):
             wolf_gain_from_food: Energy a wolf gains from eating a sheep
             grass: Whether to have the sheep eat grass for energy
             grass_regrowth_time: How long it takes for a grass patch to regrow
-                                 once it is eaten
-            sheep_gain_from_food: Energy sheep gain from grass, if enabled.
+                                once it is eaten
+            sheep_gain_from_food: Energy sheep gain from grass, if enabled
+            seed: Random seed
+            simulator: ABMSimulator instance for event scheduling
         """
         super().__init__(seed=seed)
-        # Set parameters
-        self.width = width
+
+        # Initialize model parameters
         self.height = height
-        self.initial_sheep = initial_sheep
-        self.initial_wolves = initial_wolves
+        self.width = width
         self.grass = grass
-        self.grass_regrowth_time = grass_regrowth_time
-
-        self.grid = OrthogonalMooreGrid((self.width, self.height), torus=True)
-
-        collectors = {
+        self.simulator = simulator
+
+        # Create grid using experimental cell space
+        self.grid = OrthogonalVonNeumannGrid(
+            [self.height, self.width],
+            torus=True,
+            capacity=math.inf,
+            random=self.random,
+        )
+
+        # Set up data collection
+        model_reporters = {
             "Wolves": lambda m: len(m.agents_by_type[Wolf]),
             "Sheep": lambda m: len(m.agents_by_type[Sheep]),
-            "Grass": lambda m: len(
+        }
+        if grass:
+            model_reporters["Grass"] = lambda m: len(
                 m.agents_by_type[GrassPatch].select(lambda a: a.fully_grown)
             )
-            if m.grass
-            else -1,
-        }
 
-        self.datacollector = mesa.DataCollector(collectors)
+        self.datacollector = DataCollector(model_reporters)
 
         # Create sheep:
-        for _ in range(self.initial_sheep):
-            x = self.random.randrange(self.width)
-            y = self.random.randrange(self.height)
-            energy = self.random.randrange(2 * self.sheep_gain_from_food)
-            Sheep(
-                self, energy, sheep_reproduce, sheep_gain_from_food, self.grid[(x, y)]
+        for _ in range(initial_sheep):
+            pos = (
+                self.random.randrange(width),
+                self.random.randrange(height),
             )
+            energy = self.random.randrange(2 * sheep_gain_from_food)
+            Sheep(self, energy, sheep_reproduce, sheep_gain_from_food, self.grid[pos])
 
         # Create wolves
-        for _ in range(self.initial_wolves):
-            x = self.random.randrange(self.width)
-            y = self.random.randrange(self.height)
-            energy = self.random.randrange(2 * self.wolf_gain_from_food)
-            Wolf(self, energy, wolf_reproduce, wolf_gain_from_food, self.grid[(x, y)])
-
-        # Create grass patches
-        if self.grass:
-            for cell in self.grid.all_cells:
-                fully_grown = self.random.choice([True, False])
-
-                if fully_grown:
-                    countdown = self.grass_regrowth_time
-                else:
-                    countdown = self.random.randrange(self.grass_regrowth_time)
-
-                patch = GrassPatch(self, fully_grown, countdown)
-                patch.cell = cell
-
+        for _ in range(initial_wolves):
+            pos = (
+                self.random.randrange(width),
+                self.random.randrange(height),
+            )
+            energy = self.random.randrange(2 * wolf_gain_from_food)
+            Wolf(self, energy, wolf_reproduce, wolf_gain_from_food, self.grid[pos])
+
+        # Create grass patches if enabled
+        if grass:
+            possibly_fully_grown = [True, False]
+            for cell in self.grid:
+                fully_grown = self.random.choice(possibly_fully_grown)
+                countdown = (
+                    0 if fully_grown else self.random.randrange(0, grass_regrowth_time)
+                )
+                GrassPatch(self, countdown, grass_regrowth_time, cell)
+
+        # Collect initial data
         self.running = True
         self.datacollector.collect(self)
 
     def step(self):
-        # This replicated the behavior of the old RandomActivationByType scheduler
-        # when using step(shuffle_types=True, shuffle_agents=True).
-        # Conceptually, it can be argued that this should be modelled differently.
-        self.random.shuffle(self.agent_types)
-        for agent_type in self.agent_types:
-            self.agents_by_type[agent_type].shuffle_do("step")
-
-        # collect data
-        self.datacollector.collect(self)
+        """Execute one step of the model."""
+        # First activate all sheep, then all wolves, both in random order
+        self.agents_by_type[Sheep].shuffle_do("step")
+        self.agents_by_type[Wolf].shuffle_do("step")
 
-    def run_model(self, step_count=200):
-        for _ in range(step_count):
-            self.step()
+        # Collect data
+        self.datacollector.collect(self)
diff --git a/mesa/examples/basic/boid_flockers/agents.py b/mesa/examples/basic/boid_flockers/agents.py
index 480d1b56..48ce2b5a 100644
--- a/mesa/examples/basic/boid_flockers/agents.py
+++ b/mesa/examples/basic/boid_flockers/agents.py
@@ -1,3 +1,9 @@
+"""A Boid (bird-oid) agent for implementing Craig Reynolds's Boids flocking model.
+
+This implementation uses numpy arrays to represent vectors for efficient computation
+of flocking behavior.
+"""
+
 import numpy as np
 
 from mesa import Agent
@@ -7,9 +13,9 @@ class Boid(Agent):
     """A Boid-style flocker agent.
 
     The agent follows three behaviors to flock:
-        - Cohesion: steering towards neighboring agents.
-        - Separation: avoiding getting too close to any other agent.
-        - Alignment: try to fly in the same direction as the neighbors.
+        - Cohesion: steering towards neighboring agents
+        - Separation: avoiding getting too close to any other agent
+        - Alignment: trying to fly in the same direction as neighbors
 
     Boids have a vision that defines the radius in which they look for their
     neighbors to flock with. Their speed (a scalar) and direction (a vector)
@@ -31,13 +37,14 @@ class Boid(Agent):
         """Create a new Boid flocker agent.
 
         Args:
-            speed: Distance to move per step.
-            direction: numpy vector for the Boid's direction of movement.
-            vision: Radius to look around for nearby Boids.
-            separation: Minimum distance to maintain from other Boids.
-            cohere: the relative importance of matching neighbors' positions
-            separate: the relative importance of avoiding close neighbors
-            match: the relative importance of matching neighbors' headings
+            model: Model instance the agent belongs to
+            speed: Distance to move per step
+            direction: numpy vector for the Boid's direction of movement
+            vision: Radius to look around for nearby Boids
+            separation: Minimum distance to maintain from other Boids
+            cohere: Relative importance of matching neighbors' positions (default: 0.03)
+            separate: Relative importance of avoiding close neighbors (default: 0.015)
+            match: Relative importance of matching neighbors' directions (default: 0.05)
         """
         super().__init__(model)
         self.speed = speed
@@ -47,25 +54,49 @@ class Boid(Agent):
         self.cohere_factor = cohere
         self.separate_factor = separate
         self.match_factor = match
-        self.neighbors = None
 
     def step(self):
         """Get the Boid's neighbors, compute the new vector, and move accordingly."""
-        self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False)
-        n = 0
-        match_vector, separation_vector, cohere = np.zeros((3, 2))
-        for neighbor in self.neighbors:
-            n += 1
+        neighbors = self.model.space.get_neighbors(self.pos, self.vision, False)
+
+        # If no neighbors, maintain current direction
+        if not neighbors:
+            new_pos = self.pos + self.direction * self.speed
+            self.model.space.move_agent(self, new_pos)
+            return
+
+        # Initialize vectors for the three flocking behaviors
+        cohere = np.zeros(2)  # Cohesion vector
+        match_vector = np.zeros(2)  # Alignment vector
+        separation_vector = np.zeros(2)  # Separation vector
+
+        # Calculate the contribution of each neighbor to the three behaviors
+        for neighbor in neighbors:
             heading = self.model.space.get_heading(self.pos, neighbor.pos)
+            distance = self.model.space.get_distance(self.pos, neighbor.pos)
+
+            # Cohesion - steer towards the average position of neighbors
             cohere += heading
-            if self.model.space.get_distance(self.pos, neighbor.pos) < self.separation:
+
+            # Separation - avoid getting too close
+            if distance < self.separation:
                 separation_vector -= heading
+
+            # Alignment - match neighbors' flying direction
             match_vector += neighbor.direction
-        n = max(n, 1)
+
+        # Weight each behavior by its factor and normalize by number of neighbors
+        n = len(neighbors)
         cohere = cohere * self.cohere_factor
         separation_vector = separation_vector * self.separate_factor
         match_vector = match_vector * self.match_factor
+
+        # Update direction based on the three behaviors
         self.direction += (cohere + separation_vector + match_vector) / n
+
+        # Normalize direction vector
         self.direction /= np.linalg.norm(self.direction)
+
+        # Move boid
         new_pos = self.pos + self.direction * self.speed
         self.model.space.move_agent(self, new_pos)
diff --git a/mesa/examples/basic/boid_flockers/model.py b/mesa/examples/basic/boid_flockers/model.py
index a05caab1..5b4974f3 100644
--- a/mesa/examples/basic/boid_flockers/model.py
+++ b/mesa/examples/basic/boid_flockers/model.py
@@ -1,60 +1,81 @@
-"""Flockers.
-=============================================================
+"""
+Boids Flocking Model
+===================
 A Mesa implementation of Craig Reynolds's Boids flocker model.
 Uses numpy arrays to represent vectors.
 """
 
 import numpy as np
 
-import mesa
+from mesa import Model
 from mesa.examples.basic.boid_flockers.agents import Boid
+from mesa.space import ContinuousSpace
 
 
-class BoidFlockers(mesa.Model):
+class BoidFlockers(Model):
     """Flocker model class. Handles agent creation, placement and scheduling."""
 
     def __init__(
         self,
-        seed=None,
         population=100,
         width=100,
         height=100,
-        vision=10,
         speed=1,
-        separation=1,
+        vision=10,
+        separation=2,
         cohere=0.03,
         separate=0.015,
         match=0.05,
+        seed=None,
     ):
-        """Create a new Flockers model.
+        """Create a new Boids Flocking model.
 
         Args:
-            population: Number of Boids
-            width, height: Size of the space.
-            speed: How fast should the Boids move.
-            vision: How far around should each Boid look for its neighbors
-            separation: What's the minimum distance each Boid will attempt to
-                    keep from any other
-            cohere, separate, match: factors for the relative importance of
-                    the three drives.
+            population: Number of Boids in the simulation (default: 100)
+            width: Width of the space (default: 100)
+            height: Height of the space (default: 100)
+            speed: How fast the Boids move (default: 1)
+            vision: How far each Boid can see (default: 10)
+            separation: Minimum distance between Boids (default: 2)
+            cohere: Weight of cohesion behavior (default: 0.03)
+            separate: Weight of separation behavior (default: 0.015)
+            match: Weight of alignment behavior (default: 0.05)
+            seed: Random seed for reproducibility (default: None)
         """
         super().__init__(seed=seed)
+
+        # Model Parameters
         self.population = population
         self.vision = vision
         self.speed = speed
         self.separation = separation
 
-        self.space = mesa.space.ContinuousSpace(width, height, True)
+        # Set up the space
+        self.space = ContinuousSpace(width, height, torus=True)
+
+        # Store flocking weights
         self.factors = {"cohere": cohere, "separate": separate, "match": match}
+
+        # Create and place the Boid agents
         self.make_agents()
 
+        # For tracking statistics
+        self.average_heading = None
+        self.update_average_heading()
+
     def make_agents(self):
-        """Create self.population agents, with random positions and starting headings."""
+        """Create and place all Boid agents randomly in the space."""
         for _ in range(self.population):
+            # Random position
             x = self.random.random() * self.space.x_max
             y = self.random.random() * self.space.y_max
             pos = np.array((x, y))
-            direction = np.random.random(2) * 2 - 1
+
+            # Random initial direction
+            direction = np.random.random(2) * 2 - 1  # Random vector between -1 and 1
+            direction /= np.linalg.norm(direction)  # Normalize
+
+            # Create and place the Boid
             boid = Boid(
                 model=self,
                 speed=self.speed,
@@ -65,5 +86,20 @@ class BoidFlockers(mesa.Model):
             )
             self.space.place_agent(boid, pos)
 
+    def update_average_heading(self):
+        """Calculate the average heading (direction) of all Boids."""
+        if not self.agents:
+            self.average_heading = 0
+            return
+
+        headings = np.array([agent.direction for agent in self.agents])
+        mean_heading = np.mean(headings, axis=0)
+        self.average_heading = np.arctan2(mean_heading[1], mean_heading[0])
+
     def step(self):
+        """Run one step of the model.
+
+        All agents are activated in random order using the AgentSet shuffle_do method.
+        """
         self.agents.shuffle_do("step")
+        self.update_average_heading()
diff --git a/mesa/examples/basic/boltzmann_wealth_model/agents.py b/mesa/examples/basic/boltzmann_wealth_model/agents.py
index 12abff18..35c8e6b1 100644
--- a/mesa/examples/basic/boltzmann_wealth_model/agents.py
+++ b/mesa/examples/basic/boltzmann_wealth_model/agents.py
@@ -2,13 +2,26 @@ from mesa import Agent
 
 
 class MoneyAgent(Agent):
-    """An agent with fixed initial wealth."""
+    """An agent with fixed initial wealth.
+
+    Each agent starts with 1 unit of wealth and can give 1 unit to other agents
+    if they occupy the same cell.
+
+    Attributes:
+        wealth (int): The agent's current wealth (starts at 1)
+    """
 
     def __init__(self, model):
+        """Create a new agent.
+
+        Args:
+            model (Model): The model instance that contains the agent
+        """
         super().__init__(model)
         self.wealth = 1
 
     def move(self):
+        """Move the agent to a random neighboring cell."""
         possible_steps = self.model.grid.get_neighborhood(
             self.pos, moore=True, include_center=False
         )
@@ -16,16 +29,21 @@ class MoneyAgent(Agent):
         self.model.grid.move_agent(self, new_position)
 
     def give_money(self):
+        """Give 1 unit of wealth to a random agent in the same cell."""
         cellmates = self.model.grid.get_cell_list_contents([self.pos])
-        cellmates.pop(
-            cellmates.index(self)
-        )  # Ensure agent is not giving money to itself
-        if len(cellmates) > 0:
+        # Remove self from potential recipients
+        cellmates.pop(cellmates.index(self))
+
+        if cellmates:  # Only give money if there are other agents present
             other = self.random.choice(cellmates)
             other.wealth += 1
             self.wealth -= 1
 
     def step(self):
+        """Execute one step for the agent:
+        1. Move to a neighboring cell
+        2. If wealth > 0, maybe give money to another agent in the same cell
+        """
         self.move()
         if self.wealth > 0:
             self.give_money()
diff --git a/mesa/examples/basic/boltzmann_wealth_model/app.py b/mesa/examples/basic/boltzmann_wealth_model/app.py
index 15663f69..c03d1763 100644
--- a/mesa/examples/basic/boltzmann_wealth_model/app.py
+++ b/mesa/examples/basic/boltzmann_wealth_model/app.py
@@ -1,4 +1,4 @@
-from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealthModel
+from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealth
 from mesa.visualization import (
     SolaraViz,
     make_plot_component,
@@ -35,7 +35,7 @@ def post_process(ax):
 
 
 # Create initial model instance
-model = BoltzmannWealthModel(50, 10, 10)
+model = BoltzmannWealth(50, 10, 10)
 
 # Create visualization elements. The visualization elements are solara components
 # that receive the model instance as a "prop" and display it in a certain way.
diff --git a/mesa/examples/basic/boltzmann_wealth_model/model.py b/mesa/examples/basic/boltzmann_wealth_model/model.py
index 03ef5a21..21dbaf63 100644
--- a/mesa/examples/basic/boltzmann_wealth_model/model.py
+++ b/mesa/examples/basic/boltzmann_wealth_model/model.py
@@ -1,43 +1,78 @@
-import mesa
+"""
+Boltzmann Wealth Model
+=====================
+
+A simple model of wealth distribution based on the Boltzmann-Gibbs distribution.
+Agents move randomly on a grid, giving one unit of wealth to a random neighbor
+when they occupy the same cell.
+"""
+
+from mesa import Model
+from mesa.datacollection import DataCollector
 from mesa.examples.basic.boltzmann_wealth_model.agents import MoneyAgent
+from mesa.space import MultiGrid
 
 
-class BoltzmannWealthModel(mesa.Model):
+class BoltzmannWealth(Model):
     """A simple model of an economy where agents exchange currency at random.
 
-    All the agents begin with one unit of currency, and each time step can give
-    a unit of currency to another agent. Note how, over time, this produces a
-    highly skewed distribution of wealth.
+    All agents begin with one unit of currency, and each time step agents can give
+    a unit of currency to another agent in the same cell. Over time, this produces
+    a highly skewed distribution of wealth.
+
+    Attributes:
+        num_agents (int): Number of agents in the model
+        grid (MultiGrid): The space in which agents move
+        running (bool): Whether the model should continue running
+        datacollector (DataCollector): Collects and stores model data
     """
 
     def __init__(self, n=100, width=10, height=10, seed=None):
+        """Initialize the model.
+
+        Args:
+            n (int, optional): Number of agents. Defaults to 100.
+            width (int, optional): Grid width. Defaults to 10.
+            height (int, optional): Grid height. Defaults to 10.
+            seed (int, optional): Random seed. Defaults to None.
+        """
         super().__init__(seed=seed)
+
         self.num_agents = n
-        self.grid = mesa.space.MultiGrid(width, height, True)
+        self.grid = MultiGrid(width, height, torus=True)
 
-        self.datacollector = mesa.DataCollector(
+        # Set up data collection
+        self.datacollector = DataCollector(
             model_reporters={"Gini": self.compute_gini},
             agent_reporters={"Wealth": "wealth"},
         )
-        # Create agents
+
+        # Create and place the agents
         for _ in range(self.num_agents):
-            a = MoneyAgent(self)
+            agent = MoneyAgent(self)
 
-            # Add the agent to a random grid cell
+            # Add agent to random grid cell
             x = self.random.randrange(self.grid.width)
             y = self.random.randrange(self.grid.height)
-            self.grid.place_agent(a, (x, y))
+            self.grid.place_agent(agent, (x, y))
 
         self.running = True
         self.datacollector.collect(self)
 
     def step(self):
-        self.agents.shuffle_do("step")
-        self.datacollector.collect(self)
+        self.agents.shuffle_do("step")  # Activate all agents in random order
+        self.datacollector.collect(self)  # Collect data
 
     def compute_gini(self):
+        """Calculate the Gini coefficient for the model's current wealth distribution.
+
+        The Gini coefficient is a measure of inequality in distributions.
+        - A Gini of 0 represents complete equality, where all agents have equal wealth.
+        - A Gini of 1 represents maximal inequality, where one agent has all wealth.
+        """
         agent_wealths = [agent.wealth for agent in self.agents]
         x = sorted(agent_wealths)
         n = self.num_agents
+        # Calculate using the standard formula for Gini coefficient
         b = sum(xi * (n - i) for i, xi in enumerate(x)) / (n * sum(x))
         return 1 + (1 / n) - 2 * b
diff --git a/mesa/examples/basic/boltzmann_wealth_model/st_app.py b/mesa/examples/basic/boltzmann_wealth_model/st_app.py
index 4e722935..7b925ca8 100644
--- a/mesa/examples/basic/boltzmann_wealth_model/st_app.py
+++ b/mesa/examples/basic/boltzmann_wealth_model/st_app.py
@@ -5,7 +5,7 @@ import time
 import altair as alt
 import pandas as pd
 import streamlit as st
-from model import BoltzmannWealthModel
+from model import BoltzmannWealth
 
 model = st.title("Boltzman Wealth Model")
 num_agents = st.slider(
@@ -19,7 +19,7 @@ num_ticks = st.slider(
 )
 height = st.slider("Select Grid Height", min_value=10, max_value=100, step=10, value=15)
 width = st.slider("Select Grid Width", min_value=10, max_value=100, step=10, value=20)
-model = BoltzmannWealthModel(num_agents, height, width)
+model = BoltzmannWealth(num_agents, height, width)
 
 
 status_text = st.empty()
diff --git a/mesa/examples/basic/schelling/agents.py b/mesa/examples/basic/schelling/agents.py
index dffb5679..67940b56 100644
--- a/mesa/examples/basic/schelling/agents.py
+++ b/mesa/examples/basic/schelling/agents.py
@@ -1,25 +1,29 @@
-from mesa import Agent, Model
+from mesa import Agent
 
 
 class SchellingAgent(Agent):
     """Schelling segregation agent."""
 
-    def __init__(self, model: Model, agent_type: int) -> None:
+    def __init__(self, model, agent_type: int) -> None:
         """Create a new Schelling agent.
 
         Args:
-           agent_type: Indicator for the agent's type (minority=1, majority=0)
+            model: The model instance the agent belongs to
+            agent_type: Indicator for the agent's type (minority=1, majority=0)
         """
         super().__init__(model)
         self.type = agent_type
 
     def step(self) -> None:
+        """Determine if agent is happy and move if necessary."""
         neighbors = self.model.grid.iter_neighbors(
             self.pos, moore=True, radius=self.model.radius
         )
-        similar = sum(1 for neighbor in neighbors if neighbor.type == self.type)
 
-        # If unhappy, move:
+        # Count similar neighbors
+        similar = sum(neighbor.type == self.type for neighbor in neighbors)
+
+        # If unhappy, move to a random empty cell:
         if similar < self.model.homophily:
             self.model.grid.move_to_empty(self)
         else:
diff --git a/mesa/examples/basic/schelling/model.py b/mesa/examples/basic/schelling/model.py
index aa390f14..3ee0746c 100644
--- a/mesa/examples/basic/schelling/model.py
+++ b/mesa/examples/basic/schelling/model.py
@@ -1,6 +1,7 @@
-import mesa
 from mesa import Model
+from mesa.datacollection import DataCollector
 from mesa.examples.basic.schelling.agents import SchellingAgent
+from mesa.space import SingleGrid
 
 
 class Schelling(Model):
@@ -8,52 +9,73 @@ class Schelling(Model):
 
     def __init__(
         self,
-        height=20,
-        width=20,
-        homophily=3,
-        radius=1,
-        density=0.8,
-        minority_pc=0.2,
+        height: int = 40,
+        width: int = 40,
+        density: float = 0.8,
+        minority_pc: float = 0.5,
+        homophily: int = 3,
+        radius: int = 1,
         seed=None,
     ):
         """Create a new Schelling model.
 
         Args:
-            width, height: Size of the space.
-            density: Initial Chance for a cell to populated
-            minority_pc: Chances for an agent to be in minority class
-            homophily: Minimum number of agents of same class needed to be happy
-            radius: Search radius for checking similarity
-            seed: Seed for Reproducibility
+            width: Width of the grid
+            height: Height of the grid
+            density: Initial chance for a cell to be populated (0-1)
+            minority_pc: Chance for an agent to be in minority class (0-1)
+            homophily: Minimum number of similar neighbors needed for happiness
+            radius: Search radius for checking neighbor similarity
+            seed: Seed for reproducibility
         """
         super().__init__(seed=seed)
+
+        # Model parameters
+        self.height = height
+        self.width = width
+        self.density = density
+        self.minority_pc = minority_pc
         self.homophily = homophily
         self.radius = radius
 
-        self.grid = mesa.space.SingleGrid(width, height, torus=True)
+        # Initialize grid
+        self.grid = SingleGrid(width, height, torus=True)
 
+        # Track happiness
         self.happy = 0
-        self.datacollector = mesa.DataCollector(
-            model_reporters={"happy": "happy"},  # Model-level count of happy agents
+
+        # Set up data collection
+        self.datacollector = DataCollector(
+            model_reporters={
+                "happy": "happy",
+                "pct_happy": lambda m: (m.happy / len(m.agents)) * 100
+                if len(m.agents) > 0
+                else 0,
+                "population": lambda m: len(m.agents),
+                "minority_pct": lambda m: (
+                    sum(1 for agent in m.agents if agent.type == 1)
+                    / len(m.agents)
+                    * 100
+                    if len(m.agents) > 0
+                    else 0
+                ),
+            },
+            agent_reporters={"agent_type": "type"},
         )
 
-        # Set up agents
-        # We use a grid iterator that returns
-        # the coordinates of a cell as well as
-        # its contents. (coord_iter)
+        # Create agents and place them on the grid
         for _, pos in self.grid.coord_iter():
-            if self.random.random() < density:
+            if self.random.random() < self.density:
                 agent_type = 1 if self.random.random() < minority_pc else 0
                 agent = SchellingAgent(self, agent_type)
                 self.grid.place_agent(agent, pos)
 
+        # Collect initial state
         self.datacollector.collect(self)
 
     def step(self):
         """Run one step of the model."""
         self.happy = 0  # Reset counter of happy agents
-        self.agents.shuffle_do("step")
-
-        self.datacollector.collect(self)
-
-        self.running = self.happy != len(self.agents)
+        self.agents.shuffle_do("step")  # Activate all agents in random order
+        self.datacollector.collect(self)  # Collect data
+        self.running = self.happy < len(self.agents)  # Continue until everyone is happy


Test patch:
diff --git a/tests/test_examples.py b/tests/test_examples.py
index ff5cd478..436bd805 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -1,7 +1,7 @@
 # noqa: D100
 from mesa.examples import (
     BoidFlockers,
-    BoltzmannWealthModel,
+    BoltzmannWealth,
     ConwaysGameOfLife,
     EpsteinCivilViolence,
     PdGrid,
@@ -13,7 +13,7 @@ from mesa.examples import (
 
 
 def test_boltzmann_model():  # noqa: D103
-    model = BoltzmannWealthModel(seed=42)
+    model = BoltzmannWealth(seed=42)
 
     for _i in range(10):
         model.step()
@@ -66,7 +66,9 @@ def test_sugarscape_g1mt():  # noqa: D103
 
 
 def test_wolf_sheep():  # noqa: D103
-    model = WolfSheep(seed=42)
+    from mesa.experimental.devs import ABMSimulator
 
-    for _i in range(10):
-        model.step()
+    simulator = ABMSimulator()
+    model = WolfSheep(seed=42, simulator=simulator)
+    simulator.setup(model)
+    simulator.run_for(10)