Return to 参考:Maxima 編(旧版アーカイブ)

Maxima で運動方程式を解いて振幅と周期の関係を調べる

Maxima を使って運動方程式を数値的に解き,単振り子の振幅と周期の関係を調べる。導出については,以下を参照。

一様重力場中の単振り子

単振り子のひもの長さを $\ell$,重力加速度の大きさを $g$,鉛直下向きからの振れ角を $\theta$ とすると,単振り子の運動方程式は

$$\frac{d^2\theta}{dt^2} = – \frac{g}{\ell} \sin\theta$$

となる。

振幅が小さい場合には単振動となること

$|\theta| \ll 1$ の場合には,$\sin\theta \simeq \theta$ と近似することで,運動方程式は

$$\frac{d^2\theta}{dt^2} = – \frac{g}{\ell} \theta$$

となり,これは単振動となる。単振動の周期 $\tau_0$ は

$$\tau_0 = 2 \pi \sqrt{\frac{\ell}{g}}$$

となり,単振り子のひもの長さと重力加速度だけで決まり,初期条件で与えられる振幅 $\theta_0$ に依存しない。これを「振り子の等時性」と言ったりする。

無次元化

単振動の周期 $\tau_0$ で無次元化した時間 $T$ を

$$T \equiv \frac{t}{\tau_0}$$

と定義する。この無次元化された時間座標で書いた運動方程式は

$$\frac{d^2 \theta}{dT^2} = – 4 \pi^2 \sin\theta$$

となる。

問題:振幅が大きい場合の周期は?

振幅が大きい,つまり $\sin\theta \simeq \theta$ と近似できない場合は,振り子の周期は一般に振幅に依存するのではないかと考えられる。

そこで,$T = 0$ での振幅 $\theta_0$ をたとえば 10° から 90° まで 10° きざみで大きくしていった場合,ふりこの周期はどうなるか? というのが問題。

ルンゲ・クッタ法で解くための準備

2階常微分方程式をルンゲ・クッタ法で数値的に解くためには,連立1階微分方程式系になおす。

\begin{eqnarray}
\frac{d\theta}{dT} &=& F_1(V) = V \\
\frac{dV}{dT} &=& F_2(\theta) = – 4 \pi^2 \sin\theta
\end{eqnarray}

これを初期条件 $T = 0$ で

\begin{eqnarray}
\theta(0) &=& \theta_0 \\
V(0) &=& 0
\end{eqnarray}

とし,$\theta_0 = 10^{\circ}, \ 60^{\circ}, \ 80^{\circ}$ の場合に $T_0 = 0$ から $ T_1 = 2$ まで解く。

(くどいようですが,規格化された時間で $T_1 = 2$ というのは,単振動の場合の周期 $\tau_0$ の 2 倍の時間まで,という意味ですよ。)

In [1]:
/* 連立微分方程式の右辺 */
F1(V):= V;
F2(theta):= - 4 * %pi**2 * sin(theta); 

/* 単振動の場合 
F2(theta):= - 4 * %pi**2 * theta; */
Out[1]:
\[\tag{${\it \%o}_{1}$}F_{1}\left(V\right):=V\]
Out[1]:
\[\tag{${\it \%o}_{2}$}F_{2}\left(\vartheta\right):=\left(-4\right)\,\pi^2\,\sin \vartheta\]

まず,$\theta_0 = 80^{\circ}$ の場合に,刻み幅 $h$ と数値計算の精度について確認しておく。

Maxima の Runge-Kutta 法の関数 rk() では,数値計算の精度に関係するのは刻み幅 $h$ だけであるからね。

In [2]:
/* 度をラジアンに変換する関数を準備しておく */
radians(deg):= deg * float(%pi)/180$

/* 初期条件と計算範囲 */
T0: 0$
th: 80$
theta0: radians(th)$ /* ラジアンになおす */
V0: 0$
T1: 2$

/* N を変えて数値計算... */
for N in [200, 2000, 20000] do(
  /* 刻み幅 */
  h: float((T1 - T0)/N), 
  
  /* Runge-Kutta 法で数値計算 */
  ansN: rk([F1(V), F2(theta)], 
           [theta, V], [theta0, V0], 
           [T, T0, T1, h]),
           
  /* 最後の値を表示し,誤差を確認。 */
  printf(true, "N = ~6d, h = ~,8f, V(T=2) = ~15,13f~%", 
                N, h, ansN[length(ansN)][3])
)$
N =    200, h = 0.01000000, V(T=2) = 8.0634656639326
N =   2000, h = 0.00100000, V(T=2) = 8.0634655783821
N =  20000, h = 0.00010000, V(T=2) = 8.0634655783315

