codexheat-exchange-entropy.py
heat-exchange-entropy.py
"""Plot direct thermal equilibration of two equal finite heat capacities.
Run from _media with MPLBACKEND=Agg MPLCONFIGDIR=/tmp.
Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
The PNG is written to the current working directory.
"""
import matplotlib.pyplot as plt
import numpy as np
def main():
capacity, hot_start, cold_start = 100.0, 600.0, 300.0
heat = np.linspace(0, capacity * (hot_start - cold_start) / 2, 401)
hot = hot_start - heat / capacity
cold = cold_start + heat / capacity
entropy_hot = capacity * np.log(hot / hot_start)
entropy_cold = capacity * np.log(cold / cold_start)
entropy_total = entropy_hot + entropy_cold
assert np.allclose(capacity * (hot + cold), capacity * (hot_start + cold_start))
assert np.all(np.diff(entropy_total) >= 0)
assert np.isclose(hot[-1], cold[-1])
assert np.isclose(entropy_total[-1], capacity * np.log(9 / 8))
plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
"axes.spines.right": False, "figure.facecolor": "white",
"axes.facecolor": "white", "savefig.facecolor": "white"})
fig, (temperature_ax, entropy_ax) = plt.subplots(1, 2, figsize=(10.5, 4.6), dpi=120)
fig.suptitle("Direct contact: energy conserved, total entropy increases", fontsize=15)
fig.subplots_adjust(left=0.07, right=0.98, bottom=0.22, top=0.78, wspace=0.30)
heat_kj = heat / 1000
hot_color, cold_color, total_color = "#b44324", "#226a9e", "#247657"
temperature_ax.plot(heat_kj, hot, color=hot_color, lw=2.5, label="Initially hot body")
temperature_ax.plot(heat_kj, cold, color=cold_color, lw=2.5, label="Initially cold body")
temperature_ax.plot(15, 450, "o", color="#39424e", clip_on=False)
temperature_ax.annotate("450 K", (15, 450), xytext=(-60, 9), textcoords="offset points")
temperature_ax.set_ylabel("Temperature (K)")
temperature_ax.set_ylim(275, 650)
temperature_ax.set_yticks([300, 450, 600])
temperature_ax.set_title(r"Each body's heat capacity: $C=100$ J/K")
temperature_ax.legend(loc="upper center", frameon=False, fontsize=10)
entropy_ax.axhline(0, color="#7b818a", lw=0.8)
entropy_ax.plot(heat_kj, entropy_cold, color=cold_color, lw=2, label="Cold body")
entropy_ax.plot(heat_kj, entropy_hot, color=hot_color, lw=2, label="Hot body")
entropy_ax.plot(heat_kj, entropy_total, color=total_color, lw=3, label="Total")
entropy_ax.set_title("Entropy measured from the initial state")
entropy_ax.set_ylabel(r"Entropy change (J K$^{-1}$)")
entropy_ax.set_ylim(-38, 48)
for values, color, offset in [(entropy_cold, cold_color, (7, -3)),
(entropy_hot, hot_color, (7, -3)),
(entropy_total, total_color, (7, -3))]:
entropy_ax.annotate(f"{values[-1]:+.2f}", (15, values[-1]),
xytext=offset, textcoords="offset points", color=color, fontsize=10)
entropy_ax.set_xlim(0, 18.5)
entropy_ax.legend(loc="lower left", frameon=False, fontsize=10)
for ax in (temperature_ax, entropy_ax):
ax.set_xlabel("Heat transferred from hot to cold (kJ)", labelpad=8)
ax.set_xticks([0, 5, 10, 15])
ax.grid(alpha=0.18)
temperature_ax.set_xlim(0, 15)
fig.text(0.5, 0.06, "Isolated pair; rigid bodies; no work extracted; "
"uniform temperature within each body", ha="center", fontsize=10)
fig.savefig("heat-exchange-entropy.png", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()