Return to Python で数値解析

Python の SciPy で数値解析

Python で数値解析するためのライブラリである SciPy の使用例を示します。グラフは Matplotlibpyplot.plot() を使って描きます。

In [1]:
# NumPy も使います
import numpy as np
# Matplotlib でグラフを描きます
import matplotlib.pyplot as plt

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

Matplotlib.pyplot.plot の基本

In [2]:
# 横軸 x 座標の値と... 
x = [0, 1, 2, 3, 4, 5]
# 対応する縦軸 y 座標の値を準備する
y = [0**2, 1**2, 2**2, 3**2, 4**2, 5**2]

plt.plot(x, y);

In [3]:
# 線でつながなず,点々だけを描く例
plt.plot(x, y, linestyle='', marker='o');

In [4]:
# 線でつながなず,点々だけを描くもう一つの例
plt.scatter(x, y);

In [5]:
# 関数 y = sin(x) を -2π から 2π まで描く例
# π は numpy で定義されている定数 np.pi を使う
# -2π から 2π の範囲を 100 等分した点を生成する
x = np.linspace(-2*np.pi, 2*np.pi, 100)

# sin も numpy で定義されている関数 np.sin() を使う
plt.plot(x, np.sin(x));

SciPy による数値微分

scipy.misc.derivative()

SciPy には数値微分してくれる関数 scipy.misc.derivative() が用意されています。それを使ってみましょう。

例として $f(x) = \sin x$ の $x = 3$ における数値微分を求めてみます。

(答えは $\cos(3)$ です。)

In [6]:
from scipy.misc import derivative

def f(x):
    return np.sin(x)

# 数値微分の値
print("数値微分", derivative(f, 3, dx=1E-6))

# 解析的微分の値
print("解析解 ", np.cos(3))
数値微分 -0.9899924967304852
解析解  -0.9899924966004454

以下の例では,$f(x) = \sin x$ に対して $f'(x) = \cos x$ と,上記の数値微分との誤差を描いています。

In [7]:
def f(x):
    return np.sin(x)

def df(x, h):
    return derivative(f, x, dx=h)

x = np.linspace(-2*np.pi, 2*np.pi, 101)
plt.plot(x, np.cos(x) - df(x, 1E-6));

以下の例からわかるように,h をやみくもに小さくすればするほど,数値微分の精度があがる,というわけでもなさそうです。(縦軸の目盛りに注目。)

In [8]:
plt.plot(x, np.cos(x) - df(x, 1E-7));

SciPy による数値積分

scipy.integrate.quad()

SciPy には数値積分してくれる関数 scipy.integrate.quad() が用意されています。それを使ってみましょう。結果には積分値と推定誤差が出力されます。

$\displaystyle \int_0^{\pi} \sin(\sin x)\, dx$ の計算をします。

In [9]:
from scipy.integrate import quad

def sinsin(x):
    return np.sin(np.sin(x))

# quad で数値積分
ans = quad(sinsin, 0, np.pi)
print(ans)

# 積分値のみを取り出す
print(ans[0])
(1.7864874819500525, 2.4687274069628184e-11)
1.7864874819500525

SciPy による求根(方程式の数値解)

scipy.optimize.root_scalar

例としてベッセル関数 $J_0(x)$ のゼロ点を求めてみます。SciPy におけるベッセル関数 $J_0(x)$ は scipy.special.jv(0,x) です。

まずは $J_0(x) = 0$ となる $x$ のあたりをつけるためにグラフを描いてみます。

In [10]:
from scipy.special import jv

def J0(x):
    return jv(0,x)

# pyplot.plot でグラフを描く
x = np.linspace(0, 10, 101)
plt.plot(x, J0(x));

# pyplot.plot で x 軸を描く
plt.plot(x, 0*x, color='black');

plt.xlabel('x');
plt.ylabel('J0(x)');
plt.grid();
plt.xlim(0, 10);
plt.title('ベッセル関数 J0(x)');

In [11]:
from scipy.optimize import root_scalar

