codexcalorimetry-mixing-temperature-energy-balance.py
calorimetry-mixing-temperature-energy-balance.py
"""Original synthetic water-mixing balance; Python 3.14, matplotlib 3.10.7, numpy 2.3.5.
Run from _media; write the matching PNG basename into the current directory.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import FancyBboxPatch
def main():
cw, mh, mc = 4180.0, 0.100, 0.200
th, tc, ccal = 80.0, 20.0, 100.0
ch, cc = mh * cw, mc * cw
tf = (ch * th + (cc + ccal) * tc) / (ch + cc + ccal)
qh, qc, qcal = ch * (th - tf), cc * (tf - tc), ccal * (tf - tc)
assert np.isclose(qh, qc + qcal, atol=1e-9)
capacities = np.linspace(0, 300, 301)
finals = (ch * th + (cc + capacities) * tc) / (ch + cc + capacities)
assert np.all(np.diff(finals) < 0)
plt.rcParams.update({"font.size": 12, "axes.titlesize": 14, "axes.labelsize": 12})
fig, (flow, graph) = plt.subplots(1, 2, figsize=(11.8, 5.7),
gridspec_kw={"width_ratios": [1.18, 1]}, layout="constrained")
fig.patch.set_facecolor("white")
fig.suptitle("Water mixing: an insulated vessel still absorbs energy", fontsize=17)
flow.set(xlim=(0, 10), ylim=(0, 10))
flow.axis("off")
def box(x, y, width, height, text, color):
flow.add_patch(FancyBboxPatch((x, y), width, height, boxstyle="round,pad=0.12",
facecolor=color, edgecolor="#40464d", linewidth=1.2))
flow.text(x + width / 2, y + height / 2, text, ha="center", va="center")
box(0.25, 3.25, 3.55, 3.4,
f"Hot water\n0.100 kg at 80 °C\n\nReleases {qh / 1000:.3f} kJ", "#fde1d9")
box(6.4, 6.3, 3.3, 2.5, "Cold water\n0.200 kg at 20 °C", "#dfeff9")
box(6.4, 1.6, 3.3, 2.5, "Vessel + immersed parts\n100 J/K at 20 °C", "#eee9f7")
for end, value in [((6.2, 7.1), qc), ((6.2, 3.0), qcal)]:
flow.annotate("", xy=end, xytext=(3.98, 5.1),
arrowprops={"arrowstyle": "->", "lw": 2.4, "color": "#b95525"})
flow.text(5.0, (end[1] + 5.1) / 2 + (0.35 if end[1] > 5 else -0.5),
f"{value / 1000:.3f} kJ", ha="center", color="#913717",
bbox={"facecolor": "white", "edgecolor": "none", "pad": 2})
flow.text(5, 9.45, "Heat transfers inside the combined boundary", ha="center", fontsize=13)
flow.text(5, 0.5, f"All finish at {tf:.3f} °C • No room losses", ha="center", fontsize=13)
flow.text(5, -0.05, "Synthetic example; water specific heat = 4180 J/(kg K)",
ha="center", fontsize=10.5)
graph.plot(capacities, finals, color="#1e6b8b", lw=2.5)
graph.scatter([ccal], [tf], color="#b95525", s=65, zorder=4)
graph.axvline(ccal, color="#b95525", ls=":", lw=1.3)
graph.annotate(f"100 J/K → {tf:.3f} °C", xy=(ccal, tf), xytext=(130, 39.3),
arrowprops={"arrowstyle": "->", "color": "#555555"}, fontsize=12)
graph.scatter([0], [40], color="#1e6b8b", s=40)
graph.text(12, 40.1, "Ignoring vessel: 40 °C", fontsize=11)
graph.set(title="Separate runs with different vessel capacities",
xlabel="Vessel heat capacity (J/K)", ylabel="Final common temperature (°C)",
xlim=(0, 300), ylim=(35.5, 40.8))
graph.grid(alpha=0.22)
fig.savefig(Path(__file__).with_suffix(".png").name, dpi=100,
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()