library(regnans)GM99: the evolution of seed size
Balancing seed number, survival, and competitive ability
What the model represents
Plants have a fixed amount of resource to invest in seeds. A plant can therefore produce many small seeds or fewer large ones. Large seeds are more likely to survive before competition and their seedlings compete more effectively, but each one is more expensive to make.
Seeds land at discrete safe sites, and the number arriving at any site is Poisson distributed. Seedlings at the same site then compete for its resources in a size-asymmetric lottery: larger seeds have an advantage. If that advantage is strong enough, the intermediate seed-size strategy becomes a branching point. Small, numerous seeds can then coexist with large, highly competitive seeds.
The model comes from Geritz, van der Meijden, and Metz (1999), Evolutionary Dynamics of Seed Size and Seedling Competitive Ability.
Fitness equations
Pre-competitive survival and competitive ability:
\[ s(x) = \max\!\left(0,\; 1 - 2e^{-\beta x}\right), \qquad c(x) = e^{\alpha x}. \]
A focal mutant x' lands in a site alongside \(k_j \sim \text{Poisson}(N_j)\) seedlings of each resident strategy x_j. Its expected share of the site, averaged across all possible seed arrivals, is
\[ g = \mathbb{E}\!\left[\frac{c(x')}{c(x') + \sum_j k_j\,c(x_j)}\right]. \]
Invasion fitness is the mutant’s expected number of established offspring (a lifetime reproductive ratio, = 1, i.e. log = 0, at equilibrium):
\[ W(x') = \frac{R}{x'}\, s(x')\, g . \]
A single resident reaches the density N* that solves \(\tfrac{R}{x}s(x)\,\tfrac{1-e^{-N}}{N} = 1\). Only the products \(\alpha R\) and \(\beta R\) affect the outcome. There is no formula for the singular strategy, so we find it numerically. Branching becomes more likely as the competitive asymmetry \(\alpha\) increases.
Example analysis with regnans
Hold pre-competitive survival at \(\beta R = 15\). Weak competitive asymmetry (\(\alpha R = 4.5\)) gives a single stable strategy, whereas stronger asymmetry (\(\alpha R = 7\)) gives a branching point.
solve_x <- function(alpha, beta = 15, bounds_x = c(0.06, 0.95)) {
community_start(bounds(x = bounds_x),
harness = harness_gm99(alpha = alpha, beta = beta)) |>
community_solve_singularity()
}
css <- solve_x(4.5) # CSS
branch <- solve_x(7.0) # branching point
c(css = as.numeric(css$traits), branch = as.numeric(branch$traits)) css branch
0.1836378 0.6481794
We classify each solution using the curvature of invasion fitness at the singular strategy. Negative curvature indicates a fitness maximum and a stable strategy; positive curvature indicates a fitness minimum and branching:
curv <- function(comm, at, d = 1e-4) {
f <- comm$fitness_function
(f(at + d) - 2 * f(at) + f(at - d)) / d^2
}
c(css = curv(css, as.numeric(css$traits)),
branch = curv(branch, as.numeric(branch$traits))) css branch
-10.564513 1.439901
As a check, a resident at its own demographic equilibrium must have a lifetime reproductive ratio of exactly 1, or a log fitness of 0:
comm <- community_start(bounds(x = c(0.06, 0.95)),
harness = harness_gm99(alpha = 7, beta = 15)) |>
community_add(trait_matrix(0.645, "x")) |>
community_demography()
comm$resident_fitness # ≈ 0[1] 0
Pairwise invasibility plot
The green regions show which mutant seed sizes can invade each resident seed size. The pattern around the singular strategy shows that mutants on either side can invade, as expected at a branching point. Seed size is a strictly positive trait, so the community uses the log trait scale and the axes follow it.
pip <- community_start(bounds(x = c(0.06, 0.95)), trait_scale = "log",
harness = harness_gm99(alpha = 7, beta = 15)) |>
community_pip()
plot(pip) + ggplot2::ggtitle("GM99 (alpha R = 7, beta R = 15)")
A second branching as competition becomes more asymmetric
At \(\alpha R = 7\) the single seed-size strategy is a branching point. After branching, the community settles at a pair: a small-seeded strategy that makes many seeds, and a large-seeded one that wins the sites it reaches. That pair is the next singular point, and it too can be stable or a branching point.
Here we follow the pair as competitive asymmetry \(\alpha R\) grows. community_continue_singularity() steps through values of \(\alpha R\), predicting where the pair will be from its sensitivity, solving for it from there, and classifying it at each step (the method is described on the DD99 page). community_parameter_map() builds the model as a function of \(\alpha\) (with \(R = 1\)). There is no closed form for this pair, so what to look for is the curvature of invasion fitness at each resident.
Plotting helpers: plot_path(), add_path() and legend_path()
class_col <- c("CSS" = "#2a78d6", "branching point" = "#eb6834", "degenerate" = "grey45")
class_pch <- c("CSS" = 16, "branching point" = 17, "degenerate" = 1)
# One line per resident, each point marked by its classification, and a grey
# band where the classification changes between two parameter values.
add_path <- function(path) {
ch <- path$changes
if (nrow(ch) > 0)
rect(ch$p_before, par("usr")[3], ch$p_after, par("usr")[4],
col = "grey92", border = NA)
cls <- path$classification
for (j in seq_len(ncol(path$traits))) {
lines(path$p, path$traits[, j], col = "grey40")
points(path$p, path$traits[, j], pch = class_pch[cls], col = class_col[cls])
}
}
plot_path <- function(path, xlab, ylab, ylim = range(path$traits), ...) {
plot(range(path$p), ylim, type = "n", xlab = xlab, ylab = ylab, ...)
add_path(path)
}
# A legend for the classifications on a plot, and optionally one reference line.
legend_path <- function(where, cls, line = NULL, lty = 2) {
n <- length(line)
legend(where, legend = c(cls, line), bty = "n",
pch = c(class_pch[cls], rep(NA, n)), col = c(class_col[cls], rep("grey40", n)),
lty = c(rep(0, length(cls)), rep(lty, n)))
}Solve for the pair from two starting residents, then follow it from \(\alpha R = 7\) to \(7.75\):
pair <- community_solve_singularity(branch, x0 = trait_matrix(c(0.2, 0.7), "x"))
pair$traits x
[1,] 0.2061229
[2,] 0.6998292
path_alpha <- community_continue_singularity(pair, community_parameter_map(pair, "alpha"),
seq(7, 7.75, by = 0.125))
path_alpha<singularity_path: 7 points, p from 7 to 7.75 (coalition of 2)>
classification: CSS (p 7 to 7.5), branching point (p 7.625 to 7.75)
equilibrium solves: 75
path_alpha$changes p_before p_after from to
1 7.5 7.625 CSS branching point
At \(\alpha R = 7\) the pair is a CSS: both residents sit at fitness maxima, so once the population has branched, evolution stops there. As \(\alpha R\) grows, both seed sizes increase, and between \(\alpha R = 7.5\) and \(7.625\) the pair becomes a branching point. The curvature of invasion fitness at each resident shows why. The large-seeded resident stays at a fitness maximum, but the small-seeded resident’s curvature changes sign, so it becomes a fitness minimum and branches again:
curvature <- t(vapply(path_alpha$classifications,
function(cl) vapply(cl$hessian, as.numeric, 0), numeric(2)))
par(mfrow = c(1, 2))
plot_path(path_alpha, xlab = expression(alpha * R), ylab = "seed size of each resident",
ylim = c(0.15, 0.8))
legend_path("right", unique(path_alpha$classification))
matplot(path_alpha$p, curvature, type = "n",
xlab = expression(alpha * R), ylab = "curvature of invasion fitness")
ch <- path_alpha$changes
rect(ch$p_before, par("usr")[3], ch$p_after, par("usr")[4], col = "grey92", border = NA)
abline(h = 0, lty = 3)
matlines(path_alpha$p, curvature, lty = c(1, 2), col = "grey40")
legend("topleft", legend = c("small-seeded resident", "large-seeded resident"),
lty = c(1, 2), col = "grey40", bty = "n")
The small-seeded resident’s curvature changes sign from -0.44 at \(\alpha R = 7.5\) to 0.34 at \(7.625\), while the large-seeded resident’s stays near -0.7. Once competition is asymmetric enough, the small-seeded strategy splits in two and the pair gives way to a community of three.