Fishery Simulation

A fishery simulation of stocks, flows, and feedback loops managing fish populations.

Level:Beginner

populationresource-managementsustainabilitymanagementecosystemstocks-flowsreinforcing-loopbalancing-looprenewable-resourcequota-policy

  • Stocks:population
  • Flows:births, quota
  • Feedback Loops:reproduction (reinforcing), quota (balancing)
  • Probes:population, quota, gap_to_capacity, extracted_total

Feedback Loops

Understand the balancing and reinforcing feedback loops that drive system behavior and create complex dynamics in systems thinking.

Explore Feedback Loops

System Archetypes

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

Explore System Archetypes
simulation.py

Managing a fishery with delayed quotas

Our lake has a healthy fish population that grows logistically. Local managers set the harvest quota based on numbers from last week. If the population drops but the quota lags behind, we risk overfishing.


from tys import probe, progress

def simulate(cfg: dict):

    import simpy
    env = simpy.Environment()

Parameters that shape the scenario.

    population        = cfg["initial_pop"]        # starting fish in the lake
    capacity          = cfg["carrying_capacity"]  # ecological limit of the lake
    growth_rate       = cfg["growth_rate"]        # how quickly fish reproduce
    quota_fraction    = cfg["quota_fraction"]     # fraction of perceived stock harvested
    perception_delay  = cfg["perception_delay"]   # steps our information is behind
    sim_time          = cfg["sim_time"]           # length of the simulation
    history           = [population] * perception_delay  # ring buffer of past counts
    extracted_total   = 0.0  # running total of fish taken

    done = env.event()

Each tick we look at old population data, set a quota, let fish reproduce, and harvest accordingly. The delay means we might be taking too many fish.

    def dynamics():
        nonlocal population, extracted_total
        for t in range(sim_time):
            perceived = history[0]  # what the managers believe
            quota     = quota_fraction * perceived
            birth     = growth_rate * population * (1 - population / capacity)
            population = max(population + birth - quota, 0)  # update stock safely
            extracted_total += quota
            history.append(population)
            history.pop(0)  # advance the perception window

            yield env.timeout(1)

        progress(100)

When all done report the final state of the ecosystem.

        done.succeed({
            "final_population": population,
            "utilisation": population / capacity,
            "survived": population > 0.05 * capacity,
            "extracted_total": extracted_total
        })
    env.process(dynamics())

Record the actual population and the quota computed from our stale perception.

    def recorder():
        while True:
            perceived = history[0]
            quota = quota_fraction * perceived
            probe("population", env.now, population)
            probe("quota", env.now, quota)
            probe("gap_to_capacity", env.now, capacity - population)
            probe("extracted_total", env.now, extracted_total)
            yield env.timeout(0.5)  # half-step for smoother lines
    env.process(recorder())
    
    env.run(until=done)
    return done.value

Install the necessary required libraries for this simulation.

def requirements():
    return {
        "builtin": ["micropip", "pyyaml"],
        "external": ["simpy==4.1.1"],
    }
Stable.yaml
initial_pop: 30
carrying_capacity: 100
growth_rate: 0.4
quota_fraction: 0.21
perception_delay: 7
sim_time: 300
Charts (Stable)

population

population chartCSV
Samples601 @ 0.00–300.00
Valuesmin 32.10, mean 47.16, median 47.36, max 60.44, σ 4.68

quota

quota chartCSV
Samples601 @ 0.00–300.00
Valuesmin 6.30, mean 9.83, median 9.91, max 12.69, σ 1.10

gap_to_capacity

gap_to_capacity chartCSV
Samples601 @ 0.00–300.00
Valuesmin 39.56, mean 52.84, median 52.64, max 67.90, σ 4.68

extracted_total

extracted_total chartCSV
Samples601 @ 0.00–300.00
Valuesmin 6.30, mean 1461.50, median 1458.69, max 2944.14, σ 858.98
Final Results (Stable)
MetricValue
final_population48.27
utilisation0.48
survivedtrue
extracted_total2944.14
Collapse.yaml
initial_pop: 30
carrying_capacity: 100
growth_rate: 0.4
quota_fraction: 0.22
perception_delay: 7
sim_time: 300
Charts (Collapse)

population

population chartCSV
Samples601 @ 0.00–300.00
Valuesmin 0.00, mean 30.07, median 31.45, max 66.86, σ 21.39

quota

quota chartCSV
Samples601 @ 0.00–300.00
Valuesmin 0.00, mean 6.75, median 6.92, max 14.71, σ 4.61

gap_to_capacity

gap_to_capacity chartCSV
Samples601 @ 0.00–300.00
Valuesmin 33.14, mean 69.93, median 68.55, max 100.00, σ 21.39

extracted_total

extracted_total chartCSV
Samples601 @ 0.00–300.00
Valuesmin 6.60, mean 1276.63, median 1398.45, max 2033.96, σ 667.49
Final Results (Collapse)
MetricValue
final_population0.00
utilisation0.00
survivedfalse
extracted_total2033.96
FAQ
Why can quotas overshoot the population?
Quota decisions look at a delayed perception of stock stored in a ring buffer, so real fish numbers may drop before managers react.
How is population growth calculated each step?
Births follow logistic growth: growth_rate * population * (1 - population/carrying_capacity).
Which probe shows risk of collapse?
gap_to_capacity tracks how far the population is from the lake's carrying capacity.