codex/catalysis-barriers-and-net-free-energy.py

catalysis-barriers-and-net-free-energy.py
"""Illustrative free-energy paths, not experimental data.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media: MPLBACKEND=Agg python ../catalysis-barriers-and-net-free-energy.py
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def profile(x, peak):
    """Two cosine ramps with flat endpoints and a prescribed maximum."""
    left = peak * (1 - np.cos(np.pi * np.clip((x - 0.10) / 0.40, 0, 1))) / 2
    right = peak + (-3 - peak) * (
        1 - np.cos(np.pi * np.clip((x - 0.50) / 0.40, 0, 1))
    ) / 2
    return np.where(x <= 0.50, left, right)


def main():
    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
    fig, ax = plt.subplots(figsize=(8.5, 5), dpi=120)
    fig.subplots_adjust(left=0.11, right=0.96, bottom=0.26, top=0.83)
    x = np.linspace(0, 1, 1001)
    for peak, color, label in [(12, "#b64b27", "Uncatalysed"), (7, "#176b9b", "Catalysed")]:
        y = profile(x, peak)
        assert y[0] == 0 and y[-1] == -3 and y.max() == peak
        ax.plot(x, y, lw=3, color=color, label=label)
        ax.plot([0.50], [peak], "o", color=color)
        ax.text(0.52, peak + 0.25, f"Chosen maximum: {peak}", color=color)
    ax.hlines([0, -3], 0, 1.05, color="#999999", linestyle=":", lw=1)
    ax.text(0.03, -1.2, "A + C", ha="left")
    ax.text(0.97, -4.25, "B + C", ha="right")
    ax.annotate("", xy=(1.045, -3), xytext=(1.045, 0),
                arrowprops={"arrowstyle": "<->", "color": "#333333", "lw": 1.5})
    ax.text(1.07, -1.5, r"$\Delta_r G^\circ=-3$", va="center", fontsize=11)
    ax.set(xlim=(-0.015, 1.27), ylim=(-5, 14),
           ylabel=r"Standard molar Gibbs free energy / kJ mol$^{-1}$",
           xlabel="Reaction coordinate (not time)")
    ax.set_xticks([])
    ax.spines[["top", "right"]].set_visible(False)
    ax.legend(loc="upper left", frameon=False, fontsize=11)
    fig.suptitle("Illustrative barriers: different paths, identical endpoints", fontsize=15)
    fig.text(0.5, 0.90, "C is included at both endpoints and regenerated; chosen values, not measurements",
             ha="center", fontsize=10)
    fig.text(0.5, 0.12,
             "Forward barriers: 12 → 7       Reverse barriers: 15 → 10  (kJ/mol)",
             ha="center", fontsize=12)
    fig.text(0.5, 0.06, "Both climbs fall by 5; the net free-energy change stays −3 kJ/mol.",
             ha="center", fontsize=11)
    fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()