codexsolvation-enthalpy-entropy-balance.py
solvation-enthalpy-entropy-balance.py
"""Synthetic dissolution bookkeeping, not measured salt or solvent data.
Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; the matching opaque PNG is written to the current directory.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
TEMPERATURE = 300.0 # K, stipulated model temperature
SEPARATION = 100.0 # kJ per mol of hypothetical 1:1 salt
REORGANIZATION = 20.0
INTERACTIONS = np.array([-150.0, -90.0])
ENTROPIES = np.array([-0.050, 0.150]) # kJ mol^-1 K^-1, total process
def main():
enthalpies = SEPARATION + REORGANIZATION + INTERACTIONS
entropy_terms = -TEMPERATURE * ENTROPIES
free_energies = enthalpies + entropy_terms
assert np.allclose(enthalpies, [-30, 30])
assert np.allclose(free_energies, [-15, -15])
plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
fig, axes = plt.subplots(1, 2, figsize=(11.6, 6.2), facecolor="white")
fig.subplots_adjust(left=0.08, right=0.97, bottom=0.25, top=0.77, wspace=0.36)
ax = axes[0]
x = np.arange(4)
for interaction, name, color, offset in zip(INTERACTIONS, ["A", "B"], ["#246d9d", "#b15c22"], [-0.035, 0.035]):
levels = np.array([0, SEPARATION, SEPARATION + REORGANIZATION, SEPARATION + REORGANIZATION + interaction])
ax.plot(x + offset, levels, marker="o", color=color, lw=2.2, label=f"Solvent {name}")
ax.text(3.10, levels[-1], f"{levels[-1]:+.0f}", va="center", color=color, fontsize=13)
ax.axhline(0, color="#88919b", lw=1)
ax.set_ylim(-50, 155)
ax.set_xlim(-0.25, 3.6)
ax.set_xticks(x, ["Initial", "Separate\nsolute", "Reorganize\nsolvent", "Establish\ninteractions"], fontsize=10)
ax.set_ylabel(r"Cumulative enthalpy / kJ mol$^{-1}$")
ax.set_title("Enthalpy accounting path", fontsize=14, pad=15)
ax.text(0.50, 61, "+100", ha="center", color="#4d5968")
ax.text(1.50, 130, "+20", ha="center", color="#4d5968")
ax.text(2.48, 45, "A: −150\nB: −90", ha="center", color="#4d5968", fontsize=11)
ax.legend(loc="upper left", frameon=False, fontsize=11)
ax = axes[1]
width = 0.33
x = np.arange(3)
for i, (name, color) in enumerate(zip(["A", "B"], ["#246d9d", "#b15c22"])):
values = [enthalpies[i], entropy_terms[i], free_energies[i]]
bars = ax.bar(x + (i - 0.5) * width, values, width=width, color=color, label=f"Solvent {name}")
for bar, value in zip(bars, values):
ax.text(bar.get_x() + width / 2, value + (3 if value >= 0 else -3), f"{value:+.0f}", ha="center", va="bottom" if value >= 0 else "top", fontsize=12)
ax.axhline(0, color="#88919b", lw=1)
ax.set_ylim(-65, 60)
ax.set_xticks(x, [r"$\Delta H$", r"$-T\Delta S$", r"$\Delta G$"], fontsize=15)
ax.set_ylabel(r"Molar contribution / kJ mol$^{-1}$")
ax.set_title(r"$\Delta G=\Delta H-T\Delta S$ at 300 K", fontsize=14, pad=15)
ax.legend(loc="upper right", frameon=False, fontsize=11)
for ax in axes:
ax.spines[["top", "right"]].set_visible(False)
ax.grid(axis="y", color="#edf0f3", zorder=0)
ax.set_axisbelow(True)
fig.suptitle("Dissolution balances solvent interactions and total entropy", y=0.96, fontsize=17)
fig.text(0.5, 0.86, "Synthetic example: one mole of a hypothetical 1:1 salt in either of two unspecified solvents", ha="center", fontsize=12)
fig.text(0.5, 0.14, r"A: $\Delta S=-50$ J mol$^{-1}$ K$^{-1}$ B: $\Delta S=+150$ J mol$^{-1}$ K$^{-1}$", ha="center", fontsize=12)
fig.text(0.5, 0.08, "Both stipulated processes have ΔG = −15 kJ/mol, despite opposite signs of ΔH.", ha="center", fontsize=12)
fig.text(0.5, 0.035, "Invented, internally consistent values. The accounting path is not a time sequence or activation barrier.", ha="center", fontsize=10.5)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=110, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()