🗂

有限要素法による非時間依存1次元シュレディンガー方程式

2023/11/22に公開

n番煎じですがやってみたかったので.

理論編

理論と題するのもおこがましいほどですが少しだけ説明を.

1.有限要素法とは?

対象とする領域を小領域(要素)に分割し, 各領域ごとに適当な近似関数で解を近似する方法. 具体的には, 微分方程式を用いて各要素ごとに近似関数が満たすべき連立方程式を作成し, 各要素同士の関係から全体の連立方程式を作成, これを解きます. 今回はシュレディンガー方程式が対象なので, 解くべき方程式は一般化固有方程式となります.
今回行う方法は, 有限要素法の中でもGalerkin法と呼ばれるものです. これは重み付き残差法の一種で, 特に近似関数の基底と重み関数の基底が同じ関数系であるものを言います.


近似のイメージ

2.シュレディンガー方程式

時間に依存しない1次元のシュレディンガー方程式は以下の式(2.1)で与えられます.

i\hbar\frac{\partial\psi(x, t)}{\partial t}=\left(-\frac{\hbar^2}{2m}\frac{\partial^2}{\partial x^2}+V(x)\right)\psi(x, t)\;\;\;(2.1)

この記事では式(2.1)ではなく, 以下の式(2.2)に示す時間に依存しないシュレディンガー方程式を解きます.

\left(-\frac{\hbar^2}{2m}\frac{d^2}{dx^2}+V(x)\right)\phi(x)=E\phi(x)\;\;\;(2.2)

計算編

3.弱形式を求める

重み関数として\phi(x)の複素共役\phi^{*}(x)を選ぶ. これを式(2.2)の両辺にかけ, 計算対象区間 [0, L] で積分すると以下の式(3.1)を得ます.

-\frac{\hbar^2}{2m}\int_{0}^{L}\phi^{*}(x)\frac{d^2\phi(x)}{dx^2}dx+\int_{0}^{L}V(x)\phi^{*}(x)\phi(x) dx=E\int_{0}^{L}\phi^{*}(x)\phi(x)dx\;\;\;(3.1)

式(3.1)左辺第1項の積分に対して部分積分を施すと以下の式(3.2)を得ます.

\begin{align*} \int_{0}^{L}\phi^{*}(x)\frac{d^2\phi(x)}{dx^2}dx&=\left[\phi^{*}(x)\frac{d\phi(x)}{dx}\right]_0^L-\int_{0}^{L}\frac{d\phi^{*}(x)}{dx}\frac{d\phi(x)}{dx}dx\\ &=-\int_{0}^{L}\frac{d\phi^{*}(x)}{dx}\frac{d\phi(x)}{dx}dx\;\;\;(3.2) \end{align*}

ただし, 1行目から2行目への変形にはDirichlet境界条件 \phi(0)=\phi(L)=0 を用いた. 以上より, 以下の式(3.3)に示す弱形式が得られます.

\frac{\hbar^2}{2m}\int_{0}^{L}\frac{d\phi^{*}(x)}{dx}\frac{d\phi(x)}{dx}dx+\int_{0}^{L}V(x)\phi^{*}(x)\phi(x) dx=E\int_{0}^{L}\phi^{*}(x)\phi(x)dx\;\;\;(3.3)

4.形状関数

対象区間 [0, L]N 個の要素に分割します. このとき, e 番目(e=0, 1, ..., N-1)の要素の2つの節点の x 座標をそれぞれ x_0^e, x_1^eとします(つまり x_1^i=x_0^{i+1} が成立している). また, 同じように, 2つの節点における解 \phi の値をそれぞれ \phi_0^e, \phi_1^eと, e 番目の要素において解を近似する関数は

(h_0^e(x)\;\;h_1^e(x))\begin{pmatrix*}\phi_0^e\\ \phi_1^e\end{pmatrix*} \;\;\;(4.1)

と表現できます. ただし, h_i^e(x) は形状関数の基底であり,

