#!/usr/local/bin/ruby -w

require "nbody.rb"

module Integrator_force_default

  def setup_integrator
    @acc = @pos*0
  end

  def startup_force(wl, era)
    force(wl, era)
  end

  def force(wl, era)
    @acc = era.acc(wl, @pos, @time)
  end
end

module Integrator_pec_mode

include Integrator_force_default

  def integrator_step(wl, era)
    new_point = predict(@next_time)
    new_point.force(wl, era)
    new_point.correct(self, new_point.time - @time)
    new_point
  end
end

module Integrator_forward

  include Integrator_pec_mode

  def predict_pos_vel(dt)
    [ @pos + @vel*dt,  @vel + @acc*dt ]
  end

  def correct(old, dt)
    @pos, @vel = predict_pos_vel(dt)
  end

  def interpolate_pos_vel(wp, dt)
    predict_pos_vel(dt)
  end
end

module Integrator_forward_plus

  include Integrator_pec_mode

  def predict_pos_vel(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2,  @vel + @acc*dt ]
  end

  def correct(old, dt)
  end

  def interpolate_pos_vel(wp, dt)
    predict_pos_vel(dt)
  end
end

module Integrator_protohermite

  include Integrator_pec_mode

  attr_reader :acc

  def predict_pos_vel(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2,  @vel + @acc*dt ]
  end

  def correct(old, dt)
    @vel = old.vel + (1/2.0)*(old.acc + @acc)*dt
    @pos = old.pos + (1/2.0)*(old.vel + @vel)*dt
# same as leapfrog, apart from the an extra term in @pos, proportional to jerk:
#   @pos = old.pos + old.vel*dt + (1/4.0)*(old.acc + @acc)*dt^2
  end

  def interpolate_pos_vel(wp, dt)
    jerk = (wp.acc - @acc) / (wp.time - @time)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2,
      @vel + @acc*dt + (1/2.0)*jerk*dt**2 ]
  end
end

module Integrator_leapfrog

  include Integrator_pec_mode

  attr_reader :acc

  def predict_pos_vel(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2,  @vel + @acc*dt ]
  end

  def correct(old, dt)
    @pos = old.pos + old.vel*dt + (1/2.0)*old.acc*dt**2
    @vel = old.vel + (1/2.0)*(old.acc + @acc)*dt
  end

  def interpolate_pos_vel(wp, dt)
    jerk = (wp.acc - @acc) / (wp.time - @time)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2,
      @vel + @acc*dt + (1/2.0)*jerk*dt**2 ]
  end
end

module Integrator_multistep

  include Integrator_pec_mode

  attr_reader :acc, :acc_0_history, :time_history

  ORDER = 4

  def setup_integrator
    @acc_0_history = []
    @time_history = []
    @acc = [@pos*0]
  end

  def force(wl, era)
    @acc[0] = era.acc(wl, @pos, @time)
  end

  def intermediate_point(old_ip, i)
    nil
  end

  def predict_pos_vel(dt)
    [ @pos + taylor_increment([@vel, *@acc], dt),
      @vel + taylor_increment(@acc, dt)          ]
  end

  def new_order(order)
    [order + 1, ORDER].min
  end

  def correct(old, dt)
    order = new_order(old.acc_0_history.size + 1)
    @acc_0_history = [old.acc[0], *old.acc_0_history][0...(order-1)]
    @time_history = [old.time, *old.time_history][0...(order-1)]
    make_taylor(@acc, [@acc[0], *@acc_0_history], [@time, *@time_history])
    @vel = old.vel - taylor_increment(@acc, -dt)
    @pos = old.pos - taylor_increment([@vel, *@acc], -dt)
  end

  def interpolate_pos_vel(wp, dt)
    if (wp.next_time - @time).abs < (@next_time - wp.time).abs
      predict_pos_vel(dt)
    else
      wp.predict_pos_vel(dt + @time - wp.time)
    end
  end

  def make_taylor(a, d_0, t)
    dt = []
    t.each do |time|
      dt.push(@time - time)
    end
    order = t.size
    d = [d_0]
    for k in (1...order)
      d.push([])
      for i in (0...(order-k))
        d[k][i] = (d[k-1][i] - d[k-1][i+1])/(t[i]-t[i+k])
      end
    end
    c = [[1]] 
    for k in (1...order)
      c[k] = [0]
      for i in (1...k)
        c[k][i] = c[k-1][i-1] + dt[k-1] * c[k-1][i]
      end
      c[k][k] = 1
    end
    for j in (1...order)
      a[j] = a[0]*0
      for k in (j...order)
        a[j] += c[k][j] * d[k][0]
      end
      (1..j).each{|i| a[j] *= i}
    end
  end

  def taylor_increment(a, dt, number = 1)
    result = a[number-1]
    if number < a.size
      result += (1.0/(number+1))*taylor_increment(a, dt, number+1)
    end
    result*dt
  end
end

