【科学技術計算講座4-7】Pythonで有限体積法プログラミング~線形方程式の作成

前回は構造格子のメッシュ情報を求めるPythonプログラムを作りました。

Pythonで有限体積法の構造格子のメッシュ情報を取得するプログラムを作ります。そのセルや隣接セルのセル番号を求めます。科学技術計算講座4「有限体積法で熱伝導シミュレーション」の第6回目です。

今回は有限体積法の計算のもとになる線形方程式を作成するプログラムを作っていきたいと思います。

線形方程式

以前4-4章で構造格子での離散化を行い(4-12)式を得ました。これは各セルの温度 $T$ の一次式の形をしています。このような式を線形方程式といいます。セル $C$ とその隣接セル $F$ に対して、一般的に書くと次のような式です。

$$ a_C T_C + \sum_F a_F T_F = b_C \tag{7-1}$$

そして、この式がセル数分あり、連立方程式を形成します。この連立方程式を解くと求めたい温度の解が求まるというわけです。

方程式をどう解くかというのは、また次回お話したいと思いますが、今回は係数 $a$ や $b_C$ を計算して解くべき線形方程式を作っておきましょう。

各係数 $a$ と $b_C$ は(4-13)式のように計算できます。もう一度書いておきます。

$$\begin{split} a_E &= -\lambda_e \frac{S_e}{d_{CE}} \\ a_W &= -\lambda_w \frac{S_w}{d_{CW}} \\ a_N &= -\lambda_n \frac{S_n}{d_{CN}} \\ a_S &= -\lambda_s \frac{S_s}{d_{CS}} \\ a_C &= - (a_E + a_W + a_N + a_S) \\ b_C &= q_C V_C \end{split} \tag{7-2} $$

境界面では少し式が違いますが、これは4-5章でお話したとおりです。面積 $S$ や 距離 $d$ は与えた形状データから求められますし、$b_C$ の項も既知の値から計算できます。

面での熱伝導率

では、$\lambda_e$ の値はどうすればよいでしょうか?これは面 $e$ での熱伝導率を表しています。熱伝導率がいたるところ同じであれば、一定値として与えればよいのですが、今回の問題では熱伝導率が場所によって異なっています。

単純に考えるとセル中心と面の位置から線形補間で求めるという方法がありますが、ここでは調和平均という方法を使って面での値を求めます。

$$\lambda_e = \frac{\lambda_C \lambda_E}{(1-g_e) \lambda_C + g_e \lambda_E} \tag{7-3}$$

ここで、$g_e = d_{Ce} / (d_{Ce} + d_{eE})$。

調和平均

これは $C-e$ 間 と $e-E$ 間の総括的な熱伝達を考えると求まります。

Pythonプログラム

これで全ての量が求められるのでPythonでプログラムを作成していきます。線形方程式の係数を求めるプログラムを以下に示します。

プログラムの表示
import numpy as np

# input parameter =====================================================
#-- geometry
lx = 1.0
ly = 1.0
nx = 50
ny = 50

dx = lx / nx
dy = ly / ny 
ncell = nx * ny 

# condition ====================================================
#-- temperature
temp = np.full(ncell+1, 300.0)

#-- thermal conductivity
cond = np.full_like(temp, 100.0)
for i in range(1, nx+1):
    for j in range(1, ny+1):
        icell = i + (j - 1) * nx
        x = (i - 0.5) * dx
        y = (j - 0.5) * dy
        if y >= 0.8:
            cond[icell] = 10.0

#-- heat source
qc = np.zeros_like(temp)   

# algebraic equations ==============================================  
def boundary(icell, iface, btype, tb=300.0, qb=0.0, ta=300.0, ht=10.0):
    condf = cond[icell]
    dcb = 0.5 * dcf[iface]

    if btype == 'dirichlet': # Dirichlet
        a_b = -condf * s[iface] / dcb
        a_f = 0.0
        a_c = -a_b
        b_c = -a_b * tb
    elif btype == 'neumann': # Neumann
        a_f = 0.0
        a_c = 0.0
        b_c = -qb * s[iface]
    elif btype == 'mixed': # Mixed                     
        htc = condf / dcb
        htf = ht * htc / (ht + htc) 
        a_b = -htf * s[iface] 
        a_f = 0.0
        a_c = -a_b
        b_c = -a_b * ta   

    return a_f, a_c, b_c  
    
s = [dy, dy, dx, dx]
dcf = [dx, dx, dy, dy] 
volume = dx * dy

ac = np.zeros_like(temp)
af = np.zeros((ncell+1, 4))
bc = np.zeros_like(temp)
cnbor = np.zeros_like(af, dtype='int')

