codexising-chain-finite-boundary-free-energy.py
ising-chain-finite-boundary-free-energy.py
"""Exact finite Ising boundaries; Python 3.14, matplotlib 3.10.7, numpy 2.3.5.
Run from _media; the matching opaque PNG is written to the current directory.
The open chain has N-1 bonds and the periodic ring has N bonds, with N >= 3.
"""
from itertools import product
from math import exp, fsum, log
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def log_partition(n, coupling, periodic):
bulk = n * np.log(2 * np.cosh(coupling))
if periodic:
return bulk + np.log1p(np.tanh(coupling)**n)
return bulk - np.log(np.cosh(coupling))
def check():
worst = 0.0
cases = 0
for n in (3, 4, 7, 11):
for coupling in (0.0, 0.3, 0.8, 1.6):
transfer = np.array([[exp(coupling), exp(-coupling)], [exp(-coupling), exp(coupling)]])
for periodic in (False, True):
# Direct microstate enumeration, independently of transfer eigenvalues.
weights = []
for spins in product((-1, 1), repeat=n):
bonds = sum(spins[i] * spins[i + 1] for i in range(n - 1))
if periodic:
bonds += spins[-1] * spins[0]
weights.append(exp(coupling * bonds))
z = fsum(weights)
matrix_z = np.trace(np.linalg.matrix_power(transfer, n)) if periodic else np.ones(2) @ np.linalg.matrix_power(transfer, n - 1) @ np.ones(2)
errors = (abs(log(z) - log_partition(n, coupling, periodic)), abs(matrix_z / z - 1), abs(fsum(w / z for w in weights) - 1))
worst = max(worst, *errors)
assert max(errors) < 3e-12
cases += 1
return {"enumerated_cases": cases, "max_absolute_error": float(worst), "checks": "direct sum, transfer-matrix power, probability normalization"}
def main():
print(check())
plt.rcParams.update({"font.size": 11, "axes.spines.top": False, "axes.spines.right": False})
coupling = 0.8
n = np.arange(3, 81)
bulk_g = -np.log(2 * np.cosh(coupling))
open_g = -log_partition(n, coupling, False) / n
ring_g = -log_partition(n, coupling, True) / n
fig, axes = plt.subplots(1, 2, figsize=(10, 4.5), layout="constrained", facecolor="white")
axes[0].plot(1 / n, open_g, "o-", ms=3, color="#0072B2", label="Open chain (N − 1 bonds)")
axes[0].plot(1 / n, ring_g, "s-", ms=3, color="#D55E00", label="Periodic ring (N bonds)")
axes[0].axhline(bulk_g, color="0.4", ls="--", label=r"Limit $g_\infty$ (reference)")
axes[0].set(title="The same bulk limit", xlabel="Inverse number of spins 1/N", ylabel=r"$g_N=F_N/(Nk_BT)$", xlim=(0, 0.34))
axes[0].legend(fontsize=9, loc="upper left")
axes[1].plot(n, n * (open_g - bulk_g), "o-", ms=3, color="#0072B2", label="Open chain")
axes[1].plot(n, n * (ring_g - bulk_g), "s-", ms=3, color="#D55E00", label="Periodic ring")
axes[1].axhline(0, color="0.6", lw=0.8)
axes[1].text(22, 0.315, r"$\log\cosh K$: constant boundary term", fontsize=10, color="#0072B2")
axes[1].text(17, -0.07, r"$-\log[1+(\tanh K)^N]\to0$", fontsize=10, color="#D55E00")
axes[1].set(title="Expose the total correction", xlabel="Number of spins N", ylabel=r"$N(g_N-g_\infty)$", ylim=(-0.27, 0.4), xlim=(2, 81))
fig.suptitle("Finite Ising chain and ring: exact sums at J/(kBT) = 0.8", fontsize=14)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=140, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()