The 2D classical Ising model using CTMRG
While PEPSKit has a lot of use in quantum systems, describing states using InfinitePEPS that can be contracted via CTMRG or boundary MPS techniques, here we shift our focus to classical physics. We consider the 2D classical Ising model and compute its partition function defined as:
\[\mathcal{Z}(\beta) = \sum_{\{s\}} \exp(-\beta H(s)) \text{ with } H(s) = -J \sum_{\langle i, j \rangle} s_i s_j .\]
where the classical spins $s_i \in \{+1, -1\}$ are located on the vertices $i$ of a 2D square lattice. The idea is to encode the partition function as an infinite square network consisting of local rank-4 tensors, which can then be contracted using CTMRG. An infinite square network of these rank-4 tensors can be represented as an InfinitePartitionFunction
object, as we will see.
But first, let's seed the RNG and import all required modules:
using Random, LinearAlgebra
using TensorKit, PEPSKit
using QuadGK
Random.seed!(234923);
Defining the partition function
The first step is to define the rank-4 tensor that, when contracted on a square lattice, evaluates to the partition function value at a given $\beta$. This is done through a fairly generic procedure where the interaction weights are distributed among vertex tensors in an appropriate way. Concretely, here we first define a 'link' matrix containing the Boltzmann weights associated to all possible spin configurations across a given link on the lattice. Next, we define site tensors as delta-tensors that ensiure that the spin value on all adjacent links is the same. Since we only want tensors on the sites in the end, we can symmetrically absorb the link weight tensors into the site tensors, which gives us exactly the kind of network we're looking for. Since we later want to compute the magnetization and energy to check our results, we define the appropriate rank-4 tensors here as well while we're at it.
function classical_ising(; beta=log(1 + sqrt(2)) / 2, J=1.0)
K = beta * J
# Boltzmann weights
t = ComplexF64[exp(K) exp(-K); exp(-K) exp(K)]
r = eigen(t)
nt = r.vectors * sqrt(Diagonal(r.values)) * r.vectors
# local partition function tensor
O = zeros(2, 2, 2, 2)
O[1, 1, 1, 1] = 1
O[2, 2, 2, 2] = 1
@tensor o[-1 -2; -3 -4] := O[3 4; 2 1] * nt[-3; 3] * nt[-4; 4] * nt[-2; 2] * nt[-1; 1]
# magnetization tensor
M = copy(O)
M[2, 2, 2, 2] *= -1
@tensor m[-1 -2; -3 -4] := M[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * nt[-3; 3] * nt[-4; 4]
# bond interaction tensor and energy-per-site tensor
e = ComplexF64[-J J; J -J] .* nt
@tensor e_hor[-1 -2; -3 -4] :=
O[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * nt[-3; 3] * e[-4; 4]
@tensor e_vert[-1 -2; -3 -4] :=
O[1 2; 3 4] * nt[-1; 1] * nt[-2; 2] * e[-3; 3] * nt[-4; 4]
e = e_hor + e_vert
# fixed tensor map space for all three
TMS = ℂ^2 ⊗ ℂ^2 ← ℂ^2 ⊗ ℂ^2
return TensorMap(o, TMS), TensorMap(m, TMS), TensorMap(e, TMS)
end;
So let's initialize these tensors at inverse temperature $\beta=0.6$, check that they are indeed rank-4 and construct the corresponding InfinitePartitionFunction
:
beta = 0.6
O, M, E = classical_ising(; beta)
@show space(O)
Z = InfinitePartitionFunction(O)
InfinitePartitionFunction{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 2, 2, Vector{ComplexF64}}}(TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 2, 2, Vector{ComplexF64}}[TensorMap((ℂ^2 ⊗ ℂ^2) ← (ℂ^2 ⊗ ℂ^2)):
[:, :, 1, 1] =
3.169519816780443 + 0.0im 0.4999999999999995 + 0.0im
0.4999999999999995 + 0.0im 0.1505971059561009 + 0.0im
[:, :, 2, 1] =
0.4999999999999995 + 0.0im 0.1505971059561009 + 0.0im
0.1505971059561009 + 0.0im 0.4999999999999995 + 0.0im
[:, :, 1, 2] =
0.4999999999999995 + 0.0im 0.1505971059561009 + 0.0im
0.1505971059561009 + 0.0im 0.4999999999999995 + 0.0im
[:, :, 2, 2] =
0.1505971059561009 + 0.0im 0.4999999999999995 + 0.0im
0.4999999999999995 + 0.0im 3.169519816780443 + 0.0im
;;])
Contracting the partition function
Next, we can contract the partition function as per usual by constructing a CTMRGEnv
with a specified environment virtual space and calling leading_boundary
with appropriate settings:
Venv = ℂ^20
env₀ = CTMRGEnv(Z, Venv)
env, = leading_boundary(env₀, Z; tol=1e-8, maxiter=500);
[ Info: CTMRG init: obj = +1.784252138312e+00 -1.557258880375e+00im err = 1.0000e+00
[ Info: CTMRG conv 63: obj = +3.353928644031e+00 err = 4.5903314811e-09 time = 8.12 sec
Note that CTMRG environments for partition functions differ from the PEPS environments only by the edge tensors. Instead of two legs connecting the edges and the PEPS-PEPS sandwich, there is only one leg connecting the edges and the partition function tensor, meaning that the edge tensors are now rank-3:
space.(env.edges)
4×1×1 Array{TensorKit.TensorMapSpace{TensorKit.ComplexSpace, 2, 1}, 3}:
[:, :, 1] =
(ℂ^20 ⊗ ℂ^2) ← ℂ^20
(ℂ^20 ⊗ ℂ^2) ← ℂ^20
(ℂ^20 ⊗ (ℂ^2)') ← ℂ^20
(ℂ^20 ⊗ (ℂ^2)') ← ℂ^20
To compute the value of the partition function, we have to contract Z
with the converged environment using network_value
. Additionally, we will compute the magnetization and energy (per site), again using expectation_value
but this time also specifying the index in the unit cell where we want to insert the local tensor:
λ = network_value(Z, env)
m = expectation_value(Z, (1, 1) => M, env)
e = expectation_value(Z, (1, 1) => E, env)
@show λ m e;
λ = 3.3539286440313782 - 5.873212040152551e-16im
m = 0.9736086674403001 + 1.8262157316829647e-17im
e = -1.8637796145082437 - 1.4609725853463717e-16im
Comparing against the exact Onsager solution
In order to assess our results, we will compare against the exact Onsager solution of the 2D classical Ising model. To that end, we compute the exact free energy, magnetization and energy per site (where we use quadgk
to perform integrals of an auxiliary variable from $0$ to $\pi/2$):
function classical_ising_exact(; beta=log(1 + sqrt(2)) / 2, J=1.0)
K = beta * J
k = 1 / sinh(2 * K)^2
F = quadgk(
theta -> log(cosh(2 * K)^2 + 1 / k * sqrt(1 + k^2 - 2 * k * cos(2 * theta))), 0, pi
)[1]
f = -1 / beta * (log(2) / 2 + 1 / (2 * pi) * F)
m = 1 - (sinh(2 * K))^(-4) > 0 ? (1 - (sinh(2 * K))^(-4))^(1 / 8) : 0
E = quadgk(theta -> 1 / sqrt(1 - (4 * k) * (1 + k)^(-2) * sin(theta)^2), 0, pi / 2)[1]
e = -J * cosh(2 * K) / sinh(2 * K) * (1 + 2 / pi * (2 * tanh(2 * K)^2 - 1) * E)
return f, m, e
end
f_exact, m_exact, e_exact = classical_ising_exact(; beta);
And indeed, we do find agreement between the exact and CTMRG values (keeping in mind that energy accuracy is limited by the environment dimension and the lack of proper extrapolation):
@show (-log(λ) / beta - f_exact) / f_exact
@show (abs(m) - abs(m_exact)) / abs(m_exact)
@show (e - e_exact) / e_exact;
(-(log(λ)) / beta - f_exact) / f_exact = -6.605563039765528e-16 - 1.447068124555315e-16im
(abs(m) - abs(m_exact)) / abs(m_exact) = -4.561270094458082e-16
(e - e_exact) / e_exact = -0.023732068099090543 + 7.652732508485748e-17im
This page was generated using Literate.jl.