# x が 2 ~ 3 の間で j0(x) = 0 の根を探す
ans = root_scalar(J0, bracket = [2, 3])
ans.root
Out[11]:
2.404825557695807
In [12]:
# ほんとに根になってるか確認。ゼロに近ければよい。
J0(_)
Out[12]:
-1.7918655681235457e-14
In [13]:
# どうしてももっと精度が欲しいというのであれば...
# xtol に,小さい値をいれてみる
ans = root_scalar(J0, bracket = [2, 3], xtol=1e-14)
ans.root
Out[13]:
2.4048255576957724
In [14]:
# ほんとに根になってるか確認。ゼロに近ければよい。
J0(_)
Out[14]:
1.535884772676773e-16

SciPy による1階常微分方程式の数値解法

SciPy には常微分方程式を数値的に解いてくれる関数 scipy.integrate.solve_ivp() が用意されています。それを使ってみましょう。

scipy.integrate.solve_ivp()

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

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

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

参考までに,解析解は

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

In [15]:
from scipy.integrate import solve_ivp

# scipy.integrate.solve_ivp() は
# dy/dt = fun(t, y) の形を解く

def fun(t, y):
# solve.ivp() に渡す関数の引数の順番に注意。t が先
# 連立でないときは,1次元リストとして返す
    return [(1-y)*y]

t0 = 0
t1 = 10
t_span = [t0, t1]
y_ini = [0.1]

t_list = np.linspace(t0, t1, 21)
answer = solve_ivp(fun, t_span, y_ini, 
                   t_eval = t_list, 
                   rtol = 1.e-12, 
                   atol = 1.e-14)

# scatter では点のサイズは s=10 などと設定
# 計算結果は .y として取り出します。
plt.scatter(answer.t, answer.y[0], s=10);
plt.grid();
plt.xlabel("t");
plt.ylabel("x(t)");
plt.title("ロジスティック方程式の数値解");

In [16]:
# 解析解
def x(t):
    return np.exp(t)/(9+np.exp(t))

# 解析解のラインも描く
t = np.linspace(t0, t1, 201)
plt.plot(t, x(t), color='red', linewidth=1, label='解析解');

plt.scatter(answer.t, answer.y[0], s=10, label='数値解');
plt.grid();
plt.xlabel("t");
plt.ylabel("x(t)");
plt.title("ロジスティック方程式の数値解");
plt.legend();

SciPy による2階常微分方程式の数値解法

scipy.integrate.solve_ivp()

SciPy には常微分方程式を数値的に解いてくれる関数 scipy.integrate.solve_ivp() が用意されています。この函数は,1階常微分方程式だけでなく,連立1階常微分方程式も,従って高階の常微分方程式も解いてくれるので,それを使ってみましょう。

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

$$ \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$,
  • $t = t_0 = 0$ から $t_1 = 20$ までを解きます。

2階常微分方程式は,

\begin{eqnarray}
\frac{dx}{dt} &=& F_1(t,x,v) = v \\
\frac{dv}{dt} &=& F_2(t,x,v) = -x – a v + b \cos t
\end{eqnarray}

として連立1階微分方程式の形にして解きます。

In [17]:
from scipy.integrate import solve_ivp

# scipy.integrate.solve_ivp() は
# dy/dt = Fun(t, y) の形を解く

def Fun(t, y, a, b):
# solve.ivp() に渡す関数の引数の順番に注意。t が先
    x = y[0]
    v = y[1]
    F1 = v
    F2 = -x - a*v + b*np.cos(t)
    return [F1, F2]

t0 = 0
t1 = 20
t_span = [t0, t1]
y_ini = [3, 0]

t_list = np.linspace(t0, t1, 201)

a = 0.5; b = 0.2
answer1 = solve_ivp(Fun, t_span, y_ini, 
                    t_eval = t_list, 
                    args=(a, b), 
                    rtol = 1.e-12, 
                    atol = 1.e-14)

# 数値解は .y[0] のようにして取り出す
plt.plot(answer1.t, answer1.y[0], label="a=0.5, b=0.2");
plt.grid();
plt.xlabel("t");
plt.ylabel("x(t)");
plt.legend();
plt.title("減衰+強制振動");
plt.xlim(0, 20);

問題

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

まず $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$
In [ ]: