import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
mpl_rc = {'figure.dpi': 150,
'axes.spines.top': False,
'axes.spines.right': False}
for k, v in mpl_rc.items():
plt.rcParams[k] = v
def closed_form(t, p0, A, B):
"""Exact solution (2.4); returns NaN where denominator <= 0."""
if abs(A) < 1e-12: # A = 0 case: p' = -B p^2
return p0 / (1.0 + B * p0 * t)
ratio = A / B
denom = 1.0 + (ratio / p0 - 1.0) * np.exp(-A * t)
return np.where(np.abs(denom) > 1e-8, ratio / denom, np.nan)
def ode_rhs(t, p, ri, ro, q):
return [ri * p[0] * (ro * (1 - q - p[0]) - q)]
ri = 1.0
scenarios = [
dict(ro=0.8, q=0.3, label_suffix="$A>0$: control emerges", cmap='viridis'),
dict(ro=0.5, q=1.0/3.0, label_suffix="$A=0$: borderline", cmap='plasma'),
dict(ro=0.2, q=0.3, label_suffix="$A<0$: control fails", cmap='coolwarm'),
]
fig, axes = plt.subplots(3, 1, figsize=(8, 10))
t_plot = np.linspace(0, 12, 500)
p0_vals = [0.05, 0.2, 0.5, 0.8]
for ax, sc in zip(axes, scenarios):
ro, q = sc['ro'], sc['q']
A = ri * (ro * (1 - q) - q)
B = ri * ro
colors = plt.get_cmap(sc['cmap'])(np.linspace(0.2, 0.85, len(p0_vals)))
for p0, color in zip(p0_vals, colors):
# Closed-form solution
p_cf = closed_form(t_plot, p0, A, B)
ax.plot(t_plot, p_cf, color=color, lw=2.2, label=f'$p_0={p0}$')
# Numerical verification (dots)
sol = solve_ivp(ode_rhs, (0, 12), [p0], args=(ri, ro, q),
dense_output=True, max_step=0.05)
t_dots = np.linspace(0, 12, 18)
ax.plot(t_dots, sol.sol(t_dots)[0], 'o', color=color, markersize=4)
# Mark equilibrium p* when A > 0
if A > 1e-10:
p_star = A / B
ax.axhline(p_star, color='seagreen', ls='--', lw=1.8,
label=f'$p^* = A/B \\approx {p_star:.2f}$')
ax.axhline(0, color='crimson', ls='--', lw=1.2, label='$p=0$ (unstable)')
ax.set_ylim(-0.05, 1.05)
ax.set_xlabel('$t$')
ax.set_ylabel('$p(t)$')
ax.set_title(
rf"$r_i={ri},\; r_o={ro},\; q={round(q,3)}$ — {sc['label_suffix']} "
rf"($A={A:.3f}$, $B={B:.3f}$)"
)
ax.legend(fontsize=8, ncol=3)
plt.tight_layout()
plt.show()