上の結果から $N=2000$ で小数点以下10桁程度の精度があるとわかる。ここでは $N=20000$ で計算を行うことにしてみる。グラフを描くだけなら $N$ をそんなに大きくとる必要はありませんが,あとで理由がわかります。

In [3]:
/* 初期条件と計算範囲 */
T0: 0$
V0: 0$
T1: 2$
/* N を大きめにとる理由は,あとでわかる */
N: 20000$
/* 刻み幅 */
h: float((T1 - T0)/N)$ 

/* 振幅の初期値を変えて数値計算... */
for th in [10, 60, 80] do(
  theta0: radians(th),
  ansdeg[th]: rk([F1(V), F2(theta)], 
                 [theta, V], [theta0, V0], 
                 [T, T0, T1, h])
)$

数値計算できたら,結果をグラフにしてみます。

In [4]:
/* あらかじめグラフにする points のリストを作成しておく */

i: 0$
for th in [10, 60, 80] do(
  /* 線の色 */
  i: i + 1,
  col[th]: i,

  /* 凡例に振幅の初期値をいれる */
  legend[th]: concat("θ_0 = ", th, "°"),

  /* たくさん点を描くと時間がかかるので
     プロットするデータを少し間引く */
  pts[th]: makelist([ansdeg[th][i][1], ansdeg[th][i][2]], i, 1, N+1, 100)
)$

tmp: makelist([key =legend[th], 
               color = col[th], 
               points(pts[th])], th, [80, 60, 10])$
draw2d(
  font = "Arial", font_size = 16, 
  title = "単振り子の振幅と周期",
  user_preamble = "set key samplen 1",
  /* 表示範囲 */
  xrange = [0, 2], yrange = [-1.5, 1.5], 
  /* グリッド  */
  grid = true, 
  xaxis = true, 
  /* 座標軸のラベル */
  xlabel = "規格化された時間 T", ylabel = "振幅 θ (rad)", 
      
  point_type = dot, 
  points_joined = true, 
  line_width = 1.5,
  tmp  
)$

縦軸の振幅をラジアンから度に変えたグラフを描きます。

なお,以下では直接使いませんが,参考までに Maxima でリスト内の全ての要素を2倍する例は以下の通りです。

In [5]:
a: [1, 2, 3];
a*2;
Out[5]:
\[\tag{${\it \%o}_{20}$}\left[ 1 , 2 , 3 \right] \]
Out[5]:
\[\tag{${\it \%o}_{21}$}\left[ 2 , 4 , 6 \right] \]
In [6]:
/* ラジアンから度へ変換する関数を準備しておく */
degrees(rad):= rad * 180/float(%pi)$

/* あらかじめグラフにする points のリストを作成しておく */

i: 0$
for th in [10, 60, 80] do(
  /* 線の色 */
  i: i + 1,
  col[th]: i,
  /* 凡例 */
  legend[th]: concat("θ_0 = ", th, "°"),
  /* プロットするデータを少し間引き,縦軸をラジアンから度に。 */
  pts[th]: makelist([ansdeg[th][i][1], 
                     degrees(ansdeg[th][i][2])], i, 1, N+1, 100) 
)$

tmp: makelist([key =legend[th], 
               color = col[th], 
               points(pts[th])], th, [80,60,10])$
draw2d(
  font = "Arial", font_size = 16, 
  title = "単振り子の振幅と周期",
  user_preamble = "set key samplen 1",
  /* 表示範囲 */
  xrange = [0, 2], yrange = [-90, 90], 
  /* グリッド  */
  grid = true, 
  xaxis = true, 
  /* 座標軸のラベル */
  xlabel = "規格化された時間 T", ylabel = "振幅 θ (°)", 
      
  point_type = dot, 
  points_joined = true, 
  line_width = 1.5,
  tmp  
)$

周期は速度の向きが変わる時刻から…

初速度 $V(0) = 0$ ではじまる振り子の運動は,半周期ごとに $V = 0$ となるはずである。実際の数値計算では,刻み幅 $h$ ごとの値を求めているので,厳密に $V=0$ となる瞬間が得られるとは限らない。したがって,速度の向きが変わる時刻,つまり $V_i \times V_{i+1} \leq 0$ となる時刻を探すことにする。

ただグラフを描くためだけなら,N: 20000 などという大きな値をとる必要はないのだが,このように速度の向きが変わる瞬間をとらえたいときには,なるべく刻み幅を小さくすると良いはず。

このような理由で N の値を大きくしている。

In [7]:
for th in [10, 60, 80] do(
  printf(true, "θ_0 = ~2d° のとき,速度の向きが変わる時刻~%", th),
  for i:2 thru N do(
    /* if then の後に複数行の実行文を書く場合は () で囲む */
    if ansdeg[th][i][3] * ansdeg[th][i+1][3] <= 0 then (
      t: (ansdeg[th][i][1] + ansdeg[th][i+1][1])/2,
      printf(true, "    T = ~,5f", t)
    )
  ), 
  printf(true, "~%") /* 改行 */
)$
θ_0 = 10° のとき,速度の向きが変わる時刻
    T = 0.50095    T = 1.00195    T = 1.50285
