C++で数値計算 (常微分方程式)

数値計算の基本をC++で書いていきます. 

`boost`や`gnuplot`を使用しているコードのコンパイルには, `boost`や`gnuplot`のインストールがそれぞれ必要です. 

変更した方が良い箇所があれば, この記事かTwitterの[@Yonono_01](https://twitter.com/Yonono_01)まで連絡ください.

## 目次

- [常微分方程式 (Ordinaly Differential Equation)](#常微分方程式-ordinaly-differential-equation)
  - [Euler法 (Euler Method)](#euler法-euler-method)
  - [前進Euler法の誤差](#前進euler法の誤差)
  - [Adams Boshforth法 (Adams Bashforth Method)](#adams-boshforth法-adams-bashforth-method)
  - [Runge Kutta法 (Runge-Kutta Method)](#runge-kutta法-runge-kutta-method)
  - [その他の解法](#その他の解法)
  - [boost::odeint](#boostodeint)
  - [補遺 boostついて](#補遺-boostついて)

# 常微分方程式 (Ordinaly Differential Equation)

常微分方程式を数値的に解くとは, ある初期値から始めて, ある刻み幅で増加する時の値を順々に求めていくことをいう. 
一階常微分方程式を数値的に解く方法として, Euler法やRunge-Kutta法などがある. 

## Euler法 (Euler Method)

Euler法は一階常微分方程式
```math
\frac{dy}{dx} = f(x,y) 
```
と初期条件
```math
y(x_0) = y_0
```
について, $x_{i+1}=x_i+dh$に対応する$y_{i+1}$の値を
```math
y_{i+1}=y_i+f(x,y)dh
```
と近似する.
Euler法は直線での近似であるので, 精度はあまり良くない. 

例えば, 初期条件$v(t_0)=v_0, x(t_0)=x_0$で自由落下する物体の運動を, 刻み幅$dt$で増加する時刻$t_1, t_2, \cdots$に対して順に求める. 
この物体の運動方程式は
```math
\frac{d^2x}{dt^2} = g
```
のようになる. 
この二階常微分方程式を次の連立一階常微分方程式
```math
\frac{dv}{dt} = g \\
\frac{dx}{dt} = v
```
としてEuler法を用いる.

```cpp
#include <iomanip>  // for std::ios::fixed, std::setiosflags, std::setprecision
#include <iostream> // for std::cout
#include <string>   // for std::string

namespace{
    static auto constexpr MAXBUFSIZE = 32;
    static auto constexpr g = 9.80665;

    template <typename T, bool check_positive_num = true>
    T input_parameter(std::string const & str){
        T param;
        while (true){
		    std::cout << str;
            std::cin >> param;

		    std::cin.clear();
		    std::cin.ignore(MAXBUFSIZE, '\n');

            // 初期条件として<=0の場合に再入力が要らない時は<type, false>
            if constexpr (check_positive_num){
                if (!std::cin.fail() && param > static_cast<T>(0)){ 
			        break;
		        }
            } 
            else{
                if (!std::cin.fail()){
                    break;
                }
            }
	    }

        return param;
    }

    // motion equation dv/dt
    double dvdt(double v);

    // motion equation dx/dt
    double dxdt(double v);

    void Euler(double v0, double x0, double dt);
}

int main(){
    auto const v0 = input_parameter<double, false> ("Enter the initial velocity v0[m/s]\n");
    auto const x0 = input_parameter<double> ("Enter the initial position x0[m]\n");
    auto const dt = input_parameter<double> ("Enter the partition width dt[s]\n");

    Euler(v0, x0, dt);
}

namespace{
    void Euler(double v0, double x0, double dt){
        // t, v, xの初期化
        double t = 0;
        auto v = v0;
        auto x = x0;

        std::cout << std::setprecision(7) << std::setiosflags(std::ios::fixed);

        // x<=0になるまで計算
        while (x > 0){
            std::cout << "when the time is " << t << "[s], the velocity is " << v << "[m/s] and the position x is " << x << "[m]" << std::endl;
            
            // Euler method
            t += dt;
            v += dvdt(v) * dt;
            x -= dxdt(v) * dt;
        }
    }

    double dvdt(double v){
        return g;
    }

    double dxdt(double v){
        return v;
    }
}
```

## 前進Euler法の誤差

前進Euler法による数値計算が$1$次精度であることをグラフを描いて確認する. 
空気抵抗のある質点の運動方程式は適当な変数変換により無次元化すると
```math
\frac{dv}{dt} = 1 - v
```
となる. 
この微分方程式を前進Euler法で計算する. 

```cpp
#include <array>    // for std::array
#include <cmath>    // for std::exp
#include <fstream>  // for std::ofstream
#include <iostream> // for std::cerr, std::endl
#include <utility>  // for std::make_pair, std::move, std::pair
#include <vector>   // for std::vector
#include <boost/format.hpp> // for boost::format

namespace{
    using mypair = std::pair<std::vector<double>, std::vector<double> >;

    static auto constexpr V0 = 0.0;
    static auto constexpr TEND = 1.0;
    static std::array<double, 3> const dtarray = {0.1, 0.01, 0.001};
    static auto const size = static_cast<int>(dtarray.size());

    double dvdt(double v);

    std::vector<double> error_assesment(std::vector<double> const &vendvec);

    std::vector<double> forward_Euler(std::vector<double> const &stependvec);

    std::vector<double> make_stependvec();

    bool output_result(std::vector<double> const &dterrvec);
}

int main(){
    auto const stependvec = make_stependvec();

    auto const vendvec = forward_Euler(stependvec);

    auto const dterrvec = error_assesment(vendvec);

    if (!output_result(dterrvec)){
        std::cerr << "output file not open" << std::endl;
        return -1;
    }
}

namespace{
    double dvdt(double v){
        return 1 - v;
    }

    std::vector<double> error_assesment(std::vector<double> const &vendvec){
        std::vector<double> vexactvec(size, 1.0 - exp(-TEND));
        std::vector<double> dterrvec;

        for (auto i = 0; i < size; i++){
           dterrvec.push_back((vendvec[i] - vexactvec[i]) / vexactvec[i]);
        }

        return dterrvec;
    }

    std::vector<double> forward_Euler(std::vector<double> const &stependvec){
        std::vector<double> vendvec(size, 0.0);

        for (auto i = 0; i < size; i++){
            for (auto j = 0; j < stependvec[i]; j++){
                vendvec[i] += dvdt(vendvec[i]) * dtarray[i];
            }
        }

        return vendvec;
    }

    std::vector<double> make_stependvec(){
        std::vector<double> stependvec;

        for (auto i = 0; i < size; i++){
            stependvec.push_back(TEND / dtarray[i]);
        }

        return stependvec;
    }

    bool output_result(std::vector<double> const &dterrvec){
        std::ofstream ofs;
        ofs.open("data_forward_Euler.txt");

        if (!ofs){
            return false;
        }

        for (auto i = 0; i < size; i++){
            ofs << boost::format("%.7f %.7f\n") % dtarray[i] % dterrvec[i];
        }

        return true;
    }
}
```

出力ファイル`data_forward_Euler.txt`からデータを読み取り, gnuplotを用いてグラフを描き, `forward_Euler.png`に保存する.

```gnuplot
set title 'forward Euler method'

set xlabel 'dt'
set ylabel 'error'
set key left top

set logscale x
set logscale y

set xr[1.0e-4:1.0]
set yr[1.0e-4:1.0]

set grid

plot 'data_forward_Euler.txt' using 1:2 title 'forward Euler'
replot x title "\Delta t"

set terminal pngcairo
set output "forward_Euler.png"
replot
```

`forward_Euler.png`は次のようになる. 

![forward_Euler](https://user-images.githubusercontent.com/75283304/174062475-db64cbb6-bcb3-4a22-9923-256ae30ad32e.png)


## Adams Boshforth法 (Adams Bashforth Method)

空気抵抗のある質点の自由落下の運動をAdams Bashforth法で計算し, 理論解との誤差を見る. 
微分方程式
```math
\frac{dv}{dt} = 1 - v
```
の理論解は
```math
v(t) = 1 - \exp(-t)
```
である.

```cpp
#include <fstream>  // for std::ofstream
#include <iostream> // for std::cerr, std::cout
#include <utility>  // for std::make_pair, std::move, std::pair
#include <vector>   // for std::vector
#include <boost/format.hpp> // for boost::format

namespace{
    using mypair = std::pair<std::vector<double>, std::vector<double> >;

    static auto constexpr V0 = 0.0;
    static auto constexpr DT = 0.1;
    static auto constexpr TEND = 1.0;
    static auto constexpr STEPEND = TEND / DT;

    // Adams Bashforth method
    mypair Adams_Bashforth();

    // open output file
    bool output_result(mypair const &tvpair);

    // motion equation
    double dvdt(double v);
}

int main(){
    auto const tvpair = Adams_Bashforth();

    if (!output_result(tvpair)){
        std::cerr << "output file not open" << std::endl;
        return -1;
    }
}

namespace{
    double dvdt(double v){
        return 1 - v;
    }

    mypair Adams_Bashforth(){
        std::vector<double> tvec(STEPEND + 2, 0.0), vvec(STEPEND + 2, 0.0);

        // vの初期条件
        vvec[0] = V0;

        // 2nd order Runge Kutta for vvec[1]
        auto const k1 = DT * dvdt(vvec[0]);
        auto const k2 = DT * dvdt(vvec[0] + k1 * 0.5);
        vvec[1] = vvec[0] + k2;

        for (auto i = 1; i < STEPEND; i++){
            tvec[i] = static_cast<double>(i) * DT; 
            
            // Adams Bashforth method
            vvec[i + 1] = vvec[i] + (DT / 2) * (-dvdt(vvec[i - 1]) + 3 * dvdt(vvec[i]));      
        }

        return std::make_pair(std::move(tvec), std::move(vvec));
    }

    bool output_result(mypair const &tvpair){
        std::ofstream ofs;
        ofs.open("data_Adams_Bashforth.txt");

        if (!ofs){
            return false;
        }

        auto const [tvec, vvec] = tvpair;
        for (int i = 0; i <= STEPEND; i++){
            ofs << boost::format("%.7f %.7f\n") % tvec[i] % vvec[i];
        }

        return true;
    }
}
```

出力ファイル`data_Adams_Bashforth.txt`からデータを読み取り, gnuplotを用いてグラフを描き, `Adams_Borshforth.png`に保存する.

```gnuplot
set title 'Adams Bashforth method'

set xlabel 'time'
set ylabel 'velocity'
set key left top

set xr[0.0:1.1]
set yr[0.0:0.7]

set xtics 0.1
set ytics 0.1

set grid

plot 'data_Adams_Bashforth.txt' using 1:2 title 'Adams Bashforth'
replot 1 - exp(-x) title 'theoritical solution 1 - exp(-t)'

set terminal pngcairo
set output 'Adams_Bashforth.png'
replot
```

`Adams_Borshforth.png`は次のようになる. 

![Adams_Bashforth](https://user-images.githubusercontent.com/75283304/173978383-83f7ca9f-8ed8-4ec9-a7fc-afc815836a5b.png)

## Runge Kutta法 (Runge-Kutta Method)

Euler法は$y(t+dt)$をTaylor展開した時の$1$次の項まで考えたものと捉えることができる. 
高階の微分を一回の微分関数$f(x,y)$のみで表す式をRunge Kutta法という. 
しかし, 特に断らない限り, $4$次のRunge Kutta法の次の形を単にRunge Kutta法という.

Runge Kutta法は一階常微分方程式
```math
\frac{dy}{dx} = f(x,y) 
```
と初期条件
```math
y(x_0) = y_0
```
について, $x_{i+1}=x_i+dh$に対応する$y_{i+1}$の値を
```math
y_{i+1} = y(x_i)+\frac{dh}{6}(k_1+2k_2+2k_3+k_4) \\
k_1 = f(x,y) \\
k_2 = f(x+\frac{dh}{2}, y+dh\frac{k_1}{2}) \\
k_3 = f(x+\frac{dh}{2}, y+dh\frac{k_2}{2}) \\
k_4 = f(x+dh, y+dhk_3)
```
と近似する. 

例えば, 初期条件$v(t_0)=v_0$で粘性抵抗を受けながら自由落下する物体の運動を, 刻み幅$dt$で増加する時刻$t_1, t_2, \cdots$に対して順に求める. 
この物体の運動方程式は
```math
\frac{dv}{dt} = g - cv
```
のようになる (ただし, $c$は粘性抵抗係数).

```cpp
#include <iomanip>  // for std::ios::fixed, std::setiosflags, std::setprecision
#include <iostream> // for std::cout
#include <string>   // for std::string

namespace{
    static auto constexpr MAXBUFSIZE = 32;
    static auto constexpr c = 0.1;
    static auto constexpr g = 9.80665;

    template <typename T, bool check_positive_num = true>
    T input_parameter(std::string const & str){
        T param;
        while (true){
		    std::cout << str;
            std::cin >> param;

		    std::cin.clear();
		    std::cin.ignore(MAXBUFSIZE, '\n');

            // 初期条件として<=0の場合に再入力が要らない時は<type, false>
            if constexpr (check_positive_num){
                if (!std::cin.fail() && param > static_cast<T>(0)){ 
			        break;
		        }
            } 
            else{
                if (!std::cin.fail()){
                    break;
                }
            }
	    }

        return param;
    }

    // motion equation
    double f(double v);

    void Runge_Kutta(int n, double tmax, double v0);
}

int main(){
    // 分割数, 最大時間, 初期条件の入力
    auto const n = input_parameter<int> ("Enter the partition number n\n");
    auto const tmax = input_parameter<double> ("Enter the maximum time t_max[s]\n");
    auto const v0 = input_parameter<double, false> ("Enter the initial velocity v_0[m/s]\n");
    
    Runge_Kutta(n, tmax, v0);
}

namespace{
    double f(double v){
        return g - c * v;
    }

    void Runge_Kutta(int n, double tmax, double v0){
        // 分割幅
        auto const dt = tmax / static_cast<double>(n);
        // vの初期化
        auto v = v0;

        std::cout << std::setprecision(7) << std::setiosflags(std::ios::fixed);
        for (auto i = 0; i <= n; i++){
            auto const t = static_cast<double>(i) * dt;

            std::cout << "when the time is " << t << "[s], velocity is " << v << "[m/s]" << std::endl;

            // Runge Kutta method
            auto const k1 = f(v);
            auto const k2 = f(v + k1 * dt * 0.5);
            auto const k3 = f(v + k2 * dt * 0.5);
            auto const k4 = f(v + k3 * dt);
            v += dt / 6.0 * (k1 + 2.0 * k2 + 2.0 * k3 + k4);            
        }
    }
}
```

物体の運動が高次元であれば, 配列として考えればよい. 

## その他の解法

Euler法は前進差分で求めたので, 前進Euler法(forward Euler method)と呼ばれることもある. 後退差分で求める方法を後退Euler法(backward Euler method)と呼ぶ. 

$2$次のRunge-Kutta法として, 係数を$1$としたものをHeun法(Heun method)や改良Euler法(improved Euler method)という. 
係数を$1/2$としたものは中点法(midpoint method)や修正Euler法(modified Euler method)と呼ばれる.

## boost::odeint

常微分方程式の数値解析ライブラリである`boost::odeint`を用いる.
実際にC++で常微分方程式を解く時は`boost::odeint`を用いる. 

例えば, $2$次元のLotka Volterra方程式
```math
\frac{dx}{dt} = -x (\alpha - \beta y) \\
\frac{dy}{dt} = -y (\gamma - \delta x)
```
をあるパラメタ$\alpha, \beta, \gamma, \delta$に対して解いてみる.

```cpp
#include <array>    // for std::array
#include <fstream>  // for std::ofstream
#include <iostream> // for std::cerr, std::endl
#include <boost/format.hpp>         // for boost::format
#include <boost/numeric/odeint.hpp> // for boost::numeric::odeint

namespace{
    using state = std::array<double, 2>;

    static auto constexpr DT = 0.05;
    static auto constexpr T0 = 0.0;
    static auto constexpr TMAX = 5.0;

    class Lotka_Volterra_system final{
        double const alpha_;
        double const beta_;
        double const delta_;
        double const gamma_;

        public:
            Lotka_Volterra_system(double alpha, double beta, double delta, double gamma)
                : alpha_(alpha), beta_(beta), delta_(delta), gamma_(gamma)
            {}
            Lotka_Volterra_system() = delete;
            ~Lotka_Volterra_system() = default;

            void operator()(state const &x, state &dx, double t) const; //関数内でalpha等を変更しないためのconst
    }; 
}

int main(){   
    Lotka_Volterra_system System(2.0, 3.0, 5.0, 4.0);
    state State = {1.0, 0.5};

    //6th-Adams Bashforth Moulton method
    boost::numeric::odeint::adams_bashforth_moulton<6, state> Stepper;

    // open output file
    std::ofstream ofs("data_Lotka_Volterra.txt");

    if (!ofs){
        std::cerr << "output file not open" << std::endl;
        return -1;
    }

    //time = T0からtime = TMAXまで時間発展をDT刻みで計算
    boost::numeric::odeint::integrate_const(
        Stepper,
        System,
        State,
        T0,
        TMAX,
        DT,
        [&ofs](state const &x, double t) {ofs << boost::format("%.2f %.7f %.7f\n") % t % x[0] % x[1];}
    );
}

namespace{
    void Lotka_Volterra_system::operator()(state const &x, state &dx, double t) const{
        dx[0] = x[0] * (alpha_ - beta_ * x[1]);
        dx[1] = - x[1] * (gamma_ - delta_ * x[0]);
    }
}
```

出力ファイル`data_Lotka_Volterra.txt`からデータを読み取り, gnuplotを用いてグラフを描き, `Lotka_Volterra.png`に保存する.

```plt
set title 'boost::odeint Lotka Volterra'

set xlabel 'time'
set ylabel 'x and y'
set key left top

set xr[0:5.1]
set yr[0.3:1.2]

set grid

plot 'data_Lotka_Volterra.txt' using 1:2 w lp title 'x'
replot "data_Lotka_Volterra.txt" using 1:3 w lp title 'y'

set terminal pngcairo
set output 'Lotka_Volterra.png'
replot 
```

`Lotka_Volterra.png`は次のようになる. 

![Lotka_Volterra](https://user-images.githubusercontent.com/75283304/174499281-03a91c05-37be-4998-886b-13112eeac399.png)

## 補遺 boostついて

`boost`はC++のライブラリであり, 様々な機能がある. 
この記事では常微分方程式の数値計算に特化した`boost::odeint`について説明する. 
ここで紹介していない便利なコマンドもたくさんあるので, 参考文献を適宜参考にしてほしい.