Return to 単振り子のエネルギー保存則から周期を求める準備

SciPy で単振り子のエネルギー保存則から周期を求める

単振り子の運動方程式から得られるエネルギー保存則から,Python の SciPy と NumPy を使って(かつ SymPy は使わずに)数値積分によって周期を求める。導出については以下を参照:

必要なモジュールの import

In [1]:
# NumPy も使います
import numpy as np

# 数値積分 scipy.integrate.quad()
from scipy.integrate import quad

# 第1種完全楕円積分 K(k)
from scipy.special import ellipk

# Matplotlib でグラフを描きます
import matplotlib.pyplot as plt

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

平方根や三角関数などは NumPy の関数 np.sqrt()np.cos()np.sin() などを使います。scipy.integrate.quad() については授業で簡単な説明をしています。

数値計算に限ることにし,SymPy の関数を混在させないことにして,NumPy で定義された関数や定数のみを使うようにします。

SciPy の quad() で数値積分する

振幅 $\theta_0$ の単振り子の周期 $T_p$ は

\begin{eqnarray}
T_p(\theta_0) &=&
\frac{2}{\pi} \int_{0}^{\theta_0} \frac{1}{\sqrt{2(\cos\theta-\cos\theta_0)}}d\theta \\
&=&
\frac{\sqrt{2}}{\pi} \int_{0}^{\theta_0} \frac{1}{\sqrt{\cos\theta-\cos\theta_0}}d\theta
\end{eqnarray}

積分部分は数値積分 scipy.integrate.quad() を使う。

$\theta = \theta_0$ で被積分関数の分母はゼロになってしまうので,何かエラーでも起こるのかと思いきや,scipy.integrate.quad() は優秀で何事もなかったかのように数値積分してしまいます!

quad() は (積分の近似値, 見積もられた近似の絶対誤差) の2つの数値を返します。

SciPy の quad()Tp1(th0)

In [2]:
# 被積分関数の定義
# quad() を使う場合は積分変数名は x 決め打ち
def f1(x, theta0):
    return np.sqrt(2)/(np.pi*np.sqrt(np.cos(x)-np.cos(theta0)))

# 周期 Tp1(theta0) の定義
def Tp1(th0):
    """度で与えられた振幅 th0 から
       規格化された単振り子の規格化された周期を求める"""
    # 度で与えられた th0 からラジアンへ変換
    theta0 = np.radians(th0)
    ans = quad(f1, 0, theta0, args=theta0)
    # 積分値のみを ans[0] としてとり,周期をかえす
    # 参考までに誤差 ans[1] も返す
    return ans[0], ans[1]

セルの先頭に %%time を書くと,セルの実行(計算)にかかった時間を表示します。

In [3]:
%%time 

Tp1(60)
CPU times: user 1.37 ms, sys: 0 ns, total: 1.37 ms
Wall time: 1.38 ms
Out[3]:
(1.0731820071170235, 6.641198702084239e-11)

(SymPy の関数で定義した場合とくらべて)圧倒的に短い時間で計算してくれます。ただし,このままだと,(被積分関数が端点で発散するという問題もあり)誤差が小さくならないようです。

Tp1(th0) のグラフ

In [4]:
# 振幅と周期のグラフを描くために x1, y1 に追加していく
x1 = []
y1 = []

# 参考として,ほぼ単振動となるであろう θ_0 = 1°の場合
th0 = 1
x1 += [th0]
# 数値積分値のみを取り出すには Tp1(th0)[0]
y1 += [Tp1(th0)[0]]
# リストに append したばかりの最後の値を表示 
print('θ_0 = %2d° のとき  ' % x1[-1], end = '')
print('周期 T = %10.8f' % y1[-1])

# θ_0 を 10° から 90° まで 10° ごとにかえて計算
for th0 in range(10, 91, 10):
    x1 += [th0]
    y1 += [Tp1(th0)[0]]
    print('θ_0 = %2d° のとき  ' % x1[-1], end = '')
    print('周期 T = %10.8f' % y1[-1])
θ_0 =  1° のとき  周期 T = 1.00001904
θ_0 = 10° のとき  周期 T = 1.00190719
θ_0 = 20° のとき  周期 T = 1.00766903
θ_0 = 30° のとき  周期 T = 1.01740880
θ_0 = 40° のとき  周期 T = 1.03134052
θ_0 = 50° のとき  周期 T = 1.04978296
θ_0 = 60° のとき  周期 T = 1.07318201
θ_0 = 70° のとき  周期 T = 1.10214491
θ_0 = 80° のとき  周期 T = 1.13749256
θ_0 = 90° のとき  周期 T = 1.18034060
In [5]:
# 横軸に振幅 x1, 縦軸に周期 y1 
plt.scatter(x1, y1, s=10)

plt.grid(linestyle='dotted')
plt.xlabel("振幅 θ (°)")
plt.xlim(0, 91)
plt.xticks(list(range(10,91,10)))
plt.ylim(0.95, 1.2)
plt.ylabel("規格化された周期 T")
plt.title("単振り子の振幅と周期");

被積分関数が発散しないように変数変換して置換積分

このへんにまとめているように,変数変換によって
\begin{eqnarray}
\int_0^{\theta_0} \frac{1}{\sqrt{2(\cos\theta-\cos\theta_0)}} d\theta
&=&
\int_0^{\pi/2} \frac{dt}{\sqrt{1 – k^2 \sin^2 t}}, \\ \quad k &\equiv& \sin\frac{\theta_0}{2}
\end{eqnarray}

