Return to Python で数値解析

SymPy で(あえて)数値解析

Python で数値解析のために必要なプログラミングと,いくつかの例を示します。
ここでは数値解析ライブラリである SciPy を使わず,計算機代数システムである SymPy をあえて使った例を示します。

また SymPy 自体でもグラフ作成機能がありますが,ここでは,SymPy Plotting Backends (SPB) を使ってグラフを描いてみます。

In [1]:
# SymPy を使うときのおまじない
from sympy import *
from sympy.abc import *
# 虚数単位,円周率,ネイピア数
from sympy import I, pi, E
# グラフは SymPy Plotting Backends (SPB) で描く
from spb import *

# 以下はグラフを SVG で Notebook にインライン表示させる設定
%config InlineBackend.figure_formats = ['svg']

SymPy による数値積分

$\displaystyle \int_0^{\pi} \sin(\sin x)\, dx$ の計算をします。解析的に解けないと,そのままで返します。

In [2]:
integrate(sin(sin(x)), (x, 0, pi))
Out[2]:
$\displaystyle \int\limits_{0}^{\pi} \sin{\left(\sin{\left(x \right)} \right)}\, dx$

.evalf() すると数値積分した値を返します。

In [3]:
integrate(sin(sin(x)), (x, 0, pi)).evalf()
Out[3]:
$\displaystyle 1.78648748195005$

SymPy による方程式の数値解

まず,解を求める関数 $f(x)$ を定義し,$f(x) = 0$ となる $x$ のあたりをつけるためにグラフを描いてみます。

以下では例としてベッセル関数 besselj(0,x) のゼロ点を求めてみます。ベッセル関数 besselj()

from sympy import *

すれば使えます。$x$ 軸を横切る点がわかりやすいように,$y=0$ ($x$ 軸)も描いてみます。

In [4]:
plot(besselj(0,x), 0, (x, 0, 10), 
     legend = False, # 凡例は無しに
     xlabel = "$x$", ylabel = "$J_0(x)$", 
     xlim = [0, 10], 
     title = "ベッセル関数 $J_0(x)$");

方程式の数値解を求めてくれる関数は nsolve() です。$x=2.5$ のあたりで $x$ 軸と交わっていますから,このあたりの解を探します。

In [5]:
# besselj(0,x) = 0 となる解を
# x が 2.5 のあたりから始めて 15 桁の精度で求める

nsolve(besselj(0,x), x, 2.5, prec=15)
Out[5]:
$\displaystyle 2.40482555769577$

[2, 4] のように範囲を指定して解を探すこともできます。

In [6]:
nsolve(besselj(0,x), x, [2, 4], prec=15)
Out[6]:
$\displaystyle 2.40482555769577$
In [7]:
# ほんとに解になってるか確認。ゼロに近ければよい。
besselj(0,_).evalf()
Out[7]:
$\displaystyle -6.10876525973673 \cdot 10^{-17}$

ルンゲ・クッタ法による1階常微分方程式の数値解法

常微分方程式の数値解法をしてくれる関数は SymPy には用意されていないようです。備え付けの関数に頼らず,1階常微分方程式

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

を4次のルンゲ・クッタ法で解いてみます。

初期条件を $t=t_0$ で $x(t_0) = x_0$ とし,$t = t_1$ までを $N$ 分割して解きます。

$$ h = \frac{t_1 – t_0}{N}, \quad t_i = t_0 + i\, h,
\quad x_i = x(t_i)$$

とし,以下の式から次のステップの値 $x_{i+1}$ を決めます。

\begin{eqnarray}
k_1 &=& h\, f(t_i, x_i) \\
k_2 &=& h\, f(t_i + h/2, x_i + k_1/2) \\
k_3 &=& h\, f(t_i + h/2, x_i + k_2/2) \\
k_4 &=& h\, f(t_i + h, x_i + k_3) \\
x_{i+1} &=& x_i + \frac{1}{6} (k_1 + 2 k_2 + 2 k_3 + k_4)
\end{eqnarray}

以上のような計算を行う関数 rk1() を Python の関数として以下のようにして定義します。

In [8]:
def rk1(func, x0, t0, t1, N):
    """ dx/dt = func(t, x) を
        初期条件 t = t0 で x = x0 とし,
        [t0, t1] を N 分割して
        4次のルンゲ・クッタ法で解く
    """
    h = (t1 - t0)/N
    T = [t0]
    X = [x0]
    t = t0
    x = x0
    for i in range(N):
        k1 = h*func(t, x)
        k2 = h*func(t + h/2, x + k1/2)
        k3 = h*func(t + h/2, x + k2/2)
        k4 = h*func(t + h, x + k3)
        x = x + (k1 + 2*k2 + 2*k3 + k4)/6
        t = t + h
        T.append(t)
        X.append(x)
    return [T, X]

