Loading [MathJax]/jax/output/HTML-CSS/jax.js

トップページに戻る

SymPyー代数演算(2)応用:二重振り子

SymPy の応用例:二重振り子

SymPyとNumPyの組み合わせが強力であることを示す例として、二重振り子を数値計算で解くということを試みます。
もちろん普通に解いてもしょうがないので、座標設定からラグランジアン、そして運動方程式をシンボリックに求め、最後に数値計算することにします。

解析力学(Wikipedia) の知識が必要ですが、コメントで補足しながら進めます。

モデル設定

水平にx軸、鉛直上向きにy軸があり、y軸負方向に重力がかかっています。
また、原点から長さr1の棒の先に1つ目の質点がついていて、そこから長さr2の棒で2つ目の質点がつながっているとします。
それぞれの棒がy軸とのなす角をそれぞれθ1,θ2とします。

二つの質点の質量をそれぞれ m1,m2 とします。

ゴールの確認

二重振り子の運動方程式は、最終的には時刻 t についての連立2階線形微分方程式になります。

今回は、極座標表現を用いるので、質点の(角度の)加速度 ¨θ1,¨θ2についての微分方程式が得られれば、数値計算することができます。

¨θ1=f1(θ1,˙θ1,θ2,˙θ2)¨θ2=f2(θ1,˙θ1,θ2,˙θ2)

f1,f2 の具体的な形を求めにいきましょう。

In [1]:
# 準備
import sympy as sy
sy.init_printing()

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

時刻を表す t や、質点ごとの棒の長さを表す r1,r2 、 質量を表す m1,m2 、重力加速度を表す g を定義します。

θ1,θ2t の関数 θ1(t),θ2(t)として扱います。

˙θ1˙θ2θ1,θ2 の時間微分です。

In [2]:
# 変数と関数を定義
t = sy.Symbol("t", real=True)
r1, r2, m1, m2, g = sy.symbols(r"r_1 r_2 m_1 m_2 g", positive=True)
theta1 = sy.Function(r"\theta_1")(t)
theta2 = sy.Function(r"\theta_2")(t)
theta1_dot, theta2_dot = sy.symbols(r"\dot{\theta_1} \dot{\theta_2}")

質点1 は原点を中心とする半径 r1 の円上を動きます。

y 軸とのなす角である θ1 を使うことで、 (x1,y1) 座標は以下のように表せます。

In [3]:
# 質点1の位置と速度
x1 = sy.Matrix([r1 * sy.sin(theta1), - r1 * sy.cos(theta1)])
v1 = x1.diff(t)
x1, v1
Out[3]:
([r1sin(θ1(t))r1cos(θ1(t))], [r1cos(θ1(t))ddtθ1(t)r1sin(θ1(t))ddtθ1(t)])

質点2 は質点1を中心とする半径 r2 の円上を動きます。

y 軸とのなす角である θ2 を使うことで、 (x2,y2) 座標は以下のように表せます。

In [4]:
# 質点2の位置と速度
x2 = x1 + sy.Matrix([r2 * sy.sin(theta2), - r2 * sy.cos(theta2)])
v2 = x2.diff(t)
x2, v2
Out[4]:
([r1sin(θ1(t))+r2sin(θ2(t))r1cos(θ1(t))r2cos(θ2(t))], [r1cos(θ1(t))ddtθ1(t)+r2cos(θ2(t))ddtθ2(t)r1sin(θ1(t))ddtθ1(t)+r2sin(θ2(t))ddtθ2(t)])

運動エネルギーを T、 ポテンシャルエネルギーを V とすると、ラグランジアンは L=TV と表せます。

ラグランジアンを求めるため、運動エネルギーを求めましょう。

高校物理でやるように、運動エネルギーは T=mv22 です。それをそのまま入れると計算できます。

In [5]:
# 運動エネルギー
T1 = (m1 * (v1[0]**2 + v1[1]**2) / 2).simplify()
T2 = (m2 * (v2[0]**2 + v2[1]**2) / 2).simplify()
T1, T2
Out[5]:
(m1r21(ddtθ1(t))22, m2(r21(ddtθ1(t))2+2r1r2cos(θ1(t)θ2(t))ddtθ1(t)ddtθ2(t)+r22(ddtθ2(t))2)2)

つづいて、ポテンシャルエネルギーを求めましょう。

V=mgh です。今、 x1 がベクトルを表し、 y 座標が x1[1] となります(ちょっとややこしいので注意..)。

In [6]:
# ポテンシャル
V1 = m1 * g * x1[1]
V2 = m2 * g * x2[1]
V1, V2
Out[6]:
(gm1r1cos(θ1(t)), gm2(r1cos(θ1(t))r2cos(θ2(t))))

