Previous ToC Up Next

9. The WorldPoint Class

9.1. Down at the Bottom

Bob: Now we come to the bottom of our hierarchy, where we find the WorldPoint class, which is a subclass of the Body class:

 class WorldPoint < Body
 
   attr_accessor :acc, :jerk,
                 :time, :next_time, :nsteps,
                 :minstep, :maxstep,
                 :body_id
 
   def setup(time)
     @time = @next_time = time
     @acc = @pos*0
     @jerk = @pos*0
     @nsteps = 0
     @minstep = VERY_LARGE_NUMBER
     @maxstep = 0
   end
 
   def startup(acc, jerk, timescale, dt_param, dt_max)
     @acc = acc
     @jerk = jerk
     dt = timescale * dt_param
     dt = dt_max if dt > dt_max
     @next_time = @time + dt
   end    
 
   def correct(old_point, acc, jerk, timescale, dt_param, dt_max)
     @acc = acc
     @jerk = jerk
     dt = timescale * dt_param
     dt = dt_max if dt > dt_max
     @next_time = @time + dt
     dt = @time - old_point.time
     @vel = old_point.vel + (1/2.0)*(old_point.acc + @acc)*dt +               
                            (1/12.0)*(old_point.jerk - @jerk) dt*2           
     @pos = old_point.pos + (1/2.0)*(old_point.vel + @vel)*dt +               
                            (1/12.0)*(old_point.acc - @acc) dt*2             
     admin(old_point.time)
     self
   end
 
   def admin(old_time)
     dt = @time - old_time
     @maxstep = dt if @maxstep < dt
     @minstep = dt if @minstep > dt
     @nsteps = @nsteps + 1
   end
 
   def extrapolate(t)
     if t > @next_time
       raise "t = " + t.to_s + " > @next_time = " + @next_time.to_s + "\n"
     end
     wp = WorldPoint.new
     wp.minstep = @minstep
     wp.maxstep = @maxstep
     wp.nsteps = @nsteps
     wp.mass = @mass
     wp.time = t
     dt = t - @time
     wp.pos = @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3
     wp.vel = @vel + @acc*dt + (1/2.0)*@jerk*dt**2
     wp
   end
 
   def interpolate(other, t)
     wp = WorldPoint.new
     wp.minstep = @minstep
     wp.maxstep = @maxstep
     wp.nsteps = @nsteps
     wp.mass = @mass
     wp.time = t
     dt = other.time - @time
     snap = (-6*(@acc - other.acc) - 2*(2*@jerk + other.jerk)*dt)/dt**2       
     crackle = (12*(@acc - other.acc) + 6*(@jerk + other.jerk)*dt)/dt**3      
     dt = t - @time
     wp.pos = @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3 +     
              (1/24.0)*snap*dt**4 + (1/144.0)*crackle*dt**5                   
     wp.vel = @vel + @acc*dt + (1/2.0)*@jerk*dt**2 + (1/6.0)*snap*dt**3 +     
              (1/24.0)*crackle*dt**4                                          
     wp
   end
 
   def kinetic_energy
     0.5*@mass*@vel*@vel
   end
 
   def potential_energy(body_array)
     p = 0
     body_array.each do |b|
       unless b == self
         r = b.pos - @pos
         p += -@mass*b.mass/sqrt(r*r)
       end
     end
     p
   end
 
 end

9.2. Setting Up and Starting Up

Alice: We have defined three phases of operation, a setup phase, a startup phase, and a normal phase in which we push particles forward. The difference between there three is finally becoming clear now.

Bob: At startup, we are revving the engines, so to speak: we are computing the initial force calculations, but we are not moving any particle yet. The engines are running in place.

Alice: In a way, we are simulating a previous step, by leaving the system in exactly the type of state it would have been in, had we arrived at the initial time from a previous integration. To the extent that a previous integration would have provided us with force calculations at the end of a previous step, we are doing those force calculations now.

Bob: Now why did we not just combine the setup and startup routines?

Alice: Good question. Hmm. On this level there certainly does not seem to be any clear reason not to combine these two. But we must have had some reason. Let's go back to the next level up, to WordLine, where these functions must be called from.

Ah, yes, there too we have two functions with the same name, setup and startup. Hmm again. At this point, too, it seems clear that we could have combined these functions. Well, let's keep moving up, to the WorldEra level.