例として,簡単なロジスティック方程式

$$ \frac{dx}{dt} = f(t, x) = (1-x)\,x$$

を,初期条件 $t_0 = 0$ で $x_0 = x(t_0) = 0.1$,$t = t_0 = 0$ から $t_1 = 10$ までを $ N$ 分割して解きます。

ルンゲ・クッタ法の精度は分割数 $N$ に依存します。$N$ の値を変えて $x(t_1)$ の値を計算して精度を確認します。

In [9]:
# dx/dt = f(t, x) を解く。
def f(t, x):
    return (1-x)*x

x0 = 0.1
t0 = 0
t1 = 10

for N in [100, 200, 400, 800, 1000]:
    tmp = rk1(f, x0, t0, t1, N)
    print("N =%5d, x(t1) =%15.12f" % (N, tmp[1][-1]))
N =  100, x(t1) = 0.999591565222
N =  200, x(t1) = 0.999591567379
N =  400, x(t1) = 0.999591567509
N =  800, x(t1) = 0.999591567517
N = 1000, x(t1) = 0.999591567517

上の結果から,$N=800$ くらいで小数点以下 $12$ 桁程度の精度があることがわかります。ではこのくらいの $N$ で計算をすることにします。

In [10]:
# dx/dt = f(t, x) を解く。
def f(t, x):
    return (1-x)*x

x0 = 0.1
t0 = 0
t1 = 10

ndiv = 40
Ndiv = 20
N = ndiv * Ndiv

ans1 = rk1(f, x0, t0, t1, N)

ans1 には N+1 点の数値解の値が代入されています。滑らかな曲線のグラフを描くには,全部の点を使えばいいですが,ここでは少し間引いて Ndiv+1 個の点をグラフにしてみます。(あとで解析解と比べてみるため。)

In [11]:
# データを間引いてグラフに
T1, X1 = ans1
t1 = [T1[i] for i in range(0, len(T1), ndiv)]
x1 = [X1[i] for i in range(0, len(X1), ndiv)]

plot_list(t1, x1, is_point = True, 
          xlabel = "$t$", ylabel = "$x(t)$",
          title = "ロジスティック方程式の数値解", 
          # "ms" つまり "markersize" を小さめに
          rendering_kw={"ms":3});

SymPy による1階常微分方程式の解析解

簡単な常微分方程式の場合は SymPy の dsolve() で解析的に解くことができます。

例として,簡単なロジスティック方程式

$$ \frac{dx}{dt} = (1-x)\,x$$

を,初期条件 $t = 0$ で $x(0) = 0.1$ として解きます。

In [12]:
# 解くべき微分方程式を eq として定義
var('t')
x = Function('x')(t)
eq = Eq(Derivative(x,t), (1-x)*x)
eq
Out[12]:
$\displaystyle \frac{d}{d t} x{\left(t \right)} = \left(1 – x{\left(t \right)}\right) x{\left(t \right)}$
In [13]:
# 初期条件を特に設定せずに微分方程式を解く
dsolve(eq, x)
Out[13]:
$\displaystyle x{\left(t \right)} = \frac{1}{C_{1} e^{- t} + 1}$
In [14]:
# 初期条件を t=0 で x(0) = 1/10 として解く
sol1 = dsolve(eq, x, ics={x.subs(t,0): Rational(1,10)})
sol1
Out[14]:
$\displaystyle x{\left(t \right)} = \frac{1}{1 + 9 e^{- t}}$
In [15]:
# ans の右辺を表示 
# rhs は rirhgt-hand-side
simplify(sol1.rhs)
Out[15]:
$\displaystyle \frac{e^{t}}{e^{t} + 9}$

… ということで,解析解は

$$x(t) = \frac{\exp(t)}{9 + \exp(t)}$$

であることがわかります。
得られた解析解 $x(t)$ とルンゲ・クッタ法で得られた数値解をグラフにする例。

In [16]:
p1 = plot(sol1.rhs, (t, 0, 10), "解析解", 
          xlabel = "$t$", ylabel = "$x(t)$", 
          title = "ロジスティック方程式の解", 
          rendering_kw={"color":"red"},
          show=False)

p2 = plot_list(t1, x1, "数値解", 
               is_point = True, 
               # "ms" つまり "markersize" を小さめに
               rendering_kw={"ms":3, "color":"tab:blue"}, 
               show=False)

(p1 + p2).show();

得られた $x(t)$ をどうやって関数に定義するか悩みます。以下の例でなんとかなりますが,少しトリッキーですかねぇ。

In [17]:
def xsol1(s):
    return sol1.rhs.subs(t,s)

