Adaptive Dynamics
Adaptive dynamics asks where a continuous trait — a pathogen’s transmission rate, a host’s defense investment, a body size — will settle under natural selection. The idea is to test, for every possible resident population, whether a slightly different rare mutant can invade. Following the sequence of successful invasions traces the trait to its long-term evolutionary endpoints and reveals whether those endpoints are stable or split a population in two.
Invasion fitness
Fix a resident population with trait at its ecological equilibrium. Introduce a rare mutant with trait and ask whether it grows. Its long-term per-capita growth rate in the environment set by the resident is the invasion fitness . A mutant spreads when and dies out when , and by construction a resident cannot invade itself, so always. Evolution proceeds as a succession of invasions by nearby mutants, each replacing the resident and shifting the trait a small step.
The selection gradient and singular strategies
Near the resident the direction of change is set by the slope of invasion fitness in the mutant trait, the selection gradient
Where selection pushes the trait up; where it pushes down. A trait value where the gradient vanishes,
is an evolutionarily singular strategy — a candidate endpoint where directional selection stops. What happens at depends on two independent second-order properties.
Evolutionary versus convergence stability
Two distinct questions must both be asked at a singular point.
Evolutionary stability (ESS). Can any nearby mutant invade once the population sits at ? If invasion fitness has a maximum in at ,
then no neighbor invades and is uninvadable.
Convergence stability. Does the succession of invasions actually lead toward from nearby residents? This holds when the selection gradient decreases through the singular point,
The two properties are logically separate. A singular strategy that is both convergence stable and evolutionarily stable is a continuously stable strategy (CSS): selection carries the trait to and holds it there. A point that is convergence stable but not evolutionarily stable is an evolutionary branching point — the population is drawn to , but once there it experiences disruptive selection, splits, and diversifies into two coexisting strains.
The pairwise invasibility plot
The pairwise invasibility plot (PIP) shows the sign of over the plane of resident against mutant . The diagonal is always neutral. A singular strategy sits where the boundary between the invasion and exclusion regions crosses the diagonal, and the local pattern of the shaded regions around that crossing reads off its stability: whether the vertical strip above lies inside or outside the invasion region distinguishes a CSS from a branching point.
A worked example
Take the transmission-virulence trade-off from the evolution of virulence. An infection with virulence transmits at and clears at total rate , so its reproduction number is
A rare mutant strain invades the resident’s disease-free host environment exactly when its own exceeds the resident’s, so invasion fitness has the sign of and the singular strategy maximizes . Setting with gives , so
Because has a single interior maximum, its second derivative at is negative, so is both evolutionarily stable and convergence stable — a CSS at , with no branching. With and the optimum is .
In code
R
gamma <- 0.5; mu <- 0.1; a <- 3
R0 <- function(al) a * sqrt(al) / (gamma + al + mu)
grad <- function(x, h = 1e-6) (R0(x + h) - R0(x - h)) / (2 * h)
x_star <- uniroot(grad, c(0.05, 5))$root # selection gradient = 0
$
d2 <- function(x, h = 1e-4) (R0(x + h) - 2 * R0(x) + R0(x - h)) / h^2
c(x_star = x_star, ess = d2(x_star) < 0) # ~0.6, TRUE (uninvadable)
Python
import numpy as np
from scipy.optimize import brentq
gamma, mu, a = 0.5, 0.1, 3.0
def R0(al):
return a * np.sqrt(al) / (gamma + al + mu)
def grad(x, h=1e-6):
"""Selection gradient: slope of invasion fitness at y = x."""
return (R0(x + h) - R0(x - h)) / (2 * h)
x_star = brentq(grad, 0.05, 5.0) # singular strategy where gradient vanishes
def curv(x, h=1e-4):
return (R0(x + h) - 2 * R0(x) + R0(x - h)) / h**2
def dgrad(x, h=1e-3):
return (grad(x + h) - grad(x - h)) / (2 * h)
ess = curv(x_star) < 0 # fitness maximum: uninvadable
conv = dgrad(x_star) < 0 # gradient decreasing: attracting
kind = "CSS" if ess and conv else "branching point" if conv else "repellor"
print(f"singular strategy alpha* = {x_star:.4f}")
print(f"analytic gamma + mu = {gamma + mu:.4f}")
print(f"evolutionarily stable = {ess}")
print(f"convergence stable = {conv}")
print(f"classification = {kind}")
singular strategy alpha* = 0.6000
analytic gamma + mu = 0.6000
evolutionarily stable = True
convergence stable = True
classification = CSS
Julia
using Roots
γ, μ, a = 0.5, 0.1, 3.0
R0(α) = a * sqrt(α) / (γ + α + μ)
grad(x; h = 1e-6) = (R0(x + h) - R0(x - h)) / (2h)
x_star = find_zero(grad, (0.05, 5.0)) # singular strategy
curv(x; h = 1e-4) = (R0(x + h) - 2R0(x) + R0(x - h)) / h^2
ess = curv(x_star) < 0 # uninvadable
println("alpha* = ", round(x_star, digits = 4), " ESS = ", ess)
Why it matters
Adaptive dynamics gives infectious-disease evolution a common language for continuous traits: virulence, drug resistance, host range, and antigenic escape are all traits under invasion selection. Its central warning is that being attracting and being uninvadable are separate properties, so a trait can evolve toward a value where selection then turns disruptive and the pathogen population diversifies — the branching-point route to coexisting strains. The same invasion-fitness logic connects to evolutionary game theory, the evolution of cooperation, and the Price equation, and it frames the treatment problem taken up in resistance evolution.