module Integrator_hermite

  include Integrator_pec_mode

  attr_reader :acc, :jerk

  def setup_integrator
    @acc = @pos*0
    @jerk = @pos*0
  end

  def force(wl, era)
    @acc, @jerk = era.acc_and_jerk(wl, @pos, @vel, @time)
  end

  def predict_pos_vel(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3,
      @vel + @acc*dt + (1/2.0)*@jerk*dt**2                       ]
  end

  def correct(old, dt)
    @vel = old.vel + (1/2.0)*(old.acc + @acc)*dt +
                     (1/12.0)*(old.jerk - @jerk)*dt**2
    @pos = old.pos + (1/2.0)*(old.vel + @vel)*dt +
                     (1/12.0)*(old.acc - @acc)*dt**2
  end

  def interpolate_pos_vel(wp, dt)
    tau = wp.time - @time
    snap = (-6*(@acc - wp.acc) - 2*(2*@jerk + wp.jerk)*tau)/tau**2
    crackle = (12*(@acc - wp.acc) + 6*(@jerk + wp.jerk)*tau)/tau**3
    [ @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,
      @vel + @acc*dt + (1/2.0)*@jerk*dt**2 + (1/6.0)*snap*dt**3 +
                       (1/24.0)*crackle*dt**4                        ]
  end
end

module Integrator_rk4n                  # not partitioned

  include Integrator_force_default

  attr_reader :acc, :jerk
  attr_writer :time

  def setup_integrator
    @acc = @pos*0
    @jerk = @pos*0
  end

  def startup_force(wl, era)
    @acc, @jerk = era.acc_and_jerk(wl, @pos, @vel, @time)
  end

  def force_on_pos_at_time(pos, time, wl, era)
    era.acc(wl, pos, time)
  end

  def derivative(pos, vel, time, wl, era)
    [ vel, force_on_pos_at_time(pos,time,wl, era) ]
  end

  def integrator_step(wl, era)
    dt = @next_time - @time
    k1 = derivative(@pos,@vel,@time,wl,era)
    k2 = derivative(@pos+0.5*dt*k1[0],@vel+0.5*dt*k1[1],
                       @time+0.5*dt,wl,era)
    k3 = derivative(@pos+0.5*dt*k2[0],@vel+0.5*dt*k2[1],
                       @time+0.5*dt,wl,era)
    k4 = derivative(@pos+dt*k3[0],@vel+dt*k3[1],@time+dt,wl,era)
    new_point = deep_copy
    new_point.pos += (k1[0]+2*k2[0]+2*k3[0]+k4[0])*dt/6
    new_point.vel += (k1[1]+2*k2[1]+2*k3[1]+k4[1])*dt/6
    new_point.time=@next_time
    new_point.force(wl,era)
    new_point.estimate_jerk(self)
    new_point
  end

  def estimate_jerk(old)
    @jerk = (old.acc - @acc) / (old.time - @time)
  end

  def predict_pos_vel(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3,
      @vel + @acc*dt + (1/2.0)*@jerk*dt**2                       ]
  end

  def interpolate_pos_vel(wp, dt)
    tau = wp.time - @time
    jerk = (-6*(@vel - wp.vel) - 2*(2*@acc + wp.acc)*tau)/tau**2
    snap = (12*(@vel - wp.vel) + 6*(@acc + wp.acc)*tau)/tau**3
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*jerk*dt**3 +
                       (1/24.0)*snap*dt**4,
      @vel + @acc*dt + (1/2.0)*jerk*dt**2 + (1/6.0)*snap*dt**3 ]
  end
end

module Integrator_rk2n                        # not partitioned

  include Integrator_force_default

  attr_reader :acc
  attr_writer :time

  def setup_integrator
    @acc = @pos*0
  end

  def force_on_pos_at_time(pos,time,wl, era)
    era.acc(wl, pos, time)
  end

  def derivative(pos,vel,time,wl,era)
    [ vel, force_on_pos_at_time(pos,time,wl, era) ]
  end

  def integrator_step(wl, era)
    dt = @next_time - @time
    k1 = derivative(@pos,@vel,@time,wl,era)
    k2 = derivative(@pos+dt*k1[0],@vel+dt*k1[1],@time+dt,wl,era)
    new_point = deep_copy
    new_point.pos += (k1[0]+k2[0])*dt/2
    new_point.vel += (k1[1]+k2[1])*dt/2
    new_point.time=@next_time
    new_point.force(wl,era)
    new_point
  end

  def predict_pos_vel(dt)
    [ @pos + @vel*dt,
      @vel           ]
  end

  def interpolate_pos_vel(wp, dt)
    tau = wp.time - @time
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2,
      @vel + @acc*dt                      ]
  end
end

module Integrator_rk2n_fsal                  # not partitioned

  include Integrator_force_default

  attr_accessor :acc
  attr_writer :time

  def setup_integrator
    @acc = @pos*0
  end

  def force_on_pos_at_time(pos,time,wl, era)
    era.acc(wl, pos, time)
  end

  def derivative(pos,vel,time,wl,era)
    [ vel, force_on_pos_at_time(pos,time,wl, era) ]
  end

  def integrator_step(wl, era)
    dt = @next_time - @time
    k1 = [ @vel, @acc ]
    k2 = derivative(@pos+dt*k1[0],@vel+dt*k1[1],@time+dt,wl,era)
    new_point = deep_copy
    new_point.pos += (k1[0]+k2[0])*dt/2
    new_point.vel += (k1[1]+k2[1])*dt/2
    new_point.time=@next_time
    new_point.acc = k2[1]
    new_point
  end

  def predict_pos_vel(dt)
    [ @pos + @vel*dt,
      @vel           ]
  end

  def interpolate_pos_vel(wp, dt)
    tau = wp.time - @time
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2,
      @vel + @acc*dt                      ]
  end
end

