Python で数値解析のために必要なプログラミングと,いくつかの例を示します。
ここでは数値解析ライブラリである SciPy を使わず,計算機代数システムである SymPy をあえて使った例を示します。
また SymPy 自体でもグラフ作成機能がありますが,ここでは,SymPy Plotting Backends (SPB) を使ってグラフを描いてみます。
from sympy.abc import *
from sympy import *
# グラフは SymPy Plotting Backends (SPB) で描く
from spb import *
# 以下はグラフを SVG で Notebook にインライン表示させる設定
%config InlineBackend.figure_formats = ['svg']
SymPy による数値積分
$\displaystyle \int_0^{\pi} \sin(\sin x)\, dx$ の計算をします。解析的に解けないと,そのままで返します。
integrate(sin(sin(x)), (x, 0, pi))
.evalf()
すると数値積分した値を返します。
integrate(sin(sin(x)), (x, 0, pi)).evalf()
SymPy による方程式の数値解
まず,解を求める関数 $f(x)$ を定義し,$f(x) = 0$ となる $x$ のあたりをつけるためにグラフを描いてみます。
以下では例としてベッセル関数 besselj(0,x)
のゼロ点を求めてみます。ベッセル関数 besselj()
は
from sympy import *
すれば使えます。$x$ 軸を横切る点がわかりやすいように,$y=0$ ($x$ 軸)も描いてみます。
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$ 軸と交わっていますから,このあたりの解を探します。
# besselj(0,x) = 0 となる解を
# x が 2.5 のあたりから始めて 15 桁の精度で求める
nsolve(besselj(0,x), x, 2.5, prec=15)
[2, 4]
のように範囲を指定して解を探すこともできます。
nsolve(besselj(0,x), x, [2, 4], prec=15)
# ほんとに解になってるか確認。ゼロに近ければよい。
besselj(0,_).evalf()
ルンゲ・クッタ法による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 の関数として以下のようにして定義します。
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)$ の値を計算して精度を確認します。
# 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=800$ くらいで小数点以下 $12$ 桁程度の精度があることがわかります。ではこのくらいの $N$ で計算をすることにします。
# 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
個の点をグラフにしてみます。(あとで解析解と比べてみるため。)
# データを間引いてグラフに
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$ として解きます。
# 解くべき微分方程式を eq として定義
var('t')
x = Function('x')(t)
eq = Eq(Derivative(x,t), (1-x)*x)
eq
# 初期条件を特に設定せずに微分方程式を解く
dsolve(eq, x)
# 初期条件を t=0 で x(0) = 1/10 として解く
sol1 = dsolve(eq, x, ics={x.subs(t,0): Rational(1,10)})
sol1
# ans の右辺を表示
# rhs は rirhgt-hand-side
simplify(sol1.rhs)
… ということで,解析解は
$$x(t) = \frac{\exp(t)}{9 + \exp(t)}$$
であることがわかります。
得られた解析解 $x(t)$ とルンゲ・クッタ法で得られた数値解をグラフにする例。
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)$ をどうやって関数に定義するか悩みます。以下の例でなんとかなりますが,少しトリッキーですかねぇ。
def xsol1(s):
return sol1.rhs.subs(t,s)
xsol1(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 の関数として以下のようにして定義します。
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$ の値をいくつか変えてみて,精度を確認します。(結構時間がかかります。)
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=800$ くらいで小数点以下 $8$ 桁程度の精度があることがわかります。ではこのくらいの $N$ で計算をすることにします。
ndiv = 20
Ndiv = 40
N = ndiv * Ndiv
T2, X2, V2 = rk2(F, x0, v0, t0, t1, N)
T, X, V
には N+1
点の数値解の値が代入されています。滑らかな曲線のグラフを描くには,全部の点を使えばいいですが,ここでは少し間引いてグラフにしてみます。(あとで解析解と比べてみるため。)
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)
を定義します。
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)
まずは $a = 0, b = 0$ の場合に,初期条件 $x(0) = 3, v(0) = \dot{x}(0) = 0$ のもとで dsolve()
で解析的に解きます。
sol0 = dsolve(eq(0, 0), x,
ics = {x.subs(t, 0):3, diff(x, t).subs(t,0):0})
sol0
次に,$a = \frac{1}{2}, b = \frac{2}{10}$ の場合。以下のように Rational()
関数を使うと,実数値に変換しないで(約分された)分数のままで扱います。
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
求まった解析解を関数として定義する例です。(ちょっとトリッキーですかね?)
解 sol2
の右辺 rhs
の t
に s
を代入して関数 xsol2(s)
を定義します。
def xsol2(s):
return sol2.rhs.subs(t, s)
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$ の影響を調べるため,
- $a = 0, b = 0$
- $a = 0.2, b = 0$
- $a = 0.5, b = 0$
- $a = 0.8, b = 0$
次に,$b$ の影響を調べるため,
- $a = 0, b = 0$
- $a = 0, b = 0.2$
- $a = 0, b = 0.5$
- $a = 0, b = 0.8$