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 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 full_forward(M1, M2, A, ecc, v_k, theta, phi, t_obs): """ Evolve a binary forward from its initial conditions Parameters ---------- M1 : float Initial primary mass (Msun) M2 : float Initial secondary mass (Msun) A : float Initial orbital separation (Rsun) ecc : float Initial orbital eccentricity (unitless) v_k : float SN kick velocity theta : float SN kick polar angle phi : float SN kick azimuthal angle t_obs : float observation time Returns ------- M_NS : float or ndarray Array of final primary masses (Currently set to the NS mass, c.M_NS) M_2 : float or ndarray Array of final secondary masses (Msun) L_x : float or ndarray X-ray luminosity (erg/s) v_sys : float or ndarray Systemic velocity (km/s) M2_dot : float or ndarray Mass accretion rate (Msun/yr) A : float or ndarray Orbital separation (Rsun) ecc : float or ndarray Orbital eccentricity (unitless) theta : float or ndarray Projected angular distance traveled from birth location (radians) k_type : int k-type of HMXB donor """ if load_sse.func_sse_mass is None: load_sse.load_sse() if isinstance(M1, np.ndarray): dtypes = [('M_NS','<f8'), \ ('M_2','<f8'), \ ('L_x','<f8'), \ ('v_sys','<f8'), \ ('M2_dot','<f8'), \ ('A','<f8'), \ ('ecc','<f8'), \ ('theta','<f8'), \ ('k_type','<i8')] HMXB = np.recarray(len(M1), dtype=dtypes) for i in np.arange(len(M1)): if isinstance(t_obs, np.ndarray): if t_obs[i] < load_sse.func_sse_ms_time(M1[i]): HMXB["M_NS"][i] = M1[i] HMXB["M_2"][i] = M2[i] HMXB["A"][i] = A[i] continue else: if t_obs < load_sse.func_sse_ms_time(M1[i]): HMXB["M_NS"][i] = M1[i] HMXB["M_2"][i] = M2[i] HMXB["A"][i] = A[i] continue # First MT phase M_1_b, M_2_b, A_b = binary_evolve.func_MT_forward(M1[i], M2[i], A[i], ecc[i]) if isinstance(t_obs, np.ndarray): if t_obs[i] < load_sse.func_sse_tmax(M1[i]): HMXB["M_NS"][i] = M_1_b HMXB["M_2"][i] = M_2_b HMXB["A"][i] = A_b continue else: if t_obs < load_sse.func_sse_tmax(M1[i]): HMXB["M_NS"][i] = M_1_b HMXB["M_2"][i] = M_2_b HMXB["A"][i] = A_b continue # SN A_tmp, v_sys_tmp, e_tmp = binary_evolve.func_SN_forward(M_1_b, M_2_b, A_b, v_k[i], theta[i], phi[i]) # XRB if isinstance(t_obs, np.ndarray): M_2_tmp, L_x_tmp, M2_dot_out, A_out = binary_evolve.func_Lx_forward(M1[i], M2[i], M_2_b, A_tmp, e_tmp, t_obs[i]) theta_out = (t_obs[i] - load_sse.func_sse_tmax(M1[i])) * v_sys_tmp / c.dist_SMC * c.yr_to_sec * 1.0e6 * np.sin(get_theta(1)) tobs_eff = binary_evolve.func_get_time(M1[i], M2[i], t_obs[i]) else: M_2_tmp, L_x_tmp, M2_dot_out, A_out = binary_evolve.func_Lx_forward(M1[i], M2[i], M_2_b, A_tmp, e_tmp, t_obs) theta_out = (t_obs - load_sse.func_sse_tmax(M1[i])) * v_sys_tmp / c.dist_SMC * c.yr_to_sec * 1.0e6 * np.sin(get_theta(1)) tobs_eff = binary_evolve.func_get_time(M1[i], M2[i], t_obs) # To get k-type of HMXB donor if M_2_b > c.max_mass: k_type = -999 else: M_tmp, M_dot_tmp, R_tmp, k_type = load_sse.func_get_sse_star(M_2_b, tobs_eff) HMXB["M_NS"][i] = c.M_NS HMXB["M_2"][i] = M_2_tmp HMXB["L_x"][i] = L_x_tmp HMXB["v_sys"][i] = v_sys_tmp HMXB["M2_dot"][i] = M2_dot_out HMXB["A"][i] = A_out HMXB["ecc"][i] = e_tmp HMXB["theta"][i] = theta_out HMXB["k_type"][i] = int(k_type) return HMXB["M_NS"], HMXB["M_2"], HMXB["L_x"], HMXB["v_sys"], HMXB["M2_dot"], HMXB["A"], HMXB["ecc"], HMXB["theta"], HMXB["k_type"] else: # Star does not make it to MT phase if t_obs < load_sse.func_sse_ms_time(M1): return M1, M2, 0.0, 0.0, 0.0, A, ecc, 0.0 # MT phase M_1_b, M_2_b, A_b = binary_evolve.func_MT_forward(M1, M2, A, ecc) # Star does not make it to SN if t_obs < load_sse.func_sse_tmax(M1): return M_1_b, M_2_b, 0.0, 0.0, 0.0, A_b, ecc, 0.0 # SN A_tmp, v_sys_tmp, e_tmp = binary_evolve.func_SN_forward(M_1_b, M_2_b, A_b, v_k, theta, phi) # XRB M_2_tmp, L_x_tmp, M2_dot_out, A_out = binary_evolve.func_Lx_forward(M1, M2, M_2_b, A_tmp, e_tmp, t_obs) theta_out = (t_obs - load_sse.func_sse_tmax(M1)) * v_sys_tmp / c.dist_SMC * c.yr_to_sec * 1.0e6 * np.sin(get_theta(1)) # To get k-type of HMXB donor tobs_eff = binary_evolve.func_get_time(M1, M2, t_obs) M_tmp, M_dot_tmp, R_tmp, k_type = load_sse.func_get_sse_star(M_2_b, tobs_eff) return c.M_NS, M_2_tmp, L_x_tmp, v_sys_tmp, M2_dot_out, A_out, e_tmp, theta_out, int(k_type)
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_Lx_forward(M_1_a, M_2_a, M_2_in, A_in, ecc_in, t_obs): """ Calculate the X-ray luminosity from accretion for a binary Parameters ---------- M_1_a : float ZAMS mass of the primary, now a NS (Msun) M_2_a : float ZAMS mass of the secondary (Msun) M_2_in : float Post-mass transfer mass of the secondary (Msun) A_in : float Post-SN orbital separation (Rsun) ecc_in : float Post-SN eccentricity (unitless) t_obs : float Time which the binary is being observed Returns ------- M_2_out : float Current mass of the secondary L_x : float X_ray luminosity M_dot_out : float Mass accretion rate A_out : float Current orbital separation """ t_eff_obs = func_get_time(M_1_a, M_2_a, t_obs) if isinstance(t_eff_obs, np.ndarray): M_2_out = np.array([]) M_dot_wind = np.array([]) R_out = np.array([]) k_out = np.array([]) for i in np.arange(len(t_eff_obs)): if (t_eff_obs[i] < 0.0 or ecc_in[i] < 0.0 or ecc_in[i] >= 1.0): ecc_in[i] = 0.0 if isinstance(M_2_in, np.ndarray): M_2_out = np.append(M_2_out, M_2_in[i]) else: M_2_out = np.append(M_2_out, M_2_in) M_dot_wind = np.append(M_dot_wind, 0.0) R_out = np.append(R_out, 0.0) else: if isinstance(M_2_in, np.ndarray): if M_2_in[i] > c.max_mass: aa, bb, cc = 0.0, 0.0, 0.0 else: aa, bb, cc, dd = load_sse.func_get_sse_star(M_2_in[i], t_eff_obs[i]) else: if M_2_in > c.max_mass: aa, bb, cc = 0.0, 0.0, 0.0 else: aa, bb, cc, dd = load_sse.func_get_sse_star(M_2_in, t_eff_obs[i]) M_2_out = np.append(M_2_out, aa) M_dot_wind = np.append(M_dot_wind, bb) R_out = np.append(R_out, cc) k_out = np.append(k_out, dd) else: if (t_eff_obs < 0.0 or M_2_in > c.max_mass or ecc_in < 0.0 or ecc_in > 1.0 or t_eff_obs > load_sse.func_sse_tmax(M_2_in)): M_2_out = M_2_in M_dot_wind = 0.0 R_out = 0.0 ecc_in = 0.0 else: M_2_out, M_dot_wind, R_out, k_out = load_sse.func_get_sse_star(M_2_in, t_eff_obs) # Get wind velocity v_wind = get_v_wind(M_2_out, R_out) if isinstance(v_wind, np.ndarray): v_wind[np.where(v_wind <= 0.0)] = 1.0e50 # To eliminate "bad" winds else: if v_wind <= 0.0: v_wind = 1.0e50 # Get final orbital separation if isinstance(A_in, np.ndarray): A_in[np.where(A_in <= 0.0)] = 1.0e50 # To eliminate "bad" separations else: if A_in <= 0.0: A_in = 1.0e50 A_out = (c.M_NS + M_2_in) / (c.M_NS + M_2_out) * A_in # Capture fraction takes into account eccentricity f_capture = (c.GGG*c.M_NS / (v_wind*v_wind*A_out))**2 / np.sqrt(1.0 - ecc_in**2) M_dot_out = f_capture * M_dot_wind L_bol = c.GGG * c.M_NS * M_dot_out / c.R_NS * c.km_to_cm * c.Msun_to_g * c.Rsun_to_cm / c.yr_to_sec L_x = c.eta_bol * L_bol return M_2_out, L_x, M_dot_out, A_out