Haskell で振動子
Haskell を使って,振動子を数値的に解こう。
まずは振動子の基本的な道具立て。
-- Oscillator.hs module Oscillator where -- まず、1自由度系の状態を表す型 State を定義しよう。 -- これは 時刻 (t) 位置座標 (x) 速度 (v) からなる。 data State = State { t, x, v :: Double } -- 例えば、時刻 0.0 位置 1.0 速度 0.0 の状態は、 -- 次のようにして作ることができる。 -- s = State 0.0 1.0 0.0 -- この状態の位置座標を取得するには次のようにする。 -- x s -- 質量を表す型 Mass を定義しておこう。これは Double と同じで良い。 type Mass = Double -- 力を表す型 Force を定義しよう。 -- 力は、一般に、状態の関数であるから、次のようにすると良いだろう。 -- これを力関数と呼ぶことにする。 type Force = State -> Double -- 系は質量と力によって完全に特徴づけられるので、 -- これらを合わせて System という型を作ろう。 type System = (Mass, Force) -- 系がある状態にあるときの、加速度を得る関数を用意する。 -- これは、ニュートンの運動方程式であり、 -- これによって、系の時間発展が実現される。 acceleration :: System -> State -> Double acceleration (m,f) state = (f state) / m -- 調和振動を引き起こす力関数を生成する関数を定義する。 -- これは、バネ定数を受け取って、力関数を返す。 type SpringConstant = Double makeHarmonicForce :: SpringConstant -> Force makeHarmonicForce k state = - k * (x state) -- 例えば、バネ定数 1.0 のときの力関数を得るためには、次のようにする。 -- makeHarmonicForce 1.0 -- 摩擦力を追加する関数を定義しよう。 -- 摩擦係数と元になる力関数を受け取って,新しい力関数を返す。 addDamping :: Double -> Force -> Force addDamping coefficient f0 state = (f0 state) - coefficient * (v state) -- 強制力を追加する関数も定義しよう。 -- 振幅、角振動数、位相、元になる力関数を受け取って、 -- sin関数型の強制力を付け加えられた力関数を返す。 addDriving :: Double -> Double -> Double -> Force -> Force addDriving amp angFreq phase f0 state = (f0 state) + amp * sin (angFreq * (t state) + phase) -- 系の全エネルギーを得る関数も用意しておこう。 -- 運動エネルギーとポテンシャルエネルギーの和であり、 -- 状態の関数として表される。 -- また、パラメーターとして、バネ定数と質量も必要だ。 energy :: SpringConstant -> Mass -> State -> Double energy k m state = (kinetic m state) + (potential k state) where kinetic :: Mass -> State -> Double kinetic m state = (m / 2.0) * ((v state) ** 2) potential :: SpringConstant -> State -> Double potential k state = (k / 2.0) * ((x state) ** 2)
次に、差分法を実装しよう。基本的な Euler法と Runge-Kutta法(4次)を実装する。
-- FiniteDifferenceMethods.hs module FiniteDifferenceMethods where import Oscillator -- 差分法を特徴づける型 FiniteDifferenceMethod を定義したい。 -- これは、状態を受け取って、新しい状態を返す。 -- 新しい状態を作るには、時間刻み幅と、時間発展に関する系の情報が必要だ。 -- そこで、次のように定義できるだろう。 type TimeDiff = Double type FiniteDifferenceMethod = TimeDiff -> System -> State -> State -- Euler法は、次のように定義できる。 euler :: FiniteDifferenceMethod euler dt sys (State t0 x0 v0) = State t1 x1 v1 where t1 = t0 + dt x1 = x0 + dt * v0 v1 = v0 + dt * a0 where a0 = acceleration sys (State t0 x0 v0) -- Runge-Kutta法(4次)は、次のように定義できる。 rk4 :: FiniteDifferenceMethod rk4 dt sys (State t0 x0 v0) = State t1 x1 v1 where t' = t0 + dt / 2.0 t1 = t0 + dt dx1 = dt * v0 dv1 = dt * (acceleration sys (State t0 x0 v0)) x'1 = x0 + dx1 / 2.0 v'1 = v0 + dv1 / 2.0 dx2 = dt * v'1 dv2 = dt * (acceleration sys (State t' x'1 v'1)) x'2 = x0 + dx2 / 2.0 v'2 = v0 + dv2 / 2.0 dx3 = dt * v'2 dv3 = dt * (acceleration sys (State t' x'2 v'2)) x'3 = x0 + dx3 v'3 = v0 + dv3 dx4 = dt * v'3 dv4 = dt * (acceleration sys (State t' x'3 v'3)) x1 = x0 + (dx1 + 2.0 * dx2 + 2.0 * dx3 + dx4) / 6.0 v1 = v0 + (dv1 + 2.0 * dv2 + 2.0 * dv3 + dv4) / 6.0
そして、系の時間発展を得るためのソルバーを実装する。
-- Solver.hs module Solver where import Oscillator import qualified FiniteDifferenceMethods as F -- 系の時間発展をシミュレートする関数を定義しよう。 -- これは、初期状態を受け取って、状態のリストを返すものとする。 solve :: F.TimeDiff -> Double -> F.FiniteDifferenceMethod -> System -> State -> [State] solve dt endtime fdm sys state0 = solve' nsteps state0 where nsteps = ceiling (endtime / dt) solve' :: Int -> State -> [State] solve' n state | n <= 0 = [state] | otherwise = state : (solve' n' state') where n' = n - 1 state' = fdm dt sys state -- 状態のリストから、特定の物理量のリストを得るための関数を用意しよう。 -- 時刻と物理量の組のリストを返すとする。 makeData :: (State -> Double) -> [State] -> [(Double,Double)] makeData operator states = map f states where f :: State -> (Double, Double) f state = (t state, operator state) -- 物理量のリストを出力する関数を用意しよう。 -- gnuplotで表示することを念頭において、 -- 時刻と物理量を空白で区切って出力する。 printData :: [(Double,Double)] -> IO () printData [] = putStrLn "" printData ((t,q):rest) = do putStrLn $ show t ++ " " ++ show q printData rest
ちなみに、ここまでのファイルの行数をカウントすると次のようになる。
$ egrep -v '^\s*(--|$)' -c Oscillator.hs FiniteDifferenceMethods.hs Solver.hs Oscillator.hs:23 FiniteDifferenceMethods.hs:32 Solver.hs:25
さて、それでは、適当にパラメーターを決めて、計算してみよう。
-- Main.hs import Oscillator import qualified FiniteDifferenceMethods as F import qualified Solver as S m = 1.0 k = 1.0 f0 = makeHarmonicForce k sys = (m, f0) state0 = State { t = 0.0, x = 1.0, v = 0.0 } endtime = 20.0 dt = 0.01
例えば、次の1行を追加すると、Euler法の計算結果を得られる。
main = S.printData $ S.makeData x $ S.solve dt endtime F.euler (m, f0) state0
Euler法、Runge-Kutta法、解析解をプロットすると、次のようになる。
また、減衰振動の結果を得るためには、次のようにすれば良い。
main = S.printData $ S.makeData x $ S.solve dt endtime F.rk4 (m, addDamping 0.1 f0) state0
摩擦係数を変えたときの計算結果は次のようになる。
とりあえず、今日はここまで。
Haskell を使うと、概念が
- 自然に
- 簡潔に
表現できると思う。