xsol1(t)
Out[17]:
$\displaystyle \frac{1}{1 + 9 e^{- t}}$

ルンゲ・クッタ法による2階常微分方程式の数値解法

常微分方程式の数値解法をしてくれる関数は SymPy には用意されていないようです。備え付けの関数に頼らず,次のような2階常微分方程式をルンゲ・クッタ法で解いてみます。

$$ \frac{d^2 x}{dt^2 } = F\left(t, x, \frac{dx}{dt}\right)$$

この場合には, $\displaystyle v \equiv \frac{dx}{dt}$ とおけば,次のような連立1階常微分方程式の形に帰着できる。

\begin{eqnarray}
\frac{dx}{dt} &=& F_1(t, x, v) = v \\
\frac{dv}{dt} &=& F_2(t, x, v) = F(t, x, v)
\end{eqnarray}

初期条件を $t=t_0$ で $x(t_0) = x_0, v(t_0) = v_0$ とし,$t = t_1$ までを $N$ 分割して解きます。

$$ h = \frac{t_1 – t_0}{N}, \quad t_i = t_0 + i\, h,
\quad x_i = x(t_i), \quad v_i = v(t_i)$$

とし,以下の式から次のステップの値 $x_{i+1}, v_{i+1}$ を決めます。

\begin{eqnarray}
k_1 &=& h\, F_1(t_i, x_i, v_i) = h\,v_i \\
m_1 &=& h\, F_2(t_i, x_i, v_i) \\
k_2 &=& h\, F_1(t_i + h/2, x_i + k_1/2, v_i + m_1/2) = h\,(v_i + m_1/2)\\
m_2 &=& h\, F_2(t_i + h/2, x_i + k_1/2, v_i + m_1/2) \\
k_3 &=& h\, F_1(t_i + h/2, x_i + k_2/2, v_i + m_2/2) = h\,(v_i + m_2/2)\\
m_3 &=& h\, F_2(t_i + h/2, x_i + k_2/2, v_i + m_2/2) \\
k_4 &=& h\, F_1(t_i + h, x_i + k_3, v_i + m_3) = h\,(v_i + m_3)\\
m_4 &=& h\, F_2(t_i + h, x_i + k_3, v_i + m_3) \\
x_{i+1} &=& x_i + \frac{1}{6} (k_1 + 2 k_2 + 2 k_3 + k_4)\\
v_{i+1} &=& v_i + \frac{1}{6} (m_1 + 2 m_2 + 2 m_3 + m_4)
\end{eqnarray}

以上のような計算を行う関数 rk2() を Python の関数として以下のようにして定義します。

In [18]:
def rk2(Func, x0, v0, t0, t1, N):
    """ d^2 x/dt^2 = Func(t, x, v) を
        初期条件 t = t0 で x = x0, v = v0 とし,
        [t0, t1] を N 分割して
        4次のルンゲ・クッタ法で解く
    """
    h = (t1 - t0)/N
    T = [t0]
    X = [x0]
    V = [v0]
    t = t0
    x = x0
    v = v0
    for i in range(N):
        k1 = h*v
        m1 = h*Func(t, x, v)
        k2 = h*(v+m1/2)
        m2 = h*Func(t+h/2, x+k1/2, v+m1/2)
        k3 = h*(v+m2/2)
        m3 = h*Func(t+h/2, x+k2/2, v+m2/2)
        k4 = h*(v+m3)
        m4 = h*Func(t+h, x+k3, v+m3)
        x = x + (k1 + 2*k2 + 2*k3 + k4)/6
        v = v + (m1 + 2*m2 + 2*m3 + m4)/6
        t = t + h 
        T.append(t)
        X.append(x)
        V.append(v)
    return [T, X, V]

例として,簡単な減衰+強制振動の方程式

$$ \frac{d^2 x}{dt^2} = F(t, x, v) = -x – a v + b \cos(t), \quad v = \frac{dx}{dt} $$

を,

  • $a , b $ にいくつか値を入れて
  • 初期条件 $t_0 = 0$ で $x_0 = x(t_0) = 3, v_0 = v(t_0) = 0$,
  • $t = t_0 = 0$ から $t_1 = 20$ までを $ N $ 分割

して解きます。

$N$ の値をいくつか変えてみて,精度を確認します。(結構時間がかかります。)

In [19]:
def F(t, x, v):
    global a, b
    return -x - a*v + b*cos(t)

a = 0.5
b = 0.2
x0 = 3
v0 = 0
t0 = 0
t1 = 20

for N in [200, 400, 800, 1000]:
    T, X, V = rk2(F, x0, v0, t0, t1, N)
    print("N =%5d, x(t1) =%15.12f" % (N, X[-1]))
