Previous ToC Up Next

8. The WorldLine Class

8.1. A Matter of Identity

Alice: We started on the top level, with the World class, which you could view as the generalization of our old NBody class. Now, in the WorldLine class, we finally encounter the generalization of our old Body class.

Bob: I thought that WorldPoint generalized, or better took over, our Body notion.

Alice: Yes and no. It is all a question of identity. What is important about a particular body, say a particular star, perhaps with particular planets revolving around it . . .

Bob: . . . and programmers living on those planets . . .

Alice: . . . which would make it a very particular planet indeed. Anyway, such a star is instantiated at different times through different world points, but it is the particular world line that belongs to that particular star. So in terms of identity, it is really the WorldLine class that takes over here from the Body class.

Bob: While the positions and velocities and all that important type of detail can only be found by digging one level deeper, to WorldPoint. And if you project everything down the time direction, on a space like hypersurface, both a given world line, as well as the world points on it, all project onto a single particle.

Alice: Yes, that's it! Well, here is the listing for the WorldLine class:

 class WorldLine
 
   attr_accessor  :worldpoint
 
   def initialize
     @worldpoint = []
   end
 
   def grow(era, dt_max)
     new_point = take_step(era, dt_max)
     @worldpoint.push(new_point)
   end
 
   def setup(time)
     @worldpoint[0].setup(time)
   end
 
   def startup(era, dt_max)
     wp = @worldpoint[0]
     acc, jerk = era.acc_and_jerk(self, wp)
     timescale = era.timescale(self, wp)
     wp.startup(acc, jerk, timescale, @dt_param, dt_max)
   end
 
   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
 
   def predict
     wp = @worldpoint.last
     wp.extrapolate(wp.next_time)
   end
 
   def valid_extrapolation?(time)
     unless @worldpoint.last.time <= time and time <= @worldpoint.last.next_time
       raise "#{time} not in [#{@worldpoint.last.time}, #{@worldpoint.last.next_time}]"
     end
   end
 
   def valid_interpolation?(time)
     unless @worldpoint[0].time <= time and time <= @worldpoint.last.time
       raise "#{time} not in [#{@worldpoint[0].time}, #{@worldpoint.last.time}]"
     end
   end
 
   def take_snapshot_of_worldline(time)
     if time >= @worldpoint.last.time
       valid_extrapolation?(time)
       wp = @worldpoint.last.extrapolate(time)
       wp.body_id = @body_id if defined? @body_id
       wp
     else
       valid_interpolation?(time)
       @worldpoint.each_index do |i|
         if @worldpoint[i].time > time
           wp = @worldpoint[i-1].interpolate(@worldpoint[i], time)
           wp.body_id = @body_id if defined? @body_id
           return wp
         end
       end
     end
   end
 
   def next_worldline(time)
     valid_interpolation?(time)
     i = @worldpoint.size
     loop do
       i -= 1
       if @worldpoint[i].time <= time
         wl = self.clone
         wl.worldpoint = @worldpoint[i...@worldpoint.size]
         return wl
       end
     end
   end
 
   def setup_from_single_worldpoint(b, dt_param, time)
     @worldpoint[0] = b.to_worldpoint
     @body_id = @worldpoint[0].body_id if @worldpoint[0].body_id
     @dt_param = dt_param
     setup(time)
   end
 
 end

8.2. Evolving on a Third Level

Bob: I see no particular prefered entry point here. Why don't we just go down the list of methods.

Alice: Well, extend plays the role that evolve has played on the two higher levels; we could have called this method evolve as well: a world line evolves by extending itself. But it doesn't make any difference: extend happens to be the first method, following the initializer that assigns an empty array of world points.

Bob: All extend does is to take one step, and to add the resulting new world point to the growing array of world points. We should have written this as a oneliner method:

  def extend(era, dt_max)
    @worldpoint.push(take_step(era, dt_max))
  end
Alice: That would probably have been just a bit too terse for me; on the face of it, it's not clear that take_step returns a world point. But perhaps, when we get more familiar with this notation, and if we really stick to this notation, such a way of writing may well begin to look natural. For now, let's stick to two lines.

Bob: The next method, startup, does the first force calculation, and the determination of the time scale needed for finding the next time step size. All of this is done here for one particular particle, the one associated with our world line.