h_0^e(x)=-\frac{x-x_1^e}{x_1^e-x_0^e}\;\;\;h_1^e(x)=\frac{x-x_0^e}{x_1^e-x_0^e}\;\;\;(x_0^e\le x\le x_1^e)

とします. つまり, h_0^e(x) は点 (x_0^e, 1) と点 (x_1^e, 0) を通る直線, h_1^e(x) は点 (x_0^e, 0) と点 (x_1^e, 1) を通る直線(ただし, 要素の外では0)です. 即ち, この2つの関数を基底とする線形結合で表される式(4.1)は, 点 (x_0^e, \phi_0^e) と点 (x_1^e, \phi_1^e) を通る直線だということが分かります.

5.離散化

式(4.1)を用いると, e 番目の要素における \phi(x) は以下のように書くことができます.

\phi(x)=\sum_{i=0}^1\phi_i^eh_i^e(x)

同様に, ポテンシャル V(x)

V(x)=\sum_{i=0}^1V_i^eh_i^e(x)

と書けます. これらを式(3.3)の弱形式に代入すると

\frac{\hbar^2}{2m}\sum_{e=0}^{N-1}\int_{x_0^e}^{x_1^e}\left(\sum_{i=0}^1\phi_i^{*e}\frac{dh_i^e(x)}{dx}\right)\left(\sum_{i=0}^1\phi_i^e\frac{dh_i^e(x)}{dx}\right) dx+\sum_{e=0}^{N-1}\int_{x_0^e}^{x_1^e}\left(\sum_{i=0}^1\phi_i^{*e}h_i^e(x)\right)\left(\sum_{i=0}^1V_i^{e}h_i^e(x)\right)\left(\sum_{i=0}^1\phi_i^{e}h_i^e(x)\right)dx=E\sum_{e=0}^{N-1}\int_{x_0^e}^{x_1^e}\left(\sum_{i=0}^1\phi_i^{*e}h_i^e(x)\right)\left(\sum_{i=0}^1\phi_i^{e}h_i^e(x)\right)dx

を得ます. さらに, l^e=x_1^e-x_0^eとおき, t=\frac{x-x_0^e}{l^e} として変数変換すると

h_0^e(t)=-t+1\;\;\;h_1^e(t)=t
\frac{\hbar^2}{2m}\sum_{e=0}^{N-1}\frac{1}{l^e}\int_0^1\left(\sum_{i=0}^1\phi_i^{*e}\frac{dh_i^e(t)}{dt}\right)\left(\sum_{i=0}^1\phi_i^e\frac{dh_i^e(t)}{dt}\right) dt+\sum_{e=0}^{N-1}l^e\int_{0}^{1}\left(\sum_{i=0}^1\phi_i^{*e}h_i^e(t)\right)\left(\sum_{i=0}^1V_i^{e}h_i^e(t)\right)\left(\sum_{i=0}^1\phi_i^{e}h_i^e(t)\right)dt=E\sum_{e=0}^{N-1}l^e\int_{0}^{1}\left(\sum_{i=0}^1\phi_i^{*e}h_i^e(t)\right)\left(\sum_{i=0}^1\phi_i^{e}h_i^e(t)\right)dt\;\;\;(5.1)

を得ます.

6.要素行列を求める.

式(5.1)をさらに計算していきましょう. 式(5.1)左辺第1項は

\begin{align*} \frac{\hbar^2}{2m}\sum_{e=0}^{N-1}\frac{1}{l^e}\int_0^1\left(\sum_{i=0}^1\phi_i^{*e}\frac{dh_i^e(t)}{dt}\right)\left(\sum_{i=0}^1\phi_i^e\frac{dh_i^e(t)}{dt}\right) dt&=\frac{\hbar^2}{2m}\sum_{e=0}^{N-1}\frac{1}{l^e}(\phi_1^{*e}-\phi_0^{*e})(\phi_1^e-\phi_0^e)\\ &=\frac{\hbar^2}{2m}\sum_{e=0}^{N-1}\frac{1}{l^e}(\phi_0^{*e}\;\;\phi_1^{*e})\begin{pmatrix*}1&-1\\-1&1\end{pmatrix*}\begin{pmatrix*}\phi_0^e\\ \phi_1^e\end{pmatrix*} \end{align*}

