Verlet Integration in Haskell
Asked Answered
G

3

5

I am new to Haskell, and as an exercise I've been trying to implement some of the code (written in Mathematica) from Joel Franklin's book Computational Methods in Physics. I've written the following code to take a lambda expression (acceleration) as the first argument. In general, acceleration is of the form x'' = f(x',x,t), but not always of all three variables.

-- 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)

In ghci, I would run this code with the following command (the acceleration function a = -(2pi)2x comes from an exercise in the book):

verlet (\x v t -> -(2*pi)^2*x) 1 0 0.01 1000

My problem is that this isn't really the Verlet method - here xn+1 = xn + dt*vn+1/2*a(xn,vn,n), while the Verlet method described in the Wikipedia goes xn+1 = 2*xn - xn-1+a(xn,vn,n). How would I re-write this function to more faithfully represent the Verlet integration method?

Tangentially, is there a way to write this more elegantly and succinctly? Are there linear algebra libraries that would make this easier? I appreciate your advice.

Gadmann answered 9/3, 2014 at 5:51 Comment(0)
C
5

The faithful Verlet sequence has xn depending on the previous two values of x -- xn-1 and xn-2. A canonical example of such a sequence is the Fibonacci sequence which has this one-liner Haskell definition:

fibs :: [Int]
fibs = 0 : 1 : zipWith (+) fibs     (tail fibs)
                        -- f_(n-1)  f_n

This defines the Fibonacci sequence as an infinite (lazy) list. The self-reference to tail fibs gives you the previous term, and the reference to fibs gives you the term before that. The terms are then combined with (+) to yield the next term in the sequence.

You can take the same approach with your problem as follows:

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)

The data structure State holds whatever state variables you need - x, v, t, n, ... The function step is analogous to (+) in the Fibonacci case and computes the next state given the previous two. The verlet function determines the entire sequence of states given the initial two states.

Claudell answered 9/3, 2014 at 6:50 Comment(3)
This is a nice solution, but when I try to load it into ghci to play with it, I get the following error message: parse error in constructor in data/newtype declaration: (Double, Double, Double) I'm going to take this to another thread.Gadmann
@castle-bravo: There was a typo in the answer (which I've fixed). user5402 used data where they should have used type; the latter is for defining type synonyms (a new name for an old type), whereas data is for defining brand new types, and so requires constructor names. (That'd be another option: data State = State Double Double Double, or data State = State { position, velocity, time :: Double}.) There's a whole Learn You A Haskell chapter about defining types if any of this is unclear and you want to learn more.Surbeck
@AntalS-Z and user5402 - Thanks for your help! I put my implementation of your suggestions in an answer below.Gadmann
T
2

Actually, if you read on you will find that both variants are represented on the wikipedia page.


Basic Verlet

The basic second order central difference quotient discretization for second order ODE x''(t)=a(x(t)) is

xn+1 - 2*xn + xn-1 = an*dt^2

Note that there are not velocities in the iteration and also not in the acceleration function a(x). This is because Verlet integration is only then superior to other integration methods when the dynamical system is conservative, that is, -m*a(x) is the gradient of some potential function, and potential functions are static objects, they depend only on the position, not on time and not on velocity. Many frictionless mechanical systems fall into this category.


Velocity Verlet

Now set, using first order central difference quotients, the velocity at time tn to

vn*(2*dt) = xn+1 - xn-1

and add and subtract this to and from the first equation to obtain

-2*xn + 2*xn-1 = -2*vn*dt + an*dt^2

2*xn+1 - 2*xn = 2*vn*dt + an*dt^2

or

vn = (xn - xn-1)/dt + 0.5*an*dt

xn+1 = xn + vn*dt + 0.5*an*dt^2

This is one variant to write the velocity-Verlet algorithm.


(updated to have all state variables correspond to the same time before and after the iteration step)

Using the equations of the previous step from n-1 to n, one can replace xn-1 by vn-1 and an-1 in the velocity computation. Then

vn = vn-1 + 0.5*(an-1 + an)*dt

To avoid having two instances of any of the vectors x,v,a, one can arrange the update process so that everything is in place. Assume that at entry of the iteration step, the stored data corresponds to (tn-1,xn-1,vn-1,an-1). Then the next state is computed as

vn-0.5 = vn-1 + 0.5*an-1*dt

xn = xn-1 + vn-0.5*dt

Do collision detection with xn and vn-0.5

an = a(xn)

vn = vn-0.5 + 0.5*an*dt

Do statistics with xn and vn

or as code

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); 

Changing the order of these operations will destroy the Verlet scheme and significantly alter the results, rotation of the oparations is possible, but one has to be careful about the interpretation of the state after the iteration step.

The only initialization needed is the computation of a0 = a( x0 ).


Leapfrog Verlet

From the formulas of velocity Verlet one can see that for the updates of the position one does not need the velocities vn, but only the half point velocities vn+0.5. Then

an = a(xn)

vn+0.5 = vn-0.5 + an*dt

xn+1 = xn + vn+0.5*dt

or in code

a = eval_a(x);
v += a*dt;
x += v*dt;

Again, the order of these operations is fundamentally important, changes will lead to strange results for conservative systems.


(Update) However, one may rotate the execution sequence to

x += v*dt;
a = eval_a(x);
v += a*dt;

This corresponds to the iteration of the triples (tn,xn,vn+0.5) as in

xn = xn-1 + vn-0.5*dt

an = a(xn)

vn+0.5 = vn-0.5 + an*dt

The initialization only needs to compute

v0+0.5 = v0 + 0.5*a( x0 )*dt

(end update)

Any statistics that one computes using xn and vn-0.5 or vn+0.5 will be off by an error proportional to dt, since the time indices do not match. Collisions can be detected with the position vector alone, but in the deflection the velocities need to be sensibly updated too.

Tilden answered 9/3, 2014 at 17:50 Comment(0)
G
0

Here is my solution after implementing the suggestion from 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)

I ran it in ghci using the following command:

verlet1 (\x -> -(2*pi)^2*(first x)) (1, 0, 0) 0.01 100

That seems to yield exactly what I was expecting - apparently sinusoidal motion! I have yet to plot x (if anyone has any suggestions how to do that in Haskell, they're welcome). Also, if you see any obvious refactorings, feel free to point them out. Thanks!

Gadmann answered 10/3, 2014 at 7:4 Comment(2)
This seems to be an odd mix of velocity and leapfrog verlet. From the step one reads that each state s = (t,x,v) corresponds to s[n]=(t[n], x[n], v[n+0.5]). Then the initialization of s[0] should have x[0]=x0, t[0]=t0, v[0+0.5] = v0 + 0.5*dt*(f (x0, v0, t0)). Using that, the step can be simplified in leapfrog manner to: step s = ( first s + dt*(second s), second s + dt*(f s), third s + dt).Tilden
Or maybe I read that wrong and the state is intended to be s[n]=(t[n], x[n], v[n-0.5]). Then one would need to update v[n+0.5] = v[n]+dt*(f s[n]) and then x[n+1] = x[n]+dt*v[n+0.5] in exactly that order, if one wants to avoid the redundant call to the evaluation of f. Disregarding that duplication to have no reference to the state to be built, one can write the step only using the last state as step s = ( first s + dt*(second s + dt*(f s)), second s + dt*(f s), third s + dt).Tilden

© 2022 - 2024 — McMap. All rights reserved.