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

1 import numpy as _np

2 import joblib as _joblib

3 import rebound as _rebound

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

5

6 from . import tools as _tools

7 from . import units as _u

8 from .core import add_star_to_sim, _rotate_into_plane

9

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

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

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

13

14 def 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

28 def 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

107 def 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

120 def 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

134 def 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

223 def 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

236 def 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

259 def 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])