式(5.1)左辺第2項は

\begin{align*} \sum_{e=0}^{N-1}l^e\int_{0}^{1}\left(\sum_{i=0}^1\phi_i^{*e}h_i^e(t)\right)\left(\sum_{i=0}^1V_i^{e}h_i^e(t)\right)\left(\sum_{i=0}^1\phi_i^{e}h_i^e(t)\right)dt&=\frac{1}{12}\sum_{e=0}^{N-1}l^e(\phi_0^{*e}\;\;\phi_1^{*e})\begin{pmatrix*}3V_0^e+V_1^e&V_0^e+V_1^e\\V_0^e+V_1^e&V_0^e+3V_1^e\end{pmatrix*}\begin{pmatrix*}\phi_0^e\\\phi_1^e\end{pmatrix*} \end{align*}

式(5.1)右辺は

\begin{align*} E\sum_{e=0}^{N-1}l^e\int_{0}^{1}\left(\sum_{i=0}^1\phi_i^{*e}h_i^e(t)\right)\left(\sum_{i=0}^1\phi_i^{e}h_i^e(t)\right)dt&=\frac{1}{6}E\sum_{e=0}^{N-1}l^e(\phi_0^{*e}\;\;\phi_1^{*e})\begin{pmatrix*}2&1\\1&2\end{pmatrix*}\begin{pmatrix*}\phi_0^e\\\phi_1^e\end{pmatrix*} \end{align*}

以上をまとめると以下を得ます.

\sum_{e=0}^{N-1}(\phi_0^{*e}\;\;\phi_1^{*e}) A^e \begin{pmatrix*}\phi_0^e\\\phi_1^e\end{pmatrix*}=E\sum_{e=0}^{N-1}(\phi_0^{*e}\;\;\phi_1^{*e}) B^e \begin{pmatrix*}\phi_0^e\\\phi_1^e\end{pmatrix*}\;\;\;(6.1)
A^e = \begin{pmatrix*}\frac{\hbar^2}{2ml^e}+\frac{1}{12}(3V_0^e+V_1^e)l^e&-\frac{\hbar^2}{2ml^e}+\frac{1}{12}(V_0^e+V_1^e)l^e\\-\frac{\hbar^2}{2ml^e}+\frac{1}{12}(V_0^e+V_1^e)l^e&\frac{\hbar^2}{2ml^e}+\frac{1}{12}(V_0^e+3V_1^e)l^e\end{pmatrix*}
B^e = \begin{pmatrix*}\frac{1}{3}l^e&\frac{1}{6}l^e\\ \frac{1}{6}l^e&\frac{1}{3}l^e\end{pmatrix*}

7.全体行列を構成する

式(6.1)は\phi_1^e=\phi_0^{e+1}の関係を考慮すると

\left(\phi^{*0}\;\;\phi^{*1}\;\;...\;\;\phi^{*N}\right) A \begin{pmatrix*} \phi^0\\ \phi^1\\ \vdots\\ \phi^N \end{pmatrix*} =E \left(\phi^{*0}\;\;\phi^{*1}\;\;...\;\;\phi^{*N}\right) B \begin{pmatrix*} \phi^0\\ \phi^1\\ \vdots\\ \phi^N \end{pmatrix*}

と書き表せます. さらに, 変分原理を用いることで以下のような方程式に変形できます(らしいです. 変分原理について勉強したらしっかり書きます).

A \begin{pmatrix*} \phi^0\\ \phi^1\\ \vdots\\ \phi^N \end{pmatrix*} =E B \begin{pmatrix*} \phi^0\\ \phi^1\\ \vdots\\ \phi^N \end{pmatrix*}

行列 A^eij 列要素を A_{ij}^e と表すと(B^e も同様)全体行列は以下のようになります.

