codex/microstate-energy-degeneracy.py

microstate-energy-degeneracy.py
"""Show the four microstates of two distinguishable, independent thermal units.

Build with: make _media/microstate-energy-degeneracy.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
from matplotlib.patches import Circle, FancyBboxPatch
import numpy as np


def main():
    states = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
    energies = states.sum(axis=1)
    weights = np.exp(-np.log(3.0) * energies)
    probabilities = weights / weights.sum()
    degeneracies = np.bincount(energies, minlength=3)
    grouped = np.bincount(energies, weights=probabilities, minlength=3)
    assert np.array_equal(degeneracies, [1, 2, 1])
    assert np.allclose(probabilities, np.array([9, 3, 3, 1]) / 16.0)
    assert np.allclose(grouped, np.array([9, 6, 1]) / 16.0)
    assert np.isclose(grouped.sum(), 1.0)
    assert np.isclose(np.dot(np.arange(3), grouped), 0.5)

    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white"})
    fig, ax = plt.subplots(figsize=(9.0, 5.4), dpi=120)
    fig.subplots_adjust(left=0.03, right=0.97, bottom=0.06, top=0.79)
    fig.suptitle("Four microstates, three energy macrostates", fontsize=16, y=0.97)
    fig.text(0.5, 0.88, r"Two labeled units A, B  |  Energies add  |  $\beta\Delta=\ln 3$, $p=1/4$",
             ha="center")
    ax.set(xlim=(0, 10), ylim=(0, 5.9))
    ax.axis("off")
    ax.text(2.0, 5.62, "Microstate (A, B) and probability", ha="center", weight="bold")
    ax.text(8.03, 5.62, "Total energy, count and probability", ha="center", weight="bold")
    colors = ["#236a9e", "#277953", "#bd582b"]
    ys = [4.75, 3.55, 2.35, 1.15]
    group_ys = [4.75, 2.95, 1.15]
    fractions = ["9/16", "3/16", "3/16", "1/16"]
    for state, energy, y, fraction in zip(states, energies, ys, fractions):
        color = colors[energy]
        ax.add_patch(FancyBboxPatch((0.20, y - 0.40), 3.85, 0.80,
                                    boxstyle="round,pad=0.08", facecolor="white",
                                    edgecolor=color, linewidth=1.5))
        ax.text(0.45, y, f"({state[0]}, {state[1]})", va="center")
        for x, level in zip([1.80, 2.55], state):
            ax.add_patch(Circle((x, y), 0.23, facecolor=color if level else "white",
                                edgecolor=color, linewidth=1.5))
            ax.text(x, y, str(level), ha="center", va="center",
                    color="white" if level else color, fontsize=10)
        ax.text(3.25, y, fraction, ha="center", va="center")
        ax.annotate("", xy=(6.28, group_ys[energy]), xytext=(4.17, y),
                    arrowprops={"arrowstyle": "->", "color": color, "lw": 1.8})
    for energy, y, count, fraction, label in zip(range(3), group_ys, degeneracies,
                                               ["9/16", "6/16", "1/16"],
                                               ["0", r"\Delta", r"2\Delta"]):
        ax.add_patch(FancyBboxPatch((6.42, y - 0.51), 3.10, 1.02,
                                    boxstyle="round,pad=0.08", facecolor="white",
                                    edgecolor=colors[energy], linewidth=1.8))
        ax.text(7.97, y + 0.21, rf"$E={label}$,  $g={count}$", ha="center", va="center")
        ax.text(7.97, y - 0.22, f"Probability = {fraction}", ha="center", va="center")
    ax.text(5.0, 0.18, r"The middle macrostate merges (0,1) and (1,0):  3/16 + 3/16 = 6/16",
            ha="center", fontsize=11)
    fig.savefig(Path.cwd() / "microstate-energy-degeneracy.png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()