for i in range(1, nx+1):
    for j in range(1, ny+1):
        icell = i + (j - 1) * nx
        x = (i - 0.5) * dx
        y = (j - 0.5) * dy

        ac[icell] = 0.0
        bc[icell] = qc[icell] * volume

        #-- face loop
        for iface in range(4):

            #-- face type
            if iface == 0 and i == nx: 
                ftype = 'right'
            elif iface == 1 and i == 1: 
                ftype = 'left'
            elif iface == 2 and j == ny: 
                ftype = 'top'
            elif iface == 3 and j == 1: 
                ftype = 'bottom'
            else:
                ftype = 'internal'
            
            #-- neighbor cell
            if ftype == 'internal':
                if iface == 0: inbor = icell + 1 # East
                elif iface == 1: inbor = icell - 1 # West
                elif iface == 2: inbor = icell + nx # North
                elif iface == 3: inbor = icell - nx # South
            else:
                inbor = 0    
            cnbor[icell][iface] = inbor                
            
            #-- matrix
            if ftype == 'internal': # internal face
                gf = 0.5
                condf = cond[icell] * cond[inbor] / ((1.0 - gf) * cond[icell] + gf * cond[inbor])
                a_f = -condf * s[iface] / dcf[iface]
                a_c = -a_f
                b_c = 0.0
            else: # boundary face
                if ftype == 'right' and y <= 0.2:
                    a_f, a_c, b_c = boundary(icell, iface, 'dirichlet', tb=500.0)
                elif ftype == 'top' and x <= 0.5: 
                    a_f, a_c, b_c = boundary(icell, iface, 'dirichlet', tb=300.0)  
                elif ftype == 'left':
                    a_f, a_c, b_c = boundary(icell, iface, 'mixed', ta=400.0, ht=100.0)
                else:
                    a_f, a_c, b_c = boundary(icell, iface, 'neumann', qb=0.0)

            ac[icell] += a_c
            af[icell][iface] = a_f
            bc[icell] += b_c   

        print('cell {:d} neighbor {:d} {:d} {:d} {:d}'.format(icell, cnbor[icell][0], cnbor[icell][1], cnbor[icell][2], cnbor[icell][3]))
        print('ac {:6.3f} af {:6.3f} {:6.3f} {:6.3f} {:6.3f}'.format(ac[icell], af[icell][0], af[icell][1], af[icell][2], af[icell][3]))
        print('bc {:6.3f}'.format(bc[icell]))
        print(' ')

→プログラムはこちら(GitHub)

以下、前回説明したところは端折って説明します。

import numpy as np

まずnumpy配列を使うので、numpyをインポートしておきます。

#-- temperature
temp = np.full(ncell+1, 300.0)

温度の配列 temp を作り、初期値を設定します。配列の要素番号はセル番号に対応させるため、1 からncell までとするので、ncell+1 個分の配列を作ります。これで、要素番号 0 から ncell までの配列ができます。要素番号 0 はダミーの要素としておきます。初期値は300Kとします。

#-- thermal conductivity
cond = np.full_like(temp, 100.0)
for i in range(1, nx+1):
    for j in range(1, ny+1):
        icell = i + (j - 1) * nx
        x = (i - 0.5) * dx
        y = (j - 0.5) * dy
        if y >= 0.8:
            cond[icell] = 10.0

次に熱伝導率の配列 cond を作ります。full_like(temp, 100.0) は、temp と同じ要素数で100.0の値に設定するという意味です。ひとまず熱伝導率を一様に 100W/(mK)にしています。問題を忘れた方はもう一度見直しておいてください。

次に、forループで ijを回し、y が0.8m以上のところで、熱伝導率を10W/(mK)に設定しています。

#-- heat source
qc = np.zeros_like(temp)   

qc はセルの発熱量 $q_C$ で、zeros_like(temp)temp と同じ要素数で値をゼロにするという意味です。今回の問題では特に発熱量は与えないのでゼロとしています。

s = [dy, dy, dx, dx]
dcf = [dx, dx, dy, dy] 
volume = dx * dy

少し飛ばしてこの部分ですが、s は面の面積 $S$ を表す配列で、4つの面 $e$、$w$、$n$、$s$ の順番に面積を格納しています。dcf はセル $C$ の中心と隣接セル $F$ の中心との間の距離 $d_{CF}$ を表す配列で $E$、$W$、$N$、$S$ の順番に格納されています。 volume はセルの体積です。今は均一なサイズのセルを考えているので、どのセルでもこれらの値は変わりません。

ac = np.zeros_like(temp)
af = np.zeros((ncell+1, 4))
bc = np.zeros_like(temp)
cnbor = np.zeros_like(af, dtype='int')