\begin{pmatrix*} A_{00}^0&A_{01}^0&0&0&\cdots&0&0\\ A_{10}^0&A_{11}^0+A_{00}^1&A_{01}^1&0&\cdots&0&0\\ 0&A_{10}^1&A_{11}^1+A_{00}^2&A_{01}^2&\cdots&0&0\\ \vdots&\vdots&\vdots&\vdots&\ddots&\vdots&\vdots\\ 0&0&0&0&\cdots&A_{11}^{N-2}+A_{00}^{N-1}&A_{01}^{N-1}\\ 0&0&0&0&\cdots&A_{10}^{N-1}&A_{11}^{N-1}\\ \end{pmatrix*}\begin{pmatrix*}\phi_0^0\\ \phi_0^1\\ \phi_0^2\\ \vdots\\ \phi_0^{N-1}\\ \phi_1^{N-1}\end{pmatrix*}=E\begin{pmatrix*} B_{00}^0&B_{01}^0&0&0&\cdots&0&0\\ B_{10}^0&B_{11}^0+B_{00}^1&B_{01}^1&0&\cdots&0&0\\ 0&B_{10}^1&B_{11}^1+B_{00}^2&B_{01}^2&\cdots&0&0\\ \vdots&\vdots&\vdots&\vdots&\ddots&\vdots&\vdots\\ 0&0&0&0&\cdots&B_{11}^{N-2}+B_{00}^{N-1}&B_{01}^{N-1}\\ 0&0&0&0&\cdots&B_{10}^{N-1}&B_{11}^{N-1}\\ \end{pmatrix*}\begin{pmatrix*}\phi_0^0\\ \phi_0^1\\ \phi_0^2\\ \vdots\\ \phi_0^{N-1}\\ \phi_1^{N-1}\end{pmatrix*}

さらに, Dirichlet境界条件 \phi(0)=\phi(L)=0 を考慮する必要がありますが, これは行列の1行目と1列目, および N+1 行目と N+1 列目を消すだけで構いません.
後はこれを解くだけですが, 今回はライブラリに任せます. そのうち別記事にまとめるかもしれません.

プログラミング編

8.Pythonで有限要素法

おまけ

import numpy as np
import matplotlib.pyplot as plt
from collections.abc import Callable
import scipy.sparse.linalg as spsl

