codex/past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2009/ii/paper-1-population-cobweb.py

past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2009/ii/paper-1-population-cobweb.py
"""Generate an original dimensionless population cobweb (Python 3.14).
Outputs only paper-1-population-cobweb.png in the caller's current directory.
Uses the root NumPy/Matplotlib dependencies and caller MPLCONFIGDIR.
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

r = 9.0
def population_map(n):
    return r*n/(1+n)**2
upper = r/4
lower = 4*r*r/(r+4)**2
values = [1.0]
for _ in range(10):
    values.append(population_map(values[-1]))
assert np.isclose(values[1], upper) and np.isclose(values[2], lower)
assert all(lower <= value <= upper for value in values[1:])
mesh = np.unique(np.r_[np.linspace(0, 2.6, 500), 1, lower, upper])
fig, ax = plt.subplots(figsize=(6.2, 4.6), layout='constrained', facecolor='white')
ax.set_facecolor('white')
ax.axvspan(lower, upper, color='#edf3e5', label='Invariant interval for t ≥ 2')
ax.plot(mesh, population_map(mesh), color='#245b96', linewidth=2, label='F(n) = 9n / (1+n)²')
ax.plot(mesh, mesh, color='#6d6d6d', linewidth=1.2, label='Diagonal')
current = values[0]
previous_height = 0.0
for new in values[1:]:
    ax.plot([current, current, new], [previous_height, new, new], color='#c14c37', linewidth=1.3)
    previous_height = new
    current = new
for n, label, offset in [(1.0, 'n₁ = 1', (-20, 12)), (upper, 'n₂ = M', (8, 12)), (lower, 'n₃ = L', (-48, 12))]:
    ax.scatter([n], [0], color='#c14c37', s=22, zorder=5, clip_on=False)
    ax.annotate(label, (n, 0), xytext=offset, textcoords='offset points', fontsize=9)
ax.scatter([2], [2], color='#222222', s=23, zorder=5)
ax.annotate('Stable fixed point', (2, 2), xytext=(-93, 16), textcoords='offset points', fontsize=9, arrowprops={'arrowstyle': '-', 'color': '#555555'})
ax.set(xlim=(0, 2.6), ylim=(0, 2.6), xlabel='Current scaled population n = bN', ylabel='Next scaled population', title='Population cobweb and post-initial invariant bounds')
ax.legend(loc='upper left', fontsize=8, framealpha=1)
ax.grid(alpha=0.18)
fig.savefig('paper-1-population-cobweb.png', dpi=150, facecolor='white', transparent=False)
plt.close(fig)