ここでは係数の配列を作っています。ac は係数 $a_C$、af は係数 $a_F$、bc は $b_C$ です。af は各セルに対して4つの隣接セル分必要なので、第一要素がセル番号、第二要素が4つの面のインデックスを表す2次元配列として定義しています。

cnbor はセルに隣接する4つのセル番号を格納しておくための配列です。これも、af と同じ2次元配列です。dtype='int' はその数値が整数であることを示しています。dtypeを指定しないと、デフォルトで浮動小数点数となっています。

        ac[icell] = 0.0
        bc[icell] = qc[icell] * volume

次に ij でforループを回します。まず、ac にゼロ、bc に $q_C V_C$ をセットします。

        for iface in range(4):

次に4つの面でループを回します。ftypeinbor は前回説明したとおり、面のタイプと隣接セルの番号を求めています。

cnbor[icell][iface] = inbor で、cnbor に隣接セル番号を格納します。
            #-- matrix
            if ftype == 'internal': # internal face
                gf = 0.5
                condf = cond[icell] * cond[inbor] / ((1.0 - gf) * cond[icell] + gf * cond[inbor])
                a_f = -condf * s[iface] / dcf[iface]
                a_c = -a_f
                b_c = 0.0

ここは、面のタイプが 'internal' (境界面ではない内部の面)の場合、(7-2)式から係数を計算しています。

            else: # boundary face
                if ftype == 'right' and y <= 0.2:
                    a_f, a_c, b_c = boundary(icell, iface, 'dirichlet', tb=500.0)
                elif ftype == 'top' and x <= 0.5: 
                    a_f, a_c, b_c = boundary(icell, iface, 'dirichlet', tb=300.0)  
                elif ftype == 'left':
                    a_f, a_c, b_c = boundary(icell, iface, 'mixed', ta=400.0, ht=100.0)
                else:
                    a_f, a_c, b_c = boundary(icell, iface, 'neumann', qb=0.0)

ここは面タイプが境界面の場合、境界条件を適用する部分です。例えば、if ftype == 'right' and y <= 0.2: は、境界面が 'right' (右端面)でかつ y ≦ 0.2 の場合のディリクレ条件( $T_b = 500K$ )を与えます。ここでは、boundary() という関数で各係数を計算するようにしています。

boundary() の関数はforループに入る前に定義しています。

def boundary(icell, iface, btype, tb=300.0, qb=0.0, ta=300.0, ht=10.0):
    condf = cond[icell]
    dcb = 0.5 * dcf[iface]

    if btype == 'dirichlet': # Dirichlet
        a_b = -condf * s[iface] / dcb
        a_f = 0.0
        a_c = -a_b
        b_c = -a_b * tb
    elif btype == 'neumann': # Neumann
        a_f = 0.0
        a_c = 0.0
        b_c = -qb * s[iface]
    elif btype == 'mixed': # Mixed                     
        htc = condf / dcb
        htf = ht * htc / (ht + htc) 
        a_b = -htf * s[iface] 
        a_f = 0.0
        a_c = -a_b
        b_c = -a_b * ta   

    return a_f, a_c, b_c  

関数の引数は、セル番号 icell、面インデックス iface、境界タイプ btype、ディリクレ境界の温度 tb、ノイマン境界の熱流束 qb、混合境界の外部温度 ta、熱伝達率 ht です。

tb=300.0 としているのは、呼び出し側で tb を指定しない場合、デフォルトとして 300.0K が使用されるということを意味します。

指定された境界タイプに応じて、各係数が計算されます。ディリクレ条件は(5-3)式、ノイマン条件は(5-6)式、混合条件は(5-13)式のとおりです。そして各係数を返します。

            ac[icell] += a_c
            af[icell][iface] = a_f
            bc[icell] += b_c   

面のforループの説明に戻ります。ループの最後で、acafbc に各面の係数をセットします。acbc は各面からの寄与を足し合わせています。

プログラムでは最後 print で計算された係数を出力しています。値を確認してみてください。

まとめ

今回は線形方程式の各係数を計算するプログラムを作成しました。この係数が分かればあとは線形方程式を解いていくだけです。次回は線形方程式を解く方法について説明していきます。


←前回 Pythonで有限体積法プログラミング~構造格子のメッシュ情報

→次回 共役勾配法で線形方程式系を解く

全体の目次

スポンサーリンク
科学技術計算のご相談は「キャットテックラボ」へ

科学技術計算やCAEに関するご相談、計算用プログラムの開発などお困りのことは「株式会社キャットテックラボ」へお問い合わせください。

お問い合わせはこちら

フォローする