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
« 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
6from . import tools as _tools
7from . import units as _u
8from .core import add_star_to_sim, _rotate_into_plane
10############################################################
11################## Analytical Estimates ####################
12############################################################
14def binary_energy(sim, particle_index=1):
15 '''
16 The energy of a binary system, -(G*M*m)/(2*a).
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()))
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.
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.
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)
44 sim = sim.copy()
45 sim.rotate(_rebound.Rotation.to_new_axes(newz=sim.angular_momentum()))
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
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)
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
58 n = _np.sqrt(G*M12/a**3) # compute the mean motion of the planet
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
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)
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))
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))
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) )
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()))
90 # Case: Circular Binary
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)
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()))
104 return circular_result + noncircular_result
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.
111 Combines energy_change_adiabatic_estimate(...) and binary_energy(...) functions.
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)
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.
124 Combines energy_change_adiabatic_estimate(...) and binary_energy(...) functions.
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))
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.
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.
141 Parameters
142 ----------
143 sim : REBOUND Simulation with two bodies, a central star and a planet
144 star : AIRBALL Star flyby object
145 '''
147 index = int(particle_index)
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)
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
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)
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
164 w, W, i = star.omega, star.Omega, star.inc # redefine the orientation elements of the flyby star for convenience
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
174 # Case: Non-Circular Binary
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
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()))
188 # Case: Circular Binary
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))
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
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)
206 # Case: Exponential
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)))
215 if averaged: exponential_result = prefactor * _np.exp(exponential)
216 else: exponential_result = prefactor * _np.exp(exponential) * angles
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()
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.
227 Combines energy_change_adiabatic_estimate(...) and binary_energy(...) functions.
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))
236def eccentricity_change_impulsive_estimate(sim, star, particle_index=1):
238 index = int(particle_index)
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
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
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
255 theta = _tools.angle_between(p[index].xyz, p['flybystar'].xyz)
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()
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.
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.
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
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
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
287 V = star.v # velocity of the star
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'])
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())
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
299 return (prefactor * (t1 + t2)).decompose([unit_set[k] for k in unit_set])