codextwo-level-thermal-response.py
two-level-thermal-response.py
"""Plot the exact thermal response of one nondegenerate two-level unit.
Build with: make _media/two-level-thermal-response.png
Writes the same-basename, opaque white PNG to the current working directory.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def response(theta):
"""Return excited probability, C/k_B and S/k_B for theta=k_B T/Delta."""
x = 1.0 / np.asarray(theta)
weight = np.exp(-x)
p = weight / (1.0 + weight)
heat = x * x * p * (1.0 - p)
entropy = np.log1p(weight) + x * p
return p, heat, entropy
def main():
theta = np.linspace(0.025, 3.0, 1500)
p, heat, entropy = response(theta)
x_example = np.log(3.0)
theta_example = 1.0 / x_example
p0, c0, s0 = response(theta_example)
assert np.isclose(p0, 0.25)
assert np.isclose(c0, 3.0 * x_example**2 / 16.0)
assert np.isclose(s0, -0.75 * np.log(0.75) - 0.25 * np.log(0.25))
# At the maximum, x*tanh(x/2)=2. Bisection uses only NumPy/stdlib.
low, high = 2.0, 3.0
for _ in range(60):
mid = (low + high) / 2.0
if mid * np.tanh(mid / 2.0) < 2.0:
low = mid
else:
high = mid
theta_peak = 2.0 / (low + high)
_, heat_peak, _ = response(theta_peak)
assert np.isclose(theta_peak, 0.4167782798)
assert np.isclose(heat_peak, 0.4392288399)
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
"axes.facecolor": "white", "savefig.facecolor": "white",
"axes.spines.top": False, "axes.spines.right": False})
fig, axes = plt.subplots(1, 3, figsize=(10.0, 4.4), dpi=120)
fig.subplots_adjust(left=0.07, right=0.98, bottom=0.20, top=0.77, wspace=0.38)
fig.suptitle("One two-level unit in a heat bath", fontsize=16, y=0.97)
fig.text(0.5, 0.86, r"Energies $0,\Delta$ | $\Delta>0$ fixed | Dashed line: $\beta\Delta=\ln 3$",
ha="center")
curves = [(p, p0, "#236a9e", r"$p=U/\Delta$", "Excited population", 0.56),
(heat, c0, "#bd582b", r"$C/k_B$", "Heat capacity", 0.52),
(entropy, s0, "#277953", r"$S/k_B$", "Entropy", 0.79)]
for ax, (values, value0, color, ylabel, title, ymax) in zip(axes, curves):
ax.plot(theta, values, lw=2.6, color=color)
ax.axvline(theta_example, color="#666666", lw=1, ls="--")
ax.scatter([theta_example], [value0], color=color, zorder=4)
ax.set(xlim=(0, 3), ylim=(0, ymax), xlabel=r"$k_B T/\Delta$",
ylabel=ylabel, title=title)
ax.grid(alpha=0.15)
axes[0].axhline(0.5, lw=1.1, ls=":", color="#236a9e")
axes[0].text(1.52, 0.515, "Limit: 1/2", fontsize=10)
axes[1].scatter([theta_peak], [heat_peak], color="#bd582b", zorder=4)
axes[1].annotate("Peak: 0.4392", (theta_peak, heat_peak), xytext=(1.1, 0.44),
fontsize=10, arrowprops={"arrowstyle": "->", "color": "#555555"})
axes[2].axhline(np.log(2), lw=1.1, ls=":", color="#277953")
axes[2].text(1.45, 0.71, r"Limit: $\ln 2$", fontsize=10)
fig.text(0.5, 0.055, r"At $\beta\Delta=\ln 3$: $p=0.25$, $C/k_B=0.2263$, $S/k_B=0.5623$",
ha="center", fontsize=11)
fig.savefig(Path.cwd() / "two-level-thermal-response.png", facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()