Coverage for src/airball/analytic.py: 10%

154 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 10:31 +0900

1import numpy as _np 

2import joblib as _joblib 

3import rebound as _rebound 

4from scipy.special import j0 as _j0,jv as _jv 

5 

6from . import tools as _tools 

7from . import units as _u 

8from .core import add_star_to_sim, _rotate_into_plane 

9 

10############################################################ 

11################## Analytical Estimates #################### 

12############################################################ 

13 

14def binary_energy(sim, particle_index=1): 

15 ''' 

16 The energy of a binary system, -(G*M*m)/(2*a). 

17  

18 Parameters 

19 ---------- 

20 sim : REBOUND Simulation 

21 ''' 

22 index = int(particle_index) 

23 unit_set = _tools.rebound_units(sim) 

24 G = (sim.G * unit_set['length']**3 / unit_set['mass'] / unit_set['time']**2) 

25 p = sim.particles 

26 return (-G * p[0].m * unit_set['mass'] * p[index].m * unit_set['mass'] / (2. * p[index].a * unit_set['length'])).decompose(list(unit_set.values())) 

27 

28def energy_change_adiabatic_estimate(sim, star, averaged=True, particle_index=1): 

29 ''' 

30 An analytical estimate for the change in energy of a binary system due to a flyby star. 

31  

32 From the conclusions of Roy & Haddow (2003) https://ui.adsabs.harvard.edu/abs/2003CeMDA..87..411R/abstract and Heggie (2006) https://ui.adsabs.harvard.edu/abs/2006fbp..book...20H/abstract. The orbital element angles of the flyby star are determined with respect to the plane defined by the binary orbit. In REBOUND this is the same as when the inclination of the planet is zero. 

33  

34 Parameters 

35 ---------- 

36 sim : REBOUND Simulation 

37 star : AIRBALL Star flyby object 

38 ''' 

39 index = int(particle_index) 

40 unit_set = _tools.rebound_units(sim) 

41 t0 = 0*unit_set['time'] 

42 G = (sim.G * unit_set['length']**3 / unit_set['mass'] / unit_set['time']**2) 

43 

44 sim = sim.copy() 

45 sim.rotate(_rebound.Rotation.to_new_axes(newz=sim.angular_momentum())) 

46 

47 p = sim.particles 

48 m1, m2, m3 = p[0].m * unit_set['mass'], p[index].m * unit_set['mass'], star.mass # redefine the masses for convenience 

49 M12 = m1 + m2 # total mass of the binary system 

50 M123 = m1 + m2 + m3 # total mass of all the objects involved 

51 

52 mu = G * (_tools.system_mass(sim) * unit_set['mass'] + m3) 

53 es = _tools.vinf_and_b_to_e(mu=mu, star_b=star.b, star_v=star.v) 

54 

55 a, e = p[index].a * unit_set['length'], p[index].e # redefine the orbital elements of the planet for convenience 

56 # Case: Non-circular Binary 

57 

58 n = _np.sqrt(G*M12/a**3) # compute the mean motion of the planet 

59 

60 w, W, i = star.omega, star.Omega, star.inc # redefine the orientation elements of the flyby star for convenience 

61 V = star.v 

62 # GM123 = G*M123  

63 q = (- mu + _np.sqrt( mu**2. + star.b**2. * V**4.))/V**2. # compute the periapsis of the flyby star 

64 

65 # Calculate the following convenient functions of the planet's eccentricity and Bessel functions of the first kind of order n. 

66 e1 = _jv(-1,e) - 2*e*_j0(e) + 2*e*_jv(2,0) - _jv(3,e) 

67 e2 = _jv(-1,e) - _jv(3,e) 

68 e4 = _jv(-1,e) - e*_j0(e) - e*_jv(2,e) + _jv(3,e) 

69 

70 # Calculate a convenient function of the planet's semi-major axis and the flyby star's periapsis. 

71 k = _np.sqrt((2*M12*q**3)/(M123*a**3)) 

72 

73 # Calculate convenient functions of the flyby star's eccentricity. 

74 f1 = ((es + 1.0)**(0.75)) / ((2.0**(0.75)) * (es*es)) 

75 with _u.set_enabled_equivalencies(_u.dimensionless_angles()): 

76 if (1.0/es) > 1 or (1.0/es) < -1: raise Exception(f'There is an issue here. {1.0/es:1.3f} {es}') 

