OS-030 · Population & epidemics

Lotka–Volterra Predator–Prey Model

Prey grow exponentially, predators die exponentially, and every encounter turns prey into predators. Those three assumptions give two differential equations whose solutions cycle forever on closed loops — elegant, exactly solvable in part, and, as the lab shows, balanced on a knife edge.

Interactive model

OS-030 · lotka-volterra · deterministic
prey xpredators yγ/δα/β
orbitneighboring orbitsnullclinesequilibriumstart
Parameters
Prey lost per prey–predator encounter rate
Predator growth per prey–predator encounter rate
Equilibrium prey γ/δ
50.00
Equilibrium predators α/β
20.00
Period (simulated)
9.472
Small-oscillation period 2π/√(αγ)
8.886
Prey min
14.66
Prey max
119.62
Predators min
8.81
Predators max
38.08
Conserved quantity V
−3.1917
max |V − V₀| / |V₀|
3.3e-13

Example run: default parameters.

The equations

With prey x and predators y (numbers or densities), four positive rates define the model:

dx/dt = αx − βxy
dy/dt = δxy − γy
  • α — prey growth rate without predators (exponential, no food limit).
  • β — rate at which encounters remove prey; encounters are proportional to xy (mass action).
  • δ — rate at which encounters add predators.
  • γ — predator death rate without prey.

Setting both derivatives to zero gives the coexistence equilibrium x* = γ/δ, y* = α/β, where the dashed lines (nullclines) cross. The prey level at equilibrium is set by the predator's parameters and vice versa. Averaged over a full cycle, the populations equal these equilibrium values exactly: integrating d(ln x)/dt = α − βy over one period gives ⟨y⟩ = α/β, and likewise ⟨x⟩ = γ/δ.

Closed orbits and neutral stability

Dividing one equation by the other separates the variables and yields a quantity that never changes along a solution:

V = δx − γ ln x + βy − α ln y

V has a single minimum at the equilibrium and rises in every direction, so its level curves are nested closed loops. Each solution is trapped on the loop it starts on: the populations return exactly to their initial values, again and again. Move the initial prey or predators and the lab draws a different loop; the gray curves are neighboring loops for the same parameters.

This is neutral stability, and it is not the same thing as a limit cycle. A limit cycle is a single loop that nearby trajectories approach, so the amplitude is a property of the system. Here there is no preferred loop: a disturbance moves the system to another orbit and it stays there, with a new amplitude, forever.

That makes the model structurally fragile. The closed orbits exist only because V is exactly conserved, and almost any change to the equations breaks the conservation law. Give the prey a carrying capacity and the orbits spiral in to a steady state; add predator saturation and they can spiral out or settle on a genuine limit cycle. Real cycles, such as those of the agent-based wolf–sheep model, are noisy and have amplitudes set by the system, not by the starting point.

Near the equilibrium the orbits are nearly ellipses and the period approaches 2π/√(αγ), from linearizing the equations. Wider orbits take longer: they spend a long time near the axes where one population is scarce and changes slowly.

How the simulation solves it

The lab integrates the equations with the classical fourth-order Runge–Kutta method (RK4). The number of steps per output interval is chosen from the current per-capita rates, so the fast crashes of a wide orbit get small steps and a quiet stretch gets large ones. Because V should be exactly constant, its largest relative drift over the run is a direct measure of numerical error. With RK4 it stays tiny; the simple Euler method would instead let V grow every step and turn the closed loops into outward spirals — see Euler vs Runge–Kutta.

The simulated period is the mean time between upward crossings of the prey population through γ/δ, interpolated within steps.

Example result

With α = 1, β = 0.05, δ = 0.01, γ = 0.5 and 40 prey with 9 predators at the start, the equilibrium is (50.00, 20.00). The prey swing between 14.66 and 119.62, the predators between 8.81 and 38.08. One cycle takes 9.472 time units — longer than the small-oscillation period 8.886, because this orbit is wide. Over 50 time units the conserved quantity V = −3.1917 drifts by only 3.3e-13 of its value.