module Integrator_rk4                  # Abramowitz and Stegun's eq. 25.5.22

  include Integrator_force_default

  attr_reader :acc, :jerk
  attr_writer :time

  def setup_integrator
    @acc = @pos*0
    @jerk = @pos*0
  end

  def startup_force(wl, era)
    @acc, @jerk = era.acc_and_jerk(wl, @pos, @vel, @time)
  end

  def force_on_pos_at_time(pos, time, wl, era)
    era.acc(wl, pos, time)
  end

  def integrator_step(wl, era)
    dt = @next_time - @time
    k1 = @acc
    k2 = force_on_pos_at_time(@pos + 0.5*@vel*dt + 0.125*k1*dt**2,
                              @time + 0.5*dt, wl, era)
    k3 = force_on_pos_at_time(@pos + @vel*dt + 0.5*k2*dt**2,
                              @time + dt, wl, era)
    new_point = deep_copy
    new_point.pos += @vel*dt + (1.0/6)*(k1 + 2*k2)*dt**2
    new_point.vel += (1.0/6)*(k1 + 4*k2 + k3)*dt
    new_point.time = @next_time
    new_point.force(wl,era)
# replace the line above by the line below, to get a third-order FSAL scheme:
#    new_point.acc = k3
    new_point.estimate_jerk(self)
    new_point
  end

  def estimate_jerk(old)
    @jerk = (old.acc - @acc) / (old.time - @time)
  end

  def predict_pos_vel(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3,
      @vel + @acc*dt + (1/2.0)*@jerk*dt**2                       ]
  end

  def interpolate_pos_vel(wp, dt)
    tau = wp.time - @time
    jerk = (-6*(@vel - wp.vel) - 2*(2*@acc + wp.acc)*tau)/tau**2
    snap = (12*(@vel - wp.vel) + 6*(@acc + wp.acc)*tau)/tau**3
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*jerk*dt**3 +
                       (1/24.0)*snap*dt**4,
      @vel + @acc*dt + (1/2.0)*jerk*dt**2 + (1/6.0)*snap*dt**3 ]
  end
end

module Integrator_rk3

  include Integrator_force_default

  attr_reader :acc, :jerk
  attr_writer :time

  def setup_integrator
    @acc = @pos*0
    @jerk = @pos*0
  end

  def startup_force(wl, era)
    @acc, @jerk = era.acc_and_jerk(wl, @pos, @vel, @time)
  end

  def force_on_pos_at_time(pos, time, wl, era)
    era.acc(wl, pos, time)
  end

  def integrator_step(wl, era)
    dt = @next_time - @time
    k1 = @acc
    k2 = force_on_pos_at_time(@pos + (2.0/3)*@vel*dt + (2.0/9)*k1*dt**2,
                              @time + (2.0/3)*dt, wl, era)
    new_point = deep_copy
    new_point.pos += @vel*dt + 0.25*(k1 + k2)*dt**2
    new_point.vel += 0.25*(k1 + 3*k2)*dt
    new_point.time = @next_time
    new_point.force(wl,era)
    new_point.estimate_jerk(self)
    new_point
  end

  def estimate_jerk(old)
    @jerk = (old.acc - @acc) / (old.time - @time)
  end

  def predict_pos_vel(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3,
      @vel + @acc*dt + (1/2.0)*@jerk*dt**2                       ]
  end

  def interpolate_pos_vel(wp, dt)
    tau = wp.time - @time
    jerk = (-6*(@vel - wp.vel) - 2*(2*@acc + wp.acc)*tau)/tau**2
    snap = (12*(@vel - wp.vel) + 6*(@acc + wp.acc)*tau)/tau**3
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*jerk*dt**3 +
                       (1/24.0)*snap*dt**4,
      @vel + @acc*dt + (1/2.0)*jerk*dt**2 + (1/6.0)*snap*dt**3 ]
  end
end

module Integrator_cc # NOTE: ONLY WORKS NOW IF ALL BODIES USE THIS METHOD
                     # since gforce is not (yet) implemented for other methods
  include Integrator_force_default

  attr_accessor :acc
  attr_reader :jerk
  attr_writer :time

  def setup_integrator
    @acc = @pos*0
    @jerk = @pos*0
  end

  def startup_force(wl, era)
    @acc, @jerk = era.acc_and_jerk(wl, @pos, @vel, @time)
  end

  def force_on_pos_at_time(pos, time, wl, era)
    era.acc(wl, pos, time)
  end

  def gforce_on_pos_at_time(pos, acc, time, wl, era)
    era.gacc(wl, pos, acc, time)
  end

  def integrator_step(wl, era)
    dt = @next_time - @time
    k1 = @acc
    k2 = force_on_pos_at_time(@pos + 0.5*@vel*dt + (1.0/12)*k1*dt**2,
                              @time + 0.5*dt, wl, era)
    k2 += (1/48.0)*dt*dt*
          gforce_on_pos_at_time(@pos + 0.5*@vel*dt + (1.0/12)*k1*dt**2,
                                k2, @time + 0.5*dt, wl, era)
    new_point = deep_copy
    new_point.pos += @vel*dt + (1/6.0)*(k1 + 2*k2)*dt**2
    new_point.time = @next_time
    new_point.force(wl,era)
    k3 = new_point.acc
    new_point.vel += (1/6.0)*(k1 + 4*k2 + k3)*dt
    new_point.estimate_jerk(self)
    new_point
  end

  def estimate_jerk(old)
    @jerk = (old.acc - @acc) / (old.time - @time)
  end

  def predict_pos_vel(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3,
      @vel + @acc*dt + (1/2.0)*@jerk*dt**2                       ]
  end

  def predict_pos_vel_acc(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3,
      @vel + @acc*dt + (1/2.0)*@jerk*dt**2,
      @acc + @jerk*dt                                            ]
  end

  def interpolate_pos_vel(wp, dt)
    tau = wp.time - @time
    jerk = (-6*(@vel - wp.vel) - 2*(2*@acc + wp.acc)*tau)/tau**2
    snap = (12*(@vel - wp.vel) + 6*(@acc + wp.acc)*tau)/tau**3
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*jerk*dt**3 +
                       (1/24.0)*snap*dt**4,
      @vel + @acc*dt + (1/2.0)*jerk*dt**2 + (1/6.0)*snap*dt**3   ]
  end

  def interpolate_pos_vel_acc(wp, dt)
    tau = wp.time - @time
    jerk = (-6*(@vel - wp.vel) - 2*(2*@acc + wp.acc)*tau)/tau**2
    snap = (12*(@vel - wp.vel) + 6*(@acc + wp.acc)*tau)/tau**3
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*jerk*dt**3 +
                       (1/24.0)*snap*dt**4,
      @vel + @acc*dt + (1/2.0)*jerk*dt**2 + (1/6.0)*snap*dt**3,
      @acc + jerk*dt + (1/2.0)*snap*dt**2                        ]
  end
