前回の記事で、水素原子の電子雲の可視化する方法として、3つの方法を紹介しました。
tsujimotter.hatenablog.com
特に、方法2「散布図としてプロット」については、棄却サンプリング法を使った方法を紹介しました。
散布図としてプロットするにあたって、電子の確率分布を表す(3次元の)確率密度関数 にしたがう乱数を得る必要がありました。
棄却サンプリング法は、 とは直接関係ない(たとえば一様乱数のような)確率分布 にしたがう乱数から、 と の確率密度関数としての差を考慮しつつ、適切な確率で乱数を「棄却」することによって、目的の にしたがう乱数を得る手法です。
ところがこの方法、非常に時間がかかることが知られています。実際、上記の散布図を描くだけでかなりの時間を要しました。特に、 と の差が大きければ大きいほど、棄却率が高くなり、乱数を生成するのに時間がかかるというわけです。
今回紹介する メトロポリス・ヘイスティングス法 を用いれば、上記の問題の多くは解決します。
マルコフ連鎖モンテカルロ法(MCMC)という一般的な枠組みの一手法であり、棄却サンプリングのような素朴な手法と比べて、効率的に乱数生成を行うことができます。
これまでこの手の手法(マルコフ連鎖モンテカルロ法)については、話には聞いてはいたものの、なんとなく手が出せないでいました。しかしながら、今回の目的に向けて調べてみると、案外簡単に実装できることがわかりました。(やっぱりモチベーションって大事ですね。)
仕組みはちゃんと理解できているわけではないですが、アルゴリズムの流れと実装方法は理解できたので、電子雲の可視化に応用してみたというのが今回の記事です。
当初の予定では、日曜化学シリーズの記事として書く予定はなかったので、番外編として日曜化学(2.5)としています。よろしければご覧になってください!
メトロポリス・ヘイスティングス法
簡単にアルゴリズムの流れを紹介します。
このアルゴリズムによって生成したいのは、波動関数を として、確率密度関数 にしたがう 個のランダムな3次元ベクトルの列
です。
まず、3次元ベクトル および を変数に持つ、条件付き確率密度関数 を用意します。この を提案分布といいます。
この については、 に対して にしたがうランダムなベクトル を生成できるような関数(乱数が作れる確率密度関数)に決める必要があります。また、あとに示す「条件」を満たすようにすると話が簡単になります。
初期値となる3次元ベクトル を用意します。
からスタートし、 の情報から を生成します。続けて の情報から を生成します。こんな風に の情報から を生成するというのを に対して行います。
(時刻 の情報に基づいて、時刻 における状態の確率が定まる系のことを「マルコフ連鎖」といいます。)
より具体的な生成方法として、先ほど用意した を用いて、 によって、 の候補となる乱数 を生成します。以下の確率 によって、候補 を として採用します。
ただし、 のときは、 とします。
逆に、確率 によって、 を棄却します。この場合、再度同じ手順で を得て、同様に上記の確率で採用するか棄却するかを判定します。
以上を繰り返して、採用されたランダムなベクトルの列
を考えると、これが目的の分布 にしたがうランダムなベクトルの列となっているというわけです。
たったこれだけなので、実装も非常に簡単です。理屈を理解していないので、こんな方法でなぜうまくいくのかはよく分かりませんが、とにかくこれでうまくいくのだそうです。
また、 を決める必要がありますが、これについては多次元正規分布を用いることにします。
ここで、 は分散共分散行列です。変数間の相関を考える必要がないので、単に対角行列を考えれば十分です。
この は「① にしたがうランダムなベクトル を生成する」と「② 式 を計算するのに用いる」という2つの役割があります。
①については、numpyに多次元正規分布にしたがう乱数を生成する関数
np.random.multivariate_normal
がありますので、これを使えば簡単です。具体的な使い方は、こちらのページ を参照。
②については、そもそも計算する必要がないという話があります。というのも、式 の形を見ると、明らかに と で対称的な形になっていますね。すなわち
ということです。
この性質を使うと、式 は次のように劇的に簡単になります:
なるほど、これなら の値は計算する必要がなくなりますね。むしろこうなるように、多次元正規分布を選んだということでしょう。
さらにいうと、棄却率を計算する と という式は、その比がわかれば十分です。つまり、波動関数の規格化定数はわからなくても全く問題ないのです。波動関数の規格化定数の決定は大変面倒だったので、この性質はありがたいですね。
電子雲を可視化するPythonプログラム
そんなわけで、メトロポリス・ヘイスティングス法を用いた可視化のプログラムを紹介します。
例によってPythonのmatplotlibを使っていますので、以下をインストールしておいてください。
- numpy
- matplotlib
また、前回紹介したPlotlyというライブラリを使った可視化もできるようになっています。使いたい人は、Plotlyをインストールした上で、適切にコード内のコメントアウトを外してください。
# メトロポリス・ヘイスティング法による電子分布の可視化 import numpy as np import matplotlib.pyplot as plt import mpl_toolkits.mplot3d.axes3d as axes3d import math # 動径波動関数(ボーア半径を a_0 = 1 に正規化) def radial_wave_function(r, n, l, Z_eff): rho = 2.0 * Z_eff * r / n if n == 1 and l == 0: return (Z_eff ** 1.5) * np.exp(-rho/2.0) * 2.0 elif n == 2 and l == 0: return (Z_eff ** 1.5) * np.exp(-rho/2.0) * (2.0 - rho) / (2.0*np.sqrt(2.0)) elif n == 2 and l == 1: return (Z_eff ** 1.5) * np.exp(-rho/2.0) * rho / (2.0*np.sqrt(6.0)) elif n == 3 and l == 0: return (Z_eff ** 1.5) * np.exp(-rho/2.0) * (6.0 - 6.0*rho + rho**2) / (9.0*np.sqrt(3.0)) elif n == 3 and l == 1: return (Z_eff ** 1.5) * np.exp(-rho/2.0) * (4.0 - rho) * rho / (9.0*np.sqrt(6.0)) elif n == 3 and l == 2: return (Z_eff ** 1.5) * np.exp(-rho/2.0) * (rho**2) / (9.0*np.sqrt(30.0)) # 球面調和関数(ただし、実関数表示したもの) def spherical_harmonics(theta, phi, l, m): if l == 0: if m == 0: # l=0, m=0 return np.sqrt(1.0/(4*np.pi)) if l == 1: if m == 0: # l=1, m=0 return np.sqrt(3.0/(4.0*np.pi)) * np.cos(theta) if m == 1: # l=1, m=+1 return np.sqrt(3.0/(4.0*np.pi)) * np.sin(theta) * np.cos(phi) if m == -1: # l=1, m=+1 return np.sqrt(3.0/(4.0*np.pi)) * np.sin(theta) * np.sin(phi) if l == 2: if m == 0: return np.sqrt(5.0/(16.0*np.pi)) * (3.0*(np.cos(theta)**2) - 1.0) if m == 1: return np.sqrt(15.0/(4.0*np.pi)) * np.cos(theta) * np.sin(theta) * np.cos(phi) if m == -1: return np.sqrt(15.0/(4.0*np.pi)) * np.cos(theta) * np.sin(theta) * np.sin(phi) if m == 2: return np.sqrt(15.0/(16.0*np.pi)) * (np.sin(theta)**2) * np.cos(2*phi) if m == -2: return np.sqrt(15.0/(16.0*np.pi)) * (np.sin(theta)**2) * np.sin(2*phi) # 水素様原子の波動関数 Ψ_{n,l,m}(x, y, z) # # (n,l,m): 量子数 # Z_eff: 有効核電荷 # def f(x, y, z, n, l, m, Z_eff): # 座標系を (x,y,z) -> (r,θ,φ) に変換 r = np.sqrt(x*x + y*y + z*z) theta = 0.0 phi = 0.0 if r > 0: theta = np.arccos( z / r ) if y == 0: if x < 0: phi = np.pi elif x*x+y*y > 0: phi = np.sign(y) * np.arccos(x/np.sqrt(x*x+y*y)) # 動径波動関数と球面調和関数の積を計算して出力 return (radial_wave_function(r, n, l, Z_eff) * spherical_harmonics(theta, phi, l, m)) # 量子数 n = 3 l = 2 m = 0 # 有効核電荷 Z = 1 # 水素原子を想定しているので Z = 1 Z_eff = Z # サンプリング数の設定 N = 5000 # サンプリング目標個数 x_range = 18 # 座標の範囲 (-x_range, x_range) # メトロポリス・ヘイスティング法を使った電子雲のサンプリング num_of_points = 0 # 採択されたデータ数 x = 1 y = 1 z = 1 # 波動関数が正のデータを格納する変数 x1_list = [] y1_list = [] z1_list = [] # 波動関数が負のデータを格納する変数 x2_list = [] y2_list = [] z2_list = [] while num_of_points < N: if num_of_points % 10 == 0: print(num_of_points) # 確率分布 Q(x*|x_t) から次の状態の候補 x* の乱数生成(今回は Q として3次元ガウス分布を選択) values = np.random.multivariate_normal([x, y, z], [[10,0,0],[0,10,0],[0,0,10]], 1) # 乱数生成 x_star = values[0,0] y_star = values[0,1] z_star = values[0,2] f_t = f(x, y, z, n, l, m, Z_eff) # xの波動関数 f_star = f(x_star, y_star, z_star, n, l, m, Z_eff) # x* の波動関数 # 採択率を計算 α = p(x*) / p(x_t) alpha = (f_star ** 2) / (f_t ** 2) # 採択率により決定 r = np.random.rand(1)[0] # 0 <= r <= 1 の一様乱数 r を生成 if r <= alpha: x = x_star y = y_star z = z_star if f_star > 0: x1_list.append(x) y1_list.append(y) z1_list.append(z) else: x2_list.append(x) y2_list.append(y) z2_list.append(z) num_of_points += 1 # figureを生成する X1 = np.array(x1_list) Y1 = np.array(y1_list) Z1 = np.array(z1_list) X2 = np.array(x2_list) Y2 = np.array(y2_list) Z2 = np.array(z2_list) # matplotlibによる可視化 fig = plt.figure(figsize=(8.0, 8.0)) ax = fig.add_subplot(1,1,1, projection='3d') ax.set_xlim3d(-x_range, x_range) ax.set_ylim3d(-x_range, x_range) ax.set_zlim3d(-x_range, x_range) # 軸の設定 ax.set_xlabel("x", size = 14) ax.set_ylabel("y", size = 14) ax.set_zlabel("z", size = 14) ax.plot(X2,Y2,Z2,color='b',marker=".",linestyle='None') ax.plot(X1,Y1,Z1,color='r',marker=".",linestyle='None') plt.show() ''' # Plotlyによる可視化 import plotly.graph_objects as go fig = go.Figure(data=[go.Scatter3d( x = X1, y = Y1, z = Z1, mode='markers', marker = dict( size = 1, color = 'red' ) ), go.Scatter3d( x = X2, y = Y2, z = Z2, mode='markers', marker = dict( size = 1, color = 'blue' ) )]) fig.update_layout( scene = dict( camera=dict(eye=dict(x=1.7, y=-1.7, z=1.7)), #eye=dict(x=1.25, y=1.25, z=1.25)), xaxis = dict(range=[-x_range, x_range]), yaxis = dict(range=[-x_range, x_range]), zaxis = dict(range=[-x_range, x_range]), aspectmode='cube', #this string can be 'data', 'cube', 'auto', 'manual' #a custom aspectratio is defined as follows: aspectratio=dict(x=1, y=1, z=0.95) ), width=700, height=700, ) fig.show() '''
上記のコードは、量子数 に対応する水素原子の波動関数(すなわち、d軌道)を図示するものです。量子数については、以下の部分を変更すれば、違うものをプロットすることができます。
# 量子数 n = 3 l = 2 m = 0
また、デフォルトではサンプル数5000としていますが、これも変更可能です。
実行してみると、冒頭に紹介した以下の図が得られます。
図はPlotlyによるもの
たしかにd軌道の形に電子が分布していますね!!すごい!!
実際実行してみると分かりますが、5000個のサンプリングであっても、ものの数秒で実行完了します。これは棄却サンプリングの結果とは大違いです。
超楽しい!!!
追記:2021.07.12
@dc1394さんという方が開発されている「SchracVisualize_Direct3D_11」をご紹介させてください。水素原子の可視化ツールです。実行ファイルによりローカルで実行でき、とても高速に、綺麗な絵を出力してくれます。(3次元をマウスでグリグリ動かして見ることができて便利です。)
「SchracVisualize_Direct3D_11」がv0.3にバージョンアップしました!
— dc1394 (@dc1394) 2021年7月12日
アルゴリズムの変更(棄却法をメトロポリス・ヘイスティングス法に)により、描画が高速になっています。バイナリは以下URLからダウンロードできます。左図は水素原子の5dxy軌道、右図は5fxz^2軌道です。https://t.co/yyJWi9Sb3H pic.twitter.com/dWkuKHaFJs
このツールは、元々棄却サンプリングを使用されていたそうなのですが、こちらの記事を見てメトロポリス・ヘイスティングス法を使った実装に書き換え、高速化に成功されたそうです。
この方に教えていただいたのですが、メトロポリス・ヘイスティングス法に使っている正規分布について、私は適当に分散 とおいていたのですが、どうやら波動関数の動径分布関数、すなわち
と変数分離した を用いて によって定義された関数を考えて、そのピーク値をとるような を分散に採用すればよいとのことです。
また、3次元正規分布によって乱数生成していた部分も、1次元正規分布を独立に について適用すれば問題ないということもご指摘いただきました。(言われてみれば確かにそうですね。)
教えていただいてありがとうございます!