Skip to content
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
Binary file modified episodes/fig/predprey_out.png
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah this looks so much better.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 49 additions & 52 deletions episodes/files/pred-prey/predprey.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,34 @@
import numpy as np

# Reproduction
REPRODUCE_PREY_PROB = 0.05
REPRODUCE_PRED_PROB = 0.03
REPRODUCE_PREY_PROB = 0.012
REPRODUCE_PRED_PROB = 0.015

# Cohesion/Avoidance
SAME_SPECIES_AVOIDANCE_RADIUS = 0.035
PREY_GROUP_COHESION_RADIUS = 0.2
PREY_GROUP_COHESION_RADIUS = 0.25

# Predator/Prey/Grass interaction
PRED_PREY_INTERACTION_RADIUS = 0.3
PRED_SPEED_ADVANTAGE = 3.0
PRED_KILL_DISTANCE = 0.03
PRED_PREY_INTERACTION_RADIUS = 0.10
PRED_SPEED_ADVANTAGE = 1.5
PRED_KILL_DISTANCE = 0.06
GRASS_EAT_DISTANCE = 0.05
GAIN_FROM_FOOD_PREY = 80
GAIN_FROM_FOOD_PREDATOR = 100
GRASS_REGROW_CYCLES = 20
PRED_HUNGER_THRESH = 100
PREY_HUNGER_THRESH = 100
GAIN_FROM_FOOD_PREY = 18
GAIN_FROM_FOOD_PREDATOR = 70
GRASS_REGROW_CYCLES = 24
PRED_HUNGER_THRESH = 115
PREY_HUNGER_THRESH = 140

# Simulation properties
DELTA_TIME = 0.001
BOUNDS_WIDTH = 2.0
MIN_POSITION = -1.0
MAX_POSITION = 1.0
MAX_POSITION = 1.0

NEXT_PRED_ID = 1
NEXT_PREY_ID = 1


class Prey:
def __init__(self):
global NEXT_PREY_ID
Expand All @@ -53,11 +54,7 @@ def avoid_predators(self, predator_list):

# Add a steering factor away from each predator. Strength increases with closeness.
for predator in predator_list:
# Fetch location of predator
predator_x = self.x;
predator_y = self.y;

# Check if the two predators are within interaction radius
# Check if predator and prey are within interaction radius
dx = self.x - predator.x
dy = self.y - predator.y
distance = math.sqrt(dx * dx + dy * dy)
Expand Down Expand Up @@ -145,24 +142,25 @@ def eaten_or_starve(self, predator_list):
if predator_index >= 0:
predator_list[predator_index].life += GAIN_FROM_FOOD_PREDATOR
return True
# If the life has reduced to 0 then the prey should die or starvation

# If the life has reduced to 0 then the prey should die of starvation
if self.life < 1:
return True
return False

def reproduce(self):
if np.random.uniform() < REPRODUCE_PREY_PROB:
self.life /= 2

child = Prey()
child.x = np.random.uniform() * BOUNDS_WIDTH - BOUNDS_WIDTH / 2.0
child.y = np.random.uniform() * BOUNDS_WIDTH - BOUNDS_WIDTH / 2.0
child.vx = np.random.uniform() * 2 - 1
child.vy = np.random.uniform() * 2 - 1
child.life = self.life


return child


class Predator:
def __init__(self):
global NEXT_PRED_ID
Expand All @@ -175,7 +173,7 @@ def __init__(self):
self.steer_x = 0.0
self.steer_y = 0.0
self.life = 0

def follow_prey(self, prey_list):
# Find the closest prey by iterating the prey_location messages
closest_prey_x = 0.0
Expand All @@ -200,7 +198,6 @@ def follow_prey(self, prey_list):
self.steer_x = closest_prey_x - self.x
self.steer_y = closest_prey_y - self.y


def avoid_predators(self, predator_list):
# Fetch this predator's position
avoid_velocity_x = 0.0
Expand All @@ -219,7 +216,7 @@ def avoid_predators(self, predator_list):

self.steer_x += avoid_velocity_x
self.steer_y += avoid_velocity_y