Then, in the method following that one, take_step does the real thing: like startup, it does a force calculation, but it does that calculation at a predicted position and velocity, and after that it uses the information given by the forces to do a corrector step, finishing off all the work. Presumably this means that everything is done as before, in nbody_ind1.rb, but now hidden in the internal workings of the WorldPoint class.

Alice: It must be, otherwise our code wouldn't have given the same results. Now predict does an extrapolation. But wait, we have to be careful. We have been talking about two different types of prediction.

8.3. Two Types of Prediction

Bob: Two types?

Alice: In order for one particle to step forwards, it has to predict its own position and velocity for its own desired time @next_time. Then, in order to calculate the forces that it will receive at this time from all other particles, it has to asked all other particles to predict their positions and velocities too. So there is the active prediction of one particle, for its own purposes, and the passive prediction of all other particles, obeying the wishes of the one particle that is, temporarily, in charge here.

Bob: Clearly, predict here is the active prediction, since it asks our particle to step forwards to its own @next_time.

Alice: But where does the passive prediction take place? Ah, that must be what is done in take_snapshot_of_worldline. That is the next method, after the two trivial, but important, checking methods valid_extrapolation? and valid_interpolation?. At least that seems like the most logical place. But I'd like to trace the actual flow of the logic. Let's take a step back, to take_step, so that we can follow exactly where the active and passive prediction takes place.

The active prediction happens directly in the first line. Now in the second line, we are already doing a force calculation, which implies that both and active and passive prediction has taken place already. So the passive prediction mechanism must be invoked as a side effect of the call era.acc_and_jerk(self, new_point). Let's go back to WorldEra, to inspect that that call is doing:

   def acc_and_jerk(wl, wp)
     take_snapshot_except(wl, wp.time).get_acc_and_jerk(wp.pos, wp.vel)
   end

Aha: first a snapshot is taken of all particles, except of the particle associated with the worldline that calls this function.

Bob: And a snapshot is constructed by asking all particles to predict their positions and velocities at the time of that snapshot. That must be the passive prediction part: it happens for all particles, except the calling particle, that has already predicted its position and velocity in the active prediction call.

Alice: That must be right, but still, I'd like to see specifically how that is done. Let's go once more to take_snapshot_except:

   def take_snapshot_except(wl, time)
     ws = WorldSnapshot.new
     ws.time = time
     @worldline.each do |w|
       s = w.take_snapshot_of_worldline(time)
       ws.body.push(s) unless w == wl
     end
     ws
   end

Aha, each world line is asked to execute take_snapshot_of_worldline, which we already suspected to be the executioner of the passive predict step. And here we have the proof! Okay, I'm happy, we now have all the pieces on the table, and we can see the flow of the logic.

8.4. Extrapolation and Interpolation

Bob: While we have identified the role that take_snapshot_of_worldline plays, we haven't yet looked at how it does what it does. The first part, after the if statement, makes sense to me. If we ask a particle to predict itself, passively as you said, to a time that is past the time at which it computed its last world point, we have to do an extrapolation. If not, we can interpolate between two completed worldpoints, one before and one after the time at which we order our particle to be predicted.

The extrapolation part is clear: first find the lst world point, and then extrapolate beyond. As an English sentence: extrapolate beyond the last world point, which means in Rubyese: worldpoint.last.extrapolate. And the particle that is handed back has to be branded with the right body_id number.

Alice: I don't like that image.

Bob: What image?

Alice: Branded. I can see a poor calf in front of me, being branded with a hot iron.

Bob: Aren't you a bit too sensitive?

Alice: It may be the body part of the terminology that triggered my additional imagination. It's hard to brand a world point.

Bob: Well, let's say that the body_id is a sticker that is stuck on the particle. In any case, thus equipped the world point is returned. Now what I am puzzled about is the question of what happens in the else part of the method. There is a each loop that is traversed. Why?

Alice: If the time at which we want to take a snapshot is not in the extrapolated part of the world line, it must be in the interpolated part, and to be more precise, it must be in between two world points, as you just said. The question is: in between which two. This loop loops over all points, to find the right two.

