codex/dulong-petit-classical-energy.py

dulong-petit-classical-energy.py
"""Generate dulong-petit-classical-energy.png in the current working directory.

Tested: Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
The bars show an ideal classical harmonic model, not material measurements.
Energy units are k_B times 300 K; the common energy origin is omitted.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white",
                         "axes.spines.top": False, "axes.spines.right": False})
    temperatures = np.array([300.0, 600.0])
    positions = np.arange(temperatures.size)
    fig, axes = plt.subplots(1, 2, figsize=(8.4, 4.8))
    fig.subplots_adjust(left=0.09, right=0.97, bottom=0.27, top=0.76, wspace=0.32)
    fig.suptitle("Classical harmonic energy: doubling temperature doubles energy",
                 fontsize=13, y=0.96)
    for ax, directions, heading in zip(
            axes, [1, 3], ["One oscillator direction", "One atom: three directions"]):
        kinetic = directions * temperatures / (2 * 300.0)
        potential = kinetic.copy()
        ax.bar(positions, kinetic, width=0.56, color="#247aa6",
               label="Mean kinetic energy")
        ax.bar(positions, potential, bottom=kinetic, width=0.56,
               color="#e5aa49", label="Mean potential energy")
        for pos, kin, pot in zip(positions, kinetic, potential):
            ax.text(pos, kin / 2, f"{kin:g}", ha="center", va="center", color="white")
            ax.text(pos, kin + pot / 2, f"{pot:g}", ha="center", va="center")
            ax.text(pos, kin + pot + 0.10 * directions, f"total {kin + pot:g}",
                    ha="center", va="bottom", fontsize=10)
        ax.set_xticks(positions, ["300 K", "600 K"])
        ax.set(ylim=(0, 2.65 * directions), xlabel="Temperature",
               ylabel=r"Mean energy / $[k_B(300\,\mathrm{K})]$")
        ax.set_title(heading + "\n" +
                     (r"$\langle E\rangle=k_BT$, $C_V=k_B$" if directions == 1
                      else r"$\langle E\rangle=3k_BT$, $C_V=3k_B$"), fontsize=11)
        ax.set_yticks(np.arange(0, 2.65 * directions, directions))
        ax.set_axisbelow(True)
        ax.grid(axis="y", alpha=0.2)
    handles, labels = axes[0].get_legend_handles_labels()
    fig.legend(handles, labels, loc="lower center", bbox_to_anchor=(0.5, 0.08),
               ncol=2, frameon=False)
    fig.text(0.5, 0.02, r"Equipartition: each quadratic term stores $k_BT/2$; "
             "six terms per atom.", ha="center", fontsize=10)
    fig.savefig(Path.cwd() / "dulong-petit-classical-energy.png", dpi=130,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()