Logistic Growth

Grow a population too fast and it overshoots its carrying capacity before settling; grow it gently and it glides up to the same limit.

Level: Beginner

populationcapacityovershootnonlinear-dynamicsreinforcing-loop

  • Stocks: population
  • Flows: growth
  • Feedback Loops: reinforcing adoption, saturation constraint
  • Probes: population, growth

Dynamic Behavior Patterns

Explore common system behaviors: exponential growth, goal-seeking decay, overshoot-and-collapse, and S-curve saturation.

Take the Quiz

System Archetypes

Learn recurring structural patterns like Limits to Growth, Fixes That Fail, and Tragedy of the Commons, plus high-leverage interventions.

Take the Quiz
simulation.py

Overshoot: when fast growth blows past its limit

A population grows logistically toward the carrying capacity K. With a gentle growth rate it glides up and settles. Push the rate past a threshold and each step's correction is so large the population overshoots K, then rings back down — the same loop, two very different fates.

The knob is growth_rate. Below ~1 the approach is smooth; between 1 and 2 it overshoots once and oscillates back to K; above 2 it never settles.


from tys import probe, progress

Evolve a population with the discrete logistic equation.

def simulate(cfg: dict):

    import simpy
    env = simpy.Environment()

    population = cfg["initial_pop"]
    r = cfg["growth_rate"]
    K = cfg["carrying_capacity"]
    steps = cfg["steps"]

    peak = population
    done = env.event()

Each step nudges the population by r * P * (1 - P/K).

    def run():
        nonlocal population, peak
        for t in range(steps):
            growth = r * population * (1 - population / K)   # net change this step
            population = max(population + growth, 0)          # a population can't go below zero
            peak = max(peak, population)
            probe("population", env.now, population)
            probe("growth", env.now, growth)                 # turns negative while overshooting
            progress(int(100 * (t + 1) / steps))
            yield env.timeout(1)
        done.succeed({
            "final_population": population,
            "peak_population": peak,
            "overshot": peak > K * 1.001,
        })

    env.process(run())
    env.run(until=done)
    return done.value


def requirements():
    return {
        "external": ["simpy==4.1.1"],
    }
Overshoot.yaml
# overshoot.yaml — the default, with the growth rate past the overshoot threshold.
# At growth_rate 1.9 the population shoots above the carrying capacity, then
# oscillates back down to it.
initial_pop: 10
growth_rate: 1.9
carrying_capacity: 100
steps: 50
Charts (Overshoot)

population

Samples50 @ 0.00–49.00
Valuesmin 27.10, mean 97.85, median 99.94, max 108.07, σ 11.57

growth

Samples50 @ 0.00–49.00
Valuesmin -16.56, mean 1.80, median 0.14, max 43.43, σ 9.70
Final Results (Overshoot)
MetricValue
final_population99.95
peak_population108.07
overshottrue
Smooth.yaml
# smooth.yaml — the contrast: a gentle growth rate that glides up to capacity.
# Identical to overshoot.yaml except growth_rate (1.9 -> 0.8): below 1, the
# population approaches K from below and never crosses it.
initial_pop: 10
growth_rate: 0.8
carrying_capacity: 100
steps: 50
Charts (Smooth)

population

Samples50 @ 0.00–49.00
Valuesmin 17.20, mean 94.62, median 100.00, max 100.00, σ 17.38

growth

Samples50 @ 0.00–49.00
Valuesmin 0.00, mean 1.80, median 0.00, max 19.79, σ 4.82
Final Results (Smooth)
MetricValue
final_population100.00
peak_population100.00
overshotfalse
FAQ
What makes the population overshoot the carrying capacity?
Each step changes the population by r*population*(1 - population/K). When the growth rate r is large, that correction is bigger than the remaining gap to K, so the population climbs above capacity in a single step before the now-negative growth term pulls it back.
Where is the tipping point?
For this discrete model the approach is smooth when growth_rate is below 1, overshoots once and rings back down to K when it is between 1 and 2, and stops settling (sustained oscillation) once it exceeds 2.
Why does the smooth config never cross the line?
At growth_rate 0.8 each step closes only part of the gap to K, so the population approaches capacity from below and never exceeds it.
What is a real-world example of overshoot?
Populations that reproduce faster than their resources can recover - algal blooms or species introduced to a new habitat - shoot past what the environment can sustain, then crash back toward it.