end

module Integrator_cco # NOTE: ONLY WORKS NOW IF ALL BODIES USE THIS METHOD
                      # since gforce is not (yet) implemented for other methods
  include Integrator_force_default

  attr_accessor :acc
  attr_reader :jerk
  attr_writer :time

  def setup_integrator
    @acc = @pos*0
    @jerk = @pos*0
  end

  def startup_force(wl, era)
    @acc, @jerk = era.acc_and_jerk(wl, @pos, @vel, @time)
  end

  def force_on_pos_at_time(pos, time, wl, era)
    era.acc(wl, pos, time)
  end

  def gforce_on_pos_at_time(pos, acc, time, wl, era)
    era.gacc(wl, pos, acc, time)
  end

  def integrator_step(wl, era)
    dt = @next_time - @time
    np = deep_copy
    np.vel += (1/6.0)*np.acc*dt
    np.pos += 0.5*np.vel*dt
    np.acc = np.force_on_pos_at_time(np.pos, @time + 0.5*dt, wl, era)
    np.acc += (1/48.0)*dt*dt*np.gforce_on_pos_at_time(np.pos, np.acc,
                                                      @time + 0.5*dt, wl, era)
    np.vel += (2/3.0)*np.acc*dt
    np.pos += 0.5*np.vel*dt
    np.time = @next_time
    np.force(wl,era)
    np.vel += (1/6.0)*np.acc*dt
    np.estimate_jerk(self)
    np
  end

  def estimate_jerk(old)
    @jerk = (old.acc - @acc) / (old.time - @time)
  end

  def predict_pos_vel(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3,
      @vel + @acc*dt + (1/2.0)*@jerk*dt**2                       ]
  end

  def predict_pos_vel_acc(dt)
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*@jerk*dt**3,
      @vel + @acc*dt + (1/2.0)*@jerk*dt**2,
      @acc + @jerk*dt                                            ]
  end

  def interpolate_pos_vel(wp, dt)
    tau = wp.time - @time
    jerk = (-6*(@vel - wp.vel) - 2*(2*@acc + wp.acc)*tau)/tau**2
    snap = (12*(@vel - wp.vel) + 6*(@acc + wp.acc)*tau)/tau**3
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*jerk*dt**3 +
                       (1/24.0)*snap*dt**4,
      @vel + @acc*dt + (1/2.0)*jerk*dt**2 + (1/6.0)*snap*dt**3   ]
  end

  def interpolate_pos_vel_acc(wp, dt)
    tau = wp.time - @time
    jerk = (-6*(@vel - wp.vel) - 2*(2*@acc + wp.acc)*tau)/tau**2
    snap = (12*(@vel - wp.vel) + 6*(@acc + wp.acc)*tau)/tau**3
    [ @pos + @vel*dt + (1/2.0)*@acc*dt**2 + (1/6.0)*jerk*dt**3 +
                       (1/24.0)*snap*dt**4,
      @vel + @acc*dt + (1/2.0)*jerk*dt**2 + (1/6.0)*snap*dt**3,
      @acc + jerk*dt + (1/2.0)*snap*dt**2                        ]
  end
end

