From 88a8d895b97af124eada131da5b52772623cad86 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Sat, 9 Nov 2024 20:30:52 +0100 Subject: [PATCH] Integrate benchmarks and example models (#2473) 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. * benchmarks: Use official example models Use the official example models for the benchmarks * wolf_sheep: Merge benchmark model into example model Key changes and improvements in this merged version: 1. Used the experimental cell space features: - Switched to the more efficient `OrthogonalVonNeumannGrid` - Leveraged `CellAgent` and `FixedAgent` base classes - Used the cell's neighborhood property for movement 2. Implemented discrete event scheduling for grass regrowth: - Used the experimental `ABMSimulator` for event scheduling - Replaced the countdown-based grass regrowth with scheduled events 3. Modern Mesa practices: - Used `agents_by_type` for agent management - Employed `shuffle_do()` for efficient random activation - Proper initialization of the Model class with `super().__init__()` - Clear data collection setup with dedicated model reporters 4. Code organization: - Maintained separation between agents and model - Added detailed docstrings - Used property decorators for grass state management - Consistent style and naming conventions 5. Additional improvements: - Made grass optional while keeping full functionality - Improved type hints and property usage - More efficient agent selection and movement - Better encapsulation of agent behaviors * schelling: Merge benchmark model into example model * flocking: Merge benchmark model into example model The merged implementation includes several improvements: 1. Code Organization: - Separated into agents.py and model.py following Mesa best practices - Clear docstrings and comments throughout - Consistent code style 2. Modern Mesa Features: - Uses AgentSet's shuffle_do() for random activation - Proper initialization using super().__init__() - Direct access to model.agents 3. Improvements to the Boid Implementation: - Better vector normalization handling - Added tracking of average heading for statistics - More robust neighbor handling - Cleaner separation of the three flocking behaviors - Added parameter validation and documentation 4. Key Changes: - Simplified the step() method using AgentSet - Improved documentation and type hints - Added model statistics tracking - Made parameter names more descriptive - Better default parameters for stable flocking * boltzmann: Merge benchmark model into example model Merged the two implementations with the following improvements and best practices: 1. Code Organization: - Separated model and agent code into distinct files - Added comprehensive docstrings following Google style - Improved code organization and readability 2. Model Implementation: - Used Mesa 3.0's automatic agent management via `model.agents` - Used `shuffle_do()` for random agent activation 3. Agent Implementation: - Simplified agent code while maintaining functionality - Improved method documentation - Added clear separation of responsibilities between methods 4. Latest Mesa Best Practices: - Proper model initialization using `super().__init__(seed=seed)` - Use of `model.agents` instead of a scheduler - Clear attribute definitions and typing - Consistent code style following Mesa conventions 5. Performance Considerations: - Efficient use of list operations - Minimal object creation during runtime - Direct access to model properties This implementation maintains all the core functionality while being more organized, better documented, and following current Mesa best practices. It uses only stable features and avoids experimental ones. The main changes from the original implementations: 1. Unified the different versions of the Gini coefficient calculation 2. Added proper docstrings throughout 3. Removed duplicate code 4. Added the optional run_model method from one version 5. Simplified some method implementations --- benchmarks/BoltzmannWealth/__init__.py | 1 - .../BoltzmannWealth/boltzmann_wealth.py | 112 --------- benchmarks/Flocking/__init__.py | 1 - benchmarks/Flocking/flocking.py | 156 ------------ benchmarks/Schelling/__init__.py | 1 - benchmarks/Schelling/schelling.py | 113 --------- benchmarks/WolfSheep/__init__.py | 1 - benchmarks/WolfSheep/wolf_sheep.py | 231 ------------------ benchmarks/configurations.py | 5 +- benchmarks/global_benchmark.py | 15 +- mesa/examples/__init__.py | 4 +- mesa/examples/advanced/wolf_sheep/agents.py | 92 ++++--- mesa/examples/advanced/wolf_sheep/model.py | 147 ++++++----- mesa/examples/basic/boid_flockers/agents.py | 67 +++-- mesa/examples/basic/boid_flockers/model.py | 74 ++++-- .../basic/boltzmann_wealth_model/agents.py | 28 ++- .../basic/boltzmann_wealth_model/app.py | 4 +- .../basic/boltzmann_wealth_model/model.py | 61 ++++- .../basic/boltzmann_wealth_model/st_app.py | 4 +- mesa/examples/basic/schelling/agents.py | 14 +- mesa/examples/basic/schelling/model.py | 74 ++++-- tests/test_examples.py | 12 +- 22 files changed, 377 insertions(+), 840 deletions(-) delete mode 100644 benchmarks/BoltzmannWealth/__init__.py delete mode 100644 benchmarks/BoltzmannWealth/boltzmann_wealth.py delete mode 100644 benchmarks/Flocking/__init__.py delete mode 100644 benchmarks/Flocking/flocking.py delete mode 100644 benchmarks/Schelling/__init__.py delete mode 100644 benchmarks/Schelling/schelling.py delete mode 100644 benchmarks/WolfSheep/__init__.py delete mode 100644 benchmarks/WolfSheep/wolf_sheep.py diff --git a/benchmarks/BoltzmannWealth/__init__.py b/benchmarks/BoltzmannWealth/__init__.py deleted file mode 100644 index b70e37fa100..00000000000 --- a/benchmarks/BoltzmannWealth/__init__.py +++ /dev/null @@ -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 e15aec20319..00000000000 --- 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 684c3743037..00000000000 --- a/benchmarks/Flocking/__init__.py +++ /dev/null @@ -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 d2d23b1ac79..00000000000 --- 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 de8d0f1a187..00000000000 --- a/benchmarks/Schelling/__init__.py +++ /dev/null @@ -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 f4543cb4312..00000000000 --- 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 89c18853af6..00000000000 --- a/benchmarks/WolfSheep/__init__.py +++ /dev/null @@ -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 f085ce429df..00000000000 --- 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 0f2be5410b2..95bb41c806a 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 41c2643f88c..c3f0c0716b2 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 048138b7b00..d0a736fa773 100644 --- a/mesa/examples/__init__.py +++ b/mesa/examples/__init__.py @@ -3,14 +3,14 @@ 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 8e71988bc9a..e3f308aa479 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 @@ def __init__(self, model, energy, p_reproduce, energy_from_food, cell): 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 @@ def spawn_offspring(self): 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 2ee09d3f732..982fcc2809f 100644 --- a/mesa/examples/advanced/wolf_sheep/model.py +++ b/mesa/examples/advanced/wolf_sheep/model.py @@ -9,30 +9,20 @@ 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 @@ def __init__( 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 480d1b56f52..48ce2b5a868 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 @@ def __init__( """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 @@ def __init__( 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 a05caab1880..5b4974f3a20 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 @@ def make_agents(self): ) 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 12abff186a9..35c8e6b1014 100644 --- a/mesa/examples/basic/boltzmann_wealth_model/agents.py +++ b/mesa/examples/basic/boltzmann_wealth_model/agents.py @@ -2,13 +2,26 @@ 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 @@ def move(self): 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 15663f69036..c03d1763619 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 03ef5a21634..21dbaf63e19 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 4e722935ede..7b925ca8494 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 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 @@ ) 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 dffb5679a50..67940b5654e 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 aa390f14e96..3ee0746c073 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 diff --git a/tests/test_examples.py b/tests/test_examples.py index ff5cd478e06..436bd805f70 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 @@ 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)