運動エネルギーとポテンシャルが求まったので、ラグランジアン L(θ1,θ2,˙θ1,˙θ2) を得ることができます。

系全体のラグランジアンは、質点1, 質点2 のものを足し合わせることで得ることができます。

つまり、系のラグランジアン L は、 L=L1+L2=(T1V1)+(T2V2)=T1+T2V1V2

ということになります。

In [7]:
L = T1 + T2 - V1 - V2
L
Out[7]:
gm1r1cos(θ1(t))gm2(r1cos(θ1(t))r2cos(θ2(t)))+m1r21(ddtθ1(t))22+m2(r21(ddtθ1(t))2+2r1r2cos(θ1(t)θ2(t))ddtθ1(t)ddtθ2(t)+r22(ddtθ2(t))2)2

だんだん結果は複雑になってきましたが、人間側は相当楽をしています。

ラグランジアンから運動方程式を導出する

なぜラグランジアンを求めたかというと、ラグランジアンを使うと、運動方程式が(形式的には)簡単に求められるからです。

ラグランジアンをある座標表現を表現し、それをラグランジュ方程式に突っ込めば、自動的にその座標系での運動方程式が導出されます。

ddtL˙θ1=Lθ1ddtL˙θ2=Lθ2

ここでは、 (θ1,θ2,˙θ1,˙θ2) という座標系でのラグランジアンということになります。

質点1 に対応する運動方程式は、一つ目のラグランジュ方程式から導出できます。

In [8]:
# 質点1の運動方程式
EOM_1 = sy.Eq(L.diff(theta1.diff(t)).diff(t), L.diff(theta1)) 
EOM_1
Out[8]:
m1r21d2dt2θ1(t)+m2(2r21d2dt2θ1(t)2r1r2(ddtθ1(t)ddtθ2(t))sin(θ1(t)θ2(t))ddtθ2(t)+2r1r2cos(θ1(t)θ2(t))d2dt2θ2(t))2=gm1r1sin(θ1(t))gm2r1sin(θ1(t))m2r1r2sin(θ1(t)θ2(t))ddtθ1(t)ddtθ2(t)

質点2 に対応する運動方程式は、二つ目のラグランジュ方程式から導出できます。

In [9]:
# 質点2の運動方程式
EOM_2 = sy.Eq(L.diff(theta2.diff(t)).diff(t), L.diff(theta2)) 
EOM_2
Out[9]:
m2(2r1r2(ddtθ1(t)ddtθ2(t))sin(θ1(t)θ2(t))ddtθ1(t)+2r1r2cos(θ1(t)θ2(t))d2dt2θ1(t)+2r22d2dt2θ2(t))2=gm2r2sin(θ2(t))+m2r1r2sin(θ1(t)θ2(t))ddtθ1(t)ddtθ2(t)

運動方程式は、連立2階微分方程式となっています。

今回の微分方程式は、線形であり、数値的には簡単に解けるものです。

質点の(角度の)加速度 ¨θ1,¨θ2について解き、数値計算しやすい形に表します。

¨θ1=f1(θ1,˙θ1,θ2,˙θ2)¨θ2=f2(θ1,˙θ1,θ2,˙θ2)
In [10]:
EOMs = sy.solve([EOM_1, EOM_2], (theta1.diff(t, 2), theta2.diff(t, 2)))
EOMs
Out[10]:
{d2dt2θ1(t):gm1sin(θ1(t))m1r1+m2r1cos2(θ1(t)θ2(t))m2r1+gm2sin(θ1(t))m1r1+m2r1cos2(θ1(t)θ2(t))m2r1gm2sin(θ2(t))cos(θ1(t)θ2(t))m1r1+m2r1cos2(θ1(t)θ2(t))m2r1+m2r1sin(θ1(t)θ2(t))cos(θ1(t)θ2(t))(ddtθ1(t))2m1r1+m2r1cos2(θ1(t)θ2(t))m2r1+m2r2sin(θ1(t)θ2(t))(ddtθ2(t))2m1r1+m2r1cos2(θ1(t)θ2(t))m2r1, d2dt2θ2(t):gm1sin(θ1(t))cos(θ1(t)θ2(t))m1r2+m2r2cos2(θ1(t)θ2(t))m2r2+gm1sin(θ2(t))m1r2+m2r2cos2(θ1(t)θ2(t))m2r2gm2sin(θ1(t))cos(θ1(t)θ2(t))m1r2+m2r2cos2(θ1(t)θ2(t))m2r2+gm2sin(θ2(t))m1r2+m2r2cos2(θ1(t)θ2(t))m2r2m1r1sin(θ1(t)θ2(t))(ddtθ1(t))2m1r2+m2r2cos2(θ1(t)θ2(t))m2r2m2r1sin(θ1(t)θ2(t))(ddtθ1(t))2m1r2+m2r2cos2(θ1(t)θ2(t))m2r2m2r2sin(θ1(t)θ2(t))cos(θ1(t)θ2(t))(ddtθ2(t))2m1r2+m2r2cos2(θ1(t)θ2(t))m2r2}
In [11]:
# 関数を変数に置き換える
t1, t2 = sy.symbols(r"\theta_1 \theta_2")
diff2dot = [(theta1.diff(t), theta1_dot),
            (theta2.diff(t), theta2_dot)]