class WorldPoint

  ACS_OUTPUT_NAME = "Body"

  MAX_TIMESTEP_INCREMENT_FACTOR = 2

  attr_accessor :pos, :vel, :next_time

  attr_reader :mass, :time,
              :nsteps, :minstep, :maxstep

  def setup(method, dt_param, time)
    extend(eval("Integrator_#{method}"))
    setup_integrator
    setup_admin(dt_param, time)
  end

  def setup_admin(dt_param, time)
    @dt_param = dt_param
    @time = @next_time = time
    @nsteps = 0
    @minstep = VERY_LARGE_NUMBER
    @maxstep = 0
  end

  def startup(wl, era, dt_max, init_timescale_factor)
    startup_force(wl, era)
    timescale = era.timescale(wl, self)
    startup_admin(timescale * init_timescale_factor, dt_max)
    true
  end

  def startup_admin(timescale, dt_max)
    dt = timescale * @dt_param
    dt = dt_max if dt > dt_max
    @next_time = @time + dt
  end

  def step(wl, era, dt_max)
    new_point = integrator_step(wl,era)
    timescale = era.timescale(wl, new_point)
    new_point.step_admin(@time, timescale, dt_max)
    new_point
  end

  def step_admin(old_time, timescale, dt_max)
    old_dt = @time - old_time
    @maxstep = old_dt if @maxstep < old_dt
    @minstep = old_dt if @minstep > old_dt
    @nsteps = @nsteps + 1
    new_dt = timescale * @dt_param
    new_dt = dt_max if new_dt > dt_max
    timestep_increment_factor = (new_dt/old_dt).abs
    if timestep_increment_factor > MAX_TIMESTEP_INCREMENT_FACTOR
      new_dt = old_dt * MAX_TIMESTEP_INCREMENT_FACTOR
    end
    @next_time = @time + new_dt
  end

  def predict(t)
    extrapolate(t)
  end

  def extrapolate(t)
    wp = deep_copy
    wp.pos, wp.vel = predict_pos_vel(t - @time)
    wp.extrapolate_admin(t)
    wp
  end

  def gacc_extrapolate(t)
    wp = deep_copy
    wp.pos, wp.vel, wp.acc = predict_pos_vel_acc(t - @time)
    wp.extrapolate_admin(t)
    wp
  end

  def extrapolate_admin(t)
    @time = t
  end

  def interpolate(other, t)
    wp = deep_copy
    wp.pos, wp.vel = interpolate_pos_vel(other, t-@time)
    wp.interpolate_admin(self, other, t)
    wp
  end

  def gacc_interpolate(other, t)
    wp = deep_copy
    wp.pos, wp.vel, wp.acc = interpolate_pos_vel_acc(other, t-@time)
    wp.interpolate_admin(self, other, t)
    wp
  end

  def interpolate_admin(wp1, wp2, t)
    @minstep = [wp1.minstep, wp2.minstep].min
    @maxstep = [wp1.maxstep, wp2.maxstep].max
    @nsteps = [wp1.nsteps, wp2.nsteps].max
    @time = t
  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

class WorldLine

  attr_accessor  :worldpoint

  def initialize
    @worldpoint = []
  end

  def setup(b, method, dt_param, time)
    @worldpoint[0] = b.to_worldpoint
    @worldpoint[0].setup(method, dt_param, time)
  end

  def startup(era, dt_max, init_timescale_factor)
    @worldpoint[0].startup(self, era, dt_max, init_timescale_factor)
  end

  def grow(era, dt_max)
    @worldpoint.push(@worldpoint.last.step(self, era, dt_max))
  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 acc(pos, t)
    p = take_snapshot_of_worldline(t)
    r = p.pos - pos
    r2 = r*r
    r3 = r2*sqrt(r2)
    p.mass*r/r3
  end

  def gacc(pos, acc, t)
    p = take_gacc_snapshot_of_worldline(t)
    r = p.pos - pos
    r2 = r*r
    r3 = r2*sqrt(r2)
#    a = p.acc - acc    this is not correct; only works for shared time steps
    a = - acc
    2*(p.mass/r3)*(a - 3*((r*a)/r2)*r)
  end

  def acc_and_jerk(pos, vel, t)
    p = take_snapshot_of_worldline(t)
    r = p.pos - pos
    r2 = r*r
    r3 = r2*sqrt(r2)
    v = p.vel - vel
    [ p.mass*r/r3 , p.mass*(v-3*(r*v/r2)*r)/r3 ]
  end

  def t_at_or_after(t)
    @worldpoint.each do |p|
      return p.time if p.time >= t
    end
    return nil
  end

  def census(t_start, t, t_overshoot)
    n = ([0]*5).to_v
    @worldpoint.each do |p|
      pt = p.time
      if p.nsteps > 0
        case
        when pt < t_start      : n[0] = p.nsteps
        when pt == t_start     : n[1] += 1
        when pt < t            : n[2] += 1
        when pt == t           : n[3] += 1
        when pt <= t_overshoot : n[4] += 1
        end
      end
    end
    n
  end

  def prune(k, t_start, t_end)
    new_worldpoint = []      # protect the original; not yet cleanly modular
    @worldpoint.each do |wp|
      if (wp.nsteps == 0 or t_start < wp.time) and wp.time <= t_end
        if wp.nsteps%k == 0
          new_worldpoint.push(wp)
        end
      end
    end
    @worldpoint = new_worldpoint
    self
  end

  def take_snapshot_of_worldline(time)
    if time >= @worldpoint.last.time
      valid_extrapolation?(time)
      @worldpoint.last.extrapolate(time)
    else
      valid_interpolation?(time)
      i = @worldpoint.size
      loop do
        i -= 1
        if @worldpoint[i].time <= time
          return @worldpoint[i].interpolate(@worldpoint[i+1], time)
        end
      end
    end
  end

  def take_gacc_snapshot_of_worldline(time)
    if time >= @worldpoint.last.time
      valid_extrapolation?(time)
      @worldpoint.last.gacc_extrapolate(time)
    else
      valid_interpolation?(time)
      i = @worldpoint.size
      loop do
        i -= 1
        if @worldpoint[i].time <= time
          return @worldpoint[i].gacc_interpolate(@worldpoint[i+1], time)
        end
      end
    end
  end

  def next_worldline(time)
    valid_interpolation?(time)
    i = @worldpoint.size
    loop do
      i -= 1
      if @worldpoint[i].time <= time
        wl = WorldLine.new
        wl.worldpoint = @worldpoint[i...@worldpoint.size]
        return wl
      end
    end
  end
end