Output of lotka-volterra with alpha=1 beta=0.05 delta=0.01 gamma=0.5 x0=40 y0=9 t_end=50
Equilibrium prey γ/δ50.00
Equilibrium predators α/β20.00
Period (simulated)9.472
Small-oscillation period 2π/√(αγ)8.886
Prey min14.66
Prey max119.62
Predators min8.81
Predators max38.08
Conserved quantity V−3.1917
max |V − V₀| / |V₀|3.3e-13

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. The excerpts show the core of each.

JavaScript — simulation.js
// dx/dt = αx − βxy (prey),  dy/dt = δxy − γy (predators)

export function derivatives(x, y, p) {
  return [p.alpha * x - p.beta * x * y, p.delta * x * y - p.gamma * y];
}

export function rk4Step(x, y, h, p) {
  const k1 = derivatives(x, y, p);
  const k2 = derivatives(x + (h / 2) * k1[0], y + (h / 2) * k1[1], p);
  const k3 = derivatives(x + (h / 2) * k2[0], y + (h / 2) * k2[1], p);
  const k4 = derivatives(x + h * k3[0], y + h * k3[1], p);
  return [x + (h / 6) * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]), y + (h / 6) * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1])];
}

// Along every exact solution this quantity is constant, so each orbit is a closed curve.

export const conserved = (x, y, p) => p.delta * x - p.gamma * Math.log(x) + p.beta * y - p.alpha * Math.log(y);
Python — simulation.py
def derivatives(x, y, p):
    """dx/dt = alpha*x - beta*x*y (prey), dy/dt = delta*x*y - gamma*y (predators)"""
    return (p["alpha"] * x - p["beta"] * x * y, p["delta"] * x * y - p["gamma"] * y)


def rk4_step(x, y, h, p):
    k1 = derivatives(x, y, p)
    k2 = derivatives(x + h / 2 * k1[0], y + h / 2 * k1[1], p)
    k3 = derivatives(x + h / 2 * k2[0], y + h / 2 * k2[1], p)
    k4 = derivatives(x + h * k3[0], y + h * k3[1], p)
    return (x + h / 6 * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]), y + h / 6 * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]))


def conserved(x, y, p):
    """Constant along every exact solution, so each orbit is a closed curve."""
    return p["delta"] * x - p["gamma"] * math.log(x) + p["beta"] * y - p["alpha"] * math.log(y)

Run the full model from a terminal:

Shell
node simulation.js --alpha 1 --beta 0.05
python simulation.py --alpha 1 --beta 0.05

Download the model

lotka-volterra.zip6 files · 30.6 KBDownload .zip
Files in lotka-volterra.zip
FileContentsSize
README.mdHow to run it, parameters, example result2.4 KB
model.jsonParameters with ranges and defaults, outputs, the seed-42 example3.1 KB
simulation.jsThe model used on this page plus a command-line runner (Node 18+)9.4 KB
simulation.pyPython 3.9+ port, standard library only8.0 KB
example.csvResult table from the example run45.8 KB
LICENSE.txtMIT License1.0 KB

Assumptions & limitations

  • Unlimited prey food. Without predators the prey grow exponentially forever; there is no carrying capacity.
  • Mass-action predation. Encounters are proportional to xy, and a predator never gets full or spends time handling prey.
  • Continuous and deterministic. Populations are real numbers with no randomness, so they can dip to tiny values and recover; real populations that small would likely go extinct.
  • No structure. No age, space, seasons or time delays; every individual is identical.
  • Structurally fragile. The closed orbits depend on the exact form of the equations; small changes to the model change the qualitative behavior.

References

  1. Lotka, A. J. (1925). Elements of Physical Biology. Williams & Wilkins.
  2. Volterra, V. (1926). Fluctuations in the abundance of a species considered mathematically. Nature, 118, 558–560. doi.org/10.1038/118558a0
  3. Murray, J. D. (2002). Mathematical Biology I: An Introduction, 3rd ed. Springer.
  4. Strogatz, S. H. (2015). Nonlinear Dynamics and Chaos, 2nd ed. Westview Press.