θ_0 = 60° のとき,速度の向きが変わる時刻
    T = 0.53655    T = 1.07315    T = 1.60975
θ_0 = 80° のとき,速度の向きが変わる時刻
    T = 0.56875    T = 1.13745    T = 1.70625

以上の結果から,例えば振幅の初期条件を $\theta_0 = 60^{\circ}$ とした時の周期は $T = 1.07315$,すなわち単振動の場合の周期

$$T_0 = 2\pi \sqrt{\frac{\ell}{g}}$$

の $1.07315$ 倍だということになる。

問題

では本題。$\theta_0 = 10^{\circ}$ から $10^{\circ}$ きざみで $90^{\circ}$ まで, $T = 0$ から $ T = 2$ まで解き,振幅 $\theta_0$ に対する周期 $T$ を求めてみよ。

ヒント:

for th in [10, 60, 80]

などとなっているところを

for th:10 thru 90 step 10

となどすればいいかも。また,上記の例では半周期ごとに T = と表示しているが,周期は2回目に速度の向きが変わる時間だから,その時間だけを表示するようにしてください。

解答例

In [8]:
/* 振幅と周期のグラフを描くために x, y に追加していく */
x: []$ /* 振幅 */
y: []$ /* 周期 */

/* 参考として,ほぼ単振動となるであろう θ = 1°の場合 */
th: 1$
theta0: radians(th)$
ansdeg[th]: rk([F1(V), F2(theta)], 
               [theta, V], [theta0, V0], 
               [T, T0, T1, h])$

printf(true, "θ_0 = ~2d° のとき  ", th)$
count: 0$
for i:2 thru N do(
  /* if then の後に複数の実行文を書く場合は () で囲む */
  if ansdeg[th][i][3] * ansdeg[th][i+1][3] <= 0 then (
    count: count + 1,
    /* 周期は2回目に速度の向きが変わる時間だから */
    if count = 2 then (
      t: (ansdeg[th][i][1] + ansdeg[th][i+1][1])/2,
      x: append(x, [th]), 
      y: append(y, [t]),
      printf(true, "周期 T = ~,5f~%", t), 
      return() /* 周期を表示したら for ループから抜ける */
    )
  )
)$

/* 振幅の初期値を変えて数値計算... */
for th:10 thru 90 step 10 do(
  theta0: radians(th),
  ansdeg[th]: rk([F1(V), F2(theta)], 
                 [theta, V], [theta0, V0], 
                 [T, T0, T1, h])
)$

for th:10 thru 90 step 10 do(
  printf(true, "θ_0 = ~2d° のとき  ", th), 
  count: 0,
  for i:2 thru N do(
    /* if then の後に複数の実行文を書く場合は () で囲む */
    if ansdeg[th][i][3] * ansdeg[th][i+1][3] <= 0 then (
      count: count + 1,
      /* 周期は2回目に速度の向きが変わる時間だから */
      if count = 2 then (
        t: (ansdeg[th][i][1] + ansdeg[th][i+1][1])/2,
        x: append(x, [th]), 
        y: append(y, [t]),
        printf(true, "周期 T = ~,5f~%", t), 
        return() /* 周期を表示したら for ループから抜ける */
      )
    )
  )
)$
θ_0 =  1° のとき  周期 T = 1.00005
θ_0 = 10° のとき  周期 T = 1.00195
θ_0 = 20° のとき  周期 T = 1.00765
θ_0 = 30° のとき  周期 T = 1.01745
θ_0 = 40° のとき  周期 T = 1.03135
θ_0 = 50° のとき  周期 T = 1.04975
θ_0 = 60° のとき  周期 T = 1.07315
θ_0 = 70° のとき  周期 T = 1.10215
θ_0 = 80° のとき  周期 T = 1.13745
θ_0 = 90° のとき  周期 T = 1.18035
In [9]:
/* オプションを設定してグラフを描く */
draw2d(
  font = "Arial", font_size = 16, 
  title = "単振り子の振幅と周期の関係",
  /* 表示範囲 */
  xrange = [-2, 92], yrange = [0.95, 1.2], 
  /* グリッド  */
  grid = true, 
  /* 座標軸のラベル */
  xlabel = "振幅 θ (°)", ylabel = "規格化された周期 T", 
  
  /* 点を塗りつぶした丸に */
  point_type = 7,
  /* 点の大きさ */
  point_size = 0.5,
  points(x, y)
)$