77 f2 = (3.0/(2.0*_np.sqrt(2.0))) * (_np.sqrt((es*es) - 1.0) - _np.arccos(1.0/es)) / ((es - 1.0)**(1.5)) 

78 

79 # Compute the prefactor and terms of the calculation done by Roy & Haddow (2003) 

80 prefactor = (-_np.sqrt(_np.pi)/8.0) * ((G*m1*m2*m3)/M12) * ((a*a)/(q*q*q)) * f1 * k**(2.5) * _np.exp((-2.0*k/3.0)*f2) 

81 with _u.set_enabled_equivalencies(_u.dimensionless_angles()): 

82 term1 = e1 * ( _np.sin(2.0*w + n*t0)*_np.cos(2.0*i - 1.0)- _np.sin(2.0*w + n*t0)*_np.cos(2.0*i)*_np.cos(2.0*W) - 3.0*_np.sin(n*t0 + 2.0*w)*_np.cos(2.0*W) - 4.0*_np.sin(2.0*W)*_np.cos(2.0*w + n*t0)*_np.cos(i) ) 

83 term2 = e2 * (1.0 - e*e) * ( _np.sin(2.0*w + n*t0)*(1.0-_np.cos(2.0*i)) - _np.sin(2.0*w + n*t0)*_np.cos(2.0*i)*_np.cos(2.0*W) - 3.0*_np.sin(n*t0 +2.0*w)*_np.cos(2.0*W) - 4.0*_np.cos(n*t0 + 2.0*w)*_np.sin(2.0*W)*_np.cos(i) ) 

84 term3 = e4 * _np.sqrt(1.0 - e*e) * (-2.0*_np.cos(2.0*i)*_np.cos(2.0*w + n*t0)*_np.sin(2.0*W) - 6.0*_np.cos(2.0*w + n*t0)*_np.sin(2.0*W) - 8.0*_np.cos(2.0*W)*_np.sin(2.0*w + n*t0)*_np.cos(i) ) 

85 

86 noncircular_result = None 

87 if averaged: noncircular_result = (prefactor * (e1 + e2 * (1 - e*e) + 2 * e4 * _np.sqrt(1 - e*e))).decompose(list(unit_set.values())) 

88 else: noncircular_result = (prefactor * ( term1 + term2 + term3)).decompose(list(unit_set.values())) 

89 

90 # Case: Circular Binary 

91 

92 # Compute the prefactor and terms of the calculation done by Roy & Haddow (2003) 

93 prefactor = (-_np.sqrt(_np.pi)/8.0) * ((G*m1*m2*m3)/M12) * ((a*a*a)/(q*q*q*q)) * f1 * k**(3.5) * _np.exp((-2.0*k/3.0)*f2) * (m2/M12 - m1/M12) 

94 # if m2 < m1: prefactor /= twopi 

95 with _u.set_enabled_equivalencies(_u.dimensionless_angles()): 

96 term1 = (1.0 + _np.cos(i)) * _np.sin(i)**2.0 

97 term2 = ( (_np.cos(w)**3.0) - 3.0 * (_np.sin(w)**2.0) * _np.cos(w) ) * _np.sin(n*t0) 

98 term3 = ( 3.0 * (_np.cos(w)**2.0) * _np.sin(w) - (_np.sin(w)**3.0)) * _np.cos(n*t0) 

99 

100 circular_result = None 

101 if averaged: circular_result = (prefactor / _np.pi).decompose(list(unit_set.values())) 

102 else: circular_result = (prefactor * term1 * (term2 + term3)).decompose(list(unit_set.values())) 

103 

104 return circular_result + noncircular_result 

105 

106 

107def relative_energy_change(sim, star, averaged=False, particle_index=1): 

108 ''' 

109 An analytical estimate for the relative change in energy of a binary system due to a flyby star. 

110  

111 Combines energy_change_adiabatic_estimate(...) and binary_energy(...) functions. 

112  

113 Parameters 

114 ---------- 

115 sim : REBOUND Simulation with two bodies, a central star and a planet 

116 star : AIRBALL Star flyby object 

117 ''' 

118 return energy_change_adiabatic_estimate(sim=sim, star=star, averaged=averaged, particle_index=particle_index)/binary_energy(sim, particle_index=particle_index) 

119 

120def parallel_relative_energy_change(sims, stars, averaged=False, particle_index=1): 

