OS-028 · Population & epidemics

Logistic Growth Simulation

Exponential growth assumes every individual keeps reproducing at the same rate forever. Logistic growth adds one correction — the per-capita rate falls linearly as the population fills its carrying capacity K — and turns an explosion into an S-curve. Add a constant harvest and the same equation shows how a population can be fished to collapse.

Interactive model

OS-028 · logistic-growth · deterministic
logistic N(t)exponential N₀eʳᵗKequilibria (h > 0)inflection point
growth rN(1 − N/K)harvest hequilibria (filled: stable)direction N moves
Parameters
Per-capita growth rate when the population is small
Constant number removed per time unit; 0 gives the classic curve
Outcome
grows toward K
Logistic N(T)
1,000.0
Exponential N₀eʳᵀ
3.27e+7
Doubling time ln 2 / r
1.39
Inflection time (N = K/2)
9.19
Max growth rate rK/4
125.0
Stable equilibrium N*₊
1,000.0
Unstable equilibrium N*₋
0.0
Collapse time (N = 0)
—
max |RK4 − exact| / K (h = 0)
1.1e-12

Example run: default parameters.

Exponential vs logistic growth

Both models start from the same per-capita growth rate r. Exponential growth keeps it forever; logistic growth scales it by the fraction of capacity still free:

exponential: dN/dt = rN  ⇒  N(t) = N₀ert
logistic: dN/dt = rN(1 − N/K)  ⇒  N(t) = K / (1 + ((K − N₀)/N₀)·e−rt)

While N is a small fraction of K the factor 1 − N/K is close to 1 and the two curves are indistinguishable: the population doubles every ln 2/r time units. They part company as crowding sets in. The logistic curve is steepest exactly at N = K/2, its inflection point, where the growth rate reaches its maximum rK/4. For N₀ < K/2 that happens at

t* = (1/r) · ln((K − N₀)/N₀)

After the inflection point growth slows symmetrically: the S-curve has the same shape on both sides of it. A population that starts above K simply declines toward K. The lower chart shows why: the growth rate is a parabola in N, positive between 0 and K and negative above K.

Harvesting and the collapse threshold

Now remove a constant number h of individuals per time unit — a fixed fishing quota, say:

dN/dt = rN(1 − N/K) − h

In the lower chart the harvest is the horizontal line. Where it crosses the parabola, growth exactly replaces the catch:

N* = K/2 ± √(K²/4 − hK/r)
  • The upper root N*₊ is stable: a little above it the catch exceeds growth and N falls back; a little below, growth wins.
  • The lower root N*₋ is a tipping point: start below it, or get pushed below it, and harvesting outruns growth all the way to zero.
  • Above h = rK/4 there is no equilibrium at all. The line clears the top of the parabola, dN/dt is negative for every N, and the population collapses from any starting size. rK/4 is the maximum sustainable yield of this model, taken at N = K/2.

With the defaults (r = 0.5, K = 1,000) the threshold is 125. Set N₀ to 1,000 and h to 100: the population settles at N*₊ ≈ 723.6. Raise h to 130 and set T to 100: the equilibria vanish, yet the decline stalls near K/2 for a long time — the ghost of the lost equilibrium — before the population crashes at t ≈ 54.9. Near the threshold, a harvest that looks stable for years can still be fatal. The two roots merging and vanishing is a saddle-node bifurcation.

How the curves are computed

Where a closed form exists the lab uses it: the exponential curve always, and the logistic curve when h = 0. With harvesting it integrates the equation with the classical fourth-order Runge–Kutta method (RK4), 4,000 fixed steps over the time span. Once N reaches zero it stays there — the model treats extinction as permanent, and the collapse time is interpolated inside the step where N crosses zero.

For h = 0 the lab also runs RK4 and reports its largest deviation from the exact solution, relative to K, as an accuracy check; Euler vs Runge–Kutta explains why the error is so small.

Example result

With r = 0.5, K = 1,000, N₀ = 10 and no harvest, the population doubles every 1.39 time units at first, passes the inflection point N = K/2 at t = 9.19 with the maximum growth rate of 125.0 per time unit, and reaches 1,000.0 by t = 30. Exponential growth from the same start would reach 3.27e+7. RK4 matches the exact logistic curve to within 1.1e-12 of K.

