def func_common_envelope(M1_in, M2_in, A_in, ecc_in, alpha_CE=1.0, lambda_struc=1.0): """ Calculate orbital evolution due to a common envelope. We use the envelope binding energy approximation from De Marco et al. (2011). This will hopefully be updated soon to account for actual stellar structure models, which will include internal energy as well. Obviously reality is much more complex. """ # M_1 is determined as the core of the primary M_1_out = load_sse.func_sse_he_mass(M_1_in) # M_2 accretes nothing M_2_out = M_2_in # r_1_roche is at its roche radius when the primary overfills its Roche lobe, entering instability r_1_roche = func_Roche_radius(M_1_in, M_2_in, A_in*(1.0-ecc_in)) # Envelope mass M_env = M_1_in - M_1_out # alpha-lambda prescription E_binding = -c.G * M_env * (M_env/2.0 + M_1_out) / (lambda_struc * r_1_roche) E_orb_in = -c.G * M_1_in * M_2_in / (2.0 * A_in) E_orb_out = E_orb_in + (1.0/alpha_CE) * E_binding A_out = -c.G * M_1_out * M_2_out / (2.0 * E_orb_out) return M_1_out, M_2_out, A_out
def ln_posterior_initial(x, args): """ Calculate the posterior probability for the initial mass and birth time calculations. Parameters ---------- x : M1, M2, t_obs model parameters args : M2_d observed companion mass Returns ------- lp : float posterior probability """ M1, M2, t_obs = x M2_d = args y = M1, M2, M2_d, t_obs lp = ln_priors_initial(y) if np.isinf(lp): return -np.inf # Get observed mass, mdot t_eff_obs = binary_evolve.func_get_time(M1, M2, t_obs) M2_c = M1 + M2 - load_sse.func_sse_he_mass(M1) M2_tmp, M2_dot, R_tmp, k_tmp = load_sse.func_get_sse_star(M2_c, t_eff_obs) # Somewhat arbitrary definition of mass error delta_M_err = 1.0 coeff = -0.5 * np.log( 2. * np.pi * delta_M_err*delta_M_err ) argument = -( M2_d - M2_tmp ) * ( M2_d - M2_tmp ) / ( 2. * delta_M_err*delta_M_err ) return coeff + argument + lp
def func_get_time(M1, M2, t_obs): """ Get the adjusted time for a secondary that accreted the primary's envelope in thermal timescale MT parameters ---------- M1 : float Primary mass before mass transfer (Msun) M2 : float Secondary mass before mass transfer (Msun) t_obs : float Observation time (Myr) Returns ------- Effective observed time: float Time to be fed into load_sse.py function func_get_sse_star() (Myr) """ t_lifetime_1 = load_sse.func_sse_ms_time(M1) he_mass_1 = load_sse.func_sse_he_mass(M1) t_lifetime_2 = load_sse.func_sse_ms_time(M2) he_mass_2 = load_sse.func_sse_he_mass(M2) # Relative lifetime through star 2 at mass gain he_mass = t_lifetime_1/t_lifetime_2 * he_mass_2 # Get new secondary parameters mass_new = M2 + M1 - he_mass_1 t_lifetime_new = load_sse.func_sse_ms_time(mass_new) he_mass_new = load_sse.func_sse_he_mass(mass_new) # New, effective lifetime t_eff = he_mass / he_mass_new * t_lifetime_new # Now, we obtain the "effective observed time" return t_eff + t_obs - t_lifetime_1
def ln_priors_initial(x): """ Calculate the prior probability for the initial mass and birth time calculations. Parameters ---------- x : M1, M2, M2_d, t_obs Model parameters plus the observed companion mass Returns ------- ll : float Prior probability of the model parameters """ M1, M2, M2_d, t_obs = x # M1 if M1 < c.min_mass or M1 > c.max_mass: return -np.inf # M2 if M2 < 0.3*M1 or M2 > M1: return -np.inf # Add a prior so that the post-MT secondary is within the correct bounds M2_c = M1 + M2 - load_sse.func_sse_he_mass(M1) if M2_c > c.max_mass or M2_c < c.min_mass: return -np.inf # Add a prior so the primary can go through a SN by t_obs if load_sse.func_sse_tmax(M1) > t_obs: return -np.inf # Add a prior so the effective time remains bounded t_eff_obs = binary_evolve.func_get_time(M1, M2, t_obs) if t_eff_obs < 0.0: return -np.inf # Add a prior so that only those masses with a non-zero Mdot are allowed M2_tmp, M2_dot, R_tmp, k_tmp = load_sse.func_get_sse_star(M2_c, t_eff_obs) if M2_dot == 0.0: return -np.inf return 0.0
def get_initial_values(M2_d, nwalkers=32): """ Calculate an array of initial masses and birth times Parameters ---------- M2_d : float Observed secondary mass Returns ------- pos : ndarray, shape=(nwalkers,3) Array of (M1, M2, t_b) """ # Start by using MCMC on just the masses to get a distribution of M1 and M2 args = [[M2_d]] sampler = emcee.EnsembleSampler(nwalkers=nwalkers, dim=3, lnpostfn=ln_posterior_initial, args=args) # Picking the initial masses and birth time will need to be optimized t_b = 1000.0 M1_tmp = max(0.6*M2_d, c.min_mass) M2_tmp = 1.1*M2_d - M1_tmp p_i = [M1_tmp, M2_tmp,t_b] tmp = binary_evolve.func_get_time(*p_i) - 1000.0 t_b = 0.9 * (load_sse.func_sse_tmax(p_i[0] + p_i[1] - load_sse.func_sse_he_mass(p_i[0])) - tmp) p_i[2] = t_b t_eff_obs = binary_evolve.func_get_time(*p_i) M_b_prime = p_i[0] + p_i[1] - load_sse.func_sse_he_mass(p_i[0]) M_tmp, Mdot_tmp, R_tmp, k_tmp = load_sse.func_get_sse_star(M_b_prime, t_eff_obs) min_M = load_sse.func_sse_min_mass(t_b) n_tries = 0 while t_eff_obs < 0.0 or Mdot_tmp == 0.0: p_i[0] = (c.max_mass - min_M) * np.random.uniform() + min_M p_i[1] = (0.7 * np.random.uniform() + 0.3) * p_i[0] p_i[2] = (np.random.uniform(5.0) + 1.2) * load_sse.func_sse_tmax(M2_d*0.6) t_eff_obs = binary_evolve.func_get_time(*p_i) if t_eff_obs < 0.0: continue M_b_prime = p_i[0] + p_i[1] - load_sse.func_sse_he_mass(p_i[0]) if M_b_prime > c.max_mass: continue M_tmp, Mdot_tmp, R_tmp, k_tmp = load_sse.func_get_sse_star(M_b_prime, t_eff_obs) # Exit condition n_tries += 1 if n_tries > 100: break # initial positions for walkers p0 = np.zeros((nwalkers,3)) a, b = (min_M - p_i[0]) / 0.5, (c.max_mass - p_i[0]) / 0.5 p0[:,0] = truncnorm.rvs(a, b, loc=p_i[0], scale=1.0, size=nwalkers) # M1 p0[:,1] = np.random.normal(p_i[1], 0.5, size=nwalkers) # M2 p0[:,2] = np.random.normal(p_i[2], 0.2, size=nwalkers) # t_b # burn-in pos,prob,state = sampler.run_mcmc(p0, N=100) return pos
def ln_priors(y): """ Priors on the model parameters Parameters ---------- y : ra, dec, M1, M2, A, ecc, v_k, theta, phi, ra_b, dec_b, t_b Current HMXB location (ra, dec) and 10 model parameters Returns ------- lp : float Natural log of the prior """ # M1, M2, A, v_k, theta, phi, ra_b, dec_b, t_b = y ra, dec, M1, M2, A, ecc, v_k, theta, phi, ra_b, dec_b, t_b = y lp = 0.0 # P(M1) if M1 < c.min_mass or M1 > c.max_mass: return -np.inf norm_const = (c.alpha+1.0) / (np.power(c.max_mass, c.alpha+1.0) - np.power(c.min_mass, c.alpha+1.0)) lp += np.log( norm_const * np.power(M1, c.alpha) ) # M1 must be massive enough to evolve off the MS by t_obs if load_sse.func_sse_tmax(M1) > t_b: return -np.inf # P(M2) # Normalization is over full q in (0,1.0) q = M2 / M1 if q < 0.3 or q > 1.0: return -np.inf lp += np.log( (1.0 / M1 ) ) # P(ecc) if ecc < 0.0 or ecc > 1.0: return -np.inf lp += np.log(2.0 * ecc) # P(A) if A*(1.0-ecc) < c.min_A or A*(1.0+ecc) > c.max_A: return -np.inf norm_const = np.log(c.max_A) - np.log(c.min_A) lp += np.log( norm_const / A ) # A must avoid RLOF at ZAMS, by a factor of 2 r_1_roche = binary_evolve.func_Roche_radius(M1, M2, A*(1.0-ecc)) if 2.0 * load_sse.func_sse_r_ZAMS(M1) > r_1_roche: return -np.inf # P(v_k) if v_k < 0.0: return -np.inf lp += np.log( maxwell.pdf(v_k, scale=c.v_k_sigma) ) # P(theta) if theta <= 0.0 or theta >= np.pi: return -np.inf lp += np.log(np.sin(theta) / 2.0) # P(phi) if phi < 0.0 or phi > np.pi: return -np.inf lp += -np.log( np.pi ) # Get star formation history sf_history.load_sf_history() sfh = sf_history.get_SFH(ra_b, dec_b, t_b, sf_history.smc_coor, sf_history.smc_sfh) if sfh <= 0.0: return -np.inf # P(alpha, delta) # From spherical geometric effect, we need to care about cos(declination) lp += np.log(np.cos(c.deg_to_rad * dec_b) / 2.0) ################################################################## # We add an additional prior that scales the RA and Dec by the # area available to it, i.e. pi theta^2, where theta is the angle # of the maximum projected separation over the distance. # # Still under construction ################################################################## M1_b, M2_b, A_b = binary_evolve.func_MT_forward(M1, M2, A, ecc) A_c, v_sys, ecc = binary_evolve.func_SN_forward(M1_b, M2_b, A_b, v_k, theta, phi) if ecc < 0.0 or ecc > 1.0 or np.isnan(ecc): return -np.inf # Ensure that we only get non-compact object companions tobs_eff = binary_evolve.func_get_time(M1, M2, t_b) M_tmp, M_dot_tmp, R_tmp, k_type = load_sse.func_get_sse_star(M2_b, tobs_eff) if int(k_type) > 9: return -np.inf # t_sn = (t_b - func_sse_tmax(M1)) * 1.0e6 * yr_to_sec # The time since the primary's core collapse # theta_max = (v_sys * t_sn) / dist_LMC # Unitless # area = np.pi * rad_to_dec(theta_max)**2 # lp += np.log(1.0 / area) ################################################################## # Instead, let's estimate the number of stars formed within a cone # around the observed position, over solid angle and time. # Prior is in Msun/Myr/steradian ################################################################## t_min = load_sse.func_sse_tmax(M1) * 1.0e6 * c.yr_to_sec t_max = (load_sse.func_sse_tmax(M2_b) - binary_evolve.func_get_time(M1, M2, 0.0)) * 1.0e6 * c.yr_to_sec if t_max-t_min < 0.0: return -np.inf theta_C = (v_sys * (t_max - t_min)) / c.dist_SMC stars_formed = get_stars_formed(ra, dec, t_min, t_max, v_sys, c.dist_SMC) if stars_formed == 0.0: return -np.inf volume_cone = (np.pi/3.0 * theta_C**2 * (t_max - t_min) / c.yr_to_sec / 1.0e6) lp += np.log(sfh / stars_formed / volume_cone) ################################################################## # # P(t_b | alpha, delta) # sfh_normalization = 1.0e-6 # lp += np.log(sfh_normalization * sfh) # Add a prior so that the post-MT secondary is within the correct bounds M2_c = M1 + M2 - load_sse.func_sse_he_mass(M1) if M2_c > c.max_mass or M2_c < c.min_mass: return -np.inf # Add a prior so the effective time remains bounded t_eff_obs = binary_evolve.func_get_time(M1, M2, t_b) if t_eff_obs < 0.0: return -np.inf if t_b * 1.0e6 * c.yr_to_sec < t_min: return -np.inf if t_b * 1.0e6 * c.yr_to_sec > t_max: return -np.inf return lp
def ln_priors_population(y): """ Priors on the model parameters Parameters ---------- y : M1, M2, A, ecc, v_k, theta, phi, ra_b, dec_b, t_b 10 model parameters Returns ------- lp : float Natural log of the prior """ M1, M2, A, ecc, v_k, theta, phi, ra_b, dec_b, t_b = y lp = 0.0 # P(M1) if M1 < c.min_mass or M1 > c.max_mass: return -np.inf norm_const = (c.alpha+1.0) / (np.power(c.max_mass, c.alpha+1.0) - np.power(c.min_mass, c.alpha+1.0)) lp += np.log( norm_const * np.power(M1, c.alpha) ) # M1 must be massive enough to evolve off the MS by t_obs if load_sse.func_sse_tmax(M1) > t_b: return -np.inf # P(M2) # Normalization is over full q in (0,1.0) q = M2 / M1 if q < 0.3 or q > 1.0: return -np.inf lp += np.log( (1.0 / M1 ) ) # P(ecc) if ecc < 0.0 or ecc > 1.0: return -np.inf lp += np.log(2.0 * ecc) # P(A) if A*(1.0-ecc) < c.min_A or A*(1.0+ecc) > c.max_A: return -np.inf norm_const = np.log(c.max_A) - np.log(c.min_A) lp += np.log( norm_const / A ) # A must avoid RLOF at ZAMS, by a factor of 2 r_1_roche = binary_evolve.func_Roche_radius(M1, M2, A*(1.0-ecc)) if 2.0 * load_sse.func_sse_r_ZAMS(M1) > r_1_roche: return -np.inf # P(v_k) if v_k < 0.0: return -np.inf lp += np.log( maxwell.pdf(v_k, scale=c.v_k_sigma) ) # P(theta) if theta <= 0.0 or theta >= np.pi: return -np.inf lp += np.log(np.sin(theta) / 2.0) # P(phi) if phi < 0.0 or phi > np.pi: return -np.inf lp += -np.log( np.pi ) # Get star formation history sfh = sf_history.get_SFH(ra_b, dec_b, t_b, sf_history.smc_coor, sf_history.smc_sfh) if sfh <= 0.0: return -np.inf lp += np.log(sfh) # P(alpha, delta) # From spherical geometric effect, scale by cos(declination) lp += np.log(np.cos(c.deg_to_rad*dec_b) / 2.0) M1_b, M2_b, A_b = binary_evolve.func_MT_forward(M1, M2, A, ecc) A_c, v_sys, ecc = binary_evolve.func_SN_forward(M1_b, M2_b, A_b, v_k, theta, phi) if ecc < 0.0 or ecc > 1.0 or np.isnan(ecc): return -np.inf # Ensure that we only get non-compact object companions tobs_eff = binary_evolve.func_get_time(M1, M2, t_b) M_tmp, M_dot_tmp, R_tmp, k_type = load_sse.func_get_sse_star(M2_b, tobs_eff) if int(k_type) > 9: return -np.inf # Add a prior so that the post-MT secondary is within the correct bounds M2_c = M1 + M2 - load_sse.func_sse_he_mass(M1) if M2_c > c.max_mass or M2_c < c.min_mass: return -np.inf # Add a prior so the effective time remains bounded t_eff_obs = binary_evolve.func_get_time(M1, M2, t_b) if t_eff_obs < 0.0: return -np.inf t_max = (load_sse.func_sse_tmax(M2_b) - binary_evolve.func_get_time(M1, M2, 0.0)) if t_b > t_max: return -np.inf return lp
def func_MT_forward(M_1_in, M_2_in, A_in, ecc_in, beta=1.0): """ Evolve a binary through thermal timescale mass transfer. It is assumed that the binary (instantaneously) circularizes at the pericenter distance. Parameters ---------- M_1_in : float Primary mass input (Msun) M_2_in : float Secondary mass input (Msun) A_in : float Orbital separation (any unit) ecc_in : float Eccentricity (unitless) beta : float Mass transfer efficiency Returns ------- M_1_out : float Primary mass output (Msun) M_2_out : float Secondary mass output (Msun) A_out : float Orbital separation output (any unit) """ M_1_out = load_sse.func_sse_he_mass(M_1_in) M_2_out = M_2_in + beta * (M_1_in - M_1_out) # Mass is lost with specific angular momentum of donor (M1) alpha = (M_2_out / (M_1_out + M_2_out))**2 # Old formula for conservative MT: A_out = A_in * (1.0-ecc_in) * (M_1_in*M_2_in/M_1_out/M_2_out)**2 # Allowing for non-conservative MT (also works for conservative MT) C_1 = 2.0 * alpha * (1.0-beta) - 2.0 C_2 = -2.0 * alpha / beta * (1.0 - beta) - 2.0 A_out = A_in * (1.0-ecc_in) * (M_1_out+M_2_out)/(M_1_in+M_2_in) * (M_1_out/M_1_in)**C_1 * (M_2_out/M_2_in)**C_2 ################################## # NOTE: Technically, the above equation only works for a fixed alpha. If # Alpha is indeed varying as mass is lost, then the above equation needs # to be adjusted. This is probably solved elsewhere in the literature. ################################## # Make sure systems don't overfill their Roche lobes # r_1_max = load_sse.func_sse_r_MS_max(M_1_out) # r_1_roche = func_Roche_radius(M_1_in, M_2_in, A_in*(1.0-ecc_in)) # r_2_max = load_sse.func_sse_r_MS_max(M_2_out) # r_2_roche = func_Roche_radius(M_2_in, M_1_in, A_in) ##### TESTING ##### # # Get the k-type when the primary overfills its Roche lobe # if isinstance(M_1_in, np.ndarray): # k_RLOF = load_sse.func_sse_get_k_from_r(M_1_in, r_1_roche) # else: # k_RLOF = load_sse.func_sse_get_k_from_r(np.asarray([M_1_in]), np.asarray([r_1_roche])) # # # # Only systems with k_RLOF = 2, 4 survive # if isinstance(A_out, np.ndarray): # idx = np.intersect1d(np.where(k_RLOF!=2), np.where(k_RLOF!=4)) # A_out[idx] = -1.0 # else: # if (k_RLOF != 2) & (k_RLOF != 4): A_out = -1.0 ##### TESTING ##### # Post-MT, Pre-SN evolution M2_He_in = M_2_out M2_He_out = M2_He_in M1_He_in = M_1_out M1_He_out = load_sse.func_sse_he_star_final(M_1_out) A_He_in = A_out A_He_out = A_He_in * (M1_He_in + M2_He_in) / (M1_He_out + M2_He_out) # return M_1_out, M_2_out, A_out return M1_He_out, M2_He_out, A_He_out