2.3. Predicting and Correcting
Alice: Ah, there at the very top is our friend autonomous_step,
which invited us to come here in the first place.
Bob: Yes. This method takes
as a first argument the reference to the array of all bodies in the N-body
system. It needs that as a handle to reach all other particles. The second
parameter is the factor with which we multiply the time scale to
obtain the new time step. That part is the same as before, the only
difference being that each particle now computes its own time scale; the
parameter dt_param is a constant, an overall scaling factor that
the user can set at the beginning of the run.
Alice: And all that autonomous_step does, is take a step
from the present time time to the @next_time.
Bob: Exactly. And you see in the next definition why I have added
that extra time argument in the middle: when we need to synchronice all
particles, we can no longer give them the luxury to step autonomously.
Instead of letting them choose their own next time, we need to force them
to all converge at a fixed target time t, the middle argument of the
method forced_time_step. The only difference is that in the
autonomous case, a particle can determine itself where it wants to force
itself to go to, so to speak.
Alice: So to speak. Trying to explain code in words is always a tricky
business, but with the code in hand, I see what you mean. Okay, let's
move to where the real work is being done, in the method
take_one_step. For starters, you ask all other particles to
predict their position to time t.
Bob: All other particles, yes, and also the particle itself that is doing
the asking. Note that I have written ba.each, without taking
exception to self. So in one stroke I push all particles virtually
to the target time t, self as well as others.
Alice: And then you correct something, I presume, in correct_step.
What is it that you correct?
Bob: It is in correct_step that we finally encounter the full
Hermite algorithm. Let's compare this with the shared time step
case. There we sent the Hermite algorithm to individual particles
using the following method on the NBody level:
def hermite
calc(" @old_pos = @pos ")
calc(" @old_vel = @vel ")
calc(" @old_acc = acc(ba) ")
calc(" @old_jerk = jerk(ba) ")
calc(" @pos += @vel*dt + @old_acc*(dt*dt/2.0) + @old_jerk*(dt*dt*dt/6.0) ")
calc(" @vel += @old_acc*dt + @old_jerk*(dt*dt/2.0) ")
calc(" @vel = @old_vel + (@old_acc + acc(ba))*(dt/2.0) +
(@old_jerk - jerk(ba))*(dt*dt/12.0) ")
calc(" @pos = @old_pos + (@old_vel + vel)*(dt/2.0) +
(@old_acc - acc(ba))*(dt*dt/12.0) ")
end
Lines five and six of the body of this method do exactly what is now done
in predict_step, which I will show here again:
def predict_step(t)
if t > @next_time
STDERR.print "predict_step: t = ", t, " > @next_time = "
STDERR.print @next_time, "\n"
exit
end
dt = t - @time
@pred_pos = @pos + @vel*dt + @acc*(dt*dt/2.0) + @jerk*(dt*dt*dt/6.0)
@pred_vel = @vel + @acc*dt + @jerk*(dt*dt/2.0)
end
And the next two lines in the old code do exactly what is now done in
correct_step in the new code:
def correct_step(ba, t, dt_param)
dt = t - @time
new_acc, new_jerk = get_acc_and_jerk(ba)
new_vel = @vel + (@acc + new_acc)*(dt/2.0) + # first compute new_vel
(@jerk - new_jerk)*(dt*dt/12.0) # since new_vel is used
new_pos = @pos + (@vel + new_vel)*(dt/2.0) + # to compute new_pos
(@acc - new_acc)*(dt*dt/12.0)
@pos = new_pos
@vel = new_vel
@acc = new_acc
@jerk = new_jerk
@pred_pos = @pos
@pred_vel = @vel
@time = t
@next_time = @time + collision_time_scale(ba) * dt_param
end
Alice: I see. It is beginning to make sense for me now. And I am also
starting to remember how we derived the Hermite algorithm in the first place.
In the old code, two steps were involved. Starting from a given position,
we made a tentative move to a new position, in order to calculate the new
acceleration and jerk. Then we combined that result with the
knowledge of the old acceleration and jerk, and together we were able to
reconstruct the higher derivatives snap and crackle at the start of
the step. Finally, we built a Taylor series using the acceleration,
jerk, snap and crackle at the starting time, to make a more accurate
determination of the ending position.
Bob: Yes, that is exactly what happens. And what you called here a
tentative step is what I call predict_step, while what you called
a more accurate determination I call correct_step.
2.4. Shifting Perspective
Alice: That all makes a lot of sense. And the next four lines, following
the actual correction part of correct_step are the same as the
first four lines in our old hermite method. The only difference
is one of notation: in the nbody_sh1.rb we compared the old values
with the current values, while here in nbody_ind1.rb we
compare the current values with the new values.
Bob: Yes, I hadn't quite realized that I had implicitly shifted my
perspective by one time step. Why did I do that? Ah, of course.
Interesting! In the past we were dealing with shared time steps.
So as soon as we were ready to move, we could put ourselves one step
ahead, so to speak, in our imagination, while looking back on the
previous values as the old values.
However, in the individual time step case, most of the time we are
moving forward in time only in a virtual way: when particles predict
their position, in order to help another particle find its way, they
don't really move themselves. Since they stay at their old place, it
makes more sense to called the old place the current place, current
for each particle that is, and to call the predicted time the new
time.
Alice: So your shift in perspective makes sense. To predict, you have
to look ahead. But if you're walking in lock step, you move with the
whole group, and all you can do is look back.
Bob: You may be stretching the analogy here, but if it helps you remember
the notation, fine! Now the remaining two lines of correct_step
update the two time variables that belong to our particle: the new current
time @time, for which @pos and @vel are
actually calculated, is t, and the new @next_time is found by
calculating the next time step, as we have already seen.