121 ''' 

122 An analytical estimate for the relative change in energy of a binary system due to a flyby star. 

123  

124 Combines energy_change_adiabatic_estimate(...) and binary_energy(...) functions. 

125  

126 Parameters 

127 ---------- 

128 sims : REBOUND Simulations with two bodies, a central star and a planet 

129 stars : AIRBALL Stars flyby object 

130 ''' 

131 return _joblib.Parallel(n_jobs=-1)(_joblib.delayed(relative_energy_change)(sim=sims[i], star=stars[i], averaged=averaged, particle_index=particle_index) for i in range(stars.N)) 

132 

133 

134def eccentricity_change_adiabatic_estimate(sim, star, averaged=False, particle_index=1, mode='all', rmax=1e5*_u.au): 

135 ''' 

136 An analytical estimate for the change in eccentricity of an eccentric binary system due to a flyby star. 

137  

138 From Equation (7) of Heggie & Rasio (1996) Equation (A3) from Spurzem et al. (2009) https://ui.adsabs.harvard.edu/abs/2009ApJ...697..458S/abstract.  

139 The orbital element angles of the flyby star are determined with respect to the plane defined by the binary orbit (the invariant plane). In REBOUND this is the same as when the inclination of the planet is zero. 

140  

141 Parameters 

142 ---------- 

143 sim : REBOUND Simulation with two bodies, a central star and a planet 

144 star : AIRBALL Star flyby object 

145 ''' 

146 

147 index = int(particle_index) 

148 

149 unit_set = _tools.rebound_units(sim) 

150 t0 = sim.t * unit_set['time'] 

151 G = (sim.G * unit_set['length']**3 / unit_set['mass'] / unit_set['time']**2) 

152 

153 p = sim.particles 

154 m1, m2, m3 = p[0].m * unit_set['mass'], p[index].m * unit_set['mass'], star.mass # redefine the masses for convenience 

155 M12 = m1 + m2 # total mass of the binary system 

156 M123 = m1 + m2 + m3 # total mass of all the objects involved 

157 

158 mu = G * (_tools.system_mass(sim) * unit_set['mass'] + m3) 

159 es = _tools.vinf_and_b_to_e(mu=mu, star_b=star.b, star_v=star.v) 

160 

161 a, e = p[index].a * unit_set['length'], p[index].e # redefine the orbital elements of the planet for convenience 

162 # n = _np.sqrt(G*M12/a**3) # compute the mean motion of the planet 

163 

164 w, W, i = star.omega, star.Omega, star.inc # redefine the orientation elements of the flyby star for convenience 

165 

166 star_params = _tools.hyperbolic_elements_from_stellar_params(sim, star, rmax) 

167 tperi = star_params['T'] 

168 Mperi = p[index].M + (p[index].n/unit_set['time']) * (tperi - t0) # get the Mean anomaly when the flyby star is at perihelion 

169 f = _tools.reb_M_to_f(p[index].e, Mperi.value) << _u.rad # get the true anomaly when the flyby star is at perihelion 

170 Wp = W + f 

171 V = star.v 

172 q = (- mu + _np.sqrt( mu**2. + star.b**2. * V**4.))/V**2. # compute the periapsis of the flyby star 

173 

174 # Case: Non-Circular Binary 

175 

176 prefactor = (-15.0/4.0) * m3 / _np.sqrt(M12*M123) * ((a/q)**1.5) * ((e * _np.sqrt(1.0 - e*e))/((1.0 + es)**1.5)) 

177 with _u.set_enabled_equivalencies(_u.dimensionless_angles()): 

178 if (-1.0/es) > 1 or (-1.0/es) < -1: raise Exception(f'There is an issue here. {-1.0/es:1.3f} {es}') 

179 t1 = (_np.sin(i) * _np.sin(i) * _np.sin(2.0*W) * ( _np.arccos(-1.0/es) + _np.sqrt(es*es - 1.0) )).value 

180 t2 = ((1.0/3.0) * (1.0 + _np.cos(i)*_np.cos(i)) * _np.cos(2.0*w) * _np.sin(2.0*W)).value 

181 t3 = (2.0 * _np.cos(i) * _np.sin(2.0*w) * _np.cos(2.0*W) * ((es*es - 1.0)**1.5)/(es*es)).value 

182 

183 if averaged: 

184 if (-1.0/es) > 1 or (-1.0/es) < -1: raise Exception(f'There is an issue here. {-1.0/es:1.3f} {es}') 

