EXAFS解析の"Thorough Search"法について。
Curve fitting (CF)だと最小のR因子を与えるパラメータ(の組)しか最終結果が得られないが、Thorough search (TS)だと(大雑把に言って)他の極小値もわかる、というもの。
具体的には、R因子に上限(例えばR<0.05等)を設定して、それを満たすパラメータ群の中で、一つのパラメータに注目したヒストグラム(ある幅を持たせた値の中に、条件を満たしたパラメータが何個存在するかをプロットしたもの。R因子の値そのものは関係がない点に注意。)のピークが、CFで言うところの極小値に対応する(つまり、ピークを与えるパラメータが解析結果の候補となる)というもの。
複数の候補が得られるメリットとしては、他の実験事実との整合が付く「より正しい構造パラメータ」を抽出出来るという点である。スペクトルと幾何構造が一対一対応している保証がないので、一体何対応なのかがわかっておくと、そこから条件をかけて絞ることができる。
しかし、「R因子」を見る代わりに「ヒストグラム」に置き換えることは正当化されるのだろうか?少し考察する。
- ヒストグラムのピークがR因子の極小値に対応して良いのか?
R因子がパラメータに対して連続関数であれば(そしてこれは通常満たされると期待して良い)、R因子の極小値においてパラメータによる微分はゼロになる。すなわち、極小値近傍でパラメータを微小変化させてもR因子は変化しないから、極小値を与えるパラメータ近傍もR因子に対する条件を満たしており、ヒストグラムを取った時に極小値近傍のパラメータは全部カウントされることになる。なので、一見良さそうに見える。
しかし、重要なのがパラメータの数である。パラメータの数が少ないとこの方法は使えない。例えばパラメータ1個では、条件を満たすか満たさないかだけでカウントするから、ゼロか定数かになってピーク構造が現れない。パラメータの数が多くなって条件を満たすことが難しくなった時に、極小値に対応したピークがヒストグラムに現れるようになる。
実際、論文のFig.2で示されている、パラメータが2個の場合の模式図では、TSは単に中心を与えていて、最小値はおろか極小値からもズレている。一方、Fig.4辺りで示されているPt L3-edge EXAFSの解析では、パラメータは4個であり、結果もCFとよく対応している。
- ヒストグラムのピークの幅は誤差として評価して良いのか?
前述したことを踏まえると、「ピークが狭ければ狭いほど、『ヒストグラムのピーク』と『R因子の極小値』の対応が良くなる」ということは言えると思う。論文のFig.2のヒストグラムのピークは明らかにlocal minimumを与えるピークより広く、対応が悪いことがわかる。
ただし、R因子が元々パラメータにどれくらい強く依存しているかを見ていないので、そもそも依存性がそんなに強くないパラメータでは元々幅が広いだろうから、「幅が広いからダメ」と直ぐに決め付けることは出来ない。
また、「スペクトルと構造モデルの誤差」そのものはR因子に含まれているので、ヒストグラムのピーク幅が統計的な意味での誤差には対応しないように思う。
- ヒストグラムにすることのメリットについて。
極小値を与えるパラメータ(の組み)の候補が知りたいのであれば、R因子を小さい順に並べて、下からいくつか選べば済むように直観的には感じられる。しかし、前述した通り、極小値近傍のパラメータも小さいR因子を与えるため、並び替えるだけでは極小値が単一なのか複数あるのかがわからないのである。重要なのは、小さいR因子を与えるパラメータ群が近いのか遠いのかを判断することである。その意味において、ヒストグラムは「ピーク構造」という形で各パラメータ群を類別することができる。
以上のように、パラメータの多い複雑な系に対して上手い手法であることがわかる。
一つ注意としては、結局はパラメータフィッティングであるため、TSであれCFと同じようにフィッティングパラメータの数はデータ点の数によって制限を受けているという点である。そのため、複雑な系に対して有効であるが、複雑な系であればパラメータの数は増えるので、やはりパラメータの数をいかに減らすかというところに職人芸が必要であることには変わらない。
単一三角格子の固有値問題。
単一三角格子において、各サイトへのホッピングをで表すと、ハミルトニアンは
と書ける。
このハミルトニアンの固有値と固有ベクトルを求める。
三次方程式の解き方を忘れたので、以下のサイトを参照。
三次方程式の解き方と例題3問 | 高校数学の美しい物語
方程式の有理数解 | 高校数学の美しい物語
要するに、 の係数はなので、解が有理数であるならば、定数次項であるの約数が一つの解になっているはずである。
実際、 で確かに方程式がゼロになる。
よって、 で約分して行けば、
等方的な関数のフーリエ変換。
- 1次元
- 2次元
- 3次元
Gaussianの半値幅。
規格化されたGaussianは次の形で与えられる。
規格化されている(積分値が1になっている)ことは、次のようにして確かめられる。
を用いた表記は、統計学的な処理を行う際に便利である(と想像される)が、直感的にどれくらいの幅を持つのかが分かりにくい。
そこで、半値半幅を用いた表記を考えることにする。
の定義は、次で与えられる。
したがって、の変換は、
数値的に見積もると、となる。半値半幅はよりも気持ちちょっと大きい程度である。
元のGaussianの式をで表すと、
(おそらくLorentz関数でもそうだと思うが、)Gaussian型のピーク形状の場合、半値全幅(文献によっては、を半値全幅の表記に用いる場合もあるため、注意が必要)を分解能として扱うため、Gaussian broadening等で理論値に実験誤差を入れ込む場合には、半値幅の用いた表記の方が便利である。
NiOの反強磁性結晶構造における対称性をspglibを使って求める。
通常の結晶構造であれば、データベースか何かから引っ張って来れるが、それをベースにした超周期構造を作ろうと思うと、対称性(空間群)が変わるため、手でやるには歯が立たない。
そこで、入力した原子座標に対して対称性を見つけてくれる spglib を使って、反強磁性でのNiOの対称性を決定することを試みる。
spglibはC言語、FORTRAN, pythonに対応しているが、いずれにおいても「実行ファイル(今風に言えばアプリケーション)」ではなく、(~libとあるように)ライブラリーとして提供されている。そのためGUIのようなものに結晶構造を入れるようなものではないため、多少プログラミングに慣れている必要がある。
Spglib — spglib 1.12.0 documentation
個人的には、pythonが簡便でオススメである。なので、ここではpythonでの例を紹介する。
Spglib for Python — spglib 1.12.0 documentation
pythonバージョンのインストールは、pipですぐ入る。
pip install spglib
これを使って、まずは通常のNiOの結晶構造で試してみる。
import numpy as np import spglib # lattice constant a = 4.19 # structural information ( [lattice constants], [coordination], [atomic numbers] ) # Caution: it should be a tuple nio = \ ( [ [ a, 0, 0 ], [ 0, a, 0 ], [ 0, 0, a ] ], [ [ 0, 0, 0 ], # Ni [ 0, 0.5, 0.5 ], # Ni [ 0.5, 0, 0.5 ], # Ni [ 0.5, 0.5, 0 ], # Ni [ 0.5, 0, 0 ], # O [ 0, 0.5, 0 ], # O [ 0, 0, 0.5 ], # O [ 0.5, 0.5, 0.5 ] # O ], [28,] * 4 + [8,] * 4 ) # just space group print( spglib.get_spacegroup( nio ) ) #Fm-3m (225) # primitive cell print( spglib.find_primitive( nio ) ) #(array([[0. , 2.095, 2.095], # [2.095, 0. , 2.095], # [2.095, 2.095, 0. ]]), array([[0. , 0. , 0. ], # [0.5, 0.5, 0.5]]), array([28, 8], dtype=int32))
のsuper cell でもちゃんと元の構造の対称性を返してくれる。
# some coordinates are just added 1 to discribe them in the other unit cell # therefore, lattice constans and coordinates should be multipled and divided by 2, respectively nio_sc \ = [ [ ( a, 0, 0 ), ( 0, a, 0 ), ( 0, 0, a ) ], [ [ 0, 0, 0 ], # Ni 0, 0, 0 [ 0, 0.5, 0.5 ], [ 0.5, 0, 0.5 ], [ 0.5, 0.5, 0 ], [ 1, 0, 0 ], # Ni 1, 0, 0 [ 1, 0.5, 0.5 ], [ 1.5, 0, 0.5 ], [ 1.5, 0.5, 0 ], [ 0, 1, 0 ], # Ni 0, 1, 0 [ 0, 1.5, 0.5 ], [ 0.5, 1, 0.5 ], [ 0.5, 1.5, 0 ], [ 0, 0, 1 ], # Ni 0, 0, 1 [ 0, 0.5, 1.5 ], [ 0.5, 0, 1.5 ], [ 0.5, 0.5, 1 ], [ 1, 1, 0 ], # Ni 1, 1, 0 [ 1, 1.5, 0.5 ], [ 1.5, 1, 0.5 ], [ 1.5, 1.5, 0 ], [ 1, 0, 1 ], # Ni 1, 0, 1 [ 1, 0.5, 1.5 ], [ 1.5, 0, 1.5 ], [ 1.5, 0.5, 1 ], [ 0, 1, 1 ], # Ni 0, 1, 1 [ 0, 1.5, 1.5 ], [ 0.5, 1, 1.5 ], [ 0.5, 1.5, 1 ], [ 1, 1, 1 ], # Ni 1, 1, 1 [ 1, 1.5, 1.5 ], [ 1.5, 1, 1.5 ], [ 1.5, 1.5, 1 ], [ 0.5, 0, 0 ], # O 0, 0, 0 [ 0, 0.5, 0 ], [ 0, 0, 0.5 ], [ 0.5, 0.5, 0.5 ], [ 1.5, 0, 0 ], # O 1, 0, 0 [ 1, 0.5, 0 ], [ 1, 0, 0.5 ], [ 1.5, 0.5, 0.5 ], [ 0.5, 1, 0 ], # O 0, 1, 0 [ 0, 1.5, 0 ], [ 0, 1, 0.5 ], [ 0.5, 1.5, 0.5 ], [ 0.5, 0, 1 ], # O 0, 0, 1 [ 0, 0.5, 1 ], [ 0, 0, 1.5 ], [ 0.5, 0.5, 1.5 ], [ 1.5, 1, 0 ], # O 1, 1, 0 [ 1, 1.5, 0 ], [ 1, 1, 0.5 ], [ 1.5, 1.5, 0.5 ], [ 1.5, 0, 1 ], # O 1, 0, 1 [ 1, 0.5, 1 ], [ 1, 0, 1.5 ], [ 1.5, 0.5, 1.5 ], [ 0.5, 1, 1 ], # O 0, 1, 1 [ 0, 1.5, 1 ], [ 0, 1, 1.5 ], [ 0.5, 1.5, 1.5 ], [ 1.5, 1, 1 ], # O 1, 1, 1 [ 1, 1.5, 1 ], [ 1, 1, 1.5 ], [ 1.5, 1.5, 1.5 ] ], [28,] * 32 + [8,] * 32 ] # check whether there is dupulication for i, p1 in enumerate( nio_sc[1] ): for p2 in nio_sc[1][ i+1 : ]: if p1 == p2: print( p1 ) # lattice constants should be twice in 2*2*2 super cell nio_sc[0] = [ list( a_ ) for a_ in np.array( nio_sc[0] ) * 2 ] # coordinate should be divided by 2 nio_sc[1] = [ list( p_ ) for p_ in np.array( nio_sc[1] ) / 2 ] nio_sc = tuple( nio_sc ) print( spglib.get_spacegroup( nio_sc ) ) print( spglib.find_primitive( nio_sc ) )
座標の重複があると、'None'が返って来るため、チェックする必要がある。目で見て一生懸命探すのには限界がある。
最後に反強磁性だが、格子定数と座標はsuper cellのままでよく、原子番号で反強磁性秩序の情報を入れる。仮に磁化軸方向と平行な原子をNi(28)として、反平行なものを仮にPd(46)とする。酸素はそのままで良い。
NiOの反強磁性磁化軸は[111]方向なので、それを考慮して原子番号を対応させていく。
nio_af \ = [ [ ( a, 0, 0 ), ( 0, a, 0 ), ( 0, 0, a ) ], [ [ 0, 0, 0 ], # Ni 0, 0, 0 [ 0, 0.5, 0.5 ], [ 0.5, 0, 0.5 ], [ 0.5, 0.5, 0 ], [ 1, 0, 0 ], # Ni 1, 0, 0 [ 1, 0.5, 0.5 ], [ 1.5, 0, 0.5 ], [ 1.5, 0.5, 0 ], [ 0, 1, 0 ], # Ni 0, 1, 0 [ 0, 1.5, 0.5 ], [ 0.5, 1, 0.5 ], [ 0.5, 1.5, 0 ], [ 0, 0, 1 ], # Ni 0, 0, 1 [ 0, 0.5, 1.5 ], [ 0.5, 0, 1.5 ], [ 0.5, 0.5, 1 ], [ 1, 1, 0 ], # Ni 1, 1, 0 [ 1, 1.5, 0.5 ], [ 1.5, 1, 0.5 ], [ 1.5, 1.5, 0 ], [ 1, 0, 1 ], # Ni 1, 0, 1 [ 1, 0.5, 1.5 ], [ 1.5, 0, 1.5 ], [ 1.5, 0.5, 1 ], [ 0, 1, 1 ], # Ni 0, 1, 1 [ 0, 1.5, 1.5 ], [ 0.5, 1, 1.5 ], [ 0.5, 1.5, 1 ], [ 1, 1, 1 ], # Ni 1, 1, 1 [ 1, 1.5, 1.5 ], [ 1.5, 1, 1.5 ], [ 1.5, 1.5, 1 ], [ 0.5, 0, 0 ], # O 0, 0, 0 [ 0, 0.5, 0 ], [ 0, 0, 0.5 ], [ 0.5, 0.5, 0.5 ], [ 1.5, 0, 0 ], # O 1, 0, 0 [ 1, 0.5, 0 ], [ 1, 0, 0.5 ], [ 1.5, 0.5, 0.5 ], [ 0.5, 1, 0 ], # O 0, 1, 0 [ 0, 1.5, 0 ], [ 0, 1, 0.5 ], [ 0.5, 1.5, 0.5 ], [ 0.5, 0, 1 ], # O 0, 0, 1 [ 0, 0.5, 1 ], [ 0, 0, 1.5 ], [ 0.5, 0.5, 1.5 ], [ 1.5, 1, 0 ], # O 1, 1, 0 [ 1, 1.5, 0 ], [ 1, 1, 0.5 ], [ 1.5, 1.5, 0.5 ], [ 1.5, 0, 1 ], # O 1, 0, 1 [ 1, 0.5, 1 ], [ 1, 0, 1.5 ], [ 1.5, 0.5, 1.5 ], [ 0.5, 1, 1 ], # O 0, 1, 1 [ 0, 1.5, 1 ], [ 0, 1, 1.5 ], [ 0.5, 1.5, 1.5 ], [ 1.5, 1, 1 ], # O 1, 1, 1 [ 1, 1.5, 1 ], [ 1, 1, 1.5 ], [ 1.5, 1.5, 1.5 ] ], [ 28, 46, 46, 46 ] + [ 46, 28, 28, 28 ] * 3 + [ 28, 46, 46, 46 ] * 3 + [ 46, 28, 28, 28 ] + [ 8, ] * 32 ] # check whether there is dupulication for i, p1 in enumerate( nio_af[1] ): for p2 in nio_af[1][ i+1 : ]: if p1 == p2: print( p1 ) # lattice constants should be twice in 2*2*2 super cell nio_af[0] = [ list( a_ ) for a_ in np.array( nio_af[0] ) * 2 ] # coordinate should be divided by 2 nio_af[1] = [ list( p_ ) for p_ in np.array( nio_af[1] ) / 2 ] nio_af = tuple( nio_af ) print( spglib.get_spacegroup( nio_af ) ) #R-3m (166) print( spglib.find_primitive( nio_af ) ) #(array([[ 1.48138871, 0.85528017, 4.83819526], # [-1.48138871, 0.85528017, 4.83819526], # [ 0. , -1.71056034, 4.83819526]]), array([[-3.70074342e-17, 0.00000000e+00, 0.00000000e+00], # [ 5.00000000e-01, 5.00000000e-01, 5.00000000e-01], # [ 7.50000000e-01, 7.50000000e-01, 7.50000000e-01], # [ 2.50000000e-01, 2.50000000e-01, 2.50000000e-01]]), array([28, 46, 8, 8], dtype=int32))
対称性がFm-3mからR-3mに落ちているのが分かる。
ここで、結晶軸がベクトル表記のままだと結晶構造作成プログラムでは不便な場合があるので、格子定数の形式に変換すると、
prim_nio_af = spglib.find_primitive( nio_af ) axes = prim_nio_af[0] abc = [ np.linalg.norm( axis ) for axis in axes ] print( abc ) #[5.131681011130759, 5.131681011130759, 5.131681011130759] # alpha beta gamma abg =[] abg.append( np.degrees( np.arccos( np.dot( axes[1], axes[2] ) / np.linalg.norm(axes[1]) / np.linalg.norm(axes[2]) ) ) ) abg.append( np.degrees( np.arccos( np.dot( axes[2], axes[0] ) / np.linalg.norm(axes[2]) / np.linalg.norm(axes[0]) ) ) ) abg.append( np.degrees( np.arccos( np.dot( axes[0], axes[1] ) / np.linalg.norm(axes[0]) / np.linalg.norm(axes[1]) ) ) ) print( abg ) #[33.55730976192072, 33.55730976192072, 33.55730976192072]
これで、第一原理計算プログラムで反強磁性が計算できるようになった。
gfortran での変数の次元の制約。
何も考えずにプログラムを作っていたら、配列の次元が7個を超えるとコンパイルできないらしい。
program test_array7 real( kind( 0d0 ) ) :: r( 1, 2, 3, 4, 5, 6, 7, 8 ) end program test_array7
これを gfortran でコンパイルすると
test_array7.f90:3:46: real( kind( 0d0 ) ) :: r( 1, 2, 3, 4, 5, 6, 7, 8 ) 1 Error: Array specification at (1) has more than 7 dimensions
添字が11個ある変数の計算しようとしていたので、これはちょっと困った。。。
構造体とかを使って、何とか逃げるしかないか。。。
ちなみに構造体なら7個以上の要素があっても大丈夫であった。
program test_type7 implicit none type :: test real( kind( 0d0 ) ) :: r1 real( kind( 0d0 ) ) :: r2 real( kind( 0d0 ) ) :: r3 real( kind( 0d0 ) ) :: r4 real( kind( 0d0 ) ) :: r5 real( kind( 0d0 ) ) :: r6 real( kind( 0d0 ) ) :: r7 real( kind( 0d0 ) ) :: r8 end type type( test ) :: t end program test_type7