# 簡單高效的2D Ising model 蒙地卡羅程式實現 :::info 此文章主旨為展示如何使用程式進行簡短且高效的Ising model simulation 因此本文將不會對 Ising model 做過多的物理解釋 也不會使用較難的方式如使用 MPI 文中會展示的程式語言有 C++, Python, Julia ::: ## 1. Ising model 簡介 Ising model 是最簡單的相變模型,對於計算物理、統計物理及凝態物理都是較為重要且基本的模型 學習 Ising model simulation 對於瞭解這些領域及物理概念如:鐵磁性物質、相變等 有很大幫助 Ising model 有 Hamiltonian \begin{equation} H = E = -J\sum_{<i,j>}s_is_j+h \sum_i s_i \end{equation} 其中$s$為自旋,有值$s=±1$;$J$為自旋-自旋耦合常數;$h$為外場,本文中$h=0, J=1$ $\sum_{<i,j>}$ 表示對所有點$i$及其鄰近點$j$做求和 在溫度$T$時,Ising model 滿足波茲曼分布(Boltzmann distribution) \begin{equation} P(σ)={1 \over Z}exp(-{E(σ) \over k_B T}) \end{equation} 其中$\sigma$為自旋組態,$Z$為配分函數,在模擬中波茲曼常數$k_B=1$ ## 2. 模擬方法 在電腦上進行 Ising model 模擬的主要方法為馬可夫鍊蒙地卡羅 (MCMC),其宗旨是在電腦上建立晶格,並透過 Metropolis 演算法使該晶格"演化"至平衡態,藉由統計在各平衡態上的觀測量,我們可以獲得系統在該溫度下的觀測量。 \ 模擬的步驟分為以下幾個部分 * 設定參數:如溫度、晶格大小、演化步數等 * 初始化:初始化晶格、亂數等 * 演化 * 測量物理量:常見物理量有磁化強度、磁化率、熱容等 * 重複演化及測量直至有足夠多的統計數據 ### Metroplis 演算法 Metropolis 演算法是一種高效的模擬演化方法,但要注意的是使用此方法的演化過程並沒有物理意義,物理意義僅在統計系統到達熱平衡時的物理量後才出現。 在 Metropolis 演算法中,狀態$\sigma_A$變為狀態$\sigma_B$的機率為 \begin{equation} P(\sigma_A → \sigma_B)=min[1,exp(-{\Delta E_{AB} \over T})] \end{equation} *此方程式意為:若$\Delta E_{AB} < 0$則一定翻轉$(P=1)$,反之則有翻轉機率$P=exp(-{\Delta E_{AB} \over T})$ 具體的演化做法為在晶格中選定一點自旋$s_i$(可以照順序選也可以隨機選),計算其翻轉前後的能量差,由能量差可以得知翻轉機率$P$,取一在$[0,1)$上均勻分布的亂數$r$,若$r<P$則該點翻轉,之後進行下一次演化 \ 注意,在一次僅選定一個自旋$s_i$的情況下,由於系統的總能量僅因該點翻轉而改變,因此我們不用考慮整個系統的能量,僅須關注該點造成的影響,此時我們有 $$ \begin{align} \Delta E_{AB} &= E_B - E_A \\ &= \sum_{<j>}(-s_i)s_j-\sum_{<j>}s_is_j \\ &=-2\sum_{<j>}s_is_j \\ &=-2E_A \end{align} $$ 則狀態$\sigma_A$變為狀態$\sigma_B$的機率可以寫為 \begin{equation} P(\sigma_A → \sigma_B)=min[1,exp({2E_A \over T})] \end{equation} ## 3. 程式碼實例 :::warning 此程式碼僅作簡單計算,結果有一定物理意義,但若要有更好更合理的結果則須考慮測量時系統是否已達到平衡、資料相關性等問題 ::: 程式依照 C++, Python, Julia 順序提供範例,程式碼中有註解,在程式碼下會對內容進行詳細補充 *註:以下程式中變數名稱定義如下 $$ \begin{align} L &= 晶格線性大小(邊長) \\ T &= 溫度 \\ lat &= 晶格本體 \\ i,j &= x,y\ 方向晶格編號 \\ m &= 磁化強度 \\ t &= 目前步數 \\ step &= 總步數 \\ dH &= 翻轉前後能量差 = 2E_A \end{align} $$ 範例觀測之物理量為磁化強度(magnetization),定義為 \begin{equation} m={1 \over N} \sum_{i=0}^{N} s_i \end{equation} 其中$N$為晶格總大小$L \times L$ ### C++ 模擬實作 ```C++= #include <iostream> #include <cmath> int main(){ #定義參數 constexpr int L=8; constexpr float T=2.0; constexpr int step=1000; #初始化 int lat[L][L]; float m[step]; for(int i=0;i<L;i++){ for(int j=0;j<L;j++){ lat[i][j]=1; } } #開始演化 for(int t=0;t<step;t++){ float sum=0.0; #每一步演化皆將每個格點訪問一次 for(int i=0;i<L;i++){ for(int j=0;j<L;j++){ #計算能量差(2E_A) float dH=-2.0*lat[i][j]*(lat[(i+1)%L][j]+lat[(i==0)?L-1:i-1][j]+lat[i][(j+1)%L]+lat[i][(j==0)?L-1:j-1]); float rf = 1.0 * rand() / (RAND_MAX + 1); #判斷是否翻轉 if(rf < exp(dH/T)){ lat[i][j] = -lat[i][j]; } #進行測量 sum+=1.0*lat[i][j]; } } m[t]=sum/(L*L); } return 0; } ``` * 使用 constexpr 能有較高的執行效率,減少程式運行時的計算 * `lat[i][j]=1`表示此範例在運行時採用冷啟動(固定自旋為1或-1),如有需求也可以改用熱啟動(每個點隨機是1或-1) *利用vector函式庫有助於程式優化,但這裡簡單演示,不過多引入非必要函式庫 * `[(i+1)%L]`及`[(i==0)?L-1:i-1]`其目的為建立週期性邊界條件,詳細運作原理看下面補充 * 由於`rf`範圍為$[0,1)<exp(-{\Delta E_{AB} \over T}), \forall \Delta E_{AB}\leq0$因此在實作中不用將$\Delta E_{AB} < 0$情況特別寫出 * 由於C++作圖不便,這裡沒有將我們想要的資料`m[t]`繪圖及存檔,在實際操作中需將該資料存下並用其他程式分析 #### 補充 為了實現週期性邊界條件,我們假設一晶格線性大小$L$,則其一邊可以表示為 \begin{equation} [...,L-2,L-1],[0,1,...,L-2,L-1],[0,1,...] \end{equation} C++陣列編號由0開始,一個[ ]內為一個周期,可以看出$L-1$的右邊格$L$滿足$L\%L=0$,但因C\+\+中`(-1%L)=-1`無法透過`%`算符構成向左的週期性邊界條件,因此使用三元運算符`a ? b : c`代替,其邏輯上等價於 ```C++ if(a){ b; }else{ c; } ``` 即只有$i=0$時有左邊為$L-1$,其餘皆為$i-1$與原本相同 ### Python 模擬實作 ```python= import numpy as np import random as rd import matplotlib.pyplot as plt #設定參數 L=16 T=2.0 step=1000 #初始化 lat=np.ones((L,L)) m=np.zeros(step) #開始演化 for t in range(step): for i in range(L): for j in range(L): dH=-2.0*lat[i,j]*(lat[(i+1)%L,j]+lat[(i-1)%L,j]+lat[i,(j+1)%L]+lat[i,(j-1)%L]) #判斷是否翻轉 if rd.random() < np.exp(dH/T): lat[i,j]=-lat[i,j] #進行測量 m[t]=np.mean(lat) #將結果繪圖 plt.plot(np.linspace(0,step-1,step),m) plt.show() ``` * 利用`lat=np.ones((L,L))`直接初始化冷啟動晶格 * 利用`(i+1)%L`及`(i-1)%L`建立週期性邊界條件,需要注意的是與C++不同,Python中的取餘算符`%`回傳值與除數相同,如`(-1%3)=2`,因此可以直接使用`%`算符實現週期性邊界條件 由於 Python 運算速度較慢,若沒有需要使用已存在的函式庫,或使用平行運算、向量化等方式進行優化,不建議使用 Python 生產數據 ### Julia 模擬實作 ```julia= using Plots function main(::Val{L}, ::Val{T}) where {L, T} #設定參數 step = 1000 #初始化 lat = ones(Int8, L, L) m = zeros(step) #開始演化 for t in 1:step for i in 1:L, j in 1:L dH = 2*lat[i, j]*(lat[i%L+1, j] + lat[(i==1) ? L : i-1, j] + lat[i, j%L+1] + lat[i, (j==1) ? L : j-1]) #判斷是否翻轉 rand(Float64) < exp(-dH/T) && (lat[i, j] = -lat[i, j]) end #進行測量 m[t] = sum(lat)/L^2 end #將結果繪圖 plot(1:step, m, framestyle=:box, xlabel = "step", ylabel = "m", label = "T = $T", size=(900, 600)) end #呼叫函式以進行模擬 main(Val(16), Val(2.0)) ``` * 在 Julia 中,將程式包裝為函式有助於編譯器優化,因此寫成函式會帶來極大的效能提升 * 將參數$L$及$T$用`::Val{}`包裝,有助於編譯器優化,能大幅提高效能,但會消耗較多的編譯資源 * 週期化邊界條件邏輯與C++相同,但因 Julia 陣列編號由1開始,因此詳細稍有不同 * 在 Julia 中,`a && b`邏輯等效於`if a; b; end` ## 4. 總結 由於 C++ 較為複雜且有些短處如:繪圖不易、語法困難,因此在初心者使用上若沒有特殊需求,作者強烈推薦使用 Julia 作為主要程式語言。Julia 有與 C++ 相近的運算速度及與 Python 相似的簡單語法,也包含 Plots、Dataframe 等工具,實作上不用為了進行分析而學習第二種程式語言,但由於該語言較新,社群較小、應用不如 C++ 般廣泛,在尋找資源時可能反而不如 C++ 方便