codexpast-exam-of-the-short-options-course-of-the-university-of-oxford2021-population-bifurcation.py
past-exam-of-the-short-options-course-of-the-university-of-oxford/2021-population-bifurcation.py
"""Continuous population growth and equilibria; Python 3.14, NumPy/Matplotlib per pyproject.toml."""
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
def main():
b, delta, r = 1., 1., 3.
n = np.linspace(0, 3, 700)
f = n*(r/(1+b*n*n)-delta)
star = np.sqrt((r-delta)/(b*delta))
fig, axes = plt.subplots(1, 2, figsize=(10, 3.8), layout='constrained')
ax = axes[0]
ax.axhline(0, color='#555f68', lw=.9)
ax.plot(n, f, color='#176da5', lw=2)
ax.scatter([0, star], [0, 0], color='#263238', zorder=4, s=25)
for start, stop in [(.28,.7), (2.45,1.95)]:
ax.annotate('', xy=(stop,.09), xytext=(start,.09),
arrowprops={'arrowstyle':'->', 'color':'#c05b1f', 'lw':1.6})
ax.annotate(r'$N^*$', (star,0), xytext=(0,-19), textcoords='offset points', ha='center')
ax.set(xlabel=r'Population $N$', ylabel=r'Growth rate $f(N)$',
title=r'Growth curve: $r=3$, $b=\delta=1$', xlim=(0,3))
ax = axes[1]
rp = np.linspace(delta, 3.5, 600)
ax.plot([0,delta], [0,0], color='#176da5', lw=2.3, label='Stable')
ax.plot([delta,3.5], [0,0], color='#c15b21', lw=2, ls='--', label='Unstable')
ax.plot(rp, np.sqrt((rp-delta)/(b*delta)), color='#176da5', lw=2.3)
ax.scatter([delta], [0], color='#263238', zorder=5, s=30)
ax.axvline(delta, color='#a8afb5', ls=':', lw=.9)
ax.text(1.8, 1.72, r'$N^*=\sqrt{(r-\delta)/(b\delta)}$', fontsize=10)
ax.set(xlabel=r'Recruitment parameter $r$', ylabel='Nonnegative steady population',
title=r'Bifurcation diagram: $b=\delta=1$', xlim=(0,3.5), ylim=(-.15,1.9), xticks=[0,1,2,3])
ax.legend(frameon=False, loc='upper left')
for ax in axes:
ax.spines[['top','right']].set_visible(False)
fig.savefig(Path.cwd() / (Path(__file__).stem + '.png'), dpi=150, facecolor='white', transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()