theta1_ddot = EOMs[theta1.diff(t, 2)].subs(diff2dot).subs([(theta1, t1), (theta2, t2)])
theta2_ddot = EOMs[theta2.diff(t, 2)].subs(diff2dot).subs([(theta1, t1), (theta2, t2)])

質点1の微分方程式

¨θ1=f1(θ1,˙θ1,θ2,˙θ2)

は次のような関数となります。

In [12]:
theta1_ddot
Out[12]:
˙θ12m2r1sin(θ1θ2)cos(θ1θ2)m1r1+m2r1cos2(θ1θ2)m2r1+˙θ22m2r2sin(θ1θ2)m1r1+m2r1cos2(θ1θ2)m2r1+gm1sin(θ1)m1r1+m2r1cos2(θ1θ2)m2r1+gm2sin(θ1)m1r1+m2r1cos2(θ1θ2)m2r1gm2sin(θ2)cos(θ1θ2)m1r1+m2r1cos2(θ1θ2)m2r1

質点2の微分方程式

¨θ2=f2(θ1,˙θ1,θ2,˙θ2)

は次のような関数となります。

In [13]:
theta2_ddot
Out[13]:
˙θ12m1r1sin(θ1θ2)m1r2+m2r2cos2(θ1θ2)m2r2˙θ12m2r1sin(θ1θ2)m1r2+m2r2cos2(θ1θ2)m2r2˙θ22m2r2sin(θ1θ2)cos(θ1θ2)m1r2+m2r2cos2(θ1θ2)m2r2gm1sin(θ1)cos(θ1θ2)m1r2+m2r2cos2(θ1θ2)m2r2+gm1sin(θ2)m1r2+m2r2cos2(θ1θ2)m2r2gm2sin(θ1)cos(θ1θ2)m1r2+m2r2cos2(θ1θ2)m2r2+gm2sin(θ2)m1r2+m2r2cos2(θ1θ2)m2r2

t についての微分方程式を数値的に解くため、 f1,f2 を NumPy 関数に変換します。

In [14]:
# NumPyの関数に変換する
args = (t1, t2, theta1_dot, theta2_dot, m1, m2, r1, r2, g)
func1 = sy.lambdify(args, theta1_ddot, "numpy")
func2 = sy.lambdify(args, theta2_ddot, "numpy")

数値計算

scipy の ode ソルバーを使って常微分方程式を解いてみます。

(解析的に解いてから数値計算をしていますが、陰関数のまま数値計算を行い、数値計算時にタイムステップごとに逆行列を求める方法もあると思います。興味ある人は試してみてください)

In [15]:
# 数値計算
from scipy.integrate import ode

def time_evolve(t, y, params):
    theta1, theta2, theta1_dot, theta2_dot = y
    return [theta1_dot, theta2_dot,
            func1(*y, *params),
            func2(*y, *params)]

y0 = [np.pi * 1.01, np.pi * 1.01, 0, 0]
params = {"m1": 1, "m2": 1, "r1": 0.1, "r2": 0.1, "g": 9.8}
solver = ode(time_evolve).set_initial_value(y0, 0).set_f_params(params.values())

dt = 0.01
tmax = 100
results = []
while solver.t < tmax:
    y = solver.integrate(solver.t + dt)
    results.append([solver.t, *y])
In [16]:
import pandas as pd
results = pd.DataFrame(results, columns=["t", "theta1", "theta2", "theta1_dot", "theta2_dot"])
results["x1"] = params["r1"] * np.sin(results.theta1)
results["x2"] = results.x1 + params["r2"] * np.sin(results.theta2)
results["y1"] = -params["r1"] * np.cos(results.theta1)
results["y2"] = results.y1 - params["r2"] * np.cos(results.theta2)
In [17]:
import matplotlib.animation as animation
import matplotlib.patches as patches

def gen():
    for i, vals in results[["t", "x1", "x2", "y1", "y2"]].iterrows():
        yield vals.values

