DEV Community

Alan Matthew
Alan Matthew

Posted on

Building Ecological Simulations

Building Ecological Simulations: How to Model Population Growth & Carrying Capacity

When building simulations—whether you are working on game economies, agent-based modeling (ABM), resource management systems, or ecological software—modeling how populations or entities grow over time is a common challenge.

While simple linear or exponential growth models are easy to implement, they fail in real-world scenarios where resources are finite. Unchecked exponential growth leads to infinite numbers, which breaks simulations and game loops.

To solve this, developers rely on the Verhulst Logistic Growth Model, which introduces a hard ceiling known as Carrying Capacity (K). In this post, we’ll break down the mathematics behind logistic growth, examine how the rate of change behaves under resource constraints, and explore an interactive Carrying Capacity Calculator.


1. From Exponential J-Curves to Logistic S-Curves

The Exponential Baseline

In an environment with unlimited resources, a population ($N$) grows at a rate proportional to its current size. This is expressed by the differential equation:

dN/dt = rN

  • dN/dt: The instantaneous rate of population change over time.
  • N: Current population size.
  • r: Intrinsic per capita growth rate (biotic potential).

Integrating this yields the exponential growth formula Nt = N0 * e^(rt), which results in an unrealistic J-curve where growth accelerates infinitely.

Adding the Environmental Brake: Logistic Growth

To fix this, Pierre François Verhulst introduced a scaling feedback factor in 1838: (K - N) / K. This term represents the unused capacity of the environment.

The complete logistic differential equation is:

dN/dt = rN * (K - N) / K

  • K: Carrying capacity (the maximum sustainable population size determined by available resources).

How the Braking Term Behaves:

  1. When N << K (Low Density): The fraction (K - N) / K is approximately 1, meaning the population grows near its exponential rate (rN).
  2. When N -> K (Approaching Capacity): The fraction approaches 0, causing the growth rate dN/dt to decelerate smoothly to zero.
  3. When N > K (Overshoot): The fraction becomes negative, driving dN/dt < 0, which simulates a population dieback or crash back down toward the threshold.

2. Analytical Solution vs. Numerical Simulation

When writing code to simulate population growth over discrete time steps, developers often use Euler’s method (iterative loops). However, numerical step approximations introduce cumulative rounding errors.

To calculate the exact population size (Nt) at any arbitrary future time t instantly, we use the analytical integral of the logistic differential equation:

Nt = K / (1 + ((K - N0) / N0) * e^(-rt))

  • N0: Initial population size at t = 0.
  • e: Euler's number (~2.71828).

3. Finding the Inflection Point & Peak Growth Rate

In system dynamics and resource planning, finding the inflection point on an S-curve is critical. This is the exact moment where population growth transitions from acceleration to deceleration.

Maximum population growth always occurs when the population is exactly half of the carrying capacity:

N_inflection = K / 2

Substituting this back into the logistic equation gives the maximum theoretical growth rate (Maximum Sustainable Yield):

max(dN/dt) = rK / 4

This principle is widely used in fisheries management, wildlife conservation, and capacity planning to harvest or scale resources without destabilizing the system.


4. Implementation Example (JavaScript)

Here is a clean JavaScript implementation of the integrated logistic growth formula that you can plug straight into a dashboard or simulation script:


javascript
/**
 * Calculates future population size using the integrated logistic growth model.
 * 
 * @param {number} N0 - Initial population size
 * @param {number} K  - Carrying capacity
 * @param {number} r  - Intrinsic growth rate
 * @param {number} t  - Time elapsed
 * @returns {number}  - Population size at time t
 */
function calculateLogisticGrowth(N0, K, r, t) {
    if (N0 <= 0 || K <= 0 || r <= 0 || t < 0) {
        throw new Error("Invalid parameters provided.");
    }

    const numerator = K;
    const denominator = 1 + ((K - N0) / N0) * Math.exp(-r * t);

    return numerator / denominator;
}

// Example: Starting with 50 individuals, K = 1000, r = 0.15, over 25 time units
const populationAtT25 = calculateLogisticGrowth(50, 1000, 0.15, 25);
console.log(`Population at t=25: ${Math.round(populationAtT25)}`);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)