C++
Julia
物理学
量子力学
量子化学
77
どのような問題がありますか?

この記事は最終更新日から1年以上が経過しています。

投稿日

更新日

できるだけ簡単にHartree-Fock法を解説してみる(C++17とJuliaのコード付き)

はじめに

この記事は,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近似を用いると以下のように書けます(電子の質量mとディラック定数を1とする,Hartree原子単位系(参考サイト[6])を用いていることに注意してください)。

[121212222r12r2+1|r1r2|]ψ(x1,x2)=Eψ(x1,x2)(1)

ただし,座標x1およびx2 は,空間位置座標ri及びスピン座標siをひとまとめにしたxi=(ri,si)です。またここで,左辺括弧の中の第一項は電子1の運動エネルギーポテンシャル,同第二項は電子2の運動エネルギーポテンシャル,同第三項は核と電子1間のCoulombポテンシャル,同第四項は核と電子2間のCoulombポテンシャル,同第五項は電子1と電子2間のCoulombポテンシャル(この項が問題)です。
左辺括弧の中の第五項があるせいで,(1)式を変数分離して解析的に解くのは不可能なのですが,ひとまずこの項を無視して,波動関数ψ(x1,x2)が以下のように変数分離できると仮定してみましょう。

ψ(x1,x2)=ϕ1(x1)ϕ2(x2)(2)

(2)式はHartree積と呼ばれますが,これは,第一近似としては悪くありません(実際,この方法はHartree近似(参考サイト[7])として知られています)。しかし(2)式は,電子はフェルミオンであるので,波動関数は2個の座標x1およびx2に対して反対称でなければならない(x1x2を入れ替えたら,波動関数の符号が逆転しなければならない),という要請を満たしていません。(2)式の右辺を,この要請を満たすように改良するのは簡単で,それは以下のように書けるでしょう。

ψ(x1,x2)=12[ϕ1(x1)ϕ2(x2)ϕ2(x1)ϕ1(x2)](3)

(3)式は,x1x2を入れ替えると,波動関数の符号が逆転します(係数1/2は,波動関数の規格化に必要です)。従って確かに,2個の座標x1およびx2に対して反対称になっています。あるいは,ヘリウム原子では,軌道ϕを,2個の電子が占有していることを考慮すれば,

ψ(x1,x2)=ϕ(r1)ϕ(r2)12[α(s1)β(s2)α(s2)β(s1)](4)

と書くことも可能でしょう。ここで,α(s)は上向きスピン,β(s)は下向きスピンの波動関数です。さて,(4)式の(近似)波動関数を,(1)式に代入してみましょう(なお,これは変分法であり,(4)式の波動関数は試行関数に対応します。以下では,(4)式の波動関数を試行関数と表記します)。ここで,ヘリウム原子に対するSchrödinger方程式のHamiltonian

H^=121212222r12r2+1|r1r2|(5)

は,スピンには全く作用しないので,スピンに依存する部分はSchrödinger方程式の両辺で落ちることを用いると,試行関数を(1)式に代入した結果の式は,以下のようになります。

[121212222r12r2+1|r1r2|]ϕ(r1)ϕ(r2)=Eϕ(r1)ϕ(r2)(6)

より簡単な式を得るため,(6)式の両辺に左からϕ(r2)を乗じて,r2について積分し,r2に関する依存性を取り除くと,以下の式が得られます。

[12122r1+dr2|ϕ(r2)|21|r1r2|]ϕ(r1)=Eϕ(r1)(7)

ここで,積分して定数になる項(すなわちr1に依存しない項)はEに含めました。すなわち,

E=Edr2ϕ(r2)(12222r2)ϕ(r2)(8)

です(ただし,ϕは1に規格化されていることを用いました)。ここで重要なことは,(7)式のSchrödinger方程式が,自己無撞着問題の形をしていることです。すなわち,ϕはこのSchrödinger方程式の解ですが,このSchrödinger方程式はまた,左辺括弧の中の第三項が存在するために,ϕ自身にも依存しています。従って,このSchrödinger方程式は非線形偏微分方程式となっており,(すでに大胆な近似を行ったにもかかわらず)解析的には解けません。従って,何らかの方法で数値的に解かなければなりません。

