C++でstd::valarrayを利用して微分方程式の数値解析を行う

# 目的
C++で微分方程式の数値計算をしたい．

# 結論
オイラー法と4次のルンゲクッタ法について関数を書いた．

```cpp:rungekutta_4th
template <class Value, class Time, class Function>
auto rungekutta_4th(
  const std::valarray <Value> & y, Time t, Time dt, Function && f
) -> std::valarray <Value>
{
  auto k1 = f(t, y);
  auto k2 = f(t + dt / 2.0, y + k1 * (dt / 2.0));
  auto k3 = f(t + dt / 2.0, y + k2 * (dt / 2.0));
  auto k4 = f(t + dt, y + k3 * dt);
  return (k1 + (k2 * 2.0) + (k3 * 2.0) + k4) * (dt / 6.0);
}
```

```cpp:euler
template <class Value, class Time, class Function>
auto euler(
  const std::valarray <Value> & y, Time t, Time dt, Function && f
) -> std::valarray <Value>
{
  return f(t, y) * dt;
}
```

# 環境
C++14だよ．コンパイラはvc141を使っているよ．

# std::valarray
ベクトルどうしの演算に特化したクラスだよ．なんかあまり普及している感じがない．というか**弊研究室ではだれも知らなかった**．かなしい．
ベクトルといえばstd::vectorなんだけど，汎用性が高いぶんベクトル計算は苦手みたいだ．std::valarrayだと特化しているだけあって，各種演算子が便利に作られている．

```cpp:comparation_between_vector_and_valarray
{// std::vectorの場合
  std::vector <int> a{0, 1, 2};
  std::vector <int> b{0, 2, 4};
  // しんどい
  auto c = a;
  std::transform(
    c.begin(), c.end(),
    b.begin(),
    c.begin(),
    [](auto && v1, auto && v2) {
      return v1 + v2;
    });
}


{// std::valarrayの場合
  std::valarray <int> a{0, 1, 2};
  std::valarray <int> b{0, 2, 4};
  // 直感的
  auto c = a + b;
}
```

# 微分方程式の数値解析
本日のゲストはこちら，減衰振動に関する微分方程式です．式はバネ-質量-ダンパ系に関するものだけど，これについての詳細は省くよ．あ，○○法みたいな感じで紹介していくんですけど，**それがどんな考えのもとに作られたかとかは紹介しないです**．たぶんもっと分かりやすく解説してくださる方がいらっしゃると思うので……．

```math
\left\{\begin{array}{}
    \frac{dx^2}{d^2t}+2\gamma\frac{dx}{dt}+\omega^2x=0
    \qquad; \gamma:減衰率,\omega:角振動数\\
    x|_{t=0}=1\\
    \frac{dx}{dt}|_{t=0}=0
\end{array}\right.
```

上の式について，時間*t*がある値をとるときのバネの長さ*x*を求める．

## オイラー法
微分方程式の数値解析法のひとつとして，オイラー法がある．これは最も単純で，**時間で微分してるなら時間をかければいいじゃん？** である．
さて，

```math
\frac{dx}{dt}=f(t,x)
```

とおくと，オイラー法の式は

```math
x_{t=t+\Delta t}=x_{t=t}+f(t,x_{t=t})\Delta t
```

と表せる．
それはそれとして，今回は

```math
\left\{\begin{array}{}
    x_1=x\\
    x_2=\frac{dx}{dt}
\end{array}\right.
```

とおくと，もとの式は，

```math
\left\{\begin{array}{}
\frac{dx_2}{dt}+2\gamma x_2+\omega^2x_1=0\\
    x_1|_{t=0}=1\\
    x_2|_{t=0}=0
\end{array}\right.
```

と表せる．こいつを整理すると，

```math
\left\{
  \begin{array}{}
    \frac{x_1}{dt}=x_2\\
    \frac{x_2}{dt}=-2\gamma x_2-\omega^2x_1\\
    x_1|_{t=0}=1\\
    x_2|_{t=0}=0
  \end{array}
\right.
```