Bob: Ho, before we do that, notice that there is another setup related method on the WorldLine level. At the very end, we have setup_from_single_worldpoint. In that method, the worldpoint itself is set up. Not that type of setting up surely has to be done for each particle, before we can even think of starting up the force calculation for each particle.

Alice: Perhaps we should have combined those two methods, WorldLine#setup and WorldLine#setup_from_single_worldpoint. Something to keep in mind for a future version. For now, let's leave this version alone as it is, since it works.

Still, I'd like to see what happens on higher levels. I'll just do a search for setup.

On the WorldEra level, the only thing that turns up is setup_from_snapshot. Ah, this is in contrast with what we just saw on the WorldLine level, where we set up a single word line from a single world point, as the starting point. Here we setup the whole system, starting from a whole snapshot.

Continuing the search for setup, here is the next function, now on the top level, World#setup_from_world and World#setup_from_snapshot.

Bob: The first one is almost trivial, since everything has already been set up in the previous run; all we have to do is to set a few appropriate variables and off we go. The second one, setup_from_snapshot, is the interesting one. And hey, look, on the fifth line, we are doing a form of startup, in startup_and_report_energy.

Alice: Now that is confusing. Definitely, in the next rewrite, I will insist on separating setting up and starting up on all levels!

Bob: But as you already said, not today. We still have to go through the WorldPoint class definition, remember! We've only looked at the first two methods.

9.3. Predicting and Correcting.

Alice: The rest should not contain too many surprises. The correct step looks just like what we already had in nbody_ind1.rb: all the coefficients are the same, as they should be, just the notation is somewhat different.

But wait a minute, if we have a corrector here, what happened with our predictor?

Bob: You have forgotten already? On the WorldLine level, we took a single step as follows:

   def take_step(era, dt_max)
     new_point = predict
     acc, jerk = era.acc_and_jerk(self, new_point)
     timescale = era.timescale(self, new_point)
     new_point.correct(@worldpoint.last, acc, jerk,
                               timescale, @dt_param, dt_max)
   end

which indeed contains a predict step and a correct step. The correct step is a direct invocation of WorldPoint#correct which we are now looking at. And the predict step is given on the WorldLine level as:

   def take_step(era, dt_max)
     new_point = predict
     acc, jerk = era.acc_and_jerk(self, new_point)
     timescale = era.timescale(self, new_point)
     new_point.correct(@worldpoint.last, acc, jerk,
                               timescale, @dt_param, dt_max)
   end

This means that we should not look for a method WorldPoint#predict but a method WorldPoint#extrapolate.

Alice: Hmm, not exactly what I would have expected. And I don't want to rely on memory. The logic of the code should speak for itself!

Perhaps we should call WorldPoint#extrapolaten WorldPoint#predict instead. But no, that won't do either, since there are other places where we only want to exrapolate. There we specifically want to call WorldPoint#extrapolate and it would be confusing to have to call WorldPoint#predict. Perhaps the best thing to do is to introduce the name WorldPoint#predict as just a one line method calling WorldPoint#extrapolate, as a kind of alias.

Bob: Not today! I'm sure that once you get started, your taste for abstraction and modularity and what not will get you carried away. We'll leave that for the next pass.

Where were we? The method admin is trivial, just does some bookkeeping. And then we have extrapolate, which we just discussed. It does indeed exactly what Body#predict_step did in nbody_ind1.rb.

The next method is interpolate. Ah, this is new.

Alice: Yes. We decided that we'd better be as accurate as we could reasonably be with our interpolation. After all, the main reason to interpolate is to construct snapshots that we then hand to a diagnostics routine.

Bob: I thought the main reason to construct snapshots was to be able to do force calculations.

Alice: True, but for those snapshots, we have to extrapolate particles; excuse me, predict particle positions. This double meaning of predicting and extrapolating is making things sound complicated.

Bob: I see. So while you're growing world lines dynamically, you're always taking snapshots that are a bit ahead of every known world point. That makes sense. But by the time you look back to do all the diagnostics for a completed era, you use interpolation.

Alice: Exactly. And in order to get the most accurate energy estimate, we may as well use all the information we have, from both world points, before and after the time at which we want to interpolated.

Bob: And if we count parameters, we have computed acceleration and jerk at each of the two world point. These four pieces of information can be transformed into four quantities at one the points, for convenience, to allow us to construct a Taylor series there, without loss of information.