ヘリウム原子に対するSchrödinger方程式の数値的解法と基底による解法

(7)式のSchrödinger方程式は,差分法,あるいは有限要素法などで,完全に数値的に解くことも可能です。ここで,(7)式は,ポテンシャルが球対称であるため,動径方向の常微分方程式と角度方向の偏微分方程式に厳密に分離できることを用いれば,(7)式は,単なる常微分方程式に還元され,数値的に解くことがより容易になります(角度方向の偏微分方程式の解は,球面調和関数として解析的に得られます)。ヘリウム原子において,(動径方向の)常微分方程式を完全に数値的に解いたコードとしては,例えば拙作のschracがあります。
しかし一般に,Hartree-Fock方程式を完全に数値的に解くことは不可能です(参考サイト[3],参考文献[1])。従って,Hartree-Fock方程式を解く際は,専ら,基底で展開して行列方程式に変換して解くという方法が用いられます(この方法によって,行列方程式に変換されたHartree-Fock方程式は,Roothaan(ローターン)方程式と呼ばれます)。ここで(7)式を,基底で展開して行列方程式に変換して解くことは,Roothaan方程式の一般的な議論をする前の導入として有用でしょう。従って以下では,この方法について説明します。

基底の導入

ヘリウム原子のことは一旦忘れ,基底での展開によって微分方程式が行列方程式に変換できることを,簡単な例を挙げて具体的に説明したいと思います。まず,波動関数ϕ(r)が,以下のように2個の基底関数の線形結合で表せる(2個の基底で展開できる)と仮定してみましょう。

ϕ(r)=C1χ1(r)+C2χ2(r)(9)

このϕ(r)を,Schrödinger方程式に代入すると,

H^[C1χ1(r)+C2χ2(r)]=E[C1χ1(r)+C2χ2(r)](10)

となります。さらに,左からϕ(r)を乗じると,

[C1χ1(r)+C2χ2(r)]H^[C1χ1(r)+C2χ2(r)]=[C1χ1(r)+C2χ2(r)]E[C1χ1(r)+C2χ2(r)](11)

となります。何も難しいことはありません。ここで,χ1(r)と,χ2(r)を実関数に限定すると,C1C2も実数となり,従って(11)式は,

[C1χ1(r)+C2χ2(r)]H^[C1χ1(r)+C2χ2(r)]=[C1χ1(r)+C2χ2(r)]E[C1χ1(r)+C2χ2(r)](12)

となります。これを一旦展開します。

C1χ1(r)H^C1χ1(r)+C1χ1(r)H^C2χ2(r)+C2χ2(r)H^C1χ1(r)+C2χ2(r)H^C2χ2(r)=C1χ1(r)EC1χ1(r)+C1χ1(r)EC2χ2(r)+C2χ2(r)EC1χ1(r)+C2χ2(r)EC2χ2(r)(13)

さらにこれを全空間で積分すると,

dr[C1χ1(r)HC1χ1(r)+C1χ1(r)HC2χ2(r)+C2χ2(r)HC1χ1(r)+C2χ2(r)HC2χ2(r)]=dr[C1χ1(r)EC1χ1(r)+C1χ1(r)EC2χ2(r)+C2χ2(r)EC1χ1(r)+C2χ2(r)EC2χ2(r)](14)

となります。ごちゃごちゃしていますが,難しいことは何もしていません。ここで,(14)式を以下のような「連立方程式」に書き直します。