と書ける。この形にすると,積分範囲内で被積分関数が発散することもない。

SciPy の quad()Tp2(th0)

In [6]:
# 被積分関数の定義
# quad() を使う場合は積分変数名は x 決め打ち
def f2(x, theta0):
    k = np.sin(theta0/2)
    return 2/(np.pi * np.sqrt(1-k**2*np.sin(x)**2))

def Tp2(th0):
    theta0 = np.radians(th0)
    ans = quad(f2, 0, np.pi/2, args=theta0)
    # 積分値のみを ans[0] としてとり,係数をかけて周期をかえす
    return ans[0], ans[1] 
In [7]:
%%time

Tp2(60)
CPU times: user 143 µs, sys: 0 ns, total: 143 µs
Wall time: 148 µs
Out[7]:
(1.0731820071493643, 1.1914713739506635e-14)

被積分関数が発散しないように変数変換してから quad() で数値積分すると,誤差が小さく,より短時間で実行しているのがわかります。

第1種完全楕円積分を使う

このへんにまとめているように,
第1種完全楕円積分 $K(k)$ を使うと,振幅 $\theta_0$ の単振り子の周期 $T_p$ は

\begin{eqnarray}
T_p(\theta_0) &=&
\frac{2}{\pi} \int_{0}^{\theta_0} \frac{1}{\sqrt{2(\cos\theta-\cos\theta_0)}}d\theta \\
&=& \frac{2}{\pi} K(k) \\
&\equiv& \frac{2}{\pi}\int_0^{\pi/2} \frac{dt}{\sqrt{1-k^2 \sin^2 t}}, \quad k \equiv \sin\frac{\theta_0}{2}
\end{eqnarray}

SciPy の ellipk(m)Tp3(th0)

ありがたいことに,SciPy では第1種完全楕円積分が使える。 マニュアルは以下を参照:

$$\mbox{ellipk(m)} = K(m) \equiv \int_0^{\frac{\pi}{2}}\frac{1}{\sqrt{1 – m \sin^2 x}} dx$$

第1種完全楕円積分 scipy.special.ellipk(m) を使って,振幅 $\theta_0$ の単振り子の周期 $T_p$ を以下のように定義する。($m = k^2$ に注意。)

In [8]:
def Tp3(th0):
    theta0 = np.radians(th0)
    m = np.sin(theta0/2)**2
    return 2/np.pi*ellipk(m)
In [9]:
%%time

Tp3(60)
CPU times: user 49 µs, sys: 0 ns, total: 49 µs
Wall time: 62.2 µs
Out[9]:
1.0731820071493645

練習

Tp1(th0), Tp2(th0), Tp3(th0)の誤差および計算時間を調べる。

th0 = 60 の場合にはすでに出している。他の角度についても計算してみる。

In [10]:
th0_list = np.append(1, np.arange(10, 91, 10))
th0_list
Out[10]:
array([ 1, 10, 20, 30, 40, 50, 60, 70, 80, 90])
In [11]:
%%time

TP1 = []
for th0 in th0_list:
    TP1 += [Tp1(th0)[0]]
CPU times: user 12.5 ms, sys: 0 ns, total: 12.5 ms
Wall time: 12.1 ms
In [12]:
%%time

TP2 = []
for th0 in th0_list:
    TP2 += [Tp2(th0)[0]]
CPU times: user 453 µs, sys: 329 µs, total: 782 µs
Wall time: 788 µs
In [13]:
%%time

TP3 = []
for th0 in th0_list:
    TP3 += [Tp3(th0)]
CPU times: user 46 µs, sys: 34 µs, total: 80 µs
Wall time: 84.4 µs
In [14]:
for i in range(len(th0_list)):
    th0 = th0_list[i]
    print('Tp1(%2d)=%.15f' % (th0, TP1[i]))
    print('Tp2(%2d)=%.15f' % (th0, TP2[i]))
    print('Tp3(%2d)=%.15f' % (th0, TP3[i]))
    print('')
Tp1( 1)=1.000019039346868
Tp2( 1)=1.000019038921006
Tp3( 1)=1.000019038921006

Tp1(10)=1.001907188148492
Tp2(10)=1.001907188143217
Tp3(10)=1.001907188143217

Tp1(20)=1.007669025790702
Tp2(20)=1.007669025791545
Tp3(20)=1.007669025791545

Tp1(30)=1.017408797590877
Tp2(30)=1.017408797595956
Tp3(30)=1.017408797595956

Tp1(40)=1.031340519129575
Tp2(40)=1.031340519130037
Tp3(40)=1.031340519130037

Tp1(50)=1.049782960621919
Tp2(50)=1.049782960623032
Tp3(50)=1.049782960623032

Tp1(60)=1.073182007117023
Tp2(60)=1.073182007149364
Tp3(60)=1.073182007149365

Tp1(70)=1.102144909638314
Tp2(70)=1.102144909639270
Tp3(70)=1.102144909639270

Tp1(80)=1.137492559922196
Tp2(80)=1.137492559923922
Tp3(80)=1.137492559923922

Tp1(90)=1.180340599016056
Tp2(90)=1.180340599016096
Tp3(90)=1.180340599016096