codex/ising-four-spin-ring-configurations.py

ising-four-spin-ring-configurations.py
"""Enumerate the sixteen states of a four-edge classical Ising ring.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; the opaque PNG is written to the current directory.
"""

from itertools import product
from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.patches import Circle
import numpy as np


def main():
    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
    states = {d: [] for d in (0, 2, 4)}
    for spins in product((1, -1), repeat=4):
        walls = sum(spins[i] != spins[(i + 1) % 4] for i in range(4))
        states[walls].append(spins)
    assert [len(states[d]) for d in (0, 2, 4)] == [2, 12, 2]

    fig = plt.figure(figsize=(11, 7), dpi=100, facecolor="white")
    fig.suptitle("Four spins, four distinct edges, sixteen configurations", y=0.96, fontsize=17)
    columns = fig.add_gridspec(1, 3, width_ratios=(1.1, 3.2, 1.1),
                              left=0.03, right=0.97, top=0.82, bottom=0.16, wspace=0.13)
    points = np.array([[-0.55, 0.55], [0.55, 0.55], [0.55, -0.55], [-0.55, -0.55]])
    for col, walls in enumerate((0, 2, 4)):
        panel = fig.add_subplot(columns[col])
        panel.axis("off")
        panel.set_title(f"{walls} walls  |  {len(states[walls])} states\n"
                        + {0: r"$H=-4J$", 2: r"$H=0$", 4: r"$H=+4J$"}[walls],
                        fontsize=14, pad=12)
        rows, cols = (4, 3) if walls == 2 else (2, 1)
        for n, spins in enumerate(states[walls]):
            row, smallcol = divmod(n, cols)
            height = 0.92 / rows
            width = 0.96 / cols
            y = 0.04 + (rows - row - 1) * height
            ax = panel.inset_axes([0.02 + smallcol * width, y, width, height])
            ax.set(xlim=(-0.85, 0.85), ylim=(-0.85, 0.85), aspect="equal")
            ax.axis("off")
            for i in range(4):
                j = (i + 1) % 4
                broken = spins[i] != spins[j]
                ax.plot(points[[i, j], 0], points[[i, j], 1],
                        color="#b93b47" if broken else "#9ca6ae",
                        linestyle="--" if broken else "-", linewidth=2.2, zorder=1)
            for (x, y), spin in zip(points, spins):
                ax.add_patch(Circle((x, y), 0.17,
                                   facecolor="#246caa" if spin == 1 else "#ca691b",
                                   edgecolor="white", linewidth=1.2, zorder=2))
                ax.text(x, y, "+" if spin == 1 else "−", color="white",
                        ha="center", va="center", fontsize=13, weight="bold", zorder=3)
    handles = [Line2D([], [], marker="o", linestyle="", color="#246caa", label=r"spin $+1$"),
               Line2D([], [], marker="o", linestyle="", color="#ca691b", label=r"spin $-1$"),
               Line2D([], [], linestyle="--", color="#b93b47", label=r"domain wall: cost $2J$")]
    fig.legend(handles=handles, loc="lower center", bbox_to_anchor=(0.5, 0.06), ncol=3, frameon=False)
    fig.text(0.5, 0.035, r"Zero field, $J>0$. Vertex labels remain fixed; rotations are distinct microstates.",
             ha="center", fontsize=11)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()