日々の雑記帳

日々感じたこと、学んだことの備忘録

simpleFoam流体力履歴のプロット

概要・背景

物体周りの流れの解析を定常解析で実施する場合、特に形状が複雑な場合、目標残差に収束せず計算が何ステップにわたり継続し、計算がなかなか終了できない状況も起こりうる。

そのような場合、残差だけではなく、評価したい物理量の履歴をプロットして、その変動が落ち着いている(変動幅が一定範囲内にあるか、周期的であるか等)ことを確認して計算の終了を判断することも一つの手段である。

評価する物理量としては、解析する対象により様々であるが、物体周りの流れ解析の場合は、例えば以下のものが代表的であろう。

  • 物体に作用する流体力(圧力、粘性力、圧力と粘性力の合成)
  • 抗力
  • 揚力
  • モーメント

ここでは、OpenFOAMのsimpleFomaソルバー(定常乱流場解析)で出力される物体に作用する流体力(圧力、粘性力、圧力と粘性力の合成)の履歴をプロットするPythonスクリプトを作成したメモである。

使用環境

使用環境は以下になる。
- OpenFOAM ver2312 - Python 3.10

OpenFOAMにおける設定

simpleFoamの計算で物体に作用する流体力を出力するには、functionObjectの機能を用いる。具体的には、/system/controlDictファイルに以下の出力指示を追加する。設定項目の詳細説明は追々調査して記事にしてみようと思う。

なお、ここではmotorbikeのチュートリアルで実施した。

functions
{
    forces1
    {
        type    forces;
        libs    (forces);
        writeControl timeStep;
        timeInterval 1;
        patches (motorBikeGroup);
        rho     rhoInf;
        log     true;
        rhoInf  1;
        CofR    (0.72 0 0);
        pitchAxis   (0 1 0);
    }
}

流体力の出力

流体力は以下のとおり、/postProcessing/ディレクトリに出力される。
- 流体力の履歴 /postProcessing/forces1/0/force.dat

流体力のプロット

上記で出力された流体力の履歴データファイルを読み込み、プロットする。 以下、プロットするのに作成したPythonスクリプトを示す。

流体力の履歴をプロットするPythonスクリプト例。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os

# Forceダータの読み込み
def read_force_dat():
    df_force = np.loadtxt("./postProcessing/forces1/0/force.dat")
    df_force = pd.DataFrame(df_force, columns=['Time','total_x','total_y','total_z','pressure_x','pressure_y','pressure_z','viscous_x','viscous_y','viscous_z'])
    return df_force

# 読み込んだForceデータのプロット
def force_plot(df):
    x = ['Time']
    y1 = ['total_x','total_y','total_z']
    y2 = ['pressure_x','pressure_y','pressure_z']
    y3 = ['viscous_x','viscous_y','viscous_z']

    for i in range(len(y1)):
        plt.plot(df[x[0]], df[y1[i]],label=y1[i])
        plt.grid()
        plt.legend(loc='best', bbox_to_anchor=(1, 1))
        plt.xlabel(x[0])
        plt.ylabel('force_total')
    plt.savefig("force_total.png")
    plt.show()
    
    for i in range(len(y2)):
        plt.plot(df[x[0]], df[y2[i]],label=y2[i])
        plt.grid()
        plt.legend(loc='best', bbox_to_anchor=(1, 1))
        plt.xlabel(x[0])
        plt.ylabel('force_pressure')
    plt.savefig("force_pressure.png")
    plt.show()

    for i in range(len(y3)):
        plt.plot(df[x[0]], df[y3[i]],label=y3[i])
        plt.grid()
        plt.legend(loc='best', bbox_to_anchor=(1, 1))
        plt.xlabel(x[0])
        plt.ylabel('force_viscous')
    plt.savefig("force_viscous.png")
    plt.show()

# メイン処理
df = read_force_dat()
force_plot(df)

以下に上記Pythonスクリプトでプロットした図を示す。
流体力の履歴としては以下の3種類出力される。
- トータル(圧力+粘性力) - 圧力 - 粘性力   

履歴のプロットから分かるように、流体力は圧力の寄与分が支配的である。

流体力の履歴プロット(圧力+粘性力)

流体力の履歴プロット(圧力)

流体力の履歴プロット(粘性)

simpleFoamの残差プロット

概要

本記事は、OpenFOAMのsimpleFomaソルバー(定常乱流場解析)の残差をプロットするPythonスクリプトを作成したメモである。

使用環境

使用環境は以下になる。
- OpenFOAM ver2312 - Python 3.10

