Fishery Simulation
A fishery simulation of stocks, flows, and feedback loops managing fish populations.
Level:Beginner
Feedback Loops
Understand the balancing and reinforcing feedback loops that drive system behavior and create complex dynamics in systems thinking.
Explore Feedback LoopsSystem Archetypes
Learn recurring structural patterns like Limits to Growth, Fixes That Fail, and Tragedy of the Commons, plus high-leverage interventions.
Explore System ArchetypesManaging 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"],
}
initial_pop: 30
carrying_capacity: 100
growth_rate: 0.4
quota_fraction: 0.21
perception_delay: 7
sim_time: 300
Metric | Value |
---|---|
final_population | 48.27 |
utilisation | 0.48 |
survived | true |
extracted_total | 2944.14 |
initial_pop: 30
carrying_capacity: 100
growth_rate: 0.4
quota_fraction: 0.22
perception_delay: 7
sim_time: 300
Metric | Value |
---|---|
final_population | 0.00 |
utilisation | 0.00 |
survived | false |
extracted_total | 2033.96 |
- 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.