class SchrodingerFEM():

    DIRAC_CONSTANT: float = 1.054571817e-34
    DIRAC_CONSTANT_DOUBLE: float = DIRAC_CONSTANT**2

    EIG_VAL_FILE = 'eigenvalue.csv'
    EIG_VEC_FILE = 'eigenvector.csv'

    n: int
    m: float

    matrix_genned: bool

    x: np.ndarray
    l: np.ndarray
    A: np.ndarray
    B: np.ndarray
    eigen_val_vec: list

    V: Callable[[float], float]

    def __init__(self, n: int, m: float, x: np.ndarray, V: Callable[[float], float])->None:
        self.n = n
        self.m = m
        self.x = x
        self.V = V

        self.matrix_genned = False
        self.eigen_val_vec = []

        if len(x) != n+1:
            raise ValueError(f'Array x size should be {n+1} but is {len(x)}.')
        
        self.l = np.zeros((n,))
        for i in range(n):
            self.l[i] = self.x[i+1]-self.x[i]
        
        self.A = np.zeros((n+1, n+1), np.float64)
        self.B = np.zeros((n+1, n+1), np.float64)

    def gen_A_element(self, e)->np.ndarray:
        Ae = np.zeros((2, 2), np.float64)
        Ae[0][0] = self.DIRAC_CONSTANT_DOUBLE/(2*self.m*self.l[e])+self.l[e]*(3*self.V(self.x[e])+self.V(self.x[e+1]))/12
        Ae[0][1] = -self.DIRAC_CONSTANT_DOUBLE/(2*self.m*self.l[e])+self.l[e]*(self.V(self.x[e])+self.V(self.x[e+1]))/12
        Ae[1][0] = Ae[0][1]
        Ae[1][1] = self.DIRAC_CONSTANT_DOUBLE/(2*self.m*self.l[e])+self.l[e]*(self.V(self.x[e])+3*self.V(self.x[e+1]))/12
        return Ae

    def gen_B_element(self, e)->np.ndarray:
        Be = np.zeros((2, 2), np.float64)
        Be[0][0] = self.l[e]/3
        Be[0][1] = self.l[e]/6
        Be[1][0] = Be[0][1]
        Be[1][1] = Be[0][0]
        return Be
    
    def gen_matrix(self)->None:
        for e in range(self.n):
            Ae = self.gen_A_element(e)
            Be = self.gen_B_element(e)
            for i in range(2):
                for j in range(2):
                    self.A[e+i][e+j] += Ae[i][j]
                    self.B[e+i][e+j] += Be[i][j]

        self.A, self.B = self.A[1:self.n, 1:self.n], self.B[1:self.n, 1:self.n]

        print('Matrix calculation has done !\n\n', end='')
        self.matrix_genned = True
    
    def solve(self)->None:
        if not self.matrix_genned:
            raise ValueError('Matrix has not been generated.')
        
        val, vec = spsl.eigs(self.A, self.n, self.B, maxiter=self.n*500, which='SM', sigma=0.05)
        for i in range(len(val)):
            self.eigen_val_vec.append((val[i], np.concatenate([[0], vec[:, i], [0]])))

        self.eigen_val_vec.sort(key=lambda x:x[0])
    
    def get_matrix(self)->tuple[np.ndarray, np.ndarray]:
        return self.A, self.B
    
    def get_eigenvalue(self, i)->np.complex64:
        return self.eigen_val_vec[i][0]
    
    def get_eigenvector(self, i)->np.ndarray:
        return self.eigen_val_vec[i][1]
    
    def export_data(self)->None:
        val_file_handler = open(self.EIG_VAL_FILE, mode='w')
        vec_file_handler = open(self.EIG_VEC_FILE, mode='w')

        for val_vec in self.eigen_val_vec:
            val_file_handler.write(f'{val_vec[0]}\n')
            vec_file_handler.write(','.join(map(str, val_vec[1]))+'\n')
        
        val_file_handler.close()
        vec_file_handler.close()
        
        

def V(_ :float)->float:
    return 0

def V0(x: float)->float:
    return x**2

def V1(x: float)->float:
    if abs(x)>2:
        return 10
    else:
        return 0

if __name__=='__main__':
    N = 500
    m = SchrodingerFEM.DIRAC_CONSTANT_DOUBLE/2
    L = 5
    x = np.linspace(-L, L, N+1)

    fem = SchrodingerFEM(N, m, x, V0)
    fem.gen_matrix()
    fem.solve()
    fem.export_data()

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    ax.set_xlabel(r'$x$')
    ax.set_ylabel(r'$\phi$')

    for i in range(3):
        ax.plot(x, fem.get_eigenvector(i), label=f'$E_{i+1}: {fem.get_eigenvalue(i)}$')

    ax.legend()

    plt.show()

7.例

実際にいくつかのポテンシャルで計算してみました.


無限に深い井戸型ポテンシャル


調和振動子ポテンシャル
固有値の番号は小さい方から1, 2, ... と振っているので注意.


クーロンポテンシャル
x=0では適当に小さい値を充てています.

参考

@cometscome_phys Yuki Nagai. "1次元シュレーディンガー方程式を有限要素法で解いてみる in Julia". https://qiita.com/cometscome_phys/items/fbeaa92fcb4b6d8c6240

@dc1394. "水素原子に対するSchrödinger方程式を有限要素法で数値的に解いてみる(C++とJuliaのソースコード付き)". https://qiita.com/dc1394/items/c0d3d02fa738b040128c

@Sunset_Yuhi. "Pythonで1次元有限要素法(Poisson方程式)". Pythonで1次元有限要素法(Poisson方程式)". https://qiita.com/Sunset_Yuhi/items/4c4ddc25609a7619cce0

Discussion