OpenFOAMにおける設定

simpleFOAMの計算において、logファイルに残差が出力されるが、logファイルだけでは残差を抽出するには、少々フォーマットが煩雑で扱いにくい。幸い、OpemFOAMでは残差だけを抽出するような設定が準備されている。以下のように、/system/controlDictファイルに残差を出力するように命令を追記する。
continuityError1は連続式の残差(保存則)、residualsは速度、乱流諸量の残差の出力設定である。

functions
{
    continuityError1
    {
        type  continuityError;
        libs
        (
            fieldFunctionObjects
        ); // Mandatory enties (unmodifiable) // Optional entries (runtime modifiable)
        phi phi;  //Optional (inrerited) entries
        writePrecision 8;
        writeToFile yes;
        useUserTime yes;
        region region0;
        enabled yes;
        log yes;
        timeStart 0;
        timeEnd 500;
        executeControl timeStep;
        executeInterval 1;
        writeControl timeStep;
        writeInterval 50;
    }

    residuals
    {
        type    solverInfo;
        libs    ("libutilityFunctionObjects.so");
        fields  (U p k omega);
    }
}

残差の出力

残差は以下のとおり、/postProcessing/ディレクトリに出力される。
- 連続式の残差 /postProcessing/continuityError1/0/continuityError.dat - 流速、乱流諸量の残差 /postProcessing/residuals/0/solverInfo.dat

残差のプロット

上記で出力された残差データファイルを読み込み、プロットする。 以下、プロットするのに作成したPythonスクリプトを示す。

流速および乱流諸量の残差をプロットするPythonスクリプト例。

import pandas as pd
import matplotlib.pyplot as plt
import os

# 残差ファイルの読み込み
def read_residual():
    df_residual = pd.read_table('./postProcessing/residuals/0/solverInfo.dat', skiprows=1)
    df_residual = pd.DataFrame(df_residual)
    return df_residual
    #print(df_residual.columns)

# 残差プロット
def residual_plot(df):
    init_res = [data for data in df.columns if "initial" in data]
    final_res = [data for data in df.columns if "final" in data]

    # initial residualプロット
    df.plot(x="# Time        ", y=init_res)
    plt.grid()
    plt.legend(loc='best', bbox_to_anchor=(1, 1))
    plt.yscale('log')
    plt.xlabel('time')
    plt.ylabel('initial residual')
    plt.show()

    # final residualプロット
    df.plot(x="# Time        ", y=final_res)
    plt.grid()
    plt.legend(loc='best', bbox_to_anchor=(1, 1))
    plt.yscale('log')
    plt.xlabel('time')
    plt.ylabel('final residual')
    plt.show()

df = read_residual()
residual_plot(df)

連続の式の残差(保存則)をプロットするPythonスクリプト例。
連続の式の残差データファイルは、区切りスペースが統一されていないため、まずnumpyのloadtxt関数で読み込み、numpy配列とした後にpandasでデータフレーム化している。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os

# 連続の式の残差ファイルを読み込む
def read_continuity_error():
    df_continuity = np.loadtxt("./postProcessing/continuityError1/0/continuityError.dat")
    df_continuity = pd.DataFrame(df_continuity, columns=['Time','Local','Global','Cumulative'])
    return df_continuity

# 連続の式の残差プロット
def continuity_error_plot(df):
    x = ['Time']
    y = ['Local', 'Global', 'Cumulative']

    for i in range(len(y)):
        plt.plot(df[x[0]], df[y[i]])
        plt.grid()
        plt.legend(loc='best', bbox_to_anchor=(1, 1))
        plt.xlabel(x[0])
        plt.ylabel(y[i] + ' ' + 'continuity')
        plt.show()

df = read_continuity_error()
continuity_error_plot(df)

以下に上記Pythonスクリプトでプロットした図を示す。
流速、乱流諸量の残差は、initialとfinalの2種類、連続式の残差はLocal、Global、Cumulativeの3種類ある。これらの違いは追々調査してまとめる。

流速、乱流諸量の残差プロット1

流速、乱流諸量の残差プロット2

連続式の残差プロット1

連続の式の残差プロット2

連続の式の残差プロット3
連続の式の残差プロット3

reactingFoam機能確認その1

概要

OpenFOAMでは、化学反応や燃焼を計算するソルバーとして、reactingFoamが用意されている。本記事は、reactingFoamの基本的な機能を確認した際のメモである。

実施した例題

reactingFoam内にあるチュートリアルのうち、以下の例題を実施した。
- counterFlowFlame2D

