codex/second-order-concentration-and-reciprocal-time.py

second-order-concentration-and-reciprocal-time.py
"""Original second-order disappearance models; no measured data.

Tested with Python 3.14, Matplotlib 3.10.7 and NumPy 2.3.5.
Run from _media to write the matching opaque PNG into the current directory.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    k_obs = 0.50  # L mol^-1 s^-1, species disappearance convention
    time = np.linspace(0.0, 400.0, 801)
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False, "figure.facecolor": "white"})
    fig, axes = plt.subplots(1, 2, figsize=(10.0, 4.5), layout="constrained")
    colors = ("#2375a4", "#ca6230", "#33845b")
    for initial, color in zip((0.010, 0.020, 0.040), colors):
        concentration = initial / (1.0 + k_obs * initial * time)
        half_time = 1.0 / (k_obs * initial)
        label = rf"$[A]_0={initial:.3f}$ mol L$^{{-1}}$"
        axes[0].plot(time, concentration, color=color, lw=2.3, label=label)
        axes[0].scatter([half_time], [initial / 2], color=color, s=40, zorder=3)
        axes[0].annotate(f"{half_time:.0f} s", (half_time, initial / 2),
                         xytext=(9, 5), textcoords="offset points", color=color)
        axes[1].plot(time, 1.0 / concentration, color=color, lw=2.3)
        axes[1].scatter([half_time], [2.0 / initial], color=color, s=40, zorder=3)
    axes[0].set(title="Remaining concentration", xlabel="Time (s)",
                ylabel=r"$[A]$ (mol L$^{-1}$)", xlim=(0, 400), ylim=(0, 0.043))
    axes[0].legend(loc="upper right", fontsize=10, frameon=False)
    axes[1].set(title="Reciprocal concentration", xlabel="Time (s)",
                ylabel=r"$1/[A]$ (L mol$^{-1}$)", xlim=(0, 400), ylim=(0, 325))
    axes[1].text(0.04, 0.95, "Shared slope: 0.50 L mol⁻¹ s⁻¹\nIntercept: 1/[A]₀",
                 transform=axes[1].transAxes, va="top", fontsize=10,
                 bbox={"facecolor": "white", "edgecolor": "none", "alpha": 1})
    for ax in axes:
        ax.grid(alpha=0.18)
    fig.suptitle(r"Second-order disappearance: $-d[A]/dt=k_{\rm obs}[A]^2$"
                 "\nChosen model parameters; dots mark first half-lives, not measurements", fontsize=12)
    fig.savefig(Path(__file__).stem + ".png", dpi=130, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()