codex/past-exam-of-the-mathematical-methods-i-course-of-the-university-of-oxford/2014-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()