codexpast-exam-of-the-mathematical-methods-i-course-of-the-university-of-oxford2014-cyclic-conversion-dynamics.py
past-exam-of-the-mathematical-methods-i-course-of-the-university-of-oxford/2014-cyclic-conversion-dynamics.py
"""Original three-component linear conversion plots for June 2014 CP3 Q11.
Uniformization evaluates the matrix exponential without an eigenbasis, so it
also works at a defective repeated eigenvalue. Writes a matching PNG to CWD.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def trajectory(t, a, b, c):
m = np.array([[-a, 0, c], [a, -b, 0], [0, b, -c]], dtype=float)
rate = max(a, b, c)
transition = np.eye(3) + m / rate
weight = np.exp(-rate * t)
v = np.array([1.0, 0.0, 0.0])
result = v[:, None] * weight[None, :]
for j in range(1, 200):
weight = weight * (rate * t) / j
v = transition @ v
result += v[:, None] * weight[None, :]
return result
def main():
plt.rcParams.update({"font.size": 12, "figure.facecolor": "white", "axes.facecolor": "white"})
fig, axes = plt.subplots(1, 2, figsize=(10, 4), layout="constrained")
t = np.linspace(0, 8, 801)
for ax, rates, title in zip(axes, [(1, 1, 1), (1, 4, 1)],
["Equal rates: complex decay modes", "Rates (1, 4, 1): repeated-root transient"]):
a, b, c = rates
values = trajectory(t, a, b, c)
equilibrium = np.array([b*c, c*a, a*b]) / (a*b+b*c+c*a)
for j, (name, color) in enumerate(zip(["A", "B", "C"], ["#0072B2", "#D55E00", "#009E73"])):
ax.plot(t, values[j], label=name, color=color, linewidth=1.7)
ax.axhline(equilibrium[j], linestyle=":", color=color, linewidth=1.2, alpha=.8)
ax.plot(t, values.sum(axis=0), color="0.3", linestyle="--", linewidth=1, label="total")
ax.set(xlabel="time in reciprocal rate units", ylabel="fraction of conserved total",
ylim=(-0.02, 1.06), xlim=(0, 8), title=title)
ax.legend(loc="upper right", fontsize=10)
ax.grid(alpha=.2)
fig.savefig(Path.cwd()/Path(__file__).with_suffix(".png").name, dpi=120, facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()