我是Haskell的新手,作为练习,我一直在努力实现Joel Franklin的书物理计算方法中的一些代码(用Mathematica编写)。我编写了以下代码,将lambda表达式(加速度)作为第一个参数。通常,加速度的形式为x''= f(x',x,t),但并不总是所有三个变量。
-- Implementation 2.1: The Verlet Method
verlet _ _ _ _ 0 = []
verlet ac x0 v0 dt n = take n $ zip [0,dt..] $ verlet' ac x0 v0 0 dt
where verlet' ac x0 v0 t0 dt =
let
xt = x0 + dt*v0 + 0.5*(dt^2)*(ac x0 v0 t0)
vt = v0 + dt*(ac x0 v0 t0)
in
xt:(verlet' ac xt vt (t0+dt) dt)
在ghci中,我将使用以下命令运行此代码(加速函数a = - (2pi) 2 x来自书中的练习):
verlet (\x v t -> -(2*pi)^2*x) 1 0 0.01 1000
我的问题是这不是真正的Verlet方法 - 这里x n + 1 = x n + dt * v n + 1/2 * a(x n ,v n ,n),而维基百科中描述的Verlet方法为x n + 1 = 2 * x n - x n-1 + a(x n ,v n ,n)。如何重新编写此函数以更忠实地表示Verlet集成方法?
Tangentially,有没有办法更优雅,更简洁地写出来?是否有线性代数库可以使这更容易?我感谢你的建议。
答案 0 :(得分:5)
忠实的Verlet序列具有x n ,具体取决于前两个x - x n-1 和x n-2 的值。这种序列的典型例子是Fibonacci序列,它具有这种单行Haskell定义:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- f_(n-1) f_n
这将Fibonacci序列定义为无限(懒惰)列表。 tail fibs
的自引用为您提供上一个术语,fibs
的引用为您提供了之前的术语。然后将这些术语与(+)
组合以产生序列中的下一个术语。
您可以采用与您的问题相同的方法,如下所示:
type State = (Double, Double, Double) -- (x, v, t) -- whatever you need here
step :: State -> State -> State
step s0 s1 = -- determine the next state based on the previous two states
verlet :: State -> State -> [State]
verlet s0 s1 = ss
where ss = s0 : s1 : zipWith step ss (tail ss)
数据结构State
包含您需要的任何状态变量 - x,v,t,n,...
函数step
类似于Fibonacci案例中的(+)
,并计算给定前两个的下一个状态。给定初始两个状态时,verlet
函数确定整个状态序列。
答案 1 :(得分:2)
实际上,如果您继续阅读,您会发现两个变体都在维基百科页面上显示。
二阶ODE x''(t)= a(x(t))的基本二阶中心差商离散化是
x n + 1 - 2 * x n + x n-1 = a n * dt ^ 2
请注意,迭代中没有速度,加速函数a(x)也没有。这是因为当动态系统保守时,Verlet集成仅优于其他集成方法,即-m * a(x)是某些潜在函数的梯度,潜在函数是静态对象,它们仅依赖于位置,而不是准时而不是速度。许多无摩擦机械系统属于这一类。
现在使用一阶中心差分商设定时间t n 的速度
v n *(2 * dt)= x n + 1 - x n-1
并在第一个等式中加上和减去这个以获得
-2 * x n + 2 * x n-1 = -2 * v n * dt + a n 子> * dt的^ 2
2 * x n + 1 - 2 * x n = 2 * v n * dt + a n 子> * dt的^ 2
或
v n =(x n - x n-1 )/ dt + 0.5 * a n * dt的
x n + 1 = x n + v n * dt + 0.5 * a n * dt ^ 2
这是编写velocity-Verlet算法的一种变体。
(更新以使所有状态变量对应于迭代步骤之前和之后的同一时间)
使用从n-1到n的前一步骤的等式,可以用v n-1 和 n-替换x n-1 1 在速度计算中。然后
v n = v n-1 + 0.5 *(a n-1 + a n ) * dt的
为了避免任何向量x,v,a的两个实例,可以安排更新过程以使一切都到位。假设在进入迭代步骤时,存储的数据对应于(t n-1 ,x n-1 ,v n-1 ,一个<子> N-1 子>)。然后下一个状态计算为
v n-0.5 = v n-1 + 0.5 * a n-1 * dt
x n = x n-1 + v n-0.5 * dt
使用x n 进行碰撞检测并且v n-0.5
a n = a(x n )
v n = v n-0.5 + 0.5 * a n * dt
使用x n 和v n
进行统计
或代码
v += a*0.5*dt;
x += v*dt;
do_collisions(x,v);
a = eval_a(x);
v += a*0.5*dt;
do_statistics(x,v);
更改这些操作的顺序将破坏Verlet方案并显着改变结果,可以旋转操作,但是必须注意迭代步骤后对状态的解释。
唯一需要的初始化是计算 0 = a(x 0 )。
从速度Verlet的公式可以看出,对于位置的更新,人们不需要速度v n ,而只需要半点速度v n + 0.5 子>。然后
a n = a(x n )
v n + 0.5 = v n-0.5 + a n * dt
x n + 1 = x n + v n + 0.5 * dt
或代码
a = eval_a(x);
v += a*dt;
x += v*dt;
同样,这些操作的顺序从根本上讲是重要的,更改将导致保守系统的奇怪结果。
(更新)但是,可以将执行顺序旋转到
x += v*dt;
a = eval_a(x);
v += a*dt;
这对应于三元组的迭代(t n ,x n ,v n + 0.5 ),如
x n = x n-1 + v n-0.5 * dt
a n = a(x n )
v n + 0.5 = v n-0.5 + a n * dt
初始化只需要计算
v 0 + 0.5 = v 0 + 0.5 * a(x 0 )* dt
(结束更新)
使用x n 和v n-0.5 或v n + 0.5 进行计算的任何统计数据都将被一个与dt,因为时间指数不匹配。仅使用位置矢量就可以检测到碰撞,但是在偏转中,速度也需要明智地更新。
答案 2 :(得分:0)
这是我在实施user5402建议后的解决方案:
-- 1-Dimensional Verlet Method
type State = (,,) Double Double Double -- x, x', t
first :: State -> Double
first (x, _, _) = x
second :: State -> Double
second (_, x, _) = x
third :: State -> Double
third (_, _, x) = x
verlet1 :: (State -> Double) -> State -> Double -> Int -> [State]
verlet1 f s0 dt n = take n ss
where
ss = s0 : s1 : zipWith step ss (tail ss)
where
s1 = (first s0 + dt*(second s0) + 0.5*dt^2*(f s0),
second s0 + dt*(f s0), third s0 + dt)
step :: State -> State -> State
step s0 s1 = (2*(first s1) - first s0 + dt^2*(f s1),
second s1 + dt*(f s1), third s1 + dt)
我使用以下命令在ghci中运行它:
verlet1 (\x -> -(2*pi)^2*(first x)) (1, 0, 0) 0.01 100
这似乎正是我所期待的 - 显然是正弦运动!我还没有绘制x(如果有人有任何建议如何在Haskell中做到这一点,他们是受欢迎的)。此外,如果您发现任何明显的重构,请随时指出它们。谢谢!