185 noncircular_result = (prefactor * ( ( _np.arccos(-1.0/es).value + _np.sqrt(es*es - 1.0) ) + (2.0/3.0) + (2.0 * ((es*es - 1.0)**1.5)/(es*es)) )).decompose(list(unit_set.values())) 

186 else: noncircular_result = (prefactor * (t1 + t2 + t3)).decompose(list(unit_set.values())) 

187 

188 # Case: Circular Binary 

189 

190 prefactor = (15.0/8.0) * (m3 * _np.abs(m1-m2) / (M12*M12)) * _np.sqrt(M12/M123) * ((a/q)**2.5) * 1.0 / (es*es*es * ((1.0 + es)**2.5)) 

191 

192 def f1(e): 

193 with _u.set_enabled_equivalencies(_u.dimensionless_angles()): 

194 if (-1.0/es) > 1 or (-1.0/es) < -1: raise Exception(f'There is an issue here. {-1.0/es:1.3f} {es}') 

195 return ((e**4.0) * _np.arccos(-1.0/e) + (-2.0 * 9.0*e*e + 8.0*e**4.0) * _np.sqrt(e*e - 1.0) / 15.0).to(_u.dimensionless_unscaled) 

196 with _u.set_enabled_equivalencies(_u.dimensionless_angles()): 

197 t1 = (_np.cos(i)**2.0 * _np.sin(w)**2.0) * (f1(es) * (1.0 - 3.75 * _np.sin(i)**2.0) + (2.0/15.0) * (es*es - 1.0)**2.5 * (1.0 - 5.0 * _np.sin(w)**2.0 * _np.sin(i)**2.0))**2.0 

198 t2 = (_np.cos(w)**2.0) * (f1(es) * (1.0 - 1.25 * _np.sin(i)**2.0) + (2.0/15.0) * (es*es - 1.0)**2.5 * (1.0 - 5.0 * _np.sin(w)**2.0 * _np.sin(i)**2.0))**2.0 

199 

200# (187 (-1 + e^2)^5)/14400 + (199 f1[e] (4 (-1 + e^2)^(5/2) + 15 f1[e]))/7680 

201 if averaged: 

202 with _u.set_enabled_equivalencies(_u.dimensionless_angles()): 

203 circular_result = prefactor * _np.sqrt(((187.0/14400.0)*(es*es - 1.0)**5.0) + ((199.0 * f1(es)) * (4.0 * (es*es - 1.0)**2.5 + 15.0 * f1(es)))/7680.0) 

204 else: circular_result = prefactor * _np.sqrt(t1 + t2) 

205 

206 # Case: Exponential 

207 

208 prefactor = (3.0 * _np.sqrt(2.0 * _np.pi)) * (m3 * (M12**0.25) / (M123**1.25)) * ((q/a)**0.75) * (((es + 1.0)**0.75)/(es*es)) 

209 with _u.set_enabled_equivalencies(_u.dimensionless_angles()): 

210 i2 = i/2.0 

211 if (1.0/es) > 1 or (1.0/es) < -1: raise Exception(f'There is an issue here. {1.0/es:1.3f} {es}') 

212 exponential = -_np.sqrt(M12/M123) * ((q/a)**1.5) * ((_np.sqrt(es*es - 1.0) - _np.arccos(1.0/es)) / ((es - 1.0)**1.5)) 

213 angles = (_np.cos(i2)**2.0) * _np.sqrt((_np.cos(i2)**4.0) + ((4.0/9.0)*_np.sin(i2)**4.0) + ((4.0/3.0)*(_np.cos(i2)**2.0)*(_np.sin(i2)**2.0)*_np.cos(4.0*w + 2.0*Wp))) 

214 

215 if averaged: exponential_result = prefactor * _np.exp(exponential) 

216 else: exponential_result = prefactor * _np.exp(exponential) * angles 

217 

218 if mode == 'circular': return circular_result.decompose() 

219 elif mode == 'noncircular': return noncircular_result.decompose() 

220 elif mode == 'exponential': return exponential_result.decompose() 

221 else: return (_np.nan_to_num(circular_result) + _np.nan_to_num(noncircular_result) + _np.nan_to_num(exponential_result)).decompose() 

222 

223def parallel_eccentricity_change_adiabatic_estimate(sims, stars, averaged=False, particle_index=1, mode='all', rmax=1e5*_u.au): 

