codex/monoprotic-buffer-capacity-concentration.py

monoprotic-buffer-capacity-concentration.py
"""Ideal monoprotic capacity; run from _media with Python 3.14.

Dependencies: matplotlib 3.10.7, numpy 2.3.5 (root pyproject.toml).
Stipulated constants describe a model, not measured material data.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def capacity(ph, concentration, ka=1e-5, kw=1e-14):
    """Return pair and free-ion terms in mol/L per pH (c° = 1 mol/L)."""
    h = 10.0 ** (-np.asarray(ph))
    return np.log(10) * concentration * ka * h / (ka + h) ** 2, np.log(10) * (h + kw / h)


def main():
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False, "axes.spines.right": False})
    fig, axes = plt.subplots(1, 2, figsize=(10, 4.8), layout="constrained", facecolor="white")
    ph = np.linspace(2, 8, 601)
    for concentration, color in [(0.1, "#2466a8"), (0.01, "#d45d18")]:
        pair, _ = capacity(ph, concentration)
        axes[0].plot(ph, pair * 1000, lw=2.5, color=color, label=f"C = {concentration:.3f} mol/L")
        axes[0].scatter([5], [np.log(10) * concentration / 4 * 1000], color=color, zorder=3)
    axes[0].axvline(5, color="0.55", ls=":", lw=1)
    axes[0].set(xlabel="pH", ylabel="Pair capacity (mmol/L per pH)", title="Same pKa, tenfold concentration difference", xlim=(2, 8), ylim=(0, 65))
    axes[0].text(5.13, 60, "pH = pKa = 5", fontsize=10)
    axes[0].legend(loc="upper left", frameon=False, fontsize=10)
    ph = np.linspace(2, 12, 1001)
    pair, water = capacity(ph, 0.01)
    axes[1].semilogy(ph, pair, color="#d45d18", lw=2, label="Acid/base pair")
    axes[1].semilogy(ph, water, color="#248471", lw=2, label="Free H⁺ and OH⁻")
    axes[1].semilogy(ph, pair + water, color="#333333", lw=2.5, label="Total")
    axes[1].set(xlabel="pH", ylabel="Capacity (mol/L per pH)", title="Water terms matter away from the pair peak", xlim=(2, 12), ylim=(1e-9, 0.05))
    axes[1].legend(loc="lower left", frameon=False, fontsize=10)
    for ax in axes:
        ax.grid(alpha=0.18)
        ax.set_facecolor("white")
    fig.suptitle("Calculated ideal monoprotic buffer: pKa = 5, Kw = 10⁻¹⁴", fontsize=13)
    fig.savefig(Path(__file__).stem + ".png", dpi=100, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()