Bob: Ah, of course. And that is why we use each_index and not just each. The point is to traverse the world line in an ordered way. So we start at the beginning of the array, at the oldest point, @worldpoint[0], and then move forward to @worldpoint[1], @worldpoint[2], and so on, until we find a point with a time that is larger than the desired time. At that point, no pun intended, our previous point is still before the desired time. So the last two points in hand must straddle the desired time.

Alice: I think that pun was intended. But you've made your point, or points really: that's how it works. The point just before the desired time is @worldpoint[i-1], and it is given the point after the desired time, @worldpoint[i] as the argument for its interpolation method.

Bob: And the rest happens as before, in the if part: the body receives a sticker with its name on it.

Alice: Thank you!

8.5. Wrapping Up

Bob: The method next_worldline is called when we want to create a new era, using the method WorldEra#next_era, as we've seen before. Here we see how it is done, by cloning the existing world line, and returning a new one, with only a subset of the previous string of world points.

Alice: Array of world points.

Bob: Strung together, yes. And this clone method produces only a shallow clone.

Alice: I remember that this took me a while to get used to. clone only copies the instance variables of an object, but it does not copy the objects that the instance variables may be pointing to. So already in the case of an array, clone provides a new reference to the same old array.

Bob: Indeed: the world points themselves are unaffected. The variable @worldpoint that belongs to self, the objects that is calling this method, and the variable wl.worldpoint are different. However, immediately after the cloning, @worldpoint[0] of self and @worldpoint[0] of wl, that can be accessed through a call to wl.worldpoint, are identical.

Then, after the cloning operation, pruning takes place in the next line. That much I remember, but how exactly did we do that?

Ah, we should go back to the loop at the top.

Alice: Let's go to the top of next_worldline altogether. We first insist that the time at which we want to create a next worldline is a time at which interpolation can take place. Aha, yes, this method gets invoked exactly at the end of an era.

Bob: Ah, the end of an era . . .

Alice: Sounds nostalgic, doesn't it? So at that time we are guaranteed that all worldline have at least one worldpoint, and often more, sticking out in time beyond this end of our era. Hence we should be able to interpolate at that time. If not, something is seriously wrong, and it is good to check this.

Next we determine what seems like the size of a world point . . .

Bob: . . . which should be zero if it is a point.

Alice: You see, there really is something to say for giving this array the plural name worldpoints instead. So i starts of being the length of the whole array of worldpoints, that is, the number of world points in our world line. And in the loop, i is used to reference the array, while decreasing one unit at the time. In other words, we are stepping through the array, starting at the end, and walking toward the beginning of the array.

Bob: Which means that the time associated with each world point decreases. And we keep going back in time, until we find a world point that has a time that is smaller or equal to the time time at which we call our method.

Alice: The end of an era.

Bob: At it is at that point that we clone our world line, and give it a new array of world points, with the array starting at index i.

Alice: You mean, the new array is populated with the objects that were at locations i and higher in the old array. Of course, the new array starts at 0, and ends at an index that is i smaller in value that the previous ending index.

Bob: Yes, that is what I meant. And now I see the meaning: this new array is guaranteed to start with a world point that is at or before the time time, which is the end of the previous era . . .

Alice: . . . but at the same time the beginning of the new era. So the new era is guaranteed to have wordlines sticking out into the past, with at least one worldpoint either sticking out or sitting right on the boundary.

Bob: Yes. Amazing, isn't it, how complex something becomes when you have to put it into words. But yes, this is how it works.

Alice: And the final method setup_from_single_worldpoint does what it is supposed to do: it imports what might be a particle of class Body and converts it into a real Worldpoint. This is what we added at the end of the code, in an extension of the Body class, as follows:

 class Body
 
   def to_worldpoint
     wp = WorldPoint.new
     wp.restore_contents(self)
   end
 
 end

Bob: Ah yes, the method restore_contents is something we constructed within the file acsio.rb. It does a virtual output and input, to make it looks as if we were actually reading in a Worldpoint object, instead of a Body.

Alice: And after this class conversion, the world line assumes the same identity as the body we read in, by taking over the same value of its @body_id. And at the end, we call setup, which as we have seen passed the work on to the world point method with the same name. Done!
Previous ToC Up Next