a, b, c, d = 0.6, 0.5, 0.3, 0.4
xe, ye = c/d, a/b # non-trivial equilibrium
def f_lv(x, y): return a*x - b*x*y
def g_lv(x, y): return -c*y + d*x*y
fig, axes = plt.subplots(1, 2, figsize=(11, 5))
# Phase portrait
lim_x, lim_y = 3.0, 3.0
x_g, y_g = np.meshgrid(np.linspace(0.02, lim_x, 22),
np.linspace(0.02, lim_y, 22))
dx = f_lv(x_g, y_g); dy = g_lv(x_g, y_g)
nrm = np.sqrt(dx**2 + dy**2 + 1e-10)
axes[0].quiver(x_g, y_g, dx/nrm, dy/nrm, alpha=0.25, color='gray', scale=28)
# Nullclines
x_nc = np.linspace(0.01, lim_x, 300)
axes[0].axvline(xe, color='crimson', ls='--', lw=1.8,
label=rf"$y$-nullcline: $x={xe:.2f}$")
axes[0].axhline(ye, color='steelblue', ls='--', lw=1.8,
label=rf"$x$-nullcline: $y={ye:.2f}$")
axes[0].axhline(0, color='gray', ls='--', lw=0.8)
axes[0].axvline(0, color='gray', ls='--', lw=0.8)
# Closed orbits
colors_orb = plt.cm.viridis(np.linspace(0.15, 0.85, 5))
for r_frac, color in zip([0.15, 0.35, 0.55, 0.75, 0.95], colors_orb):
x0 = xe + r_frac * xe
sol = solve_ivp(lambda t, z: [f_lv(z[0], z[1]), g_lv(z[0], z[1])],
(0, 60), [x0, ye], dense_output=True,
max_step=0.05, rtol=1e-8, atol=1e-10)
xy = sol.y
mask = (xy[0] > 0) & (xy[1] > 0) & (xy[0] < lim_x) & (xy[1] < lim_y)
axes[0].plot(xy[0, mask], xy[1, mask], color=color, lw=1.5)
axes[0].plot(xe, ye, 'o', color='steelblue', markersize=9, zorder=6,
label=f'Equil. $({xe:.2f},{ye:.2f})$ — center')
axes[0].plot(0, 0, 's', color='crimson', markersize=8, zorder=6,
label='Origin — saddle')
axes[0].set_xlim(0, lim_x); axes[0].set_ylim(0, lim_y)
axes[0].set_xlabel('$x$ (prey)'); axes[0].set_ylabel('$y$ (predator)')
axes[0].set_title('Phase portrait')
axes[0].legend(fontsize=8)
# Time series
x0_ts, y0_ts = xe + 0.5*xe, ye
sol_ts = solve_ivp(lambda t, z: [f_lv(z[0], z[1]), g_lv(z[0], z[1])],
(0, 40), [x0_ts, y0_ts], dense_output=True,
max_step=0.05, rtol=1e-8, atol=1e-10)
t_plot = np.linspace(0, 40, 800)
axes[1].plot(t_plot, sol_ts.sol(t_plot)[0], color='steelblue', lw=2.5,
label='$x(t)$ prey')
axes[1].plot(t_plot, sol_ts.sol(t_plot)[1], color='crimson', lw=2.5,
label='$y(t)$ predator')
axes[1].axhline(xe, color='steelblue', ls=':', lw=1, alpha=0.7)
axes[1].axhline(ye, color='crimson', ls=':', lw=1, alpha=0.7)
axes[1].set_xlabel('$t$'); axes[1].set_ylabel('Population')
axes[1].set_title('Time series')
axes[1].legend(fontsize=9)
plt.suptitle(f'Lotka–Volterra: $a={a}$, $b={b}$, $c={c}$, $d={d}$', fontsize=12)
plt.tight_layout()
plt.show()