codexglycine-protonation-speciation-model.py
glycine-protonation-speciation-model.py
"""Draw the stipulated ideal three-state glycine model (Python 3.14).
Run from _media; dependencies are recorded in the root pyproject.toml.
The fractions are calculated, not experimental measurements. Activity
coefficients are one; the minor uncharged amino/carboxyl form is omitted.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
PKA1, PKA2 = 2.35, 9.78
PI = (PKA1 + PKA2) / 2
COLORS = ("#1764a1", "#21835b", "#c25823")
def fractions(ph):
h = 10.0 ** (-np.asarray(ph))
k1, k2 = 10.0**-PKA1, 10.0**-PKA2
denominator = h * h + k1 * h + k1 * k2
return np.array([h * h, k1 * h, np.full_like(h, k1 * k2)]) / denominator
def main():
plt.rcParams.update({"font.size": 12, "axes.spines.top": False,
"axes.spines.right": False, "savefig.facecolor": "white"})
fig = plt.figure(figsize=(11, 7.6), dpi=100, facecolor="white")
fig.suptitle("Glycine protonation and speciation — ideal model", fontsize=19, y=0.97)
fig.text(0.5, 0.919, "Stipulated pKa₁ = 2.35, pKa₂ = 9.78; calculated pI = 6.065",
ha="center", color="#333333")
scheme = fig.add_axes((0.035, 0.67, 0.93, 0.21))
scheme.axis("off")
structures = (r"$\mathrm{H_3N^{+}{-}CH_2{-}COOH}$",
r"$\mathrm{H_3N^{+}{-}CH_2{-}COO^{-}}$",
r"$\mathrm{H_2N{-}CH_2{-}COO^{-}}$")
for x, formula, name, color in zip((0.16, 0.5, 0.84), structures,
("G⁺: cation", "G⁰: zwitterion", "G⁻: anion"), COLORS):
scheme.text(x, 0.76, formula, ha="center", fontsize=17, color=color)
scheme.text(x, 0.51, name, ha="center", fontsize=14, color=color)
scheme.text(0.16, 0.29, "net charge +1", ha="center")
scheme.text(0.5, 0.29, "net charge 0", ha="center")
scheme.text(0.84, 0.29, "net charge −1", ha="center")
for a, b, label in ((0.25, 0.41, "lose H⁺; pKa₁"), (0.59, 0.75, "lose H⁺; pKa₂")):
scheme.annotate("", (b, 0.03), (a, 0.03), arrowprops={"arrowstyle": "<->", "color": "#555555"})
scheme.text((a + b) / 2, 0.13, label, ha="center", fontsize=11)
ax = fig.add_axes((0.09, 0.14, 0.87, 0.45))
ph = np.linspace(0, 14, 1401)
for fraction, label, color in zip(fractions(ph), ("G⁺", "G⁰ (zwitterion)", "G⁻"), COLORS):
ax.plot(ph, fraction, color=color, lw=2.8, label=label)
for x, label in ((PKA1, "pKa₁"), (PI, "pI"), (PKA2, "pKa₂")):
ax.axvline(x, color="#777777", lw=1.1, ls=":")
ax.text(x, 0.88, label, ha="center", bbox={"facecolor": "white", "edgecolor": "none", "pad": 3})
ax.set(xlim=(0, 14), ylim=(0, 1.04), xlabel="pH", ylabel="Calculated fraction of total glycine")
ax.set_xticks(np.arange(0, 15, 2))
ax.grid(alpha=0.17)
ax.legend(loc="upper center", bbox_to_anchor=(0.5, 1.16), ncols=3, frameon=False)
fig.text(0.5, 0.04, "Charge units: elementary charge e. At pI: 99.9615% G⁰; 0.0193% each G⁺ and G⁻.",
ha="center", fontsize=11)
fig.savefig(Path(__file__).with_suffix(".png").name, facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()