となる．さて，これを行列っぽく表す（なんか名前あるんでしょうか？）と，

```math
\left\{
  \begin{array}{}
    \frac{d}{dt}\begin{bmatrix}x_1\\x_2\end{bmatrix}
    =\begin{bmatrix}x_2\\-2\gamma x_2-\omega^2x_1\end{bmatrix}\\
    \begin{bmatrix}x_1\\x_2\end{bmatrix}_{t=0}
    =\begin{bmatrix}1\\0\end{bmatrix}
  \end{array}
\right.
```

最後に，オイラー法の式にぶちこむと，

```math
\left\{
  \begin{array}{}
    \begin{bmatrix}x_1\\x_2\end{bmatrix}_{t=t+\Delta t}
    =\begin{bmatrix}x_1\\x_2\end{bmatrix}_{t=t}
    +\begin{bmatrix}
      x_2\\-2\gamma x_2-\omega^2x_1
    \end{bmatrix}_{t=t,x=x_{t=t}}
    \Delta t\\
    \begin{bmatrix}x_1\\x_2\end{bmatrix}_{t=0}
    =\begin{bmatrix}1\\0\end{bmatrix}
  \end{array}
\right.
```

とわかる．じゃあ実装します．

```cpp:euler
#include <valarray>

const auto PI = std::acos(-1.0);
constexpr double DT = 0.1;
constexpr double GAMMA = 0.2;
constexpr double OMEGA = 1.0;
constexpr double TIME = 30.0;

auto f(double t, std::valarray <double> x) -> std::valarray <double>
{
  return {
    x[1],
    -2.0 * GAMMA * x[1] - OMEGA * OMEGA * x[0]
  };
}

auto main() -> int
{
  std::valarray <double> x{ 1.0, 0.0 };

  for (double t = 0.0; t <= TIME; t += DT)
  {
    x += f(t, x) * DT;
  }
}
```

実装しました．解説します．
定数となるのは*&pi;*，*&Delta;t*，*&gamma;*，*&omega;*，*t*なので，

```cpp:constant
const auto PI = std::acos(-1.0);
constexpr double DT = 0.1;
constexpr double GAMMA = 0.2;
constexpr double OMEGA = 1.0;
constexpr double TIME = 30.0;
```

と適当に設定します．また，更新する配列に初期条件を入れます．

```cpp:initialize
std::valarray <double> x{ 1.0, 0.0 };
```

更新式は前の時点*t*とその状態*x*が必要であり更新の操作なので，

```cpp:update_method
auto f(double t, std::valarray <double> x) -> std::valarray <double>
{
  return {
    x[1],
    -2.0 * GAMMA * x[1] - OMEGA * OMEGA * x[0]
  };
}
```

のように関数化してやります．あ，今回は*t*は必要ないわね．
オイラー法の式は`f()`を使って，

```cpp:euler
x += f(x) * DT;
```

