Previous | ToC | Up | Next |
Bob: Now that we've begun to modify my original N-body code,
I would like to write a third version, where I keep local copies of
the auxiliary variables, such as old_acc, half_vel,
and the like, as instance variables of the Body class. That should
make the notation within the integrator methods a lot simpler. I don't
like the notion of proliferating instance variables, though.
Alice: I don't think that would be such a bad thing. One could argue
that in object-oriented programming, it is appropriate to modify the
object if you use it for a different purpose. And especially in Ruby,
such a modification will be easy. You can always add a few lines in a
new class definition, and as you know, those new lines will be
directly added to the existing definitions.
Bob: Yeah, well, I still prefer to be parsimonious. But I'm curious
to see how much simpler the integrators will become. The first step
will be to add those auxiliary variables to the Body class. In the
first version of the code, I had:
In the alternative version, we left out the backward link:
And now let me put in the extra variables:
Ah, this is the only modification that we have to make to the Body class,
so you're right, it is not as bad as I thought. All other changes happen
only in the Nbody class!
Alice: But is this really necessary? I thought that in Ruby you can take
any class definition, whether you defined the class yourself or
whether you get it from a library, and extend that class by adding something
like
Alice: Well, you know I like modularity. And you yourself were just
complaining about the fact that a modification in the Nbody class forced
you to intrude in the earlier definition of the Body class. At the very
least, writing three definitions that way will make it more clear what is
going on.
In fact, I would probably prefer to put the standard Body class definition in
one file, and the Nbody class definition in another file. In that case,
you can add the necessary Body extensions to the second file,
together with the Nbody class definition that triggered the extensions.
Bob: I don't like to create a plethora of small files. But I see your point.
And come to think of it, there must be other solutions.
Alice: Well, the best thing would be if you could modify the definition
of the Body class from within Nbody, but I'm pretty sure that would violate
some Ruby rules.
Bob: Actually, I'm not so sure. In Ruby you can do almost anything. The
trick is to find out how. Hmmm. I have read something about a Ruby class
called Binding, and a method binding, both of which seem to be related
to creating the possibility to do just the sort of thing you brought up.
Alice: What do you know. Well, not now, I suggest.
Bob: I agree.
Alice: Let's start again with the forward Euler method, listing now all three
versions in order: your original one; the one we got by including an explicit
parameter in the acc call; and the new version you're writing now with extra
variables in the Body class:
Bob: I must say, it is very gratifying to see how much cleaner this last
version looks. Let me rewrite the other three integration methods as
well, and call this file rknbody4.rb. Here they all are:
Alice: A lot easier to read. A great improvement over the previous
two versions, though not quite as clean as the two-body version. Let's
put up the rk4 method for the old two-body code:
Bob: The difference is that in our latest version we still have to indicate
which body b it is that gets the instructions, hence the "b."
in front of each pos, vel, etc., call.
Alice: Well, I have an idea. Perhaps we can get rid of the "b."
in front of each physical variable, after all.
Bob: How? I mean, you have to loop over each particle! I can't see how
you can get rid of that.
Alice: You can't get rid of that, I agree, but you don't have to repeat
the fact that you are looping many times in one line, as we are doing now.
I think we can express the idea of a loop just once in each line.
Bob: I still don't see how you can do that. In the old two-body code,
we could get away with writing pos += vel*dt because there was
only one pos and one vel, but here we have little choice but writing
b.pos += b.vel*dt, at a minimum. And I already chose the shortest
name I could think of, b for the body whose pos gets updated!
Alice: My idea is to let the Nbody class give a command to the Body
class, specifying directly to do pos += vel*dt for particle b,
without repeating the b presence separately for pos and vel.
Bob: That would be nice, yes, but it still looks impossible to do,
since it would involve something sending that whole line to the Body
class!
Alice: Exactly!
Bob: I beg your pardon?
Alice: You got it! Let us send that line to the Body class! We
can ask the integration methods in Nbody to specify what needs to be
done, but instead of making the actual command calls, these methods can
write the commands into a string, and then pass that string down to
the Body class.
Bob: Hey, that is a great idea! I would never have thought about that.
Can you really do that? Well, of course you can. I guess I'm still
thinking too much in Fortran and C terms. But I must say, I'm beginning
to get really worried about speed; this may slow down execution of the
code even more. And Ruby is already slow to begin with.
Alice: Let us postpone efficiency discussions for later. I'm pretty
hopeful that we can find ways to speed things up later. In the worst case
we can switch back to a more traditional form in our final production code.
If we can make the prototyping process easier and more transparent, that
would already be a big gain. I care less about elegance of the final
version than I do about the intermediate test versions.
Bob: Well, okay, but we shouldn't wait too long in checking the
speed. I would feel more comfortable if we had some hard-nosed proof
that a Ruby-based N-body code can really compete with codes written in
C or Fortran. Meanwhile, I must admit, I do like the flexibility of Ruby.
What a trick, to implement message passing between the Nbody system
scheduler and the individual bodies, literally by passing messages in
the form of a string!
Alice: Yes, Ruby invites a different way of thinking. Even if you
could find a way to do such a trick in C++, in some convolved way, the
point is that it would not occur to you to do so, since the language
does not invite that way of thinking.
Bob: Quite convolved: for one thing, you would need to find a way to
implement a form of dynamic loading! C++ is a compiled language, not
an interpreted language like Ruby.
Alice: Ah, yes, of course, I'm already beginning to forget what it
means to work with compile cycles in prototyping code!
Bob: How did you get this idea, of using message passing?
Alice: When I browsed through some Ruby code on the web, I came
across something similar as what I just suggested, and then I realized
that that would be a very natural way of passing instructions between
classes.
Bob: Let's try it! I'll create yet another file, rknbody5.rb,
and see how we can implement your idea.
Alice: This time, I suggest we start on the level of the Nbody class.
In a wishful thinking sort of way, let us assume that we can tell a Body
named b to calculate something, by issuing a command b.calc(s),
where s is a string that will get executed by b. The forward Euler
method would then look like this:
Bob: And indeed, with no mention of b anymore within the string that is
passed as the third argument of calc.
Alice: The first two arguments are needed, because otherwise the Body
class will not know what to do with the string: it knows about its own
instance variables such as @pos, and it knows about the method
acc, but not about the argument to acc, which has to
be specified explicitly.
Bob: I see. What does ba stand for?
Alice: Body array. When calc executes the call, it will replace ba
by its first argument, @body. Similarly, it will replace dt
by dt. I'll show you in a moment. I hope it will all work. And I'm
pretty sure it will.
Bob: You used the calc method in the middle line of forward as well,
even though you could have used the simpler statement
which we used before, in the previous version.
Alice: Yes, but my intention was to not break the symmetry between the
lines. By treating all of them in the same way, your eye can be guided
to what is different on each line, forgetting the left half of each line,
which is the same in all cases. Here, let me write the other three methods
as well, and then it will become more clear how the actual strings
that contain the commands will stand out:
Bob: It's an improvement over the first two versions, which used
an index i. In the previous version, we could leave out that obnoxious
i index at the expense of introducing extra variables on the Body level.
But now we can have our cake and eat it: no more i and no more extra
variables with Body, I presume.
Alice: Correct! The content of the string effectively
will declare the extra variables for us when the string is evaluated in
the Body class. Here is how I would write the calc method for the
Body class:
Bob: Simplicity itself. I see now what you meant, when you described
the way the two parameters ba and dt were going to be substituted
in an actual call to calc.
Alice: Almost too simple to be true, but I think this is all correct.
Bob: Shall we move on? Or do you have a suggestion for further
improvements?
Alice: I must say that the integration methods still look too
cluttered for my taste. They miss the simple elegance and brevity of
expression of our previous version. For one thing, in that version we
did not have to break any statement up over two lines. What bothers
me especially is that for most statements, more than half of the line
gets repeated exactly.
Bob: I wonder whether we can do something about that.
Alice: I think we can. So far, we have introduced a calc function on
the Body level. How about introducing a second calc function on the
Nbody level? Let's create one more file, rknbody6.rb, in which
we give the Nbody class the following extra method:
Bob: Brevity indeed. I see what you mean. Forward Euler then becomes,
instead of
which we just wrote, quite a bit shorter as:
Alice: Exactly. And the following three methods become:
6. Returning to Simplicity
6.1. Extra Body Variables
attr_accessor :mass, :pos, :vel, :nb
attr_accessor :mass, :pos, :vel
attr_accessor :mass, :pos, :vel, :old_pos, :half_vel, :a0, :a1, :a2
6.2. Alternatives
class Body
. . .
end
Bob: Yes, that is true. Indeed, we have used that procedure to define
the method to_v for the Array class. However, in our code we
have two class definitions directly following each other in a single file.
The first one gives the Body class, and the second one gives the Nbody
class. I suppose I could have given three definitions: first a standard
one for Body, then the particular extension in the form of a second,
additional definition for Body as you just indicated, and finally
the Nbody class definition. But since everything is bundled in the
same file anyway, that seemed to me to be a rather unnecessary maneuver.
6.3. Forward
6.4. Clean Code
6.5. Sending a String
6.6. Wishful Thinking
6.7. Implementation
@body.each{|b| b.pos += b.vel*dt}
def calc(body_array, time_step, s)
ba = body_array
dt = time_step
eval(s)
end
6.8. Indirect String Sending
def calc(y,s)
@body.each{|b| b.calc(@body,y,s)}
end
Previous | ToC | Up | Next |