有限要素法による非時間依存3次元シュレディンガー方程式
はじめに
有限要素法による非時間依存1次元シュレディンガー方程式で1次元, 有限要素法による非時間依存2次元シュレディンガー方程式で2次元のシュレディンガー方程式を有限要素法を用いて解いたので, 今回は3次元のシュレディンガー方程式を解く.
3次元のシュレディンガー方程式は
です(
計算編
1.弱形式
2次元の場合と同じですが一応.
冒頭のシュレディンガー方程式の両辺に重み関数として
式(1.1)左辺第1項は
の関係式を用いると
となります. さらに, ガウスの発散定理
を用いると
となります. Dirichlet境界条件
を得ます. 以上より弱形式は以下の式(1.2)のようになります.
2.形状関数
2次元のときはメッシュ形状を具体的に定めましたが, 一般的に書いた方が記述が見やすいと思ったので今回は一般的なメッシュを考えます. 用いる要素は四面体1次要素とし, 領域を 0から
を用いるとすると
ここで, 行列
とし, さらに行列
とします. よって
となるので, 行列
となります. これが1次四面体要素の形状関数です. また,
3.離散化と要素行列
式(1.2)の弱形式は
と離散化できます.
式(3.1)左辺第1項から計算していきましょう.
となります (上付き添え字略). よって
となります(対称行列になるので下三角要素を省略).
とおくと, これは
ただし,
で計算できます.
体積の計算について
4点
となります. これを1行目に関して余因子展開すると
(絶対値を計算するので符号を無視しつつ)計算を進めると
となります. これはいわゆるスカラー三重積であり, 絶対値は3つのベクトル
が張る平行六面体の体積になっています (外積の部分で底面積を計算し, 外積のベクトルに対して内積をとる, つまり正射影することで高さを計算していると考えられます). よって, これを6で割った値は四面体の体積になります.
次に式(3.1)左辺第2項を計算していきます.
となります. 例によってポテンシャルも基底
で計算できます.
四面体1次要素の積分の導出
行列
と表します(行列
となります(行と列の添え字に注意).
基底をここに改めて示しておきます.
計算したい積分は
です. 頑張って計算すると
となります(MATLABで計算しました). よって
となります. 次に積分する範囲を考えましょう.
となります. さらに
となります.
これを用いると
と計算できます. 同じように
と計算できます. 従って
ポテンシャル
です. 上に示した式を用いて式(3.1)左辺第1項の積分を計算すると
となります(対称行列なので下三角要素は省略). この行列を
とおくと式(3.1)左辺第2項は
となります.
最後に式(3.1)右辺を計算しますが, これは式(3.1)左辺第2項のときと同じように計算できるので途中を省略します. 式(3.1)右辺は行列
を用いて
以上より, 式(3.1)は
となり,
と表されます.
4.全体行列を構成する
1次元, 2次元のときと同じです.
となるように要素行列の各要素を全体行列に足し合わせましょう. 最後に変分原理を用いて
と変形することで解くべき一般化固有方程式を得ます.
5.境界条件処理
これも1次元, 2次元のときと同じです. 対応する行, および列を削除しましょう.
プログラミング編
6.Pythonで有限要素法
PCスペックの都合でプログラムの正当性を確認できていないですがせっかくなので簡単な説明と併せて載せておきます.
要素クラスを定義します. 要素クラスは四面体の頂点の座標, M行列, 要素の体積を保持します.
class Element():
# 要素クラス
points: tuple # 4頂点の座標
global_numbers: tuple # 4頂点のグローバル節点番号
volume: float # 要素の体積
M: np.ndarray # 要素のM行列
def __init__(self, p0: tuple, p1: tuple, p2: tuple, p3: tuple, global_numbers: tuple) -> None:
self.points = (p0, p1, p2, p3)
self.global_numbers = global_numbers
S = np.concatenate(
[self.points, [[1], [1], [1], [1]]], axis=1, dtype=np.float64)
self.volume = np.abs(npl.det(S))/6
self.M = npl.inv(S)
return None
メッシュクラスも定義します. 今回は以下に示すように直方体ごとに5つの四面体に分割します.
メッシュその1(3個)
メッシュその2(1個, 中心)
メッシュその3(1個)
class Mesh():
# メッシュクラス
N: int # 1辺(2N-1)節点
L: float # 1辺2L
X: np.ndarray # x軸方向の節点座標
Y: np.ndarray # y軸方向の節点座標
Z: np.ndarray # z軸方向の節点座標
elements: list # 要素を保持
indices: tuple = ((0, 1, 2, 4), (1, 4, 5, 7),
(1, 2, 3, 7), (2, 4, 6, 7), (1, 2, 4, 7)) # 要素生成用のインデックス
def __init__(self, N: int, L: float) -> None:
self.N = N
self.L = L
self.X = np.concatenate(
[np.linspace(-L, 0, N-1, endpoint=False), np.linspace(0, L, N, endpoint=True)])
self.Y = np.concatenate(
[np.linspace(-L, 0, N-1, endpoint=False), np.linspace(0, L, N, endpoint=True)])
self.Z = np.concatenate(
[np.linspace(-L, 0, N-1, endpoint=False), np.linspace(0, L, N, endpoint=True)])
self.elements = []
return None
def gen_mesh(self) -> None:
# 直方体ごとに要素を生成する. 1つの直方体を5つの四面体に分割する.
for i in range(2*self.N-2):
for j in range(2*self.N-2):
for k in range(2*self.N-2):
p = []
gn = []
# 直方体の8節点の座標とグローバル節点番号の割り当て
for l in range(2):
for m in range(2):
for n in range(2):
p.append(
(self.X[i+l], self.Y[j+m], self.Z[k+n])) # 節点を追加
if i+l == 0 or j+m == 0 or k+n == 0:
# 境界のグローバル節点番号は-1
gn.append(-1)
elif i+l == (2*self.N-2) or j+m == (2*self.N-2) or k+n == (2*self.N-2):
# 境界のグローバル節点番号は-1
gn.append(-1)
else:
# グローバル節点番号を計算して割り当て
gn.append((i+l-1)+(2*self.N-3) *
(j+m-1)+((2*self.N-3)**2)*(k+n-1))
# 要素生成
for idx in self.indices:
points = (p[idx[0]], p[idx[1]], p[idx[2]], p[idx[3]])
gns = (gn[idx[0]], gn[idx[1]], gn[idx[2]], gn[idx[3]])
self.elements.append(Element(*points, gns))
return None
残りは1次元, 2次元のときとほとんど同じです. 境界にあたる節点は初めから全体行列に含まないようにしています. また, 全体行列を疎行列クラス(SciPyのCSR形式)に変換しています.
class SchrodingerFEM3():
DIRAC_CONSTANT: float = 1.054571817e-34
DIRAC_CONSTANT_DOUBLE: float = DIRAC_CONSTANT**2
N: int # 要素数
N_points: int # 境界を除いた節点数
m: float # 質量
V: Callable[[float, float, float], float] # ポテンシャル
A: np.ndarray # A行列
B: np.ndarray # B行列
elements: list # 要素を保持
eig: list # 計算結果を保持
# ポテンシャル項の計算用行列
V_base: np.array = np.array([[[6, 2, 2, 2], [2, 2, 1, 1], [2, 1, 2, 1], [2, 1, 1, 2]], [[2, 2, 1, 1], [2, 6, 2, 2], [1, 2, 2, 1], [1, 2, 1, 2]],
[[2, 1, 2, 1], [1, 2, 2, 1], [2, 2, 6, 2], [1, 1, 2, 2]], [[2, 1, 1, 2], [1, 2, 1, 2], [1, 1, 2, 2], [2, 2, 6, 6]]], np.float64)
def __init__(self, N_points: int, m: float, V: Callable[[float, float, float], float], elements: list) -> None:
self.N = len(elements)
self.N_points = N_points
self.m = m
self.V = V
self.elements = elements
self.A = np.zeros((self.N_points, self.N_points))
self.B = np.zeros((self.N_points, self.N_points))
self.eig = []
return None
def gen_K_matrix(self, e: int) -> np.ndarray:
Ke = np.zeros((4, 4), np.float64)
for i in range(4):
for j in range(4):
for k in range(3):
Ke[i][j] += self.elements[e].M[k][i] * \
self.elements[e].M[k][j]
Ke *= (self.DIRAC_CONSTANT_DOUBLE/(2*self.m))
return Ke
def gen_V_matrix(self, e: int) -> np.ndarray:
# 要素の各頂点におけるポテンシャルの値
V = [self.V(p[0], p[1], p[2]) for p in self.elements[e].points]
Ve = np.zeros((4, 4), np.float64)
for i in range(4):
Ve += V[i]*self.V_base[i]
Ve *= (self.elements[e].volume/120)
return Ve
def gen_A_element(self, e: int) -> np.ndarray:
Ae = np.zeros((4, 4), np.float64)
Ae += self.elements[e].volume*self.gen_K_matrix(e)+self.gen_V_matrix(e)
return Ae
def gen_B_element(self, e: int) -> np.ndarray:
Be = np.array([[2, 1, 1, 1], [1, 2, 1, 1], [
1, 1, 2, 1], [1, 1, 1, 2]], np.float64)
Be *= (self.elements[e].volume/20)
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(4):
i_gn = self.elements[e].global_numbers[i]
if i_gn < 0:
continue
for j in range(4):
j_gn = self.elements[e].global_numbers[j]
if j_gn < 0:
continue
self.A[i_gn][j_gn] += Ae[i][j]
self.B[i_gn][j_gn] += Be[i][j]
self.A = sps.csr_matrix(self.A)
self.B = sps.csr_matrix(self.B)
return None
def solve(self) -> None:
val, vec = spsl.eigsh(self.A, self.N_points-2, self.B,
maxiter=self.N_points*600, which='SM', sigma=0.01)
for i in range(val.shape[0]):
self.eig.append((val[i], vec[:, i]))
self.eig.sort(key=lambda x: x[0])
return None
最後にまとめたものも載せておきます.
import numpy as np
import numpy.linalg as npl
import scipy.sparse.linalg as spsl
import scipy.sparse as sps
from collections.abc import Callable
import matplotlib.pyplot as plt
class Element():
# 要素クラス
points: tuple # 4頂点の座標
global_numbers: tuple # 4頂点のグローバル節点番号
volume: float # 要素の体積
M: np.ndarray # 要素のM行列
def __init__(self, p0: tuple, p1: tuple, p2: tuple, p3: tuple, global_numbers: tuple) -> None:
self.points = (p0, p1, p2, p3)
self.global_numbers = global_numbers
S = np.concatenate(
[self.points, [[1], [1], [1], [1]]], axis=1, dtype=np.float64)
self.volume = np.abs(npl.det(S))/6
self.M = npl.inv(S)
return None
class Mesh():
# メッシュクラス
N: int # 1辺(2N-1)節点
L: float # 1辺2L
X: np.ndarray # x軸方向の節点座標
Y: np.ndarray # y軸方向の節点座標
Z: np.ndarray # z軸方向の節点座標
elements: list # 要素を保持
indices: tuple = ((0, 1, 2, 4), (1, 4, 5, 7),
(1, 2, 3, 7), (2, 4, 6, 7), (1, 2, 4, 7)) # 要素生成用のインデックス
def __init__(self, N: int, L: float) -> None:
self.N = N
self.L = L
self.X = np.concatenate(
[np.linspace(-L, 0, N-1, endpoint=False), np.linspace(0, L, N, endpoint=True)])
self.Y = np.concatenate(
[np.linspace(-L, 0, N-1, endpoint=False), np.linspace(0, L, N, endpoint=True)])
self.Z = np.concatenate(
[np.linspace(-L, 0, N-1, endpoint=False), np.linspace(0, L, N, endpoint=True)])
self.elements = []
return None
def gen_mesh(self) -> None:
# 直方体ごとに要素を生成する. 1つの直方体を5つの四面体に分割する.
for i in range(2*self.N-2):
for j in range(2*self.N-2):
for k in range(2*self.N-2):
p = []
gn = []
# 直方体の8節点の座標とグローバル節点番号の割り当て
for l in range(2):
for m in range(2):
for n in range(2):
p.append(
(self.X[i+l], self.Y[j+m], self.Z[k+n])) # 節点を追加
if i+l == 0 or j+m == 0 or k+n == 0:
# 境界のグローバル節点番号は-1
gn.append(-1)
elif i+l == (2*self.N-2) or j+m == (2*self.N-2) or k+n == (2*self.N-2):
# 境界のグローバル節点番号は-1
gn.append(-1)
else:
# グローバル節点番号を計算して割り当て
gn.append((i+l-1)+(2*self.N-3) *
(j+m-1)+((2*self.N-3)**2)*(k+n-1))
# 要素生成
for idx in self.indices:
points = (p[idx[0]], p[idx[1]], p[idx[2]], p[idx[3]])
gns = (gn[idx[0]], gn[idx[1]], gn[idx[2]], gn[idx[3]])
self.elements.append(Element(*points, gns))
return None
class SchrodingerFEM3():
DIRAC_CONSTANT: float = 1.054571817e-34
DIRAC_CONSTANT_DOUBLE: float = DIRAC_CONSTANT**2
N: int # 要素数
N_points: int # 境界を除いた節点数
m: float # 質量
V: Callable[[float, float, float], float] # ポテンシャル
A: np.ndarray # A行列
B: np.ndarray # B行列
elements: list # 要素を保持
eig: list # 計算結果を保持
# ポテンシャル項の計算用行列
V_base: np.array = np.array([[[6, 2, 2, 2], [2, 2, 1, 1], [2, 1, 2, 1], [2, 1, 1, 2]], [[2, 2, 1, 1], [2, 6, 2, 2], [1, 2, 2, 1], [1, 2, 1, 2]],
[[2, 1, 2, 1], [1, 2, 2, 1], [2, 2, 6, 2], [1, 1, 2, 2]], [[2, 1, 1, 2], [1, 2, 1, 2], [1, 1, 2, 2], [2, 2, 6, 6]]], np.float64)
def __init__(self, N_points: int, m: float, V: Callable[[float, float, float], float], elements: list) -> None:
self.N = len(elements)
self.N_points = N_points
self.m = m
self.V = V
self.elements = elements
self.A = np.zeros((self.N_points, self.N_points))
self.B = np.zeros((self.N_points, self.N_points))
self.eig = []
return None
def gen_K_matrix(self, e: int) -> np.ndarray:
Ke = np.zeros((4, 4), np.float64)
for i in range(4):
for j in range(4):
for k in range(3):
Ke[i][j] += self.elements[e].M[k][i] * \
self.elements[e].M[k][j]
Ke *= (self.DIRAC_CONSTANT_DOUBLE/(2*self.m))
return Ke
def gen_V_matrix(self, e: int) -> np.ndarray:
# 要素の各頂点におけるポテンシャルの値
V = [self.V(p[0], p[1], p[2]) for p in self.elements[e].points]
Ve = np.zeros((4, 4), np.float64)
for i in range(4):
Ve += V[i]*self.V_base[i]
Ve *= (self.elements[e].volume/120)
return Ve
def gen_A_element(self, e: int) -> np.ndarray:
Ae = np.zeros((4, 4), np.float64)
Ae += self.elements[e].volume*self.gen_K_matrix(e)+self.gen_V_matrix(e)
return Ae
def gen_B_element(self, e: int) -> np.ndarray:
Be = np.array([[2, 1, 1, 1], [1, 2, 1, 1], [
1, 1, 2, 1], [1, 1, 1, 2]], np.float64)
Be *= (self.elements[e].volume/20)
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(4):
i_gn = self.elements[e].global_numbers[i]
if i_gn < 0:
continue
for j in range(4):
j_gn = self.elements[e].global_numbers[j]
if j_gn < 0:
continue
self.A[i_gn][j_gn] += Ae[i][j]
self.B[i_gn][j_gn] += Be[i][j]
self.A = sps.csr_matrix(self.A)
self.B = sps.csr_matrix(self.B)
return None
def solve(self) -> None:
val, vec = spsl.eigsh(self.A, self.N_points-2, self.B,
maxiter=self.N_points*600, which='SM', sigma=0.01)
for i in range(val.shape[0]):
self.eig.append((val[i], vec[:, i]))
self.eig.sort(key=lambda x: x[0])
return None
if __name__ == '__main__':
N = 8
L = 3.
m = SchrodingerFEM3.DIRAC_CONSTANT_DOUBLE
mesh = Mesh(N, L)
mesh.gen_mesh()
fem = SchrodingerFEM3((2*N-3)**3, m, lambda x, y, z: 0., mesh.elements)
fem.gen_matrix()
fem.solve()
for e in fem.eig[:30]:
print(e[0])
参考
mutsumunemitsutan. Hatena Blog 数学とか語学とか楽しいよね. "【有限要素法】2次元有限要素法における面積座標(局所座標)の積分公式". 2018/10/08. https://mathlang.hatenablog.com/entry/2018/10/08/122539
Discussion