ある位置での温度に応じて流入温度をPID制御する（OpenFOAM）

# はじめに
OpenFOAMの境界条件の`exprFixedValue`を使用すれば、`controlDict`に設定した`probes`のfunction objectで取得した指定位置での温度に応じて、流入温度をPID制御をすることができます。

この方法では、OpenFOAMのexpressions syntaxの機能とFunction1の`functionObjectValue`の機能を使用しています。

Function1については、こちらの記事で説明をしています。

https://qiita.com/jmu26/items/9686ca6a592420e962f8

# サンプルモデル

赤点がセンサー点です。ここの温度が20℃になるように、流入温度を制御しています。

![temp.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3948655/ed71a3d4-066e-8004-a1a4-a3d5edf3f276.gif)

センサー点での温度（probe）と流入温度（inlet）の履歴のグラフです。センサー点の温度が徐々に20℃に近づいていることが確認できます。
![graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3948655/e43529c8-5e84-cbfd-06b5-5d76db7c265c.png)

[こちらのサンプル２：sample_probe_PID](https://github.com/jmu-26/OF_inletTempExpression)にOpenFOAM v2212で動作確認をしたサンプルモデルが置いてあります。


# 説明
このサンプルで設定されている制御の仕組みについて説明します。

## probes
解析結果からのセンサー点の温度の取得は、`system/controlDict`に設定している`probes`のfunction objectを使っています。ここでは、`probeLocations`にセンサー点の座標値を指定しています。

```c++:controlDict
probeTemperature
{
    type            probes;
    libs            (sampling);
    name            probes;
    writeControl    timeStep;
    timeInterval    1;
    fields          (T);
    probeLocations
    (
        (0.025 0.025 0.005)
    );
}
```

ここで取得した温度は、`postProcessing/probeTemperature/0/T`ファイルに時系列データとして保存されます。

## inletパッチの温度の境界条件
流入境界である`inlet`パッチでの温度の境界条件は、以下のように、`exprFixedValue` [^type] を使用しています。
`exprFixedValue`の境界条件は[ここ](https://qiita.com/jmu26/items/25bbfb9cd47ac8d5c7fa)で説明しているPatchFunction1の`expression`のタイプが設定できる境界条件です。

[^type]: `uniformFixedValue`の境界条件を使って、`expression`のタイプを指定しても同様の設定をすることができますが、この場合は、リスタート計算に必要な情報のうち結果ファイルに保存されないものがありますので（バグ？）、`exprFixedValue`で設定することをお勧めします。

```c++:T（inletパッチの境界条件の部分）
inlet
{
    type            exprFixedValue;

    functions<scalar>
    {
        probeTemperature
        {
            type                    functionObjectValue;
            functionObject          probeTemperature;
            functionObjectResult    average(T);
            defaultValue            278.15;
        }
    }

        storedVariables
        (
            {
                name            I;
                initialValue    "0.0";
                value
                {
                    resultType      scalar;
                    valueType       scalar;
                    isSingleValue   true;
                    value           0.0;
                }
            }
            {
                name            dTOld;
                initialValue    "15.0";
                value
                {
                    resultType      scalar;
                    valueType       scalar;
                    isSingleValue   true;
                    value           15.0;
                }
            }
        );

        variables
        (
            "KP = 1.0"
            "KI = 0.5"
            "KD = 0.2"
            "dT = 293.15 - fn:probeTemperature()"
            "I = I + dT*deltaT()"
            "D = (dT - dTOld)/deltaT()"
            "dTOld = dT"
        );

        valueExpr       "KP*dT + KI*I + KD*D + 278.15";

        value           uniform 278.15;
    }
 ```

 これ以降では、この`exprFixedValue`の各設定項目について説明します。

### probesで取得したセンサー点の温度の参照

以下の部分で、`probes`のfunction objectで指定したセンサー点での温度の値を、境界条件の中で参照するためのFunction1で記述された関数を定義しています。

```
        functions<scalar>
        {
            probeTemperature
            {
                type                    functionObjectValue;
                functionObject          probeTemperature;
                functionObjectResult    average(T);
                defaultValue            278.15;
            }
        }
```

このように`functions<scalar>`の部分で、Function1の関数を定義すると、「`fn:{Function1の関数の名前}()`」（この例の場合は`fn:probeTemperature()`）と記述することで、この関数から返される値を参照することができます。

Function1のタイプは[`functionObjectValue`](https://qiita.com/jmu26/items/9686ca6a592420e962f8#functionobjectvalue)を指定しています。この`functionObjectValue`では、以下の値を設定します。

#### `functionObject`
値を参照するfunction objectの名前を設定します。

#### `functionObjectResult`
参照する値の種類を設定します。
ここに設定することができる名前は、各function objectのソースコードで定義されていますが、結果の時刻ディレクトリ以下にある`uniform/functionObjects/functionObjectProperties`ファイルでも調べることができます。一度、function objectを設定して計算を実行して、このファイルの出力を確認してみるとよいと思います。
このサンプルの場合には、以下のように出力されています。これから、目的の設定値が`average(T)` [^functionObjectResult] だということが分かります。

[^functionObjectResult]: もちろんこの場合は、`min(T)`や`max(T)`を使っても同じ結果が得られます。

```c++:functionObjectProperties
cdf7e925f5746741c316f5fbcf39ad0dfca90775
{
    probeTemperature
    {
        scalar
        {
            average(T)      278.1500756750618;
            min(T)          278.1500756750618;
            max(T)          278.1500756750618;
        }
        label
        {
            size(T)         1;
        }
    }
```

#### `defaultValue`
最初の時刻など、まだfunction objectが実行されずに、値を参照できない場合に返される値を設定します。これは、オプションです。

<details><summary>別の方法</summary>

`probeTemperature`の設定を次のようにすると、function objectで取得した値を参照するのではなく、この境界条件内でセンサー点での温度を取得することもできます。

```c++:別の方法
        functions<scalar>
        {
            probeTemperature
            {
                type        sample;
                field       T;
                position    (0.025 0.025 0.005);
            }
        }
```

</details>

### 保存される値（storedVariables）
PID制御では、目標値$r(t)$と制御量（この場合はセンサー点での温度）$y(T)$との差である偏差を$e(t)=r(t)-y(t)$としたとき、制御入力（この場合は流入境界での温度）$u(T)$は、以下のように表すことができます。
```math
u(t)=K_p e(t)+K_i \int_0^t e(\tau) d\tau + K_d \frac{d e(t)}{dt}
```
ここで、$K_p$、$K_i$、$K_p$は、それぞれ比例ゲイン、積分ゲイン、微分ゲインです。

この式を離散化をして考えると、積分項は
```math
\int_0^t e(\tau) \tau \simeq \sum_{\tau=0}^{t-\Delta t} e(\tau) d\tau + e(t) \Delta t
```
と書くことができ、微分値は
```math
\frac{d e(t)}{dt} \simeq \frac{1}{\Delta t} \bigl(e(t) -e(t-\Delta t) \bigr)
```
と書くことができます。つまり、これらの値を計算するためには、前のイタレーションまでの偏差の積分値と、前のイタレーションでの偏差の値が必要になります。

このような前のイタレーションでの値を保存できる変数を`storedVariables`を使って定義をすることができます。

```
        storedVariables
        (
            {
                name            I;
                initialValue    "0.0";
                value
                {
                    resultType      scalar;
                    valueType       scalar;
                    isSingleValue   true;
                    value           0.0;
                }
            }
            {
                name            dTOld;
                initialValue    "15.0";
                value
                {
                    resultType      scalar;
                    valueType       scalar;
                    isSingleValue   true;
                    value           15.0;
                }
            }
        );
```

ここでは、前のイタレーションまでの偏差の積分値を保存する変数`I`と前のイタレーションでの偏差の値を保存する変数`dTOld`が定義されています[^isSingleValue]。

[^isSingleValue]: このexpressionの表現は、パッチ上での値（patchExpr）として使用されています。patchExprでは定義された変数はそのパッチの各要素に値を持ちますが、`I`と`dTOld`の値は１つのスカラーの値です。このような場合は、初期化に問題が生じるようなので、設定に`isSingleValue true;`などを記述することが必要です。

### 各変数の定義と計算（variables）
`variables`の部分で各変数（パラメータ）の定義と計算を行っています。

```c++
        variables
        (
            "KP = 1.0"                                // 比例ゲイン
            "KI = 0.5"                                // 積分ゲイン
            "KD = 0.2"                                // 微分ゲイン
            "dT = 293.15 - fn:probeTemperature()"     // 偏差
            "I = I + dT*deltaT()"                     // 偏差の積分値の計算
            "D = (dT - dTOld)/deltaT()"               // 偏差の微分値の計算
            "dTOld = dT"                              // 現在の偏差の値をdTOldに保存
        );
```

ここで、`deltaT()`は解析での時間刻みです。

### 境界条件値の設定（valueExpr）

`valueExpr`で境界条件値を設定します。ここでは、制御入力$u(T)$の値を計算して設定しています。
```c++
        valueExpr       "KP*dT + KI*I + KD*D + 278.15";
```

# おわりに
このようにOpenFOAMでもexpressions syntaxやPatchFunction1の機能を使用すれば、C++のソースコードを書かなくても、境界条件の設定などだけで、かなり複雑なことができるようになります。
（新しく境界条件を作ってしまったほうが早いという話もありますが…）
expressions syntaxについは、公式にも[ここ](https://www.openfoam.com/documentation/guides/latest/doc/openfoam-guide-expression-syntax.html)で説明されていますが、ここに書かれている以外にも説明されていない機能が実装されています。ただし、まだバグや思いもよらない制限などがありそうなので、使う場合にはよく検証を行ってから使ったほうがよいです。