燃焼計算を行う場合

チュートリアルファイルを変更せず、そのまま実行する。

blockMesh
reactingFoam

以下結果を示す。

温度 0.05sec

温度 0.5sec

メタン濃度 0.05sec

メタン濃度0.5sec

酸素濃度 0.05sec

酸素濃度 0.5sec

上記の結果より、左側より燃料(メタン)、右側より空気(酸素)が流入し、中央部で燃料と空気が混合、燃焼反応が生じて温度の上昇が見られる。

コールドフロー計算を行う場合

次は、反応を伴わない、つまりコールドフローの計算を行うことを考える。例えば、化学種の移流拡散計算を行うことに相当する。
反応を伴わない計算を実行する場合、constant/chemistryPropertiesファイルとconstant/combustionPropertiesファイルを編集する必要がある。
具体的には以下を変更する。
- constant/chemistryPropertiesの場合
chemistry on → offに変更.

  • constant/combustionPropertiesの場合
    active true → fasleに変更.

chemistryProperties変更点
.

combustionProperties変更点
.

下記に結果を示す。
左側からメタン、右側から酸素が流入しているが、燃焼反応がせず温度上昇は見られないことより、コールドフローの計算がされていることが確認できる。

温度 0.05sec
.

温度 0.5sec
.

メタン濃度 0.05sec
.

メタン濃度 0.5sec
.

酸素濃度 0.05sec
.

酸素濃度 0.5sec
.

M1 Mac OpenFOAM v2312インストールメモ

概要

昨年の5月にOpenFOAM v2112をインストールをしていたが、その後更新されたバージョンのインストールが滞っていた。

これは、最新バージョン(2024年3月30日現在)をインストールした際のメモである。

前回(2022年5月11日)同様に、以下のGabriel GerleroさんのGithubサイトを参考にした。
https://github.com/gerlero/openfoam-app    

手順

Homebrew経由でインストールするのは前回と同様。

  • Homebrewを更新
brew update
  • OpenFOAM v2312のインストール(Homebrew経由)
brew install --no-quarantine gerlero/openfoam/openfoam2312
  • アプリケーションにOpenFOAM-v2312.appが追加される

  • OpenFOAM v2312セッションのターミナルを開く

OpenFOAM v2312を動かす

試しにsimpleFoamのチュートリアルであるmotorbikeの例題を実行してみる。 チュートリアルフォルダ(echo $FOAM_TUTORIALS$でチュートリアルフォルダを確認できる)から計算実行フォルダ(ユーザ指定)に設定ファイル類をコピーし、Allrunコマンドで実行。 エラーなく、実行できたことを確認。

Pythonによる計算物理(6)

前回、ロジスティック方程式について、数値解法を自作の関数を作成して解いた。

akh1116.hatenablog.com

今回は、scipyライブラリのintegrateモジュールを用いてコーディングをし、解いてみる。 自作で関数を作成するより、モジュールに渡す引数に注意すれば、簡潔かつバグも生じにくいと感じる。 以下、プログラム例である。

import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

# dy/dt=f(t,y)の定義
def f_logistic(t, y):
    return y * (1.0 - y)    # ロジスティック方程式

# 解析解
def log_anlytical(y0, t):
    return y0 / (y0 + (1.0 - y0) * np.exp(-t))


# グラフ作成
def plot(t_sparse, y_sparse, t_dense, y_dense, y_exact, filename):
    fig, ax = plt.subplots()
    ax.plot(t_sparse, y_sparse, 'o', zorder=2, color='r', markerfacecolor='None', label="selected time points")
    ax.plot(t_dense, y_dense, '.', zorder=1, color='b', label="dense_output")
    ax.plot(t_dense, y_exact, '-', zorder=1.5, color='c', label="exact")
    ax.axhline(y=0, color='k', linestyle='dashed', zorder=0)   # 横線
    ax.axhline(y=1, color='k', linestyle='dashed', zorder=0)   # 横線
    ax.set_xlabel(r'$t$')  # xラベル(LaTeX表記)
    ax.set_ylabel(r'$y$')  # yラベル(LaTeX表記)
    ax.legend()            # 凡例
    plt.show()
    fig.savefig(filename)

