codex/renormalization-block-spin-coarse-graining.py

renormalization-block-spin-coarse-graining.py
"""Construct a majority-block diagram. Tested on Python 3.14, NumPy 2.3.5,
Matplotlib 3.10.7. Run from _media; emits this script's basename as PNG.
"""
from pathlib import Path
from itertools import product

import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from matplotlib.patches import FancyArrowPatch
import numpy as np


def example():
    signs = np.array([[1, 1, -1], [1, -1, -1], [-1, 1, 1]])
    positive = np.array([[1, 1, -1], [1, -1, 1], [-1, 1, -1]])
    spins = np.empty((9, 9), dtype=int)
    for row in range(3):
        for col in range(3):
            tile = np.rot90(positive, (row + 2 * col) % 4) * signs[row, col]
            spins[3 * row:3 * row + 3, 3 * col:3 * col + 3] = tile
    blocked = np.sign(spins.reshape(3, 3, 3, 3).sum(axis=(1, 3)))
    assert np.array_equal(blocked, signs)
    return spins, blocked


def checks():
    patterns = np.array(list(product((-1, 1), repeat=9)))
    totals = patterns.sum(axis=1)
    assert (totals != 0).all()
    assert np.count_nonzero(totals > 0) == 256
    spins, blocked = example()
    # Separate slicing checks the reshape convention used to construct the diagram.
    expected = [[np.sign(spins[3*r:3*r+3, 3*c:3*c+3].sum())
                 for c in range(3)] for r in range(3)]
    assert np.array_equal(blocked, expected)
    return {"majority_patterns": len(patterns), "positive_majorities": 256,
            "negative_majorities": 256, "block_sums":
            spins.reshape(3, 3, 3, 3).sum(axis=(1, 3)).tolist()}


def grid(ax, cells, extent):
    n = len(cells)
    ax.imshow(cells, cmap=ListedColormap(["#386aa0", "#c8584b"]),
              vmin=-1, vmax=1, extent=(0, extent, extent, 0),
              interpolation="nearest")
    for pos in np.linspace(0, extent, n + 1):
        ax.plot([pos, pos], [0, extent], color="white", lw=0.9)
        ax.plot([0, extent], [pos, pos], color="white", lw=0.9)
    for row in range(n):
        for col in range(n):
            ax.text((col+.5)*extent/n, (row+.5)*extent/n,
                    "+" if cells[row, col] > 0 else "−", color="white",
                    ha="center", va="center", fontsize=11 if n == 9 else 16)
    ax.set_xlim(-.1, 9.1)
    ax.set_ylim(9.1, -.1)
    ax.set_aspect("equal")
    ax.axis("off")


def main():
    print(checks())
    spins, blocked = example()
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    fig = plt.figure(figsize=(11, 4.8), facecolor="white")
    axes = [fig.add_axes([x, .20, .265, .64]) for x in (.035, .365, .695)]
    grid(axes[0], spins, 9)
    grid(axes[1], blocked, 9)
    grid(axes[2], blocked, 3)
    for pos in (0, 3, 6, 9):
        axes[0].plot([pos, pos], [0, 9], color="#242b37", lw=1.5)
        axes[0].plot([0, 9], [pos, pos], color="#242b37", lw=1.5)
    labels = [("1  Resolve the spins", "9 × 9 sites; spacing a"),
              ("2  Keep each majority", "3 × 3 variables; spacing 3a"),
              ("3  Rescale coordinates", "x′ = x/3; spacing a again")]
    for ax, (title, detail) in zip(axes, labels):
        ax.text(0, 1.10, title, transform=ax.transAxes, fontsize=12, weight="bold")
        ax.text(0, 1.02, detail, transform=ax.transAxes, fontsize=10, color="#434d5d")
    for start, end, label in [(.305, .363, "sum"), (.635, .693, "rescale")]:
        fig.add_artist(FancyArrowPatch((start, .52), (end, .52),
                       transform=fig.transFigure, arrowstyle="-|>",
                       mutation_scale=18, lw=1.6, color="#434d5d"))
        fig.text((start+end)/2, .57, label, ha="center", fontsize=9)
    axes[2].text(0, 4.3, "Same nine signs.\nSmaller coordinate extent.\nNo physical compression.",
                 fontsize=10, va="top", color="#434d5d", linespacing=1.6)
    fig.text(.05, .13, "Each 3 × 3 block has five spins of its majority sign and four of the other sign.", fontsize=11)
    fig.text(.05, .065, "Constructed configuration: the coarse probabilities still require summing microscopic Boltzmann weights.",
             fontsize=10, color="#434d5d")
    fig.savefig(Path(__file__).stem + ".png", dpi=120, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()