def move(self):
# Integrate steering forces and cap velocity
self.vx += self.steer_x
Expand All @@ -234,21 +231,21 @@ def move(self):
self.x += self.vx * DELTA_TIME * PRED_SPEED_ADVANTAGE
self.y += self.vy * DELTA_TIME * PRED_SPEED_ADVANTAGE

# Bound the position within the environment
# Bound the position within the environment
self.x = max(self.x, MIN_POSITION)
self.x = min(self.x, MAX_POSITION)
self.y = max(self.y, MIN_POSITION)
self.y = min(self.y, MAX_POSITION)

# Reduce life by one unit of energy
self.life -= 1

def starve(self):
# Did the predator starve?
if self.life < 1:
return True
return False

def reproduce(self):
if np.random.uniform() < REPRODUCE_PRED_PROB:
self.life /= 2
Expand All @@ -260,14 +257,14 @@ def reproduce(self):
child.vy = np.random.uniform() * 2 - 1
child.life = self.life
return child

class Grass:
def __init__(self):
self.x = 0.0
self.y = 0.0
self.dead_cycles = 0
self.available = 1

def grow(self):
new_dead_cycles = self.dead_cycles + 1
if self.dead_cycles == GRASS_REGROW_CYCLES:
Expand All @@ -277,7 +274,7 @@ def grow(self):
if self.available == 0:
self.dead_cycles = new_dead_cycles


def eaten(self, prey_list):
if self.available:
prey_index = -1
Expand All @@ -299,14 +296,14 @@ def eaten(self, prey_list):
if prey_index >= 0:
# Add grass eaten message
prey_list[prey_index].life += GAIN_FROM_FOOD_PREY

# Update grass agent variables
self.dead_cycles = 0
self.available = 0

class Model:

def __init__(self, steps = 250):
def __init__(self, steps = 50):
self.steps = steps
self.num_prey = 200
self.num_predators = 50
Expand All @@ -323,7 +320,7 @@ def _init_population(self):
p.vy = np.random.uniform(-1.0, 1.0)
p.life = np.random.randint(10, 50)
self.prey.append(p)

# Initialise predator agents
self.predators = []
for i in range(self.num_predators):
Expand All @@ -334,48 +331,48 @@ def _init_population(self):
p.vy = np.random.uniform(-1.0, 1.0)
p.life = np.random.randint(10, 15)
self.predators.append(p)

# Initialise grass agents
self.grass = []
for i in range(self.num_grass):
g = Grass()
g.x = np.random.uniform(-1.0, 1.0)
g.y = np.random.uniform(-1.0, 1.0)
self.grass.append(g)

def _step(self):
## Shuffle agent list order to avoid bias
np.random.shuffle(self.predators) # todo, this probably doesn't like Python lists
np.random.shuffle(self.prey)

for p in self.predators:
p.follow_prey(self.prey)
for p in self.prey:
p.avoid_predators(self.predators)

for p in self.prey:
p.flock(self.prey)
for p in self.predators:
p.avoid_predators(self.predators)

for p in self.prey:
p.move()
for p in self.predators:
p.move()


for g in self.grass:
g.eaten(self.prey)

self.prey = [p for p in self.prey if not p.eaten_or_starve(self.predators)]
self.predators = [p for p in self.predators if not p.starve()]

children = []
for p in self.prey:
c = p.reproduce()
if c:
children.append(c)
self.predators.extend(children)
self.prey.extend(children)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Still such a dumb mistake.

children = []
for p in self.predators:
c = p.reproduce()
Expand All @@ -384,17 +381,17 @@ def _step(self):
self.predators.extend(children)
for g in self.grass:
g.grow()

def _init_log(self):
self.prey_log = [len(self.prey)]
self.predator_log = [len(self.predators)]
self.grass_log = [sum(g.available for g in self.grass)/20]

def _log(self):
self.prey_log.append(len(self.prey))
self.predator_log.append(len(self.predators))
self.grass_log.append(sum(g.available for g in self.grass)/20)

def _plot(self):
plt.figure(figsize=(16,10))
plt.rcParams.update({'font.size': 18})
Expand All @@ -405,7 +402,7 @@ def _plot(self):
plt.plot(range(0, len(self.grass_log)), self.grass_log, 'g', label="Grass/20")
plt.legend()
plt.savefig('predprey_out.png')

