codexboltzmann-microstates-and-degeneracy.py
boltzmann-microstates-and-degeneracy.py
"""Show individual Boltzmann probabilities and their energy grouping.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Writes the matching PNG basename into the current working directory.
Two distinguishable noninteracting two-level units; stipulated toy model.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def main():
energies = np.array([0.0, 1.0, 1.0, 2.0]) # units of Delta
weights = np.exp(-np.log(2.0) * energies)
probabilities = weights / weights.sum()
aggregates = np.array([probabilities[energies == e].sum() for e in range(3)])
assert np.allclose(probabilities, np.array([4, 2, 2, 1]) / 9)
assert np.allclose(aggregates, np.array([4, 4, 1]) / 9)
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
"axes.facecolor": "white", "savefig.facecolor": "white",
"axes.spines.top": False, "axes.spines.right": False})
colors = ["#236b9b", "#bd6429", "#bd6429", "#33836b"]
fig = plt.figure(figsize=(9.0, 5.2), dpi=120)
grid = fig.add_gridspec(1, 3, width_ratios=[1.0, 1.1, 1.05],
left=0.075, right=0.975, bottom=0.28, top=0.78, wspace=0.5)
energy_ax, micro_ax, macro_ax = [fig.add_subplot(grid[0, i]) for i in range(3)]
fig.suptitle("Boltzmann probabilities: count microstates before grouping", fontsize=15, y=0.97)
fig.text(0.5, 0.885, r"$\Delta = k_B T\ln 2$, $q=1/2$, $Z=9/4$", ha="center", fontsize=13)
positions = np.arange(4)
state_labels = ["00", "01", "10", "11"]
energy_ax.bar(positions, energies, color=colors, width=0.62, alpha=0.8)
energy_ax.scatter(positions, energies, color=colors, marker="_", s=300, linewidths=3, zorder=4)
energy_ax.set(ylim=(-0.1, 2.45), ylabel=r"Energy $E_i/\Delta$", title="Four microstates")
energy_ax.set_xticks(positions, state_labels)
energy_ax.set_yticks([0, 1, 2])
for x, y, label in zip(positions, energies, ["0", r"$\Delta$", r"$\Delta$", r"$2\Delta$"]):
energy_ax.text(x, y + 0.14, label, ha="center", fontsize=11)
micro_ax.bar(positions, probabilities, color=colors, width=0.62)
micro_ax.set(ylim=(0, 0.56), ylabel=r"Individual probability $p_i$", title="One bar per microstate")
micro_ax.set_xticks(positions, state_labels)
for x, y, label in zip(positions, probabilities, ["4/9", "2/9", "2/9", "1/9"]):
micro_ax.text(x, y + 0.02, label, ha="center", fontsize=11)
macro_ax.bar(np.arange(3), aggregates, color=[colors[0], colors[1], colors[3]], width=0.62)
macro_ax.set(ylim=(0, 0.56), ylabel=r"Aggregate probability $P(E)$", title="Group by energy")
macro_ax.set_xticks([0, 1, 2], ["0\ng = 1", r"$\Delta$" + "\ng = 2", r"$2\Delta$" + "\ng = 1"])
for x, y, label in zip(range(3), aggregates, ["4/9", "4/9", "1/9"]):
macro_ax.text(x, y + 0.02, label, ha="center", fontsize=11)
for ax in [micro_ax, macro_ax]:
ax.set_yticks([0, 0.25, 0.5], ["0", "1/4", "1/2"])
for ax in [energy_ax, micro_ax, macro_ax]:
ax.grid(axis="y", alpha=0.18)
ax.set_axisbelow(True)
fig.text(0.5, 0.14, "Middle energy: two distinct microstates, 2/9 + 2/9 = 4/9", ha="center", fontsize=12)
fig.text(0.5, 0.06, "Two labeled, noninteracting units; fixed positive temperature; finite toy model",
ha="center", fontsize=10, color="#444444")
fig.savefig(Path(__file__).with_suffix(".png").name, transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()