Output of logistic-growth with r=0.5 k=1000 n0=10 h=0 t_end=30
Outcomegrows toward K
Logistic N(T)1,000.0
Exponential N₀eʳᵀ3.27e+7
Doubling time ln 2 / r1.39
Inflection time (N = K/2)9.19
Max growth rate rK/4125.0
Stable equilibrium N*₊1,000.0
Unstable equilibrium N*₋0.0
Collapse time (N = 0)—
max |RK4 − exact| / K (h = 0)1.1e-12

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
// dN/dt = rN(1 − N/K) − h. With h = 0 this is Verhulst's logistic equation.
export const rate = (n, r, k, h) => r * n * (1 - n / k) - h;

// Closed forms: exponential growth dN/dt = rN, and the logistic curve for h = 0.
export const exponential = (t, r, n0) => n0 * Math.exp(r * t);
export const logistic = (t, r, k, n0) => k / (1 + ((k - n0) / n0) * Math.exp(-r * t));

// Equilibria of the harvested model: rN(1 − N/K) = h has real roots only if h ≤ rK/4.
export function equilibria(r, k, h) {
  const disc = (k * k) / 4 - (h * k) / r;
  if (disc < 0) return null; // harvest exceeds the maximum growth rate: no equilibrium
  const s = Math.sqrt(disc);
  return { stable: k / 2 + s, unstable: k / 2 - s };
}

// One RK4 step of size dt.
export function rk4Step(n, dt, r, k, h) {
  const k1 = rate(n, r, k, h);
  const k2 = rate(n + (dt / 2) * k1, r, k, h);
  const k3 = rate(n + (dt / 2) * k2, r, k, h);
  const k4 = rate(n + dt * k3, r, k, h);
  return n + (dt / 6) * (k1 + 2 * k2 + 2 * k3 + k4);
}
Python — simulation.py
def rate(n, r, k, h):
    """dN/dt = r*N*(1 - N/K) - h; h = 0 is Verhulst's logistic equation."""
    return r * n * (1 - n / k) - h


def exponential(t, r, n0):
    return n0 * math.exp(r * t)


def logistic(t, r, k, n0):
    return k / (1 + (k - n0) / n0 * math.exp(-r * t))


def equilibria(r, k, h):
    """Roots of r*N*(1 - N/K) = h; none when h > r*K/4."""
    disc = k * k / 4 - h * k / r
    if disc < 0:
        return None
    s = math.sqrt(disc)
    return {"stable": k / 2 + s, "unstable": k / 2 - s}


def rk4_step(n, dt, r, k, h):
    k1 = rate(n, r, k, h)
    k2 = rate(n + dt / 2 * k1, r, k, h)
    k3 = rate(n + dt / 2 * k2, r, k, h)
    k4 = rate(n + dt * k3, r, k, h)
    return n + dt / 6 * (k1 + 2 * k2 + 2 * k3 + k4)

Run the full model from a terminal:

Shell
node simulation.js --r 0.5 --k 1000
python simulation.py --r 0.5 --k 1000

Download the model

logistic-growth.zip6 files · 18.1 KBDownload .zip
Files in logistic-growth.zip
FileContentsSize
README.mdHow to run it, parameters, example result2.3 KB
model.jsonParameters with ranges and defaults, outputs, the seed-42 example2.8 KB
simulation.jsThe model used on this page plus a command-line runner (Node 18+)9.3 KB
simulation.pyPython 3.9+ port, standard library only7.8 KB
example.csvResult table from the example run17.8 KB
LICENSE.txtMIT License1.0 KB

Assumptions & limitations

  • Linear crowding. The per-capita growth rate falls in a straight line from r at N = 0 to zero at N = K. Real populations often show thresholds or Allee effects (poor growth at very low density).
  • Constant environment. r and K do not change with seasons, weather or evolution, and the population responds instantly, with no age structure or maturation delay.
  • Continuous and deterministic. N is a real number, not a count of individuals, and there is no chance: a population of 3.2 behaves like any other. Small populations are in reality vulnerable to random extinction.
  • Constant harvest. The catch h is fixed regardless of stock size. Proportional (effort-based) harvesting behaves differently and does not collapse this way.
  • Extinction at zero. Once N reaches 0 the model stops harvesting and N stays at 0.

References

  1. Verhulst, P.-F. (1838). Notice sur la loi que la population suit dans son accroissement. Correspondance mathématique et physique, 10, 113–121.
  2. Murray, J. D. (2002). Mathematical Biology I: An Introduction, 3rd ed. Springer.
  3. Strogatz, S. H. (2015). Nonlinear Dynamics and Chaos, 2nd ed. Westview Press.