def run(self, random_seed=12):
np.random.seed(random_seed)
# init
Expand All @@ -417,7 +414,7 @@ def run(self, random_seed=12):
self._log()
# plot graph of results
self._plot()

# Argument parsing
if len(sys.argv) != 2:
print("Script expects 1 positive integer argument (number of steps), %u found."%(len(sys.argv) - 1))
Expand All @@ -431,4 +428,4 @@ def run(self, random_seed=12):
model = Model(steps=steps)
model.run()
end_time = time.monotonic()
print(f"Execution time: {end_time - start_time:.3f} s")
print(f"Execution time: {end_time - start_time:.3f} s")
Binary file modified episodes/files/pred-prey/predprey.py.lprof
Binary file not shown.
8 changes: 4 additions & 4 deletions episodes/profiling-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ python -m snakeviz predprey_out.prof

## Exercise 2: Predator Prey

Download and profile <a href="files/pred-prey/predprey.py" download>the Python predator prey model</a>, try to locate the function call(s) where the majority of execution time is being spent
Download and profile <a href="files/pred-prey/predprey.py" download>the Python predator prey model</a>, try to locate the function call(s) where the majority of execution time is being spent.

*This exercise uses the packages `numpy` and `matplotlib`, they can be installed via `pip install numpy matplotlib`.*

Expand All @@ -438,7 +438,7 @@ Download and profile <a href="files/pred-prey/predprey.py" download>the Python p
> The three agents; predators, prey and grass exist in a two dimensional grid. Predators eat prey, prey eat grass. The size of each population changes over time. Depending on the parameters of the model, the populations may oscillate, grow or collapse due to the availability of their food source.

The program can be executed via `python predprey.py <steps>`.
The value of `steps` for a full run is 250, however a full run may not be necessary to find the bottlenecks.
The value of `steps` for a full run is 400, which may take a few minutes. However, using 100–200 steps should be sufficient to find the bottlenecks.

When the model finishes it outputs a graph of the three populations `predprey_out.png`.

Expand All @@ -454,13 +454,13 @@ If the table is ordered by `ncalls`, it can be identified as the joint 4th most

If you checked `predprey_out.png` (shown below), you should notice that there are significantly more `Grass` agents than `Predators` or `Prey`.

![`predprey_out.png` as produced by the default configuration of `predprey.py`.](episodes/fig/predprey_out.png){alt="A line graph plotting population over time through 250 steps of the pred prey model. Grass/20, shown in green, has a brief dip in the first 30 steps, but recovers holding steady at approximately 240 (4800 agents). Prey, shown in blue, starts at 200, quickly drops to around 185, before levelling off for steps and then slowly declining to a final value of 50. The data for predators, shown in red, has significantly more noise. There are 50 predators to begin, this rises briefly before falling to around 10, from here it noisily grows to around 70 by step 250 with several larger declines during the growth."}
![`predprey_out.png` as produced by the default configuration of `predprey.py`.](episodes/fig/predprey_out.png){alt="A line graph plotting population over time through 400 time steps of the pred prey model. The amount of grass, shown in green, is scaled down by a factor of 20 to fit onto the graph. It has a brief dip in the first 25 steps, then slowly declines from approximately 220 to 150 over the next 200 steps, before steadily returning to 250. The number of prey, shown in blue, starts at 200, then grows to around 600 after 200 steps, before declining quickly and reaching zero at 350 to 400 steps. The number of predators, shown in red, falls from 50 to around 30 after 15 time steps, then grows to almost 700 by step 330 before declining quickly."}

Similarly, the `Grass::eaten()` has a `percall` time is inline with other agent functions such as `Prey::flock()` (from `predprey.py:67`).

Maybe we could investigate this further with line profiling!

*You may have noticed many iciles on the right hand of the diagram, these primarily correspond to the `import` of `matplotlib` which is relatively expensive!*
*You may have noticed many icicles on the right hand of the diagram, these primarily correspond to the `import` of `matplotlib` which is relatively expensive!*

:::::::::::::::::::::::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::
Expand Down
Loading