TI-Nspire版Pythonによる振り子のシミュレーション / 単振り子

#概要
TI-NspireでMicroPythonがサポートされた。ためしに単振り子の運動方程式(微分方程式)を近似的に解いて質点の動きをアニメ化してみる。      

リアルタイムシミュレーションにしたいので、現状を描画 → 1ステップだけ未来を計算 → その状態を描画 → さらに1ステップだけ未来を計算 → ... を繰り返すことにする。    

#サンプルファイル
```example_pend_single.py
from PEND_SINGLE_Class import *
from CREATE_SIM_SPACE_Class import *
from math import pi as PI

# シミュレートすべきオブジェクトをいくつか作る。
# 引数は(振り子長さ,錘描画半径,初期角度,初期角速度,1ステップ<,1ステップ分割数>)。
# 角度は、鉛直方向下向きを0ラジアン、反時計回りをプラス、時計回りをマイナスとする。
# 錘描画半径はシミュレーションには無関係。
h = 1/2**5
num_of_div = 2**2
p0 = PEND_SINGLE_Class(1, 0.1, PI * 0.998986328504169326 , 0, h, num_of_div)
p1 = PEND_SINGLE_Class(0.5, 0.1, PI * 0.7, 0, h, num_of_div)
p2 = PEND_SINGLE_Class(0.2, 0.1, PI * 0.5, 0, h, num_of_div)

# 各オブジェクトをシミュレーション空間に投入し、シミュレーションを始める。
CREATE_SIM_SPACE_Class([p0, p1, p2]).start_sim()
```

