Pythonによる台車形倒立振子のシミュレーション
はじめに
P計画
車輪形倒立振子を作る──この目標に対して問題を分解しながらアタックするため,前回にて床を転がる車輪のシミュレーションとアニメーションをPython (+ NumPy, SciPy, Matplotlib)で実現した経緯をまとめました。次は振り子をシミュレーションしようと思っていましたが,勢い余って(?)振り子だけでなく台車も含めた台車形倒立振子まで進むことができたので,今回はその様子について記述していきます*1。
床を転がる車輪とは異なり,台車形倒立振子では運動方程式がもろに非線形になります。本記事では非線形のままの運動方程式を表現するPythonの関数を定義し,SciPyのsolve_ivp関数にて初期値問題を解きます。また,これも前回同様にMatplotlibにて見て楽しい(!)アニメーションを生成します。運動方程式を整理する際にSymPyも用いています*2。
完成イメージ
図1に完成したアニメーションを示します。20秒間の現象をシミュレーションしております。振り子が180°,つまり真下を向いた状態から(テキトーな力を台車に加えた)振り上げ制御にて振り子を上向きに立てた後に,時刻t = 2 sにて状態フィードバック制御に入ります。
その後,時刻t = 7.5 sから0.5秒間,振り子の先端に外乱を加えていますが,状態フィードバックのおかげで転倒することなく倒立し続けていることが分かります。最後に時刻t = 12.5 s以降で制御を止めて無制御状態に移行すると,振り子は倒立できなくなって下向きにブラブラと減衰振動している様子が見て取れます。このアニメーションを生成するJupyter NotebookをGitHubに置きましたので適宜ご参照下さい。
アニメーションの作り方については前回とほぼ同様で,舞台を作ってから台車,振り子,力を表す矢印,時刻などの文字列のアーティストの属性(プロパティ)を書き換えながらフレームを生成し,HTML5のmp4動画やGIFアニメーションを生成しています。
目次
- はじめに
- 目次
- 運動方程式を立てる
- シミュレーションできる形に運動方程式を変形 ~ SymPy活用 ~
- SciPyでの初期値問題の求解 ~自律系の場合~
- 状態フィードバックの初期値応答と外乱応答
- まとめ
- 付録:アニメーションの作り方
運動方程式を立てる
台車形倒立振子の運動方程式については幾多の先賢たちが既に述べているため,ここでは特に新しいことは書いていません。例えばY-engineerさんのQiita記事を参考にさせて頂いております。
以下は筆者としての理解の確認を目的にしています。
考える系
図2に,今回考える台車形倒立振子の全体図を示します。水平方向をx軸,垂直方向をz軸とします。今は考えませんが奥行方向がy軸ですね。なお,図2では台車を長方形で描いていますが,本記事では力学的に質点として扱います。そのため,台車のz座標は零で,振り子を取り付けたピボットのz座標も零とします(後述のアニメーションを作る際には,台車が適当な大きさをもつものとして描きます)。
質量Mの台車の重心*3に質量m,長さ2lの振り子が取り付けられています。振り子の重心はちょうど真ん中,ピボットからlの距離にあるとします。台車のx座標をxc,振り子の重心のx座標をxp,同z座標をzpとし,振り子の鉛直上向きからの角度をθとします。
振り子を倒立させ続けるため,台車には制御力fを加えます。また,外乱として振り子の先端には外乱力fdが加わるものとします。さらに,台車には速度に比例した摩擦力μc dxc / dt,振り子には角速度に比例した摩擦力μp dθ / dtが生じるものとします。
台車と振り子に働く力
台車と振り子にはそれぞれ系の外部からfやfdが働くだけでなく,お互いの間にも力が働きます。台車が加減速する場合には当然ながら振り子にも力が働くはずです。振り子の方も重力で台車を下向きに押すだけはなさそうです。振り子が鉛直上向きから倒れようとする場合を想像すると,振り子はピボットを介して倒れこむ方向とは反対の水平方向に台車を押しそうです。これらの力が台車と振り子のダイナミクスを決めることになります。図3に台車と振り子のそれぞれに働く力を図示しました。
台車と振り子の間に働く力の水平成分をひとまずH,鉛直成分をVとします。このHとVは今のところ具体的に書き下せませんが,実は後から消去できます*4。
図3より,台車の並進運動,振り子の水平・鉛直方向の並進運動,振り子の回転運動に関する4つの運動方程式を記述できます*5。
ここで,振り子が台車から離れない,つまり,振り子の重心の座標(xp, zp)に拘束条件があることを利用すると,台車と振り子の間に働く力HとVを消去できます。まず,振り子の重心のx座標xpは,台車のx座標xcと振り子の角度θでも表現できます。
また,z座標についても
ですね。
(1) ~ (4)式から未知数であるHとVを消去するために,(5),(6)式を時間tで2階微分します。これによって,HとVを消去するのみならず,系をxcとその時間微分,θとその時間微分だけで表現できるようにします。まず,(5)式をtで微分します。
さらに,もう一度微分すると,
となります。ここで,数学弱者たる筆者は(7)式右辺第2項につまづきそうになりました。
だと…? でないの? は何処から来た…?
しかし,今はθではなくtで微分していること,そして,θも時間tの関数θ(t)であることを思い起こせば,高校数学で出会った合成関数の微分公式
を適用していると理解できました。もう一度微分している(8)式右辺第2項と第3項はそれに加えて積の微分公式
を適用していますね*6。同様に,(6)式もtで微分していくと,
さらに,
を得ます。
台車の位置xcと振り子の角度θのみで系を表現する
(2)式をHについて解き,さらにに(8)式を代入すると,
となります。同様に,(3)式をVについて解き,さらにに(12)式を代入すると,
を得ます。
(13)式を(1)式に代入して整理すると*7
同様に,(13),(14)式を(4)式に代入して整理*8すると,
を得ます。以上で台車の位置xcと振り子の角度θのみで表現した台車形倒立振子の運動方程式を導出できました。
シミュレーションできる形に運動方程式を変形 ~ SymPy活用 ~
SciPyのsolve_ivp関数で解ける形
以上のように(15),(16)式の運動方程式を導出できましたが,このままではSciPyのsolve_ivp関数で解くことができません…! どうしてか…? そう,高次の微分を低次の微分で表す形になっていないからです。(15),(16)式をとの連立方程式だとみなして解き,
という形,もっと言えば,状態変数を
と定義して,
という形にまとめなければなりません。前回述べたように,(20)式を表現するPythonの関数を定義できればSciPyのsolve_ivp関数にて初期値問題を解くことができます。
SymPyで連立方程式を解く
(15),(16)式はとの単純な連立1次方程式ですが,数学弱者である筆者は既に心が折れたのでSymPyに頼ることにしました。早速,SymPyをインポートします。
import sympy as sp from sympy.physics.mechanics import dynamicsymbols, init_vprinting init_vprinting()
ここで,dynamicsymbolsは時間tの関数として簡単にシンボルを定義できるようにするものです。またinit_vprinting関数を実行しておくと,時間tの関数とその時間微分について,例えばではなくのようにJupyterLab上で表示してくれます。
早速,シンボルを定義しましょう。
# Symbols for cart x_c = dynamicsymbols('x_c') M, mu_c = sp.symbols('M mu_c') # Symbols for pendulum theta = dynamicsymbols('theta') m, l, J, mu_p = sp.symbols('m l J mu_p') # Symbol for external force f = sp.symbols('f') # Symbol for disturbance at tip of pendulum f_d = sp.symbols('f_d') # Symbol for gravity acceleration g = sp.symbols('g') # Time t = sp.symbols('t')
ここで,dynamicsymbolsを用いて定義したシンボルはデフォルトで時間tの関数となります。次に,(15),(16)式をSymPyで定義します。ここではeq1,eq2と名付けました。
# Define system of equations eq1 = sp.Eq((M + m) * x_c.diff(t, 2) + m * l * theta.diff(t, 2) * sp.cos(theta), \ -mu_c * x_c.diff(t) + m * l * theta.diff(t)**2 * sp.sin(theta) + f) eq2 = sp.Eq(m * l * x_c.diff(t, 2) * sp.cos(theta) + (J + m * l**2) * theta.diff(t, 2), \ -mu_p * theta.diff(t) + m * g * l * sp.sin(theta) + f_d * sp.cos(theta))
SymPyよ,今こそ解くのだ…!
# Solve system of equations sol = sp.solve([eq1, eq2], [x_c.diff(t, 2), theta.diff(t, 2)])
変数solに解を格納しておきます。JupyterLabで以上を実行し,解solを表示させてみると,図4のような結果を得ます。
これはまた随分と複雑になります。やはり手計算ではなかなか厄介です。解を1つずつ取り出し,とりあえずsimplifyしてみます。
A = sol[x_c.diff(t, 2)] B = sol[theta.diff(t, 2)] display(sp.simplify(A)) display(sp.simplify(B))
JupyterLab上ではこのdisplay関数でベースの数式を表示してくれます。結果として,
および
を得ます。ただし,
です。これは運動方程式というよりは非線形な状態方程式と呼んだ方が良さそうですね。(22),(23)式の各項について物理的な意味を具体的に読み解くのは筆者には難しいため,ごく一部を除いて特定の因子で括り出すなどはできませんでした。ひとまず(22),(23)式のままPythonの関数として実装し,SciPyで求解していきます。
SciPyでの初期値問題の求解 ~自律系の場合~
以上で運動方程式を非線形な状態方程式に整理しました。これをPythonの関数として実装し,SciPyのsolve_ivp関数で解いていきます。ここではひとまず制御力fも外乱力fdもない場合を想定します。用語が適切か分かりませんが,非線形力学系で言うところの「自律系」というものに相当すると筆者は理解しました。当然ですが,振り子は倒立状態を保てないため,最終的には真下(θ = 180°)に向かってブラブラと減衰振動することになりますね。
以下では
import numpy as np from scipy.integrate import solve_ivp from scipy.constants import g from IPython.display import HTML
のように各ライブラリ・モジュールをインポートしているものとします。
非線形状態方程式をPythonの関数として定義する
(22),(23)式をsolve_ivp関数で使える形にPythonの関数として実装します。
# Differential equation # State x = [x_c, x_c_dot, theta, theta_dot]^T def func(t, x): # Extract variables x_c, x_c_dot, theta, theta_dot = x # External force applied to cart as control input f = 0 # External force applied at tip of pendulum as disturbance f_d = 0 # Calculate x_ddot and theta_ddot denom = J * (M + m) + M * m * l**2 + (m * l * np.sin(theta))**2 x_c_ddot = (J * f + J * m * l * theta_dot**2 * np.sin(theta) \ - J * mu_c * x_c_dot + m * l**2 * f \ - m * l * f_d * (np.cos(theta))**2 \ - m**2 * l**2 * g * np.sin(theta) * np.cos(theta) \ + m**2 * l**3 * theta_dot**2 * np.sin(theta) \ - m * l**2 * mu_c * x_c_dot \ + m * l * mu_p * theta_dot * np.cos(theta)) / denom theta_ddot = (M * f_d * np.cos(theta) \ + M * m * g * l * np.sin(theta) - M * mu_p * theta_dot \ - m * l * f * np.cos(theta) + m * f_d * np.cos(theta) \ + m**2 * l * g * np.sin(theta) \ - m**2 * l**2 * theta_dot**2 * np.sin(theta) * np.cos(theta) \ + m * l * mu_c * x_c_dot * np.cos(theta) \ - m * mu_p * theta_dot) / denom # Prepare empty array to store derivatives dxdt = np.zeros_like(x) # Differential equation dxdt[0] = x[1] dxdt[1] = x_c_ddot dxdt[2] = x[3] dxdt[3] = theta_ddot return dxdt
前回の車輪の場合に比較してかなり複雑になりました。言及し忘れていましたが,
です。上記Pythonプログラムの36,38行目はそれぞれ(24),(25)式に相当します。
初期値問題を解く
例として台車の質量M = 1 kg,振り子の質量m = 0.75 kg,振り子の長さ2l = 60 cm,台車と床面の摩擦に関する減衰係数μc = 0.05 N/(m/s),振り子の摩擦に関する減衰係数μp = 0.05 N/(rad/s)とします。振り子を一様な細い棒と考えると,振り子の重心の周りの慣性モーメントJ = ml2 / 3となります。
# Parameters M = 1.00 # Mass of cart m = 0.75 # Mass of pendulum, kg l = 0.30 # Half length, m J = m * l**2 / 3 # Inertia around center of mass, kg m**2 mu_c = 0.050 # Friction of cart, N/(m/s) mu_p = 0.050 # Friction of pendulum, Nm/(rad/s)
さらに初期条件として,時刻t = 0 sでの台車の位置がxc(0) = xc0 = 0 m,同じく振り子の角度がでθ(0) = θ0 = 1°であった場合*9における初期値問題をいよいよ解いてみましょう*10。
# Solve initial value problem t_end = 15 dt = 0.01 t = np.arange(0, t_end, dt) x_c0 = 0 theta_0 = 1 x0 = [x_c0, 0, np.radians(theta_0), 0] sol = solve_ivp(func, [0, t_end], x0, t_eval = t)
solve_ivp関数の使い方に関しては前回をご覧下さい。ここでは15秒間の現象をシミュレーションし,結果を0.01秒毎に評価することとしました。解を変数solに格納します。早速,解として得られた各状態変数をプロットしてみましょう。ここでは
import matplotlib.pyplot as plt import matplotlib.patches as patches import matplotlib.animation as animation from IPython.display import HTML
をインポートしておくこととします。
図5に結果の時間波形を示します。プロットの際には分かりやすくするため,台車の速度を,振り子の角速度をとして記載しています*11。図5より,最初は倒立状態に近かった振り子がブラブラと下向きに減衰振動している様子が読み取れます(ただしθを−180° ~ 180°でプロットしているため,真下である180°と−180°を跨ぐ際にグラフが不連続に見えて,まるで矩形波のようになっています)。また,台車の方も振り子に振られて左右に10 cmあまり動いていることが分かりますね。
アニメーションにしてみると,図6のようになります。
アニメーションにすると動きのイメージが格段にクリアになりますね。
状態フィードバックの初期値応答と外乱応答
台車の初期位置xc0 = 1 mの場合の初期値応答
ここまで来れば倒立させたいというのが人情です。きちんとした制御設計をまったく経ていませんが,状態フィードバックを組んで試行錯誤しているうちに,うまく倒立するゲインを見つけることができました。まず,現在の状態と時刻tから制御力fを求める関数を定義します。
# Force controller # State x = [x_c, x_c_dot, theta, theta_dot]^T def force_control(t, x): # Clip angle (theta) from -180 to 180 degree _x = x.copy() np.put(_x, 2, (x[2] + np.pi) % (2 * np.pi) - np.pi) # State feedback gain and offset F = np.array([20, 30, 150, 30]) f_max = 20 f = np.clip(np.dot(F, _x), -f_max, f_max) return f
5,6行目で角度を−180° ~ 180°にクリップしています。9行目にて状態フィードバックゲインを
と定義しています。この状態フィードバックゲインでは,角度θについては0°,つまり鉛直上向きに戻すような力を生じますが,台車の位置xcだけに着目すると,台車が原点x = 0から少しでも離れると,ますます離れるような不安定なゲインになっています。このような組み合わせで全体としては安定な系になっているようです。線形化してから(フィードバックありの系の)安定性や固有値を見てみたいと思っています。
次の行では制御力の最大値を20 Nに制限しています。無限の力を出せない場合という現実的なシミュレーションにするため,このような最大値をお試しで設定してみました。12行目が状態フィードバックの本体であり,状態フィードバックゲインと状態の内積を計算して制御力fを求め,returnしています。ただし,NumPyのclip関数を使って最大値を±20 Nに制限しています。
ここで,前節で定義した台車形倒立振子の非線形状態方程式を表すfunc関数のうち,制御力fを計算する部分である8行目を書き換えます。
# Differential equation # State x = [x_c, x_c_dot, theta, theta_dot]^T def func(t, x): # Extract variables x_c, x_c_dot, theta, theta_dot = x # External force applied to cart as control input f = force_control(t, x) # External force applied at tip of pendulum as disturbance f_d = 0 # Calculate x_ddot and theta_ddot denom = J * (M + m) + M * m * l**2 + (m * l * np.sin(theta))**2 (以下略)
台車の初期位置xc0 = 1 mとして初期値問題を解いてみましょう。ここでは振り子の初期角度θ0 = 0°とします。
# Solve t_end = 15 dt = 0.01 t = np.arange(0, t_end, dt) x_c0 = 1 theta_0 = 0 x0 = [x_c0, 0, np.radians(theta_0), 0] sol = solve_ivp(func, [0, t_end], x0, t_eval = t)
時間波形は図7のようになります。 おお…倒立している…ように読み取れます…! アニメーションでは図8のようになります。 まず右方向に振り出してから左方向に戻りますね。これは,箒などを手で立てながら移動する場合の挙動と同じだと思います。
振り子の先端に1 Nの力を0.5秒間加えた場合
今度は外乱応答を試してみましょう。時刻t = 8 sから8.5 sまでの0.5秒間にわたって,振り子の先端に右向きに1 Nの力を加えた場合を考えます。倒立している振り子を指でつっつくようなイメージですね。外乱力fdを与える関数を定義します。
# Disturbance generator # State x = [x_c, x_c_dot, theta, theta_dot]^T def force_disturbance(t, x): if 8 <= t < 8.5: f_d = 1 else: f_d = 0 return f_d
同じように,非線形状態方程式を表現した関数からこの関数を呼び出すように,以下のように11行目を書き換えます。
# Differential equation # State x = [x_c, x_c_dot, theta, theta_dot]^T def func(t, x): # Extract variables x_c, x_c_dot, theta, theta_dot = x # External force applied to cart as control input f = force_control(t, x) # External force applied at tip of pendulum as disturbance f_d = force_disturbance(t, x) # Calculate x_ddot and theta_ddot denom = J * (M + m) + M * m * l**2 + (m * l * np.sin(theta))**2 (以下略)
外乱力があまり強いと振り子は倒れ,状態フィードバック制御の結果として台車ごとx座標のはるか遠くまで行ってしまいますが,ある程度弱い外乱であれば,状態フィードバックの効果によって倒立を維持できます。同じようにsolve_ivp関数で解いてみます。 時刻t = 8 sから8.5 sで台車が右方向に移動し,倒れそうになる振り子を元に戻そうとしています。時刻t = 8.5 s以降で外乱力が零になると,再びxc = 0 m,θ = 0°に向かって収束していく様子が見て取れます。
これもアニメーションにしてみると図10のようになります。 なかなか倒立振子らしくなって参りました。
振り上げ制御
振り子が真下を向いた状態から台車をぐわんぐわんと動かして,振り子を振り上げることができます。これを振り上げ制御といい,様々な方式が検討されているようです。しかし,筆者は不勉強であるためそれらについても知識がまったくありません(これから勉強するかも)。そこで,台車と振り子の動きを頭の中で想像してみると,まず,ひとつの方向に台車を押すと振り子がその逆方向に振り上がり,次に,その反対方向に台車を押してあげると,振り子が真下を経由して反対方向(最初に台車を押した方向)に振り上がって,勢いが十分であれば真上まで振り上がるのではないかと考えました。制御力fを与える関数には引数として時刻tも与えているので,時刻tの値を見ながら台車を押す方向や強さなどをトライ & エラーで探りました。その結果,次のような実装で振り上げることができました。
# Force controller # State x = [x_c, x_c_dot, theta, theta_dot]^T def force_control(t, x): # Clip angle (theta) from -180 to 180 degree _x = x.copy() np.put(_x, 2, (x[2] + np.pi) % (2 * np.pi) - np.pi) # Try swing up! if t < 1: # Standstill f = 0 elif 1 <= t < 1.3: f = -20 elif 1.3 <= t < 2: f = 12 else: # Normal state feedback control f = np.clip(np.dot(F, _x - x_ofs), -f_max, f_max) return f
まず,時刻t = 1 s以前は制御力f = 0 Nとして待機します(振り上げ開始を分かり易くするため)。時刻t = 1 sから1.3 sまでは−20 Nの力を台車に加えて左方向に押します。すると振り子は右側にある程度振り上がります。時刻t = 1.3 sから2 sまでは逆に台車を右方向に12 Nの力で押します。すると振り子はほぼ鉛直上向きに立ち上がり,時刻t = 2 s以降は通常の状態フィードバック制御に移行します。
台車の初期位置xc0 = 0 m,振り子の初期角度θ0 = 180°としてSciPyのsolve_ivp関数にて初期値問題を解けば,真下を向いた振り子を振り上げ,その後は倒立を維持していることが見て取れます。時間波形は図11のようになります。 アニメーションを生成すると,図12のようになります。
この振り上げ制御と状態フィードバック,外乱応答,無制御の集大成が冒頭の図1となります。
まとめ
以上で台車形倒立振子の運動方程式の導出から,一部SymPyを活用した非線形状態方程式への変形,SciPyを用いたシミュレーションについて述べました。今回はシミュレータを作るのみで,線形化や極配置法や最適レギュレータといった制御設計については触れておりません。それらは筆者が(再)勉強して理解し,この台車形倒立振子に適用できた時点で改めて記事にしようと思います。また,やはり今回もSciPyで解きましたが,Python Control System Libraryで取り扱った方が線形化などの取り回しが良いかもしれませんので,引き続き検討して参ります。
また,今回は状態変数をすべて観測できるという前提で状態フィードバック制御の例を記載しましたが,実際,加速度センサやジャイロセンサでは位置や角度を直接検知することはできません。さらに,センサを振り子に相当する機構のどの位置に取り付けるかによって,得られる信号も違ってくるはずですが,そのあたりの検討はまだ進んでいません。これに対応するにはオブザーバを構築する必要があります。台車形,あるいは当初の目標である車輪形倒立振子を実際に組み立てていく際には,この辺りの理解がクリティカルになるのではと考えています。次回は,線形化して様々なC行列について可観測性を確認し,オブザーバを作ってみたいと考えています。
または,軽い息抜きの記事も書いてみたいですね。
付録:アニメーションの作り方
アニメーションの基本的な作り方は前回と同様ですが,ご参考までにPythonの実装を書いておきます。まずは,舞台と時間波形(台車の速度,振り子の角速度,外力)をこのように準備します。アニメーション中で動かしたいアーティスト(Matplotlibにおける図形の呼び方)についてはそれぞれを描く関数の戻り値を変数に格納しています。
# Animate # Animation setting w_cart = 0.30 # Width of cart, m h_cart = 0.15 # Height of cart, m K_tscale = 3 # Size of theatre # Plot theatre plt.rcParams["font.size"] = 11 fig, ax = plt.subplots(4, 1, figsize = (9, 9), gridspec_kw={'height_ratios': [3, 1, 1, 1]}) fig.patch.set_facecolor('lavender') ax[0].set_aspect('equal') fig.suptitle(f'Inverted pendulum on a cart, Non-linear dynamics, by SciPy & Matplotlib\nCart: $M = {M}$ kg, $\mu_c = {mu_c}$ N/(m/s), $x_0 = {x_c0}$ m\nPendulum:$m = {m}$ kg, $2l = {2 * l}$ m, $\mu_p = {mu_p}$ Nm/(rad/s), $\\theta_0 = {theta_0}\degree$') # Set axis for animation ax[0].set_xlim(-K_tscale, K_tscale) ax[0].set_xlabel('x [m]') ax[0].set_ylim(-K_tscale / 5, K_tscale / 2) ax[0].set_ylabel('z [m]') ax[0].grid() ax[0].set_axisbelow(True) # Show state feedback gain ax[0].text(-K_tscale + 0.1, -K_tscale / 20, '$x = [x_{c}, \\dot{x}_{c}, \\theta, \\dot{\\theta}]^T$', va = 'center') ax[0].text(-K_tscale + 0.1, -2 * K_tscale / 20, f'$F = ${F}', va = 'center') # Show time text_time = ax[0].text(-K_tscale + 0.1, -3 * K_tscale / 20, f'$t = {t[0]:.2f} s$', va = 'center') # Draw floor ax[0].plot([-10, 10], [0, 0], lw = 2, color = 'black') # Draw cart cart = patches.Rectangle(xy = (x_c[0] - w_cart / 2, 0), width = w_cart, height = h_cart, lw = 1.5, ec = 'blue', fc = 'lightblue') ax[0].add_patch(cart) # Draw pendulum pendulum, = ax[0].plot([x_c[0], x_c[0] + 2 * l * np.sin(theta[0])],[h_cart / 2, h_cart / 2 + 2 * l * np.cos(theta[0])], \ lw = 3, color = 'blue') # Show angle of pendulum over it text_angle = ax[0].text(x_c[0] + 2 * l * 1.1 * np.sin(theta[0]), h_cart / 2 + 2 * l * 1.1 * np.cos(theta[0]), \ f'${np.degrees(theta[0]):.3f}\degree$', ha = 'center', va = 'bottom') # Show control and disturbance forces as arrows K_fscale = K_tscale / 40 arrow_cont = ax[0].arrow(x_c[0] - w_cart / 2 * np.sign(f_cont[0]) - K_fscale * f_cont[0], \ h_cart / 2, K_fscale * f_cont[0], 0, length_includes_head = True, width = K_tscale * 0.01, color = 'blue', alpha = 0.6) arrow_disturb = ax[0].arrow(x_c[0] + 2 * l * np.sin(theta[0]) - K_fscale * f_disturb[0], h_cart / 2 + 2 * l * np.cos(theta[0]), \ K_fscale * f_disturb[0], 0, length_includes_head = True, width = K_tscale * 0.01, color = 'orange', alpha = 0.6) # Time-domain plots # Plot speed of cart ax[1].plot(t, v_c) line_time_v_c, = ax[1].plot([t[0], t[0]], [np.min(v_c), np.max(v_c)], lw = 1, c = 'red') ax[1].set_xlabel('Time [s]') ax[1].set_ylabel('Speed of\ncart, $x_{c}$ [m/s]') ax[1].grid() # Plot angular speed of pendulum ax[2].plot(t, omega) line_time_omega, = ax[2].plot([t[0], t[0]], [np.min(omega), np.max(omega)], lw = 1, c = 'red') ax[2].set_xlabel('Time [s]') ax[2].set_ylabel('Angular speed of\npendulum, $\\omega$ [rad/s]') ax[2].grid() # Plot forces ax[3].plot(t, np.array([f_cont, f_disturb]).T, label = ['Control', 'Disturbance']) line_time_force, = ax[3].plot([t[0], t[0]], [np.min(f_cont), np.max(f_cont)], lw = 1, c = 'red') ax[3].set_xlabel('Time [s]') ax[3].set_ylabel('Force, $f$, $f_{d}$ [N]') ax[3].legend(loc = 'upper right') ax[3].grid() fig.tight_layout()
フレーム毎にアップデートする部分を取り扱う関数を定義します。
# Function to update frame def animate(i): # Update cart position cart.set_x(x_c[i] - w_cart / 2) # Update pendulum attitude pendulum.set_data([x_c[i], x_c[i] + 2 * l * np.sin(theta[i])],[h_cart / 2, h_cart / 2 + 2 * l * np.cos(theta[i])]) # Update pendulum angle text text_angle.set_position((x_c[i] + 2 * l * 1.1 * np.sin(theta[i]), h_cart / 2 + 2 * l * 1.1 * np.cos(theta[i]))) text_angle.set_text(f'${np.degrees(theta[i]):.3f}\degree$') # Update time text text_time.set_text(f'$t = {t[i]:.2f}$ s') # Update force arrow kwargs = {'x': x_c[i] - w_cart / 2 * np.sign(f_cont[i]) - K_fscale * f_cont[i], 'dx': K_fscale * f_cont[i]} arrow_cont.set_data(**kwargs) kwargs = {'x': x_c[i] + 2 * l * np.sin(theta[i]) - K_fscale * f_disturb[i], 'y': h_cart / 2 + 2 * l * np.cos(theta[i]), 'dx': K_fscale * f_disturb[i]} arrow_disturb.set_data(**kwargs) # Update time lines line_time_v_c.set_data([t[i], t[i]], [np.min(v_c), np.max(v_c)]) line_time_omega.set_data([t[i], t[i]], [np.min(omega), np.max(omega)]) line_time_force.set_data([t[i], t[i]], [np.min(f_cont), np.max(f_cont)]) return (cart, pendulum, text_angle, text_time, line_time_v_c, line_time_omega, line_time_force)
最後にアニメーションを定義して保存します。FuncAnimationでは前回同様にリアルタイム(シミュレーション内での1秒がアニメーションの表示でも1秒)となるようにしました。
# Create animation ani = animation.FuncAnimation(fig, animate, \ frames = np.arange(0, len(t), 5), interval = dt * 5 * 1000, blit = True) # Show animation HTML(ani.to_html5_video()) # Save animation GIF file ani.save('inverted-pendulum.gif')
FuncAnimation関数を用いて定義したアニメーションaniから,HTML5のmp4動画としてもGIFアニメとしても保存できます。本ブログではGIFアニメを貼り付けています。
*1:車輪と振り子の間でトルクが分配されることから,台車形倒立振子よりも車輪形倒立振子の方が複雑で難易度が高いのではないかと筆者は思っています。
*2:SymPyにはラグランジュ方程式を取り扱う機能もあるようで,SymPyのみで運動方程式を導くこともできるようです。いずれかの時点で挑戦してみます。
*3:本記事では台車を質点として扱っています。台車が回転しないという条件下では,必ずしも重心でなくても良さそうです。「台車」と呼びながら車輪はありませんね…。
*4:この考え方がひとつのコツではないかと筆者は幼稚ながらに考えています。
*5:台車は鉛直方向には動かないものとします。
*6:本当に数学弱者はこんなところで立ち止まってしまいます。
*7:魔法のワード!
*8:さらに魔法のワード!
*9:正確にθ0 = 0°にしてしまうと計算上は倒れません。
*10:すみません。台車の速度と振り子の角速度も時刻t = 0 sでは零とします。
*11:やっぱり速度がv,角速度がωの方が分かり易いです。