diff --git a/binarycpython/utils/distribution_functions.py b/binarycpython/utils/distribution_functions.py index 6731570134d11930f287092cce8a185cf53b43e9..e872326b5eea8260a24123be5cf034df4d285b4f 100644 --- a/binarycpython/utils/distribution_functions.py +++ b/binarycpython/utils/distribution_functions.py @@ -181,8 +181,8 @@ def powerlaw( # Handle faulty value if k == -1: - print("wrong value for k") - raise ValueError + msg = "wrong value for k" + raise ValueError(msg) if (x < min_val) or (x > max_val): print("input value is out of bounds!") @@ -590,8 +590,8 @@ def imf_chabrier2003(m: Union[int, float]) -> Union[int, float]: chabrier_a2 = 4.43e-2 chabrier_x = -1.3 if m <= 0: - print("below bounds") - raise ValueError + msg = "below bounds" + raise ValueError(msg) if 0 < m < 1.0: A = 0.158 dm = math.log10(m) - chabrier_logmc @@ -1010,8 +1010,14 @@ def _poisson(lambda_val, n): def Moe_de_Stefano_2017_multiplicity_fractions(options, verbosity=0): """ Function that returns a list of multiplicity fractions for a given list - TODO: make an extrapolation functionality in this. log10(1.6e1) is low, we can probably go a bit further + + TODO: make an extrapolation functionality in this. log10(1.6e1) + is low, we can probably go a bit further + + The default result that is returned when sampling the mass outside + of the mass range is now the last known value """ + # Returns a list of multiplicity fractions for a given input list of masses # Use the global Moecache @@ -1019,7 +1025,6 @@ def Moe_de_Stefano_2017_multiplicity_fractions(options, verbosity=0): result = {} - # TODO: decide what to do with this. # if(0) # { # # we have these in a table, so could interpolate, but... @@ -1058,6 +1063,12 @@ def Moe_de_Stefano_2017_multiplicity_fractions(options, verbosity=0): else 0 ) + verbose_print( + "\tM&S: Moe_de_Stefano_2017_multiplicity_fractions: using model {}".format("Poisson"), + verbosity, + _MS_VERBOSITY_LEVEL, + ) + elif options["multiplicity_model"] == "data": # use the fractions calculated from Moe's data directly # @@ -1072,10 +1083,23 @@ def Moe_de_Stefano_2017_multiplicity_fractions(options, verbosity=0): * options["multiplicity_modulator"][n] ) result[3] = 0.0 # no quadruples + verbose_print( + "\tM&S: Moe_de_Stefano_2017_multiplicity_fractions: using model {}".format("data"), + verbosity, + _MS_VERBOSITY_LEVEL, + ) # Normalisation: if options["normalize_multiplicities"] == "raw": + # TODO: Not sure what actually happens here. It eems the same as above # do nothing : use raw Poisson predictions + + verbose_print( + "\tM&S: Moe_de_Stefano_2017_multiplicity_fractions: Not normalizing (using raw results)", + verbosity, + _MS_VERBOSITY_LEVEL, + ) + pass elif options["normalize_multiplicities"] == "norm": @@ -1084,6 +1108,12 @@ def Moe_de_Stefano_2017_multiplicity_fractions(options, verbosity=0): for key in result.keys(): result[key] = result[key] / sum_result + verbose_print( + "\tM&S: Moe_de_Stefano_2017_multiplicity_fractions: Normalizing with {}".format("norm"), + verbosity, + _MS_VERBOSITY_LEVEL, + ) + elif options["normalize_multiplicities"] == "merge": # if multiplicity == 1, merge binaries, triples and quads into singles # (de facto this is the same as "norm") @@ -1093,24 +1123,18 @@ def Moe_de_Stefano_2017_multiplicity_fractions(options, verbosity=0): # TODO: ask rob about this part. Not sure if i understand it. - multiplicity = 0 + max_multiplicity = 0 for n in range(4): if options["multiplicity_modulator"][n] > 0: - multiplicity = n + 1 + max_multiplicity = n + 1 - verbose_print( - "\tM&S: Multiplicity: {}: {}".format(multiplicity, str(result)), - verbosity, - _MS_VERBOSITY_LEVEL, - ) - - if multiplicity == 1: + if max_multiplicity == 1: result[0] = 1.0 for n in range(1, 4): result[0] += result[n] result[n] = 0 - elif multiplicity == 2: + elif max_multiplicity == 2: # we have only singles and binaries: # triples and quads are treated as binaries @@ -1118,19 +1142,19 @@ def Moe_de_Stefano_2017_multiplicity_fractions(options, verbosity=0): result[1] += result[n] result[n] = 0 - elif multiplicity == 3: + elif max_multiplicity == 3: # we have singles, binaries and triples: # quads are treated as triples result[2] += result[3] result[3] = 0 - elif multiplicity == 4: + elif max_multiplicity == 4: # we have singles, binaries, triples and quads, # so do nothing pass else: msg = "\tM&S: Error: in the Moe distribution, we seem to want no stars at all (multiplicity == {})".format( - multiplicity + max_multiplicity ) verbose_print( msg, @@ -1139,10 +1163,17 @@ def Moe_de_Stefano_2017_multiplicity_fractions(options, verbosity=0): ) raise ValueError(msg) + # Normalising results again sum_result = sum([result[key] for key in result.keys()]) for key in result.keys(): result[key] = result[key] / sum_result + verbose_print( + "\tM&S: Moe_de_Stefano_2017_multiplicity_fractions: Normalizing with {}, max_multiplicity={}".format("merge", max_multiplicity), + verbosity, + _MS_VERBOSITY_LEVEL, + ) + verbose_print( "\tM&S: Multiplicity array: {}".format(str(result)), verbosity, @@ -1306,8 +1337,8 @@ def build_q_table(options, m, p, verbosity=0): qs = sorted(qdata.keys()) if len(qs) == 0: - print("No qs found error") - raise ValueError + msg = "No qs found error" + raise ValueError(msg) elif len(qs) == 1: # only one q value : pretend there are two @@ -1400,14 +1431,15 @@ def build_q_table(options, m, p, verbosity=0): _MS_VERBOSITY_LEVEL, ) else: - verbose_print( - "\tM&S: build_q_table: Error no other methods available. The chosen method ({}) does not exist!".format( + msg = "\tM&S: build_q_table: Error no other methods available. The chosen method ({}) does not exist!".format( method - ), + ) + verbose_print( + msg, verbosity, _MS_VERBOSITY_LEVEL, ) - raise ValueError + raise ValueError(msg) # TODO: consider implementing the poly method (see perl version) @@ -1536,14 +1568,15 @@ def get_integration_constant_q(q_interpolator, verbosity=0): for q in np.arange(0, 1 + 2e-6, dq): x = q_interpolator.interpolate([q]) if len(x) == 0: - verbose_print( - "\tM&S: build_q_table: Q interpolator table interpolation failed.\n\t\ttmp_table = {}\n\t\tq_data = {}".format( + msg = "\tM&S: build_q_table: Q interpolator table interpolation failed.\n\t\ttmp_table = {}\n\t\tq_data = {}".format( str(tmp_table), str(qdata) - ), + ) + verbose_print( + msg, verbosity, _MS_VERBOSITY_LEVEL, ) - raise ValueError + raise ValueError(msg) else: I += x[0] * dq # verbose_print( @@ -1580,6 +1613,57 @@ def fill_data(sample_values, data_dict): return data +def calc_e_integral( + options, integrals_string, interpolator_name, mass_string, period_string, verbosity=0 +): + """ + Function to calculate the P integral + + We need to renormalize this because min_per > 0, and not all periods should be included + """ + + global Moecache + min_ecc = 0 + max_ecc = 0.9999 + + mass_period_string = "{}_{}".format(options[mass_string], options[period_string]) + + # Check if the dict exists + if not Moecache.get(integrals_string, None): + Moecache[integrals_string] = {} + + # Check for cached value. If it doesn't exist: calculate + if not Moecache[integrals_string].get(mass_period_string, None): + I = 0 + decc = 1e-3 + + for ecc in np.arange(min_ecc, max_ecc, decc): + # Loop over all the values in the table, between the min and max P + dp_decc = Moecache[interpolator_name].interpolate( + [np.log10(options[mass_string]), np.log10(options[period_string]), ecc] + )[0] + + I += dp_decc * decc + + # Set the integral value in the dict + Moecache[integrals_string][mass_period_string] = I + verbose_print( + "\tM&S: calc_ecc_integral: min_ecc: {} max ecc: {} integrals_string: {} interpolator_name: {} mass_string: {} period_string: {} mass: {} period: {} I: {}".format( + min_ecc, max_ecc, integrals_string, interpolator_name, mass_string, period_string, options[mass_string], options[period_string], I + ), + verbosity, + _MS_VERBOSITY_LEVEL, + ) + else: + verbose_print( + "\tM&S: calc_ecc_integral: Found cached value for min_ecc: {} max ecc: {} integrals_string: {} interpolator_name: {} mass_string: {} period_string: {} mass: {} period: {} I: {}".format( + min_ecc, max_ecc, integrals_string, interpolator_name, mass_string, period_string, options[mass_string], options[period_string], Moecache[integrals_string][mass_period_string] + ), + verbosity, + _MS_VERBOSITY_LEVEL, + ) + + def calc_P_integral( options, min_P, integrals_string, interpolator_name, mass_string, verbosity=0 ): @@ -1592,32 +1676,58 @@ def calc_P_integral( global Moecache max_logP = 10 - I = 0 - dlogP = 1e-3 + # Check if the dict exists + if not Moecache.get(integrals_string, None): + Moecache[integrals_string] = {} - for logP in np.arange(np.log10(min_P), max_logP, dlogP): - # Loop over all the values in the table, between the min and max P - dp_dlogP = Moecache[interpolator_name].interpolate( - [np.log10(options[mass_string]), logP] - )[0] + # Check for cached value. If it doesn't exist: calculate + if not Moecache[integrals_string].get(options[mass_string], None): + I = 0 + dlogP = 1e-3 - I += dp_dlogP * dlogP - Moecache[integrals_string][options[mass_string]] = I - verbose_print( - "\tM&S: calc_P_integral: min_P: {} integrals_string: {} interpolator_name: {} mass_string: {} I: {}".format( - min_P, integrals_string, interpolator_name, mass_string, I - ), - verbosity, - _MS_VERBOSITY_LEVEL, - ) + for logP in np.arange(np.log10(min_P), max_logP, dlogP): + # Loop over all the values in the table, between the min and max P + dp_dlogP = Moecache[interpolator_name].interpolate( + [np.log10(options[mass_string]), logP] + )[0] + I += dp_dlogP * dlogP + + # Set the integral value in the dict + Moecache[integrals_string][options[mass_string]] = I + verbose_print( + "\tM&S: calc_P_integral: min_P: {} integrals_string: {} interpolator_name: {} mass_string: {} mass: {} I: {}".format( + min_P, integrals_string, interpolator_name, mass_string, options[mass_string], I + ), + verbosity, + _MS_VERBOSITY_LEVEL, + ) + else: + verbose_print( + "\tM&S: calc_P_integral: Found cached value for min_P: {} integrals_string: {} interpolator_name: {} mass_string: {} mass: {} I: {}".format( + min_P, integrals_string, interpolator_name, mass_string, options[mass_string], Moecache[integrals_string][options[mass_string]] + ), + verbosity, + _MS_VERBOSITY_LEVEL, + ) + +def calc_total_probdens(prob_dict): + """ + Function to calculate the total probability density + """ + + total_probdens = 1 + for key in prob_dict: + total_probdens *= prob_dict[key] + prob_dict['total_probdens'] = total_probdens + + return prob_dict def Moe_de_Stefano_2017_pdf(options, verbosity=0): """ Moe & distefano function to calculate the probability density. - - takes a dctionary as input (in options) with options: + takes a dictionary as input (in options) with options: M1, M2, M3, M4 => masses (Msun) [M1 required, rest optional] P, P2, P3 => periods (days) [number: none=binary, 2=triple, 3=quadruple] @@ -1635,18 +1745,18 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): _MS_VERBOSITY_LEVEL, ) - prob = [] # Value that we will return prob_dict = {} # Dictionary containing all the pdf values for the different parameters # Get the multiplicity from the options, and if its not there, calculate it based on the # TODO: the function below makes no sense. We NEED to pass the multiplicity in the if not options.get("multiplicity", None): + msg = "\tM&S: Moe_de_Stefano_2017_pdf: Did not find a multiplicity value in the options dictionary" verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Did not find a multiplicity value in the options dictionary", + msg, verbosity, _MS_VERBOSITY_LEVEL, ) - raise ValueError + raise ValueError(msg) # multiplicity = 1 # for n in range(2, 5): # multiplicity += 1 if options.get("M{}".format(n), None) else 0 @@ -1665,17 +1775,16 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): ############################################################ # multiplicity fraction # Calculate the probabilty, or rather, fraction, of stars that belong to this mass - prob.append( - Moe_de_Stefano_2017_multiplicity_fractions(options, verbosity)[multiplicity - 1] - ) + + multiplicity_probability = Moe_de_Stefano_2017_multiplicity_fractions(options, verbosity)[multiplicity - 1] + prob_dict['multiplicity'] = multiplicity_probability verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Appended multiplicity (mass1 = {}) probability ({}) to the prob list ({})".format( - options["M1"], prob[-1], prob + "\tM&S: Moe_de_Stefano_2017_pdf: Appended multiplicity (mass1 = {}) probability ({}) to the prob dict ({})".format( + options["M1"], prob_dict['multiplicity'], prob_dict ), verbosity, _MS_VERBOSITY_LEVEL, ) - prob_dict['multiplicity'] = prob[-1] ############################################################ # always require an IMF for the primary star @@ -1685,15 +1794,18 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): # TODO: Create an n-part-powerlaw method that can have breakpoints and slopes. I'm using a three-part powerlaw now. # TODO: is this actually the correct way? putting the M1 in there? Do we sample in logspace? - prob.append(Kroupa2001(options["M1"]) * options["M1"]) + M1_probability = Kroupa2001(options["M1"]) * options["M1"] + prob_dict['M1'] = M1_probability verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Appended Mass (m={}) probability ({}) to the prob list ({})".format( - options["M1"], prob[-1], prob + "\tM&S: Moe_de_Stefano_2017_pdf: Appended Mass (m={}) probability ({}) to the prob dict ({})".format( + options["M1"], prob_dict['M1'], prob_dict ), verbosity, _MS_VERBOSITY_LEVEL, ) - prob_dict['M1'] = prob[-1] + if M1_probability == 0: # If the probability is 0 then we don't have to calculate more + calc_total_probdens(prob_dict) + return prob_dict """ From here we go through the multiplicities. @@ -1714,137 +1826,82 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): ) verbose_print( "\tM&S: Moe_de_Stefano_2017_pdf: Created new period interpolator: {}".format( - Moecache["rinterpolator_q"] - ), - verbosity, - _MS_VERBOSITY_LEVEL, - ) - - # Make a table storing Moe's data for q distributions - if not Moecache.get("rinterpolator_q", None): - Moecache["rinterpolator_q"] = py_rinterpolate.Rinterpolate( - table=Moecache["q_distributions"], # Contains the table of data - nparams=3, # log10M, log10P, q - ndata=1, # - verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) - verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Created new q interpolator: {}".format( - Moecache["rinterpolator_q"] + Moecache["rinterpolator_log10P"] ), verbosity, _MS_VERBOSITY_LEVEL, ) # Make a table storing Moe's data for q distributions - if not Moecache.get("rinterpolator_e", None): - Moecache["rinterpolator_e"] = py_rinterpolate.Rinterpolate( - table=Moecache["ecc_distributions"], # Contains the table of data - nparams=3, # log10M, log10P, e - ndata=1, # - verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) - verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Created new e interpolator: {}".format( - Moecache["rinterpolator_e"] - ), - verbosity, - _MS_VERBOSITY_LEVEL, - ) - - if not Moecache.get("rinterpolator_log10P2", None): - Moecache["rinterpolator_log10P2"] = py_rinterpolate.Rinterpolate( - table=Moecache[ - "period_distributions" - ], # Contains the table of data - nparams=2, # log10(M1+M2), log10P # TODO: Really? Is the first column of that table M1+M2? - ndata=2, # binary, triple - verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) - - if not Moecache.get("rinterpolator_q", None): - Moecache["rinterpolator_q"] = py_rinterpolate.Rinterpolate( - table=Moecache["q_distributions"], # Contains the table of data - nparams=3, # log10(M1+M2), log10P2, q - ndata=1, # - verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) - verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Created new q interpolator: {}".format( - Moecache["rinterpolator_q"] - ), - verbosity, - _MS_VERBOSITY_LEVEL, - ) - - if not Moecache.get("rinterpolator_log10P2", None): - Moecache[ - "rinterpolator_log10P2" - ] = py_rinterpolate.Rinterpolate( - table=Moecache[ - "period_distributions" - ], # Contains the table of data - nparams=2, # log10(M1+M2), log10P3 - ndata=2, # binary, triple - verbosity=verbosity - - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) + + if options.get("M2", None) or options.get("M3", None) or options.get("M4", None): + if not Moecache.get("rinterpolator_q", None): + Moecache["rinterpolator_q"] = py_rinterpolate.Rinterpolate( + table=Moecache["q_distributions"], # Contains the table of data + nparams=3, # log10M, log10P, q + ndata=1, # + verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), + ) + verbose_print( + "\tM&S: Moe_de_Stefano_2017_pdf: Created new q interpolator: {}".format( + Moecache["rinterpolator_q"] + ), + verbosity, + _MS_VERBOSITY_LEVEL, + ) - if not Moecache.get("rinterpolator_q", None): - Moecache["rinterpolator_q"] = py_rinterpolate.Rinterpolate( - table=Moecache[ - "q_distributions" - ], # Contains the table of data - nparams=3, # log10(M1+M2), log10P2, q - ndata=1, # - verbosity=verbosity - - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) + # Make a table storing Moe's data for q distributions, but only if the ecc is actually sampled + if "ecc" in options: + if not options['ecc']==None: + if not Moecache.get("rinterpolator_e", None): + Moecache["rinterpolator_e"] = py_rinterpolate.Rinterpolate( + table=Moecache["ecc_distributions"], # Contains the table of data + nparams=3, # log10M, log10P, e + ndata=1, # + verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), + ) + verbose_print( + "\tM&S: Moe_de_Stefano_2017_pdf: Created new e interpolator: {}".format( + Moecache["rinterpolator_e"] + ), + verbosity, + _MS_VERBOSITY_LEVEL, + ) ############### # Calculation for period of the binary - # Separation of the inner binary - options["sep"] = calc_sep_from_period( - options["M1"], options["M2"], options["P"] - ) - # TODO: add check for min_P with instant RLOF? - - # Total mass inner binary: - options["M1+M2"] = options["M1"] + options["M2"] - - # Set up the interpolator for the periods - if not Moecache.get("rinterpolator_log10P", None): - Moecache["rinterpolator_log10P"] = py_rinterpolate.Rinterpolate( - table=Moecache["period_distributions"], # Contains the table of data - nparams=2, # log10M, log10P - ndata=2, # binary, triple - verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) - - if not Moecache.get("P_integrals", None): - Moecache["P_integrals"] = {} - - # Calculate normalisation contant for P1 - if not Moecache["P_integrals"].get(options["M1"], None): - calc_P_integral( - options, 1, "P_integrals", "rinterpolator_log10P", "M1", verbosity + if options.get("M2", None): + # Separation of the inner binary + options["sep"] = calc_sep_from_period( + options["M1"], options["M2"], options["P"] ) + # TODO: add check for min_P with instant RLOF? + # TODO: Actually use the value above. + # Total mass inner binary: + options["M1+M2"] = options["M1"] + options["M2"] + + # Calculate P integral or use cached value + calc_P_integral( + options, 1, "P_integrals", "rinterpolator_log10P", "M1", verbosity + ) # Set probabilty for P1 p_val = Moecache["rinterpolator_log10P"].interpolate( [np.log10(options["M1"]), np.log10(options["P"])] )[0] p_val = p_val / Moecache["P_integrals"][options["M1"]] - prob.append(p_val) + prob_dict['P'] = p_val verbose_print( "\tM&S: Moe_de_Stefano_2017_pdf: Appended period (m={}, P={}) probability ({}) to the prob list ({})".format( - options["M1"], options["P"], prob[-1], prob + options["M1"], options["P"], prob_dict['P'], prob_dict ), verbosity, _MS_VERBOSITY_LEVEL, ) - prob_dict['P'] = prob[-1] + if prob_dict['P'] == 0: # If the probability is 0 then we don't have to calculate more + calc_total_probdens(prob_dict) + return prob_dict ############################################################ # mass ratio (0 < q = M2/M1 < qmax) @@ -1852,75 +1909,72 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): # we need to construct the q table for the given M1 # subject to qmin = Mmin/M1 - # Build the table for q - primary_mass = options["M1"] - secondary_mass = options["M2"] - m_label = "M1" - p_label = "P" + if options.get("M2", None): + # Build the table for q + primary_mass = options["M1"] + secondary_mass = options["M2"] + m_label = "M1" + p_label = "P" - # Make a table storing Moe's data for q distributions - if not Moecache.get("rinterpolator_q", None): - Moecache["rinterpolator_q"] = py_rinterpolate.Rinterpolate( - table=Moecache["q_distributions"], # Contains the table of data - nparams=3, # log10M, log10P, q - ndata=1, # - verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) + # Construct the q table + build_q_table(options, m_label, p_label, verbosity=verbosity) verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Created new q interpolator: {}".format( - Moecache["rinterpolator_q"] - ), + "\tM&S: Moe_de_Stefano_2017_pdf: Created q_table ({}) for m={} p={}".format(Moecache[ + "rinterpolator_q_given_{}_log10{}".format(m_label, p_label) + ], options[m_label], options[p_label]), verbosity, _MS_VERBOSITY_LEVEL, ) - # Construct the q table - build_q_table(options, m_label, p_label, verbosity=verbosity) - verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Created q_table ({}) for m={} p={}".format(Moecache[ - "rinterpolator_q_given_{}_log10{}".format(m_label, p_label) - ], options[m_label], options[p_label]), - verbosity, - _MS_VERBOSITY_LEVEL, - ) - - # Add probability for the mass ratio - prob.append( - Moecache[ - "rinterpolator_q_given_{}_log10{}".format(m_label, p_label) - ].interpolate([secondary_mass / primary_mass])[0] - ) - verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: appended mass ratio (M={} P={} q={}) probability ({}) to the prob list ({}) ".format( - options["M1"], - options["P"], - options["M2"] / options["M1"], - prob[-1], - prob, - ), - verbosity, - _MS_VERBOSITY_LEVEL, - ) - - ############################################################ - # Eccentricity - # TODO add eccentricity calculations - - # Make a table storing Moe's data for q distributions - if not Moecache.get("rinterpolator_e", None): - Moecache["rinterpolator_e"] = py_rinterpolate.Rinterpolate( - table=Moecache["ecc_distributions"], # Contains the table of data - nparams=3, # log10M, log10P, e - ndata=1, # - verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) + # Add probability for the mass ratio + q_prob = Moecache[ + "rinterpolator_q_given_{}_log10{}".format(m_label, p_label) + ].interpolate([secondary_mass / primary_mass])[0] + prob_dict['q'] = q_prob verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Created new e interpolator: {}".format( - Moecache["rinterpolator_e"] + "\tM&S: Moe_de_Stefano_2017_pdf: appended mass ratio (M={} P={} q={}) probability ({}) to the prob list ({}) ".format( + options["M1"], + options["P"], + options["M2"] / options["M1"], + prob_dict['q'], + prob_dict, ), verbosity, _MS_VERBOSITY_LEVEL, ) + if prob_dict['q'] == 0: # If the probability is 0 then we don't have to calculate more + calc_total_probdens(prob_dict) + return prob_dict + + ############################################################ + # Eccentricity + # TODO: ask rob if the eccentricity requires an extrapolation as well. + + # Only do this if the eccentricity is sampled + if "ecc" in options: + if not options['ecc']==None: + # Calculate ecc integral or use cached value + calc_e_integral( + options, "ecc_integrals", "rinterpolator_e", "M1", "P", verbosity + ) + mass_period_string = "{}_{}".format(options["M1"], options["P"]) + + # Set probabilty for ecc + ecc_val = Moecache["rinterpolator_e"].interpolate( + [np.log10(options["M1"]), np.log10(options["P"]), options["ecc"]] + )[0] + ecc_val = ecc_val / Moecache["ecc_integrals"][mass_period_string] + prob_dict['ecc'] = ecc_val + verbose_print( + "\tM&S: Moe_de_Stefano_2017_pdf: Appended eccentricity (m={}, P={}, ecc={}) probability ({}) to the prob list ({})".format( + options["M1"], options["P"], options["ecc"], prob_dict['ecc'], prob_dict + ), + verbosity, + _MS_VERBOSITY_LEVEL, + ) + if prob_dict['ecc'] == 0: # If the probability is 0 then we don't have to calculate more + calc_total_probdens(prob_dict) + return prob_dict # Calculations for when multiplicity is bigger than 3 # BEWARE: binary_c does not evolve these systems actually and the code below should be revised for when binary_c actually evolves triples. @@ -1943,7 +1997,7 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): if options["P2"] < min_P2: # period is too short : system is not hierarchical - prob.append(0) + prob_dict['P2'] = 0 verbose_print( "\tM&S: Moe_de_Stefano_2017_pdf: period2 is too short: {} < {}, system is not hierarchichal. Added 0 to probability list".format( options["P1"], min_P2 @@ -1951,7 +2005,10 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): verbosity, _MS_VERBOSITY_LEVEL, ) - # TODO: consider directly returning here + + if prob_dict['P2'] == 0: # If the probability is 0 then we don't have to calculate more + calc_total_probdens(prob_dict) + return prob_dict else: # period is long enough that the system is hierarchical @@ -1961,68 +2018,38 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): options["M3"], options["M1+M2"], options["P2"] ) - # TODO: Consider actually removing these double interpolators. - # Not necessary to have these around, its the same dataset thats loaded - if not Moecache.get("rinterpolator_log10P2", None): - Moecache["rinterpolator_log10P2"] = py_rinterpolate.Rinterpolate( - table=Moecache[ - "period_distributions" - ], # Contains the table of data - nparams=2, # log10(M1+M2), log10P # TODO: Really? Is the first column of that table M1+M2? - ndata=2, # binary, triple - verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) - - # Create the dictionary where we store the area of the P2 integrals - if not Moecache.get("P2_integrals", None): - Moecache["P2_integrals"] = {} - - # Calculate the P2 integrals if they don't exist - if not Moecache["P2_integrals"].get(options["M1+M2"], None): - calc_P_integral( - options, - min_P2, - "P2_integrals", - "rinterpolator_log10P2", - "M1+M2", - verbosity, - ) + # Check for cached value of P integral or calculate + calc_P_integral( + options, + min_P2, + "P2_integrals", + "rinterpolator_log10P", + "M1+M2", + verbosity, + ) # Add the probability p_val = Moecache["rinterpolator_log10P"].interpolate( [np.log10(options["M1+M2"]), np.log10(options["P2"])] )[0] p_val = p_val / Moecache["P2_integrals"][options["M1+M2"]] - prob.append(p_val) + prob_dict['P2'] = p_val verbose_print( "\tM&S: Moe_de_Stefano_2017_pdf: Appended period2 (m1={} m2={}, P2={}) probability ({}) to the prob list ({})".format( - options["M1"], options["M2"], options["P2"], prob[-1], prob + options["M1"], options["M2"], options["P2"], prob_dict['P2'], prob_dict ), verbosity, _MS_VERBOSITY_LEVEL, ) + if prob_dict['P2'] == 0: # If the probability is 0 then we don't have to calculate more + calc_total_probdens(prob_dict) + return prob_dict ############################################################ # mass ratio 2 = q2 = M3 / (M1+M2) # # we need to construct the q table for the given M1 # subject to qmin = Mmin/(M1+M2) - # - # Make a table storing Moe's data for q distributions - if not Moecache.get("rinterpolator_q", None): - Moecache["rinterpolator_q"] = py_rinterpolate.Rinterpolate( - table=Moecache["q_distributions"], # Contains the table of data - nparams=3, # log10(M1+M2), log10P2, q - ndata=1, # - verbosity=verbosity - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) - verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Created new q interpolator: {}".format( - Moecache["rinterpolator_q"] - ), - verbosity, - _MS_VERBOSITY_LEVEL, - ) # Set the variables for the masses and their names primary_mass = options["M1+M2"] @@ -2039,26 +2066,27 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): ) # Add the probability - prob.append( - Moecache[ - "rinterpolator_q_given_{}_log10{}".format(m_label, p_label) - ].interpolate([secondary_mass / primary_mass])[0] - ) + q2_val = Moecache[ + "rinterpolator_q_given_{}_log10{}".format(m_label, p_label) + ].interpolate([secondary_mass / primary_mass])[0] + prob_dict['q2'] = q2_val verbose_print( "\tM&S: Moe_de_Stefano_2017_pdf: appended mass ratio (M1+M2={} M3={} P={} q={}) probability ({}) to the prob list ({}) ".format( options["M1+M2"], options["M3"], options["P"], secondary_mass / primary_mass, - prob[-1], - prob, + prob_dict['q2'], + prob_dict, ), verbosity, _MS_VERBOSITY_LEVEL, ) + if prob_dict['q2'] == 0: # If the probability is 0 then we don't have to calculate more + calc_total_probdens(prob_dict) + return prob_dict # TODO: Implement ecc2 calculation - if multiplicity == 4: # quadruple system. # TODO: Ask Rob about the strructure of the quadruple. Is this only double binary quadrupples? @@ -2076,46 +2104,33 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): options["M1+M2"], options["mmin"], max_sep3 ) - if not Moecache.get("rinterpolator_log10P2", None): - Moecache[ - "rinterpolator_log10P2" - ] = py_rinterpolate.Rinterpolate( - table=Moecache[ - "period_distributions" - ], # Contains the table of data - nparams=2, # log10(M1+M2), log10P3 - ndata=2, # binary, triple - verbosity=verbosity - - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) - - if not Moecache.get("P2_integrals", None): - Moecache["P2_integrals"] = {} - - if not Moecache["P2_integrals"].get(options["M1+M2"], None): - calc_P_integral( - options, - min_P2, - "P2_integrals", - "rinterpolator_log10P2", - "M1+M2", - verbosity, - ) + # Calculate P integral or use the cached value + # TODO: Make sure we use the correct period idea here. + calc_P_integral( + options, + min_P2, + "P2_integrals", + "rinterpolator_log10P", + "M1+M2", + verbosity, + ) - # TODO: should this really be the log10P interpolator? - # + # Set probability p_val = Moecache["rinterpolator_log10P"].interpolate( [np.log10(options["M1+M2"]), np.log10(options["P2"])] )[0] p_val = p_val / Moecache["P2_integrals"][options["M1+M2"]] - prob.append(p_val) + prob_dict['P3'] = p_val verbose_print( "\tM&S: Moe_de_Stefano_2017_pdf: Appended period2 (M=4) (m1={} m2={}, P2={}) probability ({}) to the prob list ({})".format( - options["M1"], options["M2"], options["P2"], prob[-1], prob + options["M1"], options["M2"], options["P2"], prob_dict['P3'], prob_dict ), verbosity, _MS_VERBOSITY_LEVEL, ) + if prob_dict['P3'] == 0: # If the probability is 0 then we don't have to calculate more + calc_total_probdens(prob_dict) + return prob_dict ############################################################ # mass ratio 2 @@ -2123,17 +2138,6 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): # we need to construct the q table for the given M1 # subject to qmin = Mmin/(M1+M2) # Make a table storing Moe's data for q distributions - # TODO: Check if this is correct. - if not Moecache.get("rinterpolator_q", None): - Moecache["rinterpolator_q"] = py_rinterpolate.Rinterpolate( - table=Moecache[ - "q_distributions" - ], # Contains the table of data - nparams=3, # log10(M1+M2), log10P2, q - ndata=1, # - verbosity=verbosity - - (_MS_VERBOSITY_INTERPOLATOR_LEVEL - 1), - ) # Build the table for q2 primary_mass = options["M1+M2"] @@ -2141,6 +2145,7 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): m_label = "M1+M2" p_label = "P2" + # Calculate new q table build_q_table(options, m_label, p_label, verbosity=verbosity) verbose_print( "\tM&S: Moe_de_Stefano_2017_pdf: Created q_table ".format(), @@ -2149,93 +2154,102 @@ def Moe_de_Stefano_2017_pdf(options, verbosity=0): ) # Add the probability - prob.append( - Moecache[ + q3_prob = Moecache[ "rinterpolator_q_given_{}_log10{}".format(m_label, p_label) - ].interpolate([secondary_mass / primary_mass])[0] - ) + ].interpolate([secondary_mass / primary_mass])[0] + prob_dict['q3'] = q3_prob verbose_print( "\tM&S: Moe_de_Stefano_2017_pdf: appended mass ratio (M1+M2={} M3={} P={} q={}) probability ({}) to the prob list ({}) ".format( options["M1+M2"], options["M3"], options["P"], secondary_mass / primary_mass, - prob[-1], - prob, + prob_dict['q3'], + prob_dict, ), verbosity, _MS_VERBOSITY_LEVEL, ) + if prob_dict['q3'] == 0: # If the probability is 0 then we don't have to calculate more + calc_total_probdens(prob_dict) + return prob_dict - # todo ecc 3 + # TODO ecc 3 + # check for input of multiplicity elif multiplicity not in range(1, 5): - verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: Unknown multiplicity {}".format( + msg = "\tM&S: Moe_de_Stefano_2017_pdf: Unknown multiplicity {}".format( multiplicity - ), + ) + verbose_print( + msg, verbosity, _MS_VERBOSITY_LEVEL, ) + raise ValueError(msg) - # the final probability density is the product of all the - # probability density functions - prob_dens = 1 * np.prod(prob) + # Calculate total probdens: + prob_dict = calc_total_probdens(prob_dict) + # Some info if multiplicity == 1: verbose_print( "\tM&S: Moe_de_Stefano_2017_pdf: M1={} q=N/A log10P=N/A ({}): {} -> {}\n".format( - options["M1"], len(prob), str(prob), prob_dens + options["M1"], + len(prob_dict), + str(prob_dict), + prob_dict['total_probdens'] ), verbosity, _MS_VERBOSITY_LEVEL, ) elif multiplicity == 2: verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: M1={} q={} log10P={} ({}): {} -> {}\n".format( + "\tM&S: Moe_de_Stefano_2017_pdf: M1={} q={} log10P={} ecc={} ({}): {} -> {}\n".format( options["M1"], - options["M2"] / options["M1"], + options["M2"] / options["M1"] if options.get("M2", None) else "N/A", np.log10(options["P"]), - len(prob), - str(prob), - prob_dens, + options['ecc'] if options.get("ecc", None) else "N/A", + len(prob_dict), + str(prob_dict), + prob_dict['total_probdens'] ), verbosity, _MS_VERBOSITY_LEVEL, ) elif multiplicity == 3: verbose_print( - "\tM&S: Moe_de_Stefano_2017_pdf: M1={} q={} log10P={} ({}): M3={}} P2={} ecc2={} : {} - {}".format( + "\tM&S: Moe_de_Stefano_2017_pdf: M1={} q={} log10P={} ecc={} M3={} log10P2={} ecc2={} ({}): {} -> {}".format( options["M1"], - options["M2"] / options["M1"], + options["M2"] / options["M1"] if options.get("M2", None) else "N/A", np.log10(options["P"]), - len(prob), + options["ecc"] if options.get("ecc", None) else "N/A", options["M3"], - np.log10("P2"), - np.log10("ecc2"), - str(prob), - prob_dens, + np.log10(options["P2"]), + options["ecc2"] if options.get("ecc2", None) else "N/A", + len(prob_dict), + str(prob_dict), + prob_dict['total_probdens'] ), verbosity, _MS_VERBOSITY_LEVEL, ) elif multiplicity == 4: verbose_print( - "M&S: Moe_de_Stefano_2017_pdf: M1={} q={} log10P={} ({}) : M3={} P2={}} ecc2={} : M4={} P3={} ecc3={} : {} -> {}".format( + "M&S: Moe_de_Stefano_2017_pdf: M1={} q={} log10P={} ecc={} M3={} log10P2={} ecc2={} M4={} log10P3={} ecc3={} ({}) : {} -> {}".format( options["M1"], - options["M2"] / options["M1"], + options["M2"] / options["M1"] if options.get("M2", None) else "N/A", np.log10(options["P"]), - len(prob), + options['ecc'] if options.get("ecc", None) else "N/A", options["M3"], - np.log10("P2"), - np.log10(options["ecc2"]), + np.log10(options["P2"]), + options["ecc2"] if options.get("ecc2", None) else "N/A", options["M4"], - np.log10("P3"), - np.log10(options["ecc3"]), - str(prob), - prob_dens, + np.log10(options["P3"]), + options["ecc3"] if options.get("ecc3", None) else "N/A", + len(prob_dict), str(prob_dict), prob_dict['total_probdens'] ), verbosity, _MS_VERBOSITY_LEVEL, ) - return prob_dens + return prob_dict diff --git a/binarycpython/utils/grid.py b/binarycpython/utils/grid.py index 26c52d937d9a1a3939fefee0475b82ee319d6078..9ae0a1651bf0e3264124acd606032455c0d2cb89 100644 --- a/binarycpython/utils/grid.py +++ b/binarycpython/utils/grid.py @@ -79,6 +79,7 @@ from binarycpython.utils.distribution_functions import ( Moecache, LOG_LN_CONVERTER, fill_data, + Moe_de_Stefano_2017_pdf, ) from binarycpython.utils.spacing_functions import ( @@ -3459,7 +3460,7 @@ class Population: gridtype="centred", dphasevol="dlnm1", precode='M_1 = np.exp(lnm1); options["M1"]=M_1', - probdist="Moe_de_Stefano_2017_pdf({{{}, {}, {}}}, verbosity=self.grid_options['verbosity']) if multiplicity == 1 else 1".format( + probdist="Moe_de_Stefano_2017_pdf({{{}, {}, {}}}, verbosity=self.grid_options['verbosity'])['total_probdens'] if multiplicity == 1 else 1".format( str(options)[1:-1], "'multiplicity': multiplicity", "'M1': M_1" ), ) @@ -3598,7 +3599,7 @@ q2max=maximum_mass_ratio_for_RLOF(M_1+M_2, orbital_period_triple) M_3 = q2 * (M_1 + M_2) sep2 = calc_sep_from_period((M_1+M_2), M_3, orbital_period_triple) eccentricity2=0 - """, +""", spacingfunc="const({}, {}, {})".format( options["ranges"]["q"][0] if options.get("ranges", {}).get("q", None) @@ -3728,7 +3729,7 @@ eccentricity3=0 # the real probability. The trick we use is to strip the options_dict as a string # and add some keys to it: - probdist_addition = "Moe_de_Stefano_2017_pdf({{{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}}, verbosity=self.grid_options['verbosity'])".format( + probdist_addition = "Moe_de_Stefano_2017_pdf({{{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}}, verbosity=self.grid_options['verbosity'])['total_probdens']".format( str(options)[1:-1], '"multiplicity": multiplicity', '"M1": M_1', @@ -3776,20 +3777,111 @@ eccentricity3=0 Moecache[key].destroy() del Moecache[key] - def plot_MS_grid(self, options, mass): + def plot_MS_grid_period_distribution(self, options, mass): """ Function to plot the period distribution along with its probability for a given mass. """ import matplotlib.pyplot as plt - plt.title("Period distribution for M={}".format(mass)) - plt.plot() - plt.show() + default_options = copy.deepcopy(moe_distefano_default_options) + # Take the option dictionary that was given and override. + options = update_dicts(default_options, options) + options['multiplicity'] = 2 sampling_range = const( - options["ranges"]["logP"][0], - options["ranges"]["logP"][1], + options["ranges"]["logP"][0], + options["ranges"]["logP"][1], options["resolutions"]["logP"][0]) + total_prob = 0 + probs = [] + step = sampling_range[1]-sampling_range[0] + for value in sampling_range: + options['M1'] = mass + options['P'] = 10**value + prob = Moe_de_Stefano_2017_pdf(options, verbosity=self.grid_options['verbosity'])['P'] + probs.append(prob) + total_prob += step * prob + + self._clean_interpolators() + + plt.title("Period distribution for M={}. Total prob: {}".format(mass, total_prob)) + plt.plot(sampling_range, probs) + plt.ylabel("Probability") + plt.xlabel("log10(period)") + plt.show() + + def plot_MS_grid_q_distribution(self, options, mass, period): + """ + Function to plot the q distribution along with its probability for a given mass. + """ + + import matplotlib.pyplot as plt + + default_options = copy.deepcopy(moe_distefano_default_options) + + # Take the option dictionary that was given and override. + options = update_dicts(default_options, options) + options['multiplicity'] = 2 + sampling_range = const( + options["ranges"]["q"][0], + options["ranges"]["q"][1], + options["resolutions"]["M"][1]) + + total_prob = 0 + probs = [] + step = sampling_range[1]-sampling_range[0] + print(sampling_range) + options['M1'] = mass + options['P'] = period + for value in sampling_range: + options['M2'] = value * options['M1'] + prob = Moe_de_Stefano_2017_pdf(options, verbosity=self.grid_options['verbosity']).get('q', 0) + probs.append(prob) + total_prob += step * prob + + self._clean_interpolators() + + plt.title("q distribution for M={} P={}. Total prob: {}".format(mass, period, total_prob)) + plt.plot(sampling_range, probs) + plt.ylabel("Probability") + plt.xlabel("mass ratio q") + plt.show() + + def plot_MS_grid_ecc_distribution(self, options, mass, period): + """ + Function to plot the ecc distribution along with its probability for a given mass. + """ + + import matplotlib.pyplot as plt + default_options = copy.deepcopy(moe_distefano_default_options) + + # Take the option dictionary that was given and override. + options = update_dicts(default_options, options) + options['multiplicity'] = 2 + sampling_range = const( + options["ranges"]["ecc"][0], + options["ranges"]["ecc"][1], + options["resolutions"]["ecc"][0]) + + total_prob = 0 + probs = [] + step = sampling_range[1]-sampling_range[0] + print(sampling_range) + options['M1'] = mass + options['P'] = period + for value in sampling_range: + options['ecc'] = value + prob = Moe_de_Stefano_2017_pdf(options, verbosity=self.grid_options['verbosity']).get('ecc', 0) + probs.append(prob) + total_prob += step * prob + + self._clean_interpolators() + + plt.title("ecc distribution for M={} P={}. Total prob: {}".format(mass, period, total_prob)) + plt.plot(sampling_range, probs) + plt.ylabel("Probability") + plt.xlabel("Eccentricity") + plt.show() diff --git a/binarycpython/utils/grid_options_defaults.py b/binarycpython/utils/grid_options_defaults.py index ae1f87736c032741edb209130bf50c5f86ed2851..baf4b613003a167e5a1020d2c134e650c845700f 100644 --- a/binarycpython/utils/grid_options_defaults.py +++ b/binarycpython/utils/grid_options_defaults.py @@ -516,10 +516,11 @@ moe_distefano_default_options = { None, # artificial qmax : set to None to use default ], "logP": [0.0, 8.0], # 0 = log10(1 day) # 8 = log10(10^8 days) - "ecc": [0.0, 1.0], + "ecc": [0.0, 0.99], }, # minimum stellar mass "Mmin": 0.07, + # multiplicity model (as a function of log10M1) # # You can use 'Poisson' which uses the system multiplicty @@ -530,19 +531,20 @@ moe_distefano_default_options = { # from the data, but then triples and quadruples are # combined (and there are NO quadruples). "multiplicity_model": "Poisson", + # multiplicity modulator: # [single, binary, triple, quadruple] # # e.g. [1,0,0,0] for single stars only # [0,1,0,0] for binary stars only # - # defaults to [1,1,1,1] i.e. all types + # defaults to [1,1,0,0] i.e. all types # "multiplicity_modulator": [ 1, # single 1, # binary - 1, # triple - 1, # quadruple + 0, # triple + 0, # quadruple ], # given a mix of multiplities, you can either (noting that # here (S,B,T,Q) = appropriate modulator * model(S,B,T,Q) ) @@ -575,17 +577,7 @@ moe_distefano_default_options = { # This is probably not useful except for # testing purposes or comparing to old grids. "normalize_multiplicities": "merge", - # q extrapolation (below 0.15) method - # none # q extrapolation (below 0.15) method - # none - # flat - # linear2 - # plaw2 - # nolowq - # flat - # linear2 - # plaw2 - # nolowq + # q extrapolation (below 0.15 and above 0.9) method. We can choose from ['flat', 'linear', 'plaw2', 'nolowq'] "q_low_extrapolation_method": "linear", "q_high_extrapolation_method": "linear", }