codexcurie-weiss-finite-size-rounding.py
curie-weiss-finite-size-rounding.py
"""Exact finite Curie–Weiss curves; Python 3.14, matplotlib 3.10.7, numpy 2.3.5.
Run from _media; the matching opaque PNG is written to the current directory.
All curves use finite sums, including the constant diagonal interaction terms.
"""
from itertools import product
from math import comb, exp, fsum, lgamma, log
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def distribution(n, coupling, field):
k = np.arange(n + 1)
spin_sum = 2 * k - n
log_multiplicity = np.array([
lgamma(n + 1) - lgamma(int(j) + 1) - lgamma(n - int(j) + 1)
for j in k
])
log_weights = log_multiplicity + coupling * spin_sum**2 / (2 * n) + field * spin_sum
shift = float(log_weights.max())
weights = np.exp(log_weights - shift)
denominator = float(weights.sum())
return spin_sum / n, weights / denominator, shift + log(denominator)
def check():
worst = 0.0
cases = 0
# Independent enumeration uses pair products, not grouped squared spin sums.
for n in (2, 5, 9, 12):
for coupling, field in ((0.0, 0.0), (0.7, 0.11), (1.5, -0.08)):
m, p, log_z = distribution(n, coupling, field)
raw = []
sums = []
for spins in product((-1, 1), repeat=n):
pair_sum = sum(spins[i] * spins[j] for i in range(n) for j in range(i + 1, n))
s = sum(spins)
raw.append(exp(coupling * pair_sum / n + coupling / 2 + field * s))
sums.append(s / n)
z = fsum(raw)
errors = (abs(log_z - log(z)), abs(float(m @ p) - fsum(w * s for w, s in zip(raw, sums)) / z), abs(float(p.sum()) - 1))
worst = max(worst, *errors)
assert max(errors) < 2e-12
cases += 1
# Independent integer multiplicities check normalization for larger sizes.
for n in (16, 64, 256):
assert sum(comb(n, k) for k in range(n + 1)) == 2**n
m, p, _ = distribution(n, 1.5, 0.0)
assert np.max(np.abs(p - p[::-1])) < 1e-12
assert abs(float(m @ p)) < 1e-12
for n in (16, 1024):
_, p, log_z = distribution(n, 0, 0.13)
assert abs(log_z - n * np.log(2 * np.cosh(0.13))) < 2e-10
assert abs(float(p.sum()) - 1) < 1e-12
m, p, _ = distribution(64, 1.5, 0.01)
step = 1e-6
plus = distribution(64, 1.5, 0.01 + step)
minus = distribution(64, 1.5, 0.01 - step)
slope = (plus[0] @ plus[1] - minus[0] @ minus[1]) / (2 * step)
response = 64 * (m**2 @ p - (m @ p)**2)
assert abs(slope - response) < 1e-6
return {"enumerated_cases": cases, "max_absolute_error": worst, "response_identity_error": float(abs(slope - response))}
def main():
print(check())
plt.rcParams.update({"font.size": 11, "axes.spines.top": False, "axes.spines.right": False})
fig, axes = plt.subplots(1, 2, figsize=(10, 4.5), layout="constrained", facecolor="white")
fields = np.linspace(-0.09, 0.09, 721)
temperatures = np.linspace(0.65, 1.5, 221)
for n, color in zip((16, 64, 256, 1024), ("#0072B2", "#D55E00", "#009E73", "#7A5195")):
means = []
magnitudes = []
for u in fields:
m, p, _ = distribution(n, 1.5, u)
assert abs(float(p.sum()) - 1) < 1e-12
means.append(float(m @ p))
for theta in temperatures:
m, p, _ = distribution(n, 1 / theta, 0.0)
assert abs(float(m @ p)) < 1e-11
magnitudes.append(float(np.abs(m) @ p))
axes[0].plot(fields, means, color=color, label=f"N = {n}", lw=2)
axes[1].plot(temperatures, magnitudes, color=color, label=f"N = {n}", lw=2)
axes[0].axhline(0, color="0.7", lw=0.7)
axes[0].axvline(0, color="0.7", lw=0.7)
axes[0].set(title="Field rounding: J/(kBT) = 1.5", xlabel=r"Conjugate field $u=h/(k_BT)$", ylabel=r"Mean magnetization $\langle m\rangle$", ylim=(-1, 1))
axes[0].set_xticks((-0.08, -0.04, 0, 0.04, 0.08))
axes[0].legend(loc="upper left", fontsize=10)
axes[1].axvline(1, color="0.4", ls="--", lw=1)
axes[1].text(1.025, 0.87, "Limiting critical\ntemperature", color="0.3", fontsize=10)
axes[1].set(title="Temperature rounding: h = 0", xlabel=r"Temperature $k_BT/J$", ylabel=r"Mean magnitude $\langle |m|\rangle$", ylim=(0, 1))
axes[1].text(0.05, 0.025, r"$\langle m\rangle=0$ for every finite N", transform=axes[1].transAxes, fontsize=10, bbox={"facecolor": "white", "edgecolor": "none", "pad": 2})
fig.suptitle("Curie–Weiss model: exact finite sums, not measured data", fontsize=14)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=140, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()