# メイン処理
def main():
    y0 = np.array([1e-3,], dtype=float)      # 初期値(1成分のNumpy配列)
    t_start = 0     # 初期時刻
    t_end = 20.0    # 最終時刻

    # Scipy integrateモジュールで微分方程式を解く
    sol = solve_ivp(f_logistic, (t_start, t_end), y0, dense_output=True)
    print(sol.message)    # ソルバーのメッセージを表示
    print("sol.t.shape=", sol.t.shape)    # (n_points,)
    print("sol.y.shape=", sol.y.shape)    # (1, n_points)

    # グラフ描画用のデータ(dense_output)
    nt = 101     # 時刻の解像度を100分割
    t = np.linspace(t_start, t_end, nt)    # 時刻の等間隔メッシュを生成
    y = sol.sol(t)     # メッシュ点上の時刻tにおけるy(t)の値を取得

    print("t.shape=", t.shape)
    print("y.shape=", y.shape)

    # グラフ作成
    dirname = "image/"
    filename = dirname + 'logistic_scipy.png'
    plot(sol.t, sol.y[0], t, y[0], log_anlytical(y0, t), filename)



if __name__ == '__main__':
    main()

以下、計算結果のプロット図である。
dense_outputを有効にし、任意の時刻での結果を出力させている。 実際は、モジュール内で時間刻みを調整してるようで、より少ないステップ数で効率良く計算してようである。

Pythonによる計算物理(5)

今回は、Pythonによる常微分方程式を解く際によく使われるscipyライブラリのメモである。

Scipyによる常微分方程式

  • Pythonを用いて常微分方程式を解く際は、ライブラリを用いるのが最も手軽で、かつ高精度な結果が得られる。
  • 常微分方程式を解くライブラリとして、Scipyがある。
  • Scipyの中のscipy.integrateモジュールに含まれているslove_ivp関数を使う。solve_ivpのivpは"Initial Value Problem"の略である。
  • Pythonコード中では、以下のように呼び出す。
from scipy.integrate import solve_ivp
  • 使い方は、以下のようなコーディングの形となる。
scipy.integrate.solve_ivp(fun, t_span, y0, methid='RK5', t_eval=None, dense_output=False, events=None, vectorized=False, arags=None, **options)

solve_ivp関数の引数は次の表に示す。

 引数 説明
fun(関数オブジェクト) 微分方程式の右辺$\boldsymbol{f}(t,\boldsymbol{y})$に対応する関数。$fun(t,y)$のように定義した関数を与える。$fun(t,y,a,b)$のように、他に引数を追加ししても構わない。方程式に含まれるパラメータの指定などに使う。この場合、$a$、$b$の値はargs引数を使って、args=(1.0,-3.5)のように与える。
t_span(tuple) 時刻の最初と最後をt_span=(0,10.0)やt_span=[0,10.0]のようにタプルやリストの形式で与える。
y0(ndarray(n,)) 初期値をNumpy配列として与える。
method(str) アルゴリズムを指定する。
dense_output(bool) 任意の時刻における結果を得るには、Trueにしておく必要がある。
events(関数オブジェクト) ある条件を満たす時刻tを求めたい場合に使用する。例えば、球が地面に到達する時刻を知りたい場合など。
args(tuple) funに与えた関数がパラメータ(3つ目以降の引数)をとる場合に、パラメータの値を入力する。
戻り値 説明
t(np.ndarray(n_points,)) 自動的に選ばれた時刻tの配列
y(np.ndarray(n,n_points)) 時刻tにおける結果$\boldsymbol{y}(t)$の配列。
sol(関数オブジェクト) sol(t_dense)のように呼び出すことにより、任意の時刻t_denseにおける結果が得られる(dense_ouput=Trueの場合のみ)
message(str) 計算終了時の状態を表す短い文

次にmethodに指定できるアルゴリズムを次の表に示す。なお、アルゴリズムの説明は以下のscipyの公式マニュアルを参照している。
https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html

オプション アルゴリズム
'RK5' (default)Explicit Runge-Kuttta method of order 5(4)
'RK2' Explicit Runge-Kutta method of order 3(2)
'DOP853' Explicit Runge-Kutta method of order 8
'Radau' Implicit Runge-Kutta method of the Radau IIA family of order 5
'BDF' Implicit multi-step variable-order(1 to 5) method based on a backward diffenciation formula for the derivative approximation
'LSODA' Adams/BDF method with automatic stiffness detection and switching

Pythonによる計算物理(4)

常微分方程式数値計算例題-ロジスティック方程式

ここでは、常微分方程式数値計算の例題として、ロジスティック方程式を扱ってみる。

ロジスティック方程式

ロジスティック方程式は、生物の個体数の増加を表すモデルであり、以下のような形の方程式として表される。


\frac{dy}{dt}=y(1-y)

ここで、 yは生物の個体数を最大値で規格化した無次元数で、 0 \le y \le 1の領域で与えられる。

