『ゼロからできるMCMC』 Chapter7 Ising modelをJuliaで
最終更新:2020/12/7
『ゼロからできるMCMC』 Chapter7 Ising model(イジングモデル)のコードを, 練習を兼ねてJuliaで書いてみました. 私は, イジングモデルに関しては全く素人なので結果の解釈等については大きな錯誤があるかもしれません. また,詳細なことを書くと著作権的にだめな気もするので, 解説などはありません.
Version 1.6.2
Metropolis法によるサンプリング
Julia code
$N \times N$ 格子で温度は $T$, 結合定数は $J$, 外部磁場は $h$ としている. 初期状態はランダムに+1, -1を各格子に与えた. 格子点 $i_x$, $i_y$をランダムに選び, 確率 $\min{(1, \exp{(-\Delta E/T)})}$ でその格子 ( $s_{i_{x}, i_{y}}$ ) のスピンを反転させる. $\Delta E$ は,
と計算した. 計算回数はNiter
ステップに一回のサンプリングとした.
そして, Nprint
回サンプルした.
境界条件は周期境界条件とした.
端っこに対応するために, nm
, np
という変数をつくっている.
これは例えば $N = 5$ の 場合だと,
nm
= [5, 1, 2, 3, 4]np
= [2, 3, 4, 5, 1]
using Plots using Random:seed! ## パラメータ struct Params N::Int64 Niter::Int64 J::Float64 h::Float64 T::Float64 Nprint::Int64 end ## 初期配位 function initializeState(N) # s = ones(N, N); s = randn(N, N); @. s[s>0] = 1 @. s[s<0] = -1 return s end ## ΔEの計算 function calcdE(s, J, h, ix, iy, np, nm) dE = 2*s[ix, iy] * (J*(s[np[ix], iy] + s[nm[ix], iy] + s[ix, np[iy]] + s[ix, nm[iy]])+h); return dE end ## Metropolis法で更新 function isingMetropolis(p::Params) naccept = 0; s = zeros(p.N, p.N, p.Nprint); s[:, :, 1] = initializeState(p.N); np = [Vector(2:p.N); 1] nm = [p.N; Vector(1:p.N-1)]; for I = 2:p.Nprint ss = s[:, :, I-1]; for K = 1:p.Niter # select a random cell ix = rand(1:p.N); iy = rand(1:p.N); # calculate action dE = calcdE(ss, p.J, p.h, ix, iy, np, nm); # Metropolis test if exp(-dE/p.T) > rand() ss[ix, iy] = -ss[ix, iy]; naccept += 1; end end s[:, :, I] = ss; end r = 100*naccept/(p.Nprint*p.Niter); display("acceptance rate is $(r) %") return s end ## ここからメイン p = Params(100, #N 100^2*10, # Niter 1.0, # J 0.0, # h 2/log(1+sqrt(2)), #T (crit: 2/log(1+sqrt(2))) 100) # Nprint; @time S = isingMetropolis(p::Params); # gifとして保存 anim = Animation() for I = 2:p.Nprint fig = heatmap(S[:, :, I], aspect_ratio=:equal, showaxis=:false, grid=:false, ticks = nothing, size = (200, 200), colorbar=:none) frame(anim, fig) end gif(anim, "ising3.gif", fps=10)
コード内に示したような設定で計算した.
0.454313 seconds (2.93 k allocations: 15.539 MiB)
とのこと.
温度は相転移温度に設定した.
教科書には臨界減速の話が載っているので, このケースについても自己相関をみてみた. 荒いサンプリングで申し訳ないが, 相転移温度に設定したことによって強い自己相関が現れているのがわかる.
Juliaコード
JuliaspinSum = [sum(S[:, :, I]) for I in 1:p.Nprint] plt =scatter(1:p.Nprint, spinSum) xlabel!("smaple (per 10^5 steps)") ylabel!("The sum of the spin") plt[1][:legend] = :none plt[:dpi] = 300; png(plt, "ising4")
外部磁場の変化
結合定数 $J = 1$, 温度 $T=1$ に固定し, 外部磁場 $h$ を変化させてみる. 格子サイズは64, 40960ステップに一回のサンプリングとする. これは, 教科書図7.9の再現である.
初期状態はすべてのスピンが上向き ( +1 ) とする. そして, 初期の外部磁場は $h = -0.4$ で 100サンプルに一回磁場を 0.1下げ, 400回のサンプリングを行なう. なお, 外部磁場が負のときの基底状態はすべてのスピンが下向きである.
コードは, 磁場を下げるたびに ( 100回サンプルするたびに ) そのタイムステップまでの100回分のスピンの合計を計算している. また, そのあと前の100回の最後の状態を次の初期状態としないといけないので, 新たに上の節とは異なる函数をつくった. 絶対もっと効率的な方法があると思うが, Juliaは速いので気にならなかった.
function initializeState(S) s = S[:, :, end] return s end # 前の基底状態を引き継げるようにした函数 function isingMetropolisContinue(p::Params, S) naccept = 0; s = zeros(p.N, p.N, p.Nprint); s[:, :, 1] = initializeState(S); np = [Vector(2:p.N); 1] nm = [p.N; Vector(1:p.N-1)]; for I = 2:p.Nprint ss = s[:, :, I-1]; for K = 1:p.Niter # select a random cell ix = rand(1:p.N); iy = rand(1:p.N); # calculate action dE = calcdE(ss, p.J, p.h, ix, iy, np, nm); # Metropolis test if exp(-dE/p.T) > rand() ss[ix, iy] = -ss[ix, iy]; naccept += 1; end end s[:, :, I] = ss; end r = 100*naccept/(p.Nprint*p.Niter); display("acceptance rate is $(r) %") return s end # ここからがメイン sumSpin = zeros(1, 400) hs = [-0.4 -0.5 -0.6 -0.7]; S = ones(64, 64, 1); for H = 1:4 p = Params(64, 64^2*10, 1.0, hs[H], 1.0, 100) @time S = isingMetropolisContinue(p::Params, S); sumSpin[1+100*(H-1):100*H]= [sum(S[:, :, I]) for I = 1:100] end #plot sumSpin = sumSpin’; fig = scatter(1:100, sumSpin[1:100], markershape=:x, label="h=-0.4") scatter!(101:200, sumSpin[101:200], markershape=:x, label="h=-0.5") scatter!(201:300, sumSpin[201:300], markershape=:x, label="h=-0.6") scatter!(301:400, sumSpin[301:400], markershape=:x, label="h=-0.7") xlabel!("iteration") ylabel!("The sum of the spins") png("ising1.img")
$h=-0.4$ ではなかなかスピンがすべて1の状態から抜け出せていない. $h=-0.6$ くらいまで下げてやっと基底状態 ( スピンがすべて−1, 即ちスピンの和は−4096 ) へ遷移し始める.
上の例は $T=1.0$ でのシミュレーションであった. $T=1.5$ と少し温度を上げてやると以下のように速やかに基底状態へ遷移する ( この辺の解釈については統計力学を勉強したことがないので自信がない ) .
Wolffのアルゴリズム
解説は教科書を見ていただきたい.
using Plots ## parameters struct Params N::Int64 nIter::Int64 J::Float64 T::Float64 nPrint::Int64 end function initializeState(N) s = randn(N, N); @. s[s>0] = 1 @. s[s<0] = -1 return s end function isingWolff(p::Params) S = zeros(p.N, p.N, p.nPrint) # preallocation S[:, :, 1] = initializeState(p.N) # 初期状態を格納 np = [Vector(2:p.N); 1] # 1:1:Nに+1した値 nm = [p.N; Vector(1:p.N-1)] # 1:1:Nに-1した値 prob = 1 - exp(-2 * p.J / p.T) # クラスタに追加する確率 s = S[:, :, 1] # 初期状態から開始 for J = 2:p.nPrint # p.nPrint回サンプリング for I = 1:p.nIter # サンプリング毎にp.nIterステップ ( クラスタの反転 ) 行なう # クラスタを反転させる函数 s = clusterFlip(prob, nm, np, p.N, s) end S[:, :, J] = s display(J) end return S end # クラスタを反転させる函数 function clusterFlip(prob, nm, np, N, s) inOrOut = falses(N, N) # クラスタに各点が属するかの情報. 0 is out, 1 is out of the cluster ( 教科書と逆! ) ix = rand(1:p.N) # choose a random point iy = rand(1:p.N) inOrOut = falses(N, N) inOrOut[ix, iy] = true # add the point to the cluster spinCluster = s[ix, iy] # this cluster' spin (+1 or -1) iCluster = [ix iy] # 1つ目の点をクラスタに追加 k = 1 while k-1 < size(iCluster, 1) # ここからは教科書のコードとあまりかわらない ix, iy = iCluster[k, :] # クラスタに属する点を選択 ixp1 = np[ix] ixm1 = nm[ix] iyp1 = np[iy] iym1 = nm[iy] # 右隣 # 次のif文は, 隣とスピンが同じでかつ, すでにクラスタに入っていない場合, ということ if s[ixp1, iy] == spinCluster && ~inOrOut[ixp1, iy] if rand() < prob iCluster = [iCluster; [ixp1 iy]] inOrOut[ixp1, iy] = true; end end # 上隣 if s[ixm1, iy] == spinCluster && ~inOrOut[ixm1, iy] if rand() < prob iCluster = [iCluster; [ixm1 iy]] inOrOut[ixm1, iy] = true; end end # 左隣 if s[ix, iyp1] == spinCluster && ~inOrOut[ix, iyp1] if rand() < prob iCluster = [iCluster; [ix iyp1]] inOrOut[ix, iyp1] = true; end end # 下隣 if s[ix, iym1] == spinCluster && ~inOrOut[ix, iym1] if rand() < prob iCluster = [iCluster; [ix iym1]] inOrOut[ix, iym1] = true; end end k += 1 end @. s[inOrOut] = -1*s[inOrOut] # クラスタに属する点をいっきに反転 return s end ## メイン p = Params(200, #N 10, # Niter 1.0, # J 2.30, #2/log(1+sqrt(2)), #T (crit: 2/log(1+sqrt(2))) 350) # Nprint; @time S = isingWolff(p) # 動画で保存 anim = Animation() for I = 2:p.nPrint fig = heatmap(S[:, :, I], aspect_ratio=:equal, showaxis=:false, grid=:false, ticks = nothing, size = (200, 200), colorbar=:none, title="t = $(I)") # c = :cividis) # お好きな色に frame(anim, fig) end gif(anim, "test.gif", fps=20)
上のコード内に示した条件での結果. どんどんクラスタが大きくなり, 最後は基底状態をいったりきたりする結果となった. 最後の方になると, 一つのクラスタが大きくなるからか, 計算時間が非常に長くなる. これは, コードが悪いのかそうなって当然なのかどうだろう. tが1進むごとに, 10ステップ進んでいることに注意.