##実行結果
ハンドヘルドでもそこそこ動いている。    
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193936/9ebfe272-2402-a3ac-71bf-3a42811e5091.png)
](https://youtu.be/KUUS-bcKWHU)


#常微分方程式を解くためのクラス
Nspireには```rk23()```という組み込み函数が用意されているが、ここでは古典的なルンゲクッタ法で解くことにする。外部モジュールは何も使っていないので普通のPythonでそのまま動く。              
<details><summary>`ソースコード (RK4_Class.py) を見る/隠す`</summary><div>    

```RK4_Class.py
# Python による常微分方程式の数値解法_改良版 / 古典的 Runge-Kutta 法
class RK4_Class:
  def __init__(self, funcs, inits, h, num_of_div=1):
    _num_of_funcs = len(funcs)
    
    self.funcs = funcs
    self.t0 = 0
    self.inits = inits
    self.h = h / num_of_div
    self.half_h = self.h / 2
    self.sixth_h = self.h / 6
    self.range_dim = range(_num_of_funcs)
    self.range_div = range(num_of_div)
    self.f = [[0] * _num_of_funcs] * 4
    self.temp = [[0] * _num_of_funcs] * 3

  def update(self):
    for i in self.range_div:
      self.f[0] = [self.funcs[j](self.t0, *self.inits) for j in self.range_dim]
      self.temp[0] = [self.inits[j] + self.half_h * self.f[0][j] for j in self.range_dim]
      self.f[1] = [self.funcs[j](self.t0 + self.half_h, *self.temp[0]) for j in self.range_dim]
      self.temp[1] = [self.inits[j] + self.half_h * self.f[1][j] for j in self.range_dim]
      self.f[2] = [self.funcs[j](self.t0 + self.half_h, *self.temp[1]) for j in self.range_dim]
      self.temp[2] = [self.inits[j] + self.h * self.f[2][j] for j in self.range_dim]
      self.f[3] = [self.funcs[j](self.t0 + self.h, *self.temp[2]) for j in self.range_dim]
      self.inits = [self.inits[j] + self.sixth_h * (self.f[0][j] + 2 * (self.f[1][j] + self.f[2][j]) + self.f[3][j]) for j in self.range_dim]
      self.t0 += self.h
    return self

  def print(self):
    print(self.t0, self.inits)
    return self

########
# test #
########
#if __name__ == "__main__":
def main():
  # 解くべき連立常微分方程式を定義する。
  # 確認のため、厳密解の存在する方程式を定義する。単振り子とは無関係。
  def xDot(t, x, y): # x'(t) = y(t)
    return y
  def yDot(t, x, y): # y'(t) = t - x(t)
    return t - x
  funcs = [xDot, yDot]

  # 従属変数の初期値を指定する。
  x0 = 0
  y0 = 0
  inits = [x0, y0]

  # ステップ幅を指定する。
  h = 0.25 # =1/2**2
  
  # 1ステップだけ計算する函数を実体化して、
  rk4 = RK4_Class(funcs, inits, h)

  # 初期値を何度か更新して確認する。
  while rk4.t0 < 7.5:
    rk4.update().print()
```

</div></details>

#単振り子の質点のxy座標を計算するためのクラス

<details><summary>`ソースコード (PEND_SINGLE_Class.py) を見る/隠す`</summary><div>

```PEND_SINGLE_Class.py
from RK4_Class import *
import time
import ti_draw as td
import math

PI = math.pi

class PEND_SINGLE_Class:
  # (長さ,錘描画半径,初期角度,初期速度,1ステップ,1ステップ内部分割数)
  def __init__(self, length, mass_r, init_angle, init_vel, h=1/2**5, numOfDiv=1):
    
    # リセット用に引数を取っておく。
    self.ARGs = [length, mass_r, init_angle, init_vel, h, numOfDiv]

    self.LENGTH = length
    self.MASS_R = mass_r

    #更新すべき初期値を設定する。
    self.t0 = 0
    self.angle = init_angle
    self.vel = init_vel
    self.inits = [self.angle, self.vel]

    # 解くべき微分方程式を定義する。
    def dot_angle(t, angle, vel):
      return vel
    def dot_vel(t, angle, vel):
      return (-9.80665 / self.LENGTH) * math.sin(angle)
    self.FUNCS = [dot_angle, dot_vel]

    # 上記の微分方程式を上記の初期値で解くための函数を実体化する。
    self.rk4 = RK4_Class(self.FUNCS, self.inits, h, numOfDiv)

  def update(self):
    self.rk4.update() # 初期値を更新して、
    self.t0 = self.rk4.t0 # 更新された時間を取り出して、
    self.angle = self.rk4.inits[0] # 更新された角度を取り出して、
    self.vel = self.rk4.inits[1] # 更新された角速度を取り出す。
    return self

  def draw(self):
    x = self.LENGTH * math.sin(self.angle) # 角度から質点のx座標を計算する。
    y = self.LENGTH * -math.cos(self.angle) # 角度から質点のy座標を計算する。
    td.draw_line(0, 0, x, y) # 原点からその質点まで線を引く。
    td.draw_circle(x, y, self.MASS_R) # 質点の位置に円を描く。
    return self

  def _print(self):
    print(self.t0, self.angle, self.vel) # 計算結果の確認用。

########
# test #
########
#if __name__ == "__main__":
def main():
   pend = PEND_SINGLE_Class(1, 0.1, PI * 0.999, 0, 1/2**4, 4)
   for i in range(10):
     pend.update()._print()
```

</div></details>

#シミュレーション空間を作ってシミュレートするためのクラス

<details><summary>`ソースコード (CREATE_SIM_SPACE_Class.py) を見る/隠す`</summary><div>

```CREATE_SIM_SPACE_Class.py
from PEND_SINGLE_Class import *
import ti_draw as td
import ti_system as tsys
from math import pi as PI

class CREATE_SIM_SPACE_Class:
  def __init__(self, objects):
    WIDTH, HIGHT = td.get_screen_dim()
    ASPECT = WIDTH / HIGHT

    self.objects = objects
    self.MAX_LENGTH = max([object.LENGTH for object in objects])
    self.Y_MIN = -self.MAX_LENGTH * 1.1
    self.Y_MAX = self.MAX_LENGTH * 1.1
    self.X_MIN = self.Y_MIN * ASPECT
    self.X_MAX = self.Y_MAX * ASPECT
    td.set_window(self.X_MIN, self.X_MAX, self.Y_MIN, self.Y_MAX)

    self.LINE_SPACE = (self.Y_MAX - self.Y_MIN) / 10 
    self.MESSAGES = ["To reset, press the esc key.",
                     "To pause or resume, press the enter key.",
                     "To exit, press the F12 or Home key." ,]

  def _wait_for_pressing_enter(self):
    while tsys.get_key() != "enter":
      pass

  def _update_objects(self):
    for object in self.objects:
      object.update()

  def _draw_objects(self):
    for object in self.objects:
      object.draw()

  def _reset_objects(self):
    td.clear()
    for object in self.objects:
      object.__init__(*object.ARGs)
      object.draw()

  def _display_time(self):
     td.draw_text(self.X_MIN, self.Y_MAX - self.LINE_SPACE, "sec: "+str(self.objects[0].t0))

  def _display_messages(self):
     td.draw_text(self.X_MIN, self.Y_MIN + self.LINE_SPACE * 2.5, self.MESSAGES[0])
     td.draw_text(self.X_MIN, self.Y_MIN + self.LINE_SPACE * 1.5, self.MESSAGES[1])
     td.draw_text(self.X_MIN, self.Y_MIN + self.LINE_SPACE * 0.5, self.MESSAGES[2])

  def _pause_or_resume_objects(self):
    while True:
      key = tsys.get_key()
      if key == "esc":
        self._reset_objects()
        self._display_time()
        self._display_messages()
        td.paint_buffer()
      if key == "enter":
        break

  def start_sim(self):
    td.use_buffer()
    self._draw_objects() # 初期状態を描いて、
    self._display_time()
    self._display_messages()
    td.paint_buffer()

    self._wait_for_pressing_enter() # enterキーが押されるまで時間をとめて、
    while True:
      td.clear()
      self._update_objects() # 1ステップだけ未来を計算して、
      self._draw_objects() # 描き直す、を繰り返す。
      self._display_time()
      self._display_messages()
      td.paint_buffer()

      key = tsys.get_key()
      if key == "enter":
        self._pause_or_resume_objects()
      if key == "esc":
        self._reset_objects()
      #time.sleep_ms(20)


########
# test #
########
#if __name__ == "__main__":
def main():
  # (長さ,錘描画半径,初期角度,初期速度,1ステップ<,1ステップ内部分割数>)
  p = PEND_SINGLE_Class(1, 0.1, PI * 0.999 , 0, 1/2**4 )
  sute = CREATE_SIM_SPACE_Class([p,])
  sute.start_sim()
```

</div></details>