解析解

ロジスティック方程式の解析解は以下の形で表される。


y(t)=\frac{y_0}{y_0+(1-y_0)e^{-t}}

ここで、 y_0=y(0)は初期条件であり、時刻 t=0における個体数である。

数値計算

数値計算の手法として、オイラー法、中点法、ルンゲ・クッタ法の3手法を試し、解析解との比較をしてみた。

コード例(自作関数使用)

まず、Scipyなどの常微分方程式のソルバーを使わず、自分でオイラー法、中点法、ルンゲ・クッタ法を関数として自作したコード例を示す。

# このプログラムではロジスティック方程式をPythonで数値計算を行う
# Scipyなどのライブラリを使わないバージョン
# Coded by AKH1116  2024.2.3

import numpy as np
import matplotlib.pyplot as plt
import os

# dy/dt=f(y)の定義
def f_logistic(y):
    return y * (1 - y)    # ロジスティック方程式

# オイラー法による時間発展
def euler(f, y, dt):
    return y + f(y) * dt

# 中点法による時間発展
def euler_midpoint(f, y, dt):
    k1 = f(y)
    k2 = f(y+k1 * 0.5 * dt)
    return y + k2 * dt

# ルンゲ・クッタ法による時間発展
def runge_kutta(f, y, dt):
    k1 = f(y)
    k2 = f(y + k1 * dt/2)
    k3 = f(y + k2 * dt/2)
    k4 = f(y + k3 * dt)
    return y + (k1 + 2*k2 + 2*k3 + k4) * dt / 6

# 常微分方程式を解く関数
# f: 関数f(y)
# y0: 初期値
# tmax: 終了時間
# nt: 時間の分割数
def solve_ode_euler(f, y0, tmax, nt):
    t = np.linspace(0, tmax, nt)
    dt = t[1] - t[0]
    y = [y0,]   # 結果を格納するリスト
    for _ in range(nt-1):   # 時間ループ
        y.append(euler(f, y[-1], dt))    # 時刻を1つ進めて結果をリストyに追加
    return t, np.array(y)                # 結果をNumpy配列として返す

def solve_ode_euler_midpoint(f, y0, tmax, nt):
    t = np.linspace(0, tmax, nt)
    dt = t[1] - t[0]
    y = [y0,]   # 結果を格納するリスト
    for _ in range(nt-1):   # 時間ループ
        y.append(euler_midpoint(f, y[-1], dt))    # 時刻を1つ進めて結果をリストyに追加
    return t, np.array(y)                         # 結果をNumpy配列として返す

def solve_ode_euler_rk(f, y0, tmax, nt):
    t = np.linspace(0, tmax, nt)
    dt = t[1] - t[0]
    y = [y0,]   # 結果を格納するリスト
    for _ in range(nt-1):   # 時間ループ
        y.append(runge_kutta(f, y[-1], dt))    # 時刻を1つ進めて結果をリストyに追加
    return t, np.array(y)                         # 結果をNumpy配列として返す

def exact_sol(y0, tmax, nt):
    t = np.linspace(0, tmax, nt)
    dt = t[1] - t[0]
    return t, y0 / (y0 + (1 - y0) * np.exp(-t))


def main():
    # 画像を保存するディレクトリ、ファイル名を指定
    dirname = "image/"
    filename = dirname + 'logistic.png'

    # ロジスティック方程式を解く
    t, y1 = solve_ode_euler(f_logistic, y0=1e-3, tmax=20, nt=101)
    t, y2 = solve_ode_euler_midpoint(f_logistic, y0=1e-3, tmax=20, nt=101)
    t, y3 = solve_ode_euler_rk(f_logistic, y0=1e-3, tmax=20, nt=101)
    t, y4 = exact_sol(y0=1e-3, tmax=20, nt=101)

    # グラフを描画
    fig, ax = plt.subplots()
    ax.plot(t, y1, 'x-', label='Euler')
    ax.plot(t, y2, 'o-', label='Euler Midpoint')
    ax.plot(t, y3, '+-', label='Runge-Kuuta')
    ax.plot(t, y4, ':', label='Excat')
    ax.set_xlabel('t')
    ax.set_ylabel('y')
    ax.legend()
    plt.show()
    fig.savefig(filename)
    print("Run!")

if __name__ == '__main__':
    main()

数値計算結果

以下に数値計算結果を示す。オイラー法よりも、中点法、ルンゲ・クッタ法といった高次精度の手法では解析解とほぼ同じ結果が得られている。