Source code for posydon.popsyn.independent_sample

"""Generate the initial parameters for a binary population."""


__authors__ = [
    "Devina Misra <devina.misra@unige.ch>",
    "Jeffrey Andrews <jeffrey.andrews@northwestern.edu>",
    "Kyle Akira Rocha <kylerocha2024@u.northwestern.edu>",
    "Konstantinos Kovlakas <Konstantinos.Kovlakas@unige.ch>",
    "Simone Bavera <Simone.Bavera@unige.ch>",
    "Emmanouil Zapartas <ezapartas@gmail.com>",
    "Scott Coughlin <scottcoughlin2014@u.northwestern.edu>",
]


import numpy as np
from scipy.stats import truncnorm
from posydon.utils.common_functions import rejection_sampler


[docs] def generate_independent_samples(orbital_scheme='period', **kwargs): """Randomly generate a population of binaries at ZAMS. Parameters ---------- orbital_scheme : str (default: 'period') The scheme to use to get either orbital periods or separations **kwargs : dictionary kwargs from BinaryPopulation class Returns ------- orbital_scheme_set : ndarray of floats Randomly drawn orbital separations/periods depending on the scheme eccentricity_set : ndarray of floats Randomly drawn eccentricities m1_set : ndarray of floats Randomly drawn primary masses m2_set : ndarray of floats Randomly drawn secondary masses """ # Generate eccentricities eccentricity_set = generate_eccentricities(**kwargs) # Generate primary masses m1_set = generate_primary_masses(**kwargs) # Generate secondary masses m2_set = generate_secondary_masses(m1_set, **kwargs) if orbital_scheme == 'separation': # Generate orbital separations orbital_scheme_set = generate_orbital_separations(**kwargs) elif orbital_scheme == 'period': # Generate orbital periods orbital_scheme_set = generate_orbital_periods(m1_set, **kwargs) else: raise ValueError("Allowed orbital schemes are separation or period.") return orbital_scheme_set, eccentricity_set, m1_set, m2_set
[docs] def generate_orbital_periods(primary_masses, number_of_binaries=1, orbital_period_min=0.35, orbital_period_max=10**3.5, orbital_period_scheme='Sana+12_period_extended', **kwargs): """Randomaly generate orbital periods for a sample of binaries.""" RNG = kwargs.get('RNG', np.random.default_rng()) # Check inputs # Sana H., et al., 2012, Science, 337, 444 if orbital_period_scheme == 'Sana+12_period_extended': # compute periods as if all M1 <= 15Msun (where pi = 0.0) orbital_periods_M_lt_15 = 10**RNG.uniform( low=np.log10(orbital_period_min), high=np.log10(orbital_period_max), size=number_of_binaries) # compute periods as if all M1 > 15Msun def pdf(logp): pi = 0.55 beta = 1 - pi A = np.log10(10**0.15)**(-pi)*(np.log10(10**0.15) - np.log10(orbital_period_min)) B = 1./beta*(np.log10(orbital_period_max)**beta - np.log10(10**0.15)**beta) C = 1./(A + B) pdf = np.zeros(len(logp)) for j, logp_j in enumerate(logp): # for logP<=0.15 days, the pdf is uniform if np.log10(orbital_period_min) <= logp_j and logp_j < 0.15: pdf[j] = C*0.15**(-pi) # original Sana H., et al., 2012, Science, 337, 444 elif 0.15 <= logp_j and logp_j < np.log10(orbital_period_max): pdf[j] = C*logp_j**(-pi) else: pdf[j] = 0. return pdf orbital_periods_M_gt_15 = 10**(rejection_sampler( size=number_of_binaries, x_lim=[np.log10(orbital_period_min), np.log10(orbital_period_max)], pdf=pdf)) orbital_periods = np.where(primary_masses <= 15.0, orbital_periods_M_lt_15, orbital_periods_M_gt_15) else: raise ValueError("You must provide an allowed orbital period scheme.") return orbital_periods
[docs] def generate_orbital_separations(number_of_binaries=1, orbital_separation_min=5, orbital_separation_max=1e5, log_orbital_separation_mean=None, log_orbital_separation_sigma=None, orbital_separation_scheme='log_uniform', **kwargs): """Generate random orbital separations. Use the scheme defined in this particular instance of BinaryPopulation. Parameters ---------- number_of_binaries : int Number of binaries that require randomly sampled orbital separations orbital_separation_min : float Minimum orbital separation in solar radii orbital_separation_max : float Maximum orbital separation in solar radii log_orbital_separation_mean : float Mean of the lognormal distribution. log_orbital_separation_sigma : float Standard deviation of the lorgnormal distribution. orbital_separation_scheme : string Distribution from which the orbital separations are randomly drawn Returns ------- orbital_separations : ndarray of floats Randomly drawn orbital separations """ RNG = kwargs.get('RNG', np.random.default_rng()) orbital_separation_scheme_options = ['log_uniform', 'log_normal'] # Check inputs if orbital_separation_scheme not in orbital_separation_scheme_options: raise ValueError("You must provide an allowed " "orbital separation scheme.") if orbital_separation_scheme == 'log_uniform': orbital_separations = 10**RNG.uniform( low=np.log10(orbital_separation_min), high=np.log10(orbital_separation_max), size=number_of_binaries) if orbital_separation_max < orbital_separation_min: raise ValueError("`orbital_separation_max` must be " "larger than the orbital_separation_min.") elif orbital_separation_scheme == 'log_normal': if (log_orbital_separation_mean is None or log_orbital_separation_sigma is None): raise ValueError( "For the `log_normal separation` scheme you must give " "`log_orbital_separation_mean`, " "`log_orbital_separation_sigma`.") # Set limits for truncated normal distribution a_low = (np.log10(orbital_separation_min) - log_orbital_separation_mean) / log_orbital_separation_sigma a_high = (np.log10(orbital_separation_max) - log_orbital_separation_mean) / log_orbital_separation_sigma # generate orbital separations from a truncted normal distribution log_orbital_separations = truncnorm.rvs( a_low, a_high, loc=log_orbital_separation_mean, scale=log_orbital_separation_sigma, size=number_of_binaries, random_state=RNG) orbital_separations = 10**log_orbital_separations else: pass return orbital_separations
[docs] def generate_eccentricities(number_of_binaries=1, eccentricity_scheme='thermal', **kwargs): """Generate random eccentricities. Use the scheme defined in this particular instance of BinaryPopulation. Parameters ---------- number_of_binaries : int Number of binaries that require randomly sampled orbital separations eccentricity_scheme : string Distribution from which eccentricities are randomly drawn **kwargs : dictionary kwargs from BinaryPopulation class Returns ------- eccentricities : ndarray of floats Randomly drawn eccentricities """ RNG = kwargs.get('RNG', np.random.default_rng()) eccentricity_scheme_options = ['thermal', 'uniform', 'zero'] if eccentricity_scheme not in eccentricity_scheme_options: raise ValueError("You must provide an allowed eccentricity scheme.") if eccentricity_scheme == 'thermal': eccentricities = np.sqrt(RNG.uniform(size=number_of_binaries)) elif eccentricity_scheme == 'uniform': eccentricities = RNG.uniform(size=number_of_binaries) elif eccentricity_scheme == 'zero': eccentricities = np.zeros(number_of_binaries) else: # This should never be reached pass return eccentricities
[docs] def generate_primary_masses(number_of_binaries=1, primary_mass_min=7, primary_mass_max=120, primary_mass_scheme='Salpeter', **kwargs): """Generate random primary masses. Use the scheme defined in this particular instance of BinaryPopulation. Parameters ---------- number_of_binaries : int Number of binaries that require randomly sampled orbital separations primary_mass_min : float Minimum primary mass primary_mass_max : float Maximum primary mass primary_mass_scheme : string Distribution from which the primary masses are randomly drawn Returns ------- primary_masses : ndarray of floats Randomly drawn primary masses """ RNG = kwargs.get('RNG', np.random.default_rng()) primary_mass_scheme_options = ['Salpeter', 'Kroupa1993', 'Kroupa2001'] if primary_mass_scheme not in primary_mass_scheme_options: raise ValueError("You must provide an allowed primary mass scheme.") # Salpeter E. E., 1955, ApJ, 121, 161 if primary_mass_scheme == 'Salpeter': alpha = 2.35 normalization_constant = (1.0-alpha) / (primary_mass_max**(1-alpha) - primary_mass_min**(1-alpha)) random_variable = RNG.uniform(size=number_of_binaries) primary_masses = (random_variable*(1.0-alpha)/normalization_constant + primary_mass_min**(1.0-alpha))**(1.0/(1.0-alpha)) # Kroupa P., Tout C. A., Gilmore G., 1993, MNRAS, 262, 545 elif primary_mass_scheme == 'Kroupa1993': alpha = 2.7 normalization_constant = (1.0-alpha) / (primary_mass_max**(1-alpha) - primary_mass_min**(1-alpha)) random_variable = RNG.uniform(size=number_of_binaries) primary_masses = (random_variable*(1.0-alpha)/normalization_constant + primary_mass_min**(1.0-alpha))**(1.0/(1.0-alpha)) # Kroupa P., 2001, MNRAS, 322, 231 elif primary_mass_scheme == 'Kroupa2001': alpha = 2.3 normalization_constant = (1.0-alpha) / (primary_mass_max**(1-alpha) - primary_mass_min**(1-alpha)) random_variable = RNG.uniform(size=number_of_binaries) primary_masses = (random_variable*(1.0-alpha)/normalization_constant + primary_mass_min**(1.0-alpha))**(1.0/(1.0-alpha)) else: pass return primary_masses
[docs] def generate_secondary_masses(primary_masses, number_of_binaries=1, secondary_mass_min=0.35, secondary_mass_max=120, secondary_mass_scheme='flat_mass_ratio', **kwargs): """Generate random secondary masses. Use the scheme defined in this particular instance of BinaryPopulation. Parameters ---------- primary_masses : ndarray of floats Previously drawn primary masses number_of_binaries : int Number of binaries that require randomly sampled orbital separations secondary_mass_min : float Minimum secondary mass secondary_mass_max : float Maximum secondary mass secondary_mass_scheme : string Distribution from which the secondary masses are randomly drawn Returns ------- secondary_masses : ndarray of floats Randomly drawn secondary masses """ RNG = kwargs.get('RNG', np.random.default_rng()) secondary_mass_scheme_options = ['flat_mass_ratio', 'q=1'] # Input parameter checks if secondary_mass_scheme not in secondary_mass_scheme_options: raise ValueError("You must provide an allowed secondary mass scheme.") if np.min(primary_masses) < secondary_mass_min: raise ValueError("`secondary_mass_min` is " "larger than some primary masses") # Generate secondary masses if secondary_mass_scheme == 'flat_mass_ratio': mass_ratio_min = np.max([secondary_mass_min / primary_masses, np.ones(len(primary_masses))*0.05], axis=0) mass_ratio_max = np.min([secondary_mass_max / primary_masses, np.ones(len(primary_masses))], axis=0) secondary_masses = ( (mass_ratio_max - mass_ratio_min) * RNG.uniform( size=number_of_binaries) + mass_ratio_min) * primary_masses if secondary_mass_scheme == 'q=1': secondary_masses = primary_masses return secondary_masses
[docs] def binary_fraction_value(binary_fraction_const=1,binary_fraction_scheme = 'const',m1 = None,**kwargs): """ Getting the binary fraction depending on the scheme. The two possible option are a constant binary fraction and a binary fraction based on the values given in Moe and Di Stefano (2017). Parameters: -------------------- binary scheme: string Determines if the value of the binary fraction will be constant or not binary fraction const: int Gives the value the constant value of the binary if the constant scheme is choosen. Returns ------------------ binary fraction: int """ binary_fraction_scheme_options = ['const','Moe_17'] # Input parameter checks if binary_fraction_scheme not in binary_fraction_scheme_options: raise ValueError("You must provide an allowed binary fraction scheme.") if binary_fraction_scheme == 'const': binary_fraction = binary_fraction_const elif binary_fraction_scheme == 'Moe_17': if m1 is None: raise ValueError("There was not a primary mass provided in the inputs. Unable to return a binary fraction") elif m1 < 0.8: raise ValueError("The scheme doesn't support values of m1 less than 0.8") elif m1 <= 2 and m1 >= 0.8: binary_fraction = 0.4 elif m1 <= 5 and m1 > 2: binary_fraction = 0.59 elif m1<=9 and m1 > 5 : binary_fraction = 0.76 elif m1<= 16 and m1 > 9: binary_fraction = 0.84 elif m1 > 16: binary_fraction = 0.94 else: raise ValueError(f'There primary mass provided {m1} is not supported by the Moe_17 scheme.') else: pass return binary_fraction