Why the populations cycle
The cycle is a feedback loop with a delay. When sheep are plentiful, a wandering wolf often lands on a cell with a sheep, gains energy and breeds. The growing pack eats sheep faster than they are born, so the sheep crash. Now most wolves find nothing, burn through their energy and starve — and with few wolves left, the sheep and the grass recover. The delay comes from energy: a well-fed wolf can survive many lean steps, so the wolf peak arrives well after the sheep peak.
- The time series shows the lag directly: each wolf peak follows a sheep peak. The lab estimates the cycle period from the autocorrelation of the sheep count and the lag from the cross-covariance of the two series.
- The phase plot turns the lag into a loop. Sheep grow while wolves are scarce (the path moves right), wolves grow while sheep are plentiful (up), sheep collapse (left), wolves starve (down): a counterclockwise circuit that never closes exactly on itself.
- The grid shows that the model is spatial: grazed patches, sheep clusters where grass has regrown, and wolves thinning out a crowd.
Change the seed. Most runs cycle, but in some the wolves hit zero in the first deep trough and never return. Nothing in the rules forbids that; with finite numbers of animals, a bad run of luck at a low point is permanent.
The rules
The world is a 51 × 51 grid that wraps around at the edges (a torus). Each cell has grass or is grazed bare. Every step runs in this order:
- Sheep, in list order: move to a random one of the 8 neighboring cells, lose 1 energy, eat the grass on the new cell if there is any (+4 energy, the cell becomes bare), die if energy is negative, otherwise give birth with the sheep birth probability. A newborn takes half its parent's energy.
- Wolves, in list order: move the same way, lose 1 energy, eat one sheep on the new cell if there is one (+ the energy-per-sheep parameter), die if energy is negative, otherwise give birth with the wolf birth probability, splitting energy the same way.
- Grass: a grazed cell turns green again after the regrowth time.
Initial animals are placed at random with random energy; half of the cells start with grass. Births stop while the total number of animals is at the cap of 2,500, so extreme settings cannot grind the browser or the Python port to a halt; the readout says when the cap was hit.
Two back-of-the-envelope balances explain much of what you see. A sheep gains 4 energy per green cell it steps on and spends 1 per step, so it only gains energy while more than a quarter of the cells are green. A wolf spends 1 per step and gains 20 (default) per catch, so it breaks even when roughly one step in 20 lands on a sheep — with 2,601 cells that needs on the order of 130 randomly scattered sheep. Below that the wolves shrink, above it they grow. These are mean-field estimates that ignore clustering, but they mark where the curves turn.
Agents versus the Lotka–Volterra equations
The classic mathematical predator–prey model, the Lotka–Volterra equations, treats populations as smooth numbers that change deterministically. Its orbits are closed curves: start anywhere and the system retraces the same loop forever, with an amplitude fixed by the starting point.
The agent-based version differs in three ways that matter:
- Demographic noise. Births, deaths and encounters are chance events, so each cycle has a different height and length. The loop in the phase plot wanders instead of repeating.
- Extinction is possible and final. In the equations a population can come arbitrarily close to zero and recover. Here, a population of zero stays zero.
- Prey are limited by food. Without wolves the sheep do not grow without bound: grass caps them. Set initial wolves to 0 to see the sheep settle at the level the grass supports.
Things to try: raise the energy per sheep to 40, and the wolves become so efficient that they overshoot, strip the field of sheep and usually starve out. Lower it to 10 and the wolf swings get smaller and the cycles faster. Raise the grass regrowth time to 30 and the sheep become food-limited at a low level where wolves barely persist, and the cycles fade into noise.
Example result
With the default settings and seed 42 the run ends after 1,000 steps with 364 sheep and 73 wolves, and 56% of the cells green. Sheep peaked at 748 and wolves at 327. Extinction: none. The populations cycled with a period of about 241 steps, with wolf peaks 67 steps behind sheep peaks — roughly a quarter of a cycle.
| Sheep at the end | 364 |
|---|---|
| Wolves at the end | 73 |
| Grass cover at the end | 56% |
| Peak sheep | 748 |
| Peak wolves | 327 |
| Extinction | none |
| Cycle period (steps) | 241 |
| Wolf peaks lag sheep by (steps) | 67 |
| Agent cap (2,500) reached | no |
Implementation
The page runs simulation.js from the package below; the Python file is a line-by-line port with the same random-number generator, so a given seed produces the same result in both languages. The excerpts show the core of each.
// One step: every sheep moves, pays 1 energy, grazes and may give birth; then every
// wolf moves, pays 1 energy, eats one sheep on its cell and may give birth; then
// grazed cells count down toward regrowth. Agents with negative energy die.
export function stepWorld(w, rng) {
const { p, grass, timer, head, next } = w;
let room = AGENT_CAP - w.sheep.length - w.wolves.length;
const sheep = [], lambs = [];
for (const s of w.sheep) {
move(s, rng);
s.e -= 1;
const c = s.y * SIZE + s.x;
if (grass[c] === 1) { grass[c] = 0; w.green--; timer[c] = p.grass_regrowth; s.e += SHEEP_GAIN; }
if (s.e < 0) continue; // starved
sheep.push(s);
if (rng.bernoulli(p.sheep_reproduce)) {
if (room > 0) { const e = Math.floor(s.e / 2); s.e -= e; lambs.push({ x: s.x, y: s.y, e }); room--; } else w.capHit = true;
}
}
w.sheep = sheep.concat(lambs);
// Index sheep by cell (linked lists) so a wolf finds prey on its cell in O(1).
head.fill(-1);
for (let i = 0; i < w.sheep.length; i++) { const s = w.sheep[i]; const c = s.y * SIZE + s.x; next[i] = head[c]; head[c] = i; s.eaten = false; }
const wolves = [], pups = [];
for (const f of w.wolves) {
move(f, rng);
f.e -= 1;
const c = f.y * SIZE + f.x;
if (head[c] >= 0) { w.sheep[head[c]].eaten = true; head[c] = next[head[c]]; f.e += p.wolf_gain; }
if (f.e < 0) continue;
wolves.push(f);
if (rng.bernoulli(p.wolf_reproduce)) {
if (room > 0) { const e = Math.floor(f.e / 2); f.e -= e; pups.push({ x: f.x, y: f.y, e }); room--; } else w.capHit = true;
}
}
w.wolves = wolves.concat(pups);
w.sheep = w.sheep.filter((s) => !s.eaten);
for (let c = 0; c < grass.length; c++) if (grass[c] === 0 && --timer[c] <= 0) { grass[c] = 1; w.green++; }
w.t++;
}def step_world(w, rng):
"""Sheep move, pay 1 energy, graze, maybe give birth; then wolves move, pay 1,
eat one sheep on their cell, maybe give birth; then grazed cells regrow."""
p, grass, timer = w.p, w.grass, w.timer
room = AGENT_CAP - len(w.sheep) - len(w.wolves)
sheep, lambs = [], []
for s in w.sheep:
move(s, rng)
s[2] -= 1
c = s[1] * SIZE + s[0]
if grass[c] == 1:
grass[c] = 0
w.green -= 1
timer[c] = p["grass_regrowth"]
s[2] += SHEEP_GAIN
if s[2] < 0:
continue # starved
sheep.append(s)
if rng.bernoulli(p["sheep_reproduce"]):
if room > 0:
e = s[2] // 2
s[2] -= e
lambs.append([s[0], s[1], e])
room -= 1
else:
w.cap_hit = True
w.sheep = sheep + lambs
# Index sheep by cell: the last-listed sheep on a cell is eaten first.
on_cell = {}
for i, s in enumerate(w.sheep):
on_cell.setdefault(s[1] * SIZE + s[0], []).append(i)
eaten = [False] * len(w.sheep)
wolves, pups = [], []
for f in w.wolves:
move(f, rng)
f[2] -= 1
prey = on_cell.get(f[1] * SIZE + f[0])
if prey:
eaten[prey.pop()] = True
f[2] += p["wolf_gain"]
if f[2] < 0:
continue
wolves.append(f)
if rng.bernoulli(p["wolf_reproduce"]):
if room > 0:
e = f[2] // 2
f[2] -= e
pups.append([f[0], f[1], e])
room -= 1
else:
w.cap_hit = True
w.wolves = wolves + pups
w.sheep = [s for i, s in enumerate(w.sheep) if not eaten[i]]
for c in range(len(grass)):
if grass[c] == 0:
timer[c] -= 1
if timer[c] <= 0:
grass[c] = 1
w.green += 1
w.t += 1Run the full model from a terminal:
node simulation.js --initial-sheep 200 --initial-wolves 50 --seed 42
python simulation.py --initial-sheep 200 --initial-wolves 50 --seed 42Download the model
| File | Contents | Size |
|---|---|---|
README.md | How to run it, parameters, example result | 2.8 KB |
model.json | Parameters with ranges and defaults, outputs, the seed-42 example | 3.2 KB |
simulation.js | The model used on this page plus a command-line runner (Node 18+) | 12.8 KB |
simulation.py | Python 3.9+ port, standard library only | 11.8 KB |
example.csv | Result table from the example run | 15.7 KB |
LICENSE.txt | MIT License | 1.0 KB |
Assumptions & limitations
- Random walkers. Animals move to a random neighboring cell with no memory, no sight of food or danger, and no herding or pack hunting.
- Encounters on one cell. A wolf can only eat a sheep on the cell it moves to, at most one per step. Real predation depends on search, chase and handling time.
- Asexual, ageless animals. Birth is a fixed probability per step that splits energy; there are no sexes, ages, seasons or maximum lifespans.
- Fixed-order updates. All sheep act before all wolves in each step, and within each species animals act in list order. Other schedules, such as random order or simultaneous moves, would give different individual runs.
- Capped population. Births stop while the total number of animals is at the cap of 2,500, which only binds at extreme settings.
- A toy ecosystem. The model illustrates how cycles and extinctions arise from individual rules; it is not calibrated to any real wolf or sheep population.
References
- Wilensky, U. (1997). NetLogo Wolf Sheep Predation model. Center for Connected Learning and Computer-Based Modeling, Northwestern University. ccl.northwestern.edu/netlogo/models/WolfSheepPredation
- Wilensky, U. & Rand, W. (2015). An Introduction to Agent-Based Modeling. MIT Press.
- Lotka, A. J. (1925). Elements of Physical Biology. Williams & Wilkins.
- Volterra, V. (1926). Fluctuations in the abundance of a species considered mathematically. Nature, 118, 558–560. doi.org/10.1038/118558a0