def plot_double_pendulum(data):
    t, x1, x2, y1, y2 = data
    ax.cla()
    R = params["r1"] + params["r2"]
    ax.set_xlim(-R, R)
    ax.set_ylim(-R, R)
    ax.scatter([x1, x2], [y1, y2])
    ax.add_patch(patches.Arrow(0, 0, x1, y1, width=0.01))
    ax.add_patch(patches.Arrow(x1, y1, (x2-x1), (y2-y1), width=0.01))
    ax.set_aspect("equal")
fig, ax = plt.subplots()
ani = animation.FuncAnimation(fig, plot_double_pendulum, gen, interval=30, save_count=500)
# ani.save("double_pendulum.gif", writer="imagemagick", dpi=100)
# この辺は、 Jupyter Notebook でやるより、スクリプトで実行したほうが扱いやすい気がする... のでご注意
plt.show()

結果

いい感じに二重振り子のアニメーションが作れました。(長めバージョン)

まとめると

一つのセルにまとめると以下のように書けます。

In [18]:
# 変数と関数を定義
t = sy.Symbol("t", real=True)
r1, r2, m1, m2, g = sy.symbols(r"r_1 r_2 m_1 m_2 g", positive=True)
# theta1, theta2 = sy.symbols(r"\theta_1 \theta_2", real=True)
theta1 = sy.Function(r"\theta_1")(t)
theta2 = sy.Function(r"\theta_2")(t)
theta1_dot, theta2_dot = sy.symbols(r"\dot{\theta_1} \dot{\theta_2}")

# 質点1の位置と速度
x1 = sy.Matrix([r1 * sy.sin(theta1), - r1 * sy.cos(theta1)])
v1 = x1.diff(t)

# 質点2の位置と速度
x2 = x1 + sy.Matrix([r2 * sy.sin(theta2), - r2 * sy.cos(theta2)])
v2 = x2.diff(t)

# 運動エネルギー
T1 = (m1 * (v1[0]**2 + v1[1]**2) / 2).simplify()
T2 = (m2 * (v2[0]**2 + v2[1]**2) / 2).simplify()

# ポテンシャル
V1 = m1 * g * x1[1]
V2 = m2 * g * x2[1]

# ラグランジアン
L = T1 + T2 - V1 - V2

# 質点1、2の運動方程式
EOM_1 = sy.Eq(L.diff(theta1.diff(t)).diff(t), L.diff(theta1)) 
EOM_2 = sy.Eq(L.diff(theta2.diff(t)).diff(t), L.diff(theta2)) 

# 加速度について解く
EOMs = sy.solve([EOM_1, EOM_2], (theta1.diff(t, 2), theta2.diff(t, 2)))

# 関数を変数に変換
t1, t2 = sy.symbols(r"\theta_1 \theta_2")
diff2dot = [(theta1.diff(t), theta1_dot),
            (theta2.diff(t), theta2_dot)]
theta1_ddot = EOMs[theta1.diff(t, 2)].subs(diff2dot).subs([(theta1, t1), (theta2, t2)])
theta2_ddot = EOMs[theta2.diff(t, 2)].subs(diff2dot).subs([(theta1, t1), (theta2, t2)])

# NumPy関数化
args = (t1, t2, theta1_dot, theta2_dot, m1, m2, r1, r2, g)
func1 = sy.lambdify(args, theta1_ddot, "numpy")
func2 = sy.lambdify(args, theta2_ddot, "numpy")

# 数値計算
from scipy.integrate import ode

def time_evolve(t, y, params):
    theta1, theta2, theta1_dot, theta2_dot = y
    return [theta1_dot, theta2_dot,
            func1(*y, *params),
            func2(*y, *params)]

# 初期値
y0 = [np.pi * 1.01, np.pi * 1.01, 0, 0]
params = {"m1": 1, "m2": 1, "r1": 0.1, "r2": 0.1, "g": 9.8}
solver = ode(time_evolve).set_initial_value(y0, 0).set_f_params(params.values())

dt = 0.1
tmax = 50
results = []
while solver.t < tmax:
    y = solver.integrate(solver.t + dt)
    results.append([solver.t, *y])

import pandas as pd
results = pd.DataFrame(results, columns=["t", "theta1", "theta2", "theta1_dot", "theta2_dot"])
results["x1"] = params["r1"] * np.sin(results.theta1)
results["x2"] = results.x1 + params["r2"] * np.sin(results.theta2)
results["y1"] = -params["r1"] * np.cos(results.theta1)
results["y2"] = results.y1 - params["r2"] * np.cos(results.theta2)

基礎編

応用編