#!/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)