224 ''' 

225 An analytical estimate for the relative change in energy of a binary system due to a flyby star. 

226  

227 Combines energy_change_adiabatic_estimate(...) and binary_energy(...) functions. 

228  

229 Parameters 

230 ---------- 

231 sims : REBOUND Simulations with two bodies, a central star and a planet 

232 stars : AIRBALL Stars flyby object 

233 ''' 

234 return _joblib.Parallel(n_jobs=-1)(_joblib.delayed(eccentricity_change_adiabatic_estimate)(sim=sims[i], star=stars[i], averaged=averaged, particle_index=particle_index, mode=mode, rmax=rmax) for i in range(stars.N)) 

235 

236def eccentricity_change_impulsive_estimate(sim, star, particle_index=1): 

237 

238 index = int(particle_index) 

239 

240 unit_set = _tools.rebound_units(sim) 

241 G = (sim.G * unit_set['length']**3 / unit_set['mass'] / unit_set['time']**2) 

242 sim = sim.copy() 

243 _rotate_into_plane(sim, plane=index) 

244 add_star_to_sim(sim, star, hash='flybystar', rmax=0) # Initialize Star at perihelion 

245 

246 p = sim.particles 

247 m1, m2, m3 = p[0].m * unit_set['mass'], p[index].m * unit_set['mass'], star.mass # redefine the masses for convenience 

248 M12 = m1 + m2 # total mass of the binary system 

249 

250 a = p[index].a * unit_set['length'] # redefine the orbital elements of the planet for convenience 

251 V = star.v 

252 mu = G * (_tools.system_mass(sim) * unit_set['mass'] + m3) 

253 q = (- mu + _np.sqrt( mu**2. + star.b**2. * V**4.))/V**2. # compute the periapsis of the flyby star 

254 

255 theta = _tools.angle_between(p[index].xyz, p['flybystar'].xyz) 

256 

257 return ((2.0 * _np.sqrt(G/M12) * (m3/V) * _np.sqrt(a*a*a) / (q*q)) * _np.abs(_np.cos(theta)) * _np.sqrt((_np.cos(theta)**2.0) + (4.0 * _np.sin(theta)**2.0))).decompose() 

258 

259def energy_change_close_encounters_sim(sim, star, particle_index=1, rmax=1e5*_u.au): 

260 ''' 

261 An analytical estimate for the change in energy of a binary system due to a flyby star. 

262  

263 From Equation (4.7) of Heggie (1975) https://ui.adsabs.harvard.edu/abs/1975MNRAS.173..729H/abstract. The orbital element angles of the flyby star are determined with respect to the plane defined by the binary orbit. 

264  

265 Parameters 

266 ---------- 

267 sim : a REBOUND Simulation object 

268 star: an AIRBALL Star object 

269 particle_index: int, optional 

270 Index for the sim.particles array for which object will be the reference binary plane. 

271 ''' 

272 index = int(particle_index) 

273 s = sim.copy() 

274 _rotate_into_plane(s, plane=index) 

275 add_star_to_sim(s, star, hash='flybystar', rmax=0) # Initialize Star at perihelion 

276 s.move_to_hel() 

277 p = s.particles 

278 

279 unit_set = _tools.rebound_units(sim) 

280 G = (s.G * unit_set['length']**3 / unit_set['mass'] / unit_set['time']**2) # Newton's Gravitational constant 

281 

282 m1, m2 = p[0].m * unit_set['mass'], p[index].m * unit_set['mass'] # redefine the masses for convenience 

283 m3 = star.m 

284 M12 = m1 + m2 # total mass of the binary system 

285 M23 = m2 + m3 # total mass of the second and third bodies 

286 

287 V = star.v # velocity of the star 

288 

289 x,y,z = _tools.unit_vector(p['flybystar'].xyz << unit_set['length']) 

290 vx,vy,vz = p[index].vxyz << (unit_set['length']/unit_set['time']) 

291 

292 with _u.set_enabled_equivalencies(_u.dimensionless_angles()): 

293 cosϕ = 1.0/_np.sqrt(1.0 + (((star.b**2.0)*(V**4.0))/((G*M23)**2.0)).decompose()) 

294 

295 prefactor = (-2.0 * m1 * m2 * m3)/(M12 * M23) * V * cosϕ 

296 t1 = -(x*vx + y*vy + z*vz) 

297 t2 = (m3 * V * cosϕ)/M23 

298 

299 return (prefactor * (t1 + t2)).decompose([unit_set[k] for k in unit_set])