格子ボルツマン法による流体シミュレーション (Python)
1年次の時のレポートを記事化したものです。元はProcessingによるシミュレーションでした。
格子ボルツマン法
計算方法の概要
流体解析(流体シミュレーション, computational fluid dynamics : CFD)を行う上では、連続体である流体の運動を離散化して計算する必要があります。このとき計算する方程式は次式で表される、流体の運動量保存則です.
この式はNavier-Stokes方程式と呼ばれます。ここで、は密度、は圧力、は粘性抵抗、は外力であり、は流れの速度場のLagrange微分です。よって
が成り立ちます。また、質量の保存則として連続の式
があります。このような偏微分方程式で表される質量・運動量・エネルギーの保存則を離散化して数値的に解く方法としては有限差分法、有限要素法、有限体積法などがあります。また、流体を粒子の集合に分割し、流体の方程式に従う粒子の動きを計算します方法を粒子法と呼びます。今回用いる格子ボルツマン法(Lattice Boltzmann Method;LBM)とは、これらに属さない方法であり、流体を有限個の種類の速度を持つ仮想的な粒子の集合とみなし、それらの粒子の衝突と並進を計算します。流体を連続体ではなく粒子として扱う点は粒子法に似ていますが、LBMではあくまでも仮想的な粒子の分布を扱います。質量・運動量・エネルギーの保存則を巨視的(マクロ)な方程式であるとすれば、LBMが解く方程式は統計熱力学に基づく微視的 (ミクロ) な方程式と言えます。
9つの速度は
で表されます。ここでは定数ですが、実装上は簡単のためとします。なお、この2D9Qモデルですが、数式で表記する場合には数字で方向をラベリングするほうがよいです。逆に実装する上では方角による方向のラベリングを行う方がミスがなくなりやすいでしょう (下図:2D9Q のラベリング)。
実装上ではの取り扱いが重要とりますが、BGK (Bhatnagar-Gross-Krook)近似に基づく衝突則を用いられることが多いです。BGK近似モデルを用いたLBMの基礎方程式は次式で表されます。
ここで、は単一緩和時間です。 実装上はとします。なお、粘度(動粘性抵抗)と単一緩和時間およびとの間には
が成り立ちます。
計算のアルゴリズム
アルゴリズムは以下のようになります.
-
密度, 流速, 速度分布関数, 平衡分布関数を初期化します。今回の場合は、左から右に流れが生じている場合を考えるのでは方向のみ大きさを持ちます。また、計算開始前、分布関数は平衡に達していたとします。よって次式のようになります。
-
密度, 流速をから(4), (5), (6)式によって求めます。
-
平衡分布関数を(3)式より計算します。
-
(Streaming Step) (2)式の左辺により、並進後の分布関数を次のように計算します。
-
(Bounce Step) 5において並進した格子に障害物(境界)が存在した場合、bounce-back条件により、修正します。
-
(Collision Step) 仮想粒子の衝突を(2)式の右辺により次のように計算します。ただし、は仮想粒子が衝突し、並進する前の分布関数です。
-
2~6を繰り返します。
Pythonでの実装
ここからはPythonでの実装について解説していきます。なお、コードはFluid Dynamics Simulationを参考にしています。Processing版はOpenProcessingで公開しています (https://www.openprocessing.org/sketch/497312/)。
ライブラリをimportします。このとき、%matplotlib nbagg
を付ければipynb上でアニメーションが実行できます。
%matplotlib nbagg
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
各種定数を定義していきます。
# Define constants:
height = 80 # lattice dimensions
width = 200
viscosity = 0.02 # fluid viscosity
omega = 1 / (3*viscosity + 0.5) # "relaxation" parameter
u0 = 0.1 # initial and in-flow speed
four9ths = 4.0/9.0 # abbreviations for lattice-Boltzmann weight factors
one9th = 1.0/9.0
one36th = 1.0/36.0
格子の初期化を(3)式に基づいて行います。
# Initialize all the arrays to steady rightward flow:
n0 = four9ths * (np.ones((height,width)) - 1.5*u0**2) # particle densities along 9 directions
nN = one9th * (np.ones((height,width)) - 1.5*u0**2)
nS = one9th * (np.ones((height,width)) - 1.5*u0**2)
nE = one9th * (np.ones((height,width)) + 3*u0 + 4.5*u0**2 - 1.5*u0**2)
nW = one9th * (np.ones((height,width)) - 3*u0 + 4.5*u0**2 - 1.5*u0**2)
nNE = one36th * (np.ones((height,width)) + 3*u0 + 4.5*u0**2 - 1.5*u0**2)
nSE = one36th * (np.ones((height,width)) + 3*u0 + 4.5*u0**2 - 1.5*u0**2)
nNW = one36th * (np.ones((height,width)) - 3*u0 + 4.5*u0**2 - 1.5*u0**2)
nSW = one36th * (np.ones((height,width)) - 3*u0 + 4.5*u0**2 - 1.5*u0**2)
rho = n0 + nN + nS + nE + nW + nNE + nSE + nNW + nSW # macroscopic density
ux = (nE + nNE + nSE - nW - nNW - nSW) / rho # macroscopic x velocity
uy = (nN + nNE + nNW - nS - nSE - nSW) / rho # macroscopic y velocity
障害物の位置を初期化します。このとき、障害物内の粒子の数は0とします。
# Initialize barriers:
barrier = np.zeros((height,width), bool) # True wherever there's a barrier
barrier[int((height/2)-8):int((height/2)+8), int(height/2)] = True # simple linear barrier
barrierN = np.roll(barrier, 1, axis=0) # sites just north of barriers
barrierS = np.roll(barrier, -1, axis=0) # sites just south of barriers
barrierE = np.roll(barrier, 1, axis=1) # etc.
barrierW = np.roll(barrier, -1, axis=1)
barrierNE = np.roll(barrierN, 1, axis=1)
barrierNW = np.roll(barrierN, -1, axis=1)
barrierSE = np.roll(barrierS, 1, axis=1)
barrierSW = np.roll(barrierS, -1, axis=1)
Streaming StepとBounce-back stepです。np.roll
により仮想粒子を移動させます。このとき, 粒子の進行方向と逆向きの順で格子を選び, 分布関数を更新します.
# Move all particles by one step along their directions of motion (pbc):
def stream():
global nN, nS, nE, nW, nNE, nNW, nSE, nSW
nN = np.roll(nN, 1, axis=0) # axis 0 is north-south; + direction is north
nNE = np.roll(nNE, 1, axis=0)
nNW = np.roll(nNW, 1, axis=0)
nS = np.roll(nS, -1, axis=0)
nSE = np.roll(nSE, -1, axis=0)
nSW = np.roll(nSW, -1, axis=0)
nE = np.roll(nE, 1, axis=1) # axis 1 is east-west; + direction is east
nNE = np.roll(nNE, 1, axis=1)
nSE = np.roll(nSE, 1, axis=1)
nW = np.roll(nW, -1, axis=1)
nNW = np.roll(nNW, -1, axis=1)
nSW = np.roll(nSW, -1, axis=1)
# Use tricky boolean arrays to handle barrier collisions (bounce-back):
nN[barrierN] = nS[barrier]
nS[barrierS] = nN[barrier]
nE[barrierE] = nW[barrier]
nW[barrierW] = nE[barrier]
nNE[barrierNE] = nSW[barrier]
nNW[barrierNW] = nSE[barrier]
nSE[barrierSE] = nNW[barrier]
nSW[barrierSW] = nNE[barrier]
Collide stepの関数を定義します。
# Collide particles within each cell to redistribute velocities (could be optimized a little more):
def collide():
global rho, ux, uy, n0, nN, nS, nE, nW, nNE, nNW, nSE, nSW
rho = n0 + nN + nS + nE + nW + nNE + nSE + nNW + nSW
ux = (nE + nNE + nSE - nW - nNW - nSW) / rho
uy = (nN + nNE + nNW - nS - nSE - nSW) / rho
ux2 = ux * ux # pre-compute terms used repeatedly...
uy2 = uy * uy
u2 = ux2 + uy2
omu215 = 1 - 1.5*u2 # "one minus u2 times 1.5"
uxuy = ux * uy
n0 = (1-omega)*n0 + omega * four9ths * rho * omu215
nN = (1-omega)*nN + omega * one9th * rho * (omu215 + 3*uy + 4.5*uy2)
nS = (1-omega)*nS + omega * one9th * rho * (omu215 - 3*uy + 4.5*uy2)
nE = (1-omega)*nE + omega * one9th * rho * (omu215 + 3*ux + 4.5*ux2)
nW = (1-omega)*nW + omega * one9th * rho * (omu215 - 3*ux + 4.5*ux2)
nNE = (1-omega)*nNE + omega * one36th * rho * (omu215 + 3*(ux+uy) + 4.5*(u2+2*uxuy))
nNW = (1-omega)*nNW + omega * one36th * rho * (omu215 + 3*(-ux+uy) + 4.5*(u2-2*uxuy))
nSE = (1-omega)*nSE + omega * one36th * rho * (omu215 + 3*(ux-uy) + 4.5*(u2-2*uxuy))
nSW = (1-omega)*nSW + omega * one36th * rho * (omu215 + 3*(-ux-uy) + 4.5*(u2+2*uxuy))
# Force steady rightward flow at ends (no need to set 0, N, and S components):
nE[:,0] = one9th * (1 + 3*u0 + 4.5*u0**2 - 1.5*u0**2)
nW[:,0] = one9th * (1 - 3*u0 + 4.5*u0**2 - 1.5*u0**2)
nNE[:,0] = one36th * (1 + 3*u0 + 4.5*u0**2 - 1.5*u0**2)
nSE[:,0] = one36th * (1 + 3*u0 + 4.5*u0**2 - 1.5*u0**2)
nNW[:,0] = one36th * (1 - 3*u0 + 4.5*u0**2 - 1.5*u0**2)
nSW[:,0] = one36th * (1 - 3*u0 + 4.5*u0**2 - 1.5*u0**2)
次に流速の回転の成分を計算する関数を定義します。の成分は
で表されます。偏微分項の中心差分を取ると,
となります。流れの速度場が格子状に離散化されていることから, とすべきであるので次式のように計算されます。
なお、実装上は2で割ることを省略しています。
# Compute rot of the macroscopic velocity field:
def rot(ux, uy):
return np.roll(uy,-1,axis=1) - np.roll(uy,1,axis=1) - np.roll(ux,-1,axis=0) + np.roll(ux,1,axis=0)
シミュレーションのメインの関数です。nextFrame
関数を実行することでシミュレーションが進行します。
# Here comes the graphics and animation
theFig = plt.figure(figsize=(8,3))
fluidImage = plt.imshow(rot(ux, uy), origin='lower', norm=plt.Normalize(-.1,.1),
cmap=plt.get_cmap('jet'), interpolation='none')
bImageArray = np.zeros((height, width, 4), np.uint8)
bImageArray[barrier,3] = 255
barrierImage = plt.imshow(bImageArray, origin='lower', interpolation='none')
def nextFrame(arg):
for step in range(20):
stream()
collide()
fluidImage.set_array(rot(ux, uy))
return (fluidImage, barrierImage) # return the figure elements to redraw
animate = animation.FuncAnimation(theFig, nextFrame, interval=1, blit=True)
plt.show()
参考文献
- Schroeder, D., Fluid Dynamics Simulation, (2018年1月30日閲覧)
- Guo, Z. and Shu, C., 2013, Lattice Boltzmann method and its applications in engineering, World Scientific (Singapore)
- 蔦原 道久, 他2名, 1999, 『格子気体法・格子ボルツマン法―新しい数値流体力学の手法』, コロナ社
- 太田 光浩, 他4名, 2015, 『混相流の数値シミュレーション 』, 丸善出版
- Bao, Y. and Meskas, J., 2011。Lattice Boltzmann Method for Fluid Simulations, (2018年1月30日閲覧)
- 立石 絢也, 樫山 和男, 2006, 「非圧縮性粘性流体解析のためのCIVA-格子ボルツマン法の精度と安定性」, 『日本計算工学会論文集』, 2006年度号, No.20060008