と書けます．これを実行すると，横軸*t*，縦軸*x*のグラフは，
![Fig.1.png](https://qiita-image-store.s3.amazonaws.com/0/193192/90351a81-2f6f-8577-5297-f47b8b997952.png)
のようになります．青が*x<sub>1</sub>*なので，これが減衰振動のグラフだよ．

## 4次のルンゲクッタ法
オイラー法は単純で高速だけど，*&Delta;t*をそれなりに小さくとらないと誤差がバカにならない．そこで高精度でそこそこ速い数値解法として4次のルンゲクッタ法があるよ．4次があるならってことで1次からn次まであるんだけど，1次はオイラー法と同じだし，5次以降は速度の低下の割に精度が良くならない（つまりコスパが悪い）．というわけで今回は4次のルンゲクッタ法だけを実装します．
4次のルンゲクッタ法の式は結構えげつなくて，

```math
\left\{\begin{array}\\
k_1=f(t,x_{t=t})\\
k_2=f(t+\frac{\Delta t}{2},x_{t=t}+k_1\frac{\Delta t}{2})\\
k_3=f(t+\frac{\Delta t}{2},x_{t=t}+k_2\frac{\Delta t}{2})\\
k_4=f(t+\Delta t,x_{t=t}+k_3\Delta t)\\
x_{t=t+\Delta t}=x_{t=t}+(k_1+2k_2+2k_3+k_4)\frac{\Delta t}{6}
\end{array}\right.
```

となっている．**なんじゃこりゃ**．少々厄介なところは*k<sub>1</sub>*から最後まで伝播していることで，このせいで（*x*のすべての要素が互いに独立している場合を除いて）*k<sub>n</sub>*を並列に解くことができない．
同じ問題を解くのでオイラー法のときと同様な式変形を行うよ．
じゃあ実装します．

```cpp:rungekutta_4th
//
// オイラー法のときと同じなのでこのへんは省略
//
auto main() -> int
{
  std::valarray <double> x{ 1.0, 0.0 };

  for (double t = 0.0; t <= TIME; t += DT)
  {
    // ここから違う
    auto k1 = f(t, x);
    auto k2 = f(t + DT / 2.0, x + k1 * (DT / 2.0));
    auto k3 = f(t + DT / 2.0, x + k2 * (DT / 2.0));
    auto k4 = f(t + DT, x + k3 * DT);
    x += (k1 + (k2 * 2.0) + (k3 * 2.0) + k4) * (DT / 6.0);
  }
}
```

実装しました．ほぼ同じだね．
実行すると*x*の変化は
![Fig.2.png](https://qiita-image-store.s3.amazonaws.com/0/193192/4a01c7cd-9c7d-79a1-3f4b-16e5ac61a31e.png)
のようになる．当たり前だけど，これもオイラー法のときとほぼ同じ．

# 関数化する
オイラー法はともかく，4次のルンゲクッタ法を毎回書くのはめんどくさい．関数化しよう．

```cpp:euler
template <class Value, class Time, class Function>
auto euler(
  const std::valarray <Value> & y, Time t, Time dt, Function && f
) -> std::valarray <Value>
{
  return f(t, y) * dt;
}
```

*y|<sub>t=t+&Delta;t</sub>*を求めるとき，引数には順に，時点*t*での状態*y|<sub>t=t*，時点*t*，微小時間*dt*，関数*f*を入れる．

```cpp:rungekutta_4th
template <class Value, class Time, class Function>
auto rungekutta_4th(
  const std::valarray <Value> & y, Time t, Time dt, Function && f
) -> std::valarray <Value>
{
  auto k1 = f(t, y);
  auto k2 = f(t + dt / 2.0, y + k1 * (dt / 2.0));
  auto k3 = f(t + dt / 2.0, y + k2 * (dt / 2.0));
  auto k4 = f(t + dt, y + k3 * dt);
  return (k1 + (k2 * 2.0) + (k3 * 2.0) + k4) * (dt / 6.0);
}
```

こちらも同様．さてこうすると前のコードも少しすっきりする．

```cpp:main
auto main() -> int
{
  // 定数
  const auto PI = std::acos(-1.0);
  constexpr double DT = 0.1;
  constexpr double GAMMA = 0.2;
  constexpr double OMEGA = 1.0;
  constexpr double TIME = 30.0;

  // 初期値
  std::valarray <double> x = { 1.0, 0.0 };

  // 更新関数
  auto f = [&](auto && t, auto && x) -> std::valarray <double>
  {
    return {
      x[1],
      -2.0 * GAMMA * x[1] - OMEGA * OMEGA * x[0]
    };
  };

  // 数値解を求める
  for (double t = 0; t <= TIME; t += DT)
  {
    //x += euler(x, t, DT, f);
    x += rungekutta_4th(x, t, DT, f);
  }
}
```