オブジェクト指向を用いた線形常微分方程式の数値計算（Python）2-1：差分法基底クラス

# 1. 概要
　この記事では、常微分方程式の差分法について説明し、対応する基底クラスの実装を行います。前回の記事は[こちら](https://qiita.com/itotomball/items/8c25ffa93070603af794)です。

# 2. 差分法
### 2.1 線形常微分方程式
　前回の内容をおさらいします。以下の線形常微分方程式を対象としています。
$$ f^{(N)} + c_{N-1} f^{(N-1)} + \ldots + c_1 f^{(1)} + c_0 f = r.　\tag{1}$$
$\phi_k=f^{(k)}$ $(k=0,\ldots,N-1)$ と置くことにより、一階連立微分方程式に書き直すことができます。
```math
\left\{
\begin{align}
&\phi_0^\prime = \phi_1, \\
&\phi_1^\prime = \phi_2, \\
&\qquad\vdots \\
&\phi_{N-1}^\prime = r - (c_0\phi_0 + \ldots +c_{N-1}\phi_{N-1}).\\
\end{align}
\right.
\tag{2}
```
次節以降で表記を簡単にするため、式$(2)$の右辺を $\rho_k(x,\phi_0,\ldots,\phi_{N-1})$ と置いて、次のように表すことにします。
$$ \phi_k^\prime = \rho_k(x,\phi_0,\ldots,\phi_{N-1})\quad (k=0,\ldots,N-1) \tag{3}$$

### 2.2 差分近似
　独立変数 $x$ の領域の離散化を行い、 $n$ ステップ目の値を $x_n$ と表します。多くの場合ではステップ幅は一定にとられるため、$x_n=x_0+n\Delta x$ と表せますが、単調に増加する数列になっていれば一定幅でなくとも構いません。各点に対応する関数の値を $\phi^n_k=\phi_k(x_n)$ とします。次ステップの関数値を求めるため、差分法では一階微分を次のように差分で置き換えます。
$$ \phi^\prime \rightarrow \frac{\phi^{n+1}-\phi^n}{\Delta x} \tag{4}$$
この式を用いると、式$(3)$から差分方程式を得ます。
$$ \frac{\phi_k^{n+1}-\phi_k^n}{\Delta x} = \rho_k \tag{5}$$
この方程式を解くと次ステップの関数値 $\phi_k^{n+1}$ が求まります。ですが、ここで右辺の関数をどのステップの値で評価するかが問題であり、Euler法やCrank-Nicolson法など様々な数値計算スキームが考案されています。各手法の具体的な内容については個別の記事で解説します。この記事では、差分法の数値解法全般に共通する部分を抽出して基底クラスを実装します。

# 3. 実装
### 3.1 ライブラリ・モジュールのインポート
　このクラスでは抽象関数を作成するので、抽象基底クラスを実装するための標準ライブラリである `abc` から `ABCMeta` と `abstractmethod` をimportします（`abc` は標準ライブラリなのでインストールする必要はありません）。また、前回実装した `ordinary_differential_equation.py` から `OrdinaryDifferentialEquation` クラスをimportしておきます。　`annotations`, `FunctionType` は関数のアノテーションに使うためです。
``` finite_difference_method.py
from __future__ import annotations
from abc import ABCMeta, abstractmethod
from types import FunctionType
import numpy as np
from ordinary_differential_equation import OrdinaryDifferentialEquation
```
### 3.2 コンストラクタ
　いずれの差分スキームにおいても、解きたい問題の微分方程式の形を定義する `OrdinaryDifferentialEquation` オブジェクトが必要です。よって、基底クラス `FiniteDifferenceMethod` のコンストラクタでは `OrdinaryDifferentialEquation` オブジェクトを引数に取り、privateなインスタンス変数 `self.__ode` に代入します。
``` finite_difference_method.py
class FiniteDifferenceMethod:
    """ Base class of finite difference methods for ODE.
    """
    def __init__(self, ode:OrdinaryDifferentialEquation):
        """ Constructor.

        Parameters
        ----------
        ode : OrdinaryDifferentialEquation object
            ODE to solve using FDM.
        """
        self.__ode = ode
```

### 3.3 ステップの進行（抽象関数）
　差分法では差分方程式を解き、次ステップの関数の値を求めるメソッドが共通して必要になります。しかし、現段階ではどの差分スキームを使用するか決まっていません。そこで、抽象メソッドととして定義だけをしておき、派生クラスで各スキームの実装を行うようにします。
　抽象メソッドにするには、`@abstractmethod` デコレータをつけます。`@abstractmethod` をつけることで、派生先のクラスで `advance_step` メソッドが未実装でないかチェックしてくれます。具体的な実装は一切書きませんが、メソッド内に文がないとエラーを出すので、docstringを書いておくと良いです（`pass`でも大丈夫です）。引数はそれぞれ、`phi`:現ステップにおける未知関数の値、`x`:現ステップの独立変数の値、`x_inc`:独立変数の増分値です。
``` finite_difference_method.py
class FiniteDifferenceMethod:
    # 中略
    @abstractmethod
    def advance_step(self, phi:np.ndarray, x:float, x_inc:float) -> None:
        """ Advance a step to update state of function.

        Parameters
        ----------
        phi : array-like
            Function values at current step.
        x : float
            Current value of variable.
        x_inc : float
            Increment of variable.
        """
```

### 3.4 getter関数
　派生クラスの差分スキームの計算で使用するために、インスタンス変数 `__ode` のgetter関数を呼ぶメソッドを定義しておきます（`self.__ode.get_order()` などと何回も書くのは面倒なので）。
``` finite_difference_method.py
class FiniteDifferenceMethod:
    # 中略
    def get_order(self,) -> int:
        """ Get the order of ODE.
        """
        return self.__ode.get_order()

    def get_coeff(self, x:float=None)\
             -> np.ndarray | list[FunctionType]:
        """ Get coefficient functions in ODE.
        """
        return self.__ode.get_coeff(x)

    def get_rhs(self, x:float=None) -> float | FunctionType:
        """ Get right hand side function in ODE.
        """
        return self.__ode.get_rhs(x)
```

# 4. 付録
### ソースコード全文

``` finite_difference_method.py
""" Classes of Finite Difference Method for ODE.
"""
from __future__ import annotations
from abc import ABCMeta, abstractmethod
from types import FunctionType
import numpy as np
from ordinary_differential_equation import OrdinaryDifferentialEquation

class FiniteDifferenceMethod(metaclass=ABCMeta):
    """ Base class of finite difference methods for ODE.
    """
    def __init__(self, ode:OrdinaryDifferentialEquation):
        """ Constructor.

        Parameters
        ----------
        ode : OrdinaryDifferentialEquation object
            ODE to solve using FDM.
        """
        self.__ode = ode

    @abstractmethod
    def advance_step(self, phi:np.ndarray, x:float, x_inc:float) -> None:
        """ Advance a step to update state of function.

        Parameters
        ----------
        phi : array-like
            Function values at current step.
        x : float
            Current value of variable.
        x_inc : float
            Increment of variable.
        """

    def get_order(self,) -> int:
        """ Get the order of ODE.
        """
        return self.__ode.get_order()

    def get_coeff(self, x:float=None)\
             -> np.ndarray | list[FunctionType]:
        """ Get coefficient functions in ODE.
        """
        return self.__ode.get_coeff(x)

    def get_rhs(self, x:float=None) -> float | FunctionType:
        """ Get right hand side function in ODE.
        """
        return self.__ode.get_rhs(x)
```

# 5. 参考文献
- [差分法 - Wikipedia](https://ja.wikipedia.org/wiki/%E5%B7%AE%E5%88%86%E6%B3%95)
- [Python: 抽象クラスを実装する (abc) - け日記](https://ohke.hateblo.jp/entry/2020/02/01/123000)
- [オブジェクト指向を用いた線形常微分方程式の数値計算（Python）1：線形常微分方程式クラス - Qiita](https://qiita.com/itotomball/items/8c25ffa93070603af794)