Return to 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']

SciPy による数値積分

scipy.integrate.quad()

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

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

数値計算の際は,三角関数等は NumPy の関数を使い,円周率 $\pi$ も NumPy の np.pi を使います。

In [2]:
from scipy.integrate import quad

# quad を使うときは変数名 x 決め打ち
def sinsin(x):
    return np.sin(np.sin(x))

# quad で数値積分
quadans = quad(sinsin, 0, np.pi)
print(quadans)
(1.7864874819500525, 2.4687274069628184e-11)
In [3]:
# 積分値のみを出力するには...
print("積分値は", quadans[0])

# 別のやり方
ans, err = quadans
print("積分値は", ans)
print("推定誤差は", err)
積分値は 1.7864874819500525
積分値は 1.7864874819500525
推定誤差は 2.4687274069628184e-11

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

scipy.optimize.root_scalar()

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

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

In [4]:
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 [5]:
from scipy.optimize import root_scalar

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

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

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

scipy.integrate.solve_ivp()

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

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

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

solve_ivp() を使う場合は変数名が決め打ちで t, y です。連立常微分方程式の場合は y がリストになります。

In [10]:
from scipy.integrate import solve_ivp

# scipy.integrate.solve_ivp() は
# dy/dt = f(t, y) の形を解く
# solve_ivp を使う際は変数名 t, y は決め打ち
# solve.ivp() に渡す関数の引数の順番に注意。t が先
# 連立でないときは,1次元リストとして返す

def f(t, y):
    x = y[0]
    return [(1-x)*x]

# t の範囲
tini = 0
tend = 10
# 計算する点は t の範囲を N 等分して N+1 点
N = 20
t_list = np.linspace(tini, tend, N+1)
# y の初期値
y_ini = [0.1]

answer = solve_ivp(f, [tini, tend], y_ini, t_eval = t_list, 
                   rtol = 1.e-12, 
                   atol = 1.e-14)
print(answer)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  5.000e-01 ...  9.500e+00  1.000e+01]
        y: [[ 1.000e-01  1.548e-01 ...  9.993e-01  9.996e-01]]
      sol: None
 t_events: None
 y_events: None
     nfev: 1784
     njev: 0
      nlu: 0
In [11]:
# グラフにするために,t の値と y の値を取り出す
# t は answer.t, y は answer.y[0] として取り出す
# scatter では点のサイズは s=10 などと設定
plt.scatter(answer.t, answer.y[0], s=10)
plt.grid()
plt.xlabel("$t$")
plt.ylabel("$x(t)$")
plt.title("ロジスティック方程式の数値解");

参考までに,解析解は

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

です。(解析解がわかっているときには,わざわざ数値計算する必要はないのですが,練習ということで。)

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

# 解析解のラインも描く
t = np.linspace(tini, tend, 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 [13]:
from scipy.integrate import solve_ivp

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

def F(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]

tini = 0
tend = 20
t_list = np.linspace(tini, tend, 201)
y_ini = [3, 0]

a = 0.5; b = 0.2
answer1 = solve_ivp(F, [tini, tend], y_ini, t_eval = t_list, 
                    args=(a, b), 
                    rtol = 1.e-12, 
                    atol = 1.e-14)

print(answer1)
  message: The solver successfully reached the end of the integration interval.
  success: True
   status: 0
        t: [ 0.000e+00  1.000e-01 ...  1.990e+01  2.000e+01]
        y: [[ 3.000e+00  2.986e+00 ...  3.670e-01  3.840e-01]
            [ 0.000e+00 -2.727e-01 ...  1.879e-01  1.509e-01]]
      sol: None
 t_events: None
 y_events: None
     nfev: 8606
     njev: 0
      nlu: 0
In [14]:
# 数値解 x は .y[0] のようにして取り出す
# 数式を label に設定する際は $ で囲んで LaTeX 式に
# a, b の値を凡例に
key = '$a=%3.1f, b=%3.1f$' % (a, b)
plt.plot(answer1.t, answer1.y[0], label=key)
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$