はじめに
この記事は,Physics Advent Calendar 2017 25日目の記事です。この記事では,できる限り簡単(?)に,Hartree-Fock(ハートリー・フォック)法を解説していきます。
量子力学における多体問題(量子多体問題)とHartree-Fock法
Physics Advent Calendar 2017 3日目の,adhara_mathphysさんによる非相対論的水素原子における力学的対称代数の記事にあるように,水素原子は高度な力学的対称性を有するため,水素原子に対する(非相対論的)Schrödinger方程式は何通りもの方法で,解析的に解くことができます(参考サイト[1])。
しかし一般に,ヘリウム原子などの多電子原子,あるいは分子においては,この対称性が破れることによって可積分系ではなくなるため,これらの系に対するSchrödinger方程式を解析的に解くことは不可能です。これは,量子多体問題と呼ばれています(参考サイト[2])。そこで,これらの系に対するSchrödinger方程式を解く(エネルギー固有状態を求める)ためには,何らかの近似を行わなければなりませんが,Hartree-Fock法はそのような近似の一つです。
なおこの記事では,(特殊)相対論効果,ラムシフト,超微細構造などについては無視します(これらを扱っていると,それだけで記事がいくつも必要になります)。
Hartree-Fock法のあらまし
Hartree-Fock法(またはHartree-Fock近似)とは,多電子原子,または分子に対するSchrödinger方程式を解くために,V. A. Fockによって考案された方法であり,その際に現れる方程式をHartree-Fock方程式と呼びます(参考サイト[3])。
この方法は,N個の電子に対して適切に反対称化した行列積の波動関数を作り,相互作用を全て取り入れたHamiltonianに対して,全エネルギーを極小にする単一の行列積を探す,というものです。と言っても何のことか分からないと思いますが,要は,多体問題を一体問題に帰着させる方法です。つまり,(量子)多体問題が解けないなら,何らかの近似で,それを(比較的解きやすい)一体問題に置き換えちゃえばいいじゃない,ということです。
ヘリウム原子へのHartree-Fock法の「考え方」の適用
ヘリウム原子は,原子核1個と電子2個を有する系であり,最も単純な多電子原子です。Hartree-Fock法について一般的な議論をする前に,ヘリウム原子についてHartree-Fock法の「考え方」を適用することは有用でしょう。また,その際に現れる方程式の解き方についても,以下で示したいと思います。
Born-Oppenheimer近似
多電子原子や分子に対して,Hartree-Fock法を適用する際にまず重要なことは,(原子)核の取り扱いです。通常のHartree-Fock法においては,電子が核に相対的に運動している間は,核が「静止」していると見なし,電子と核の運動を分離します。つまり,(電子の運動について考えている間は)核の運動については一切考えません。これをBorn-Oppenheimer(ボルン・オッペンハイマー)近似と呼びます。断熱近似という用語もあり,しばしば,Born-Oppenheimer近似と同義として用いられますが,厳密には両者は異なります(参考サイト[4])。なお,水素原子の場合には,電子と核の運動は厳密に分離できます(参考サイト[5])。
ヘリウム原子に対するSchrödinger方程式
それでは早速,ヘリウム原子に対するSchrödinger方程式を書き下してみましょう。ヘリウム原子に対するSchrödinger方程式は,Born-Oppenheimer近似を用いると以下のように書けます(電子の質量とディラック定数を1とする,Hartree原子単位系(参考サイト[6])を用いていることに注意してください)。
ただし,座標および は,空間位置座標及びスピン座標をひとまとめにしたです。またここで,左辺括弧の中の第一項は電子1の運動エネルギーポテンシャル,同第二項は電子2の運動エネルギーポテンシャル,同第三項は核と電子1間のCoulombポテンシャル,同第四項は核と電子2間のCoulombポテンシャル,同第五項は電子1と電子2間のCoulombポテンシャル(この項が問題)です。
左辺括弧の中の第五項があるせいで,式を変数分離して解析的に解くのは不可能なのですが,ひとまずこの項を無視して,波動関数が以下のように変数分離できると仮定してみましょう。
式はHartree積と呼ばれますが,これは,第一近似としては悪くありません(実際,この方法はHartree近似(参考サイト[7])として知られています)。しかし式は,電子はフェルミオンであるので,波動関数は2個の座標およびに対して反対称でなければならない(とを入れ替えたら,波動関数の符号が逆転しなければならない),という要請を満たしていません。式の右辺を,この要請を満たすように改良するのは簡単で,それは以下のように書けるでしょう。
式は,とを入れ替えると,波動関数の符号が逆転します(係数は,波動関数の規格化に必要です)。従って確かに,2個の座標およびに対して反対称になっています。あるいは,ヘリウム原子では,軌道を,2個の電子が占有していることを考慮すれば,
と書くことも可能でしょう。ここで,は上向きスピン,は下向きスピンの波動関数です。さて,式の(近似)波動関数を,式に代入してみましょう(なお,これは変分法であり,式の波動関数は試行関数に対応します。以下では,式の波動関数を試行関数と表記します)。ここで,ヘリウム原子に対するSchrödinger方程式のHamiltonian
は,スピンには全く作用しないので,スピンに依存する部分はSchrödinger方程式の両辺で落ちることを用いると,試行関数を式に代入した結果の式は,以下のようになります。
より簡単な式を得るため,式の両辺に左からを乗じて,について積分し,に関する依存性を取り除くと,以下の式が得られます。
ここで,積分して定数になる項(すなわちに依存しない項)はに含めました。すなわち,
です(ただし,は1に規格化されていることを用いました)。ここで重要なことは,式のSchrödinger方程式が,自己無撞着問題の形をしていることです。すなわち,はこのSchrödinger方程式の解ですが,このSchrödinger方程式はまた,左辺括弧の中の第三項が存在するために,自身にも依存しています。従って,このSchrödinger方程式は非線形偏微分方程式となっており,(すでに大胆な近似を行ったにもかかわらず)解析的には解けません。従って,何らかの方法で数値的に解かなければなりません。
ヘリウム原子に対するSchrödinger方程式の数値的解法と基底による解法
式のSchrödinger方程式は,差分法,あるいは有限要素法などで,完全に数値的に解くことも可能です。ここで,式は,ポテンシャルが球対称であるため,動径方向の常微分方程式と角度方向の偏微分方程式に厳密に分離できることを用いれば,式は,単なる常微分方程式に還元され,数値的に解くことがより容易になります(角度方向の偏微分方程式の解は,球面調和関数として解析的に得られます)。ヘリウム原子において,(動径方向の)常微分方程式を完全に数値的に解いたコードとしては,例えば拙作のschracがあります。
しかし一般に,Hartree-Fock方程式を完全に数値的に解くことは不可能です(参考サイト[3],参考文献[1])。従って,Hartree-Fock方程式を解く際は,専ら,基底で展開して行列方程式に変換して解くという方法が用いられます(この方法によって,行列方程式に変換されたHartree-Fock方程式は,Roothaan(ローターン)方程式と呼ばれます)。ここで式を,基底で展開して行列方程式に変換して解くことは,Roothaan方程式の一般的な議論をする前の導入として有用でしょう。従って以下では,この方法について説明します。
基底の導入
ヘリウム原子のことは一旦忘れ,基底での展開によって微分方程式が行列方程式に変換できることを,簡単な例を挙げて具体的に説明したいと思います。まず,波動関数が,以下のように2個の基底関数の線形結合で表せる(2個の基底で展開できる)と仮定してみましょう。
このを,Schrödinger方程式に代入すると,
となります。さらに,左からを乗じると,
となります。何も難しいことはありません。ここで,と,を実関数に限定すると,とも実数となり,従って式は,
となります。これを一旦展開します。
さらにこれを全空間で積分すると,
となります。ごちゃごちゃしていますが,難しいことは何もしていません。ここで,式を以下のような「連立方程式」に書き直します。
式が成り立つのなら,式が成立することは自明です。ここで,この連立方程式をとでそれぞれ除し,さらに行列形式で書き直すと,以下の式が得られます。
ここで,
とおくと(は重なり行列と呼ばれます),
なる行列方程式が得られます。式は一般化固有値問題となっており,が(エネルギー)固有値で,が固有ベクトルです(なお,基底が正規直交系であれば,は単位行列となり,式は通常の固有値問題になります)。さてこれで,当初の目的である,「微分方程式を行列方程式に変換する」ことが達成できました。つまり,波動関数を,2個の基底で展開すれば,微分方程式から行列方程式への変換が可能だということが分かったわけです。
ここで一般の場合,つまり波動関数を任意の個数(個)の基底で展開した場合を考えます。この場合,波動関数は以下のように書けるでしょう。
この場合も,全く同様の手順によって式が得られます(ただし,行列および重なり行列が,次正方行列となることと,固有ベクトルが次元ベクトルとなることに注意してください)。
基底関数の種類
前節で,波動関数を任意の個数の基底で展開することで,微分方程式を行列方程式に変換できることを見ました。しかし,肝心の基底関数には一体どのような形の関数を選べば良いのでしょうか。結論から言うと,Hartree-Fock法で最も多用されている基底関数は,ガウス関数()であり,これはGTO(Gaussian Type Orbital,ガウス型軌道)と呼ばれています(これが多用されている理由は後述します)。また,型の関数も用いられており,これはSTO(Slater Type Orbital,スレーター型軌道)と呼ばれます。他には,Bessel関数や,B-splineなどが基底関数として用いられることもあります。今は(暗黙に)非周期的かつ有限の系を考えているのですが,平面波(平たく言えば三角関数)を基底関数とすることも可能のようです(参考文献[3])。
(7)式の基底による展開
ヘリウム原子の話に戻って,式のとを,以下のようにn個の基底で展開してみましょう。
式に,式および式を代入すると,以下の式が得られます。
さらに式に,左からを乗じ,について全空間で積分すると,以下の式が得られます。
ただし,
です。なお,式は1電子積分,式は2電子積分,式は重なり積分と呼ばれます。さらに,
とおくと(を行列と見なしたは,Fock行列と呼ばれます),結局式は,
なる,行列方程式(一般化固有値問題)に帰着できます(やや複雑ですが,やっていることは「基底の導入」の節で説明したことと全く同じです)。ここまでくれば,あとは具体的に積分を計算して,行列要素を埋め,一般化固有値問題を数値的に解くだけです。ここで,「具体的に積分を計算して,行列要素を埋める」ためには,具体的な基底関数の形を決めなければなりませんが,水素原子の1s軌道の波動関数がの形,つまりSTOの形をしていたことを思い出せば,ヘリウム原子の波動関数も,基底にSTOを用いて展開できると考えるのが自然でしょう。
が,基底にSTOを用いるのには問題があります。基底にSTOを使うと,一般的には2電子積分が解析的に求められないのです(参考文献[4]によると,ヘリウム原子の場合は例外で,解析的に求められるらしいです)。一方,基底にGTOを使うと,2電子積分を解析的に求めることが可能になります。これが,Hartree-Fock法で基底としてGTOが多用されている理由です。
GTOで1電子積分,2電子積分および重なり積分を求める
基底にGTOを用いると,基底状態については,の関数(s関数)だけが必要であり,これは,原子核に中心がある(そのためそこを原点に置く)ので,は,以下のようになります。
式を用いると,ヘリウム原子における,1電子積分と2電子積分,および重なり積分の要素は,以下のように求められます(計算は省略します)。
ここで,具体的なの値についてですが,参考文献[2]に,の場合(とを4つのGTOで展開した場合)の値が記載されているので,それをそのまま引用します(なお一般にの値は,非線形の最適化問題を解くことによって求められますが,ここではその方法については立ち入りません)。その値は,
です。
自己無撞着問題の解法
前節で,行列要素を埋めるための情報が全て得られたので,後は式の一般化固有値問題を数値的に解くだけ…と言いたいところですが,そうは問屋が卸しません。式の第二項を見てください。この項のとは,式の一般化固有値問題の解である,固有ベクトルの要素です。つまり,問題の「答え」が,問題を解くために必要なのです。これは,「ヘリウム原子に対するSchrödinger方程式」の節で述べたように,式が自己無撞着問題だったからです。じゃあどうすんねん,と突っ込みたくなりますが,一応解決法はあります。どうするかというと,以下のような手続きを踏みます。
- 固有ベクトルの要素の初期値を適当に選びます。これは例えば,全部とか,あるいは全部とかで構いません(今はヘリウム原子という単純な系を考えているので,たいていの初期値から正しい解に収束するはずです)。また,1電子積分と2電子積分,および重なり積分の要素も計算しておきます。
- このから,式によって,Fock行列を計算します。
- 式の一般化固有値問題を数値的に解きます。
- エネルギー
を求めます(との関係については,式を思い出してください。ここで,
は,系の対称性から自明です)。
5. ステップ3で求まった固有ベクトルを用いて,式からFock行列を計算します。
6. ステップ3~5を繰り返します。
ここで,このままでは無限ループになってしまうので,どこかで計算を打ち切らなくてはなりません。通常は,前回計算したエネルギーと,今回計算したエネルギーの差の絶対値が,ある閾値未満になったところで計算を打ち切ります。なお,この計算法を,SCF(Self-Consistent Field,自己無撞着場)計算と呼びます。
ヘリウム原子のエネルギーを求めるC++のプログラム
上述の方法によって,ヘリウム原子の(基底状態の)エネルギーを求める,C++17のプログラムは以下のようになります(多次元配列のためにBoost C++ Librariesを,また一般化固有値問題の解を求めるためにEigenをそれぞれ使っていますので,注意してください)。なお,このプログラムの完全なソースコードは,GitHubのこちらにあります。
/*! \file helium_hf.cpp
\brief Hartree-Fock法で、ヘリウム原子のエネルギーを計算する
Copyright © 2017 @dc1394 All Rights Reserved.
(but this is originally adapted by Paolo Giannozzi for helium_hf_gauss.c from http://www.fisica.uniud.it/~giannozz/Corsi/MQ/Software/C/helium_hf_gauss.c )
This software is released under the BSD 2-Clause License.
*/
#include <cmath> // for std::pow, std::sqrt
#include <cstdint> // for std::int32_t
#include <iostream> // for std::cerr, std::cin, std::cout
#include <optional> // for std::make_optional, std::optional, std::nullopt
#include <valarray> // for std::valarray
#include <boost/assert.hpp> // for BOOST_ASSERT
#include <boost/format.hpp> // for boost::format
#include <boost/math/constants/constants.hpp> // for boost::math::constants::pi
#include <boost/multi_array.hpp> // for boost::multi_array
#include <Eigen/Core> // for Eigen::MatrixXd, Eigen::VectorXd
#include <Eigen/Eigenvalues> // for Eigen::GeneralizedSelfAdjointEigenSolver
namespace {
//! A global variable (constant expression).
/*!
バッファサイズの上限
*/
static auto constexpr MAXBUFSIZE = 32;
//! A global variable (constant expression).
/*!
SCF計算のループの上限
*/
static auto constexpr MAXITER = 1000;
//! A global variable (constant expression).
/*!
SCF計算のループから抜ける際のエネルギーの差の閾値
*/
static auto constexpr SCFTHRESHOLD = 1.0E-15;
//! A global function.
/*!
SCF計算を行う
\return SCF計算が正常に終了した場合はエネルギーを、しなかった場合はstd::nulloptを返す
*/
std::optional<double> do_scfloop();
//! A global function.
/*!
nalpha個のGTOによるヘリウム原子のエネルギーを計算する
\param c 固有ベクトルC
\param ep 一般化固有値問題のエネルギー固有値E'
\param h 1電子積分
\return ヘリウム原子のエネルギー
*/
double getenergy(Eigen::VectorXd const & c, double ep, boost::multi_array<double, 2> const & h);
//! A global function.
/*!
使用するGTOの数をユーザに入力させる
\return 使用するGTOの数
*/
std::int32_t input_nalpha();
//! A global function.
/*!
GTOの肩の係数が格納された配列を生成する
\param nalpha 使用するGTOの個数
\return GTOの肩の係数が格納されたstd::vector
*/
std::valarray<double> make_alpha(std::int32_t nalpha);
//! A global function.
/*!
全ての要素が、引数で指定された値で埋められたnalpha次元ベクトルを生成する
\param nalpha 使用するGTOの個数
\param val 要素を埋める値
\return 引数で指定された値で埋められたベクトル (Eigen::VectorXd)
*/
Eigen::VectorXd make_c(std::int32_t nalpha, double val);
//! A global function.
/*!
nalphaの数で、固有ベクトル、1電子積分および2電子積分からFock行列を生成する
\param c 固有ベクトルC
\param h 1電子積分hpq
\param q 2電子積分Qprqs
\return Fock行列 (Eigen::MatrixXd)
*/
Eigen::MatrixXd make_fockmatrix(Eigen::VectorXd const & c, boost::multi_array<double, 2> const & h, boost::multi_array<double, 4> const & q);
//! A global function.
/*!
1電子積分が格納された、nalpha×nalphaの2次元配列を生成する
\param alpha GTOの肩の係数が格納されたstd::vector
\return 1電子積分が格納された2次元配列 (boost::multi_array)
*/
boost::multi_array<double, 2> make_oneelectroninteg(std::valarray<double> const & alpha);
//! A global function.
/*!
nalpha次正方行列の重なり行列を生成する
\param alpha GTOの肩の係数が格納されたstd::vector
\return 重なり行列 (Eigen::MatrixXd)
*/
Eigen::MatrixXd make_overlapmatrix(std::valarray<double> const & alpha);
//! A global function.
/*!
2電子積分が格納されたnalpha×nalpha×nalpha×nalphaの4次元配列を生成する
\param alpha GTOの肩の係数が格納されたstd::vector
\return 2電子積分が格納された4次元配列 (boost::multi_array)
*/
boost::multi_array<double, 4> make_twoelectroninteg(std::valarray<double> const & alpha);
}
int main()
{
if (auto const res(do_scfloop()); res) {
std::cout << boost::format("SCF計算が収束しました: energy = %.14f (Hartree)") % (*res) << std::endl;
return 0;
}
else {
std::cerr << "SCF計算が収束しませんでした" << std::endl;
return -1;
}
}
namespace {
std::optional<double> do_scfloop()
{
// 使用するGTOの数を入力
auto const nalpha(input_nalpha());
// GTOの肩の係数が格納された配列を生成
auto alpha = make_alpha(nalpha);
// 1電子積分が格納された2次元配列を生成
auto const h(make_oneelectroninteg(alpha));
// 2電子積分が格納された4次元配列を生成
auto const q(make_twoelectroninteg(alpha));
// 重なり行列を生成
auto const s(make_overlapmatrix(alpha));
// 全て0.0で初期化された固有ベクトルを生成
auto c(make_c(nalpha, 0.0));
// 新しく計算されたエネルギー
auto enew = 0.0;
// SCFループ
for (auto iter = 1; iter < MAXITER; iter++) {
// Fock行列を生成
auto const f(make_fockmatrix(c, h, q));
// 一般化固有値問題を解く
Eigen::GeneralizedSelfAdjointEigenSolver<Eigen::MatrixXd> es(f, s);
// E'を取得
auto const ep = es.eigenvalues()[0];
// 固有ベクトルを取得
c = es.eigenvectors().col(0);
// 前回のSCF計算のエネルギーを保管
auto const eold = enew;
// 今回のSCF計算のエネルギーを計算する
enew = getenergy(c, ep, h);
std::cout << boost::format("Iteration # %2d: HF eigenvalue = %.14f, energy = %.14f\n") % iter % ep % enew;
// SCF計算が収束したかどうか
if (std::fabs(enew - eold) < SCFTHRESHOLD) {
// 収束したのでそのエネルギーを返す
return std::make_optional(enew);
}
}
// SCF計算が収束しなかった
return std::nullopt;
}
double getenergy(Eigen::VectorXd const & c, double ep, boost::multi_array<double, 2> const & h)
{
auto const nalpha = static_cast<std::int32_t>(c.size());
auto e = ep;
for (auto p = 0; p < nalpha; p++) {
for (auto q = 0; q < nalpha; q++) {
// E = E' + Cp * Cq * hpq
e += c[p] * c[q] * h[p][q];
}
}
return e;
}
std::int32_t input_nalpha()
{
std::int32_t nalpha;
while (true) {
std::cout << "使用するGTOの個数を入力してください (3, 4 or 6): ";
std::cin >> nalpha;
if (!std::cin.fail() && (nalpha == 3 || nalpha == 4 || nalpha == 6)) {
break;
}
std::cin.clear();
std::cin.ignore(MAXBUFSIZE, '\n');
}
return nalpha;
}
std::valarray<double> make_alpha(std::int32_t nalpha)
{
switch (nalpha) {
case 3:
return { 0.31364978999999998, 1.1589229999999999, 6.3624213899999997 };
break;
case 4:
return { 0.297104, 1.236745, 5.749982, 38.2166777 };
break;
case 6:
return { 0.18595935599999999, 0.45151632200000003, 1.1627151630000001, 3.384639924, 12.09819836, 65.984568240000002 };
break;
default:
BOOST_ASSERT(!"switch文のdefaultに来てしまった!");
return std::valarray<double>();
break;
}
}
Eigen::VectorXd make_c(std::int32_t nalpha, double val)
{
Eigen::VectorXd c(nalpha);
// 固有ベクトルCの要素を全てvalで初期化
for (auto i = 0; i < nalpha; i++) {
c[i] = val;
}
return c;
}
Eigen::MatrixXd make_fockmatrix(Eigen::VectorXd const & c, boost::multi_array<double, 2> const & h, boost::multi_array<double, 4> const & q)
{
auto const nalpha = static_cast<std::int32_t>(c.size());
Eigen::MatrixXd f = Eigen::MatrixXd::Zero(nalpha, nalpha);
for (auto p = 0; p < nalpha; p++) {
for (auto qi = 0; qi < nalpha; qi++) {
// Fpq = hpq + ΣCr * Cs * Qprqs
f(p, qi) = h[p][qi];
for (auto r = 0; r < nalpha; r++) {
for (auto s = 0; s < nalpha; s++) {
f(p, qi) += c[r] * c[s] * q[p][r][qi][s];
}
}
}
}
return f;
}
boost::multi_array<double, 2> make_oneelectroninteg(std::valarray<double> const & alpha)
{
using namespace boost::math::constants;
auto const nalpha = static_cast<std::int32_t>(alpha.size());
boost::multi_array<double, 2> h(boost::extents[nalpha][nalpha]);
for (auto p = 0; p < nalpha; p++) {
for (auto q = 0; q < nalpha; q++) {
// αp + αq
auto const appaq = alpha[p] + alpha[q];
// hpq = 3αpαqπ^1.5 / (αp + αq)^2.5 - 4π / (αp + αq)
h[p][q] = 3.0 * alpha[p] * alpha[q] * std::pow((pi<double>() / appaq), 1.5) / appaq -
4.0 * pi<double>() / appaq;
}
}
return h;
}
Eigen::MatrixXd make_overlapmatrix(std::valarray<double> const & alpha)
{
using namespace boost::math::constants;
auto const nalpha = static_cast<std::int32_t>(alpha.size());
Eigen::MatrixXd s = Eigen::MatrixXd::Zero(nalpha, nalpha);
for (auto p = 0; p < nalpha; p++) {
for (auto q = 0; q < nalpha; q++) {
// Spq = (π / (αp + αq))^1.5
s(p, q) = std::pow((pi<double>() / (alpha[p] + alpha[q])), 1.5);
}
}
return s;
}
boost::multi_array<double, 4> make_twoelectroninteg(std::valarray<double> const & alpha)
{
using namespace boost::math::constants;
auto const nalpha = static_cast<std::int32_t>(alpha.size());
boost::multi_array<double, 4> q(boost::extents[nalpha][nalpha][nalpha][nalpha]);
for (auto p = 0; p < nalpha; p++) {
for (auto qi = 0; qi < nalpha; qi++) {
for (auto r = 0; r < nalpha; r++) {
for (auto s = 0; s < nalpha; s++) {
// Qprqs = 2π^2.5 / [(αp + αq)(αr + αs)√(αp + αq + αr + αs)]
q[p][r][qi][s] = 2.0 * std::pow(pi<double>(), 2.5) /
((alpha[p] + alpha[qi]) * (alpha[r] + alpha[s]) *
std::sqrt(alpha[p] + alpha[qi] + alpha[r] + alpha[s]));
}
}
}
}
return q;
}
}
なお,このプログラムは,参考サイト[8]のhelium_hf_gauss.cを参考にさせて頂きました。また,およびの場合のの値は,参考サイト[9]の,basis-sto3g.cppおよびbasis-sto6g.cppから引用させて頂きました。
ヘリウム原子のエネルギーを求めるJuliaのプログラム
上述の方法によって,ヘリウム原子の(基底状態の)エネルギーを求める,Juliaのプログラムは以下のようになります。なお,このプログラムの完全なソースコードは,GitHubのこちらにあります。
using Match
using Printf
const DR = 0.01
const MAXR = 30.0
const MAXBUFSIZE = 32
const MAXITER = 1000
const SCFTHRESHOLD = 1.0E-15
function do_scfloop(verbose=true)
# 使用するGTOの数を入力
nalpha = input_nalpha()
# GTOの肩の係数が格納された配列を生成
alpha = make_alpha(nalpha)
# 1電子積分が格納された2次元配列を生成
h = make_oneelectroninteg(alpha)
# 2電子積分が格納された4次元配列を生成
q = make_twoelectroninteg(alpha)
# 重なり行列を生成
s = make_overlapmatrix(alpha)
# 全て0.0で初期化された固有ベクトルを生成
c = make_c(nalpha, 0.0)
# 新しく計算されたエネルギー
enew = 0.0
# SCFループ
f = Symmetric(similar(h))
stmp = similar(s)
for iter = 1:MAXITER
# Fock行列を生成
make_fockmatrix!(f, c, h, q)
# 一般化固有値問題を解く
stmp .= s
eigenval, eigenvec = eigen!(f, stmp)
# E'を取得
ep = eigenval[1]
# 固有ベクトルを取得
c .= @view(eigenvec[:,1])
# 固有ベクトルを正規化
normalize!(alpha, c)
# 前回のSCF計算のエネルギーを保管
eold = enew
# 今回のSCF計算のエネルギーを計算する
enew = getenergy(c, ep, h)
verbose && @printf "Iteration # %2d: HF eigenvalue = %.14f, energy = %.14f\n" iter ep enew
# SCF計算が収束したかどうか
if abs(enew - eold) < SCFTHRESHOLD
# 収束したのでα, c, エネルギーを返す
return alpha, c, enew
end
end
# SCF計算が収束しなかった
return nothing
end
function getenergy(c, ep, h)
nalpha = length(c)
e = ep
@inbounds @simd for q = 1:nalpha
for p = 1:nalpha
# E = E' + Cp * Cq * hpq
e += c[p] * c[q] * h[p, q]
end
end
return e
end
function input_nalpha()
nalpha = nothing
while true
@printf "使用するGTOの個数を入力してください (3, 4 or 6): "
s = readline()
nalpha = tryparse(Int64, s)
if nalpha != nothing
@match nalpha begin
3 => break
4 => break
6 => break
_ => continue
end
end
end
return nalpha
end
function make_alpha(nalpha)
return @match nalpha begin
3 => [0.31364978999999998, 1.1589229999999999, 6.3624213899999997]
4 => [0.297104, 1.236745, 5.749982, 38.2166777]
6 => [0.18595935599999999, 0.45151632200000003, 1.1627151630000001, 3.384639924, 12.09819836, 65.984568240000002]
_ => []
end
end
function make_c(nalpha, val)
return fill(val, nalpha)
end
function make_fockmatrix!(f, c, h, q)
nalpha = length(c)
f .= 0
@inbounds for qi = 1:nalpha
for p = 1:qi
# Fpq = hpq + ΣCr * Cs * Qprqs
f.data[p, qi] = h[p, qi]
for s = 1:nalpha
for r = 1:nalpha
f.data[p, qi] += c[r] * c[s] * q[p, r, qi, s]
end
end
end
end
end
function make_oneelectroninteg(alpha)
nalpha = length(alpha)
h = Symmetric(zeros(nalpha, nalpha))
@inbounds for q = 1:nalpha
for p = 1:q
# αp + αq
appaq = alpha[p] + alpha[q]
# hpq = 3αpαqπ^1.5 / (αp + αq)^2.5 - 4π / (αp + αq)
h.data[p, q] = 3.0 * alpha[p] * alpha[q] * ((pi / appaq) ^ 1.5) / appaq -
4.0 * pi / appaq
end
end
return h
end
function make_overlapmatrix(alpha)
nalpha = length(alpha)
s = Symmetric(zeros(nalpha, nalpha))
@inbounds for q = 1:nalpha
for p = 1:q
# Spq = (π / (αp + αq))^1.5
s.data[p, q] = (pi / (alpha[p] + alpha[q])) ^ 1.5
end
end
return s
end
function make_twoelectroninteg(alpha)
nalpha = length(alpha)
q = Array{Float64}(undef, nalpha, nalpha, nalpha, nalpha)
@inbounds for p = 1:nalpha
for qi = 1:nalpha
for r = 1:nalpha
for s = 1:nalpha
# Qprqs = 2π^2.5 / [(αp + αq)(αr + αs)√(αp + αq + αr + αs)]
q[p, r, qi, s] = 2.0 * (pi ^ 2.5) /
((alpha[p] + alpha[qi]) * (alpha[r] + alpha[s]) *
sqrt(alpha[p] + alpha[qi] + alpha[r] + alpha[s]))
end
end
end
end
return q
end
function normalize!(alpha, c)
nalpha = length(c)
sum = 0.0
@inbounds @simd for i = 1:nalpha
for j = 1:nalpha
sum += c[i] * c[j] / (4.0 * (alpha[i] + alpha[j])) * (pi / (alpha[i] + alpha[j])) ^ 0.5
end
end
c ./= sqrt(4.0 * pi * sum)
end
プログラムの実行結果
前節で紹介したプログラムを実行した結果は,以下の画像のようになります。
で,固有ベクトルの要素の初期値がすべて0の場合,が,閾値(今の場合)未満になるまでに,29回のループを要していることが分かります。
プログラムによるヘリウム原子のエネルギーの計算結果の結果と考察
ここで,このプログラムによる,,のときのエネルギーの計算結果を表に示しますと,以下のようになります(ついでに,Hartree-Fock極限(この値は参考文献[5]より引用)と,厳密な値(この値は参考文献[6]より引用)も併せて,以下の表に示します)。
n | 計算結果 (Hartree) |
---|---|
3 | -2.8162 |
4 | -2.8552 |
6 | -2.8600 |
Hartree-Fock極限 | −2.8617 |
厳密 | −2.9037 |
上の表のように,が大きくなるほど,Hartree-Fock極限の値に近づくことが分かります。ここで,Hartree-Fock極限というのは,おおざっぱに言うと,を無限に大きくしていったときに収束する値のことです(正確には,Hartree-Fock極限に収束させるためには,ノードが異なる基底関数も必要です)。また,厳密な値とHartree-Fock極限との差のエネルギー(-0.042 (Hartree))のことを相関エネルギーと呼び,これは式で,を,(×スピン部分)と変数分離してしまったことに起因する,誤差のエネルギーです。
ここまで,ヘリウム原子のSchrödinger方程式を,Hartree-Fock法の「考え方」を用いて解く方法を解説してきました。以下では,一般の多電子原子や分子に対するSchrödinger方程式を解くために,この議論を一般化することにします。
Hartree-Fock方程式
N電子系(電子がN個ある系)における,非相関の反対称な波動関数は,以下のような式で表されることが知られています(非相関というのは,多体問題の効果を無視していることを意味しています)。
式は,1929年にJ. C. Slaterによって導かれたので(参考サイト[10]),Slater行列式と呼ばれています。ここで,は1粒子「スピン軌道」であり,空間位置座標の関数とスピン座標の関数の積です(ここで,の添字は軌道の状態とスピンの状態 を表しており,は電子の番号です)。ここで本来は,RHF(Restricted Hartree-Fock,制限ハートリー・フォック)法と,UHF(Unrestricted Hartree-Fock,非制限ハートリー・フォック)法の細かい説明が必要なのですが,ここでは省略し,以下ではRHF法についてのみ述べます。
式を使うと,(核間反発エネルギーを除いた)エネルギー固有値は,以下の式で与えられます。
ただし,添字 に対する総和は,系に存在する核の数にわたってとることにします。ここで,
とおきます。ただし,添字に対する総和は,占有軌道にわたってとることとします。また,式のはCoulomb演算子,式のは交換演算子と呼ばれます。,Coulomb演算子および交換演算子を用いて式を書き直すと,
と多少見通しが良く(?)なります。ここで,添字に対する総和は,と同様に,占有軌道にわたってとります。また式から,以下の方程式が得られます(導出は省略します)。
式が,Hartree-Fock方程式です。ここで,は,Fock演算子と呼ばれており,
です。
Roothaan方程式
すでに述べたように,式のHartree-Fock方程式を完全に数値的に解くことは,一般的には不可能です。従って,有限個の基底を用いて行列方程式に変換して解く必要があります。式および 式と同様に,空間軌道関数は,個の基底関数の線形結合で展開して,
と表すことができます。このとき式は,式と同様に,
なる行列方程式(一般化固有値問題)になります。式は,Roothaan方程式と呼ばれています。ここで,Fock行列の要素は,
で与えられます。ただし,
です。ただし,添字は,軌道を表し,また添字, , , は基底を表します。ここで,を1電子積分,をCoulomb積分, を交換積分と呼びます。また,とをあわせて,2電子積分と呼びます。また,は重なり行列であり,その要素は,
で定義されます。系の(核間反発エネルギーを除いた)全エネルギーについては,密度行列と呼ばれるものを導入すると便利であり,その要素は,
で定義されます。ここで,式を使うと,フォック行列の要素は,
と書けます。従って,系の全エネルギーは,
となります。
ヘリウム原子におけるSlater行列式,Hartree-Fock方程式およびRoothaan方程式
最後に,ヘリウム原子において,Slater行列式,Hartree-Fock方程式およびRoothaan方程式を考え,これらがそれぞれ 式,式および式と等しくなることを確認してみましょう。
ヘリウム原子におけるSlater行列式とHartree-Fock方程式
まず,ヘリウム原子(2原子系)に対するSlater行列式は,
となり,式は,確かに式と全く同じ式となっています。式については,ヘリウム原子の場合,核は1個しか存在せず(),かつ核の電荷は()なので,
となります。ここで,を原点にとりました()。また,Coulomb演算子は,占有軌道が1個なので,総和をとる必要がなく,従って添字は不要であることに注意すると,
です。よって,
となります。同様に,が不要であることに注意すると,交換演算子は,
より,
となります。よって,Fock演算子は,
となります。ここで,添字は不要であることを用いると,式のHartree-Fock方程式は結局,
となりますが,式は,確かに式と全く同じ式となっています(をに,をに,またをに適宜読み替えてください)。つまり式は,式の特別な場合であることが,これで確認できたことになります。
ヘリウム原子におけるRoothaan方程式
次に,ヘリウム原子におけるRoothaan方程式を考えてみましょう。ここで,基底関数は,実関数に限定するものとします。1電子積分は式から容易に導くことができ,
となりますが,式は,式そのものです(をに適宜読み替えてください)。次に2電子積分ですが,ヘリウム原子の場合,Coulomb積分と,交換積分は等しくなります(これは,式で,添字とを入れ替えても,結果が変わらないことから理解できます)。従って,ヘリウム原子におけるFock行列の要素は,
となります(ただし,添字は不要であることと,が実数になることを用いました)。式は,式に他なりません(をに適宜読み替えてください)。さらに,基底関数を,実関数に限定したので,式は,式と全く等しくなります。よって,式が,式の特別な場合であることが,これで確認できました。
さらに,添字が不要であることに注意すると,密度行列の要素は,
となります。ここで,式を式に代入すると,系の全エネルギーは,
となりますが,式は式そのものです。
まとめ
- Hartree-Fock法の考え方を,ヘリウム原子を例にとって説明しました。また,ヘリウム原子に対する試行関数の,基底による展開を例にとって,Roothaan方程式の考え方を説明しました。
- 一般的なHartree-Fock法について,RHF法に限り説明しました。Slater行列式から,Hartree-Fock方程式が導かれることを示しました(実際の導出はしていませんが…)。
- Hartree-Fock方程式の軌道を基底で展開することにより,Roothaan方程式が導かれることを説明しました。
- ヘリウム原子におけるSlater行列式,Hartree-Fock方程式およびRoothaan方程式を導出し,これらが1.で導いた方程式と一致することを確認しました。
この記事の続きをできるだけ簡単にHartree-Fock法を解説してみる part2と題して書きました。よろしければご覧ください。
参考サイト
[1] 水素様原子のエネルギースペクトル解法(その1)〜 Schrödingerによるラゲール陪多項式を用いた波動方程式解法 〜
[2] 多体問題 (量子論) - Wikipedia
[3] ハートリー=フォック方程式 - Wikipedia
[4] 23. Born−Oppenheimer近似と断熱近似 - 「yam@広島大」物理化学Monographシリーズ
[5] 量子力学ホームページ (2007 年度前期) ノート(§4-§6)
[6] 原子単位系 - Wikipedia
[7] ハートリー近似 - Wikipedia
[8] Numerical Methods in Quantum Mechanics
[9] HFCXX - Hartree-Fock C++ code
[10] Slater determinant - Wikipedia
参考文献
[1] A.ザボ, N.S.オストランド『新しい量子化学―電子構造の理論入門〈上〉』東京大学出版会(1987)
[2] J.M.ティッセン 『計算物理学』シュプリンガー・フェアラーク東京(2003)
[3] J. M. Perez-Jorda, Phys. Rev. E. 90, 053307 (2014).
[4] J. T. Zung, J. Chem. Phys. 41, 2888 (1964).
[5] K. Szalewicz and H. J. Monkhorst, J. Chem. Phys. 75, 5785 (1981).
[6] G. Tanner, et al., Rev. Mod. Phys. 72, 497 (2000).
[7] R.M.マーチン 『物質の電子状態 上』シュプリンガー・ジャパン株式会社(2010)
コメント