class WorldEra

  attr_accessor  :start_time, :end_time, :worldline
  attr_reader :cpu_overrun_flag, :cpu_time_used_in_last_evolve_call

  def initialize
    @worldline = []
    @cpu_overrun_flag = false
  end

  def setup(ss, method, dt_param, dt_era)
    @start_time = ss.time
    @end_time = @start_time + dt_era
    ss.body.each do |b|
      wl = WorldLine.new
      wl.setup(b, method, dt_param, ss.time)
      @worldline.push(wl)
    end
  end

  def startup(dt_max, init_timescale_factor)
    list = @worldline
    while list.size > 0
      new_list = []
      list.each do |wl|
        new_list.push(wl) unless wl.startup(self,dt_max, init_timescale_factor)
      end
      list = new_list
    end
  end

  def evolve(dt_era, dt_max, cpu_time_max, shared_flag)
    @cpu_overrun_flag = false
    cpu_time = Process.times.utime
    while wordline_with_minimum_interpolation.worldpoint.last.time < @end_time
      unless shared_flag
        wordline_with_minimum_extrapolation.grow(self, dt_max)
      else
        t = wordline_with_minimum_extrapolation.worldpoint.last.next_time
        @worldline.each do |w|
          w.worldpoint.last.next_time = t
          w.grow(self, dt_era)
        end
      end
      if Process.times.utime - cpu_time > cpu_time_max
        @cpu_overrun_flag = true
        return self
      end
    end
    @cpu_time_used_in_last_evolve_call = Process.times.utime - cpu_time
    next_era(dt_era)
  end

  def acc(wl, pos, t)
    acc = pos*0                            # null vectors of the correct length
    @worldline.each do |w|
      acc += w.acc(pos, t) unless w == wl
    end
    acc 
  end

  def gacc(wl, pos, acc, t)
    gacc = pos*0                           # null vectors of the correct length
    @worldline.each do |w|
      gacc += w.gacc(pos, acc, t) unless w == wl
    end
    gacc 
  end

  def acc_and_jerk(wl, pos, vel, t)
    acc = jerk = pos*0                  # null vectors of the correct length
    @worldline.each do |w|
      unless w == wl
        da, dj = w.acc_and_jerk(pos, vel, t)
        acc += da
        jerk += dj
      end
    end
    [acc, jerk]
  end

  def snap_and_crackle(wl, wp)
    take_snapshot_except(wl, wp.time).get_snap_and_crackle(wp.pos, wp.vel,
                                                           wp.acc, wp.jerk)
  end

  def timescale(wl, wp)
    take_snapshot_except(wl, wp.time).collision_time_scale(wp.mass,
                                                           wp.pos, wp.vel)
  end

  def take_snapshot(time)
    take_snapshot_except(nil, time)
  end

  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

  def report_energy
    take_snapshot(@start_time).total_energy
  end

  def write_diagnostics(t, initial_energy, unscheduled_output = false)
    STDERR.print "  < unscheduled > " if unscheduled_output
    STDERR.print "t = #{sprintf("%g", t)} "
    cen = census(t)
    STDERR.print "(after #{cen[0..2].inject{|n,dn|n+dn}}, "
    STDERR.print "#{cen[3]}, #{cen[4]} steps <,=,> t)\n"
    take_snapshot(t).write_diagnostics(initial_energy)
  end

  def wordline_with_minimum_extrapolation
    t = VERY_LARGE_NUMBER
    wl = nil
    @worldline.each do |w|
      if t > w.worldpoint.last.next_time
        t = w.worldpoint.last.next_time
        wl = w
      end
    end
    wl
  end

  def wordline_with_minimum_interpolation
    t = VERY_LARGE_NUMBER
    wl = nil
    @worldline.each do |w|
      if t > w.worldpoint.last.time
        t = w.worldpoint.last.time
        wl = w
      end
    end
    wl
  end

  def next_era(dt_era)
    e = WorldEra.new
    e.start_time = @end_time
    e.end_time = @end_time + dt_era
    @worldline.each do |wl|
      e.worldline.push(wl.next_worldline(e.start_time))
    end
    e
  end

  def census(t = @end_time)
    tmax = @worldline.map{|w| w.t_at_or_after(t)}.inject{|tt, tm| [tt,tm].max}
    @worldline.map{|w| w.census(@start_time, t, tmax)}.inject{|n, dn| n+dn}
  end

  def prune(k)
    new_worldline = []      # protect the original; not yet cleanly modular
    @worldline.each do |w|
      new_worldline.push(w.prune(k, @start_time, @end_time))
    end
    @worldline = new_worldline
    self
  end
end

module Output

  def diagnostics_and_output(c, at_startup)
    if at_startup
      t_target = @time
    else
      t_target = [@t_end, @era.end_time].min
    end
    output(c, t_target, at_startup)
    diagnostics(t_target, c.dt_dia)
  end

  def diagnostics(t_target, dt_dia)
    dia_output = false
    while @t_dia <= t_target
      @era.write_diagnostics(@t_dia, @initial_energy)
      @t_dia += dt_dia
      dia_output = true
    end
    dia_output
  end

  def unscheduled_diagnostics(dt_dia)
    t_era = @era.wordline_with_minimum_interpolation.worldpoint.last.time
    unless diagnostics(t_era, dt_dia)
      @era.write_diagnostics(t_era, @initial_energy, true)
    end
  end

  def output(c, t_target, at_startup)
    if (k = c.prune_factor) > 0
      pruned_dump(c, at_startup)
    else
      timed_output(c, t_target, at_startup)
    end
  end

  def pruned_dump(c, at_startup)
    unless at_startup
      @era.clone.prune(c.prune_factor).acs_write($stdout, false, c.precision,
                                                 c.add_indent)
    end
  end

  def timed_output(c, t_target, at_startup)
    while @t_out <= t_target
      if c.output_at_startup_flag or not at_startup
        if c.world_output_flag
          acs_write($stdout, false, c.precision, c.add_indent)
        else
          @era.take_snapshot(@t_out).acs_write($stdout, true,
                                               c.precision, c.add_indent)
        end
      end
      @t_out += c.dt_out
    end
  end
