平均二乗変位をフーリエ変換で求める
概要
ランダムな力を受けて動く粒子がいるとします。その運動方程式は以下のようなランジュバン方程式に従うでしょう。
ただし、
この
で定義されます。これはナイーブに計算すると
以下のコードはGitHubに公開してあり、Google Colabで直接開くことができます。
周期境界補正
平均二乗変位を求める前に、周期境界補正について考えます。多くの場合、シミュレーションでは周期境界を用いて計算を行います。すると、注目する粒子が境界を跨ぐ時に、それをちゃんと考慮してやらないと変位がおかしなことになります。まずは周期境界下で座標の時系列x
が与えられた時に、周期境界条件の補正をしてやることを考えましょう。
システムサイズを
import numpy as np
from matplotlib import pyplot as plt
import random
L = 10
粒子が
L = 10
x = [i - (i // L)*L for i in range(L*3)]
plt.plot(x)
plt.show()
これが、あたかも周期境界がなく、無限の領域を運動していたらどんな軌跡だったかを考えるのが周期境界条件補正です。単純にはこんなことをしてやりたくなります。
def adjust_periodic_wrong(x):
for i in range(len(x)-1):
if x[i+1] - x[i] > L/2:
x[i+1] -= L
if x[i+1] - x[i] < -L/2:
x[i+1] += L
要するに、一つ前の座標と
x = [i - (i // L)*L for i in range(L*3)]
adjust_periodic_wrong(x)
plt.plot(x)
plt.show()
このように、二回境界をまたぐケースでおかしくなります。
正しくはこうしてやる必要があります。
def adjust_periodic(x):
for i in range(len(x)-1):
if x[i+1] - x[i] > L/2:
x[i+1] -= (x[i+1] - x[i]+L/2)//L*L
if x[i+1] - x[i] < -L/2:
x[i+1] += (x[i] - x[i+1]+L/2)//L*L
何回境界をまたいだかを計算し、その分だけ補正してやります。結果はこうなります。
x = [i - (i // L)*L for i in range(L*3)]
adjust_periodic(x)
plt.plot(x)
plt.show()
ちゃんとまっすぐになりましたね。
念のため逆方向も確認しておきましょう(基本)。
x = [-i - (-i // L)*L for i in range(L*3)]
plt.plot(x)
plt.show()
adjust_periodic(x)
plt.plot(x)
plt.show()
大丈夫そうです。
ランダムウォーク
時系列の作成
それでは、ランダムウォークのデータを作りましょう。単に毎ステップ、-1か+1にランダムで進むだけのコードです。ただし、周期境界条件により
def diffusion(step):
x = np.zeros(step)
pos = 0.0
for i in range(step):
pos += random.choice([-1,1])
if pos < 0:
pos += L
if pos > L:
pos -= L
x[i] = pos
return x
実行結果はこんな感じです。
random.seed(12345)
x = diffusion(2**12)
plt.plot(x)
plt.show()
周期境界によりよくわからなくなっています。補正してやりましょう。
adjust_periodic(x)
plt.plot(x)
plt.show()
それっぽくなりました。以後、この軌跡について平均二乗変位を計算します。
シンプルな計算
まずは、定義式
をそのまま計算してやりましょう。こうなるでしょうか。
def calc_msd_simple(x):
n = len(x)
msd = []
for s in range(1,n//4):
x2 = 0.0
for i in range(n-s):
x2 += (x[i+s]-x[i])**2
x2 /= (n-s)
msd.append(x2)
return msd
そのままなので、特に難しくはないと思います。ただし、
平均二乗変位をプロットしてやりましょう。ついでに時間を測ってみます。
%%time
msd_simple = calc_msd_simple(x)
plt.plot(msd_simple)
plt.show()
CPU times: user 3.32 s, sys: 16.7 ms, total: 3.34 s
Wall time: 3.34 s
3.34秒かかりました。
NumPyっぽく計算
シンプルな計算では遅すぎるので、ちょっとPythonっぽく計算してみましょう。欲しいのは、インデックスがs
だけずれた座標の差です。PythonはN個の要素を持つ配列a
にたいしてa[s:]
とするとa[s]
.a[s+1]
,...a[N-1]
の部分配列を、a[:-s]
とすると、a[0]
,a[1]
,...,a[N-s-1]
の部分配列を返します。
a = np.arange(10)
a[2:] # => array([2, 3, 4, 5, 6, 7, 8, 9])
a[:-2] # => array([0, 1, 2, 3, 4, 5, 6, 7])
なので、a[s:] - a[:-s]
により、s
だけ離れたインデックスの差の配列を得ることができます。差を計算するのも二乗を計算するのもNumPy配列の演算になるので、高速化が期待できます。コードに落とすとこんな感じになるでしょう。
def calc_msd_np(x):
n = len(x)
msd = []
for s in range(1,n//4):
dx = x[s:] - x[:-s]
msd.append(np.average(dx**2))
return msd
実行して時間を測ってみます。
%%time
msd_np = calc_msd_np(x)
plt.plot(msd_np)
plt.show()
CPU times: user 151 ms, sys: 0 ns, total: 151 ms
Wall time: 156 ms
実行時間は156 msと、かなり高速化されました。
念のために二つを重ねてみましょう。
fig, ax = plt.subplots()
ax.plot(msd_simple,label="Simple")
ax.plot(msd_np,label="NumPy")
ax.legend()
完全に一致していますね。
フーリエ変換
さて、フーリエ変換のお時間です。何かを高速に計算しようとすると、やたらと顔を出す高速フーリエ変換(Fast Fourier Transform, FFT)ですが、ここにも顔を出してきます。まずは平均二乗変位の定義をバラします。
まず、
s1 = np.average(x**2)*2
と計算できます。x
をフーリエ変換し、その絶対値の二乗を計算してから逆フーリエ変換してやると自己相関関数が求まります。以上を実装してやるとこうなります。
def calc_msd_fft(x):
n=len(x)
fk = np.fft.fft(x, n=2*n)
power = fk * fk.conjugate()
res = np.fft.ifft(power)[:n].real
s2 = res/(n*np.ones(n)-np.arange(0,n))
s1 = np.average(x**2)
msd = 2*s1 - 2*s2
return msd[:n//4]
実行しましょう。
%%time
msd_fft = calc_msd_fft(x)
plt.plot(msd_fft)
CPU times: user 17.7 ms, sys: 989 µs, total: 18.7 ms
Wall time: 20.5 ms
20.5 msとなりました。早いですね。
念のため、ナイーブな方法で得られたものと重ねてみましょう。
fig, ax = plt.subplots()
msd_np = calc_msd_np(x)
msd_fft = calc_msd_fft(x)
ax.plot(msd_np,label="NumPy")
ax.plot(msd_fft,label="FFT")
ax.legend()
plt.show()
ちょっとずれてしまいました。これは、
でしたが、これを、
と近似しました。これはわりと良い近似なので別にこのままでも良い気がしますが、気になるならちゃんと計算しましょう。先ほど触れた「ちょっとずらした和」を使えば簡単です。ただし、差が0のところだけは別扱いにする必要があります。
s1 = np.zeros(n)
s1[0] = np.average(x2)*2.0
for m in range(1,n):
s1[m] = np.average(x2[m:] + x2[:-m])
これを使うとこうなるでしょう。
def calc_msd_fft2(x):
n=len(x)
fk = np.fft.fft(x, n=2*n)
power = fk * fk.conjugate()
res = np.fft.ifft(power)[:n].real
s2 = res/(n*np.ones(n)-np.arange(0,n))
x2 = x**2
s1 = np.zeros(n)
s1[0] = np.average(x2)*2.0
for m in range(1,n):
s1[m] = np.average(x2[m:] + x2[:-m])
msd = s1 - 2*s2
return msd[:n//4]
重ねてみましょう。
fig, ax = plt.subplots()
msd_np = calc_msd_np(x)
msd_fft2 = calc_msd_fft2(x)
ax.plot(msd_fft2,label="FFT2")
ax.plot(msd_np,label="NumPy")
ax.legend()
plt.show()
ぴったり一致しました。
まとめ
平均二乗変位をフーリエ変換を使って計算するサンプルを紹介しました。実際に分子動力学計算などで得られた結果を解析するには、周期境界条件補正が必要となります。補正してしまえば、あとは普通にフーリエ変換するだけです。なんとなく「自己相関関数はフーリエ変換で求められるんだったな」「平均二乗変位もフーリエ変換で求められるんだったな」と覚えていても、実際に書こうとすると「あれ?」となるものです。っていうかなりました。
この記事が誰かの役に立てば幸いです。
Discussion