Пример #1
0
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
Пример #2
0
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
Пример #3
0
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
Пример #4
0
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
Пример #5
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
Пример #6
0
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
Пример #7
0
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
Пример #8
0
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