end

class World

include Output

  def World.admit(file, c)
    object = acs_read([self, WorldSnapshot], file)
    if object.class == self
      object.continue_from_world(c)
      return object
    elsif object.class == WorldSnapshot
      w = World.new
      w.setup(object, c)
      w.startup(c)
      return w
    else
      raise "#{object.class} not recognized"
    end
  end

  def continue_from_world(c)
    diagnostics_and_output(c, true)
    @t_out += c.dt_out
    @t_end += c.dt_end
    @dt_max = c.dt_era * c.dt_max_param
    @new_era = @era.next_era(c.dt_era)
    @old_era, @era = @era, @new_era
  end

  def setup(ss, c)
    @era = WorldEra.new
    @era.setup(ss, c.integration_method, c.dt_param, c.dt_era)
    @dt_max = c.dt_era * c.dt_max_param
    @time = @era.start_time
    @t_dia = @time
    @t_out = @time
    @t_end = @time + c.dt_end
  end

  def startup(c)
    @era.startup(@dt_max, c.init_timescale_factor)
    @initial_energy = @era.report_energy
    diagnostics_and_output(c, true)
  end

  def evolve(c)
    cpu_time_max = c.cpu_time_max
    while @era.start_time < @t_end
      tmp_era = @era.evolve(c.dt_era, @dt_max, cpu_time_max, c.shared_flag)
      if tmp_era.cpu_overrun_flag
        unscheduled_diagnostics(c.dt_dia)
        cpu_time_max = c.cpu_time_max
      else
        @new_era = tmp_era
        @time = @era.end_time
        if diagnostics_and_output(c, false)
          cpu_time_max = c.cpu_time_max
        else
          cpu_time_max -= @era.cpu_time_used_in_last_evolve_call
        end
        @old_era, @era = @era, @new_era
      end
    end
  end
end

class WorldSnapshot < NBody

  attr_accessor :time

  def initialize
    super
    @time = 0.0
  end

  def get_snap_and_crackle(pos, vel, acc, jerk)
    snap = crackle = pos*0                 # null vectors of the correct length
    @body.each do |b|
      r = b.pos - pos
      r2 = r*r
      r3 = r2*sqrt(r2)
      v = b.vel - vel
      a = b.acc - acc
      j = b.jerk - jerk
      c1 = r*v/r2
      c2 = (v*v + r*a)/r2 + c1*c1
      c3 = (3*v*a + r*j)/r2 + c1*(3*c2 - 4*c1*c1)
      d_acc = b.mass*r/r3
      d_jerk = b.mass*v/r3 - 3*c1*d_acc
      d_snap = b.mass*a/r3 - 6*c1*d_jerk - 3*c2*d_acc
      d_crackle = b.mass*j/r3 - 9*c1*d_snap - 9*c2*d_jerk - 3*c3*d_acc
      snap += d_snap
      crackle += d_crackle
    end
    [snap, crackle]
  end    

  def collision_time_scale(mass, pos, vel)
    time_scale_sq = VERY_LARGE_NUMBER              # square of time scale value
    @body.each do |b|
      r = b.pos - pos
      v = b.vel - vel
      r2 = r*r
      v2 = v*v + 1.0/VERY_LARGE_NUMBER          # always non-zero, for division
      estimate_sq = r2 / v2              # [distance]^2/[velocity]^2 = [time]^2
      if time_scale_sq > estimate_sq
        time_scale_sq = estimate_sq
      end
      a = (mass + b.mass)/r2
      estimate_sq = sqrt(r2)/a           # [distance]/[acceleration] = [time]^2
      if time_scale_sq > estimate_sq
        time_scale_sq = estimate_sq
      end
    end
    sqrt(time_scale_sq)                  # time scale value
  end

  def kinetic_energy
    e = 0
    @body.each{|b| e += b.kinetic_energy}
    e
  end

  def potential_energy
    e = 0
    @body.each{|b| e += b.potential_energy(@body)}
    e/2                                # pairwise potentials were counted twice
  end

  def total_energy
    kinetic_energy + potential_energy
  end

  def write_diagnostics(initial_energy)
    e0 = initial_energy
    ek = kinetic_energy
    ep = potential_energy
    etot = ek + ep
    STDERR.print <<-END
          E_kin = #{sprintf("%.3g", ek)} ,\
     E_pot =  #{sprintf("%.3g", ep)} ,\
      E_tot = #{sprintf("%.3g", etot)}
          E_tot - E_init = #{sprintf("%.3g", etot - e0)}
          (E_tot - E_init) / E_init = #{sprintf("%.3g", (etot - e0)/e0 )}
    END
  end
end

class Body

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

