codex/solubility-product-balance-intersection.py

solubility-product-balance-intersection.py
"""Stipulated AB inventory lines intersect a conditional saturation hyperbola.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; output is this script's basename with a .png extension.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

K = 1e-4  # M^2; chosen model, not a measured material constant.
c0 = 0.03  # M
s_common = 2 * K / (np.hypot(c0, 2 * np.sqrt(K)) + c0)
s_water = np.sqrt(K)
plt.rcParams.update({"font.size": 12, "axes.spines.top": False,
                     "axes.spines.right": False, "figure.facecolor": "white",
                     "axes.facecolor": "white"})
fig, ax = plt.subplots(figsize=(9, 6), dpi=100, layout="constrained")
x = np.linspace(0.0005, 0.018, 1200)
ax.plot(x, K / x, color="#293f78", lw=2.8,
        label=r"Saturation: $c_Ac_B=10^{-4}\,\mathrm{M}^2$")
x_line = np.linspace(0, 0.018, 300)
ax.plot(x_line, x_line, color="#16856e", lw=2.3,
        label=r"Pure water balance: $c_B=c_A$")
ax.plot(x_line, x_line + c0, color="#c55b24", lw=2.3,
        label=r"Common-ion balance: $c_B=c_A+0.030\,\mathrm{M}$")
ax.plot([s_common, s_common], [0, s_common + c0], '--',
        color="#6f6f6f", lw=1.2)
ax.plot([0, s_common], [s_common + c0, s_common + c0], '--',
        color="#6f6f6f", lw=1.2)
ax.scatter([s_water, s_common], [s_water, s_common + c0], s=65,
           c=["#16856e", "#c55b24"], zorder=5, edgecolors="white")
ax.annotate("Pure water\n(0.010, 0.010) M", xy=(s_water, s_water),
            xytext=(0.0118, 0.0145), fontsize=11, color="#126b59",
            arrowprops={"arrowstyle": "->", "color": "#16856e"})
ax.annotate("Common-ion equilibrium\n(0.003028, 0.033028) M",
            xy=(s_common, s_common + c0), xytext=(0.007, 0.025),
            fontsize=11, color="#a14318",
            arrowprops={"arrowstyle": "->", "color": "#c55b24"})
ax.text(0.0003, 0.0347, r"$c_B=0.033028\,\mathrm{M}$", fontsize=10)
ax.text(0.0037, 0.0012, r"$s=0.003028\,\mathrm{M}$", fontsize=10)
ax.set(xlim=(0, 0.018), ylim=(0, 0.060),
       xlabel=r"A cation concentration, $c_A$ (M)",
       ylabel=r"B anion concentration, $c_B$ (M)",
       title="One saturation curve, two inventory constraints")
ax.set_xticks(np.arange(0, 0.0181, 0.003))
ax.grid(alpha=0.18)
ax.legend(loc="upper right", fontsize=10, framealpha=1)
fig.savefig(Path.cwd() / f"{Path(__file__).stem}.png", facecolor="white",
            transparent=False)
plt.close(fig)