This means that at that point we have the acceleration and jerk that were already available, and we compute the next two derivatives, the snap and crackle, and , from the two accelarations and jerks we have at hand. Got it!

9.4. Interpolation

Alice: I don't remember exactly how we derived the equations here. I'd feel more comfortable just to check them again.

Bob: I do remember that it was quite a bit of pen and paper work. I'm not eager to do this again, given that we know it works.

Alice: I agree, everything does seem to work, in the sense that we get good fourth-order scaling of errors, and that for small enough time steps we get an accuracy that is close what you can expect, using double precision. That is a pragmatic check, but I would like to check also on a more theoretical level.

This does not mean that we have to rederive everything here. All we have to do is to check that the equations given here for the position and velocity in between the two worldpoints actually reduce to the exact position and velocity of each worldpoint, in the limit of minimal and maximal interpolation during this interval.

Bob: Is that really enough? A straight line between the positions at the two worldpoints would not be a good interpolation, yet it would agree with the positions at either end.

Alice: You are right, I was a little to glib. I should have added that we need to check whether the appropriate derivates at either end have the correct values as well.

Bob: How many derivatives does it take to be `appropriate'?

Alice: Good question. Hmm. Let us derive the answer, from scratch. To start with, we are dealing with a fourth-order integration scheme, so we would like to see a fourth-order polynomial, an expression in powers of dt with dt**4 as the highest power. Now what are the precise conditions for the coefficients . . .

Ah, there is a simpler way. You see, the expressions for the corrected position and velocities, given by the methods correct, three methods above interpolate, provide fourth-order expressions for the position and velocities, in terms of powers of dt. We may as well reinterpret those equations as providing us the intermediate values for position and velocity as well!

Bob: They look mightily second-order in dt. But yes, that's because they have been cleverly written that way. If you express everything in terms of a Taylor series at the beginning point, the true fourth-order nature becomes clear. This is a trick similar to the one that is employed for the leapfrog: that scheme looks first-order, but is actually second-order. This one looks second-order, but is actually fourth-order.

That much is fine. But how do you want to recycle these equations to use them for interpolation?

Alice: The corrector makes one big step from one world point to the next. What we need is a method to make a smaller step, starting at the same world point, but moving a smaller distance in time. We can use these equations, just by shrinking the time dt.

Bob: But how? These expressions rely on the fact that you know the values of the acceleration and jerk both at the beginning point and end point. If you know keep the beginning point the same, but you place the end point somewhere in the middle of the interval, you have no information any more about the acceleration and jerk at that point. These two quantities had been computed, directly from Newton's equations of motion and its derivative, only at the two given world points.

Alice: True, we have to reconstruct the values for acceleration and jerk that we would have measured there, had we decided to do so. And we only have to do that to fourth order in accuracy in dt. What I did when I wrote the interpolator, was to evaluate the snap and crackle at the beginning of the interval, from the measured acceleration and jerk values . . .

Bob: . . . and then to use that snap and crackle in turn in a Taylor series to reconstruct the needed acceleration and jerk at the end of the interval.

Alice: I could have done that, but that would not be necessary: once you have the Taylor series at the beginning of the interval, to sufficiently high order, you already have the interpolation formula you were looking for!

Bob: So you've put those expressions in there, when we were creating this code. I wonder why I couldn't remember them at all!

Alice: You were rebooting the server, and I had a copy on my laptop, so I thought I might as well make myself useful too.

Bob: Useful indeed. But wait a minute, I see a fifth order term at the end of the right hand side expression for the interpolated position! It is (1/144.0)*crackle*dt**5. What is that doing there?

Alice: I remember we had a good reason to add that one, but I agree, strictly speaking, it is not needed. Well, let's just accept the fact that we put it there, and see what happens when we check the equations.

Bob: But not only it is not needed, it is inconsistent! If you differentiate the expression for wp.pos, you get the one for wp.vel, term by term, except for the last term. The coefficient for the fifth-order term in wp.pos should have been (1/120.0), shouldn't it, in order to lead to the proper last term in the velocity.

Alice: Yes, it should have been, if we had insisted to make the velocity to be the exact derivative of the position. But given that we only need the position to fourth order, this discrepancy does not necessarily spell trouble. But your objection is well taken; let's put in a note to remind ourselves to get back to it.

Bob: Fair enough.

9.5. Derivation

Alice: Let us start now by checking whether I did my home work correctly. If so, the method interpolate should reproduce the correct position and velocity values for the world point other, if we take dt to be exactly the difference in time dt = other.time - @time between the two world points.

Bob: But that is already the value we have chosen, in line 7! Where do we use the value t? Ah, I see, further down, in line 10, we change the value of dt to be t - time.

Alice: That's quite confusing. We'd better introduce a different notation for the first dt, perhaps call it , and use dt only for the last two lines.

Bob: That might be better, but as we said before, we're now analyzing the code, and let us postpone changes for later.

Alice: Anyway, to check whether the interpolator lands correctly on the end point, we actually equate these two forms of using dt. So let's get started. The values associated with the world point self, and given by instance variable names starting with a @, are the values at the beginning of the step. Let us indicate those with a subscript 0. The values at the end of the step, associated with the world point other, I will give a subscript 1. We then get the following notation, if we use for the interval dt, and translate the four lines for snap, crackle, wp.pos, and wp.vel:

     snap = (-6*(@acc - other.acc) - 2*(2*@jerk + other.jerk)*dt)/dt**2       
     crackle = (12*(@acc - other.acc) + 6*(@jerk + other.jerk)*dt)/dt**3      
     wp.pos = @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3 +     
              (1/24.0)*snap*dt**4 + (1/144.0)*crackle*dt**5                   
     wp.vel = @vel + @acc*dt + (1/2.0)*@jerk*dt**2 + (1/6.0)*snap*dt**3 +     
              (1/24.0)*crackle*dt**4                                          

We then get:

and

I will start by substituting the expressions in eq. (1) into the second line of eq. (2). This gives:

And this is exactly what our corrector correct tells us that the new velocity should be:

     @vel = old_point.vel + (1/2.0)*(old_point.acc + @acc)*dt +               
                            (1/12.0)*(old_point.jerk - @jerk) dt*2           

So far so good! Time to look at the first line of eq. (2), again substituting eq. (1):

This expression cannot be directly compared yet with the equivalent expression in our corrector, since there we used the difference between the old and the new velocity.

     @pos = old_point.pos + (1/2.0)*(old_point.vel + @vel)*dt +               
                            (1/12.0)*(old_point.acc - @acc) dt*2             

It is probably easiest to translate that expression directly into our notation, after which we can substitute eq. (2):

This is indeed exactly the same result as we obtained in eq. (4), and this concludes the proof that we have done exactly the right thing, in postulating eqs. (1) and (2). These equations were in turn what we had put in our method interpolate, so this proofs that we wrote that one correctly.

9.6. Glitches

Bob: Apart from the inconsistency in the fact that the derivative of the position is no longer exactly what it should be.

Alice: You mean the fact that the last term in the first line of eq. (2) has a coefficient of 1/444, rather than 1/120?

Bob: Yes. You told me you would get back to that.

Alice: Well, we now can find the reason. It would be nice to keep the expressions for position and velocity consistent, in that you retrieve the velocity by differentiating the position with respect to time. At the same time, it would be nice to guarantee that the interpolation formula gives you the correct values for both position and velocity, at the beginning and at the end of the interpolated time interval.

Bob: And we now see that we cannot do both.

Alice: Exactly. Note, however, that we can do both on the level of a fourth-order approximation. This inconsistency shows up only at fifth order in .

To be specific, in the first line of eq. (2), we could have left out the last term altogether. In that case, we would be fourth-order consistent, and you would be granted your wish: to this order of accuracy, indeed the velocity would be the time derivative of the position. But we our interpolation formula would be off by exactly that last term, when we would ask for the position of the interpolation at the end of the interpolated interval.

Bob: So in the end, it is mostly a matter of taste, whether we mind to have fifth-order discontinuities in interpolated position, as a function of time, in a fourth-order scheme, or whether we mind having the velocity and the time derivative of the position being off by a fifth-order term.

Alice: Well summarized. And I guess that is the penalty of having any finite precision: if you are accurate to order, you will make errors on the order, in all kinds of ways.

Bob: I'm happy with the current choice, to keep the interpolator fifth-order accurate in position. But it was a good exercise to check exactly what we did and why.
Previous ToC Up Next