【OpenFOAM】手法を変えて処理を追加⑥ : 継承を実行時に選択 (runTimeSelectionTable)

公開:2020/12/16
更新:2020/12/16
17 min読了の目安(約15800字TECH技術記事 1

オープンCAEアドベントカレンダー 16日目のエントリーです。

はじめに

『【OpenFOAM】手法を変えて処理を追加⑤ : 継承クラスに処理を記述する』の続きです。OpenFOAMの計算に手法を変えて処理を追加していきます。
 前回は自作クラスに対してOpenFOAMの標準クラスであるIOdictionaryを継承してみました。これによりケースに置いてあるscoreDictというファイルをソルバー実行時に読み込めるようになりました。
 しかしこれまでの作り方ではクラスを呼び出すためには継承クラスを呼び出す必要があります。もし別の継承クラスを使用したい場合には呼び出す継承クラスを変更して再度コンパイルする必要があります。今回はそれを解決すべく『どの継承クラスを呼び出すか』をscoreDictの中で指定する方法を紹介します。

手法一覧

この記事は様々な手法を紹介するシリーズの一つであり、全体としては以下の方法で行う予定です。実行確認済みのファイルはgithub.com/inabower/getScoreTutorialsで公開しています。

  • 例題を用意する ()(キーワード:pimpleFoam
  • 既存機能でやるとしたら ()(キーワード:functionObjects
  1. 計算ファイルに処理を書き込む ()(キーワード:codedFunctionObject
  2. ソルバーに処理を追加する ()
  3. 関数やクラスとして処理を追加する()
  4. 継承クラスに処理を追加()
  5. 設定項目を実行時に読み込む () (キーワード:IOdictionary
  6. 継承クラスを実行時に選択する ★今ここ(キーワード:runTimeSelectionTable
  7. クラスを別でコンパイルする(キーワード:wmake libso
  8. 継承クラスの一つとしてcodedを追加(キーワード:codedTemplete

動作環境

  • OS : Ubuntu 20.04 LTS (WSL2 on Windows 10)
  • OpenFOAM : ESI版 v2006 (from source)
  • gcc : 9.3.0

今回行いたいこと

今回実装する方法を説明します。

前回までの方法では以下のように継承クラスを呼び出していました。

// 宣言
#include "sumInletOutletScore.H"

()

// コンストラクタ
sumInletOutletScore myScore(mesh);

基本的にsumInletOutletScoreしか使わないのであればこれで何も問題ありません。ただ、もし『入口だけの流量の総和を求めるクラス』であるsumInletScoreがあったとして、これに切り替えて使用したい場合には以下のように書き換えることになります。

// 宣言
#include "sumInletOutletScore.H"
#include "sumInletScore.H"

()

// コンストラクタ
// sumInletOutletScore myScore(mesh);
sumInletScore myScore(mesh);

そして、再度コンパイルを行い実行することとなります。切り替えのたびにコンパイルを行う手間は大きいですし、この類似した継承クラスが5個10個と増えてくるとコードが煩雑になってしまい管理が難しくなります。
 OpenFOAMではこういった『継承クラスを実行時に切り替える』ための仕組みとしてrunTimeSelectionというものが用意されています。これを行うとコードは以下のように記述することができます。

// 宣言
#include "getScore.H"

()

// コンストラクタ
autoPtr<getScore> myScorePtr(getScore::New(mesh));
getScore& myScore = *myScorePtr;

それぞれの詳細については後述しますが、こうすることで『getScoreクラスから継承したクラス』をコードの中で同じように扱うことができます。そして『このクラスが何なのか』は実行時に決まることになります。

runTimeSelection

runTimeSelectionについてはOpenFOAM wiki/runTimeSelection mechanismに詳しく説明されています。これについてかいつまんで説明します。

実はrunTimeSelectionTableは普段のソルバー実行時にも頻繁に出会います。具体的にはタイプミスをした際によく見られます。例えばpimpleFoamの速度Uの境界条件をfixedValueとするところをbananaとタイプミスしてしまった場合は以下のようなメッセージが現れます。(通称バナナメソッド)

--> FOAM FATAL IO ERROR: 
Unknown patchField type banana for patch type patch

Valid patchField types :

91
(
SRFFreestreamVelocity
SRFVelocity
SRFWallVelocity
activeBaffleVelocity
activePressureForceBaffleVelocity
advective
atmBoundaryLayerInletVelocity
calculated
(長いので中略)
wedge
zeroGradient
)


file: /mnt/c/work/foam/003.class/funcCase/0/U.boundaryField.outlet1 at line 31 to 33.

    From static Foam::tmp<Foam::fvPatchField<Type> > Foam::fvPatchField<Type>::New(const Foam::fvPatch&, const Foam::DimensionedField<Type, Foam::volMesh>&, const Foam::dictionary&) [with Type = Foam::Vector<double>]
    in file /installdir/OpenFOAM-v2006/src/finiteVolume/lnInclude/fvPatchFieldNew.C at line 122.

FOAM exiting

「境界タイプ"patch"の場合に使用できる境界条件の中に『banana』という名前はありません。使用できる境界条件は以下の通りです。→91個のリスト」というエラーメッセージが表示されました。この91個の境界条件のリストが『速度U境界タイプpatchの場合に使用できる境界条件のrunTimeSelectionTable』です。これらは全てfvPatchField<vector>クラスを継承しています。このrunTimeSelectionTableの中の名前を使用していた場合には、その名前の継承クラスがfvPatchField<vector>として、つまり境界条件として使用されます。
 このように、Uの境界条件としては**fvPatchField<vector>クラスの継承クラス**を使用することは決まっていますが、どれを使うかはソルバー実行時までわかりません。この仕組みのおかげで、91種類の境界条件を、実行のたびにコンパイルし直すことなくソルバー実行時に切り替えることができるのです。

この仕組みを実行するためには以下のマクロと関数を用意する必要があります。

  • declareRunTimeSelectionTable : 基底クラス宣言に設置するマクロ
  • defineRunTimeSelectionTable : 基底クラス定義に設置するマクロ
  • addToRunTimeSelectionTable : 継承クラス定義に設置するマクロ
  • New : 継承クラスのコンストラクタを返す基底クラスの関数

まず、基底クラスの宣言と定義の時に専用のrunTimeSelectionTableを作成します(declareRunTimeSelectionTable, defineRunTimeSelectionTable)。継承クラスの定義の際にはこのrunTimeSelectionTableの中に自身を登録していきます(addToRunTimeSelectionTable)。実際に呼び出す際にはNew関数でrunTimeSelectionTableの中から継承クラスを選択します。
 runTimeSelectionTable自体はメモリ上に保持されるリストです。ソルバー実行時に重要となるのは、そこから継承クラスを呼び出すNew関数です。New関数は基底クラスのコンストラクタと同じ引数を持ち、autoPtr<基底クラス>を返します。ただし、その関数の中でどの継承クラスを使うかを文字列によりrunTimeSelectionTableの中から選択し、そのクラスのオブジェクトのポインタを返します。この仕組みにより、コードではなくNew関数の中で登場する文字列によって、どの継承クラスを使うかを選択しています。

実装してみた

完成済みのコードは(github.com/inabower/getScoreTutorials/tree/main/006.runTimeSelection)に公開しています。前回作成した継承クラスをアレンジする形で作成しました。

ディレクトリ構成

ソルバーとケースを含んだディレクトリ構成は以下のようになっています。

基底クラス

基底クラスではgetScore.C, getScore.H, getScoreNew.Cの三つのファイルを用意しました。getScore.C, getScore.Hは前回と同じくクラスの定義と宣言が書かれています。getScoreNew.Cには先ほど述べたNew関数が定義されています。

getScore.H

#ifndef getScore_H
#define getScore_H

#include "fvMesh.H"
#include "IOdictionary.H"
#include "autoPtr.H"
#include "runTimeSelectionTables.H"

namespace Foam
{
class getScore
:
    public IOdictionary
{
protected:
    // Protected data
        const fvMesh& mesh_;
        scalar score_;
public:
    //- Runtime type information
    TypeName("getScore");
    //- Declare runtime constructor selection table
    declareRunTimeSelectionTable
    (
        autoPtr,
        getScore,
        score,
        (const fvMesh& mesh, const IOdictionary& dict),
        (mesh, dict)
    );
    // Constructors
        // getScore(const fvMesh& mesh);
        getScore(const fvMesh& mesh, const IOdictionary& dict);
// (以下略)

前回と異なるのは23行目の以下の部分です。このdeclareRunTimeSelectionTablerunTimeSelectionTableを宣言するためのマクロです。

    declareRunTimeSelectionTable
    (
        autoPtr,
        getScore,
        score,
        (const fvMesh& mesh, const IOdictionary& dict),
        (mesh, dict)
    );

このマクロは以下のような内容になっています。($FOAM_SRC/OpenFOAM/db/runTimeSelection/construction/runTimeSelectionTables.H:49行目)

//- Declare a run-time selection
#define declareRunTimeSelectionTable(autoPtr,baseType,argNames,argList,parList)\
                                                                               \
    /* Construct from argList function pointer type */                         \
    typedef autoPtr<baseType> (*argNames##ConstructorPtr)argList;              \
                                                                               \
    /* Construct from argList function table type */                           \
    typedef HashTable<argNames##ConstructorPtr, word, string::hash>            \
        argNames##ConstructorTable;                                            \
                                                                               \
    /* Construct from argList function pointer table pointer */                \
    static argNames##ConstructorTable* argNames##ConstructorTablePtr_;         \
                                                                               \
    /* Table constructor called from the table add function */                 \
    static void construct##argNames##ConstructorTables();                      \
                                                                               \
    // (以下略)

マクロであるため、引数として与えたものがそのまま代入され、コードとして扱われます。つまり、今回のdeclareRunTimeSelectionTableは以下の内容と同義になります。
ここで最も大事なのが3行目のscoreConstructorTablePtr_です。このように今回のscoreに対してユニークな名前のテーブルが作成されます。これに対して各継承クラスのコンストラクタを加えたりのぞいたりといった操作を他の関数やクラスで行います。二重のtypedefにより省略されていますが、コンストラクタのポインタ(autoPtr)を返すようなハッシュテーブル(文字列で検索できる)です。

    typedef autoPtr<getScore> (*scoreConstructorPtr)(const fvMesh& mesh, const IOdictionary& dict);
    typedef HashTable<scoreConstructorPtr, word, string::hash> scoreConstructorTable;
    static scoreConstructorTable* scoreConstructorTablePtr_;

    static void constructscoreConstructorTables();
    static void destroyscoreConstructorTables();

    template<class getScoreType> 
    class addscoreConstructorToTable
    { 略 };

    template<class getScoreType>
    class addRemovablescoreConstructorToTable
    { 略 };

getScore.C

getScore.Cについては以下のように作成しました。

#include "getScore.H"
#include "surfaceFields.H"

namespace Foam
{
    defineTypeNameAndDebug(getScore, 0);
    defineRunTimeSelectionTable(getScore, score);
}

Foam::getScore::getScore
(
    const fvMesh& mesh,
    const IOdictionary& dict
)
:
    IOdictionary(dict),
    mesh_(mesh)
{
    Info << "Set getScore constructor from only mesh" << endl;
}

void Foam::getScore::calculate()
{
    Info << "Here will be overwritten by inheritance function" << endl;
}

ここでもやはり以下のようにマクロが利用されています。

namespace Foam
{
    defineTypeNameAndDebug(getScore, 0);
    defineRunTimeSelectionTable(getScore, score);
}

これらはgetScore.Hの時に宣言された関数やクラスの定義になっており、継承クラス側でrunTimeSelectionTableに追加したり削除したりといった操作が記述されています。

getScoreNew.C

ここで先ほど述べたNew関数が定義されています。

#include "getScore.H"

Foam::autoPtr<Foam::getScore>
Foam::getScore::New
(
    const fvMesh& mesh,
    const IOdictionary& dict
)
{
    const word scoreType(dict.get<word>("type"));

    Info<< "Selecting scoring method" << scoreType << endl;

    auto cstrIter = scoreConstructorTablePtr_->cfind(scoreType);

    if (!cstrIter.found())
    {
        FatalIOErrorInLookup
        (
            dict,
            "getScore",
            scoreType,
            *scoreConstructorTablePtr_
        ) << exit(FatalIOError);
    }

    return autoPtr<Foam::getScore>(cstrIter()(mesh, dict));
}

エラー処理等を割愛すると以下の流れになります。ここで登場するscoreConstructorTablePtr_は先ほどのdeclareRuntimeSelectionTableのマクロの中で作成された変数であり、継承クラスのコンストラクタを格納したハッシュテーブルです。これにより、辞書ファイルで"type"として指定した文字列から継承クラスのコンストラクタのポインタを呼び出すことができます。

    // ① 辞書ファイルから"type"という項目の文字列を取得
    const word scoreType(dict.get<word>("type"));
    // ② scoreConstructorTablePtr_からその文字列を検索
    auto cstrIter = scoreConstructorTablePtr_->cfind(scoreType);
    // ③ そのコンストラクタによるオブジェクトのポインタを返す
    return autoPtr<Foam::getScore>(cstrIter()(mesh, dict));

継承クラス

継承クラスについてはファイル数は変わらず、sumInletOutletScore.H, sumInletOutletScore.Cの二つです。それぞれにrunTimeSelectionTableに登録するための仕組みを追加します。

sumInletOutletScore.H

sumInletOutlet.Hについては以下のように作成しました。

#ifndef sumInletOutletScore_H
#define sumInletOutletScore_H

#include "getScore.H"

namespace Foam
{
class sumInletOutletScore
: public getScore
{
protected:
    // Protected data
public:
    //- Runtime type information
    TypeName("sumInletOutlet");
    // Constructors
        // sumInletOutletScore(const fvMesh& mesh);
        sumInletOutletScore(const fvMesh& mesh, const IOdictionary& dict);
    //- Destructor
        ~sumInletOutletScore()
        {}
    // Member Functions
        virtual void calculate();
};
}

#endif

前回と異なる点としては以下のようにこの継承クラスのtypenameが記述されています。これはそのままrunTimeSelectionTableのキーワードとなります。

TypeName("sumInletOutlet");

sumInletOutletScore.C

次にsumInletOutlet.Cについては以下のように作成しました。

#include "sumInletOutletScore.H"
#include "surfaceFields.H"
#include "addToRunTimeSelectionTable.H"

namespace Foam
{
    defineTypeNameAndDebug(sumInletOutletScore, 0);
    addToRunTimeSelectionTable
    (
        getScore,
        sumInletOutletScore,
        score
    );
}
// (以下略)

このaddToRunTimeSelectionTableはこれもマクロになります。詳しくは割愛しますが、今回登場しているscoreに関するruntimeSelectionTableへ継承コンストラクタの登録を行います。

ソルバー側

ソルバー側では以下のように変更しています。

まずこのように基底クラスのみの宣言を行っています。

#include "getScore.H"

次に先ほどまでの部分で作成したNew関数を使って基底クラスgetScoreのポインタを呼び出して、そこからオブジェクトを取り出しています。

    autoPtr<getScore> myScorePtr(getScore::New(mesh));
    getScore& myScore = *myScorePtr;

あとは使い方は一緒です。継承クラスの関数で上書きされた動作が行われます。

        myScore.calculate();
        Info << nl << "score: " << runTime.value() << tab << myScore.value() << nl << endl;

Make

New関数をコンパイルする必要があるため以下のようにMake/filesgetScoreNew.Cを追加します。

getScore/getScore/getScore.C
getScore/getScore/getScoreNew.C
getScore/sumInletOutlet/sumInletOutletScore.C
pimpleFoam.C

EXE = $(FOAM_USER_APPBIN)/scoreSelectionPimpleFoam

ケース

ケース側では継承クラスの名前を指定してあげる必要があります。今回は"type"がキーワードなので以下のように変更します。

/*--------------------------------*- C++ -*----------------------------------*\
| =========                 |                                                 |
| \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox           |
|  \\    /   O peration     | Version:  v2006                                 |
|   \\  /    A nd           | Website:  www.openfoam.com                      |
|    \\/     M anipulation  |                                                 |
\*---------------------------------------------------------------------------*/
FoamFile
{
    version     2.0;
    format      ascii;
    class       dictionary;
    location    "constant";
    object      scoreDict;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

type            sumInletOutlet;
targetPatches   (inlet outlet1 outlet2);
message         Hello_world;

// ************************************************************************* //

コンパイルと実行

wmakeとケースの実行を以前と同じように行います。ちゃんと継承クラスの動作が行われていることを確認します。

$ cd scoreSelectionPimpleFoam
$ wmake
$ cd ../plainCase
$ foamRunTutorials
$ cat log.scoreSelectionPimpleFoam | grep -e "message in scoreDict is" > log.message
message in scoreDict is hello_world
message in scoreDict is hello_world
message in scoreDict is hello_world
message in scoreDict is hello_world
message in scoreDict is hello_world

最後に

今回はOpenFOAMのruntimeSelectionTableという仕組みを使って、どの継承クラスを使用するかについてケースで指定してみました。
 次回はクラスの部分だけ別でコンパイルする方法を紹介します。