options_text = <<-END

  Description: Individual Time Step, Individual Integration Scheme Code
  Long description:
    This program evolves an N-body code with a fourth-order Hermite Scheme,
    using individual time steps.  Note that the program can be forced to let
    all particles share the same (variable) time step with the option -a.

    This is a test version, for the ACS data format

    (c) 2005, Piet Hut, Jun Makino; see ACS at www.artcompsi.org

    example:
    kali mkplummer.rb -n 4 -s 1 | kali #{$0} -t 1


  Short name:           -g
  Long name:            --integration_method
  Value type:           string
  Default value:        hermite
  Variable name:        integration_method
  Description:          Choice of integration method
  Long description:
    This option chooses the integration method.  The user is expected to
    provide a string with the name of the method, for example "leapfrog",
    "hermite".


  Short name:           -c
  Long name:            --step_size_control
  Value type:           float
  Default value:        0.01
  Variable name:        dt_param
  Description:          Determines the time step size
  Long description:
    This option sets the step size control parameter dt_param << 1.  Before
    each new time step, we first calculate the time scale t_scale on which
    changes are expected to happen, such as close encounters or significant
    changes in velocity.  The new time step is then given as the product
    t_scale * dt_param << t_scale.


  Short name:           -f
  Long name:            --init_timescale_factor
  Value type:           float
  Default value:        0.01
  Variable name:        init_timescale_factor
  Description:          Initial timescale factor
  Long description:
    This option allows the user to determine how extra small the initial
    timesteps are, for all particles.  In order to allow a safe startup
    for high-order multistep methods, all particles are forced to start
    their integration with a time scale that is significantly smaller
    than what they normally would be, by a factor "init_timescale_factor".


  Short name:           -e
  Long name:            --era_length
  Value type:           float
  Default value:        0.0078125
  Variable name:        dt_era
  Description:          Duration of an era
  Long description:
    This option sets the time interval between begin and end of an era,
    which is the period in time that contains a bundle of world lines,
    all of which are guaranteed to extend beyond the era boundaries with
    by at least one world point in either direction.  In other words, each
    world line has an earliest world point before the beginning of the era,
    and a latest world point past the end of the era.  This guarantees
    accurate interpolation at each time within an era.


  Short name:           -m
  Long name:            --max_timestep_param
  Value type:           float
  Default value:        1
  Variable name:        dt_max_param
  Description:          Maximum time step (units dt_era)
  Long description:
    This option sets an upper limit to the size dt of a time step,
    as the product of the duration of an era and this parameter:
    dt <= dt_max = dt_era * dt_max_param .


  Short name:           -d
  Long name:            --diagnostics_interval
  Value type:           float
  Default value:        1
  Variable name:        dt_dia
  Description:          Diagnostics output interval
  Long description:
    The time interval between successive diagnostics output.
    The diagnostics include the kinetic and potential energy,
    and the absolute and relative drift of total energy, since
    the beginning of the integration.
        These diagnostics appear on the standard error stream.


  Short name:           -o
  Long name:            --output_interval
  Value type:           float
  Default value:        1
  Variable name:        dt_out
  Description:          Snapshot output interval
  Long description:
    This option sets the time interval between output of a snapshot
    of the whole N-body system, which which will appear on the
    standard output channel.

    The snapshot contains the mass, position, and velocity values
    for all particles in an N-body system, in ACS format


  Short name:           -y
  Long name:            --pruned_dump
  Value type:           int
  Default value:        0
  Variable name:        prune_factor
  Description:          Prune Factor
  Long description:
    If this option is invoked with a positive argument k = 1, then
    the full information for a particle is printed as soon as it
    makes a step.  If the prune factor is set to a value k > 1,
    the information is printed only for 1 out of every k steps.
    The output appears in ACS format on the standard output channel.
    It is guaranteed that for each particle the full information will
    be printed before the first step and after the last step.  
    The resulting stream of outputs contains information for different
    particles at different times, but within each worldline, the
    world points are time ordered.

    If this option is not invoked, or if it is invoked with the default
    value k = 0, no such action will be undertaken.  This option, when
    invoked with k > 0, overrides the normal output options (a specified
    value for the normal output interval will be ignored). 


  Short name:           -t
  Long name:            --time_period
  Value type:           float
  Default value:        10
  Variable name:        dt_end
  Print name:           t
  Description:          Duration of the integration
  Long description:
    This option sets the duration t of the integration, the time period
    after which the integration will halt.  If the initial snapshot is
    marked to be at time t_init, the integration will halt at time
    t_final = t_init + t.


  Short name:           -u
  Long name:            --cpu_time_max
  Value type:           int
  Default value:        60
  Variable name:        cpu_time_max
  Description:          Max cputime diagnost. interval
  Long description:
    This option sets the maximum cpu time interval between diagnostics output,
    in seconds.


  Short name:           -i
  Long name:            --init_out
  Value type:           bool
  Variable name:        output_at_startup_flag
  Description:          Output the initial snapshot
  Long description:
    If this flag is set to true, the initial snapshot will be output
    on the standard output channel, before integration is started.


  Short name:           -r
  Long name:            --world_output
  Value type:           bool
  Variable name:        world_output_flag
  Description:          World output format, instead of snapshot
  Long description:
    If this flag is set to true, each output will take the form of a
    full world dump, instead of a snapshot (the default).  Reading in
    such an world again will allow a fully accurate restart of the
    integration,  since no information is lost in the process of writing
    out and reading in, in terms of world format.


  Short name:           -a
  Long name:            --shared_timesteps
  Value type:           bool
  Variable name:        shared_flag
  Description:          All particles share the same time step
  Long description:
    If this flag is set to true, all particles will march in lock step,
    all sharing the same time step.


  END

clop = parse_command_line(options_text)

World.admit($stdin, clop).evolve(clop)