# Wolf-Sheep Predation Model ## Summary A simple ecological model, consisting of three agent types: wolves, sheep, and grass. The wolves and the sheep wander around the grid at random. Wolves and sheep both expend energy moving around, and replenish it by eating. Sheep eat grass, and wolves eat sheep if they end up on the same grid cell. If wolves and sheep have enough energy, they reproduce, creating a new wolf or sheep (in this simplified model, only one parent is needed for reproduction). The grass on each cell regrows at a constant rate. If any wolves and sheep run out of energy, they die. The model is tests and demonstrates several Mesa concepts and features: - MultiGrid - Multiple agent types (wolves, sheep, grass) - Overlay arbitrary text (wolf's energy) on agent's shapes while drawing on CanvasGrid - Agents inheriting a behavior (random movement) from an abstract parent - Writing a model composed of multiple files. - Dynamically adding and removing agents from the schedule ## How to Run To run the model interactively, in this directory, run the following command ``` $ solara run app.py ``` ## Files * ``wolf_sheep/random_walk.py``: This defines the ``RandomWalker`` agent, which implements the behavior of moving randomly across a grid, one cell at a time. Both the Wolf and Sheep agents will inherit from it. * ``wolf_sheep/test_random_walk.py``: Defines a simple model and a text-only visualization intended to make sure the RandomWalk class was working as expected. This doesn't actually model anything, but serves as an ad-hoc unit test. To run it, ``cd`` into the ``wolf_sheep`` directory and run ``python test_random_walk.py``. You'll see a series of ASCII grids, one per model step, with each cell showing a count of the number of agents in it. * ``wolf_sheep/agents.py``: Defines the Wolf, Sheep, and GrassPatch agent classes. * ``wolf_sheep/scheduler.py``: Defines a custom variant on the RandomActivationByType scheduler, where we can define filters for the `get_type_count` function. * ``wolf_sheep/model.py``: Defines the Wolf-Sheep Predation model itself * ``wolf_sheep/server.py``: Sets up the interactive visualization server * ``run.py``: Launches a model visualization server. ## Further Reading This model is closely based on the NetLogo Wolf-Sheep Predation Model: 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. See also the [Lotka–Volterra equations ](https://en.wikipedia.org/wiki/Lotka%E2%80%93Volterra_equations) for an example of a classic differential-equation model with similar dynamics. ## Agents ```python from mesa.discrete_space import CellAgent, FixedAgent class Animal(CellAgent): """The base animal class.""" def __init__( self, model, energy=8, p_reproduce=0.04, energy_from_food=4, cell=None ): """Initialize an animal. Args: 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 self.p_reproduce = p_reproduce self.energy_from_food = energy_from_food self.cell = cell def spawn_offspring(self): """Create offspring by splitting energy and creating new instance.""" self.energy /= 2 self.__class__( self.model, self.energy, self.p_reproduce, self.energy_from_food, self.cell, ) def feed(self): """Abstract method to be implemented by subclasses.""" def step(self): """Execute one step of the animal's behavior.""" # Move to random neighboring cell self.move() 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: self.spawn_offspring() class Sheep(Animal): """A sheep that walks around, reproduces (asexually) and gets eaten.""" def feed(self): """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 def move(self): """Move towards a cell where there isn't a wolf, and preferably with grown grass.""" cells_without_wolves = self.cell.neighborhood.select( lambda cell: not any(isinstance(obj, Wolf) for obj in cell.agents) ) # If all surrounding cells have wolves, stay put if len(cells_without_wolves) == 0: return # Among safe cells, prefer those with grown grass cells_with_grass = cells_without_wolves.select( lambda cell: any( isinstance(obj, GrassPatch) and obj.fully_grown for obj in cell.agents ) ) # Move to a cell with grass if available, otherwise move to any safe cell target_cells = ( cells_with_grass if len(cells_with_grass) > 0 else cells_without_wolves ) self.cell = target_cells.select_random_cell() class Wolf(Animal): """A wolf that walks around, reproduces (asexually) and eats sheep.""" def feed(self): """If possible, eat a sheep at current location.""" sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)] if sheep: # If there are any sheep present sheep_to_eat = self.random.choice(sheep) self.energy += self.energy_from_food sheep_to_eat.remove() def move(self): """Move to a neighboring cell, preferably one with sheep.""" cells_with_sheep = self.cell.neighborhood.select( lambda cell: any(isinstance(obj, Sheep) for obj in cell.agents) ) target_cells = ( cells_with_sheep if len(cells_with_sheep) > 0 else self.cell.neighborhood ) self.cell = target_cells.select_random_cell() class GrassPatch(FixedAgent): """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, countdown, grass_regrowth_time, cell): """Create a new patch of grass. Args: 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 = countdown == 0 self.grass_regrowth_time = grass_regrowth_time self.cell = cell # Schedule initial growth if not fully grown if not self.fully_grown: self.model.simulator.schedule_event_relative( setattr, countdown, function_args=[self, "fully_grown", True] ) ``` ## Model ```python """ Wolf-Sheep Predation Model ================================ 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.datacollection import DataCollector from mesa.discrete_space import OrthogonalVonNeumannGrid from mesa.examples.advanced.wolf_sheep.agents import GrassPatch, Sheep, Wolf from mesa.experimental.devs import ABMSimulator class WolfSheep(Model): """Wolf-Sheep Predation Model. A model for simulating wolf and sheep (predator-prey) ecosystem modelling. """ description = ( "A model for simulating wolf and sheep (predator-prey) ecosystem modelling." ) def __init__( self, width=20, height=20, initial_sheep=100, initial_wolves=50, sheep_reproduce=0.04, wolf_reproduce=0.05, wolf_gain_from_food=20, 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. 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 wolf_reproduce: Probability of each wolf reproducing each step 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 seed: Random seed simulator: ABMSimulator instance for event scheduling """ super().__init__(seed=seed) self.simulator = simulator self.simulator.setup(self) # Initialize model parameters self.height = height self.width = width self.grass = grass # 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]), } if grass: model_reporters["Grass"] = lambda m: len( m.agents_by_type[GrassPatch].select(lambda a: a.fully_grown) ) self.datacollector = DataCollector(model_reporters) # Create sheep: Sheep.create_agents( self, initial_sheep, energy=self.rng.random((initial_sheep,)) * 2 * sheep_gain_from_food, p_reproduce=sheep_reproduce, energy_from_food=sheep_gain_from_food, cell=self.random.choices(self.grid.all_cells.cells, k=initial_sheep), ) # Create Wolves: Wolf.create_agents( self, initial_wolves, energy=self.rng.random((initial_wolves,)) * 2 * wolf_gain_from_food, p_reproduce=wolf_reproduce, energy_from_food=wolf_gain_from_food, cell=self.random.choices(self.grid.all_cells.cells, k=initial_wolves), ) # 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): """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") # Collect data self.datacollector.collect(self) ``` ## App ```python from mesa.examples.advanced.wolf_sheep.agents import GrassPatch, Sheep, Wolf from mesa.examples.advanced.wolf_sheep.model import WolfSheep from mesa.experimental.devs import ABMSimulator from mesa.visualization import ( CommandConsole, Slider, SolaraViz, SpaceRenderer, make_plot_component, ) from mesa.visualization.components import AgentPortrayalStyle def wolf_sheep_portrayal(agent): if agent is None: return portrayal = AgentPortrayalStyle( size=50, marker="o", zorder=2, ) if isinstance(agent, Wolf): portrayal.update(("color", "red")) elif isinstance(agent, Sheep): portrayal.update(("color", "cyan")) elif isinstance(agent, GrassPatch): if agent.fully_grown: portrayal.update(("color", "tab:green")) else: portrayal.update(("color", "tab:brown")) portrayal.update(("marker", "s"), ("size", 125), ("zorder", 1)) return portrayal model_params = { "seed": { "type": "InputText", "value": 42, "label": "Random Seed", }, "grass": { "type": "Select", "value": True, "values": [True, False], "label": "grass regrowth enabled?", }, "grass_regrowth_time": Slider("Grass Regrowth Time", 20, 1, 50), "initial_sheep": Slider("Initial Sheep Population", 100, 10, 300), "sheep_reproduce": Slider("Sheep Reproduction Rate", 0.04, 0.01, 1.0, 0.01), "initial_wolves": Slider("Initial Wolf Population", 10, 5, 100), "wolf_reproduce": Slider( "Wolf Reproduction Rate", 0.05, 0.01, 1.0, 0.01, ), "wolf_gain_from_food": Slider("Wolf Gain From Food Rate", 20, 1, 50), "sheep_gain_from_food": Slider("Sheep Gain From Food", 4, 1, 10), } def post_process_space(ax): ax.set_aspect("equal") ax.set_xticks([]) ax.set_yticks([]) def post_process_lines(ax): ax.legend(loc="center left", bbox_to_anchor=(1, 0.9)) lineplot_component = make_plot_component( {"Wolves": "tab:orange", "Sheep": "tab:cyan", "Grass": "tab:green"}, post_process=post_process_lines, ) simulator = ABMSimulator() model = WolfSheep(simulator=simulator, grass=True) renderer = SpaceRenderer( model, backend="matplotlib", ) renderer.draw_agents(wolf_sheep_portrayal) renderer.post_process = post_process_space page = SolaraViz( model, renderer, components=[lineplot_component, CommandConsole], model_params=model_params, name="Wolf Sheep", simulator=simulator, ) page # noqa ```