N =  200, x(t1) = 0.383967288338
N =  400, x(t1) = 0.383966889383
N =  800, x(t1) = 0.383966861347
N = 1000, x(t1) = 0.383966860190

上の結果から,$N=800$ くらいで小数点以下 $8$ 桁程度の精度があることがわかります。ではこのくらいの $N$ で計算をすることにします。

In [20]:
ndiv = 20
Ndiv = 40
N = ndiv * Ndiv

T2, X2, V2 = rk2(F, x0, v0, t0, t1, N)

T, X, V には N+1 点の数値解の値が代入されています。滑らかな曲線のグラフを描くには,全部の点を使えばいいですが,ここでは少し間引いてグラフにしてみます。(あとで解析解と比べてみるため。)

In [21]:
t2 = [T2[i] for i in range(0, len(T2), ndiv)]
x2 = [X2[i] for i in range(0, len(X2), ndiv)]

plot_list(t2, x2, 
          is_point = True, 
          xlabel = "$t$", ylabel = "$x(t)$",
          title = "減衰+強制振動", 
          # "ms" つまり "markersize" を小さめに
          rendering_kw={"ms":3});

SymPy による2階常微分方程式の解析解

簡単な常微分方程式の場合は SymPy の dsolve() で解析的に解くことができます。
例として,簡単な減衰+強制振動の方程式

$$ \frac{d^2 x}{dt^2} = -x – a \frac{dx}{dt} + b \cos(t) $$

を,

  • $a , b $ にいくつか値を入れて
  • 初期条件 $t_0 = 0$ で $x_0 = x(t_0) = 3, v_0 = v(t_0) = 0$,
    として解きます。

まず,$a, b$ を変数とした微分方程式を返す関数 eq(a, b) を定義します。

In [22]:
t = Symbol('t')
x = Function('x')(t)
a, b = symbols('a b')

def eq(a, b):
    return Eq(Derivative(x, t, 2), 
              -x -a*Derivative(x, t) + b*cos(t))

eq(a, b)
Out[22]:
$\displaystyle \frac{d^{2}}{d t^{2}} x{\left(t \right)} = – a \frac{d}{d t} x{\left(t \right)} + b \cos{\left(t \right)} – x{\left(t \right)}$

まずは $a = 0, b = 0$ の場合に,初期条件 $x(0) = 3, v(0) = \dot{x}(0) = 0$ のもとで dsolve() で解析的に解きます。

In [23]:
sol0 = dsolve(eq(0, 0), x, 
         ics = {x.subs(t, 0):3, diff(x, t).subs(t,0):0})
sol0
Out[23]:
$\displaystyle x{\left(t \right)} = 3 \cos{\left(t \right)}$

次に,$a = \frac{1}{2}, b = \frac{2}{10}$ の場合。以下のように Rational() 関数を使うと,実数値に変換しないで(約分された)分数のままで扱います。

In [24]:
a = Rational(5,10)
b = Rational(2,10)

sol2 = dsolve(eq(a, b), x, 
         ics = {x.subs(t, 0):3, diff(x, t).subs(t,0):0})
sol2
Out[24]:
$\displaystyle x{\left(t \right)} = \left(\frac{7 \sqrt{15} \sin{\left(\frac{\sqrt{15} t}{4} \right)}}{75} + 3 \cos{\left(\frac{\sqrt{15} t}{4} \right)}\right) e^{- \frac{t}{4}} + \frac{2 \sin{\left(t \right)}}{5}$

求まった解析解を関数として定義する例です。(ちょっとトリッキーですかね?)

sol2 の右辺 rhsts を代入して関数 xsol2(s) を定義します。

In [25]:
def xsol2(s):
    return sol2.rhs.subs(t, s)
In [26]:
key = "$a = %3.1f, b = %3.1f$ の解析解" % (a, b)
p4 = plot(xsol2(t), (t, 0, 20), key, 
          xlim = (0, 20), 
          xlabel = "$t$", ylabel = "$x(t)$",
          title = "減衰+強制振動", show=False);

key = "$a = %3.1f, b = %3.1f$ の数値解" % (a, b)
p5 = plot_list(t2, x2, key,
          is_point = True, 
          title = "減衰+強制振動", 
          # "ms" つまり "markersize" を小さめに
          rendering_kw={"ms":3}, show=False)

(p4+p5).show();

問題

減衰+強制振動の方程式を以下の場合に解き,グラフにする。

まず $a$ の影響を調べるため,

  1. $a = 0, b = 0$
  2. $a = 0.2, b = 0$
  3. $a = 0.5, b = 0$
  4. $a = 0.8, b = 0$

次に,$b$ の影響を調べるため,

  1. $a = 0, b = 0$
  2. $a = 0, b = 0.2$
  3. $a = 0, b = 0.5$
  4. $a = 0, b = 0.8$