codex/buffer-ph-versus-species-ratio.py

buffer-ph-versus-species-ratio.py
"""Illustrate a toy buffer ratio; write basename.png in the working directory.

Tested with Python 3.14.4, NumPy 2.3.5, and Matplotlib 3.10.7.
pKa = 5 is stipulated, not measured material data. Concentrations approximate
activities; the plotted ratios do not guarantee useful buffer capacity.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def main():
    pka = 5.0
    ratios = np.array([0.1, 1.0, 10.0])
    ph = pka + np.log10(ratios)
    fig, axes = plt.subplots(1, 2, figsize=(9.6, 4.8), sharey=True)
    fig.patch.set_facecolor("white")
    fig.subplots_adjust(left=0.075, right=0.97, bottom=0.23, top=0.76, wspace=0.16)
    for ax, log_scale in zip(axes, (False, True)):
        r = np.geomspace(0.08, 12, 600)
        ax.plot(r, pka + np.log10(r), color="#236d99", lw=2.5)
        ax.scatter(ratios, ph, color="#b6531f", s=42, zorder=5)
        ax.set_xlim(0.08 if log_scale else 0, 12)
        ax.set_ylim(3.7, 6.3)
        ax.set_yticks([4, 5, 6])
        ax.set_xlabel(r"Base / acid concentration ratio, $[A^-]/[HA]$", fontsize=10)
        ax.set_title("Logarithmic ratio axis" if log_scale else "Linear ratio axis", fontsize=12)
        if log_scale:
            ax.set_xscale("log")
            ax.set_xticks(ratios, labels=["0.1", "1", "10"])
            for x, y, label in zip(ratios, ph, ("0.1 → pH 4", "1 → pH 5", "10 → pH 6")):
                ax.annotate(label, (x, y), xytext=(-8 if x == 10 else 8, -24 if x == 10 else 12),
                            textcoords="offset points", ha="right" if x == 10 else "left",
                            fontsize=10, color="#8b421b",
                            bbox={"facecolor": "white", "edgecolor": "none", "pad": 1})
        ax.grid(alpha=0.2)
        ax.spines[["top", "right"]].set_visible(False)
    axes[0].set_ylabel("Calculated pH", fontsize=11)
    fig.suptitle(r"Toy buffer: $\mathrm{pH}\approx5+\log_{10}([A^-]/[HA])$", fontsize=15, y=0.97)
    fig.text(0.5, 0.84, "Stipulated pKa = 5; ideal concentration approximation; not material data", ha="center", fontsize=11)
    fig.text(0.5, 0.10, "A tenfold increase in the ratio adds one to the calculated pH.", ha="center", fontsize=11)
    fig.text(0.5, 0.045, "The ratio sets pH; available amounts limit the buffering action.", ha="center", fontsize=11, color="#444444")
    output = Path.cwd() / (Path(__file__).stem + ".png")
    fig.savefig(output, dpi=120, facecolor="white", transparent=False)
    plt.close(fig)
    print(f"Wrote {output.name}; ratios {ratios.tolist()} give pH {ph.tolist()}")


if __name__ == "__main__":
    main()