Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More pythonic implementation of wolf sheep #2011

Merged
merged 2 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions benchmarks/WolfSheep/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +0,0 @@
from .wolf_sheep import WolfSheep

if __name__ == "__main__":
# for profiling this benchmark model
import time

# model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20)
model = WolfSheep(15, 100, 100, 1000, 500, 0.4, 0.2, 20)

start_time = time.perf_counter()
for _ in range(100):
model.step()

print(time.perf_counter() - start_time)
111 changes: 0 additions & 111 deletions benchmarks/WolfSheep/agents.py

This file was deleted.

37 changes: 0 additions & 37 deletions benchmarks/WolfSheep/random_walk.py

This file was deleted.

150 changes: 135 additions & 15 deletions benchmarks/WolfSheep/wolf_sheep.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,117 @@
Northwestern University, Evanston, IL.
"""

import mesa
from mesa import Agent, Model
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 lines can be condensed to be just import mesa, then calling them would be mesa.Agent, mesa.Model, mesa.space.MultiGrid, in line with np.array or pd.DataFrame. See https://github.com/projectmesa/mesa-examples/blob/479eaf389e1a98bda0bdba5aa8f24ede0d84e764/examples/wolf_sheep/wolf_sheep/model.py#L18.

Copy link
Contributor Author

@quaquel quaquel Jan 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that is slower.

To be clear, I don't care much either way, so if there is some mesa convention for this, I am fine changing it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's about the same:

In [1]: import mesa

In [2]: m = mesa.Model()

In [3]: %timeit mesa.Agent(0, m)
627 ns ± 125 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

In [4]: from mesa import Agent, Model

In [5]: m2 = Model()

In [6]: %timeit Agent(0, m2)
627 ns ± 148 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for the import lines, this PR LGTM.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't have strong opinions on this so if others want to change this as well, fine.

Note, however, that the Schelling model already uses the same style of importing as I have used here. I also personally prefer e.g., SchellingAgent(Agent) over SchellingAgent(mesa.Agent). This is really just a matter of taste.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention in the examples repo all uses mesa.x. The ones in the Agents.jl benchmark code are from the outdated version of the examples.

This is really just a matter of taste.

But in the case of Agents.jl benchmark, they count and compare the LOC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention in the examples repo all uses mesa.x.

I ment our benchmarks

But in the case of Agents.jl benchmark, they count and compare the LOC.

And we agree that this is only a rough proxy we should not obsess about.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ment our benchmarks

The 3 versions of the code (mesa-examples, core Mesa benchmark, Agents.jl) ideally need to be kept in sync.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Once this is merged, I'll happily make PRs on the other libraries.

from mesa.space import MultiGrid
from mesa.time import RandomActivationByType

from .agents import GrassPatch, Sheep, Wolf

class Animal(Agent):
def __init__(self, unique_id, model, moore, energy, p_reproduce, energy_from_food):
super().__init__(unique_id, model)
self.energy = energy
self.p_reproduce = p_reproduce
self.energy_from_food = energy_from_food
self.moore = moore

class WolfSheep(mesa.Model):
def random_move(self):
next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True)
next_move = self.random.choice(next_moves)
# Now move:
self.model.grid.move_agent(self, next_move)

def spawn_offspring(self):
self.energy /= 2
offspring = self.__class__(
self.model.next_id(),
self.model,
self.moore,
self.energy,
self.p_reproduce,
self.energy_from_food,
)
self.model.grid.place_agent(offspring, self.pos)
self.model.schedule.add(offspring)

def feed(self):
...

def die(self):
self.model.grid.remove_agent(self)
self.remove()

def step(self):
self.random_move()
self.energy -= 1

self.feed()

if self.energy < 0:
self.die()
elif self.random.random() < self.p_reproduce:
self.spawn_offspring()


class Sheep(Animal):
"""
A sheep that walks around, reproduces (asexually) and gets eaten.

The init is the same as the RandomWalker.
"""

def feed(self):
# If there is grass available, eat it
agents = self.model.grid.get_cell_list_contents(self.pos)
grass_patch = next(obj for obj in 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):
agents = self.model.grid.get_cell_list_contents(self.pos)
sheep = [obj for obj in agents if isinstance(obj, Sheep)]
if len(sheep) > 0:
sheep_to_eat = self.random.choice(sheep)
self.energy += self.energy

# Kill the sheep
sheep_to_eat.die()


class GrassPatch(Agent):
"""
A patch of grass that grows at a fixed rate and it is eaten by sheep
"""

def __init__(self, unique_id, model, fully_grown, countdown):
"""
Creates 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
"""
super().__init__(unique_id, model)
self.fully_grown = fully_grown
self.countdown = countdown

def step(self):
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


class WolfSheep(Model):
"""
Wolf-Sheep Predation Model

Expand All @@ -35,6 +138,7 @@ def __init__(
grass_regrowth_time,
wolf_gain_from_food=13,
sheep_gain_from_food=5,
moore=False,
):
"""
Create a new Wolf-Sheep model with the given parameters.
Expand All @@ -49,41 +153,46 @@ def __init__(
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.
moore:
"""
super().__init__(seed=seed)
# Set parameters
self.height = height
self.width = width
self.initial_sheep = initial_sheep
self.initial_wolves = initial_wolves
self.sheep_reproduce = sheep_reproduce
self.wolf_reproduce = wolf_reproduce
self.wolf_gain_from_food = wolf_gain_from_food
self.grass_regrowth_time = grass_regrowth_time
self.sheep_gain_from_food = sheep_gain_from_food

self.schedule = RandomActivationByType(self)
self.grid = MultiGrid(self.height, self.width, torus=False)

# Create sheep:
for _i in range(self.initial_sheep):
for _ in range(self.initial_sheep):
pos = (
self.random.randrange(self.width),
self.random.randrange(self.height),
)
energy = self.random.randrange(2 * self.sheep_gain_from_food)
sheep = Sheep(self.next_id(), pos, self, True, energy)
energy = self.random.randrange(2 * sheep_gain_from_food)
sheep = Sheep(
self.next_id(),
self,
moore,
energy,
sheep_reproduce,
sheep_gain_from_food,
)
self.grid.place_agent(sheep, pos)
self.schedule.add(sheep)

# Create wolves
for _i in range(self.initial_wolves):
for _ in range(self.initial_wolves):
pos = (
self.random.randrange(self.width),
self.random.randrange(self.height),
)
energy = self.random.randrange(2 * self.wolf_gain_from_food)
wolf = Wolf(self.next_id(), pos, self, True, energy)
energy = self.random.randrange(2 * wolf_gain_from_food)
wolf = Wolf(
self.next_id(), self, moore, energy, wolf_reproduce, wolf_gain_from_food
)
self.grid.place_agent(wolf, pos)
self.schedule.add(wolf)

Expand All @@ -95,9 +204,20 @@ def __init__(
countdown = self.grass_regrowth_time
else:
countdown = self.random.randrange(self.grass_regrowth_time)
patch = GrassPatch(self.next_id(), pos, self, fully_grown, countdown)
patch = GrassPatch(self.next_id(), self, fully_grown, countdown)
self.grid.place_agent(patch, pos)
self.schedule.add(patch)

def step(self):
self.schedule.step()


if __name__ == "__main__":
import time

model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20)

start_time = time.perf_counter()
for _ in range(100):
model.step()
print("Time:", time.perf_counter() - start_time)
Loading