{C1dr[χ1(r)Hχ1(r)C1+χ1(r)Hχ2(r)C2]=C1Edr[χ1(r)χ1(r)C1+χ1(r)χ2(r)C2]C2dr[χ2(r)Hχ1(r)C1+χ2(r)Hχ2(r)C2]=C2Edr[χ2(r)χ1(r)C1+χ2(r)χ2(r)C2](15)

(15)式が成り立つのなら,(14)式が成立することは自明です。ここで,この連立方程式をC1C2でそれぞれ除し,さらに行列形式で書き直すと,以下の式が得られます。

(drχ1(r)Hχ1(r)drχ1(r)Hχ2(r)drχ2(r)Hχ1(r)drχ2(r)Hχ2(r))(C1C2)=E(drχ1(r)χ1(r)drχ1(r)χ2(r)drχ2(r)χ1(r)drχ2(r)χ2(r))(C1C2)(16)

ここで,

H=(drχ1(r)Hχ1(r)drχ1(r)Hχ2(r)drχ2(r)Hχ1(r)drχ2(r)Hχ2(r))(17a)S=(drχ1(r)χ1(r)drχ1(r)χ2(r)drχ2(r)χ1(r)drχ2(r)χ2(r))(17b)C=(C1C2)(17c)

とおくと(Sは重なり行列と呼ばれます),

HC=ESC(18)

なる行列方程式が得られます。(18)式は一般化固有値問題となっており,Eが(エネルギー)固有値で,Cが固有ベクトルです(なお,基底が正規直交系であれば,Sは単位行列となり,(18)式は通常の固有値問題になります)。さてこれで,当初の目的である,「微分方程式を行列方程式に変換する」ことが達成できました。つまり,波動関数ϕ(r)を,2個の基底で展開すれば,微分方程式から行列方程式への変換が可能だということが分かったわけです。
ここで一般の場合,つまり波動関数を任意の個数(n個)の基底で展開した場合を考えます。この場合,波動関数ϕ(r)は以下のように書けるでしょう。

ϕ(r)=p=1nCpχp(r)(19)

この場合も,全く同様の手順によって(18)式が得られます(ただし,行列Hおよび重なり行列Sが,n次正方行列となることと,固有ベクトルCn次元ベクトルとなることに注意してください)。

基底関数の種類

前節で,波動関数を任意の個数の基底で展開することで,微分方程式を行列方程式に変換できることを見ました。しかし,肝心の基底関数には一体どのような形の関数を選べば良いのでしょうか。結論から言うと,Hartree-Fock法で最も多用されている基底関数は,ガウス関数(ear2)であり,これはGTO(Gaussian Type Orbital,ガウス型軌道)と呼ばれています(これが多用されている理由は後述します)。また,ear型の関数も用いられており,これはSTO(Slater Type Orbital,スレーター型軌道)と呼ばれます。他には,Bessel関数や,B-splineなどが基底関数として用いられることもあります。今は(暗黙に)非周期的かつ有限の系を考えているのですが,平面波(平たく言えば三角関数)を基底関数とすることも可能のようです(参考文献[3])。

(7)式の基底による展開

ヘリウム原子の話に戻って,(7)式のϕ(r1)ϕ(r2)を,以下のようにn個の基底で展開してみましょう。

ϕ(r1)=q=1nCqχq(r1)(20)ϕ(r2)=u=1nCuχu(r2)(21)

(7)式に,(20)式および(21)式を代入すると,以下の式が得られます。

[12122r1+r,s=1nCrCsdr2χr(r2)χs(r2)1|r1r2|]q=1nCqχq(r1)=Eq=1nCqχq(r1)(22)

さらに(22)式に,左からχp(r1)を乗じ,r1について全空間で積分すると,以下の式が得られます。

p,q=1n(hpq+r,s=1nCrCsQprqs)Cq=Ep,q=1nSpqCq(23)

ただし,

hpq=dr1χp(r1)(12122r1)χq(r1)(24a)Qprqs=dr1dr2χp(r1)χr(r2)1|r1r2|χq(r1)χs(r2)(24b)Spq=dr1χp(r1)χq(r1)(24c)

です。なお,(24a)式は1電子積分,(24b)式は2電子積分,(24c)式は重なり積分と呼ばれます。さらに,

Fpq=hpq+r,s=1nCrCsQprqs(25)

とおくと(Fpqを行列と見なしたFは,Fock行列と呼ばれます),結局(7)式は,

FC=ESC(26)

なる,行列方程式(一般化固有値問題)に帰着できます(やや複雑ですが,やっていることは「基底の導入」の節で説明したことと全く同じです)。ここまでくれば,あとは具体的に積分を計算して,行列要素を埋め,一般化固有値問題を数値的に解くだけです。ここで,「具体的に積分を計算して,行列要素を埋める」ためには,具体的な基底関数の形を決めなければなりませんが,水素原子の1s軌道の波動関数がearの形,つまりSTOの形をしていたことを思い出せば,ヘリウム原子の波動関数も,基底にSTOを用いて展開できると考えるのが自然でしょう。
が,基底にSTOを用いるのには問題があります。基底にSTOを使うと,一般的には2電子積分が解析的に求められないのです(参考文献[4]によると,ヘリウム原子の場合は例外で,解析的に求められるらしいです)。一方,基底にGTOを使うと,2電子積分を解析的に求めることが可能になります。これが,Hartree-Fock法で基底としてGTOが多用されている理由です。

GTOで1電子積分,2電子積分および重なり積分を求める

基底にGTOを用いると,基底状態については,l=0の関数(s関数)だけが必要であり,これは,原子核に中心がある(そのためそこを原点に置く)ので,χp(r1)は,以下のようになります。

χp(r1)=eαpr2(27)

(27)式を用いると,ヘリウム原子における,1電子積分hpqと2電子積分Qprqs,および重なり積分Sの要素Spqは,以下のように求められます(計算は省略します)。

hpq=3αpαqπ32(αp+αq)524παp+αq(28)Qprqs=2π52(αp+αq)(αr+αs)αp+αq+αr+αs(29)Spq=(παp+αq)32(30)

ここで,具体的なαpの値についてですが,参考文献[2]に,n=4の場合(ϕ(r1)ϕ(r2)を4つのGTOで展開した場合)の値が記載されているので,それをそのまま引用します(なお一般にαpの値は,非線形の最適化問題を解くことによって求められますが,ここではその方法については立ち入りません)。その値は,

α1=0.297104α2=1.236745α3=5.749982α4=38.216677(31)

です。

自己無撞着問題の解法

前節で,行列要素を埋めるための情報が全て得られたので,後は(26)式の一般化固有値問題を数値的に解くだけ…と言いたいところですが,そうは問屋が卸しません。(25)式の第二項を見てください。この項のCrCsは,(26)式の一般化固有値問題の解である,固有ベクトルCの要素です。つまり,問題の「答え」が,問題を解くために必要なのです。これは,「ヘリウム原子に対するSchrödinger方程式」の節で述べたように,(7)式が自己無撞着問題だったからです。じゃあどうすんねん,と突っ込みたくなりますが,一応解決法はあります。どうするかというと,以下のような手続きを踏みます。

  1. 固有ベクトルCの要素Cpの初期値を適当に選びます。これは例えば,全部0とか,あるいは全部1とかで構いません(今はヘリウム原子という単純な系を考えているので,たいていの初期値から正しい解に収束するはずです)。また,1電子積分hpqと2電子積分Qprqs,および重なり積分Sの要素Spqも計算しておきます。
  2. このCから,(25)式によって,Fock行列Fを計算します。
  3. (26)式の一般化固有値問題を数値的に解きます。
  4. エネルギー
E=2p,q=1nCpCqhpq+p,q,r,s=1nCpCqCrCsQprqs=E+p,q=1nCpCqhpq(32)

を求めます(EEの関係については,(8)式を思い出してください。ここで,

hpq=dr1χp(r1)(12122r1)χq(r1)=dr2χp(r2)(12222r2)χq(r2)(33)

は,系の対称性から自明です)。
5. ステップ3で求まった固有ベクトルCを用いて,(25)式からFock行列Fを計算します。
6. ステップ3~5を繰り返します。

ここで,このままでは無限ループになってしまうので,どこかで計算を打ち切らなくてはなりません。通常は,前回計算したエネルギーEoldと,今回計算したエネルギーEnewの差の絶対値|EnewEold|が,ある閾値未満になったところで計算を打ち切ります。なお,この計算法を,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を参考にさせて頂きました。また,n=3およびn=6の場合のαpの値は,参考サイト[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

プログラムの実行結果

前節で紹介したプログラムを実行した結果は,以下の画像のようになります。
プログラム実行結果.png
n=4で,固有ベクトルCの要素Cpの初期値がすべて0の場合,|EnewEold|が,閾値(今の場合1015)未満になるまでに,29回のループを要していることが分かります。

プログラムによるヘリウム原子のエネルギーの計算結果の結果と考察

ここで,このプログラムによるn=3n=4n=6のときのエネルギーの計算結果を表に示しますと,以下のようになります(ついでに,Hartree-Fock極限(この値は参考文献[5]より引用)と,厳密な値(この値は参考文献[6]より引用)も併せて,以下の表に示します)。

n 計算結果 (Hartree)
3 -2.8162
4 -2.8552
6 -2.8600
Hartree-Fock極限 −2.8617
厳密 −2.9037

上の表のように,nが大きくなるほど,Hartree-Fock極限の値に近づくことが分かります。ここで,Hartree-Fock極限というのは,おおざっぱに言うと,nを無限に大きくしていったときに収束する値のことです(正確には,Hartree-Fock極限に収束させるためには,ノードが異なる基底関数も必要です)。また,厳密な値とHartree-Fock極限との差のエネルギー(-0.042 (Hartree))のことを相関エネルギーと呼び,これは(4)式で,ψ(x1,x2)を,ϕ(r1)ϕ(r2)(×スピン部分)と変数分離してしまったことに起因する,誤差のエネルギーです。
ここまで,ヘリウム原子のSchrödinger方程式を,Hartree-Fock法の「考え方」を用いて解く方法を解説してきました。以下では,一般の多電子原子や分子に対するSchrödinger方程式を解くために,この議論を一般化することにします。

Hartree-Fock方程式

N電子系(電子がN個ある系)における,非相関の反対称な波動関数は,以下のような式で表されることが知られています(非相関というのは,多体問題の効果を無視していることを意味しています)。

ψ(x1,,xN)=1N!|ϕ1(x1)ϕ2(x1)ϕN(x1)ϕ1(x2)ϕ2(x2)ϕN(x2)ϕ1(xN)ϕ2(xN)ϕN(xN)|(34)

(34)式は,1929年にJ. C. Slaterによって導かれたので(参考サイト[10]),Slater行列式と呼ばれています。ここで,ϕi(xj)=ψiσ(rj)ασ(sj)は1粒子「スピン軌道」であり,空間位置座標rjの関数ψiσ(rj)とスピン座標sjの関数ασ(sj)の積です(ここで,ϕiの添字iは軌道の状態とスピンの状態 σを表しており,jは電子の番号です)。ここで本来は,RHF(Restricted Hartree-Fock,制限ハートリー・フォック)法と,UHF(Unrestricted Hartree-Fock,非制限ハートリー・フォック)法の細かい説明が必要なのですが,ここでは省略し,以下ではRHF法についてのみ述べます。
(34)式を使うと,(核間反発エネルギーを除いた)エネルギー固有値Eは,以下の式で与えられます。

E=i,σdrψiσ(r)[122nZn|rRn|]ψiσ(r)+12i,j,σ,σdrdrψiσ(r)ψjσ(r)1|rr|ψiσ(r)ψjσ(r)12i,j,σdrdrψiσ(r)ψjσ(r)1|rr|ψjσ(r)ψiσ(r)(35)

ただし,添字n に対する総和は,系に存在する核の数にわたってとることにします。ここで,

h(r)=122nZn|rRn|(36a)J(r)ψ(r)=ldrψl(r)ψl(r)1|rr|ψ(r)(36b)K(r)ψ(r)=ldrψl(r)ψ(r)1|rr|ψl(r)(36c)

とおきます。ただし,添字lに対する総和は,占有軌道にわたってとることとします。また,(36b)式のJ(r)はCoulomb演算子,(36c)式のK(r)は交換演算子と呼ばれます。h(r),Coulomb演算子および交換演算子を用いて(35)式を書き直すと,

E=2kdrψk(r)h(r)ψk(r)+k(2drψk(r)J(r)ψk(r)drψk(r)K(r)ψk(r))(37)

と多少見通しが良く(?)なります。ここで,添字kに対する総和は,lと同様に,占有軌道にわたってとります。また(37)式から,以下の方程式が得られます(導出は省略します)。

F(r)ψk(r)=ϵkψk(r) (38)

(38)式が,Hartree-Fock方程式です。ここで,F(r)は,Fock演算子と呼ばれており,

F(r)=h(r)+2J(r)K(r)(39)

です。

Roothaan方程式

すでに述べたように,(38)式のHartree-Fock方程式を完全に数値的に解くことは,一般的には不可能です。従って,有限個の基底を用いて行列方程式に変換して解く必要があります。(20)式および (21)式と同様に,空間軌道関数ψk(r)は,m個の基底関数χpの線形結合で展開して,

ψk(r)=p=1mχpk(r)(40)

と表すことができます。このとき(38)式は,(26)式と同様に,

FCk=ϵkSCk(41)

なる行列方程式(一般化固有値問題)になります。(41)式は,Roothaan方程式と呼ばれています。ここで,Fock行列Fの要素Fpqは,

Fpq=hpq+kr,s=1mCrkCsk(2pr|g|qspr|g|sq)(42)

で与えられます。ただし,

hpq=drχp(r)[122nZn|rRn|]χq(r)(43a)pr|g|qs=dr1dr2χp(r1)χr(r2)1|r1r2|χq(r1)χs(r2)(43b)pr|g|sq=dr1dr2χp(r1)χr(r2)1|r1r2|χs(r1)χq(r2)(43c)

です。ただし,添字kは,軌道ψkを表し,また添字p, q, r, sは基底を表します。ここで,(43a)を1電子積分,(43b)をCoulomb積分, (43c)を交換積分と呼びます。また,(43b)(43c)をあわせて,2電子積分と呼びます。また,Sは重なり行列であり,その要素Spqは,

Spq=drχp(r)χq(r)(44)

で定義されます。系の(核間反発エネルギーを除いた)全エネルギーEについては,密度行列Pと呼ばれるものを導入すると便利であり,その要素Ppqは,

Ppq=2kCpkCqk(45)

で定義されます。ここで,(45)式を使うと,フォック行列Fの要素Fpqは,

Fpq=hpq+12r,s=1mPrs(2pr|g|qspr|g|sq)(46)

と書けます。従って,系の全エネルギーEは,

E=p,q=1mPpqhpq+12p,q,r,s=1mPpqPrs(pr|g|qs12pr|g|sq)(47)

となります。

ヘリウム原子におけるSlater行列式,Hartree-Fock方程式およびRoothaan方程式

最後に,ヘリウム原子において,Slater行列式,Hartree-Fock方程式およびRoothaan方程式を考え,これらがそれぞれ (3)式,(7)式および(26)式と等しくなることを確認してみましょう。

ヘリウム原子におけるSlater行列式とHartree-Fock方程式

まず,ヘリウム原子(2原子系)に対するSlater行列式は,

ψ(x1,x2)=12!|ϕ1(x1)ϕ2(x1)ϕ1(x2)ϕ2(x2)|=12[ϕ1(x1)ϕ2(x2)ϕ1(x2)ϕ2(x1)](48)

となり,(48)式は,確かに(3)式と全く同じ式となっています。(36a)式については,ヘリウム原子の場合,核は1個しか存在せず(n=1),かつ核の電荷は2e(Z1=2)なので,

h(r)=1222|r|=1222r(49)

となります。ここで,R1を原点にとりました(R1=0)。また,Coulomb演算子J(r)は,占有軌道が1個なので,総和をとる必要がなく,従って添字lは不要であることに注意すると,

J(r)ψ(r)=drψ(r)ψ(r)1|rr|ψ(r)(50)

です。よって,

J(r)=drψ(r)ψ(r)1|rr|=dr|ψ(r)|21|rr|(51)

となります。同様に,lが不要であることに注意すると,交換演算子K(r)は,

K(r)ψ(r)=drψ(r)ψ(r)1|rr|ψ(r)(52)

より,

K(r)=drψ(r)ψ(r)1|rr|=dr|ψ(r)|21|rr|(53)

となります。よって,Fock演算子F(r)は,

F(r)=1222r+2dr|ψ(r)|21|rr|dr|ψ(r)|21|rr|=1222r+dr|ψ(r)|21|rr|(54)

となります。ここで,添字kは不要であることを用いると,(38)式のHartree-Fock方程式は結局,

[1222r+dr|ψ(r)|21|rr|]ψ(r)=ϵψ(r)(55)

となりますが,(55)式は,確かに(7)式と全く同じ式となっています(rr1に,rr2に,またϵEに適宜読み替えてください)。つまり(7)式は,(38)式の特別な場合であることが,これで確認できたことになります。

ヘリウム原子におけるRoothaan方程式

次に,ヘリウム原子におけるRoothaan方程式を考えてみましょう。ここで,基底関数χp(r)は,実関数に限定するものとします。1電子積分hpq(49)式から容易に導くことができ,

hpq=drχp(r)(1222r)χq(r)(56)

となりますが,(56)式は,(24a)式そのものです(rr1に適宜読み替えてください)。次に2電子積分ですが,ヘリウム原子の場合,Coulomb積分pr|g|qsと,交換積分pr|g|sqは等しくなります(これは,(29)式で,添字qsを入れ替えても,結果が変わらないことから理解できます)。従って,ヘリウム原子におけるFock行列の要素Fpqは,

Fpq=hpq+r,s=1mCrCspr|g|qs(57)

となります(ただし,添字kは不要であることと,Crが実数になることを用いました)。(57)式は,(25)式に他なりません(pr|g|qsQprqsに適宜読み替えてください)。さらに,基底関数χp(r)を,実関数に限定したので,(44)式は,(24c)式と全く等しくなります。よって,(26)式が,(41)式の特別な場合であることが,これで確認できました。
さらに,添字kが不要であることに注意すると,密度行列Pの要素Ppqは,

Ppq=2CpCq(58)

となります。ここで,(58)式を(47)式に代入すると,系の全エネルギーEは,

E=2p,q=1mCpCqhpq+p,q,r,s=1mCpCqCrCspr|g|qs(59)

となりますが,(59)式は(32)式そのものです。

まとめ

  1. Hartree-Fock法の考え方を,ヘリウム原子を例にとって説明しました。また,ヘリウム原子に対する試行関数の,基底による展開を例にとって,Roothaan方程式の考え方を説明しました。
  2. 一般的なHartree-Fock法について,RHF法に限り説明しました。Slater行列式から,Hartree-Fock方程式が導かれることを示しました(実際の導出はしていませんが…)。
  3. Hartree-Fock方程式の軌道ψk(r)を基底で展開することにより,Roothaan方程式が導かれることを説明しました。
  4. ヘリウム原子における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)

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
ユーザー登録ログイン
dc1394
主にC++,C#,Juliaを使っています。数値解析が趣味。 博士後期課程在学中。専門は第一原理計算。

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
Claris Connect を使った SaaS 連携ユースケースを紹介しよう!
~
Remote TestKitを使ってレビューを書こう!
~
77
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー