9.6. Implementation
Alice: Indeed. Well, the rest is just a matter of coding up the
equations we just wrote out. Of course, in the predictor step,
there is no need to compute the predicted velocity. It is only the
predicted position that we need in order to obtain the acceleration
at the new time, and through that, the jerk.
What do you think of this:
def ms4pc(dt)
if @nsteps == 0
@ap3 = acc
rk4(dt)
elsif @nsteps == 1
@ap2 = acc
rk4(dt)
elsif @nsteps == 2
@ap1 = acc
rk4(dt)
@ap0 = acc
else
old_pos = pos
old_vel = vel
jdt = @ap0*(11.0/6.0) - @ap1*3 + @ap2*1.5 - @ap3/3.0
sdt2 = @ap0*2 - @ap1*5 + @ap2*4 - @ap3
cdt3 = @ap0 - @ap1*3 + @ap2*3 - @ap3
@pos += vel*dt + (@ap0/2.0 + jdt/6.0 + sdt2/24.0)*dt*dt
@ap3 = @ap2
@ap2 = @ap1
@ap1 = @ap0
@ap0 = acc
jdt = @ap0*(11.0/6.0) - @ap1*3 + @ap2*1.5 - @ap3/3.0
sdt2 = @ap0*2 - @ap1*5 + @ap2*4 - @ap3
cdt3 = @ap0 - @ap1*3 + @ap2*3 - @ap3
@vel = old_vel + @ap0*dt + (-jdt/2.0 + sdt2/6.0 - cdt3/24.0)*dt
@pos = old_pos + vel*dt + (-@ap0/2.0 + jdt/6.0 - sdt2/24.0)*dt*dt
end
end
Bob: That ought to work, with a little bit of luck, if we didn't
overlook any typos or logopos.
Alice: Logopos?
Bob: Errors in logic, as opposed to typing.
Alice: Yes, both are equally likely to have slipped in.
Let's try to run it and if it runs,
let's see how well we're doing:
|gravity> ruby integrator_driver4dpc_old.rb < euler.in
dt = 0.01
dt_dia = 0.1
dt_out = 0.1
dt_end = 0.1
method = ms4pc
at time t = 0, after 0 steps :
E_kin = 0.125 , E_pot = -1 , E_tot = -0.875
E_tot - E_init = 0
(E_tot - E_init) / E_init =-0
./ms4body_old.rb:158:in `ms4pc': undefined method `-@' for [-0.000500866850078996, -0.00502304344952675]:Vector (NoMethodError)
from ./ms4body_old.rb:22:in `send'
from ./ms4body_old.rb:22:in `evolve'
from integrator_driver4dpc_old.rb:21
9.7. A Logopo
Bob: And it is . . . . a logopo!
Alice: And an inscrutable logopo to boot: what does that mean,
undefined method `-@' ?!? Just when I grew fond of Ruby,
it gives us that kind of message -- just to keep us on our toes,
I guess.
Bob: Beats me. I know what the method - is, that is just
the subtraction method, which I have neatly defined for our Vector
class, while I was implementing the second-order version of our series
of multi-step integrators. I have no idea as to what the method
-@ could possibly be.
But let us look at the code first, maybe that will tell us something.
I'll scrutinize all the minus signs, to see whether we can find a clue
there. The middle part of the method ms4pc is the same as in
the predictor-only method ms4, and that one did not give any
errors. So we'll have to look in the last five lines. The first three
of those are verbatim copies of the lines above, which were already tested,
we the culprit really should lurk somewhere in the last two lines.
Hmm, minus sign, last two lines, ahaha! Unary minus!!
Alice: Unary minus?? As opposed to a dualistic minus?
Bob: As opposed to a binary minus. There are two different meanings
of a minus sign, logically speaking: the minus in
and the minus in
. In the first example, the minus
indicates that you are subtracting two numbers. You could write
that operation as
. In the second example,
you are dealing only with one number, and you take the negative value
of that number. You could write that as
.
So in the first case, you have a binary operator, with two arguments,
and in the second case, you have a unary operator, with only one argument.
Hence the name `unary minus.'
Alice: A binary minus, hey? I'm getting more and more confused with all
those different things that are called binaries. In astronomy we call
double stars binaries, in computer science they use binary trees in data
structures, and binary notation for the ones and zeros in a register,
and we talk about binary data as opposed to ASCII data, and now even a
lowly minus can become a binary. Oh well, I guess each use has its own
logic.
Bob: And many things in the universe come in pairs.
Alice: Even people trying to write a toy model and stumbling upon unary
minuses -- or unary mini? Anyway, I see now that the minus sign that I
added in front of the jerk in the middle of the line
@vel = old_vel + @ap0*dt + (-jdt/2.0 + sdt2/6.0 - cdt3/24.0)*dt
was a unary minus. All very logical. And yes, while we had defined a
binary minus before, we had not yet defined a unary minus.
Bob: So you committed a logopo.
Alice: And I could correct my logopo by writing jdt/(-2.0)
instead of -jdt/2.0. However, I don't like to do that, it looks
quite ugly.
Bob: The alternative is to add a unary minus to our Vector class.
Alice: I would like that much better! How do you do that?
Bob: Judging from the error message, Ruby seems to use the convention
that -@ stands for unary minus. But just to make sure, let me
look at the manual. Just a moment . . .
. . . and yes, my interpretation was correct. So let's just add it to
our collections of methods in the file vector.rb. How about
the following extension, within the Vector class:
def -@
self.map{|x| -x}.to_v
end
and for good measure, I may as well add a unary plus too:
def +@
self
end
Alice: I see, just in case you write something like r_new = +r_old.
Bob: Exactly. No need to do so, but also no need to let that give you
an error message if you do.
9.8. Testing
Alice: Do you think that your one-liner will work?
Bob: Only one way to find out! Let's check it:
|gravity> ruby integrator_driver4dpc.rb < euler.in
dt = 0.01
dt_dia = 0.1
dt_out = 0.1
dt_end = 0.1
method = ms4pc
at time t = 0, after 0 steps :
E_kin = 0.125 , E_pot = -1 , E_tot = -0.875
E_tot - E_init = 0
(E_tot - E_init) / E_init =-0
at time t = 0.1, after 10 steps :
E_kin = 0.129 , E_pot = -1 , E_tot = -0.875
E_tot - E_init = -9.56e-12
(E_tot - E_init) / E_init =1.09e-11
1.0000000000000000e+00
9.9499478008669873e-01 4.9916426232219237e-02
-1.0020902876280345e-01 4.9748796001291246e-01
Alice: What a charm! Not only does the unary minus fly, also the corrected
version turns out to be more than ten times more accurate than the
predictor-only version, the method ms4. Let's give a
ten times smaller time step:
|gravity> ruby integrator_driver4epc.rb < euler.in
dt = 0.001
dt_dia = 0.1
dt_out = 0.1
dt_end = 0.1
method = ms4pc
at time t = 0, after 0 steps :
E_kin = 0.125 , E_pot = -1 , E_tot = -0.875
E_tot - E_init = 0
(E_tot - E_init) / E_init =-0
at time t = 0.1, after 100 steps :
E_kin = 0.129 , E_pot = -1 , E_tot = -0.875
E_tot - E_init = -1.11e-15
(E_tot - E_init) / E_init =1.27e-15
1.0000000000000000e+00
9.9499478008955766e-01 4.9916426216148800e-02
-1.0020902860118561e-01 4.9748796006053242e-01
Even better, by a factor of about twenty this time. Great!
I now feel I understand the idea of predictor-corrector schemes.
We're building up a good laboratory for exploring algorithms!
Bob: Yes, I've never been able to plug and play so rapidly with different
integration schemes. Using a scripting language like Ruby definitely has
its advantages.