強制振動の微分方程式と理論解
対象の微分方程式
質量
mのおもりが付いているバネ(バネ定数
k)が水平に置かれており一端が固定された状態を考える。
バネの左端を原点
x=0、バネが自然長の時のおもりの位置を
x=xeとし、
x=x0の位置でおもりから手を離す。
その後、バネの左端には
acosωtの振動(
a:振幅、
ω:角振動数)を強制的に与え続ける。
任意の時刻
tで自然長の位置が元の
xeから
acosωtだけずれていると考えれば、この時のバネの伸びは
x−(xe+acosωt)になる。
さらに、おもりには速度に比例する抵抗力が速度と逆向きに働くものとし、その抵抗係数を
b(正の値)とする。
それ以外の摩擦力などは全無視。この時の運動方程式は
mx¨=−k(x−xe−acosωt)−bx˙
となるので、解析対象の微分方程式は下記になる。
x¨+mbx˙+mk(x−xe)=mkacosωt
ただし、
xの時間微分を
x˙,x¨で表している。
これをもう少し扱いやすくするために
X=x−xe、
ω0=mk、
γ=2mb、
ka=Fとそれぞれ置き換えると
X¨+2γX˙+ω02X=mFcosωt⋯(1)
となるのでこの(1)を解くことになる。
しかし、(1)は
mFcosωtという非同次(非斉次)項、つまり
Xやその微分に関わらない項が入っている非同次(非斉次)線形微分方程式と言われるもので、単振動のような同次線形微分方程式とは異なる解き方が必要になる。
非同次線形微分方程式の一般解は、その方程式の非同次項を無視した同次線形微分方程式の一般解と、非同次線形微分方程式を満たす特殊解を何かしら1つ見つけてそれらを重ね合わせたものになる。
つまり、今回の場合で言えば(1)の右辺を無視した同次線形微分方程式
X¨+2γX˙+ω02X=0⋯(2)
の一般解
Xhと、(1)を満たす特殊解
Xsを見つけ、それらを合わせた
Xh+Xsが微分方程式(1)の一般解になる。
同次線形微分方程式の一般解
まずは同次線形微分方程式(2)の一般解
Xhを求めよう。
この形の微分方程式を解く常套手段で、
Xh=eλtになるだろうと仮定して(2)に代入すると
λ2eλt+2γλeλt+ω02eλt=0
eλt=0なので
λ2+2γλ+ω02=0
これは
λに関する二次方程式であるから解の公式で
λを求められる。
最初に
γ=2mbとわざわざ1/2を付けて置き換えたのは
λをちょっとだけ見やすくするためです。
λ=−γ±γ2−ω02
ここで注意しなければならないのがルートの中身が負、0、正のどれになるかで場合分けが必要になることである。
中身が負、つまり
γ2<ω02の場合は
λは虚数解となり、(2)の解は
減衰振動を表す。
中身が0、つまり
γ2=ω02の場合は
λは実数の重解となり、(2)の解は
臨界減衰を表す。
中身が正、つまり
γ2>ω02の場合は
λは2つの実数解となり、(2)の解は
過減衰を表す。
減衰振動はおもりが振動しながら徐々に速度が0に収束するが、臨界減衰と過減衰はいずれも振動することなく指数関数的に速度が0に収束する。
各パターンで得られる解の形が変わってくるので本当はそれぞれ求めないといけないのだが、今回は減衰振動になる場合に限定する。
この時
λ=−γ±iω02−γ2 となり、
α=ω02−γ2と置いてやれば
λ=−γ±iα=λ±
と
λが2つ求まるので、(2)の解を
X1,
X2とすると以下のようになる。
X1X2=eλ+t=e(−γ+iα)t=e−γteiαt=e−γt(cosαt+isinαt)=eλ−t=e(−γ−iα)t=e−γte−iαt=e−γt(cosαt−isinαt)
ただし、途中でオイラーの公式
eiθ=cosθ+isinθ を使っている。
したがって、任意定数A, Bを用いると(2)の一般解
Xhは以下のように表せる。(ここの式展開の詳細は補足に記載)
Xh=A2X1+X2+B2iX1−X2=e−γt(Acosαt+Bsinαt)⋯(3)
e−γtは減衰を表し、
Acosαt+Bsinαtは単振動と同じ形である。
非同次線形微分方程式の特殊解と一般解
次に非同次線形微分方程式(1)を満たす特殊解
Xsをなんとか1個でいいので求めたい。
とは言ってもこのままの形で
Xsを直接予想して確かめるのは非常に困難なので、これまたトリッキーな常套手段で求めていくことになる。
まず、(1)の
Xを
Yに置き換え(単に
Xと区別するため)右辺の外力を
mFsinωtに変えた式
Y¨+2γY˙+ω02Y=mFsinωt⋯(1)′
を考える。
iY¨+2iγY˙+iω02Y=mFisinωt⋯(1)′′
(1)’ + (1)’’として整理すると
(X¨+iY¨)+2γ(X˙+iY˙)+ω02(X+iY)=mF(cosωt+isinωt)
ここで
Z=X+iYとして右辺はオイラーの公式を使えば上の式は以下のようにまとめられる。
Z¨+2γZ˙+ω02Z=mFeiωt⋯(1)′′′
で、この微分方程式(1)’’’を満たす複素数解
Zを求めてその実部を取ると、それが元の微分方程式(1)の解とみなせるというのが定石らしい。
ちなみに、(1) で外力として与える振動が
sinωtなら
Zの虚部を取ればいい。
それでは(1)’’’の解を求めるため、その解を
Z=Ceiωtと仮定して(1)’’’に代入すると(Cは任意定数)
−ω2Ceiωt+2iγCωeiωt+Cω02eiωt=mFeiωt
eiωt=0なので、辺々割ってCを求めると
C=mFω02−ω2+2γωi1⋯(4)
(4)の分母分子に
ω02−ω2−2γωi(分母の共役複素数)をかけると
C=mF(ω02−ω2)2+4γ2ω2ω02−ω2−2γωi
さらにもう一工夫として分子にある
ω02−ω2−2γωiについて複素平面上で考えれば
cosϕsinϕ=(ω02−ω2)2+4γ2ω2ω02−ω2=(ω02−ω2)2+4γ2ω2−2γω
と表せるので、結果的に
ω02−ω2−2γωi=(ω02−ω2)2+4γ2ω2(cosϕ+isinϕ)=(ω02−ω2)2+4γ2ω2eiϕ
とまとめられる。
ϕは複素数
ω02−ω2−2γωiの偏角である。
よって、最終的なCの形は
C=mF(ω02−ω2)2+4γ2ω2eiϕ
Z=mF(ω02−ω2)2+4γ2ω2ei(ωt+ϕ)⋯(5)
そして、前述したように(1)の特殊解
Xsは(5)の実部を取ればいいので
Xs=mF(ω02−ω2)2+4γ2ω2cos(ωt+ϕ)⋯(6)
となる。これで(3)(6)の通り
Xh,
Xsが求まったので、(1)の一般解は
X=Xh+Xs=e−γt(Acosαt+Bsinαt)+mF(ω02−ω2)2+4γ2ω2cos(ωt+ϕ)
であり、
X=x−xeと置き換えていたので最終的な
xの一般解は以下のようになる。
x=e−γt(Acosαt+Bsinαt)+mF(ω02−ω2)2+4γ2ω2cos(ωt+ϕ)+xe⋯(7)
初期値問題を解く
(7)は一般解なので今回想定する初期条件を代入して定数A, Bを求める。
(7)を時間微分して速度
x˙を計算すると
x˙=−eγt{(Aγ−Bα)cosαt+(Aα+Bγ)sinαt}−mF(ω02−ω2)2+4γ2ω2ωsin(ωt+ϕ)
なので、初期条件
t=0で
x(0)=x0、
x˙(0)=0を代入して頑張って計算すればA, Bは以下のように求められる。
AB=−mF(ω02−ω2)2+4γ2ω2cosϕ+x0−xe=mFα(ω02−ω2)2+4γ2ω2ωsinϕ−γcosϕ+α(x0−xe)γ
最終的にまとめると、今回の初期条件の下で強制振動の位置を表す
xの解は
x=e−γt(Acosαt+Bsinαt)+mF(ω02−ω2)2+4γ2ω2cos(ωt+ϕ)+xe
ただし、AとBは上記の形で
ω0=mkγ=2mbF=kaα=ω02−γ2ϕ=tan−1(ω02−ω2−2γω)
はい、できましたー…
理論解のプロット
求めた
xの理論解を実際にプロットして振動の様子を見る。
各種係数は強制振動と減衰の様子がいい感じに見やすくなるように適当に設定している。
係数の計算がこの話の肝かつ煩わしいところなので、最初に計算して使いまわせるようにinit_constant_params()
という初期化用の関数にまとめている。
import numpy as np
import matplotlib.pyplot as plt
from numpy import pi, sqrt, sin, cos, atan2, exp
def init_constant_params(x0, amplitude):
""" 計算で使う各種定数を初期化する
外力の振幅amplitudeは引数として受け取り色々な強制振動パターンで計算できるようにする
"""
# 物理量の設定
xe = 0.3 # バネの自然長[m]
k = 0.5 # バネ定数[N/m]
m = 0.5 # おもりの質量[kg]
b = 0.1 # 減衰係数[kg/s]
omega = pi * 2 / 3 # 外力の角振動数[rad/s]
# 計算用の各種係数
force = k * amplitude # 外力[N]
gamma = b / (2 * m)
omega_0 = sqrt(k / m) # バネ系の固有振動数
alpha = sqrt(omega_0**2 - gamma**2)
phi = atan2(-2 * gamma * omega, omega_0**2 - omega**2)
denominator = sqrt((omega_0**2 - omega**2) **
2 + 4 * gamma**2 * omega**2)
A = - force / m * cos(phi) / denominator + x0 - xe
B = force / m * (omega * sin(phi) - gamma * cos(phi)) / \
(alpha * denominator) + (x0 - xe) * gamma / alpha
return {
'equilibrium_length': xe,
'mass': m,
'gamma': gamma,
'omega': omega,
'alpha': alpha,
'force': force,
'phi': phi,
'denominator': denominator,
'A': A,
'B': B,
}
def calc_theoretical_solution(t, params):
""" 理論解を計算する
"""
# 計算に使う定数を設定
xe = params['equilibrium_length']
m = params['mass']
gamma = params['gamma']
omega = params['omega']
alpha = params['alpha']
force = params['force']
phi = params['phi']
denominator = params['denominator']
A = params['A']
B = params['B']
# 減衰振動だけを想定した計算であり臨界減衰や過減衰ではないので注意!!!
x = exp(-gamma * t) * (A * cos(alpha * t) + B * sin(alpha * t)) \
+ force / m * cos(omega * t + phi) / denominator + xe
return x
def forced_vibration(t_list, params):
""" 全時刻の理論解を計算する """
x_list = [calc_theoretical_solution(t, params) for t in t_list]
return x_list
def plot_graph(t_list, xu_list, xf_list):
fig = plt.figure(figsize=(8, 4))
fig.suptitle('Forced vibration (theoretical solution)')
plt.plot(t_list, xu_list, label='unforced', color='b', linewidth=1)
plt.plot(t_list, xf_list, label='forced', color='r', linewidth=1)
plt.xlabel("time [s]")
plt.ylabel("x [m]")
plt.legend(['unforced', 'forced'],
loc='upper right', bbox_to_anchor=(1, 1))
plt.grid()
plt.show()
if __name__ == '__main__':
t_end = float(input('計算終了時刻[s]:'))
x0 = float(input('おもりの初期位置 x0[m]: '))
v0 = 0 # 初速度は0で固定
h = 0.01 # 時間刻み幅
# 初期化した各種定数を取得
params_u = init_constant_params(x0, amplitude=0) # 強制力なし unforced
params_f = init_constant_params(x0, amplitude=0.1) # 強制力あり forced
# 計算時間ステップのリストを生成する(最後の時刻t_endも含めたいので+hしている)
t_list = np.arange(0, t_end + h, h)
# 振動の計算
xu_list = forced_vibration(t_list, params_u) # 強制力なし
xf_list = forced_vibration(t_list, params_f) # 強制力あり
plot_graph(t_list, xu_list, xf_list)
強制振動があるパターンを赤線、ないパターン(強制振動の振幅が0)を青線でプロット。
odeintを使った数値解
久々に強制振動の理論解の導出をやってみたが結構しんどい。
しかしこれでも微分方程式の初歩に過ぎない…
で、↑みたいなめんどくさい導出なんかやりたくねんだ!という場合は数値的に解こう。
「微分方程式の数値解析の基本①」「微分方程式の数値解析の基本②」でやったのと同様に自分で解析部分のプログラムを作成するのでもいいが、今回は常微分方程式の数値解を計算できるSciPyのodeintを使ってみる。
odeintとは
SciPyは様々な数学的な処理を行う関数やアルゴリズムを提供するPythonのライブラリで、その中に常微分方程式を数値的に解くことができるodeint
という関数がある。
odeint (func, y0, t, args = (),...)
公式ドキュメントを見ると指定可能な引数は他にもたくさんあるが基本はこれだけでOK。
func
計算対象の微分方程式を定義しておく関数。
連立微分方程式の場合はそれらをすべてこの中で定義しておく。
関数定義のところでは引数を指定しておき、前ステップの計算結果y
のリスト、時刻、その他のパラメータ(args
が指定されていれば)などを指定する。
y0
計算対象となる状態変数の初期値のリスト。
連立微分方程式の場合はそれぞれの状態変数の初期値をリスト形式で渡す。
t
計算の各時刻(時間ステップ)のリスト。
あらかじめnumpy
のarange()
やlinspace()
などを使って生成した時刻のリストを渡すとよい。
args
y0
、t
以外で何かfunc
に渡して使いたいパラメータがあればタプル形式で指定する。
特に何もなければargs
は不要。
odeintを使う準備
解きたい微分方程式は2階の微分方程式なので、odeint()
で計算するためには媒介変数を使って2本の連立1階微分方程式に分ける必要がある。
x¨+mbx˙+mk(x−xe)=mkacosωt
この微分方程式について媒介変数
v(速度)を使えば、
x˙=v、
x¨=v˙であるから以下のように分けることができる。
{x˙=vv˙=m1{kacosωt−bv−k(x−xe)}
ベクトルの成分表記っぽくすれば
dtd(xv)=(vm1{kacosωt−bv−k(x−xe)})
これらの式をfunc
の中で定義してodeint()
に渡してあげればよい。
そして、計算結果である
odeint()
の戻り値の中身はNumpy配列で、各時刻の
xと
vの結果の配列になっている。
これをそのままmatplotlibのプロット関数に渡すと
xと
v両方の結果がプロットされるが、
# 各時刻の結果が[x, v]の順で格納されている
result[:, 0] # x
result[:, 1] # v
のようにすれば片方だけの結果を抽出できる。
数値解のプロット
理論解の時と同じ条件で数値計算を行う。
import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
from numpy import pi, cos
def init_constant_params(amplitude):
""" 計算で使う各種定数を初期化する
外力の振幅amplitudeは引数として受け取り色々な強制振動パターンで計算できるようにする
"""
k = 0.5 # バネ定数[N/m]
force = k * amplitude # 外力[N]
return {
'equilibrium_length': 0.3, # バネの自然長[m]
'spring_constant': k,
'mass': 0.5, # おもりの質量[kg]
'damping_coefficient': 0.1, # 減衰係数[kg/s]
'force': force, # 外力[N]
'omega_f': pi * 2 / 3, # 外力の角振動数[rad/s]
}
def func(xv, t, params):
# 計算に使う定数を設定
xe = params['equilibrium_length']
k = params['spring_constant']
m = params['mass']
b = params['damping_coefficient']
force = params['force']
omega_f = params['omega_f']
x, v = xv
dx_dt = v
dv_dt = (force * cos(omega_f * t) - b * v - k * (x - xe)) / m
return [dx_dt, dv_dt]
def plot_graph(t_list, xu_list, xf_list):
fig = plt.figure(figsize=(8, 4))
fig.suptitle('Forced vibration (numerical solution)')
plt.plot(t_list, xu_list, label='unforced', color='b', linewidth=1)
plt.plot(t_list, xf_list, label='forced', color='r', linewidth=1)
plt.xlabel("time [s]")
plt.ylabel("x [m]")
plt.legend(['unforced', 'forced'],
loc='upper right', bbox_to_anchor=(1, 1))
plt.grid()
plt.show()
if __name__ == '__main__':
t_end = float(input('計算終了時刻[s]:'))
x0 = float(input('初期位置[m]:'))
v0 = 0 # おもりの初速度[m/s]
h = 0.01 # 時間刻み幅
# 初期化した各種定数を取得
params_u = init_constant_params(amplitude=0) # 強制力なし unforced
params_f = init_constant_params(amplitude=0.1) # 強制力あり forced
# 計算時間ステップのリストを生成する(最後の時刻t_endも含めたいので+hしている)
t_list = np.arange(0, t_end + h, h)
# 振動の計算
result_u = odeint(func, [x0, v0], t_list, args=(params_u,)) # 強制力なし
result_f = odeint(func, [x0, v0], t_list, args=(params_f,)) # 強制力あり
# xのみプロットしたいので抽出
xu_list = result_u[:, 0]
xf_list = result_f[:, 0]
plot_graph(t_list, xu_list, xf_list)
本当はちゃんと誤差などを出して定量的に評価しないといけないけれど、結果は理論解の時とほぼ同じなのでヨシ。
補足
同次線形微分方程式の解の変形
X1X2=e−γt(cosαt+isinαt)=e−γt(cosαt−isinαt)
X1,X2は共役複素数なので
2X1+X22iX1−X2=e−γtcosαt=e−γtsinαt
という計算でそれぞれの実部と虚部を取り出すことができる。
X1,X2は線形微分方程式(2)の解なので、それらを定数倍して足し合わせた(線形結合した)
2X1+X2,2iX1−X2
もそれぞれ(2)の解になる。さらにそれらを線型結合した
A2X1+X2+B2iX1−X2=e−γt(Acosαt+Bsinαt)
も(2)の解になるので、結局
Xh=e−γt(Acosαt+Bsinαt)
のように式変形できる。
変位
Xhは物理量なので実数でなければいけないから、この変形により虚数単位
iを使わずに解を表現することができる。
数値計算での減衰の場合分けについて
理論計算だと
γと
ω0の大小関係に応じて減衰振動、過減衰、臨界減衰の式を切り替えられるプログラムにする必要があるが、数値計算はそんなことを考えなくても条件に応じた答えを出してくれる。
例えば
減衰振動:
γ<ω0つまり
b2<4mk
臨界減衰:
γ=ω0つまり
b2=4mk
過減衰:
γ>ω0つまり
b2>4mk
となるように係数を定めると、場合分けしなくてもそれぞれの条件に応じた運動を計算して結果を出してくれる。(下の図は強制振動なしで計算した結果)